作者:鮑鳳其
愛可生 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 機制:
- 首先,有個前提:在特定的時間段內,連線如果沒有任何動作,TCP 保活機制會開始作用。
- 保活機制會每過一個固定時間傳送一個「探測報文」,如果連續幾個探測報文都沒有得到響應,則認為該 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