我一不当心找到了 Tomcat 的 bug!

做者:藤伦柳揶java

来源:https://dwz.cn/xZFW4J8Snginx

背景介绍

为了解决分布式链路追踪的问题,咱们引入了实现OpenTracing的Jaeger来实现。而后咱们为SpringBoot框架写了一个starter以让用户实现近零改造接入全链路。git

因为公司有一个封装了SpringBoot的内部框架,而后咱们的starter就以最新框架所使用的SpringBoot版本为基础进行开发。因此业务系统在接入的时候须要先升级框架,而后再引入咱们的starter才行无缝接入全链路。github

故障描述

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

故障排查

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

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

而后咱们猜测是否是以前压力测试作得不够啊,咱们仍是在压测环境中再压测一下看看会不会复现。bash

而后正好以前这个业务系统作过压测,那就赶忙找运维搭建一个压测环境。结果刚搭建完就很是给面子地复现了400错误。架构

而后运维同窗就各类折腾,而后神奇般地在nginx中的location下加了一行配置后就行了.框架

proxy_set_header HOST $host复制代码

而后就开始各类查这个配置是啥意思。

这个配置的主要是在nginx在转发htp请求的时候会加上实际的Host请求头。

如http请求是 http://abc.com/hello,那么nginx在转发http请求的时候会原封不动的把host请求头(Host:abc.com)转发给后台服务。

对于nginx而言,若是没有配置proxy_set_header HOST $host的时候会默认修改Host为upstream的名称。

而后咱们又在压测环境中试了一下修改以前的版本,发现是正常的。咱们nginx的配置大致以下


那总结一下如今的现象:

  • 在nginx没有配置proxy_set_header HOST $host的时候,修改以前的版本是正常的,修改以后的版本报400错误

  • 在nginx配置了proxy_set_header HOST $host以后,两个版本都是正常的

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

  • 升级SpringBoot的版本

  • 引入全链路starter

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

综上:是由于升级了SpringBoot版本致使了该问题,又由于是http的头部变化致使的问题,故能够大胆猜想是由于升级了Tomcat版本致使的该问题。

tomcat版本从8.5.11升级到8.5.31


故障本地复现

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

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

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

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


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



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



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



故障修复方案

  • 回退tomcat版本。代价较大

  • 线上修改nginx配置:加上配置proxy_set_header HOST $host 或者修改upstream为没有下划线的名称

根因分析

咱们虽然知道了故障的缘由,也知道了怎么修复这个故障。可是就是不知道新版的tomcat为何出现这个问题。

带着这个疑问,咱们组的同事在SpringBoot项目的issue中搜索了下400问题,发现确实有相关的issue

https://github.com/spring-projects/spring-boot/issues/13236

虽然看上去跟咱们的问题是同样的,都是400问题,可是具体发生的缘由是不同的。

这个issue是说,若是domain name .ext 包含数字,好比 "domain.sf1m",会出现400问题。这个问题也已经在tomcat的新版本中修复了。

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

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

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

因为以前没有看过tomcat的源代码,因此要分析出究竟是哪一行代码有问题是很困难的,因此我查看了下tomcat的相关的bug

https://bz.apache.org/bugzilla/show_bug.cgi?id=62371

下面是bug中的错误stack


发现对应的代码改动以下



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

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

对比结果以下:



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


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

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



咱们发如今 2018/4/6增长了对host/port的校验。

根因之根因

那为何tomcat增长了这个Host的校验呢,并且不容许使用带有下划线的Host呢?实际上这个是有规范的。具体点击这个连接

https://www.ietf.org/rfc/rfc1034.txt

经验教训

好了,到这里咱们就知道了,其实对于带有下划线的Host,tomcat是遵循的RFC1-1034的规范的,因此tomcat的处理是正确的。

可是tomcat在处理某些其余合法的Host的时候历史上出现过bug,可是对于下划线的处理一直是正确的。

因此,之后nginx在配置upstream的时候不能使用带有下划线的名称,还有最好在location位置上加上proxy_set_header HOST $host

END


我的公众号:石杉的架构笔记(ID:shishan100)

欢迎长按下图关注公众号:石杉的架构笔记!

公众号后台回复资料,获取做者独家秘制学习资料

石杉的架构笔记,BAT架构经验倾囊相授

相关文章
相关标签/搜索