Linux協(xié)議棧accept和syn隊列問題
環(huán)境:
Client 通過tcp 連接server,server端只是listen,但是不調(diào)用accept。通過netstat –ant查看兩端的連接情況。
server端listen,不調(diào)用accept。
client一直去connect server。
問題:
運行一段時間后,為什么server端的ESTABLISHED連接的個數(shù)基本是固定的129個,但是client端的ESTABLISHED連接的個數(shù)卻在不斷增加?
分析
Linux內(nèi)核協(xié)議棧為一個tcp連接管理使用兩個隊列,一個是半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態(tài)的請求),一個是accpetd隊列(用來保存處于established狀態(tài),但是應(yīng)用層沒有調(diào)用accept取走的請求)。
第一個隊列的長度是/proc/sys/net/ipv4/tcp_max_syn_backlog,默認是1024。如果開啟了syncookies,那么基本上沒有限制。
第二個隊列的長度是/proc/sys/net/core/somaxconn,默認是128,表示最多有129個established鏈接等待accept。(為什么是129?詳見下面的附錄1)。
現(xiàn)在假設(shè)acceptd隊列已經(jīng)達到129的情況:
client發(fā)送syn到server。client(SYN_SENT),server(SYN_RECV)
server端處理流程:tcp_v4_do_rcv--->tcp_rcv_state_process--->tcp_v4_conn_request
if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
goto drop;
inet_csk_reqsk_queue_yong(sk)的含義是請求隊列中有多少個握手過程中沒有重傳過的段。
在第一次的時候,之前的握手過程都沒有重傳過,所以這個syn包server端會直接drop掉,之后client會重傳syn,當(dāng)inet_csk_reqsk_queue_yong(sk) < 1,那么這個syn被server端接受。server會回復(fù)synack給client。這樣一來兩邊的狀態(tài)就變?yōu)閏lient(ESTABLISHED), server(SYN_SENT)
Client收到synack后回復(fù)ack給server。
server端處理流程: tcp_check_req--->syn_recv_sock-->tcp_v4_syn_recv_sock
if(sk_acceptq_is_full(sk)
goto exit_overflow;
如果server端設(shè)置了sysctl_tcp_abort_on_overflow,那么server會發(fā)送rst給client,并刪除掉這個鏈接;否則server端只是記錄一下LINUX_MIB_LISTENOVERFLOWS(詳見附錄2),然后返回。默認情況下是不會設(shè)置的,server端只是標(biāo)記連接請求塊的acked標(biāo)志,之后連接建立定時器,會遍歷半連接表,重新發(fā)送synack,重復(fù)上面的過程(具體的函數(shù)是inet_csk_reqsk_queue_prune),如果重傳次數(shù)超過synack重傳的閥值(/proc/sys/net/ipv4/tcp_synack_retries),會把該連接從半連接鏈表中刪除。
一次異常問題分析
Nginx通過FASTCGI協(xié)議連接cgi程序,出現(xiàn)cgi程序read讀取socket內(nèi)容的時候永遠block。通過netstat查看,cgi程序所在的服務(wù)器上顯示連接存在,但是nginx所在的服務(wù)器上顯示不存在該連接。
下面是原始數(shù)據(jù)圖:
我們從上面的數(shù)據(jù)流來分析一下:
出現(xiàn)問題的時候,cgi程序(tcp server端)處理非常慢,導(dǎo)致大量的連接請求放到accept隊列,把accept隊列阻塞。
148021 nginx(tcp client端) 連接cgi程序,發(fā)送syn
此時server端accpet隊列已滿,并且inet_csk_reqsk_queue_yong(sk) > 1,server端直接丟棄該數(shù)據(jù)包
148840 client端等待3秒后,重傳SYN
此時server端狀態(tài)與之前送變化,仍然丟棄該數(shù)據(jù)包
150163 client端又等待6秒后,重傳SYN
此時server端accept隊列仍然是滿的,但是存在了重傳握手的連接請求,server端接受連接請求,并發(fā)送synack給client端(150164)
150166 client端收到synack,標(biāo)記本地連接為ESTABLISHED狀態(tài),給server端應(yīng)答ack,connect系統(tǒng)調(diào)用完成。
Server收到ack后,嘗試將連接放到accept隊列,但是因為accept隊列已滿,所以只是標(biāo)記連接為acked,并不會將連接移動到accept隊列中,也不會為連接分配sendbuf和recvbuf等資源。
150167 client端的應(yīng)用程序,檢測到connect系統(tǒng)調(diào)用完成,開始向該連接發(fā)送數(shù)據(jù)。
Server端收到數(shù)據(jù)包,由于acept隊列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
150225 client端由于沒有收到剛才發(fā)送數(shù)據(jù)的ack,所以會重傳剛才的數(shù)據(jù)包
150296 同上
150496 同上
150920 同上
151112 server端連接建立定時器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,重新發(fā)送synack給client端。
151113 client端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
151896 client端等待3秒后,沒有收到對應(yīng)的ack,認為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
152579 server端連接建立定時器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,synack重傳次數(shù)在閥值以內(nèi),重新發(fā)送synack給client端。
152581 cient端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回
153455 client端等待3秒后,沒有收到對應(yīng)的ack,認為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
155399 server端連接建立定時器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,synack重傳次數(shù)在閥值以內(nèi),重新發(fā)送synack給client端。
155400 cient端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
156468 client端等待幾秒后,沒有收到對應(yīng)的ack,認為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
161309 server端連接建立定時器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,synack重傳次數(shù)在閥值以內(nèi),重新發(fā)送synack給client端。[!--empirenews.page--]
161310 cient端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
162884 client端等待幾秒后,沒有收到對應(yīng)的ack,認為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
Server端收到數(shù)據(jù)包,由于accept隊列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
164828 client端等待一段時間后,認為連接不可用,于是發(fā)送FIN、ACK給server端。Client端的狀態(tài)變?yōu)镕IN_WAIT1,等待一段時間后,client端將看不到該鏈接。
164829 server端收到ACK后,此時cgi程序處理完一個請求,從accept隊列中取走一個連接,此時accept隊列中有了空閑,server端將請求的連接放到accept隊列中。
這樣cgi所在的服務(wù)器上顯示該鏈接是established的,但是nginx(client端)所在的服務(wù)器上已經(jīng)沒有該鏈接了。
之后,當(dāng)cgi程序從accept隊列中取到該連接后,調(diào)用read去讀取sock中的內(nèi)容,但是由于client端早就退出了,所以read就會block那里了。
問題解決
或許你會認為在164829中,server端不應(yīng)該建立連接,這是內(nèi)核的bug。但是內(nèi)核是按照RFC來實現(xiàn)的,在3次握手的過程中,是不會判斷FIN標(biāo)志位的,只會處理SYN、ACK、RST這三種標(biāo)志位。
從應(yīng)用層的角度來考慮解決問題的方法,那就是使用非阻塞的方式read,或者使用select超時方式read;亦或者nginx中關(guān)閉連接的時候使用RST方式,而不是FIN方式。
附錄1
when I use linux TCP socket, and find there is a bug in function sk_acceptq_is_full():
When a new SYN comes, TCP module first checks its validation. If valid,send SYN,ACK to the client and add the sock
to the syn hash table.
Next time if received the valid ACK for SYN,ACK from the client. server will accept this connection and increase the
sk->sk_ack_backlog -- which is done in function tcp_check_req().
We check wether acceptq is full in function tcp_v4_syn_recv_sock().
Consider an example:
After listen(sockfd, 1) system call, sk->sk_max_ack_backlog is set to
As we know, sk->sk_ack_backlog is initialized to 0. Assuming accept() system call is not invoked now
1. 1st connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=0 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
2. 2nd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=1 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
3. 3rd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=2 sk->sk_max_ack_backlog=1, function return 1. Refuse this connection.I think it has bugs. after listen system call. sk->sk_max_ack_backlog=1
but now it can accept 2 connections.