https://plantegg.github.io/2024/05/05/%E9%95%BF%E8%BF%9E%E6%8E%A5%E9%BB%91%E6%B4%9E%E9%87%8D%E7%8E%B0%E5%92%8C%E5%88%86%E6%9E%90/
長連接黑洞重現和分析
這是一個存在多年,遍及各個不同的業務又反反覆覆地在集團內部出現的一個問題,本文先通過重現展示這個問題,然後從業務、數據庫、OS等不同的角度來分析如何解決它,這個問題值得每一位研發同學重視起來,避免再次踩到
背景
爲了高效率應對故障,本文嘗試回答如下一些問題:
- 爲什麼數據庫crash 重啓恢復後,業務還長時間不能恢復?
- 我依賴的業務做了高可用切換,但是我的業務長時間報錯
- 我依賴的服務下掉了一個節點,爲什麼我的業務長時間報錯
- 客戶做變配,升級雲服務節點規格,爲什麼會導致客戶業務長時間報錯
目的:希望通過這篇文章儘可能地減少故障時長、讓業務快速從故障中恢復
重現
空說無憑,先也通過一次真實的重現來展示這個問題
LVS+MySQL 高可用切換
OS 默認配置參數
1
|
#sysctl -a |grep -E "tcp_retries|keepalive"
|
LVS 對外服務端口是3001, 後面掛的是 3307,假設3307是當前的Master,Slave是 3306,當檢測到3307異常後會從LVS 上摘掉 3307掛上 3306做高可用切換
切換前的 LVS 狀態
1
|
#ipvsadm -L --timeout
|
Sysbench啓動壓力模擬用戶訪問,在 31秒的時候模擬管控檢測到 3307的Master無法訪問,所以管控執行切主把 3306的Slave 提升爲新的 Master,同時到 LVS 摘掉 3307,掛上3306,此時管控端着冰可樂、翹着二郎腿,得意地說,你就看吧我們管控牛逼不、我們的高可用牛逼不,這一套行雲流水3秒鐘不到全搞定
切換命令如下:
1
|
#cat del3307.sh
|
此時Sysbench運行狀態,在第 32秒如期跌0:
1
|
#/usr/local/bin/sysbench --debug=on --mysql-user='root' --mysql-password='123' --mysql-db='test' --mysql-host='127.0.0.1' --mysql-port='3001' --tables='16' --table-size='10000' --range-size='5' --db-ps-mode='disable' --skip-trx='on' --mysql-ignore-errors='all' --time='11080' --report-interval='1' --histogram='on' --threads=1 oltp_read_write run
|
5分鐘後故障報告大批量湧進來,客戶:怎麼回事,我們的業務掛掉10分鐘了,報錯都是訪問MySQL 超時,趕緊給我看看,從監控確實看到10分鐘後客戶業務還沒恢復:
1
|
[ 601s ] thds: 1 tps: 0.00 qps: 0.00 (r/w/o: 0.00/0.00/0.00) lat (ms,95%): 0.00 err/s 0.00 reconn/s: 0.00
|
這時 oncall 都被從被窩裏拎了起來,不知誰說了一句趕緊恢復吧,先試試把應用重啓,5秒鐘後應用重啓完畢,業務恢復,大家開心地笑了,又成功防禦住一次故障升級,還是重啓大法好!
在業務/Sysbench QPS跌0 期間可以看到 3307被摘掉,3306 成功掛上去了,但是沒有新連接建向 3306,業務/Sysbench 使勁薅着 3307
1
|
#ipvsadm -L -n --stats -t 127.0.0.1:3001
|
直到 900多秒後 OS 重試了15次發現都失敗,於是向業務/Sysbench 返回連接異常,觸發業務/Sysbench 釋放異常連接重建新連接,新連接指向了新的 Master 3306,業務恢復正常
1
|
[ 957s ] thds: 1 tps: 0.00 qps: 0.00 (r/w/o: 0.00/0.00/0.00) lat (ms,95%): 0.00 err/s 0.00 reconn/s: 0.00
|
到這裏重現了故障中經常碰到的業務需要900多秒才能慢慢恢復,這個問題也就是 TCP 長連接流量黑洞
如果我們把 net.ipv4.tcp_retries2 改成5 再來做這個實驗,就會發現業務/Sysbench 只需要20秒就能恢復了,也就是這個流量黑洞從900多秒變成了20秒,這回 oncall 不用再被從被窩裏拎出來了吧:
1
|
[ 62s ] thds: 1 tps: 66.00 qps: 1191.00 (r/w/o: 924.00/267.00/0.00) lat (ms,95%): 17.63 err/s 0.00 reconn/s: 0.00
|
LVS + Nginx 上重現
NGINX上重現這個問題:https://asciinema.org/a/649890 3分鐘的錄屏,這個視頻構造了一個LVS 的HA切換過程,LVS後有兩個Nginx,模擬一個Nginx(Master) 斷網後,將第二個Nginx(Slave) 加入到LVS 並將第一個Nginx(Master) 從LVS 摘除,期望業務能立即恢復,但實際上可以看到之前的所有長連接都沒有辦法恢復,進入一個流量黑洞
TCP 長連接流量黑洞原理總結
TCP 長連接在發送包的時候,如果沒收到ack 默認會進行15次重傳(net.ipv4.tcp_retries2=15, 這個不要較真,會根據RTO 時間大致是15次),累加起來大概是924秒,所以我們經常看到業務需要15分鐘左右才恢復。這個問題存在所有TCP長連接中(幾乎沒有業務還在用短連接吧?),問題的本質和 LVS/k8s Service 都沒關係
我這裏重現帶上 LVS 只是爲了場景演示方便
這個問題的本質就是如果Server突然消失(宕機、斷網,來不及發 RST )客戶端如果正在發東西給Server就會遵循TCP 重傳邏輯不斷地TCP retran , 如果一直收不到Server 的ack,大約重傳15次,900秒左右。所以不是因爲有 LVS 導致了這個問題,而是在某些場景下 LVS 有能力處理得更優雅,比如刪除 RealServer的時候 LVS 完全可以感知這個動作並 reset 掉其上所有長連接
爲什麼在K8S 上這個問題更明顯呢,K8S 講究的就是服務不可靠,隨時幹掉POD(切斷網絡),如果幹POD 之前能kill -9(觸發reset)、或者close 業務觸發斷開連接那還好,但是大多時候啥都沒幹,有強摘POD、有直接隔離等等,這些操作都會導致對端只能TCP retran
怎麼解決
業務方
業務方要對自己的請求超時時間有控制和兜底,不能任由一個請求長時間 Hang 在那裏
比如JDBC URL 支持設置 SocketTimeout、ConnectTimeout,我相信其他產品也有類似的參數,業務方要設置這些值,不設置就是如上重現裏演示的900多秒後才恢復
SocketTimeout
只要是連接有機會設置 SocketTimeout 就一定要設置,具體值可以根據你們能接受的慢查詢來設置;分析、AP類的請求可以設置大一點
最重要的:任何業務只要你用到了TCP 長連接一定要配置一個恰當的SocketTimeout,比如 Jedis 是連接池模式,底層超時之後,會銷燬當前連接,下一次重新建連,就會連接到新的切換節點上去並恢復
RFC 5482 TCP_USER_TIMEOUT
RFC 5482 中增加了TCP_USER_TIMEOUT
這個配置,通常用於定製當 TCP 網絡連接中出現數據傳輸問題時,可以等待多長時間前釋放網絡資源,對應Linux 這個 commit
TCP_USER_TIMEOUT
是一個整數值,它指定了當 TCP 連接的數據包在發送後多長時間內未被確認(即沒有收到 ACK),TCP 連接會考慮釋放這個連接。
打個比方,設置 TCP_USER_TIMEOUT
後,應用程序就可以指定說:“如果在 30 秒內我發送的數據沒有得到確認,那我就認定網絡連接出了問題,不再嘗試繼續發送,而是直接斷開連接。”這對於確保連接質量和維護用戶體驗是非常有幫助的。
在 Linux 中,可以使用 setsockopt
函數來設置某個特定 socket 的 TCP_USER_TIMEOUT
值:
1
|
int timeout = 30000; // 30 seconds
|
在這行代碼中,sock
是已經 established 的 TCP socket,我們將該 socket 的 TCP_USER_TIMEOUT
設置爲 30000 毫秒,也就是 30 秒。如果設置成功,這個 TCP 連接在發送數據包後 30 秒內如果沒有收到 ACK 確認,將開始進行 TCP 連接的釋放流程。
TCP_USER_TIMEOUT 相較 SocketTimeout 可以做到更精確(不影響慢查詢),SocketTimeout 超時是不區分ACK 還是請求響應時間的,但是 TCP_USER_TIMEOUT 要求下層的API、OS 都支持。比如 JDK 不支持 TCP_USER_TIMEOUT,但是 Netty 框架自己搞了Native 來實現對 TCP_USER_TIMEOUT 以及其它OS 參數的設置,在這些基礎上Redis 的Java 客戶端 lettuce 依賴了 Netty ,所以也可以設置 TCP_USER_TIMEOUT
原本我是想在Druid 上提個feature 來支持 TCP_USER_TIMEOUT,這樣集團絕大部分業務都可以無感知解決掉這個問題,但查下來發現 JDK 不支持設置這個值,想要在Druid 裏面實現設置 TCP_USER_TIMEOUT 的話,得像 Netty 一樣走Native 繞過JDK 來設置,這對 Druid 而言有點重了
ConnectTimeout
這個值是針對新連接創建超時時間設置,一般設置3-5秒就夠長了
連接池
建議參考這篇 《數據庫連接池配置推薦》 這篇裏的很多建議也適合業務、應用等,你把數據庫看成一個普通服務就好理解了
補充下如果用的是Druid 數據庫連接池不要用它來設置你的 SocketTimeout 參數,因爲他有bug 導致你覺得設置了但實際沒設置上,2024-03-16號的1.2.22這個Release 才fix,所以強烈建議你講 SocketTimeout 寫死在JDBC URL 中簡單明瞭
OS 兜底
假如業務是一個AP查詢/一次慢請求,一次查詢/請求就是需要半個小時,將 SocketTimeout 設置太小影響正常的查詢,那麼可以將如下 OS參數改小,從 OS 層面進行兜底
1
|
net.ipv4.tcp_retries2 = 8
|
keepalive
keepalive 默認 7200秒太長了,建議改成20秒,可以在OS 鏡像層面固化,然後各個業務可以 patch 自己的值;
如果一條連接限制超過 900 秒 LVS就會Reset 這條連接,但是我們將keepalive 設置小於900秒的話,即使業務上一直閒置,因爲有 keepalive 觸發心跳包,讓 LVS 不至於 Reset,這也就避免了當業務取連接使用的時候才發現連接已經不可用被斷開了,往往這個時候業務拋錯誤的時間很和真正 Reset 時間還差了很多,不好排查
在觸發 TCP retransmission 後會停止 keepalive 探測
LVS
作爲一個負責任的雲廠商,肯定要有擔當,不能把所有問題都推給客戶/業務,所以 LVS 也做了升級,當摘除節點的時候支持你設置一個時間,過了這個時間 LVS 就會向這些連接的客戶端發 Reset 幹掉這些流量,讓客戶端觸發新建連接,從故障中快速恢復,這是一個實例維度的參數,建議雲上所有產品都支持起來,管控可以在購買 LVS 的時候設置一個默認值:
https://alidocs.dingtalk.com/i/nodes/Qnp9zOoBVBDEydnQUXgQGAXP81DK0g6l?utm_scene=team_space
管控
如果做高可用切換、刪除節點等,最好是先強制斷掉節點上所有的連接,比如 RDS 管控會遍歷所有的 processlist 然後挨個 kill,怕殺不乾淨會循環多次殺
比如 PolarDB-X 會先kill -9 掉節點進程(會觸發 OS 向原來的連接發 reset),再從 LVS 摘除
當然最好的做法如果用了LVS 就設置 Reset時間,如果沒用LVS 就主動觸發 Reset
其它
神奇的900秒
上面闡述的長連接流量黑洞一般是900+秒就恢復了,有時候我們經常在日誌中看到 CommunicationsException: Communications link failure 900秒之類的錯誤,恰好 LVS 也是設置的 900秒閒置 Reset
1
|
#ipvsadm -L --timeout
|
爲什麼這個問題這幾年才明顯暴露
- 工程師們混沌了幾十年
- 之前因爲出現頻率低重啓業務就糊弄過去了
- 對新連接不存在這個問題
- 有些連接池配置了Check 機制(Check機制一般幾秒鐘超時 fail)
- 微服務多了
- 雲上 LVS 普及了
- k8s service 大行其道
我用的 7層是不是就沒有這個問題了?
幼稚,你4層都掛了7層還能蹦躂,再說一遍只要是 TCP 長連接就有這個問題
極端情況
A 長連接 訪問B 服務,B服務到A網絡不通,假如B發生HA,一般會先Reset/斷開B上所有連接(比如 MySQL 會去kill 所有processlist;比如重啓MySQL——假如這裏的B是MySQL),但是因爲網絡不通這裏的reset、fin網絡包都無法到達A,所以B是無法兜底這個異常場景, A無法感知B不可用了,會使用舊連接大約15分鐘
最可怕的是 B 服務不響應,B所在的OS 還在響應,那麼在A的視角 網絡是正常的,這時只能A自己來通過超時兜底
FIN_WAIT狀態下,重傳一直失敗,連接會怎麼樣?
爲了隔離此問題,我們做了以下實驗:
- 在服務器A起了一個監聽
- 在服務器B去連接監聽
- 在服務器A上drop掉流量
- 在服務器B上調用socket.close()
- 觀察服務器B上的socket的狀態變化情況
其結果是,服務器B上的socket會在FIN_WAIT1卡100s。這個100s又是怎麼來的?繼續Google,發現FIN_WAIT1狀態下,超時時間由另外一個參數指定:tcp_orphan_retries,其值默認爲0,當其值爲0時,內核會將它當作8處理,重傳8次累積時間爲100s。
然而,在CentOS 6.8下,socket會在FIN_WAIT1卡14分鐘,加上ESTABLISH狀態的1分鐘,正好15分鐘,顯然,它是由tcp_retries2控制的。爲了驗證此猜想,我們在CentOS 6.8上將tcp_retries2改成了8,然後重複了實驗,發現客戶端卡100s後就開始發SYN。符合預期。
數據庫
對於數據庫團隊來說要從這幾個方面來控制這個問題的影響:
- 我們自己的管控代碼要設置SocketTimeout 等來做到對超時時間可控、可預期
- 高可用切換、升降配涉及到節點變換,要先斷開/RST 所有老連接
- 如果從 OS 鏡像層面將 tcp_retries2 值設置小一點,對超時進行兜底也是不錯的方案,畢竟總有人沒注意到這個問題
- 用戶業務訪問我們的數據庫產品,可以對客戶做提醒,對一些有問題的第三方客戶端SDK 要做說明
總結
這種問題在 LVS 場景下暴露更明顯了,但是又和LVS 沒啥關係,任何業務長連接都會導致這個 900秒左右的流量黑洞,首先要在業務層面重視這個問題,要不以後數據庫一掛掉還得重啓業務才能從故障中將恢復,所以業務層面處理好了可以避免900秒黑洞和重啓業務,達到快速從故障中恢復
再強調下這個問題如果去掉LVS/k8s Service/軟負載等讓兩個服務直連,然後拔網線也會同樣出現
最佳實踐總結:
- 如果你的業務支持設置 SocketTimeout 那麼請一定要設置,但不一定適合分析類就是需要長時間返回的請求
- 最好的方式是設置 OS 層面的 TCP_USER_TIMEOUT 參數,只要長時間沒有 ack 就報錯返回,但 JDK 不支持直接設置
- 如果用了 ALB/SLB 就一定要配置 connection_drain_timeout 這個參數
- OS 鏡像層面也可以將 tcp_retries2 設置爲5-10次做一個兜底
- 對你的超時時間做到可控、可預期
相關故障和資料
https://aliyuque.antfin.com/apsaradb-doc/clzmlb/ke1rrdsmgu0vhl88?singleDoc# 《PolarDB-X 2.0變配只是秒級閃斷嗎?》
ALB 黑洞問題詳述 公網版本:https://mp.weixin.qq.com/s/BJWD2V_RM2rnU1y7LPB9aw
NAS前端機故障時,nfs客戶端有極大的概率卡住,且恢復時間不定,最壞情況下需要重啓用戶ECS或者宕機的前端機恢復後客戶端才能恢復
數據庫故障引發的“血案” :https://www.cnblogs.com/nullllun/p/15073022.html 這篇描述較細緻,推薦看看
[20240307S4,P4][內]Redis德國Region管控API可用率下跌 管控數據庫高可用切換,管控業務沒設置 SocketTimeout 導致業務長時間不能恢復
Druid 連接池指南 https://aliyuque.antfin.com/coronadb/ydgmzl/fl154gfvw4au00ga
tcp_retries2 的解釋:
1
|
tcp_retries1 - INTEGER
|
tcp_retries2 默認值爲15,根據RTO的值來決定,相當於13-30分鐘(RFC1122規定,必須大於100秒),但是這是很多年前的拍下來古董參數值,現在網絡條件好多了,尤其是內網,個人認爲改成 5-10 是比較恰當 azure 建議:https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection ,Oracle RAC的建議值是3:https://access.redhat.com/solutions/726753