實用TCP協議(2):TCP 引數優化

發表於2022-03-07

在瞭解 TCP 的基本機制後本文繼續介紹 Linux 核心提供的連結佇列、TW_REUSE、SO_REUSEPORT、SYN_COOKIES 等機制以優化生產環境中遇到的效能問題。

連線佇列

Linux 核心會維護兩個佇列:

  • 半連線佇列: syn_backlog, 服務端收到了 SYN 但未回覆的連線, 佇列的大小通過 net.ipv4.tcp_max_syn_backlog 指定
  • 全連線佇列: accept_backlog, 三次握手完成但未呼叫 accept 的連線, 佇列的大小為 min(net.core.somaxconn, backlog), 其中 backlog 是 listen(int sockfd,int backlog) 函式的引數

佇列滿後伺服器會丟棄溢位的連線會導致的情況:

  • 半連線被丟棄後,客戶端 SYN 會超時,客戶端將重新嘗試建立連線
  • 全連線被丟棄後,客戶端認為連線存在,服務端認為不存在。客戶端使用此連線傳送資料包後服務端可以返回 RST (reset) 要求重置連線或者設定定時任務重傳服務端SYN/ACK給客戶端。

全連線佇列溢位時伺服器根據 net.ipv4.tcp_abort_on_overflow 引數決定如何處理:

  • 當 tcp_abort_on_overflow=0,服務端丟棄三次握手的ACK保持在 SYN_RECV 狀態,設定一個定時任務重傳服務端 SYN/ACK 包, 最大重試次數由 tcp_synack_retries 配置決定
  • 當 tcp_abort_on_overflow=1:服務端直接返回RST,要求重置連線

上述引數配置可以通過 sysctl -w 命令進行修改,例如:sysctl -w net.core.somaxconn=32768。機器重啟後使用 sysctl -w 進行的修改會丟失,若需要持久化配置可以在 /etc/sysctl.conf 檔案中增加一行 net.core.somaxconn= 4000 , 然後執行 sysctl -p 使修改生效。

連線佇列溢位會導致無法與伺服器建立新連線或者客戶端出現大量 connection reset by peer 錯誤。

使用netstat -s | grep overflowed 可以檢查是否出現全連線佇列溢位的情況:

# netstat -s | grep overflowed
    11451 times the listen queue of a socket overflowed

上面的輸出表示某個 listen 狀態的 socket 全連線佇列溢位了 11451 次。這個數字是個累計值,可以多執行幾次來判斷溢位次數是否在上升。

使用 netstat -s | grep SYNs | grep dropped 可以檢查是否出現半連線佇列溢位的情況:

# netstat -s | grep SYNs | grep dropped
    32404 SYNs to LISTEN sockets dropped

上面的輸出表示有 32404 次 SYN 被丟棄,這個數字同樣是累計值。

tw_reuse 和 tw_recycle

我們之前提到 time wait 狀態會持續 60s, 過多 TIME_WAIT 狀態的連線會佔用非常有限的 TCP 埠導致無法建立新的連線。

net.ipv4.tcp_max_tw_buckets 引數控制系統中 TIME_WAIT 狀態連線的最大數量。預設值是 NR_FILE*2,並且會根據系統的記憶體容量被調整。

檢測 TIME_WAIT 是否過多

TIME_WAIT 狀態的連線過多會在 dmesg 核心日誌中報錯: kernel: TCP: kernel: TCP: time wait bucket table overflow.

使用 netstat 命令可以檢視各狀態連線數:

#netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
CLOSE_WAIT 36
ESTABLISHED 35
TIME_WAIT 173

awk 命令不好記可以直接 wc 數行數:

# netstat -n | grep 'TIME_WAIT' | wc -l
172

tcp_timestamps

在正式介紹 tw_reuse 和 tw_recycle 之前我們先來介紹它們依賴的 tcp_timestamps 機制。

TCP 最早在 RFC1323 中引入了 timestamp 選項, timestamp 有兩個目的:一是更精確地估算報文往返時間(round-trip-time, RTT) 二是防止陳舊的報文干擾正常的連線。

在引入 timestamps 機制之前,tcp 協議棧通過傳送資料包和收到 ACK 的時間差來計算 RTT,在出現丟包時這一計算方式會出現問題。比如第一次傳送的時間為 t1, 重傳包的時間是 t2, 傳送方在 t3 收到了 ack, 由於不知道這個 ack 包是確認第一個資料包還是確認重傳包我們也無法確定 RTT 是 t3 - t2 還是 t3 - t1。

在設定 net.ipv4.tcp_timestamps=1 之後, 傳送方在傳送資料時將傳送時間 timestamp 放在包裡面, 接收方在收到資料包後返回的ACK包中將收到的timestamp返回給傳送方(echo back),這樣傳送方就可以利用收到 ACK 包的時間和 ACK 包中的echo back timestamp 確定準確的 RTT。

TCP 序列號採用 32 位無符號整數儲存,SEQ 在達到最大值後會從 0 開始再次遞增,這種迴圈被稱為序列號迴繞。由於迴繞現象存在ACK和重傳機制無法通過序列號唯一確定資料包,從而導致錯誤。

上圖中由於迴繞出現了兩個 SEQ=A 的包,接收方把上一次迴圈的 SEQ A 當做了當前的 SEQ A 丟棄了正常的資料包導致資料錯誤。

PAWS (Protection Against Wrapped Sequences,即防止序號迴繞)就是為了避免這個問題而產生的,在開啟 tcp_timestamps 選項情況下,一臺機器發的所有 TCP 包都會帶上傳送時的時間戳,PAWS 要求連線雙方維護最近一次收到的資料包的時間戳(Recent TSval),每收到一個新資料包都會讀取資料包中的時間戳值跟 Recent TSval 值做比較,如果發現收到的資料包中時間戳不是遞增的,則表示該資料包是過期的,就會直接丟棄這個資料包。

tw_reuse

開啟 net.ipv4.tcp_tw_reuse 後客戶端在呼叫 connect() 函式時,核心會隨機找一個 time_wait 狀態超過 1 秒的連線給新的連線複用,所以該選項只適用於連線發起方。

開啟 tw_reuse 之後,tcp 協議棧通過 PAWS 機制來丟棄屬於舊連線的資料包。因此必須開啟 net.ipv4.tcp_timestamps 之後 tw_reuse 才會生效。

tw_recycle

net.ipv4.tw_recycle 同樣利用 timestamp 來丟棄上一個連線的資料包從而不需要在 time_wait 狀態等待太長時間即可關閉連線。

在開啟 tw_recycle 後會自動啟動 per-host paws 機制, 即對「對端 IP 做 PAWS 檢查」,而非對「IP + 埠」四元組做 PAWS 檢查。在開啟 NAT 了網路中, 客戶端 A 和 B 通過同一個 NAT 閘道器與伺服器建立連線。 在伺服器看來他們的 ip 地址相同,若 B 的 timestamp 比 客戶端 A 的 小,那麼由於服務端的 per-host 的 PAWS 機制的作用,服務端就會丟棄客戶端主機 B 發來的 SYN 包。

由於 ipv4 地址緊張目前大多數裝置均通過 NAT 接入網路(比如你的電腦和路由器), 所以在生產環境開啟 tw_recycle極度危險。在 Linux 4.12 版本後,直接取消了 tw_recycle 引數。

SO_REUSEADDR 和 SO_REUSEPORT

在呼叫 bind 後可以使用 setsockopt 函式為 socket 設定 SO_REUSEPORT 或 SO_REUSEADDR 選項。

SO_REUSEADDR

因為服務程式關閉時伺服器主動關閉了連線,程式關閉後有一些 Socket 處於 TIME_WAIT 狀態,導致服務端重啟後無法 bind 並 listen 原埠。

服務端在 bind 時設定 SO_REUSEADDR 則可以忽略 TIME_WAIT 狀態的連線,重啟後直接 bind 成功。SO_REUSEADDR 的作用僅限於讓伺服器重啟後立即 bind 成功, 對效能無改善。

SO_REUSEPORT

SO_REUSEPORT 允許多個程式同時監聽同一個ip:port。SO_REUSEPORT 允許多程式監聽同一個埠避免只有一個 listen 程式成為系統的效能瓶頸,隨著 CPU 核數的增加系統吞吐量會線性增加

主程式建立 socket、bind、 listen 之後,fork 出多個子程式,每個程式都在同一個 socket 上呼叫 accept 等待新連線進入:

這一模型利用了多核CPU的優勢但仍有兩個缺點:

  1. 單一 listener工作程式會成為瓶頸, 隨著核數的擴充套件,效能並沒有隨著提升
  2. 很難做到CPU之間的負載均衡

在 Linux 3.9 引入 SO_REUSEPORT 之後允許多個進(線)程 listen 同一個埠,因此我們可以先 fork 多個程式然後在每個子程式中進行建立 socket、bind、listen、accept。 核心會負責在多個 CPU 之間進行負載均衡, 也解決了單一 listener 稱為系統瓶頸的問題。

syn cookies

我們在前面提到當服務端收到來自客戶端的 SYN 報文之後會向客戶端回覆 SYN + ACK 並將連線放入半連線佇列中。若攻擊者大量傳送 SYN 報文服務端的半連線佇列很快就會佔滿,導致伺服器無法繼續接收連線從而無法正常提供服務。這種攻擊方式稱為 SYN 洪泛(SYN Flood)攻擊, 是一種典型的拒絕服務攻擊方式。

syn cookies 的原理是服務端在握手過程中返回 SYN+ACK 後不分配資源儲存半連線資料,而是根據 SYN 中的資料生成一個 Cookie 值作為自己的起始序列號。在收到客戶端返回的 ACK 後通過其中的序列號判斷 ACK 的合法性。由於建立連線的時候不需要儲存半連線,從而可以有效規避 SYN Flood 攻擊。

TCP連線建立時,雙方的起始報文序號是可以任意的, SYN Cookies 利用這一特性構造初始序列號:

  1. 設t為一個緩慢增長的時間戳(典型實現是每64s遞增一次)
  2. 設m為客戶端傳送的SYN報文中的MSS選項值
  3. 設s是連線的元組資訊(源IP,目的IP,源埠,目的埠)和t經過密碼學運算後的Hash值

則初始序列號n為:

  1. 高 5 位為t mod 32
  2. 接下來3位為m的編碼值
  3. 低 24 位為s

客戶端收到服務端的 SYN+ACK 後會向伺服器返回 ACK, 且報文中ack = n + 1。接下來,伺服器需要對 ack - 1 進行檢查判斷 t 是否超時以及 s 是否被篡改。若報文有效,則從中取出 mss 值建立連線。

SYN Cookies 同樣存在一些缺點:

  • MSS的編碼只有3位,因此最多隻能使用 8 種MSS值
  • 伺服器必須拒絕客戶端SYN報文中的其他只在SYN和SYN+ACK中協商的選項,原因是伺服器沒有地方可以儲存這些選項,比如Wscale和SACK

Linux 的 net.ipv4.tcp_syncookies 配置項可以開啟 syn cookies 功能:

  • 0表示關閉SYN Cookies
  • 1表示在新連線壓力比較大時啟用SYN Cookies
  • 2表示始終使用SYN Cookies

相關文章