歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux TCP擁塞控制中undo操作

Linux TCP擁塞控制中undo操作

日期:2017/3/1 12:01:31   编辑:關於Linux
Linux的TCP實現復雜且繁瑣,建議不要直接去看代碼,而是花點時間把TCP規范先撸一遍。本文主要描述一下TCP實現中undo操作,然後順便再吐一下槽(千萬不要覺得吐槽有什麼不好,很多好東西都是從吐槽開始的,從造紙,蒸汽機,到法蘭西第一共和國,再到Linux...)。

1.TCP對網絡擁塞是基於預測的

TCP屬於網絡的一部分,這無可厚非,但當一個人說自己精通網絡的時候,他更可能的意思是自己精通網絡節點的行為而不是端到端的行為。比如,一個Cisco的工程師精通各種路由協議,可以設計出復雜的OSPF網絡,BGP網絡,優化各個節點的pps,在某處啟用spanning tree,各種算法的靈活運用,不管在研發的標准還是在運維的標准,都足以讓你煙花缭亂,崇拜至極,好像網絡中的一切盡掌握在其手中,玩轉地不亦樂乎,我曾經接觸過這樣的人,也認識不少這樣的朋友,曾經站在研發的角度與其對峙,也曾站在他們的立場上置碼農的傷痛於不顧...這些人保證,只要數據包經過他們掌管的設備,在效率的意義上,他就可以保證其盡可能線速通過,在安全的意義上,任何邪惡的蛛絲馬跡都無法逃避出他們的法眼...

然而,一旦涉及TCP,他們就無能了。我一直都覺得,一個TCP連接的行為是“與全世界相關的”,沒有任何一個管理員可以控制全世界所有的計算機的行為,就算是神仙也不行。TCP只定義了端到端的行為,就好像假設兩個通信的進程是相鄰的一樣,TCP首先就是這樣假設的,這就是“端到端滑動窗口”的初衷,即發送多少數據僅僅和接收端可以接收多少有關。當發現端到端之間並不是直接相鄰而是一個變換莫測具有蝴蝶效應的網絡時,擁塞控制算法是被加進去的。

TCP在設計之初,並沒有任何的顯式擁塞提醒機制,也沒有否認應答機制,因此對於發送端而言,預測網絡情況的唯二工具就是ACK和RTT,並且,RTT是測量出來的,這就意味著它會受到接收端延遲發送ACK的影響,雖然TCP規范要求避免stretch ACK,但是如果ACK丟了,什麼也不好說...因此對於TCP網絡而言,擁塞是預測出來的,下面是現實中的類似預測的例子:
同居的室友去買菜,然後走了10個小時都沒有回來,你會怎麼想,你可能早就會打電話確認,可是如果什麼都沒有,你只能假設室友出事了,於是報警;
同居的室友和四個互不相識的鄰居前後一起去同一個市場買菜,四人回來很久了,先走的室友還沒有回來,此時你會怎麼想。
不管上面任何一種情況,都可能僅僅意味著室友玩了一個惡作劇而已...

2.當預測錯誤就是可以undo的

如果我們預測了網絡的擁塞,可是後來有跡象表明事實並不是我們想象的那樣,此時就可以undo那些為了應對網絡擁塞而做出的動作,問題是哪些可以undo,哪些不可以undo。

3.哪些可以undo

一旦我們預測了網絡的擁塞,我們需要做的有兩件事:
a).減少擁塞窗口
b).重發可能丟失的數據包(人去買菜沒有再回來,如果僅僅是為了菜的話,你可以再派一個人去買菜)。
減少擁塞窗口是為了不為既已擁塞的網絡添堵,這無疑是一種君子行為,不適合中國式司機,當然也不適合中國的TCP研發者們,雖然也會減窗,但初衷不是不添堵,而是不減窗發了數據也沒用,淨增加流量...當發現預測錯誤,網絡根本就沒有擁塞的時候,TCP最好的行為就是補償自己了,在一個“人人為我,我為人人”的世界,沒人會因為你的君子行為補償你,只有自己可以。因此可以將擁塞窗口升高至降窗之前的值,這也算是一種自我補償了。

4.哪些不可以undo

在預判斷為擁塞的時候,會重發可能丟失的數據包,這部分是不可以undo的,沒有任何機制可以將發出去的數據再收回來!唯一能做的,就是下次預測的時候再准確些吧。

5.什麼時候可以undo

我們知道,TCP可以通過reordering個重復ACK以及超時來預判斷數據包已經丟失,但是並不意味著發生了重復ACK或者超時一定是數據包丟失了,也可能是數據包發生了重排序,延遲亂序到達了接收端,因此重復ACK或者超時僅僅是數據包丟失的必要非充分條件。

有一種情況下,我們可以下結論說我們的預判斷是錯誤的,那就是我們重發的數據包均被對端證實收到了兩遍,這就是說一遍是原始的發送,一遍是重發。通過DSACK或者時間戳機制可以可以很好的發現這一點。一旦發現所有的重發包均收到了DSACK,我們就可以undo了,把可以undo的全都undo掉。

6.undo到哪裡

如果已經因為錯誤的預判斷進入了recovery狀態,那麼undo到哪裡呢?

如果在收到重發數據包的所有DSACK時,發送窗口已經足夠干淨了,那麼就可以直接到Open狀態了,但是如果在UNA之後依然有數據被標記為SACK或者LOST,這就表明依然有亂序的可能,那麼就進入Disorder狀態好了。我們知道,在recovery狀態,窗口是下降的,然而在Disorder狀態,窗口並不一定是僵住(僵住是為了在確認有足夠的重復ACK前,無法預判斷是亂序還是丟包,所以要先僵住窗口)的,如果我們在undo的時候,reordering的值已經變大,那麼就說明網絡更有可能是亂序了,此時可以繼續增加擁塞窗口的大小。

下圖是一個總體的圖,大意所在:
\

7.Linux內核協議棧的持續優化

如果你看2.6.8的內核和4.4的內核,會發現在TCP擁塞處理上有很大的不同,把2.6.32作為標准版的話,你會發現3.10版本增加了prr降窗算法,然後在緊隨的3.11,引入了“reordering大於默認值在Disorder狀態下可不必僵住而增加窗口”的算法,然後再看4.3/4.4,你會發現在擁塞狀態機中,Mid Switch這個東西事實上已經完全打破了NewReno的假設,這就是NewReno提前退出。並且看似Linux在持續優化的過程中,已經變得不再君子,突發不再被限制,搶道插隊時有發生...

8.一個刷重傳隊列的細節

這一節與本文大意無關,只是不想另外寫一篇了。
在RFC規范中,擁塞狀態下,應該首先發LOST數據包,然後發新數據包,最後“前向發送”cover(即High seq)之前既沒有標記LOST,也沒有SACK的數據包,RFC這麼規定是有根據的,背後的數學推理根本就不會在RFC中給出,RFC只給結論!請不要質疑這種方案的合理性,請不要!
但是如果你看tcp_xmit_retransmit_queue的時候,很難發現這個邏輯:
    tcp_for_write_queue_from(skb, sk) {
        __u8 sacked = TCP_SKB_CB(skb)->sacked;

        if (skb == tcp_send_head(sk))
            break;
        if (hole == NULL)
            tp->retransmit_skb_hint = skb;
        // 窗口限制檢查
        if (tcp_packets_in_flight(tp) >= tp->snd_cwnd)
            return;

        if (fwd_rexmitting) {
begin_fwd:
            // 最高前向發送到high sacked
            if (!before(TCP_SKB_CB(skb)->seq, tcp_highest_sack_seq(tp)))
                break;

        } else if (!before(TCP_SKB_CB(skb)->seq, tp->retransmit_high)) {
            tp->retransmit_high = last_lost;
            // 關鍵在這裡!tcp_can_forward_retransmit會判斷,如果有新數據就緒,就會break!
            // 進入tcp_can_forward_retransmit之後,會有tcp_may_send_now判斷,其判斷是否有
            // 新數據就緒。Linux並沒有在一個路徑按RFC規范的發送優先級進行發送,在有新數據
            // 就緒的時候,只是簡單的退出,讓發送路徑自行處理!
            if (!tcp_can_forward_retransmit(sk))
                break;
            if (hole != NULL) {
                skb = hole;
                hole = NULL;
            }
            // 只有在tcp_can_forward_retransmit不會break的時候,才會“前向發送”
            fwd_rexmitting = 1;
            goto begin_fwd;

        } else if (!(sacked & TCPCB_LOST)) {
            if (hole == NULL && !(sacked & (TCPCB_SACKED_RETRANS|TCPCB_SACKED_ACKED)))
                hole = skb;
            continue;

        } else {
            // 首先發送LOST數據
            last_lost = TCP_SKB_CB(skb)->end_seq;
        }
        // 不會發送已經被SACK,或者已經被重傳的數據包!
        if (sacked & (TCPCB_SACKED_ACKED|TCPCB_SACKED_RETRANS))
            continue;

        tcp_retransmit_skb(sk, skb);
    }

9.再次針對OpenSSL/OpenVPN/Linux TCP的吐槽

雖然目前不再從事OpenSSL/OpenVPN的工作,但是我依然在持續關注,依然無法容忍這種讓人看不懂,即便看懂了也不爽的代碼,如果有人能教我看懂了OpenSSL的ASN.1處理的代碼,我願意發 100塊錢的紅包!OpenVPN也一樣!對於TCP的代碼,請看下面的片段:
/* People celebrate: "We love our President!" */
static int tcp_try_undo_recovery(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
....
哪位大神可以解釋一下“People celebrate: "We love our President!"”的意義!我敢說,如果我在自己的代碼中寫這樣的注釋,一定會被認為是抿了兩口二鍋頭後的傑作,然後被批,被人認為是不正常。曾經我在設計conntrack cache的時候,在代碼中寫了一句“旋轉升降座椅一定會爆炸”,然後我刪了,我覺得這不符合我們這個國家中規中矩的文化,於是我刪了。在我們的文化裡,不會有“talk is cheap show me the code”,因此也就不會有哈格裡夫斯,不會有瓦特,不會有喬布斯,不會有Linus...只會有誰呢?反正就是一堆人說了算,抑揚頓挫,從小被灌輸,然後長大灌輸別人。爆炸!
Copyright © Linux教程網 All Rights Reserved