Linux協(xié)議棧accept和syn隊(duì)列問(wèn)題
環(huán)境:
Client 通過(guò)tcp 連接server,server端只是listen,但是不調(diào)用accept。通過(guò)netstat –ant查看兩端的連接情況。
server端listen,不調(diào)用accept。
client一直去connect server。
問(wèn)題:
運(yùn)行一段時(shí)間后,為什么server端的ESTABLISHED連接的個(gè)數(shù)基本是固定的129個(gè),但是client端的ESTABLISHED連接的個(gè)數(shù)卻在不斷增加?
分析
Linux內(nèi)核協(xié)議棧為一個(gè)tcp連接管理使用兩個(gè)隊(duì)列,一個(gè)是半鏈接隊(duì)列(用來(lái)保存處于SYN_SENT和SYN_RECV狀態(tài)的請(qǐng)求),一個(gè)是accpetd隊(duì)列(用來(lái)保存處于established狀態(tài),但是應(yīng)用層沒(méi)有調(diào)用accept取走的請(qǐng)求)。
第一個(gè)隊(duì)列的長(zhǎng)度是/proc/sys/net/ipv4/tcp_max_syn_backlog,默認(rèn)是1024。如果開(kāi)啟了syncookies,那么基本上沒(méi)有限制。
第二個(gè)隊(duì)列的長(zhǎng)度是/proc/sys/net/core/somaxconn,默認(rèn)是128,表示最多有129個(gè)established鏈接等待accept。(為什么是129?詳見(jiàn)下面的附錄1)。
現(xiàn)在假設(shè)acceptd隊(duì)列已經(jīng)達(dá)到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)的含義是請(qǐng)求隊(duì)列中有多少個(gè)握手過(guò)程中沒(méi)有重傳過(guò)的段。
在第一次的時(shí)候,之前的握手過(guò)程都沒(méi)有重傳過(guò),所以這個(gè)syn包server端會(huì)直接drop掉,之后client會(huì)重傳syn,當(dāng)inet_csk_reqsk_queue_yong(sk) < 1,那么這個(gè)syn被server端接受。server會(huì)回復(fù)synack給client。這樣一來(lái)兩邊的狀態(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會(huì)發(fā)送rst給client,并刪除掉這個(gè)鏈接;否則server端只是記錄一下LINUX_MIB_LISTENOVERFLOWS(詳見(jiàn)附錄2),然后返回。默認(rèn)情況下是不會(huì)設(shè)置的,server端只是標(biāo)記連接請(qǐng)求塊的acked標(biāo)志,之后連接建立定時(shí)器,會(huì)遍歷半連接表,重新發(fā)送synack,重復(fù)上面的過(guò)程(具體的函數(shù)是inet_csk_reqsk_queue_prune),如果重傳次數(shù)超過(guò)synack重傳的閥值(/proc/sys/net/ipv4/tcp_synack_retries),會(huì)把該連接從半連接鏈表中刪除。
一次異常問(wèn)題分析
Nginx通過(guò)FASTCGI協(xié)議連接cgi程序,出現(xiàn)cgi程序read讀取socket內(nèi)容的時(shí)候永遠(yuǎn)block。通過(guò)netstat查看,cgi程序所在的服務(wù)器上顯示連接存在,但是nginx所在的服務(wù)器上顯示不存在該連接。
下面是原始數(shù)據(jù)圖:
我們從上面的數(shù)據(jù)流來(lái)分析一下:
出現(xiàn)問(wèn)題的時(shí)候,cgi程序(tcp server端)處理非常慢,導(dǎo)致大量的連接請(qǐng)求放到accept隊(duì)列,把a(bǔ)ccept隊(duì)列阻塞。
148021 nginx(tcp client端) 連接cgi程序,發(fā)送syn
此時(shí)server端accpet隊(duì)列已滿,并且inet_csk_reqsk_queue_yong(sk) > 1,server端直接丟棄該數(shù)據(jù)包
148840 client端等待3秒后,重傳SYN
此時(shí)server端狀態(tài)與之前送變化,仍然丟棄該數(shù)據(jù)包
150163 client端又等待6秒后,重傳SYN
此時(shí)server端accept隊(duì)列仍然是滿的,但是存在了重傳握手的連接請(qǐng)求,server端接受連接請(qǐng)求,并發(fā)送synack給client端(150164)
150166 client端收到synack,標(biāo)記本地連接為ESTABLISHED狀態(tài),給server端應(yīng)答ack,connect系統(tǒng)調(diào)用完成。
Server收到ack后,嘗試將連接放到accept隊(duì)列,但是因?yàn)閍ccept隊(duì)列已滿,所以只是標(biāo)記連接為acked,并不會(huì)將連接移動(dòng)到accept隊(duì)列中,也不會(huì)為連接分配sendbuf和recvbuf等資源。
150167 client端的應(yīng)用程序,檢測(cè)到connect系統(tǒng)調(diào)用完成,開(kāi)始向該連接發(fā)送數(shù)據(jù)。
Server端收到數(shù)據(jù)包,由于acept隊(duì)列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
150225 client端由于沒(méi)有收到剛才發(fā)送數(shù)據(jù)的ack,所以會(huì)重傳剛才的數(shù)據(jù)包
150296 同上
150496 同上
150920 同上
151112 server端連接建立定時(shí)器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,重新發(fā)送synack給client端。
151113 client端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個(gè)ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊(duì)列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
151896 client端等待3秒后,沒(méi)有收到對(duì)應(yīng)的ack,認(rèn)為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
152579 server端連接建立定時(shí)器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,synack重傳次數(shù)在閥值以內(nèi),重新發(fā)送synack給client端。
152581 cient端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個(gè)ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊(duì)列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回
153455 client端等待3秒后,沒(méi)有收到對(duì)應(yīng)的ack,認(rèn)為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
155399 server端連接建立定時(shí)器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,synack重傳次數(shù)在閥值以內(nèi),重新發(fā)送synack給client端。
155400 cient端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個(gè)ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊(duì)列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
156468 client端等待幾秒后,沒(méi)有收到對(duì)應(yīng)的ack,認(rèn)為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
161309 server端連接建立定時(shí)器生效,遍歷半連接鏈表,發(fā)現(xiàn)剛才acked的連接,synack重傳次數(shù)在閥值以內(nèi),重新發(fā)送synack給client端。[!--empirenews.page--]
161310 cient端收到synack后,根據(jù)ack值,使用SACK算法,只重傳最后一個(gè)ack內(nèi)容。
Server端收到數(shù)據(jù)包,由于accept隊(duì)列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
162884 client端等待幾秒后,沒(méi)有收到對(duì)應(yīng)的ack,認(rèn)為之前的數(shù)據(jù)包也丟失,所以重傳之前的內(nèi)容數(shù)據(jù)包。
Server端收到數(shù)據(jù)包,由于accept隊(duì)列仍然是滿的,所以server端處理也只是標(biāo)記acked,然后返回。
164828 client端等待一段時(shí)間后,認(rèn)為連接不可用,于是發(fā)送FIN、ACK給server端。Client端的狀態(tài)變?yōu)镕IN_WAIT1,等待一段時(shí)間后,client端將看不到該鏈接。
164829 server端收到ACK后,此時(shí)cgi程序處理完一個(gè)請(qǐng)求,從accept隊(duì)列中取走一個(gè)連接,此時(shí)accept隊(duì)列中有了空閑,server端將請(qǐng)求的連接放到accept隊(duì)列中。
這樣cgi所在的服務(wù)器上顯示該鏈接是established的,但是nginx(client端)所在的服務(wù)器上已經(jīng)沒(méi)有該鏈接了。
之后,當(dāng)cgi程序從accept隊(duì)列中取到該連接后,調(diào)用read去讀取sock中的內(nèi)容,但是由于client端早就退出了,所以read就會(huì)block那里了。
問(wèn)題解決
或許你會(huì)認(rèn)為在164829中,server端不應(yīng)該建立連接,這是內(nèi)核的bug。但是內(nèi)核是按照RFC來(lái)實(shí)現(xiàn)的,在3次握手的過(guò)程中,是不會(huì)判斷FIN標(biāo)志位的,只會(huì)處理SYN、ACK、RST這三種標(biāo)志位。
從應(yīng)用層的角度來(lái)考慮解決問(wèn)題的方法,那就是使用非阻塞的方式read,或者使用select超時(shí)方式read;亦或者nginx中關(guān)閉連接的時(shí)候使用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.