1. 程式人生 > >徹底實現Linux TCP的Pacing傳送邏輯-高精度hrtimer版

徹底實現Linux TCP的Pacing傳送邏輯-高精度hrtimer版

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

                程式碼的實現是簡單的,背後的思緒是複雜的。
        如果單純的將《 徹底實現Linux TCP的Pacing傳送邏輯-普通timer版
》中的timer_list換成hrtimer,必然招致失敗。因為在hrtimer的function中,呼叫諸如tcp_write_xmit這樣的長路徑函式是一種用絲襪裝榴蓮的行為。好吧,在無奈中我只能參考TSQ的做法。舊恨心魔!
在Linux的TCP實現中,TSQ保證了一個單獨的流不會過多地佔據傳送快取,從而保證的多個數據流的相對公平。這個機制是用tasklet實現的,那麼我覺得TCP的pacing也能通過tasklet來實現。
-------------------------------------
TCP毀了整個網路世界的和諧!Why?
        是誰說TCP傳送端一定要維護一個擁塞視窗了?這人樹立了權威!然而“擁塞視窗”這個概念只說明瞭事情的一個方面而對另一個方面隻字未提!我說過,擁塞視窗是一個標量而不是一個向量,這樣說的含義在於,它僅僅是在一個維度上度量的一個數值而已,表示“目前可以傳送的資料量”,僅此而已。擁塞視窗根本不懂網路上發生了什麼。不過可以肯定的是,接收端觀察到的資料到達行為是真實的,即兩個資料包到達之間是有間隔的!如果你分別在傳送端和接收端抓包並分析,將會很容易看到這個事實。
        我們再看傳送端,以Linux為例,其傳送行為是tcp_write_xmit主持的,它會一次性發送所有可供傳送的資料包,每個資料包之間的延遲僅僅是資料包走一趟協議棧的主機延遲,這種延遲對於長肥網路延遲是可以忽略不計的!請我們不要被千兆/萬兆乙太網以及DC內部的假象所矇蔽,我們在網際網路上訪問的內容大多數來自“千里之外”,中間的路途崎嶇坎坷,不要相信什麼CDN,全都扯淡,商業宣傳的噱頭,沒個鳥用。回到正文,既然傳送端是一次性突發傳送的資料包,而接收端是間歇性接收到資料包,中間一定發生了什麼!是的,這就是根本,但是我們不用關心這個,只要好好跪舔TCP就好了。我們注意一種類似的現象,那就是結婚的婚車,接新娘出發的時候,幾十輛德系BBA(賓士,寶馬,奧迪)由一輛“超級豪車”(比如改裝的賓利)領銜,一字排開列隊,集體統一出發,可是在快到達新娘子家的時候,頭車必須停下來等待後車,以營造一種一路上隊形持續保持並且今天太陽為你升起的假象,這就是流量整形的作用,首先,車隊經由的道路是一個統計複用的系統,並沒有賣給哪個個人,加上路上紅綠燈,插隊,交通擁堵等因素,車隊會被打散,本來前後車僅僅相隔幾米,最終這個距離會拉開到以公里計算,快要到達目的地的時候,為了以出發時的陣容到達,就必須等待所有的車輛到齊,然後一字排開進入新娘子家。其實這些在網絡卡資料傳輸技術中,都有對應的東西,比如最後那個等車到齊的行為,其實就是LRO(large-receive-offload)或者分片重組之類的。
        我要說的不是這個,我要說的是,你知道你結個婚搞這麼個車隊,給交通帶來多大影響嗎?!我們假設所有人都是毫無感性的,誰也不讓誰,但你能想象這麼大的一字車隊從一個小巷子裡開出的場景嗎?令人遺憾的是,我們的網際網路上幾乎每一臺可以傳送資料的主機(不管是DC的機器,還是你我的電腦),每時每刻都在有這麼大陣容的車隊出發!難道就不能互相謙讓一下,在起碼在兩車之間拉開一輛車的距離,至少過十字路口的時候,有別的車輛可以交叉通過啊!但事實上,在中國,傻逼才會這麼做,劣幣驅良幣,因為沒人這麼做!大家都恨不得讓車隊更長些呢!
        在網路上,雖然TCP的擁塞視窗指示了可以傳送多少資料,但是為什麼不能按照接收端實際接收的行為來指導傳送行為呢?為什麼幾十年來都是一次性發送並依然如故呢?至少Linux的TCP是這樣的,我相信別的也好不到哪去,畢竟“作惡的,必被剪除”只是一個心願。劣幣驅良幣,大家都作惡,惡就成了善。
        這就是TCP的悲哀!Google的工程師看到了這種悲哀,造出了BBR演算法,引無數人跪舔,這更悲哀。Google的BBR patch如是說:
The primary control is the pacing rate: BBR applies a gain
multiplier to transmit faster or slower than the observed bottleneck
bandwidth. The conventional congestion window (cwnd) is now the
secondary control; the cwnd is set to a small multiple of the
estimated BDP (bandwidth-delay product) in order to allow full
utilization and bandwidth probing while bounding the potential amount
of queue at the bottleneck.

總結一下本段的抱怨。只用擁塞視窗控制TCP的傳送行為是一個垃圾方式,都是雞屎。有破才有立,我決定實現Linux TCP pacing的hrtimer版了。
-------------------------------------
總的框架如下:




看起來,前面實現的常規timer版本的TCP pacing僅僅是一個引子,本文將要實現的基於tasklet的hrtimer版本才是王道!上述框架理解了之後,實現起來就是信手拈來的事了。非常之簡單。
-------------------------------------
還是跟普通timer版一樣,我把程式碼拆解成幾個部分列如下,然而這並不是程式碼的全部,我省掉了一些諸如list_head初始化的程式碼,以及一些變數初始化的程式碼。

1.tasklet的實現:

// 定義pacing_tasklet:/* include/net/tcp.h */struct pacing_tasklet {        struct tasklet_struct   tasklet;        struct list_head        head; /* queue of tcp sockets */};extern struct pacing_tasklet pacing_tasklet;/* net/ipv4/tcp_output.c */// 定義per cpu的tasklet變數DEFINE_PER_CPU(struct pacing_tasklet, pacing_tasklet);// 獨立出來的handler,僅僅為了與tasklet的action分離,使其不至於太長static void tcp_pacing_handler(struct sock *sk){        struct tcp_sock *tp = tcp_sk(sk);        if(!sysctl_tcp_pacing || !tp->pacing.pacing)                return ;        if (sock_owned_by_user(sk)) {                if (!test_and_set_bit(TCP_PACING_TIMER_DEFERRED, &tcp_sk(sk)->tsq_flags))                        sock_hold(sk);                goto out;        }        if (sk->sk_state == TCP_CLOSE)                goto out;        if(!sk->sk_send_head){                goto out;        }        tcp_push_pending_frames(sk);out:        if (tcp_memory_pressure)                sk_mem_reclaim(sk);}// pacing tasklet的action函式static void tcp_pacing_func(unsigned long data){        struct pacing_tasklet *pacing = (struct pacing_tasklet *)data;        LIST_HEAD(list);        unsigned long flags;        struct list_head *q, *n;        struct tcp_sock *tp;        struct sock *sk;        local_irq_save(flags);        list_splice_init(&pacing->head, &list);        local_irq_restore(flags);        list_for_each_safe(q, n, &list) {                tp = list_entry(q, struct tcp_sock, pacing_node);                list_del(&tp->pacing_node);                sk = (struct sock *)tp;                bh_lock_sock(sk);                tcp_pacing_handler(sk);                bh_unlock_sock(sk);                clear_bit(PACING_QUEUED, &tp->tsq_flags);        }}// 初始化pacing tasklet(完全學著tsq的樣子來做)void __init tcp_tasklet_init(void){        int i,j;        struct sock *sk;        local_irq_save(flags);        list_splice_init(&pacing->head, &list);        local_irq_restore(flags);        list_for_each_safe(q, n, &list) {                tp = list_entry(q, struct tcp_sock, pacing_node);                list_del(&tp->pacing_node);                        sk = (struct sock *)tp;                bh_lock_sock(sk);                tcp_pacing_handler(sk);                bh_unlock_sock(sk);                clear_bit(PACING_QUEUED, &tp->tsq_flags);        }}

2.hrtimer相關:
/* net/ipv4/tcp_timer.c */// 重置hrtimer定時器void tcp_pacing_reset_timer(struct sock *sk, u64 expires){        struct tcp_sock *tp = tcp_sk(sk);        u32 timeout = nsecs_to_jiffies(expires);        if(!sysctl_tcp_pacing || !tp->pacing.pacing)                return;        hrtimer_start(&sk->timer,                      ns_to_ktime(expires),                      HRTIMER_MODE_ABS_PINNED);}// hrtimer的超時回撥static enum hrtimer_restart tcp_pacing_timer(struct hrtimer *timer){        struct sock *sk = container_of(timer, struct sock, timer);        struct tcp_sock *tp = tcp_sk(sk);        if (!test_and_set_bit(PACING_QUEUED, &tp->tsq_flags)) {                unsigned long flags;                struct pacing_tasklet *pacing;                // 僅僅排程起tasklet,而不是執行action!                local_irq_save(flags);                pacing = this_cpu_ptr(&pacing_tasklet);                list_add(&tp->pacing_node, &pacing->head);                tasklet_schedule(&pacing->tasklet);                local_irq_restore(flags);        }        return HRTIMER_NORESTART;}// 初始化void tcp_init_xmit_timers(struct sock *sk){        inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,                                  &tcp_keepalive_timer);        hrtimer_init(&sk->timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_PINNED);        sk->timer.function = &tcp_pacing_timer;}
3.tcp_write_xmit中的判斷:
/* net/ipv4/tcp_output.c */static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,                           int push_one, gfp_t gfp){        ...        while ((skb = tcp_send_head(sk))) {                unsigned int limit;                u64 now = ktime_get_ns();                ...                cwnd_quota = tcp_cwnd_test(tp, skb);                if (!cwnd_quota) {                        if (push_one == 2)                                /* Force out a loss probe pkt. */                                cwnd_quota = 1;                        else if(tp->pacing.pacing == 0) // 這裡是個創舉,既然pacing rate就是由cwnd算出來,檢查了pacing rate就不必再檢測cwnd了,但是在bbr演算法中要慎重,因為bbr的pacing rate真不是由cwnd算出來的,恰恰相反,cwnd是由pacing算出來的!                                break;                }                // 通告視窗與網路擁塞無關,還是要檢測的。                if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))                        break;                // 這裡的邏輯與普通timer版的一樣!                if (sysctl_tcp_pacing && tp->pacing.pacing == 1) {                        u32 plen;                        u64 rate, len;                        if (now < tp->pacing.next_to_send) {                                tcp_pacing_reset_timer(sk, tp->pacing.next_to_send);                                break;                        }                        rate = sysctl_tcp_rate ? sysctl_tcp_rate:sk->sk_pacing_rate;                        plen = skb->len + MAX_HEADER;                        len = (u64)plen * NSEC_PER_SEC;                        if (rate)                                do_div(len, rate);                        tp->pacing.next_to_send = now + len;                        if (cwnd_quota == 0)                                cwnd_quota = 1;                }                if (tso_segs == 1) {        ...}

4.tcp_release_cb中執行
/* net/ipv4/tcp_output.c */void tcp_release_cb(struct sock *sk){        ...        if (flags & (1UL << TCP_PACING_TIMER_DEFERRED)) {                if(sk->sk_send_head) {                        tcp_push_pending_frames(sk);                }                __sock_put(sk);        }        ...}

以上4個部分就是幾乎全部的邏輯了。
-------------------------------------
現在看看效果,使用netperf的結果我就不貼了,我只貼一個使用curl下載10M檔案的對比結果。

首先看標準cubic演算法的曲線:


CTMB,垃圾!都他媽的是垃圾!


其吞吐量曲線如下圖所示




然後再看我的pacing曲線:

然後再看看吞吐量的圖!我雖然沒有上過大學,其實我也是不屑於大學的,我的圈子裡,都是碩博連讀的,好久不回一次國,而我,不知本科為何?!那麼看看結果吧:


-------------------------------------

最後看看我最初的願景。
        我的想法並不是要在TCP上搞什麼pacing,整個TCP,如果你想改變點什麼的話,在中國就是個醜行,因為中國人根本不懂真正的博弈。實際上我的目標是UDP之上承載的VPN流量!因為相比一個普通的TCP資料包,一個VPN資料包被丟掉的代價太大了。這部僅僅意味著網路頻寬被浪費,由於重傳還會把CPU拉入泥潭!上週寫了一個基於DTLS的VPN,在我的測試中,CPU一直飆高,後來查出來是因為丟包太嚴重導致,然後在使用者態實現了一個pacing傳送,問題就解決了。
        UDP當然可以在使用者態實現pacing,而TCP卻不能,因為TCP的傳送並不受使用者的控制,所以就想到了這個方案並簡單實現了個Demo。然而多多少少讓人覺得我身在曹營心在漢,其實則不然,我其實身在曹營心也在曹營,只是哀其不幸,而怒其不爭。

           

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述