TCP 三次握手原理以及半連線和全連線

FeelTouch發表於2018-10-19

問題描述

JAVA的client和server,使用socket通訊。server使用NIO。 

  1. 間歇性的出現client向server建立連線三次握手已經完成,但server的selector沒有響應到這連線。 
  2. 出問題的時間點,會同時有很多連線出現這個問題。 
  3. selector沒有銷燬重建,一直用的都是一個。 
  4. 程式剛啟動的時候必會出現一些,之後會間歇性出現。

分析問題

正常TCP建連線三次握手過程: 
這裡寫圖片描述 

  • 第一步:client 傳送 syn 到server 發起握手; 
  • 第二步:server 收到 syn後回覆syn+ack給client; 
  • 第三步:client 收到syn+ack後,回覆server一個ack表示收到了server的syn+ack(此時client的56911埠的連線已經是established) 

從問題的描述來看,有點像TCP建連線的時候全連線佇列(accept佇列)滿了,尤其是症狀2、4. 為了證明是這個原因,馬上通過 ss -s 去看佇列的溢位統計資料: 
667399 times the listen queue of a socket overflowed

反覆看了幾次之後發現這個overflowed 一直在增加,那麼可以明確的是server上全連線佇列一定溢位了 
接著檢視溢位後,OS怎麼處理:

cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0

 

tcp_abort_on_overflow 為0表示如果三次握手第三步的時候全連線佇列滿了那麼server扔掉client 發過來的ack(在server端認為連線還沒建立起來) 
為了證明客戶端應用程式碼的異常跟全連線佇列滿有關係,我先把tcp_abort_on_overflow修改成 1,1表示第三步的時候如果全連線佇列滿了,server傳送一個reset包給client,表示廢掉這個握手過程和這個連線(本來在server端這個連線就還沒建立起來)。 
接著測試然後在客戶端異常中可以看到很多connection reset by peer的錯誤,到此證明客戶端錯誤是這個原因導致的。 
於是開發同學翻看java 原始碼發現socket 預設的backlog(這個值控制全連線佇列的大小,後面再詳述)是50,於是改大重新跑,經過12個小時以上的壓測,這個錯誤一次都沒出現過,同時 overflowed 也不再增加了。 
到此問題解決,簡單來說TCP三次握手後有個accept佇列,進到這個佇列才能從Listen變成accept,預設backlog 值是50,很容易就滿了。滿了之後握手第三步的時候server就忽略了client發過來的ack包(隔一段時間server重發握手第二步的syn+ack包給client),如果這個連線一直排不上隊就異常了。

深入理解TCP握手過程中建連線的流程和佇列

這裡寫圖片描述
(圖片來源:http://www.cnxct.com/something-about-phpfpm-s-backlog/) 
如上圖所示,這裡有兩個佇列:syns queue(半連線佇列);accept queue(全連線佇列) 
三次握手中,在第一步server收到client的syn後,把相關資訊放到半連線佇列中,同時回覆syn+ack給client(第二步); 
比如syn floods 攻擊就是針對半連線佇列的,攻擊方不停地建連線,但是建連線的時候只做第一步,第二步中攻擊方收到server的syn+ack後故意扔掉什麼也不做,導致server上這個佇列滿其它正常請求無法進來。

第三步的時候server收到client的ack,如果這時全連線佇列沒滿,那麼從半連線佇列拿出相關資訊放入到全連線佇列中,否則按tcp_abort_on_overflow指示的執行。 
這時如果全連線佇列滿了並且tcp_abort_on_overflow是0的話,server過一段時間再次傳送syn+ack給client(也就是重新走握手的第二步),如果client超時等待比較短,就很容易異常了。 
在我們的os中retry 第二步的預設次數是2(centos預設是5次): 
net.ipv4.tcp_synack_retries = 2

如果TCP連線佇列溢位,有哪些指標可以看呢?

上述解決過程有點繞,那麼下次再出現類似問題有什麼更快更明確的手段來確認這個問題呢?

netstat -s 
[root@server ~]# netstat -s | egrep “listen|LISTEN” 
667399 times the listen queue of a socket overflowed 
667399 SYNs to LISTEN sockets ignored

比如上面看到的 667399 times ,表示全連線佇列溢位的次數,隔幾秒鐘執行下,如果這個數字一直在增加的話肯定全連線佇列偶爾滿了。

ss 命令 
[root@server ~]# ss -lnt 
Recv-Q Send-Q Local Address:Port Peer Address:Port 
0 50 :3306 :*

上面看到的第二列Send-Q 表示第三列的listen埠上的全連線佇列最大為50,第一列Recv-Q為全連線佇列當前使用了多少 
全連線佇列的大小取決於:min(backlog, somaxconn) . backlog是在socket建立的時候傳入的,somaxconn是一個os級別的系統引數 
半連線佇列的大小取決於:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。 不同版本的os會有些差異

實踐驗證下上面的理解

把java中backlog改成10(越小越容易溢位),繼續跑壓力,這個時候client又開始報異常了,然後在server上通過 ss 命令觀察到: 
Fri May 5 13:50:23 CST 2017 
Recv-Q Send-QLocal Address:Port Peer Address:Port 
11 10 :3306 :*

按照前面的理解,這個時候我們能看到3306這個埠上的服務全連線佇列最大是10,但是現在有11個在佇列中和等待進佇列的,肯定有一個連線進不去佇列要overflow掉

容器中的Accept佇列引數

Tomcat預設短連線,backlog(Tomcat裡面的術語是Accept count)Ali-tomcat預設是200, Apache Tomcat預設100. 
ss -lnt 
Recv-Q Send-Q Local Address:Port Peer Address:Port 
0 100 :8080 :*

Nginx預設是511 
$sudo ss -lnt 
State Recv-Q Send-Q Local Address:PortPeer Address:Port 
LISTEN 0 511 :8085 :* 
LISTEN 0 511 :8085 :*

因為Nginx是多程式模式,也就是多個程式都監聽同一個埠以儘量避免上下文切換來提升效能

進一步思考

如果client走完第三步在client看來連線已經建立好了,但是server上的對應連線實際沒有準備好,這個時候如果client發資料給server,server會怎麼處理呢?(有同學說會reset,還是實踐看看) 
先來看一個例子: 
這裡寫圖片描述
(圖片來自:http://blog.chinaunix.net/uid-20662820-id-4154399.html) 
如上圖,150166號包是三次握手中的第三步client傳送ack給server,然後150167號包中client傳送了一個長度為816的包給server,因為在這個時候client認為連線建立成功,但是server上這個連線實際沒有ready,所以server沒有回覆,一段時間後client認為丟包瞭然後重傳這816個位元組的包,一直到超時,client主動發fin包斷開該連線。 
這個問題也叫client fooling,可以看這裡:https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071 (感謝淺奕的提示) 
從上面的實際抓包來看不是reset,而是server忽略這些包,然後client重傳,一定次數後client認為異常,然後斷開連線。

過程中發現的一個奇怪問題

[root@server ~]# date; netstat -s | egrep “listen|LISTEN” 
Fri May 5 15:39:58 CST 2017 
1641685 times the listen queue of a socket overflowed 
1641685 SYNs to LISTEN sockets ignored

[root@server ~]# date; netstat -s | egrep “listen|LISTEN” 
Fri May 5 15:39:59 CST 2017 
1641906 times the listen queue of a socket overflowed 
1641906 SYNs to LISTEN sockets ignored

如上所示: 
overflowed和ignored居然總是一樣多,並且都是同步增加,overflowed表示全連線佇列溢位次數,socket ignored表示半連線佇列溢位次數,沒這麼巧吧。 
翻看核心原始碼(http://elixir.free-electrons.com/linux/v3.18/source/net/ipv4/tcp_ipv4.c): 
這裡寫圖片描述 
可以看到overflow的時候一定會drop++(socket ignored),也就是drop一定大於等於overflow。 
同時我也檢視了另外幾臺server的這兩個值來證明drop一定大於等於overflow: 
server1 
150 SYNs to LISTEN sockets dropped

server2 
193 SYNs to LISTEN sockets dropped

server3 
16329 times the listen queue of a socket overflowed 
16422 SYNs to LISTEN sockets dropped

server4 
20 times the listen queue of a socket overflowed 
51 SYNs to LISTEN sockets dropped

server5 
984932 times the listen queue of a socket overflowed 
988003 SYNs to LISTEN sockets dropped

那麼全連線佇列滿了會影響半連線佇列嗎?

來看三次握手第一步的原始碼(http://elixir.free-electrons.com/linux/v2.6.33/source/net/ipv4/tcp_ipv4.c#L1249): 
這裡寫圖片描述 
TCP三次握手第一步的時候如果全連線佇列滿了會影響第一步drop 半連線的發生。大概流程的如下:

tcp_v4_do_rcv->tcp_rcv_state_process->tcp_v4_conn_request
//如果accept backlog佇列已滿,且未超時的request socket的數量大於1,則丟棄當前請求  
  if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
      goto drop;

 

總結

全連線佇列、半連線佇列溢位這種問題很容易被忽視,但是又很關鍵,特別是對於一些短連線應用(比如Nginx、PHP,當然他們也是支援長連線的)更容易爆發。 一旦溢位,從cpu、執行緒狀態看起來都比較正常,但是壓力上不去,在client看來rt也比較高(rt=網路+排隊+真正服務時間),但是從server日誌記錄的真正服務時間來看rt又很短。 
希望通過本文能夠幫大家理解TCP連線過程中的半連線佇列和全連線佇列的概念、原理和作用,更關鍵的是有哪些指標可以明確看到這些問題。 
另外每個具體問題都是最好學習的機會,光看書理解肯定是不夠深刻的,請珍惜每個具體問題,碰到後能夠把來龍去脈弄清楚。

參考文章

http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html

http://www.cnblogs.com/zengkefu/p/5606696.html

http://www.cnxct.com/something-about-phpfpm-s-backlog/

http://jaseywang.me/2014/07/20/tcp-queue-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%97%AE%E9%A2%98/

http://jin-yang.github.io/blog/network-synack-queue.html#

http://blog.chinaunix.net/uid-20662820-id-4154399.html

相關文章