由STGW下载慢问题引发的网络传输学习之旅
導(dǎo)語(yǔ):本文分享了筆者現(xiàn)網(wǎng)遇到的一個(gè)文件下載慢的問(wèn)題。最開始嘗試過(guò)很多辦法,包括域名解析,網(wǎng)絡(luò)鏈路分析,AB環(huán)境測(cè)試,網(wǎng)絡(luò)抓包等,但依然找不到原因。然后利用網(wǎng)絡(luò)命令和報(bào)文得到的蛛絲馬跡,結(jié)合內(nèi)核網(wǎng)絡(luò)協(xié)議棧的實(shí)現(xiàn)代碼,找到了一個(gè)內(nèi)核隱藏很久但在最近版本解決了的BUG。如果你也想了解如何分析和解決詭異的網(wǎng)絡(luò)問(wèn)題,如果你也想溫習(xí)一下課堂上曾經(jīng)學(xué)習(xí)過(guò)的慢啟動(dòng)、擁塞避免、快速重傳、AIMD等老掉牙的知識(shí),如果你也渴望學(xué)習(xí)課本上完全沒(méi)介紹過(guò)的TCP的一系列優(yōu)化比如混合慢啟動(dòng)、尾包探測(cè)甚至BBR等,那么本文或許可以給你一些經(jīng)驗(yàn)和啟發(fā)。
問(wèn)題背景線上用戶經(jīng)過(guò)STGW(Secure Tencent Gateway,騰訊安全網(wǎng)關(guān)-七層轉(zhuǎn)發(fā)代理)下載一個(gè)50M左右的文件,與直連用戶自己的服務(wù)器相比,下載速度明顯變慢,需要定位原因。在了解到用戶的問(wèn)題之后,相關(guān)的同事在線下做了如下嘗試:
1. 從廣州和上海直接訪問(wèn)用戶的回源VIP(Virtual IP,提供服務(wù)的公網(wǎng)IP地址)下載,都耗時(shí)4s+,正常;
2. 只經(jīng)過(guò)TGW(Tencent Gateway,騰訊網(wǎng)關(guān)-四層負(fù)載均衡系統(tǒng)),不經(jīng)過(guò)STGW訪問(wèn),從廣州和上海訪問(wèn)上海的TGW,耗時(shí)都是4s+,正常;
3. 經(jīng)過(guò)STGW,從上海訪問(wèn)上海的STGW VIP,耗時(shí)4s+,正常;
4. 經(jīng)過(guò)STGW,從廣州訪問(wèn)上海的STGW VIP,耗時(shí)12s+,異常。
前面的三種情況都是符合預(yù)期,而第四種情況是不符合預(yù)期的,這個(gè)也是本文要討論的問(wèn)題。
前期定位排查發(fā)現(xiàn)下載慢的問(wèn)題后,我們分析了整體的鏈路情況,按照鏈路經(jīng)過(guò)的節(jié)點(diǎn)順序有了如下的排查思路:
(1)從客戶端側(cè)來(lái)排查,DNS解析慢,客戶端讀取響應(yīng)慢或者接受窗口小等;
(2)從鏈路側(cè)來(lái)排查,公網(wǎng)鏈路問(wèn)題,中間交換機(jī)設(shè)備問(wèn)題,丟包等;
(3)從業(yè)務(wù)服務(wù)側(cè)來(lái)排查,業(yè)務(wù)服務(wù)側(cè)發(fā)送響應(yīng)較慢,發(fā)送窗口較小等;
(4)從自身轉(zhuǎn)發(fā)服務(wù)來(lái)排查,TGW或STGW轉(zhuǎn)發(fā)程序問(wèn)題,STGW擁塞窗口緩存等;
按照上面的這些思路,我們分別做了如下的排查:
1.是否是由于異常客戶端的DNS服務(wù)器解析慢導(dǎo)致的?
用戶下載小文件沒(méi)有問(wèn)題,并且直接訪問(wèn)VIP,配置hosts訪問(wèn),發(fā)現(xiàn)問(wèn)題依然復(fù)現(xiàn),排除。
2.是否是由于客戶端讀取響應(yīng)慢或者接收窗口較小導(dǎo)致的?
抓包分析客戶端的數(shù)據(jù)包處理情況,發(fā)現(xiàn)客戶端收包處理很快,并且接收窗口一直都是有很大空間。排除。
3.是否是廣州到上海的公網(wǎng)鏈路或者交換機(jī)等設(shè)備問(wèn)題,導(dǎo)致訪問(wèn)變慢?
從廣州的客戶端上ping上海的VIP,延時(shí)很低,并且測(cè)試不經(jīng)過(guò)STGW,從該客戶端直接訪問(wèn)TGW再到回源服務(wù)器,下載正常,排除。
4.是否是STGW到回源VIP這條鏈路上有問(wèn)題?
在STGW上直接訪問(wèn)用戶的回源VIP,耗時(shí)4s+,是正常的。并且打開了STGW LD(LoadBalance Director,負(fù)載均衡節(jié)點(diǎn))與后端server之間的響應(yīng)緩存,抓包可以看到,后端數(shù)據(jù)4s左右全部發(fā)送到STGW LD上,是STGW LD往客戶端回包比較慢,基本可以確認(rèn)是Client->STGW這條鏈路上有問(wèn)題。排除。
5.是否是由于TGW或STGW轉(zhuǎn)發(fā)程序有問(wèn)題?
由于異地訪問(wèn)必定會(huì)復(fù)現(xiàn),同城訪問(wèn)就是正常的。而TGW只做四層轉(zhuǎn)發(fā),無(wú)法感知源IP的地域信息,并且抓包也確認(rèn)TGW上并沒(méi)有出現(xiàn)大量丟包或者重傳的現(xiàn)象。STGW是一個(gè)應(yīng)用層的反向代理轉(zhuǎn)發(fā),也不會(huì)對(duì)于不同地域的cip有不同的處理邏輯。排除。
6.是否是由于TGW是fullnat影響了擁塞窗口緩存?
因?yàn)橹坝捎趂ullnat出現(xiàn)過(guò)一些類似于本例中下載慢的問(wèn)題,當(dāng)時(shí)定位的原因是由于STGW LD上開啟了擁塞窗口緩存,在fullnat的情況下,會(huì)影響擁塞窗口緩存的準(zhǔn)確性,導(dǎo)致部分請(qǐng)求下載慢。但是這里將擁塞窗口緩存選項(xiàng) sysctl -w net.ipv4.tcp_no_metrics_save=1 關(guān)閉之后測(cè)試,發(fā)現(xiàn)問(wèn)題依然存在,并且線下用另外一個(gè)fullnat的vip測(cè)試,發(fā)現(xiàn)并沒(méi)有復(fù)現(xiàn)用戶的問(wèn)題。排除。
根據(jù)一些以往的經(jīng)驗(yàn)和常規(guī)的定位手段都嘗試了以后,發(fā)現(xiàn)仍然還是沒(méi)有找到原因,那到底是什么導(dǎo)致的呢?
問(wèn)題分析首先,在復(fù)現(xiàn)的STGW LD上抓包,抓到Client與STGW LD的包如下圖,從抓包的信息來(lái)看是STGW回包給客戶端很慢,每次都只發(fā)很少的一部分到Client。
這里有一個(gè)很奇怪的地方就是為什么第7號(hào)包發(fā)生了重傳?不過(guò)暫時(shí)可以先將這個(gè)疑問(wèn)放到一邊,因?yàn)榫退?號(hào)包發(fā)生了一個(gè)包的重傳,這中間也并沒(méi)有發(fā)生丟包,LD發(fā)送數(shù)據(jù)也并不應(yīng)該這么慢。那既然LD發(fā)送數(shù)據(jù)這么慢,肯定要么是Client的接收窗口小,要么是LD的擁塞窗口比較小。
對(duì)端的接收窗口,抓包就可以看到,實(shí)際上Client的接收窗口并不小,而且有很大的空間。那是否有辦法可以看到LD的發(fā)送窗口呢?答案是肯定的:ss -it,這個(gè)指令可以看到每條連接的rtt,ssthresh,cwnd等信息。有了這些信息就好辦了,再次復(fù)現(xiàn),并寫了個(gè)命令將cwnd等信息記錄到文件:
while true; do date +"%T.%6N" >> cwnd.log; ss -it >> cwnd.log; done復(fù)現(xiàn)得到的cwnd.log如上圖,找到對(duì)應(yīng)的連接,grep出來(lái)后對(duì)照來(lái)看。果然發(fā)現(xiàn)在前面幾個(gè)包中,擁塞窗口就直接被置為7,并且ssthresh也等于7,并且可以看到后面窗口增加的很慢,直接進(jìn)入了擁塞避免,這么小的發(fā)送窗口,增長(zhǎng)又很緩慢,自然發(fā)送數(shù)據(jù)就會(huì)很慢了。
那么到底是什么原因?qū)е逻@里直接在前幾個(gè)包就進(jìn)入擁塞避免呢?從現(xiàn)有的信息來(lái)看,沒(méi)辦法直接確定原因,只能去啃代碼了,但tcp擁塞控制相關(guān)的代碼這么多,如何能快速定位呢?
觀察上面異常數(shù)據(jù)包的cwnd信息,可以看到一個(gè)很明顯的特征,最開始ssthresh是沒(méi)有顯示出來(lái)的,經(jīng)過(guò)了幾個(gè)數(shù)據(jù)包之后,ssthresh與cwnd是相等的,所以嘗試按照"snd_ssthresh ="和"snd_cwnd ="的關(guān)鍵字來(lái)搜索,按照snd_cwnd = snd_ssthresh的原則來(lái)找,排除掉一些不太可能的函數(shù)之后,最后找到了tcp_end_cwnd_reduction這個(gè)函數(shù)。
再查找這個(gè)函數(shù)引用的地方,有兩處:tcp_fastretrans_alert和tcp_process_tlp_ack這兩個(gè)函數(shù)。
tcp_fastretrans_alert看名字就知道是跟快速重傳相關(guān)的函數(shù),我們知道快速重傳觸發(fā)的條件是收到了三個(gè)重復(fù)的ack包。但根據(jù)前面的抓包及分析來(lái)看,并不滿足快速重傳的條件,所以疑點(diǎn)就落在了這個(gè)tcp_process_tlp_ack函數(shù)上面。那么到底什么是TLP呢?
什么是TLP(Tail Loss Probe)在講TLP之前,我們先來(lái)回顧下大學(xué)課本里學(xué)到的擁塞控制算法,祭出這張經(jīng)典的擁塞控制圖。?
TCP的擁塞控制主要分為四個(gè)階段:慢啟動(dòng),擁塞避免,快重傳,快恢復(fù)。長(zhǎng)久以來(lái),我們聽到的說(shuō)法都是,最開始擁塞窗口從1開始慢啟動(dòng),以指數(shù)級(jí)遞增,收到三個(gè)重復(fù)的ack后,將ssthresh設(shè)置為當(dāng)前cwnd的一半,并且置cwnd=ssthresh,開始執(zhí)行擁塞避免,cwnd加法遞增。
這里我們來(lái)思考一個(gè)問(wèn)題,發(fā)生丟包時(shí),為什么要將ssthresh設(shè)置為cwnd的一半?
想象一個(gè)場(chǎng)景,A與B之間發(fā)送數(shù)據(jù),假設(shè)二者發(fā)包和收包頻率是一致的,由于A與B之間存在空間距離,中間要經(jīng)過(guò)很多個(gè)路由器,交換機(jī)等,A在持續(xù)發(fā)包,當(dāng)B收到第一個(gè)包時(shí),這時(shí)A與B之間的鏈路里的包的個(gè)數(shù)為N,此時(shí)由于B一直在接收包,因此A還可以繼續(xù)發(fā),直到第一個(gè)包的ack回到A,這時(shí)A發(fā)送的包的個(gè)數(shù)就是當(dāng)前A與B之間最大的擁塞窗口,即為2N,因?yàn)槿绻@時(shí)A多發(fā)送,肯定就丟包了。
ssthresh代表的就是當(dāng)前鏈路上可以發(fā)送的最大的擁塞窗口大小,理想情況下,ssthresh就是2N,但現(xiàn)實(shí)的環(huán)境很復(fù)雜,不可能剛好cwnd經(jīng)過(guò)慢啟動(dòng)就可以直接到達(dá)2N,發(fā)送丟包的時(shí)候,肯定是N<1/2*cwnd<2N,因此此時(shí)將ssthresh設(shè)置為1/2*cwnd,然后再?gòu)拇颂幖臃ㄔ黾勇倪_(dá)到理想窗口,不能增長(zhǎng)過(guò)快,因?yàn)橐氨苊鈸砣薄?/p>
實(shí)際上,各個(gè)擁塞控制算法都有自己的實(shí)現(xiàn),初始cwnd的值也一直在優(yōu)化,在linux 3.0版本以后,內(nèi)核CUBIC的實(shí)現(xiàn)里,采用了Google在RFC6928的建議,將初始的cwnd的值設(shè)置為10。而在linux 3.0版本之前,采取的是RFC3390中的策略,根據(jù)不同的MSS,設(shè)置了不同的初始化cwnd。具體的策略為:
If (MSS <= 1095 bytes)? ? then cwnd=4;
If (1095 bytes < MSS < 2190 bytes)
? ? then cwnd=3;
If (2190 bytes <= MSS)
? ? then cwnd=2;
并且在執(zhí)行擁塞避免時(shí),當(dāng)前CUBIC的實(shí)現(xiàn)里也不是將ssthresh設(shè)置為cwnd的一半,而是717/1024≈0.7左右,RFC8312也提到了這樣做的原因。
Principle 4: To balance between the scalability and convergence speed, CUBIC sets the multiplicative window decrease factor to 0.7 while Standard TCP uses 0.5. While this improves the scalability of CUBIC, a side effect of this decision is slower convergence, especially under low statistical multiplexing environments.從上面的描述可以看到,在TCP的擁塞控制算法里,最核心的點(diǎn)就是ssthresh的確定,如何能快速準(zhǔn)確的確定ssthresh,就可以更加高效的傳輸。而現(xiàn)實(shí)的網(wǎng)絡(luò)環(huán)境很復(fù)雜,在有些情況下,沒(méi)有辦法滿足快速重傳的條件,如果每次都以丟包作為反饋,代價(jià)太大。比如,考慮如下的幾個(gè)場(chǎng)景:
是否可以探測(cè)到ssthresh的值,不依賴丟包來(lái)觸發(fā)進(jìn)入擁塞避免,主動(dòng)退出慢啟動(dòng)?
如果沒(méi)有足夠的dup ack(大于0,小于3)來(lái)觸發(fā)快速重傳,如何處理?
如果沒(méi)有任何的dup ack(等于0),比如尾丟包的情況,如何處理?
是否可以主動(dòng)探測(cè)網(wǎng)絡(luò)帶寬,基于反饋驅(qū)動(dòng)來(lái)調(diào)整窗口,而不是丟包等事件驅(qū)動(dòng)來(lái)執(zhí)行擁塞控制?
針對(duì)上面的前三種情況,TCP協(xié)議棧分別都做了相應(yīng)的優(yōu)化,對(duì)應(yīng)的優(yōu)化算法分別為:hystart(Hybrid Slow Start),ER(Early Retransmit)和TLP(Tail Loss Probe)。對(duì)于第四種情況,Google給出了答案,創(chuàng)造了一種新的擁塞控制算法,它的名字叫BBR,從linux 4.19開始,內(nèi)核已經(jīng)將默認(rèn)的擁塞控制算法從CUBIC改成了BBR。受限于本文的篇幅有限,無(wú)法對(duì)BBR算法做詳盡的介紹,下面僅結(jié)合內(nèi)核CUBIC的代碼來(lái)分別介紹前面的這三種優(yōu)化算法。
1. 慢啟動(dòng)的hystart優(yōu)化
混合慢啟動(dòng)的思想是在論文《Hybrid Slow Start for High-Bandwidth and Long-Distance Networks》里首次提出的,前面我也說(shuō)過(guò),如果每次判斷擁塞都依賴丟包來(lái)作為反饋,代價(jià)太大,hystart也是在這個(gè)方向上做優(yōu)化,它主要想解決的問(wèn)題就是不依賴丟包作為反饋來(lái)退出慢啟動(dòng),它提出的退出條件有兩類:
判斷在同一批發(fā)出去的數(shù)據(jù)包收到的ack包(對(duì)應(yīng)論文中的acks train length)的總時(shí)間大于min(rtt)/2;
判斷一批樣本中的最小rtt是否大于全局最小rtt加一個(gè)閾值的和;
內(nèi)核CUBIC的實(shí)現(xiàn)里默認(rèn)都是開啟了hystart,在bictcp_init函數(shù)里判斷是否開啟并做初始化
static inline void bictcp_hystart_reset(struct sock *sk) {struct tcp_sock *tp = tcp_sk(sk);struct bictcp *ca = inet_csk_ca(sk);ca->round_start = ca->last_ack = bictcp_clock();ca->end_seq = tp->snd_nxt;ca->curr_rtt = 0;ca->sample_cnt = 0; } static void bictcp_init(struct sock *sk) {struct bictcp *ca = inet_csk_ca(sk);bictcp_reset(ca);ca->loss_cwnd = 0;if (hystart)//如果開啟了hystart,那么做初始化bictcp_hystart_reset(sk);if (!hystart && initial_ssthresh)tcp_sk(sk)->snd_ssthresh = initial_ssthresh; }核心的判斷是否退出慢啟動(dòng)的函數(shù)在hystart_update里
static void hystart_update(struct sock *sk, u32 delay) {struct tcp_sock *tp = tcp_sk(sk);struct bictcp *ca = inet_csk_ca(sk);if (!(ca->found & hystart_detect)) {u32 now = bictcp_clock();/* first detection parameter - ack-train detection *///判斷如果連續(xù)兩個(gè)ack的間隔小于hystart_ack_delta(2ms),則為一個(gè)acks trainif ((s32)(now - ca->last_ack) <= hystart_ack_delta) {ca->last_ack = now;//如果ack_train的總長(zhǎng)度大于1/2 * min_rtt,則退出慢啟動(dòng),ca->delay_min = 8*min_rttif ((s32)(now - ca->round_start) > ca->delay_min >> 4)ca->found |= HYSTART_ACK_TRAIN;}/* obtain the minimum delay of more than sampling packets *///如果小于HYSTART_MIN_SAMPLES(8)個(gè)樣本則直接計(jì)數(shù)if (ca->sample_cnt < HYSTART_MIN_SAMPLES) {if (ca->curr_rtt == 0 || ca->curr_rtt > delay)ca->curr_rtt = delay;ca->sample_cnt++;} else {/** 否則,判斷這些樣本中的最小rtt是否要大于全局的最小rtt+有范圍變化的閾值,* 如果是,則說(shuō)明發(fā)生了擁塞*/if (ca->curr_rtt > ca->delay_min +HYSTART_DELAY_THRESH(ca->delay_min>>4))ca->found |= HYSTART_DELAY;}/** Either one of two conditions are met,* we exit from slow start immediately.*///判斷ca->found如果為真,則退出慢啟動(dòng),進(jìn)入擁塞避免if (ca->found & hystart_detect)tp->snd_ssthresh = tp->snd_cwnd;} }2. ER(Early?Retransmit)算法
我們知道,快重傳的條件是必須收到三個(gè)相同的dup ack,才會(huì)觸發(fā),那如果在有些情況下,沒(méi)有足夠的dup ack,只能依賴rto超時(shí),再進(jìn)行重傳,并且開始執(zhí)行慢啟動(dòng),這樣的代價(jià)太大,ER算法就是為了解決這樣的場(chǎng)景,RFC5827詳細(xì)介紹了這個(gè)算法。
算法的基本思想:
ER_ssthresh = 3 //ER_ssthresh代表觸發(fā)快速重傳的dup ack的個(gè)數(shù) if (unacked segments < 4 && no new data send)if (sack is unable) // 如果SACK選項(xiàng)不支持,則使用還未ack包的個(gè)數(shù)減一作為閾值ER_ssthresh = unacked segments - 1elif (sacked packets == unacked segments - 1) // 否則,只有當(dāng)還有一個(gè)包還未sack,才能啟用ER,并且置閾值為還未ack包的個(gè)數(shù)減一ER_ssthresh = unacked segments - 1對(duì)應(yīng)到代碼里的函數(shù)為tcp_time_to_recover:
static bool tcp_time_to_recover(struct sock *sk, int flag) {.../* Trick#6: TCP early retransmit, per RFC5827. To avoid spurious* retransmissions due to small network reorderings, we implement* Mitigation A.3 in the RFC and delay the retransmission for a short* interval if appropriate.*/if (tp->do_early_retrans //開啟ER算法&& !tp->retrans_out //沒(méi)有重傳數(shù)據(jù)&& tp->sacked_out //當(dāng)前收到了dupack包&& (tp->packets_out >= (tp->sacked_out + 1) && tp->packets_out < 4) //滿足ER的觸發(fā)條件&& !tcp_may_send_now(sk)) //沒(méi)有新的數(shù)據(jù)發(fā)送return !tcp_pause_early_retransmit(sk, flag);//判斷是立即進(jìn)入ER還是需要delay 1/4 rttreturn false; } /** 這里內(nèi)核的實(shí)現(xiàn)與rfc5827有一點(diǎn)不同,就是引入了delay ER的概念,主要是防止過(guò)多減小的dupack 閾值帶來(lái)的* 無(wú)效的重傳,所以默認(rèn)加了一個(gè)1/4 RTT的delay,在ER的基礎(chǔ)上又做了一個(gè)折中,等一段時(shí)間再判斷是否要重傳。* 如果是false,則立即進(jìn)入ER,如果是true,則delay max(RTT/4,2msec)再進(jìn)入ER*/ static bool tcp_pause_early_retransmit(struct sock *sk, int flag) {struct tcp_sock *tp = tcp_sk(sk);unsigned long delay;/* Delay early retransmit and entering fast recovery for* max(RTT/4, 2msec) unless ack has ECE mark, no RTT samples* available, or RTO is scheduled to fire first.*///內(nèi)核提供了一個(gè)參數(shù)tcp_early_retrans來(lái)控制ER和delay ER,等于2和3時(shí),是打開了delay ERif (sysctl_tcp_early_retrans < 2 || sysctl_tcp_early_retrans > 3 ||(flag & FLAG_ECE) || !tp->srtt)return false;delay = max_t(unsigned long, (tp->srtt >> 5), msecs_to_jiffies(2));if (!time_after(inet_csk(sk)->icsk_timeout, (jiffies + delay)))return false;//設(shè)置delay ER的定時(shí)器inet_csk_reset_xmit_timer(sk, ICSK_TIME_EARLY_RETRANS, delay,TCP_RTO_MAX);return true; }delay ER的定時(shí)器超時(shí)的處理函數(shù)tcp_resume_early_retransmit。
void tcp_resume_early_retransmit(struct sock *sk) {struct tcp_sock *tp = tcp_sk(sk);tcp_rearm_rto(sk);/* Stop if ER is disabled after the delayed ER timer is scheduled */if (!tp->do_early_retrans)return;//執(zhí)行快速重傳tcp_enter_recovery(sk, false);tcp_update_scoreboard(sk, 1);tcp_xmit_retransmit_queue(sk); }內(nèi)核提供了一個(gè)開關(guān),tcp_early_retrans用于開啟和關(guān)閉TLP和ER算法,默認(rèn)是3,即打開了delay ER和TLP算法。
sysctl_tcp_early_retrans (defalut:3)0 disables ER1 enables ER2 enables ER but delays fast recovery and fast retransmit by a fourth of RTT.3 enables delayed ER and TLP.4 enables TLP only.到此,這就是內(nèi)核設(shè)計(jì)ER算法的相關(guān)的代碼。ER算法在cwnd比較小的情況下,是可以有一些改善的,但個(gè)人認(rèn)為,實(shí)際的效果可能一般。因?yàn)槿绻鹀wnd較小,執(zhí)行慢啟動(dòng)與執(zhí)行快速重傳再進(jìn)入擁塞避免相比,二者的實(shí)際傳輸效率可能相差并不大。
3.TLP(Tail Loss Probe)算法
TLP想解決的問(wèn)題是:如果尾包發(fā)生了丟包,沒(méi)有新包可發(fā)送觸發(fā)多余的dup ack來(lái)實(shí)現(xiàn)快速重傳,如果完全依賴RTO超時(shí)來(lái)重傳,代價(jià)太大,那如何能優(yōu)化解決這種尾丟包的情況。
TLP算法是2013年谷歌在論文《Tail Loss Probe (TLP): An Algorithm for Fast Recovery of Tail Losses》中提出來(lái)的,它提出的基本思想是:
在每個(gè)發(fā)送的數(shù)據(jù)包的時(shí)候,都更新一個(gè)定時(shí)器PTO(probe timeout),這個(gè)PTO是動(dòng)態(tài)變化的,當(dāng)發(fā)出的包中存在未ack的包,并且在PTO時(shí)間內(nèi)都未收到一個(gè)ack,那么就會(huì)發(fā)送一個(gè)新包或者重傳最后的一個(gè)數(shù)據(jù)包,探測(cè)一下當(dāng)前網(wǎng)絡(luò)是否真的擁塞發(fā)生丟包了。
如果收到了tail包的dup ack,則說(shuō)明沒(méi)有發(fā)生丟包,繼續(xù)執(zhí)行當(dāng)前的流程;否則說(shuō)明發(fā)生了丟包,需要執(zhí)行減窗,并且進(jìn)入擁塞避免。
這里其中一個(gè)比較重要的點(diǎn)是PTO如何設(shè)置,設(shè)置的策略如下:
if unacked packets == 0:no need set PTO else if unacked packets == 1:PTO=max(2rtt, 1.5*rtt+TCP_DELACK_MAX, 10ms) else:PTO=max(2rtt, 10ms) 注:TCP_DELACK_MAX = 200ms對(duì)應(yīng)到代碼里的tcp_schedule_loss_probe函數(shù):
bool tcp_schedule_loss_probe(struct sock *sk) {struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);u32 timeout, tlp_time_stamp, rto_time_stamp;u32 rtt = tp->srtt >> 3;if (WARN_ON(icsk->icsk_pending == ICSK_TIME_EARLY_RETRANS))return false;/* No consecutive loss probes. */if (WARN_ON(icsk->icsk_pending == ICSK_TIME_LOSS_PROBE)) {tcp_rearm_rto(sk);return false;}/* Don't do any loss probe on a Fast Open connection before 3WHS* finishes.*/if (sk->sk_state == TCP_SYN_RECV)return false;/* TLP is only scheduled when next timer event is RTO. */if (icsk->icsk_pending != ICSK_TIME_RETRANS)return false;/* Schedule a loss probe in 2*RTT for SACK capable connections* in Open state, that are either limited by cwnd or application.*///判斷是否開啟了TLP及一些觸發(fā)條件if (sysctl_tcp_early_retrans < 3 || !rtt || !tp->packets_out ||!tcp_is_sack(tp) || inet_csk(sk)->icsk_ca_state != TCP_CA_Open)return false;if ((tp->snd_cwnd > tcp_packets_in_flight(tp)) &&tcp_send_head(sk))return false;/* Probe timeout is at least 1.5*rtt + TCP_DELACK_MAX to account* for delayed ack when there's one outstanding packet.*///這個(gè)與上面描述的策略是一致的timeout = rtt << 1;if (tp->packets_out == 1)timeout = max_t(u32, timeout,(rtt + (rtt >> 1) + TCP_DELACK_MAX));timeout = max_t(u32, timeout, msecs_to_jiffies(10));/* If RTO is shorter, just schedule TLP in its place. */tlp_time_stamp = tcp_time_stamp + timeout;rto_time_stamp = (u32)inet_csk(sk)->icsk_timeout;if ((s32)(tlp_time_stamp - rto_time_stamp) > 0) {s32 delta = rto_time_stamp - tcp_time_stamp;if (delta > 0)timeout = delta;}//設(shè)置PTO定時(shí)器inet_csk_reset_xmit_timer(sk, ICSK_TIME_LOSS_PROBE, timeout,TCP_RTO_MAX);return true; }?PTO超時(shí)之后,會(huì)觸發(fā)tcp_send_loss_probe發(fā)送TLP包:
/* When probe timeout (PTO) fires, send a new segment if one exists, else* retransmit the last segment.*/ void tcp_send_loss_probe(struct sock *sk) {struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *skb;int pcount;int mss = tcp_current_mss(sk);int err = -1;//如果還可以發(fā)送新數(shù)據(jù),那么就發(fā)送新數(shù)據(jù)if (tcp_send_head(sk) != NULL) {err = tcp_write_xmit(sk, mss, TCP_NAGLE_OFF, 2, GFP_ATOMIC);goto rearm_timer;}/* At most one outstanding TLP retransmission. *///一次最多只有一個(gè)TLP探測(cè)包if (tp->tlp_high_seq)goto rearm_timer;/* Retransmit last segment. *///如果沒(méi)有新數(shù)據(jù)可發(fā)送,就重新發(fā)送最后的一個(gè)數(shù)據(jù)包skb = tcp_write_queue_tail(sk);if (WARN_ON(!skb))goto rearm_timer;pcount = tcp_skb_pcount(skb);if (WARN_ON(!pcount))goto rearm_timer;if ((pcount > 1) && (skb->len > (pcount - 1) * mss)) {if (unlikely(tcp_fragment(sk, skb, (pcount - 1) * mss, mss)))goto rearm_timer;skb = tcp_write_queue_tail(sk);}if (WARN_ON(!skb || !tcp_skb_pcount(skb)))goto rearm_timer;err = __tcp_retransmit_skb(sk, skb);/* Record snd_nxt for loss detection. */if (likely(!err))tp->tlp_high_seq = tp->snd_nxt; rearm_timer:inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,inet_csk(sk)->icsk_rto,TCP_RTO_MAX);if (likely(!err))NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_TCPLOSSPROBES);return; }發(fā)送TLP探測(cè)包后,在tcp_process_tlp_ack里判斷是否發(fā)生了丟包,做相應(yīng)的處理:
/* This routine deals with acks during a TLP episode.* Ref: loss detection algorithm in draft-dukkipati-tcpm-tcp-loss-probe.*/ static void tcp_process_tlp_ack(struct sock *sk, u32 ack, int flag) {struct tcp_sock *tp = tcp_sk(sk);//判斷這個(gè)包是否是tlp包的dup ack包bool is_tlp_dupack = (ack == tp->tlp_high_seq) &&!(flag & (FLAG_SND_UNA_ADVANCED |FLAG_NOT_DUP | FLAG_DATA_SACKED));/* Mark the end of TLP episode on receiving TLP dupack or when* ack is after tlp_high_seq.*///如果是dup ack,說(shuō)明沒(méi)有發(fā)生丟包,繼續(xù)當(dāng)前的流程if (is_tlp_dupack) {tp->tlp_high_seq = 0;return;}//否則,減窗,并進(jìn)入擁塞避免if (after(ack, tp->tlp_high_seq)) {tp->tlp_high_seq = 0;/* Don't reduce cwnd if DSACK arrives for TLP retrans. */if (!(flag & FLAG_DSACKING_ACK)) {tcp_init_cwnd_reduction(sk, true);tcp_set_ca_state(sk, TCP_CA_CWR);tcp_end_cwnd_reduction(sk);tcp_try_keep_open(sk);NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_TCPLOSSPROBERECOVERY);}} }TLP算法的設(shè)計(jì)思路還是挺好的,主動(dòng)提前發(fā)現(xiàn)網(wǎng)絡(luò)是否擁塞,而不是被動(dòng)的去依賴丟包來(lái)作為反饋。在大多數(shù)情況下是可以提高網(wǎng)絡(luò)傳輸?shù)男实?#xff0c;但在某些情況下可能會(huì)"適得其反",而本文遇到的問(wèn)題就是"適得其反"的一個(gè)例子。
問(wèn)題的解決回到我們的這個(gè)問(wèn)題上,如何確認(rèn)確實(shí)是由于TLP引起的呢?
繼續(xù)查看代碼可以看到,TLP的loss probe和loss recovery次數(shù),內(nèi)核都有相應(yīng)的計(jì)數(shù)器跟蹤。
既然有計(jì)數(shù)器就好辦了,復(fù)現(xiàn)的時(shí)候netstat -s就可以查看是否命中TLP了。寫了個(gè)腳本將結(jié)果寫入到文件里。
while true; do date +"%T.%6N" >> loss.log; netstat -s | grep Loss >> loss.log; done?
查看計(jì)數(shù)器增長(zhǎng)的情況,結(jié)合抓包文件來(lái)看,基本確認(rèn)肯定是命中TLP了。知道原因那就好辦了,關(guān)掉TLP驗(yàn)證一下應(yīng)該就可以解決了。
如上面介紹ER算法時(shí)提到,內(nèi)核提供了一個(gè)開關(guān),tcp_early_retrans可用于開啟和關(guān)閉ER和TLP,默認(rèn)是3(enable TLP and delayed ER),sysctl -w net.ipv4.tcp_early_retrans=2 關(guān)掉TLP,再次重新測(cè)試,發(fā)現(xiàn)問(wèn)題解決了:
窗口增加的很快,最終的ssthresh為941,下載速度4s+,也是符合預(yù)期,到此用戶的問(wèn)題已經(jīng)解決,但所有的疑問(wèn)都得到了正確的解答了嗎?
真正的真相雖然用戶的問(wèn)題已經(jīng)得到了解決,但至少還有兩個(gè)問(wèn)題沒(méi)有得到答案:
1. 為什么會(huì)每次都在握手完的前幾個(gè)包里就會(huì)觸發(fā)TLP?
2. 雖然觸發(fā)了TLP,但從抓包來(lái)看,已經(jīng)收到了尾包的dup ack包,那說(shuō)明沒(méi)有發(fā)生丟包,為什么還是進(jìn)入了擁塞避免?
先回答第一個(gè)問(wèn)題,根據(jù)文章最前面的網(wǎng)絡(luò)結(jié)構(gòu)圖可以看到,STGW是掛在TGW的后面。在本場(chǎng)景中,用戶訪問(wèn)的是TGW的高防VIP,高防VIP有一個(gè)默認(rèn)開啟的功能就是SYN代理。
syn代理指的是client發(fā)起連接時(shí),首先是由tgw代答syn ack包,client真正開始發(fā)送數(shù)據(jù)包時(shí),tgw再發(fā)送三次握手的包到rs,并轉(zhuǎn)發(fā)數(shù)據(jù)包。
在本例中,tgw的rs就是stgw,也就是說(shuō),stgw的收到三次握手包的rtt是基于與tgw計(jì)算出來(lái)的,而后面的數(shù)據(jù)包才是真正與client之間的通信。前面背景描述中提到,用戶同城訪問(wèn)(上海client訪問(wèn)上海的vip)也是沒(méi)有問(wèn)題的,跨城訪問(wèn)就有問(wèn)題。
這是因?yàn)橥窃L問(wèn)的情況下,tgw與stgw之間的rtt與client與stgw之間的rtt,相差并不大,并沒(méi)有滿足觸發(fā)tlp的條件。而跨城訪問(wèn)后,三次握手的數(shù)據(jù)包的rtt是基于與tgw來(lái)計(jì)算的,比較小,后面收到數(shù)據(jù)包后,計(jì)算的是client到stgw之間的rtt,一下子增大了很多,并且滿足了tlp的觸發(fā)條件
PTO=max(2rtt, 10ms)設(shè)置的PTO定時(shí)器超時(shí)了,協(xié)議棧認(rèn)為是不是由于網(wǎng)絡(luò)發(fā)生了擁塞,所以重傳了尾包探測(cè)一下查看是否真的發(fā)生了擁塞,這就是為什么每次都是在握手完隨后的幾個(gè)包里就會(huì)有重傳包,觸發(fā)了TLP的原因。
再回到第二個(gè)問(wèn)題,從抓包來(lái)看,很明顯,網(wǎng)絡(luò)并沒(méi)有發(fā)生擁塞或丟包,stgw已經(jīng)收到了尾包的dup ack包,按照TLP的原理來(lái)看,不應(yīng)該進(jìn)入擁塞避免的,到底是什么原因?qū)е碌摹0偎疾坏闷浣?#xff0c;只能再繼續(xù)啃代碼了,再回到tlp_ack的這一部分代碼來(lái)看。
只有當(dāng)is_tlp_dupack為false時(shí),才會(huì)進(jìn)入到下面部分,進(jìn)入擁塞避免,也就是說(shuō)這里is_tlp_dupack肯定是為false的。ack == tp->tlp_high_seq這個(gè)條件是滿足的,那么問(wèn)題就出在了幾個(gè)flag上面,看下幾個(gè)flag的定義:
#define FLAG_SND_UNA_ADVANCED 0x400 #define FLAG_NOT_DUP (FLAG_DATA|FLAG_WIN_UPDATE|FLAG_ACKED) #define FLAG_DATA_SACKED 0x20 /* New SACK.也就是說(shuō),只要flag包含了上面幾個(gè)中的任意一個(gè),都會(huì)將is_tlp_dupack置為false,那到底flag包含了哪一個(gè)呢?如何繼續(xù)排查呢?
調(diào)試內(nèi)核信息,最常用的工具就是ftrace及systemtap。
這里首先嘗試了ftrace,發(fā)現(xiàn)它并不能滿足我的需求。ftrace最主要的功能是可以跟蹤函數(shù)的調(diào)用信息,并且可以知道各個(gè)函數(shù)的執(zhí)行時(shí)間,在有些場(chǎng)景下非常好用,但原生的ftrace命令用起來(lái)很不方便,ftrace團(tuán)隊(duì)也意識(shí)到了這個(gè)問(wèn)題,因此提供了另外一個(gè)工具trace-cmd,使用起來(lái)非常簡(jiǎn)單。
trace-cmd record -p function_graph -P 3252 //跟蹤pid 3252的函數(shù)調(diào)用情況 trace-cmd report > report.log //以可視化的方式展示ftrace的結(jié)果并重定向到文件里下圖是使用trace-cmd跟蹤的一個(gè)例子部分截圖,可以看到完整打印了內(nèi)核函數(shù)的調(diào)用信息及對(duì)應(yīng)的執(zhí)行時(shí)間。
但在當(dāng)前的這個(gè)問(wèn)題里,主要是想確認(rèn)flag這個(gè)變量的值,ftrace沒(méi)有辦法打印出變量的值,因此考慮下一個(gè)強(qiáng)大的工具:systemtap。
systemtap是一個(gè)很強(qiáng)大的動(dòng)態(tài)追蹤工具,利用它可以很方便的調(diào)試內(nèi)核信息,跟蹤內(nèi)核函數(shù),打印變量信息等,很顯然它是符合我們的需求的。systemptap的使用需要安裝內(nèi)核調(diào)試信息包(kernel-debuginfo),但由于復(fù)現(xiàn)的那臺(tái)機(jī)器上的內(nèi)核版本較老,沒(méi)有debug包,無(wú)法使用stap工具,因此這條路也走不通。
最后,聯(lián)系了h_tlinux_Helper尋求幫助,他幫忙找到了復(fù)現(xiàn)機(jī)器內(nèi)核版本的dev包,并在tcp_process_tlp_ack函數(shù)里打印了一些變量,并輸出堆棧信息。重新安裝了調(diào)試的內(nèi)核,復(fù)現(xiàn)后打印了如下的堆棧及變量信息:
綠色標(biāo)記處的那一行,就是收到的dup ack的那個(gè)包,可以看到flag的標(biāo)記為0x4902,換算成宏定義為:
FLAG_UPDATE_TS_RECENT | FLAG_DSACKING_ACK | FLAG_SLOWPATH | FLAG_WIN_UPDATE再對(duì)照tcp_process_tlp_ack函數(shù)看一下,正是FLAG_WIN_UPDATE這個(gè)標(biāo)記導(dǎo)致了is_tlp_dupack = false。那在什么情況下,flag會(huì)被置為FLAG_WIN_UPDATE呢?
繼續(xù)看代碼,對(duì)端回復(fù)的每個(gè)ack包基本會(huì)進(jìn)入到tcp_ack_update_window函數(shù)。
看到這里flag被置為FLAG_WIN_UPDATE的條件是tcp_may_update_window返回true。
?
再看到tcp_may_update_window函數(shù)這里,after(ack_seq, tp->snd_wl1)?是基本都會(huì)命中的,因?yàn)椴还艽翱谟袥](méi)有變化,ack_seq都會(huì)比snd_wl1 大的,ack_seq都是遞增的,snd_wl1在tcp_update_wl中又會(huì)被更新成上一次的ack_seq。因此絕大多數(shù)的包的flag都會(huì)被打上FLAG_WIN_UPDATE標(biāo)記。
如果是這樣的話,那is_tlp_dupack不就是都為false了嗎?不管有沒(méi)有收到dup ack包,TLP都會(huì)進(jìn)入擁塞避免,這個(gè)就不符合TLP的設(shè)計(jì)初衷了,這里是否是內(nèi)核實(shí)現(xiàn)的Bug?
隨后我查看了linux 4.14內(nèi)核代碼:
發(fā)現(xiàn)從內(nèi)核版本linux 4.0開始,BUG就已經(jīng)被修復(fù)了,去掉了flag的一些不合理的判斷條件,這才是真正的符合TLP的設(shè)計(jì)原理。
到此,整個(gè)問(wèn)題的所有疑點(diǎn)才都得到了解釋。
總結(jié)本文從一個(gè)下載慢的線上問(wèn)題入手,首先介紹了一些常規(guī)的排查思路和手段,發(fā)現(xiàn)仍然不能定位到原因。然后分享了一個(gè)可以查詢每條連接的擁塞窗口命令,結(jié)合內(nèi)核代碼分析了TCP擁塞控制ssthresh的設(shè)計(jì)理念及混合慢啟動(dòng),ER和尾包探測(cè)(TLP)等優(yōu)化算法,并介紹了兩個(gè)常用的內(nèi)核調(diào)試工具:ftrace和systemtap,最終定位到是內(nèi)核的TLP實(shí)現(xiàn)BUG導(dǎo)致的下載慢的問(wèn)題,從內(nèi)核4.0版本之后已經(jīng)修復(fù)了這個(gè)問(wèn)題。
總結(jié)
以上是生活随笔為你收集整理的由STGW下载慢问题引发的网络传输学习之旅的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 打造轻量级可视化数据爬取工具-菩提
- 下一篇: 腾讯在信息流内容理解技术上的解决方案