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

在瞭解 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章