為了解決分布式鏈路追蹤的問題,我們引入了實現OpenTracing的Jaeger來實現。然后我們為SpringBoot框架寫了一個starter以讓用戶實現近零改造接入全鏈路。
由于公司有一個封裝了SpringBoot的內部框架,然后我們的starter就以最新框架所使用的SpringBoot版本為基礎進行開發。所以業務系統在接入的時候需要先升級框架,然后再引入我們的starter才行無縫接入全鏈路。
故障描述
然后有一個業務系統就按照步驟,升級框架,引入starter就接入了全鏈路系統,并且功能測試壓力測試都已經通過了。結果我們滿懷信心地就上線了。結果,線上nginx報大量http 400錯誤。
故障排查
出現故障后,業務系統的研發人員查了所有的日志,包括elk以及機器上的日志,都沒有發現明顯的錯誤日志。這個就。。。
幾番掙扎后還是沒有在線上的日志中找到任何蛛絲馬跡。這個就比較絕望了。更奇怪的是在測試環境中是正常的,這個就比較詭異了。
然后我們猜想是不是之前壓力測試做得不夠啊,我們還是在壓測環境中再壓測一下看看會不會復現。然后正好之前這個業務系統做過壓測,那就趕緊找運維搭建一個壓測環境。結果剛搭建完就非常給面子地復現了400錯誤。
然后運維同學就各種折騰,然后神奇般地在nginx中的location下加了一行配置后就好了.
proxy_set_headerHOST$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。