在了解 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的優勢但仍有兩個缺點:
- 單一 listener工作進程會成為瓶頸, 隨着核數的擴展,性能並沒有隨着提升
- 很難做到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 利用這一特性構造初始序列號:
- 設t為一個緩慢增長的時間戳(典型實現是每64s遞增一次)
- 設m為客戶端發送的SYN報文中的MSS選項值
- 設s是連接的元組信息(源IP,目的IP,源端口,目的端口)和t經過密碼學運算后的Hash值
則初始序列號n為:
- 高 5 位為t mod 32
- 接下來3位為m的編碼值
- 低 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