tcp 協(xié)議 是互聯(lián)網(wǎng)中最常用的協(xié)議 输钩, 開發(fā)人員基本上天天和它打交道戳粒,對它進(jìn)行深入了解构韵。 可以幫助我們排查定位bug和進(jìn)行程序優(yōu)化绑咱。下面我將就TCP幾個點做深入的探討
一、TCP連接階段
一慢睡、TCP 三次握手
1逐工、兩次握手行不行?
客戶端:收到 ack 后 分配連接資源漂辐。 發(fā)送數(shù)據(jù)
服務(wù)器 : 收到 syn 后立即 分配連接資源
缺陷:
當(dāng)ack 丟失后泪喊, 服務(wù)器認(rèn)為連接建立了,等待客戶端發(fā)送數(shù)據(jù)髓涯。 但是客戶端并沒有建立連接袒啼。服務(wù)器一直在等待數(shù)據(jù),耗費資源纬纪。
PS: 網(wǎng)上有說法是什么TCP全雙工需要互相確認(rèn)蚓再,個人覺得和這個沒有任何關(guān)系的。
2育八、三次握手
客戶端:收到ACK, 立即分配資源
服務(wù)器:收到ACK赦邻, 立即分配資源
缺陷: 同樣問題, 最后一個ACK髓棋,服務(wù)器沒有收到, 這是 客戶端分配了資源惶洲, 但是服務(wù)器沒有分配資源按声。 客戶端并不知道,繼續(xù)發(fā)送數(shù)據(jù)恬吕。服務(wù)器會返回 RST 信號签则。 因此問題影響不大。
3铐料、四次渐裂、五次、六次...握手?
既然三次握手也不是100%可靠钠惩, 那四次柒凉,五次,六次篓跛。膝捞。。呢? 其實都一樣愧沟,不管多少次都有丟包問題蔬咬。
二鲤遥、連接狀態(tài)變化
1、正常連接
2林艘、半連接
client 只發(fā)送一個 SYN盖奈, server 分配一個tcb, 放入syn隊列中北启。 這時候連接叫半連接狀態(tài)卜朗;如果server 收不到 client 的ACK, 會不停重試 發(fā)送 ACK-SYN 給client 咕村。重試間隔 為 2 的 N 次方 疊加(2^0 , 2^1, 2^2 ....)场钉;直至超時才釋放syn隊列中的這個 TCB;
在半連接狀態(tài)下, 一方面會占用隊列配額資源懈涛,另一方面占用內(nèi)存資源逛万。我們應(yīng)該讓半連接狀態(tài)存在時間盡可能的小
3、異常狀況
當(dāng)client 向一個未打開的端口發(fā)起連接請求時批钠,會收到一個RST回復(fù)包
三宇植、backlog 隊列
1、tcp接收端有兩個隊列:一個是 syn 隊列 埋心, 另一個是 accept 隊列
- syn 隊列 : 收到 客戶端syn 的時候指郁, 在syn隊列分配一個 TCB,也叫半連接隊列
-
accept 隊列 : 當(dāng)收到客戶端 ack 時候拷呆,把 TCB 從syn隊列移到 accept中來闲坎, 也叫全連接隊列
tcp_queue.png
2、隊列大小如何設(shè)置茬斧?
- 設(shè)置syn 隊列大小
/proc/sys/net/ipv4/tcp_max_syn_backlog
- 設(shè)置 accept 隊列大小
int listen(int sockfd, int backlog);
/proc/sys/net/core/somaxconn
當(dāng)listen 的 backlog 和 somaxconn 都設(shè)置了得時候腰懂, 取兩者min值
3、當(dāng)前連接隊列大小查看
#ss -a
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 1 0 *:8888 *:*
Recv-Q 是accept 隊列當(dāng)前個數(shù)项秉, Send-Q 設(shè)置最大值
4绣溜、SYN隊列滿的情況
- 若設(shè)置 net.ipv4.tcp_syncookies = 0 ,則直接丟棄當(dāng)前 SYN 包娄蔼;
- 若設(shè)置 net.ipv4.tcp_syncookies = 1 怖喻,發(fā)送ACK+SYN;
5岁诉、ACCEPT隊列滿的情況
- 若設(shè)置 tcp_abort_on_overflow = 1 罢防,則 TCP 協(xié)議棧回復(fù) RST 包唉侄,并直接從 SYN queue 中刪除該連接信息咒吐;
- 若設(shè)置 tcp_abort_on_overflow = 0 ,則 TCP 協(xié)議棧將該連接標(biāo)記為 acked ,但仍保留在 SYN queue 中恬叹,并啟動 timer 以便重發(fā) SYN-ACK 包候生;當(dāng) SYN-ACK 的重傳次數(shù)超過 net.ipv4.tcp_synack_retries 設(shè)置的值時,再將該連接從 SYN queue 中刪除绽昼;
6唯鸭、SYN flooding
這種SYN洪水攻擊是一種常見攻擊方式,就是利用半連接隊列特性硅确,占滿syn 隊列的 資源目溉,導(dǎo)致 client無法連接上。
解決方案:
- 縮短SYN隊列超時時間 : 設(shè)置系統(tǒng)配置 net.ipv4.tcp_synack_retries菱农, 減少 重發(fā) syn-ack 次數(shù)
- Syn Cache技術(shù) : 這種技術(shù)是在收到SYN數(shù)據(jù)報文時不急于去分配TCB缭付,而是先回應(yīng)一個SYN ACK報文,并在一個專用HASH表(Cache)中保存這種半開連接信息循未,直到收到正確的回應(yīng)ACK報文再分配TCB到accept隊列陷猫。在linux系統(tǒng)中這種Cache每個半開連接只需使用160字節(jié),遠(yuǎn)小于TCB所需的736個字節(jié)的妖。
- Syn Cookie技術(shù): 相對于 Syn Cache技術(shù)绣檬, 這里不分配任何資源,只是巧妙的用算法計算一個Sequence Number 放在 SYN 請求中嫂粟。 為了能正確匹配識別 client 回應(yīng)的 ACK 中 Sequence Number娇未。 完全靠算法 來 完成。 如果開啟了 SYN cookies 選項星虹,在半連接隊列滿時零抬,SYN cookies 并不丟棄 SYN 請求,而是將源目的 IP搁凸、源目的端口號媚值、接收到的客戶端初始序列號以及其他一些安全數(shù)值等信息進(jìn)行 hash 運算狠毯,并加密后得到服務(wù)器端的初始序列號护糖,稱之為 cookie 。服務(wù)器端在發(fā)送初始序列號為 cookie 的 SYN+ACK 包后嚼松,會將分配的連接請求塊釋放嫡良。如果接收到客戶端的 ACK 包,服務(wù)器端將客戶端的 ACK 序列號減 1 得到的值献酗,與上述要素 hash 運算得到的值比較寝受,如果相等,直接完成三次握手罕偎,構(gòu)建新的連接很澄。SYN cookies 機(jī)制的核心就是避免攻擊造成的大量構(gòu)造無用的連接請求塊,導(dǎo)致內(nèi)存耗盡,而無法處理正常的連接請求甩苛。(echo 1 > /proc/sys/net/ipv4/tcp_syncookies)
7蹂楣、哪些是內(nèi)核自動完成的動作,哪些是應(yīng)用層觸發(fā)
- client 調(diào)用 connect 函數(shù) , 三次握手在內(nèi)核協(xié)議棧自動完成讯蒲,connect 函數(shù)立即返回痊土,不管server端是否是調(diào)用了 accept函數(shù)沒有。
- server調(diào)用 accept函數(shù)墨林, 才從 accept隊列移除 TCB資源 赁酝,返回socket 句柄句柄 給應(yīng)用程序。
- 如果server端的synt隊列未滿 旭等,client端調(diào)用connect 函數(shù)酌呆,不管server是否調(diào)用accept與否,都會立即返回辆雾; 如果隊列滿肪笋,client則一直重復(fù)發(fā)送SYN( 間隔 2的冪遞增)到直至超時返回。
二度迂、TCP關(guān)閉階段
一藤乙、TCP 四次 揮手
1、正常關(guān)閉流程
2惭墓、三次揮手行不行
為什么不像握手那樣合并成三次揮手? 因為和剛開始連接情況坛梁,連接是大家都從0開始, 關(guān)閉時有歷史包袱的腊凶。server(被動關(guān)閉方) 收到 client(主動關(guān)閉方) 的關(guān)閉請求FIN包划咐。 這時候可能還有未發(fā)送完的數(shù)據(jù),不能丟棄钧萍。 所以需要分開褐缠。事實可能是這樣
當(dāng)然,在沒有待發(fā)數(shù)據(jù)风瘦,并且允許 Delay ACK 情況下队魏, FIN-ACK合并還是非常常見的事情,這是三次揮手是可以的万搔。
3胡桨、二次揮手行不行
同上
4、半關(guān)閉(CLOSE_WAIT)
CLOSE_WAIT 是被動關(guān)閉方才有的狀態(tài)瞬雹。
被動關(guān)閉方 [收到 FIN 包 發(fā)送 ACK 應(yīng)答] 到 [發(fā)送FIN昧谊, 收到ACK ] 期間的狀態(tài)為 CLOSE_WAIT, 這個狀態(tài)仍然能發(fā)送數(shù)據(jù)酗捌。 我們叫做半關(guān)閉, 下面用個例子來分析:
這個是我實際生產(chǎn)環(huán)境碰到的一個問題呢诬,長連接會話場景涌哲,server端收到client的rpc call 請求1,處理發(fā)現(xiàn)請求包有問題尚镰,就強(qiáng)制關(guān)閉結(jié)束這次會話膛虫, 但是 因為client 發(fā)送 第二次請求之前,并沒有去調(diào)用recv钓猬,所以并不知道 這個連接被server關(guān)閉稍刀, 繼續(xù)發(fā)送 請求2 , 此時是半連接敞曹,能夠成功發(fā)送到對端機(jī)器账月,但是recv結(jié)果后,遇到連接已經(jīng)關(guān)閉錯誤澳迫。
CLOSE_WAIT 和 后面提到的 TIME_WAIT 一樣局齿,都是存在潛在危害,CLOSE_WAIT 狀態(tài)下 文件句柄資源沒有釋放橄登。要知道系統(tǒng) 句柄資源也是有限的抓歼。要盡快釋放close掉。
5拢锹、同時關(guān)閉
如果 client 和 server 恰好同時發(fā)起關(guān)閉連接谣妻。這種情況下,兩邊都是主動連接卒稳,都會進(jìn)入 TIME_WAIT狀態(tài)
4蹋半、TIME_WAIT 狀態(tài)
TIME_WAIT 狀態(tài) 是主動關(guān)閉方才有的狀態(tài):這種狀態(tài)下有個讓開發(fā)人員都很苦惱的普遍性問題,端口耗盡, 我們知道充坑,端口數(shù)據(jù)類型是 unsigned short减江。 最大值是 65535. 所以 一臺機(jī) 上 最多分配 65535個端口(不考慮accept的tcp資源情況下,也可以看成一臺機(jī)器最多只能分配這么鏈接數(shù))捻爷。
然而辈灼,TIME_WAIT* 持續(xù)保持時間是 2*MSL( Maximum segment lifetime)。 默認(rèn)是 2分鐘也榄。( 通過這個可以修改 /proc/sys/net/ipv4/tcp_fin_timeout)巡莹。想想在并發(fā)高的機(jī)器上 2分鐘 很容易發(fā)起超過 6w多個短鏈接請求。 這時候就出現(xiàn)端口不夠用手蝎,connect 錯誤“Cannot assign requested address” 榕莺。TIME_WAIT的如此設(shè)計是為了解決2個問題:
以下兩種討論的兩種情況都是 假設(shè) 前后兩次連接都是相同四元組( 源ip俐芯,源端口棵介,目的ip,目的端口)吧史,都是屬于 "在不相關(guān)的連接中接受延遲的段"現(xiàn)象
1邮辽、被動關(guān)閉方在LAST_ACK狀態(tài)(已經(jīng)發(fā)送FIN),等待主動關(guān)閉方的ACK應(yīng)答,但是 ACK丟掉吨述, 主動方并不知道岩睁,以為成功關(guān)閉。因為沒有TIME_WAIT等待時間揣云,可以立即創(chuàng)建新的連接捕儒, 新的連接發(fā)送SYN到前面那個未關(guān)閉的被動方,被動方認(rèn)為是收到錯誤指令邓夕,會發(fā)送RST刘莹。導(dǎo)致創(chuàng)建連接失敗。
2焚刚、主動關(guān)閉方斷開連接点弯,如果沒有TIME_WAIT等待時間,可以馬上建立一個新的連接矿咕,但是前一個已經(jīng)斷開連接的抢肛,延遲到達(dá)的數(shù)據(jù)包。 被新建的連接接收碳柱,如果剛好seq 和 ack字段 都正確, seq在滑動窗口范圍內(nèi)(只能說機(jī)率非常小捡絮,但是還是有可能會發(fā)生),會被當(dāng)成正確數(shù)據(jù)包接收莲镣,導(dǎo)致數(shù)據(jù)串包锦援。 如果不在window范圍內(nèi),則沒有影響( 發(fā)送一個確認(rèn)報文(ack 字段為期望ack的序列號剥悟,seq為當(dāng)前發(fā)送序列號)灵寺,狀態(tài)變保持原樣)
4、解決TIME_WAIT 問題
TIME_WAIT 問題比較比較常見区岗,特別是CGI機(jī)器略板,并發(fā)量高,大量連接后段服務(wù)的tcp短連接慈缔。因此也衍生出了多種手段解決叮称。雖然每種方法解決不是那么完美,但是帶來的好處一般多于壞處藐鹤。還是在日常工作中會使用瓤檐。
1、改短TIME_WAIT 等待時間
net/ipv4/tcp_fin_timeout
這個是第一個想到的解決辦法娱节,既然等待時間太長挠蛉,就改成時間短,快速回收端口肄满。但是實際情況往往不樂觀谴古,對于并發(fā)的機(jī)器质涛,你改多短才能保證回收速度呢,有時候幾秒鐘就幾萬個連接掰担。太短的話汇陆,就會有前面兩種問題小概率發(fā)生。
2带饱、禁止Socket lingering
struct linger stLinger;
stLinger.l_onoff = 1;
stLinger.l_linger = 0;
setsockopt(fdC, SOL_SOCKET, SO_LINGER, ( void *)&stLinger, sizeof(stLinger));
這種情況下關(guān)閉連接毡代,會直接拋棄緩沖區(qū)中待發(fā)送的數(shù)據(jù),會發(fā)送一個RST給對端勺疼,相當(dāng)于直接拋棄TIME_WAIT月趟, 進(jìn)入CLOSE狀態(tài)。同樣因為取消了 TIME_WAIT 狀態(tài)恢口,會有前面兩種問題小概率發(fā)生孝宗。
3、tcp_tw_reuse
net.ipv4.tcp_tw_reuse選項是 從 TIME_WAIT 狀態(tài)的隊列中耕肩,選取條件:1因妇、remote 的 ip 和端口相同, 2猿诸、選取一個時間戳小于當(dāng)前時間戳婚被; 用來解決端口不足的尷尬。
net/ipv4/tcp_ipv4.c
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
/* ……省略…… */
if (tcptw->tw_ts_recent_stamp &&
(!twp || (sock_net(sk)->ipv4.sysctl_tcp_tw_reuse &&
get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
/* ……省略…… */
return 1;
}
return 0;
}
net/ipv4/inet_hashtables.c
static int __inet_check_established(struct inet_timewait_death_row *death_row,
struct sock *sk, __u16 lport,
struct inet_timewait_sock **twp)
{
/* ……省略…… */
sk_nulls_for_each(sk2, node, &head->chain) {
if (sk2->sk_hash != hash)
continue;
if (likely(INET_MATCH(sk2, net, acookie,
saddr, daddr, ports, dif))) {
if (sk2->sk_state == TCP_TIME_WAIT) {
tw = inet_twsk(sk2);
if (twsk_unique(sk, sk2, twp))
break;
}
goto not_unique;
}
}
/* ……省略…… */
}
現(xiàn)在端口可以復(fù)用了梳虽,看看如何面對前面TIME_WAIT 那兩種問題址芯。 我們仔細(xì)回顧用一下前面兩種問題。都是在新建連接中收到老連接的包導(dǎo)致的問題窜觉, 那么如果我能在新連接中識別出此包為非法包谷炸,是不是就可以丟掉這些無用包,解決問題呢禀挫。
需要實現(xiàn)這些功能旬陡,需要擴(kuò)展一下tcp 包頭。 增加 時間戳字段语婴。 發(fā)送者 在每次發(fā)送的時候描孟。 在tcp包頭里面帶上發(fā)送時候的時間戳。 當(dāng)接收者接收的時候砰左,在ACK應(yīng)答中除了TCP包頭中帶自己此時發(fā)送的時間戳匿醒,并且把收到的時間戳附加在后面。也就是說ACK包中有兩個時間戳字段缠导。結(jié)構(gòu)如下:
那我們接下來一個個分析tcp_tw_reuse是如何解決TIME_WAIT的兩個問題的
tcp_tw_reuse 使用有三個條件:
1廉羔、必須開啟 timestamp,通過 net.ipv4.tcp_timestamp 參數(shù)設(shè)置, linux 2.6 后缺省是打開的酬核;
2蜜另、必須客戶端和server同時支持 timestamp. 在連接握手階段協(xié)商:當(dāng)一方不開啟時,兩方都將停用timestamps嫡意。比如client端發(fā)送的SYN包中帶有timestamp選項举瑰,但server端并沒有開啟該選項。則回復(fù)的SYN-ACK將不帶timestamp選項蔬螟,同時client后續(xù)回復(fù)的ACK也不會帶有timestamp選項此迅。當(dāng)然,如果client發(fā)送的SYN包中就不帶timestamp旧巾,雙向都將停用timestamp耸序。
3、 tcp_tw_reuse 只對outgoing connections 有效鲁猩。也就是發(fā)起連接方 client 有效坎怪。 server端產(chǎn)生新的連接是不會復(fù)用 TIME_WAIT 資源
- LAST_ACK收到 SYN問題: LAST_ACK 狀態(tài) socket 苦苦等待 主動關(guān)閉方的ACK應(yīng)答,但是卻收到了SYN廓握。注意:敲黑板搅窿,劃重點了。此時檢查有timestamp字段的話隙券,不會立即發(fā)送RST包男应,而是比較timestamp字段時間戳非常新,明顯不是自己這個連接建立時候SYN延遲包娱仔。這時候恢復(fù) FIN + ACK沐飘, 對方收到 回復(fù)一個RST結(jié)束上個連接。 繼續(xù)發(fā)SYN建立新的連接 牲迫。這樣就解決料老的連接無法關(guān)閉耐朴,新的連接無法建立的問題。
- 收到上一個連接數(shù)據(jù)串包問題盹憎,也是檢查這個數(shù)據(jù)包的TCP頭里面的timestamp隔箍,比較發(fā)現(xiàn)遠(yuǎn)遠(yuǎn)小于上一次TCP包的timestamp ,于是丟棄這個包即可脚乡。
4蜒滩、tcp_tw_recycle
tcp_tw_recycle自 Linux內(nèi)核4.12版以來,已被棄用奶稠, 大家可以放心的不用了
tcp_tw_recycle 也是借助 timestamp機(jī)制俯艰。顧名思義, tcp_tw_reuse 是復(fù)用 端口锌订,并不會減少 TIME-WAIT 數(shù)量竹握。你去查詢機(jī)器上TIME-WAIT 數(shù)量,還是 幾千幾萬個辆飘,這點對有強(qiáng)迫癥的同學(xué)感覺很不舒服啦辐。tcp_tw_recycle 是 提前 回收 TIME-WAIT資源谓传。會減少 機(jī)器上 TIME-WAIT 數(shù)量。
tcp_tw_recycle 工作原理是芹关。
動態(tài)縮小TIME-WAIT 時間, 不再是 2*MSL時間回收來回收TIME-WAIT資源续挟,而是根據(jù) RTO(Retransmission TimeOut)即重傳超時時間, 這個時間根據(jù)一套特定算法來動態(tài)計算侥衬,當(dāng)TIME-WAIT 停留時間大于 RTO诗祸。系統(tǒng)釋放資源。 這招可以減少TIME-WAIT 數(shù)量
為了解決TIME-WAIT 收到不相關(guān)連接數(shù)據(jù)包的問題轴总, 內(nèi)核會紀(jì)錄 TIME-WAIT 狀態(tài)下 最后一次 收到包的時間戳直颅,和ip地址等信息。(注意是IP地址沒有端口信息哦)怀樟。如果收到連接SYN包(注意只在連接時候檢測哦)功偿,先檢查remote IP 有沒有 相關(guān)的 TIME-WAIT狀態(tài),如果有則比較時間戳往堡,如果時間戳比最后一次收到時間戳還要小脖含,則這個包被認(rèn)為是延遲包,丟棄掉投蝉。
存在問題: 一般情況下我們不建議使用tcp_tw_recycle 养葵, 因為它在 存在 NAT 網(wǎng)關(guān)的時候。 會出問題瘩缆。NAT是地址共享关拒, 而 tcp_tw_recycle 機(jī)制下恰恰又是根據(jù)ip紀(jì)錄時間戳信息的。在 NAT下庸娱,多臺機(jī)器上client着绊。 對于server 可能是同一個IP。由于客戶端的時間戳可能不一致熟尉。會導(dǎo)致連接不上問題归露。
NAT網(wǎng)絡(luò)在國內(nèi)使用非常普遍,家庭網(wǎng)絡(luò)基本上NAT網(wǎng)絡(luò)斤儿, N個設(shè)備共享一個IP地址剧包。 公司辦公網(wǎng)基本上也是如此。