想要支持百萬長連接,需要調優哪些參數?
文件描述符限制
- 系統級別限制:操作系統會設置一個全局的文件描述符限制,控制整個系統能同時打開的最大文件數
- 用戶級別限制:每個用戶會有一個文件描述符的限制,控制這個用戶能夠同時打開的最大文件數
- 進程級別限制:每個進程也會有一個文件描述符的限制,控制單個進程能夠同時打開的最大文件數
服務器 TCP 連接數量上限
一個服務端的 TCP 網絡應用,理論上可以支持的最大連接數量是多少?
其中服務端 IP、服務端 Port 已經固定了 (就是監聽的 TCP 程序),所以理論的連接數量上限就取決于 (客戶端 IP * 客戶端 Port) 的組合數量了。
當然如果服務端程序監聽 1 ~ 65535 的所有端口號,理論的連接數量上限就變為:
當然實際情況下肯定達不到這樣的上限數量,原因有三:
- IP 地址中有分類地址 (A, B, C 類)、內網地址、保留地址 (D, E 類),其中后兩者無法用于公網通信
- 某些端口會被保留,僅供專門程序使用,例如 DNS (53), HTTPS (443)
- 服務器內存大小有上限,一個 TCP 套接字會關聯內存緩沖區、文件描述符等資源
綜上所述,一個服務端的 TCP 網絡應用,可以支持的最大連接數量主要取決于其內存大小 (內核參數都已經調優的情況下)。
如何測試?
在測試設備不充足的情況下,如何測試百萬連接數量場景?核心思路:突破 TCP 四元組限制即可。
- 客戶端配置多個 IP, 這樣每個 IP 地址就有大約 64K 個端口號可以使用,向服務端發起連接之前,綁定不同的 IP 地址 即可
- 服務端監聽多個端口號,客戶端只需要連接不同的服務端號口即可
too many open files
首先來看一個高并發場景下的 “經典問題”: too many open files, 產生這個問題的根本原因是: 短時間內打開大量網絡 (文件) 連接,超過了操作系統對單個進程允許打開的文件描述符(file descriptor)數量限制。
想要單機支持 100 萬鏈接,需要調優哪些參數呢?
解決方案
Soft open files 是 Linux 系統參數,影響系統單個進程能夠打開最大的文件句柄數量。
$ ulimit -n
# 默認輸出 1024 或者 65535
1024
表示單個進程同時最多只能維持 1024 個網絡 (例如 TCP) 連接。
可以通過增大該參數,來支持更大的網絡連接數量。
(1) 臨時性調整
只在當前會話 (終端) 中有效,退出或重啟后失效
$ ulimit -HSn 1048576
(2) 永久性設置
修改配置文件 /etc/security/limits.conf:
$ sudo vim /etc/security/limits.conf
# 追加如下內容 (例如支持百萬連接)
# 重啟永久生效
# 單個進程可以打開的最大進程數量
# 表示可以針對不同用戶配置不同的值
# 當然實際情況中,網絡應用一般會獨享整個主機/容器所有資源
# 調整文件描述符限制
# 注意: 實際生效時會以兩者中的較小值為準 (所以最好的方法就是保持兩個值相同)
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
運行 sysctl -p 命令生效,重啟之后仍然有效。
(3) 其他設置
單個進程打開的文件描述符數量 不能超過 操作系統所有進程文件描述符數量 (/proc/sys/fs/file-max), 所以需要修改對應的值:
$ sudo vim /etc/sysctl.conf
# 操作系統所有進程一共可以打開的文件數量
# 增加/修改以下內容
# 注意: 該設置只對非 root 用戶進行限制, root 不受影響
fs.file-max = 16777216
# 進程級別可以打開的文件數量
# 或者可以設置為一個比 soft nofile 和 hard nofile 略大的值
fs.nr_open = 16777216
運行 sysctl -p 命令生效,重啟之后仍然有效。
(4) 查看配置
$ cat /proc/sys/fs/file-nr
# 第一個數表示當前系統使用的文件描述符數
# 第二個數表示分配后已釋放的文件描述符數
# 第三個數等于 file-max
1344 0 1048576
Linux 內核參數調優
想要單機支持 100 萬鏈接,除了剛才的 文件描述符數量 參數調優之外,還需要針對部分內核參數進行調優。
打開系統配置文件 /etc/sysctl.conf,增加 (或修改) 以下配置數據,參數名稱及其作用已經寫在了注釋中。
# 設置系統的 TCP TIME_WAIT 數量,如果超過該值
# 不需要等待 2MSL,直接關閉
net.ipv4.tcp_max_tw_buckets = 1048576
# 將處于 TIME_WAIT 狀態的套接字重用于新的連接
# 如果新連接的時間戳 大于 舊連接的最新時間戳
# 重用該狀態下的現有 TIME_WAIT 連接,這兩個參數主要針對接收方 (服務端)
# 對于發送方 (客戶端) ,這兩個參數沒有任何作用
net.ipv4.tcp_tw_reuse = 1
# 必須配合使用
net.ipv4.tcp_timestamps = 1
# 啟用快速回收 TIME_WAIT 資源
# net.ipv4.tcp_tw_recycle = 1
# 能夠更快地回收 TIME_WAIT 套接字
# 此選項會導致處于 NAT 網絡的客戶端超時,建議設置為 0
# 因為當來自同一公網 IP 地址的不同主機嘗試與服務器建立連接時,服務器會因為時間戳的不匹配而拒絕新的連接
# 這是因為內核會認為這些連接是舊連接的重傳
# 該配置會在 Linux/4.12 被移除
# 在之后的版本中查看/設置會提示 "cannot stat /proc/sys/net/ipv4/tcp_tw_recycle"
# net.ipv4.tcp_tw_recycle = 0
# 縮短 Keepalive 探測失敗后,連接失效之前發送的?;钐綔y包數量
net.ipv4.tcp_keepalive_probes = 3
# 縮短發送 Keepalive 探測包的間隔時間
net.ipv4.tcp_keepalive_intvl = 15
# 縮短最后一次數據包到 Keepalive 探測包的間隔時間
# 減小 TCP 連接?;顣r間
# 決定了 TCP 連接在沒有數據傳輸時,多久發送一次?;钐綔y包,以確保連接的另一端仍然存在
# 默認為 7200 秒
net.ipv4.tcp_keepalive_time = 600
# 控制 TCP 的超時重傳次數,決定了在 TCP 連接丟失或沒有響應的情況下,內核重傳數據包的最大次數
# 如果超過這個次數仍未收到對方的確認包,TCP 連接將被終止
net.ipv4.tcp_retries2 = 10
# 縮短處于 TIME_WAIT 狀態的超時時間
# 決定了在發送 FIN(Finish)包之后,TCP 連接保持在 FIN-WAIT-2 狀態的時間 (對 FIN-WAIT-1 狀態無效)
# 主要作用是在 TCP 連接關閉時,為了等待對方關閉連接而保留資源的時間
# 如果超過這個時間仍未收到 FIN 包,連接將被關閉
# 更快地檢測和釋放無響應的連接,釋放資源
net.ipv4.tcp_fin_timeout = 15
# 調整 TCP 接收和發送窗口的大小,以提高吞吐量
# 三個數值分別是 min,default,max,系統會根據這些設置,自動調整 TCP 接收 / 發送緩沖區的大小
net.ipv4.tcp_mem = 8388608 12582912 16777216
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_wmem = 8192 65535 16777216
# 定義了系統中每一個端口監聽隊列的最大長度
net.core.somaxconn = 65535
# 增加半連接隊列容量
# 除了系統參數外 (net.core.somaxconn, net.ipv4.tcp_max_syn_backlog)
# 程序設置的 backlog 參數也會影響,以三者中的較小值為準
net.ipv4.tcp_max_syn_backlog = 65535
# 全連接隊列已滿后,如何處理新到連接 ?
# 如果設置為 0 (默認情況)
# 客戶端發送的 ACK 報文會被直接丟掉,然后服務端重新發送 SYN+ACK (重傳) 報文
# 如果客戶端設置的連接超時時間比較短,很容易在這里就超時了,返回 connection timeout 錯誤,自然也就沒有下文了
# 如果客戶端設置的連接超時時間比較長,收到服務端的 SYN+ACK (重傳) 報文之后,會認為之前的 ACK 報文丟包了
# 于是再次發送 ACK 報文,也許可以等到服務端全連接隊列有空閑之后,建立連接完成
# 當服務端重試次數到達上限 (tcp_synack_retries) 之后,發送 RST 報文給客戶端
# 默認情況下,tcp_synack_retries 參數等于 5, 而且采用指數退避算法
# 也就是說,5 次的重試時間間隔為 1s, 2s, 4s, 8s, 16s, 總共 31s
# 第 5 次重試發出后還要等 32s 才能知道第 5 次重試也超時了,所以總共需要等待 1s + 2s + 4s+ 8s+ 16s + 32s = 63s
# 如果設置為 1
# 服務端直接發送 RST 報文給客戶端,返回 connection reset by peer
# 設置為 1, 可以避免服務端給客戶端發送 SYN+ACK
# 但是會帶來另外一個問題: 客戶端無法根據 RST 報文判斷出,服務端拒絕的具體原因:
# 因為對應的端口沒有應用程序監聽,還是全隊列滿了
# 除了系統參數外 (net.core.somaxconn)
# 程序設置的 backlog 參數也會影響,以兩者中的較小值為準
# 所以全連接隊列大小 = min(backlog, somaxconn)
net.ipv4.tcp_abort_on_overflow = 1
# 增大每個套接字的緩沖區大小
net.core.optmem_max = 81920
# 增大套接字接收緩沖區大小
net.core.rmem_max = 16777216
# 增大套接字發送緩沖區大小
net.core.wmem_max = 16777216
# 增加網絡接口隊列長度,可以避免在高負載情況下丟包
# 在每個網絡接口接收數據包的速率比內核處理這些包的速率快時,允許送到隊列的數據包的最大數量
net.core.netdev_max_backlog = 65535
# 增加連接追蹤表的大小,可以支持更多的并發連接
# 注意:如果防火墻沒開則會提示 error: "net.netfilter.nf_conntrack_max" is an unknown key,忽略即可
net.netfilter.nf_conntrack_max = 1048576
# 縮短連接追蹤表中處于 TIME_WAIT 狀態連接的超時時間
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
運行 sysctl -p 命令生效,重啟之后仍然有效。
注意事項
如果系統已經使用了參數 net.ipv4.tcp_syncookies, 參數 net.ipv4.tcp_max_syn_backlog 將自動失效。
客戶端參數
當服務器充當 “客戶端角色” 時 (例如代理服務器),連接后端服務器器時,每個連接需要分配一個臨時端口號。
# 查詢系統配置的臨時端口號范圍
$ sysctl net.ipv4.ip_local_port_range
# 增加系統配置的臨時端口號范圍
$ sysctl -w net.ipv4.ip_local_port_range="10000 65535"