java-Tomcat爬坑之一

作者:藤伦柳揶
www.jianshu.com/p/d50bc43f505e

为了解决分布式链路追踪的问题,我们引入了实现 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 请求是 http://abc.com/hello,那么 nginx 在转发 http 请求的时候会原封不动的把 host 请求头 (Host:abc.com) 转发给后台服务。对于 nginx 而言,如果没有配置 proxysetheader HOST $host 的时候会默认修改 Host 为 upstream 的名称。

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

那总结一下现在的现象:

  • 在 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

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

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

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

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

故障修复方案

  • 回退 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

发现对应的代码改动如下

到这里我们也就知道了处理 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 位置上加上 proxysetheader HOST $host。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值