用了 10 多年的 Tomcat 居然有bug

认真写文章,用心做分享。公众号:Java耕耘者  文章都会在里面更新,整理的资料也会放在里面。

为了解决分布式链路追踪的问题,我们引入了实现OpenTracing的Jaeger来实现。然后我们为SpringBoot框架写了一个starter以让用户实现近零改造接入全链路。 由于公司有一个封装了SpringBoot的内部框架,然后我们的starter就以最新框架所使用的SpringBoot版本为基础进行开发。所以业务系统在接入的时候需要先升级框架,然后再引入我们的starter才行无缝接入全链路。

故障描述

然后有一个业务系统就按照步骤,升级框架,引入starter就接入了全链路系统,并且功能测试压力测试都已经通过了。结果我们满怀信心地就上线了。结果,线上nginx报大量http 400错误。

故障排查

出现故障后,业务系统的研发人员查了所有的日志,包括elk以及机器上的日志,都没有发现明显的错误日志。这个就。。。

几番挣扎后还是没有在线上的日志中找到任何蛛丝马迹。这个就比较绝望了。更奇怪的是在测试环境中是正常的,这个就比较诡异了。

然后我们猜想是不是之前压力测试做得不够啊,我们还是在压测环境中再压测一下看看会不会复现。然后正好之前这个业务系统做过压测,那就赶紧找运维搭建一个压测环境。结果刚搭建完就超级给面子地复现了400错误。

然后运维同学就各种折腾,然后神奇般地在nginx中的location下加了一行配置后就好了.

proxy_set_header HOST $host

然后就开始各种查这个配置是啥意思。

这个配置的主要是在nginx在转发htp请求的时候会加上实际的Host请求头。如http请求是 abc.com/hello,那么ngi… HOST $host的时候会默认修改Host为upstream的名称。

然后我们又在压测环境中试了一下修改之前的版本,发现是正常的。我们nginx的配置大体如下

用了 10 多年的 Tomcat 居然有bug

那总结一下目前的现象:

  • 在nginx没有配置proxysetheader HOST $host的时候,修改之前的版本是正常的,修改之后的版本报400错误
  • 在nginx配置了proxysetheader HOST $host之后,两个版本都是正常的

那我们到底修改了什么呢?

  • 升级SpringBoot的版本
  • 引入全链路starter

然后我们试了下去掉全链路starter的引用,发现还是400错误。然后再回退SpringBoot版本,发现是正常的

综上:是由于升级了SpringBoot版本导致了该问题,又由于是http的头部变化导致的问题,故可以大胆猜测是由于升级了Tomcat版本导致的该问题

tomcat版本从8.5.11升级到8.5.31

故障本地复现

由前面的分析可知,nginx在没有配置proxysetheader HOST $host 的时候,在转发http请求的时候会默认把upstream的名称作为Host头部的内容。

也就是说新版的tomcat在接收Host为sc_java(带有下划线)的http请求报了400错误

下面我们来复现一下这个错误:如下,本地部署两个使用新版本tomcat的后台服务,端口分别为8083和8084

用了 10 多年的 Tomcat 居然有bug

nginx配置如下。重点是upstream是带下划线的

用了 10 多年的 Tomcat 居然有bug

然后使用postman请求nginx,复现400错误

用了 10 多年的 Tomcat 居然有bug

调整nginx配置,主要修改upstream为没有下划线的

用了 10 多年的 Tomcat 居然有bug

然后再请求,发现是正常的

用了 10 多年的 Tomcat 居然有bug

故障修复方案

  • 回退tomcat版本。代价较大
  • 线上修改nginx配置:加上配置proxysetheader HOST $host 或者修改upstream为没有下划线的名称

根因分析

我们虽然知道了故障的缘由,也知道了怎么修复这个故障。但是就是不知道新版的tomcat为什么出现这个问题。带着这个疑问,我们组的同事在SpringBoot项目的issue中搜索了下400问题,发现的确 有相关的issue

[tomcat] Spring boot web always return 400 when use a domain name

虽然看上去跟我们的问题是一样的,都是400问题,但是具体发生的缘由是不一样的。这个issue是说,如果domain name .ext 包含数字,列如 “domain.sf1m”,会出现400问题。这个问题也已经在tomcat的新版本中修复了。

但是即使我使用最新的8.5.x版本的tomcat,用带有下划线的Host的http去请求tomcat的时候依然会报400错误。

也就是说,带有下划线的Host的http请求,tomcat认为是有问题的

那为什么之前版本的tomcat是正常的呢?带着这个疑问我们来分析一下tomcat的源代码。

由于之前没有看过tomcat的源代码,所以要分析出到底是哪一行代码有问题是很困难的,所以我查看了下tomcat的相关的bugImprove logging in
AbstractProcessor.parseHost()

下面是bug中的错误stack

用了 10 多年的 Tomcat 居然有bug

发现对应的代码改动如下

用了 10 多年的 Tomcat 居然有bug

到这里我们也就知道了处理Host头部的类就是这个HttpParser类。

然后我在本次check了下tomcat8.5.31 和8.5.11的代码,比对了一下HttpParser以及AbstractProcessor类。对比结果如下:

用了 10 多年的 Tomcat 居然有bug

发现8.5.31版本的AbstractProcessor类中多了一个parseHost的方法,然后主要解析方法是Host.parse(valueMB);

用了 10 多年的 Tomcat 居然有bug

到这里我们就已经知道了为什么8.5.11版本的tomcat是正常的,主要是由于8.5.11版本的tomcat没有对Host头部进行校验,而在8.5.31版本的tomcat增加了该校验。

我们来看一下tomcat源代码的提交记录

用了 10 多年的 Tomcat 居然有bug

我们发目前 2018/4/6增加了对host/port的校验。

跟因之跟因

那为什么tomcat增加了这个Host的校验呢,而且不允许使用带有下划线的Host呢?实际上这个是有规范的,可以访问下面地址

www.ietf.org/rfc/rfc1034…

经验教训

好了,到这里我们就知道了,实则对于带有下划线的Host,tomcat是遵循的RFC1-1034的规范的,所以tomcat的处理是正确的。但是tomcat在处理某些其他合法的Host的时候历史上出现过bug,但是对于下划线的处理一直是正确的。

所以,后来nginx在配置upstream的时候不能使用带有下划线的名称,还有最好在location位置上加上proxysetheader HOST $host。

© 版权声明

相关文章

暂无评论

none
暂无评论...