這個場景出現在用 Jedis ping 檢測的場景,用完直接 close,服務端穩定出現 Connection reset by peer。
tcpdump 壹下就很容易定位到問題所在,客戶端收到 PONG 響應後直接發了壹個 RST 包給服務端:
查看 Jedis 的源碼發現 socket 有個比較特殊的配置 socket.setSoLinger(true, 0) 。
先看壹下 man7/socket.7 的解釋:
坦白說不是很明白啥意思。。。
最終在 stackoverflow 上找到壹個比較容易理解的解釋:
簡而言之,設置 SO_LINGER(0) 可以不進行四次揮手直接關閉 TCP 連接,在協議交互上就是直接發 RST 包,這樣的好處是可以避免長時間處於 TIME_WAIT 狀態,當然 TIME_WAIT 存在也是有原因的,大部分評論都不建議這樣配置。
這個場景有點兒微妙,首先得理解壹下 tcp 的兩個隊列。
這篇文章講得比較清楚: SYN packet handling in the wild
accept 隊列滿通常是由於 netty boss 線程處理慢,特別是在容器化之後,服務剛啟動的時候很容易出現 CPU 受限。
為了模擬這個現象,我寫了個示例程序 shichaoyuan/netty-backlog-test ,設置 SO_BACKLOG 為 1,並且在 accept 第壹個連接後設置 autoRead 為 false,也就是讓 boss 線程不再繼續 accept 連接。
啟動第壹個 Client,可以正常連接,發送 PING,接收 PONG。
啟動第二個 Client,也可以正常連接,但是沒有收到 PONG:
可見這個連接創建成功了,已經在 Accept Queue 裏了,但是進程沒有 accept,所以沒有與進程綁定。
啟動第三個 Client,也可以正常連接,也沒有收到 PONG:
與第二個連接壹樣。
啟動第四個 Client,也可以正常連接,但是在發送 PING 後出現 Connection reset by peer:
這個連接在服務端並沒有進入 accept queue,處於 SYN_RECV 狀態,並且很快就消失了(因為 accept queue 已經滿了,無法轉入 ESTABLISHED 狀態)。
抓包看壹下:
從客戶端視角來看連接確實是建成功了,有壹個比較特殊的地方在三次握手之後,服務端又向客戶端發送了壹個 [S.],客戶端回復了壹個 [.],這個交互看起來不影響連接。
服務端後來銷毀了連接,而客戶端還認為連接是 ESTABLISHED 的,發送 PING 消息,服務端自然得回復壹個 RST。
PS:我在 Windows 10 的 WSL2 中實驗這種場景是建連接超時,可能不同的操作系統或 linux 版本對這個交互的過程處理不同,在此不進行進壹步測試了。
以上,這個故事告訴我們判斷連接是否可用,建成功之後應該發個心跳包測試壹下。