背景:
在某項目部署測試過程中, k8s中的微服務出現連接集群之外的數據庫服務超時,雖然是偶發性,但出現頻率較高,已對安全產品按期交付構成較大風險,需要盡快解決。
問題分析:
為方便更加清晰的理解問題,首先介紹下服務整體部署架構。在3台VM虛機中部署k8s集群,在k8s集群內部署安全產品的容器服務,而數據庫服務則是部署另外3台VM進行高可用,部署架構圖1如下:
圖1 安全服務整體架構圖標題
控制台中的日志報錯為以下截圖2:
圖2 控制台日志報錯圖標題
起初我也認為這是一個簡單的數據庫連接超時問題,於是首先進行了常規的排查。
1.檢查工程的k8s配置文件中db host的配置問題,沒有問題;
2.檢查網絡狀態,進入容器中對數據庫ip地址進行telnet測試,也是可以正常返回的,沒有問題;
3.檢查數據庫主機是否有對源包進行限制,運維同事反饋並未對安全產品訪問做限制,沒有問題;
4.檢查HikariCP數據庫連接池配置,經過日志排查,發現啟動的時候連接是沒有報錯的,且前幾次連接都沒有問題,超時是出現在幾次正常連接后;
5.檢查是否存在慢查詢和數據庫連接數是否正常,一切正常。
定位問題:
經過多輪的檢查,並嘗試修改數據庫連接配置,發現無論對數據庫配置連接的參數如何修改,雖然數據庫連接日志報錯信息發生了變化,但是尋根究底,其本質原因依舊是超時的問題,如下圖3。
圖3 控制台日志其他異常報錯圖題
由於所有資源池都使用了相同的標准化k8s環境,導致排查問題時直接忽略了k8s本身,各種嘗試未果后我們將目光重新投向了k8s集群本身。
確定是否是k8s集群的問題很簡單,將k8s上部署的服務進行停止,不改變工程及配置,在虛機上采用java -jar的方式將服務啟動,對出問題的接口進行高頻調用測試,沒有出現過一次數據庫連接超時問題。
緊接着通過在數據庫虛機上進行抓包,發現數據庫丟包嚴重。同時發現pod注冊在eureka上是pod的ip,而連接數據庫和redis以及mq的時候,就是通過NAT轉換成node節點的ip了,那么問題極有可能就是出現在k8s的工程連接到數據庫虛機的網絡方式有問題。
解決問題:
經過定位發現是TCP的連接出現了問題。在TCP連接中為了端口快速回收,會對連接進行時間戳的檢查,如果發現后續請求中時間戳小於緩存的時間戳,即視為無效,相應數據包會被丟棄,這樣就造成了大量的丟包。
查看數據庫主機的內核參數,如下圖4,發現net.ipv4.tcp_tw_recycle和net.ipv4.tcp_timestamps參數同時設置為1了,因此將net.ipv4.tcp_tw_recycle設置為0關閉。原來當開啟了tcp_tw_recycle選項后,會拒絕非遞增請求連接。當連接進入TIME_WAIT 狀態后,會記錄對應遠端主機最后到達節點的時間戳。如果同樣的主機有新的節點到達,且時間戳小於之前記錄的時間戳,即視為無效,相應的數據包會被丟棄。關閉這個設置后成功解決問題,再也沒有出現過超時問題。
圖4 虛機內核參數配置圖
在k8s環境中,node節點上的pod網絡互聯互通是采用網絡插件結合etcd實現的。 默認情況下pod訪問集群外部的網絡走的是對應node節點的NAT規則。在這次連接中,由於在pod內連接數據庫經過了一次NAT轉換,客戶端TCP請求到達數據庫,修改目的地址(IP+端口號)后便轉發給數據庫服務器,而客戶端時間戳數據沒有變化。對於數據庫來說,請求的源地址是node節點IP,所以在數據庫看來,原先不同的客戶端請求經過NAT的轉發,會被認為是同一個連接,加上不同客戶端的時間可能不一致,所以就會出現時間戳錯亂的現象。這樣就會導致后面的數據包被大量的丟棄,具體的表現就是客戶端發送的SYN,服務端遲遲無法響應ACK。
原理分析:
問題最終通過修改了內核參數net.ipv4.tcp_tw_recycle解決了,但是問題出現的原因以及問題的解決方案值得我們研究。
其實在文章的一開始的日志截圖中,實際上已經可以看出端倪,對於Linux,字段為TCP_TIMEWAIT_LEN硬編碼為30秒,對於Windows為2分鍾(可自行調整),而我們忽視了這個日志的報錯,把它當成了一個普通的工程連接超時問題看待。想要明白問題出現的原因,首先需要明白TCP連接中TIME-WAIT狀態,如下圖5所示:
圖5 TCP狀態機圖
當TCP連接關閉之前,首先發起關閉的一方會進入TIME-WAIT狀態,另一方可以快速回收連接。可以使用ss –tan來查看TCP連接的當前狀態。
對於TIME-WAIT狀態來說,有兩個作用:
確保遠端(服務端)能夠正確的處於關閉狀態,如圖:
圖6 ACK包異常丟棄圖
當最后一個ACK丟失時,遠程連接進入LAST-ACK狀態,它可以確保遠程已經關閉當前TCP連接。如果沒有TIME-WAIT狀態,當遠程仍認為這個連接是有效的,則會繼續與其通訊,導致這個連接會被重新打開。當遠程收到一個SYN 時,會回復一個RST包,因為這SEQ不對,那么新的連接將無法建立成功,報錯終止。如果遠程因為最后一個ACK包丟失,導致停留在LAST-ACK狀態,將影響新建立具有相同四元組的TCP連接。
2.防止上一次連接中的包,又重新收到,影響新的連接,如圖7。
圖7 正常連接中,出現了SEQ3異常重新收到圖
防止上一個TCP連接的延遲的數據包(發起關閉,但關閉沒完成),被接收后,影響到新的TCP連接。(唯一連接確認方式為四元組:源IP地址、目的IP地址、源端口、目的端口),包的序列號也有一定作用,會減少問題發生的幾率,但無法完全避免。尤其是較大接收windows size的快速(回收)連接。
那大量堆積的TCP TIME_WAIT狀態在服務器上會造成什么影響呢?
占用連接資源
TIME_WAIT占用的1分鍾時間內,相同四元組(源地址,源端口,目標地址,目標端口)的連接無法創建,通常一個ip可以開啟的端口為net.ipv4.ip_local_port_range指定的32768-61000,如果TIME_WAIT狀態過多,會導致無法創建新連接。
2.占用內存資源
保持大量的連接時,當多為每一連接多保留1分鍾,就會多消耗一些服務器的內存。
最后,理解了TIME_WAIT狀態的原理就可以很好的解決這個問題了,最合適的解決方案是增加更多的四元組數目,比如,服務器可用端口,或服務器IP,讓服務器能容納足夠多的TIME-WAIT狀態連接。
在常見的互聯網架構中(NGINX反代跟NGINX,NGINX跟FPM,FPM跟redis、mysql、memcache等),減少TIME-WAIT狀態的TCP連接,最有效的是使用長連接,不要用短連接,尤其是負載均衡跟web服務器之間。在服務端,不要啟用net.ipv4.tcp_tw_recycle,除非你能確保你的服務器網絡環境不是NAT。在服務端上啟用net.ipv4.tw_reuse對於連接進來的TCP連接來說,是沒有任何效果的。
拓展:
關於內核參數的詳細介紹可以參考官方文檔。這邊簡單說明下tcp_tw_recycle參數,在RFC1323中有這么一段描述:
An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender’s timestamp clock. Such an extension is not part of the proposal of this RFC.
其大致意思就是TCP有一種行為,可以緩存每個連接最新的時間戳,后續請求中如果時間戳小於緩存的時間戳,即視為無效,相應的數據包會被丟棄。
1.net.ipv4.tcp_tw_reuse
RFC 1323 實現了TCP拓展規范,以保證網絡繁忙狀態下的高可用。除此之外,另外,它定義了一個新的TCP選項–兩個四字節的timestamp fields時間戳字段,第一個是TCP發送方的當前時鍾時間戳,而第二個是從遠程主機接收到的最新時間戳。啟用net.ipv4.tcp_tw_reuse后,如果新的時間戳,比以前存儲的時間戳更大,那么linux將會從TIME-WAIT狀態的存活連接中,選取一個,重新分配給新的連接出去的TCP連接。
2.net.ipv4. tcp_tw_recycle
這種機制也依賴時間戳選項,這也會影響到所有進來和出去的連接。Linux將會放棄所有來自遠程端的Timestamp時間戳小於上次記錄的時間戳(也是遠程端發來的)的任何數據包。除非TIME-WAIT狀態已經過期。當遠程端主機HOST處於NAT網絡中時,時間戳在一分鍾之內(MSL時間間隔)將禁止了NAT網絡后面,除了這台主機以外的其他任何主機連接,因為他們都有各自CPU CLOCK,各自的時間戳。
在Linux中是否啟用這個行為取決於tcp_timestamps和tcp_tw_recycle,因為tcp_timestamps缺省就是開啟的,所以當tcp_tw_recycle被開啟后,這種行為就被激活了。
簡單來說就是,Linux會丟棄所有來自遠端的timestamp時間戳小於上次記錄的時間戳(由同一個遠端發送的)的任何數據包。也就是說要使用該選項,則必須保證數據包的時間戳是單調遞增的。同時從4.10內核開始,官方修改了時間戳的生成機制,所以導致 tcp_tw_recycle 和新時間戳機制工作在一起不那么友好,同時 tcp_tw_recycle 幫助也不那么的大。此處的時間戳並不是我們通常意義上面的絕對時間,而是一個相對時間。很多情況下,我們是沒法保證時間戳單調遞增的
tcp_tw_recycle 選項在4.10內核之前還只是不適用於NAT/LB的情況(其他情況下,也非常不推薦開啟該選項),但4.10內核后徹底沒有了用武之地,並且在4.12內核中被移除。
總結:
總體來說,這次數據庫連接超時故障本身並沒什么高深之處,不過拔出蘿卜帶出泥,在過程中牽扯的方方面面還是值得我們一起研究學習的,因此分享出這篇文章,也幫助遇到此類問題的小伙伴提供一個解決問題的思考方向。
(本文已在公眾號發布,轉載請注明出處。)
————————————————
版權聲明:本文為CSDN博主「弱水提滄」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_42684642/article/details/105775436
