出現大量TIME_WAIT連接的排查與解決

Last-Modified: 2019年7月10日21:58:43

項目生產環境出現大量TIME_WAIT(數千個), 需要一一排查

先上總結:

  • nginx 未開啓 keep-alive 導致大量主動斷開的tcp連接
  • nginx 與 fastcgi(php-fpm) 的連接默認是短連接, 此時必然出現 TIME_WAIT

狀態確認

統計TIME_WAIT 連接的本地地址

netstat -an | grep TIME_WAIT | awk '{print $4}' | sort | uniq -c | sort -n -k1

#    ... 前面很少的略過
#    2 127.0.0.1:56420
#    442 192.168.1.213:8080
#    453 127.0.0.1:9000

分析:

  • 8080端口是nginx對外端口
  • 9000端口是php-fpm的端口

8080 對外web端口

經過確認, nginx 的配置文件中存在一行

# 不啓用 keep-alive
keepalive_timeout 0;

嘗試抓取 tcp 包

tcpdump tcp -i any -nn port 8080 | grep "我的ip"

# 其中某一次連接的輸出如下
# 20:52:54.647907 IP 客戶端.6470 > 服務端.8080: Flags [S], seq 2369523978, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
# 20:52:54.647912 IP 服務端.8080 > 客戶端.6470: Flags [S.], seq 1109598671, ack 2369523979, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
# 20:52:54.670302 IP 客戶端.6470 > 服務端.8080: Flags [.], ack 1, win 256, length 0
# 20:52:54.680784 IP 客戶端.6470 > 服務端.8080: Flags [P.], seq 1:301, ack 1, win 256, length 300
# 20:52:54.680789 IP 服務端.8080 > 客戶端.6470: Flags [.], ack 301, win 123, length 0
# 20:52:54.702935 IP 服務端.8080 > 客戶端.6470: Flags [P.], seq 1:544, ack 301, win 123, length 543
# 20:52:54.702941 IP 服務端.8080 > 客戶端.6470: Flags [F.], seq 544, ack 301, win 123, length 0
# 20:52:54.726494 IP 客戶端.6470 > 服務端.8080: Flags [.], ack 545, win 254, length 0
# 20:52:54.726499 IP 客戶端.6470 > 服務端.8080: Flags [F.], seq 301, ack 545, win 254, length 0
# 20:52:54.726501 IP 服務端.8080 > 客戶端.6470: Flags [.], ack 302, win 123, length 0
上述具體的ip已經被我批量替換了, 不方便暴露服務器ip

分析:

  • 可以看到4次揮手的開始是由服務端主動發起的(記住TIME_WAIT只會出現在主動斷開連接的一方)
  • 個人理解是, nginx 在配置"不啓用keep-alive"時, 會在http請求結束時主動斷開連接.
  • 嘗試開啓http的keep-alive

修改 nginx 配置

keepalive_timeout 65;

reload nginx

nginx -s reload

再次抓包

tcpdump tcp -i any -nn port 8080 | grep "我的ip"

# 21:09:10.044918 IP 客戶端.8217 > 服務端.8080: Flags [S], seq 1499308169, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
# 21:09:10.044927 IP 服務端.8080 > 客戶端.8217: Flags [S.], seq 2960381462, ack 1499308170, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
# 21:09:10.070694 IP 客戶端.8217 > 服務端.8080: Flags [.], ack 1, win 256, length 0
# 21:09:10.077437 IP 客戶端.8217 > 服務端.8080: Flags [P.], seq 1:302, ack 1, win 256, length 301
# 21:09:10.077443 IP 服務端.8080 > 客戶端.8217: Flags [.], ack 302, win 123, length 0
# 21:09:10.198117 IP 服務端.8080 > 客戶端.8217: Flags [P.], seq 1:671, ack 302, win 123, length 670
# 21:09:10.222957 IP 客戶端.8217 > 服務端.8080: Flags [F.], seq 302, ack 671, win 254, length 0
# 21:09:10.222980 IP 服務端.8080 > 客戶端.8217: Flags [F.], seq 671, ack 303, win 123, length 0
# 21:09:10.247678 IP 客戶端.8217 > 服務端.8080: Flags [.], ack 672, win 254, length 0

注意看上面很有意思的地方:

  • tcp 的揮手只有3次, 而非正常的4次. 個人理解是, 服務端在收到 FIN 時, 已經確認自己不會再發送數據, 因此就將 FIN 與 ACK 一同合併發送
  • 此時是客戶端主動斷開tcp連接, 因此服務端不會出現 TIME_WAIT

再次查看連接狀態

netstat -an | grep TIME_WAIT | awk '{print $4}' | sort | uniq -c | sort -n -k1
#      ...忽略上面
#      1 127.0.0.1:60602
#      1 127.0.0.1:60604
#    344 127.0.0.1:9000

此時發現已經沒有處於 TIME_WAIT 的連接了.

9000 fast-cgi 端口

經過網上查找資料, 整理:

  • nginx 與 fast-cgi 的默認連接是短連接, 每次連接都需要經過一次完整的tcp連接與斷開

當前 nginx 配置

upstream phpserver{
    server 127.0.0.1:9000 weight=1;
}

修改nginx配置使其與fastcgi的連接使用長連接

upstream phpserver{
    server 127.0.0.1:9000 weight=1;
    keepalive 100
}

fastcgi_keep_conn on;

說明:

  • upstream 中的 keepalive 指定nginx每個worker與fastcgi的最大長連接數, 當長連接不夠用時, 此時新建立的連接會在請求結束後斷開(由於此時指定了 HTTP1.1, fastcgi不會主動斷開連接, 因此nginx這邊會出現大量 TIME_WAIT, 需謹慎(未驗證)
  • 由於php-fpm設置了最大進程數爲100, 因此此處的 keepalive 數量指定 100 (未測試)

此處題外話, 如果 nginx 是作爲反向代理, 則需增加如下配置:

# 將http版本由1.0修改爲1.1
proxy_http_version 1.1;
# 清除"Connection"頭部
proxy_set_header Connection "";    
  • 配置 proxy_pass 將請求轉發給後端
  • 這裏, 理解一下 proxy_passfastcgi_pass 區別

    客戶端 --http-->  前端負載均衡Nginx --proxy_pass--> 業務服務器Nginx --fastcgi_pass--> 業務服務器 php-fpm

再次確認 tcp 連接情況

netstat -antp  | grep :9000 | awk '{print $(NF-1)}' | sort | uniq -c
#      6 ESTABLISHED
#      1 LISTEN

ok, 問題解決.

另一種解決方法:

若 nginx 與 fast-cgi 在同一臺服務器上, 則使用 unix域 會更爲高效, 同時避免了 TIME_WAIT 的問題.

題外

經過上面優化後, TIME_WAIT數量從上千個大幅下降到幾十個, 此時發現TIME_WAIT中的存在大量的 127.0.0.1:6379, 6379是redis服務的默認端口....

趕緊改業務代碼去, 將 $redis->connect(...) 改成 $redis->pconnect(...)

說明:

  • pconnect 表示 php-fpm 與 redis 建立 tcp 連接後, 在本次http請求結束後仍維持該連接, 下次新的請求進來時可以複用該連接, 從而複用了tcp連接.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章