分散式 | 資料庫連線如何正確處理 TCP 連線三次握手失敗

愛可生雲資料庫發表於2022-05-25

作者:鮑鳳其

愛可生 dble 團隊開發成員,主要負責 dble 需求開發,故障排查和社群問題解答。少說廢話,放碼過來。

本文來源:原創投稿

*愛可生開源社群出品,原創內容未經授權不得隨意使用,轉載請聯絡小編並註明來源。


背景

在穩定性環境中,當 dble 初始化後端連線池後,後端連線池會出現連線計數器(totalConnections)和實際連線(allConnections)數量不符合的情況,理論情況下兩個變數會保持最終一致性。

後續通過查閱網上相關文件,找到了相關文件:https://mp.weixin.qq.com/s/FE... ,詳細分析過程可參考此文章。

簡單來說,在 dble 初始化後端連線池的過程中,瞬時建立的連線數量可能過大,導致部分 TCP 連線握手時觸發了 TCP 的 syn_cookie 機制並且第三次 TCP 握手的 ACK 報文丟失了,從而導致了上述的情況。

後續,在穩定性環境中將 TCP 的 syn_cookie 關閉之後暫時解決了此種情況。

但假設正常 TCP 三次握手出現如下三種異常情況:

  • TCP 第一次握手包 SYN 丟包了
  • TCP 第二次握手包 SYN、ACK 丟包了
  • TCP 第三次握手包 ACK 包丟了

客戶端和服務端是如何處理的,如果重傳,重傳多少次?每次間隔時長多少?

實驗環境

在一臺伺服器上啟動 MySQL 服務,埠是3306,IP地址:10.186.60.69

在一臺伺服器上使用 MySQL client 連線 MySQL 服務,IP地址:10.186.60.60

第一種場景

TCP 第一次握手包 SYN 報文丟包了,會發生什麼?

在 MySQL 伺服器上執行,通過 iptables 阻斷客戶端的傳送過來的所有TCP報文:

$ iptables -i eth0 -A INPUT -p tcp --dport 3306 -j DROP

在 MySQL 伺服器上開始抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_syn_timeout.cap

通過 MySQL client 連線 MySQL 服務端,過了一分多鐘後,客戶端返回報錯,此時停止抓包,如下圖:

$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2003 (HY000): Can't connect to MySQL server on '10.186.60.69' (110)

wireshark 分析抓包檔案:

客戶端在收不到 TCP SYN 報文的 ACK 報文後,會不斷進行重試,示例中會進行六次重試並且每次 RTO 是不同的:

  • 第一次是1秒後重試
  • 第二次是3秒後重試,和第一次相差 2s 左右
  • 第三次是7秒後重試,和第二次相差 4s 左右
  • 第四次是15秒後重試,和第三次相差 8s 左右
  • 第五次是31秒後重試,和第四次相差 16s 左右
  • 第六次是65秒後重試,和第五次相差 32s 左右

每次超時時間 RTO 是指數上漲的。另外,這裡的重試次數可以配置,由客戶端機器的如下核心引數指定:

$ cat /proc/sys/net/ipv4/tcp_syn_retries
6 
 
# 不同的發行版本,引數可能不同
$ uname -a
Linux ubuntu 4.15.0-36-generic #39-Ubuntu SMP Mon Sep 24 16:19:09 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

可以嘗試修改此引數看看效果:

$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries

因此:

TCP 第一次握手包 SYN 報文丟包了,會發生什麼?

客戶端會重傳 SYN 報文,直到收到 ACK 或者達到最大次數,每次重試的時間是翻倍上漲的。

第二種場景

TCP 第二次握手的 SYN + ACK 報文丟包了,會發生什麼?

為了模擬 SYN + ACK 的丟包情形,在客戶端設定防火牆,將MySQL服務端的報文全部攔截:

$ iptables -A INPUT -p tcp -s 10.186.60.69 -j DROP

在 MySQL 伺服器端抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_syn_ack_timeout.cap

在 wireshark Statistics下面 flow graph 功能分析 tcp 流:

從上圖來看,可以分為兩個視角:

客戶端視角:客戶端發出 SYN 報文之後,由於設定了防火牆,沒有收到 SYN + ACK 報文,因此,客戶端會不斷進行重試,直到收到 SYN + ACK 或者達到最大重試次數

伺服器視角:伺服器端在收到 SYN 報文之後,傳送SYN + ACK 報文,但是收不到最後一次握手的 ACK 報文,因此伺服器端會不斷進行重試傳送SYN + ACK 報文。

客戶端超時重傳的 SYN 包抵達了服務端後,服務端然後回了 SYN、ACK 包,但是 SYN、ACK 包的重傳定時器並沒有被重置,仍然持續在重傳。

從圖中可以看出,服務端在收到第三次的 SYN 報文併發出的 SYN + ACK 報文之後,後面重試了四次,將之前重試的一次也算在內。

這個重試次數也由核心引數控制:

$ cat /proc/sys/net/ipv4/tcp_synack_retries
5

將客戶端核心引數 tcp_synack_retries 設定成 1 之後,TCP 互動圖:

這樣重試次數就更加明顯了。

第三種場景

TCP 第三次握手的 ACK 丟包了

在 MySQL 伺服器端設定防火牆,攔截 TCP 第三次握手的 ACK 報文:

$ iptables -A INPUT -p tcp --tcp-flag ack ack --dport 3306 -j DROP

在 MySQL 伺服器端抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_3th_ack_timeout.cap

分析抓包檔案:

從上圖來看,可以分為兩個視角:

客戶端視角:對於客戶端來說,其實連線已經建立好了

通過 netstat 命令檢視連線的狀態:

$ netstat -napt|grep 3306
tcp 0 0 10.186.60.60:42490 10.186.60.69:3306 ESTABLISHED 14391/mysql

此時連線的狀態是 ESTABLISHED 狀態。

伺服器視角:由於沒有收到第三次的 ACK 報文,和第二種場景類似,伺服器會一直重新傳送 SYN + ACK 報文,直到達到最大次數

在重試期間,服務端連線的狀態一直處於 SYN_RECV 狀態:

$ netstat -napt|grep 3306
tcp        0      0 10.186.60.69:3306       10.186.60.60:42868      SYN_RECV    -

過了半分鐘左右,也就是伺服器端達到重試次數之後,服務端剛才處於 SYN_RECV 的狀態的 TCP 連線不見了。

可是此時客戶端的連線卻依然存在。

客戶端的連線之後怎麼處理?

此時分場景討論:

一種場景是,客戶端在 TCP 連線建立完成之後,直接傳送資料。

另一個種場景是,客戶端沒有任何操作。下面對這兩種情況進行討論。

客戶端傳送資料

為了模擬這種場景,我們預先通過客戶端連線 MySQL 伺服器。

連線上之後在 MySQL 伺服器端通過防火牆隔離客戶端的報文:

$ iptables -A INPUT -p tcp -s 10.186.60.60 -j DROP

在 MySQL 服務端進行抓包:

$ tcpdump -i eth0 tcp and port 3306 -w tcp_data.cap

之後通過剛才建立的連線,下發 use test ; 語句。

下面分析一下抓包檔案:

分析:

我們發現客戶端一共重傳了十一次。

TCP 建立連線後的資料包傳輸,最大超時重傳次數是由 tcp_retries2 指定,預設值是 15 次,這裡為了便於觀測,將數值調整成了 10 次,如下:

$ cat /proc/sys/net/ipv4/tcp_retries2
10

可是這裡的抓包檔案中傳輸了十一次報文,這裡參考文章:https://perthcharles.github.i...

無任何操作

在 MySQL 的協議中,TCP 建立完成之後,MySQL 服務端會傳送握手包,由於 MySQL 服務端連線已經不在,因此不會下發握手包,客戶端會一直 hang 住。

$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.

此時客戶端連線的存活由 TCP 的保活機制確保。

keep-alive 機制:

  1. 首先,有個前提:在特定的時間段內,連線如果沒有任何動作,TCP 保活機制會開始作用。
  2. 保活機制會每過一個固定時間傳送一個「探測報文」,如果連續幾個探測報文都沒有得到響應,則認為該 TCP 連線已經死亡,系統核心將錯誤資訊通知給上層應用程式。

在 Linux 核心可以有對應的引數可以設定保活時間、保活探測的次數、保活探測的時間間隔,以下都為預設值:

$ sysctl -a|grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
  
# 也可以通過下面的方式來檢視:
$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
$ cat /proc/sys/net/ipv4/tcp_keepalive_time

引數解釋:

  • tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;
  • tcp_keepalive_probes=9:表示檢測 9 次無響應,認為對方是不可達的,從而中斷本次的連線。
  • tcp_keepalive_time=7200:表示保活時間是 7200 秒(2小時),也就 2 小時內如果沒有任何連線相關的活 動,則會啟動保活機制

我們可以修改引數看下效果:

$ echo 10 > /proc/sys/net/ipv4/tcp_keepalive_intvl
$ echo 40 > /proc/sys/net/ipv4/tcp_keepalive_time
$ echo 2 > /proc/sys/net/ipv4/tcp_keepalive_probes

通過抓包檔案檢視:

可以看到在 40s 時候,開始探活包,探測了兩次,每次間隔 10s 中,符合引數修改的定義

修改引數之後,client 過了大約一分鐘後,就報錯了:

$ mysql -h10.186.60.69 -uroot -p123456 -P3306
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 110

相關文章