多核心Linux內核路徑優化的不二法門之-多核心平臺TCP優化

本文能夠做爲《Linux轉發性能評估與優化(轉發瓶頸分析與解決方案)》 的姊妹篇,這兩篇文章結合在一塊兒,剛好就是整個Linux內核協議棧的一個優化方案。事實上Linux協議棧原本就是面向兩個方向的,一個是轉發,更多的 是本地接收。目前大量的服務器採用Linux做爲其載體,更加體現了協議棧本地處理相對於轉發的重要性,所以本文就這個問題扯兩句,歡迎拍磚!
算法

0.聲明:

0).關於來源

昨天就答應皮鞋廠老闆了,只是昨晚心情太複雜,本文沒有趕出來,今天在飛機上寫下了剩餘的...
編程

1).關於Linux原生代碼

本文假設讀者已經對Linux的TCP實現源碼有了足夠清晰的理解,所以不會大量篇幅分析Linux內核關於TCP的源代碼,好比tcp_v4_rcv的流程之類的。緩存

2).關於個人優化代碼

因爲涉及到不少複雜的因素,本文不提供完整的可編譯的優化源碼(整個源碼並不是我一人完成,在未經同夥贊成以前,我不敢擅做主張,可是想法是個人,因此我能展現的只是原理以及我負責的那部分代碼)。性能優化

3).關於TCP協議

本文不談TCP協議自己以及其細節(細節請參考RFC以及各種論文,好比各類流控,擁控算法之類的),僅包含的內容是TCP協議以外的框架實現的優化。服務器

1.Linux的TCP實現

1.1.Linux的TCP實如今協議層面分爲兩個部分

1).鏈接握手處理

TCP首先會經過三次握手創建一個鏈接,而後就能夠傳輸數據了。TCP規範並無指定任何的實現方式,當前的socket規範只是其中一種而已。Linux實現了BSD socket規範。
       在Linux中,TCP的鏈接處理和數據傳輸處理在代碼層面是合併在一塊兒的。
數據結構

2).數據傳輸處理

這個比較簡單,略。
架構

1.2.Linux的TCP在系統架構方面分爲兩個部分

1).軟中斷協議棧處理

Linux內核在軟中斷環境中進行協議棧的處理,在這個處理流程的最上方,會有3個分支:直接將skb複製到用戶緩衝區,簡單將skb排入到prequeue隊列,簡單將skb排入backlog隊列。
併發

2).用戶進程處理

Linux的socket API的處理是在用戶進程上下文中進行的。經過1.1節,咱們知道因爲代碼層面上這些都是合併在一塊兒的,所以一個socket會被各類執行流操做,直觀的考慮,這須要大量鎖的開銷。
負載均衡

1.3.鏈接處理的整體框圖

我給出一個鏈接處理整體框圖,其中紅線表示發生競爭的地方,而正是這些地方阻止了TCP鏈接的並行處理,圖示以下:
框架


wKiom1YCwKDwjPEPAAP-UGKZW0I767.jpg


我來一一解釋這些紅線的意義:

1號紅線:

因爲用戶進程和協議棧操做的是同一個socket,若是用戶進程正在copy數據包數據,那麼協議棧就要中止一樣的操做,反過來也同樣,所以須要暫時鎖定該socket,然而這種大鎖的開銷過於大,所以Linux內核協議棧的實現採用了一個更加優雅的方式。
協 議棧鎖定socket:因爲軟中斷處理協議棧,它可能運行在硬中斷以後的任意上下文,所以不能睡眠,故而必須是一把自旋鎖slock,由socket自己 保有,該鎖不只僅保護和用戶進程之間的競態,也保護不一樣CPU上對同一個socket協議棧操做之間的競態(很常見,一個偵聽socket上能夠同時到達 不少鏈接請求[可悲的是,這些請求不能同時被處理!!])。

用戶進程鎖定socket:用戶進程是能夠隨時睡眠的,所以能夠採用非 自旋鎖xlock來保護多個進程之間的競態,然而同時又爲了和內核協議棧操做同一個socket的軟中斷互斥,所以在獲取xlock以前,首先要獲取該 socket的slock,當獲取xlock以後或者暫時沒有得到xlock要睡眠的時候,將slock釋放掉。相關的邏輯以下:


stack_process
{
    ...
    spin_lock(socket->slock); //1
    process(skb);
    spin_unlock(socket->slock);
    ...
}


user_process
{
    ...
    spin_lock(socket->slock); //2
    while(true) 
    {
        ...
        spin_unlock(socket->slock);
        睡眠;
        spin_lock(socket->slock); //2
        if (佔據xlock成功)
        {
            break;
        }
    }
    spin_unlock(socket->slock);
    ...
}


可見,Linux採用了以上的方式很完美的解決了兩類問題,第一類問題是操做socket的執行流之間的同步與互斥,第二類問題時軟中斷上下文和進程上下文之間的鎖的不一樣。       在理解了socket鎖定以後,咱們來看下backlog這個隊列是幹什麼的。其實很簡單,就是將skb推到當前正佔據socket的那個進程的一個隊列 裏面,等到進程完成任務,準備釋放socket佔有權的時候,若是發現該隊列裏面有skb,那麼在其上下文中處理它們。這其實是一種職責轉移,這個轉移 也能夠帶來一些優化效果,那就是直接在socket所屬的用戶進程上下文處理skb,這樣就避免了一部分cache刷新。

2號紅線:

這 條線在1號紅線的解釋中已經涉及了,主要就是上述代碼邏輯中的1和2之間的競爭。這個競爭不是最激烈的,本質上它們屬於縱向的競爭,一個內核態軟中斷和一 個進程上下文之間的競爭,在同一個CPU上,通常而言,這類競爭的機率很低,由於同一個CPU同時只能執行一個執行流,假設此時它在內核態執行軟中斷,那 麼用戶態的進程,它必定在睡眠或者被搶佔,好比在accept中睡眠。
       用戶態處理和內核態處理,這種縱向的競爭在單CPU上幾乎不會發生,而用戶態的xlock根本就是爲了解決用戶進程之間的競爭,內核經過一個 backlog在面對這種競爭時轉移了數據包處理職責,事實上在xlock上並不存在競爭,backlog的存在反而帶來了一點優化效果。

3號紅線(結合3'號紅線):

該 紅線爲了解決多個用戶進程之間的競爭。之因此畫出的是TCP鏈接處理圖而不是數據傳輸處理圖,是由於鏈接圖更能體現問題,在服務器端,一個主進程fork 出來N多的子進程或者建立多個線程同時在一個繼承下來的socket上accept,這幾乎成了服務器設計的準則,那麼這多個進程/線程同時到達這個 slock的時候,爭搶就會很激烈。再看3'號紅線,咱們發現針對同一個socket的accept必須排隊進行。
       若是你看Linux TCP的實現,你會發現大量的數據結構操做都沒有用鎖,事實上,進入到那裏的,只有你一人,早在入口就排隊了。
       在單CPU上,這不會產生什麼後果,由於單CPU即使在最早進的分時搶佔調度器的協調下,本質上也是一個排隊模型,充其量能夠插隊罷了,TCP實現的slock和xlock只是規範了這個排隊規則而已。然而在多CPU上,仔細想一想這有必要嗎?

4號紅線:

說 完了用戶態多個進程/線程同時在同一個socket上accept時的排隊,如今看看內核協議棧的處理,也好不到哪裏去。若是多個CPU同時被鏈接請求中 斷,大量的請求針對同一個偵聽socket,那麼你們就要在4號紅線處排隊!而這個狀況在單CPU時幾乎並不存在,根本不可能同時有多個CPU執行到同一 個地點...

2.多核CPU架構下Linux TCP鏈接處理性能瓶頸

經過以上針對幾條紅線的描述,到此爲止,我已經 展現了幾個瓶頸,這些點都是要排隊的點。以上的那個框圖事實上很是好,這些排隊點也無可厚非,由於設計一個系統,就是要自包容解決全部問題,競爭只要存 在,就必定要經過排隊來解決它,所以上述的框架根本不用更改,鎖仍是留在原處。問題的根本不是鎖的存在,問題的根本在於鎖的不易獲取!
       TCP的處理運行在各個CPU上,CPU被看成了一種資源,這是操做系統的核心概念,可是反過來呢?若是把CPU當成服務者自己,而socket當成資 源,問題就迎刃而解了。這個思路很重要,我在nf-HiPAC中第一次接觸到了它,原來能夠把match和rule顛倒過來玩,而後以這個思想作指導我設 計了DxR Pro++,如今仍是這個思想,TCP的優化依然能夠這麼作。

3.Linux TCP鏈接處理優化

總體考慮一種鏈接處理被優化後的TCP實現,我只關心從accept API中會返回什麼。只要我能快速獲取一個客戶socket,而且不破壞listen,bind這些API自己,修改其實現是必須的。

3.1.縱向拆分socket

我將一個listen socket拆分紅了兩個部分,上半部和下半部,上半部對應用戶進程,下半部對應內核協議棧。原始的socket是下圖的樣子:


wKioL1YCwLaz35MDAACvilSeHdM750.jpg


個人socket被改爲了下面的樣子:


wKiom1YCwMWyHn0_AAGuvbNVH3I938.jpg

3.1.1.消解1號紅線和2號紅線

一個socket的上半部和下半部只經過一個accept隊列關聯,事實上即便在上半部正在佔有socket的時候,下半部依然能夠繼續處理。
       然而,事實證實,在執行accept的進程綁定CPU的狀況下,1號紅線的消解並無預期的性能提高,而2號紅線的消解帶來的性能提高影響也不大。若是是不綁定CPU,性能提高反而比綁定更好,看來橫向和縱向並非獨立的兩塊,它們會相互影響。

3.2.橫向拆分socket

橫 向拆分的思路就是將一個socket下半部拆成多個,每個CPU上強制綁定一個,相似softirqd每個CPU上一個同樣,這樣能夠消解4號紅線。 可是爲什麼不拆解socket的上半部呢?我一開始的想法是讓進程自行決定,後來以爲一樣也要強行綁定一個,至於說什麼用戶進程能夠自行解除綁定之類的,加 一個層次隱藏掉Per CPU socket便可。此時的用戶進程中的文件描述符只是指示一個socket描述符,而該描述符真正指向的是nr_cpus個Per CPU socket,以下圖所示:


wKiom1YCwNTz0uYZAAEtGMds62M374.jpg


這樣3號紅線,3'號紅線,4號紅線所有消解。事實上,看到這裏好像一切都大功告成了,可是測試的時候,發現還有兩個問題,接下來我會在描述數據結構的拆解中描述這兩個問題。

3.3.全局Listeners哈希表

因爲Listener會複製nr_cpus份到每個CPU上,故而全部的Listeners會被加入到每個CPU的本地哈希表中,這種以空間換無鎖並行是值得的,由於服務器上並不會出現海量的偵聽服務。

3.4.本地Accept隊列和全局Accept隊列

如 果系統中每個CPU上都綁定有accept進程,那麼能夠保證全部的鏈接請求都會被特定的進程處理,然而若是有一個CPU上沒有綁定任何accept進 程,那麼被排隊到該CPU的Accept隊列的客戶socket將不會返回給任何進程,從而形成客戶socket餓死。
       所以引入一個全局Accept隊列。相關的代碼邏輯以下:

stack_enqueue_socket
{
    if (本CPU上沒有綁定任何與該Listener相關的用戶進程) {
        spin_lock(g_table->slock);
        enqueue_global(g_table, cli_socket);
        spin_unlock(g_table->slock);
    } else {
        local_irq_save
        enqueue_local(per_cpu(table), cli_socket);
        local_irq_restore
    }
}


user_dequeue_socket_for_accept
{
    if (當前進程沒有綁定到當前CPU) {
        spin_lock(g_table->slock);
        cli_socket = dequeue_global(g_table);
        spin_unlock(g_table->slock);
    } else {
        local_irq_save
        cli_socket = dequeue_local(per_cpu(table));
        local_irq_restore
    }
    return cli_socket;
}



事實上,能夠看到,全局的Accept隊列是專門爲那些頑固不化的進程設置的,可是仍是能夠以一把小鎖的代價換來性能提高,由於鎖的粒度小多了。

3.5.軟中斷CPU分發問題

由 於每個CPU上綁定了一個Listener socket的下半部,而且幾乎全部的數據結構都是本地維護的,CPU正式成了TCP的一部分。所以必須保證一件事,那就是3次握手必須由一個CPU處 理,不然就會出錯。而因爲現在不少啓動了irqbalance的系統,中斷可能會分發到不一樣的CPU,好比來自特定客戶端的SYN被CPU0處理,而3次 握手中的ACK則被CPU1處理,這就形成了錯誤。爲了不這個局面,底層必須維護數據流和CPU之間的映射。這個能夠經過RFS的技術來解決。

       當第一個SYN包到達之時,處理這個鏈接握手過程的CPU就肯定了,就是當前的CPU,這個和用戶態的進程「跳」到哪一個CPU上沒有任何關係。所以實現比RFS要簡單得多。代碼邏輯以下:

netif_receive_skb
{
    ...
    hash = myhash(skb)
    cpu = get_hash_cpu(hash);
    if (cpu == -1)
    {  
        record_cpu_hash(hash, cpu);
    } else if (current_cpu != cpu)
    {  
        enqueue_to_backlog(skb, cpu);
        此後由IPI觸發中斷,被別的CPU處理
    } else
    {  
        正常的接收邏輯
    }  
    ...
}


示意圖以下:

wKioL1YCwOny0GdMAAE-0Ynk7Co743.jpg

3.6.與REUSEPORT以及fastsocket的關係

我 的這個優化版本和REUSEPORT沒有任何關係,REUSEPORT是google的一個patch,很是好用,在咱們的一個產品中就用到了這個技術, 它能夠作到在多個偵聽相同IP地址和端口的不一樣socket之間作到負載均衡,然而這個負載均衡的效果則徹底取決於hash算法。事實上Sina的 fastsocket就是在REUSEPORT之上作的優化,其主要作了CPU Affinity方面的優化,效果很不錯。類似的優化還有RPS/RFS patch,然而fastsocket更進一步,它不光延續了RPS/RFS的成果,並且把鏈接性能瓶頸也解決了,其主要的作法就是採用CPU綁定技術對 一個Listen socket在REUSEPORT的基礎上作了橫向拆分,複製了多個Listen socket來reuseport。
       fastsocket的作法是用戶進程本身決定是否要綁定CPU,所以哪一個CPU處理哪些數據包是經過用戶進程設置的,而個人作法則正好相反,個人優化從 下到上,我是由第一個SYN包剛好中斷的那個CPU做爲其握手過程後續被處理的CPU,和用戶進程的設置無關,即使用戶進程沒有綁定CPU,它也充其量只 是從全局Accept隊列小代價取客戶socket而已,而若是綁定了CPU,那幾乎是全線無鎖操做。結合3.5小節,咱們看下fastsocket是怎 麼得到對應處理該數據包的CPU的:


netif_receive_skb
{
    ...
    socket = inet_lookup(skb);
    cpu = socket->sk_affinity;
    if (current_cpu != cpu)
    {
        enqueue_to_backlog(skb, cpu);
        此後由IPI觸發中斷,被別的CPU處理
    } else
    {
        正常的接收邏輯
    }
    ...
}


fastsocket有一個查socket表的操做,若是是鏈接處理,對於綁定CPU的socket,能夠在本地表 中獲取到。若是你以爲fastsocket在這裏平添了一次查找,那你就錯了,事實上這是fastsocket的另外一個優化點,即Direct TCP,也就是說在這個位置就查找具體的socket,連路由之類的都放進去,後續的全部查找結果均可以放入,也就能夠實現一次查找,屢次使用了。

       Direct TCP的處理看似不符合協議棧分層處理的規則,在如此的底層處理四層協議,事實上在某些狀況也會所以而付出性能代價:
1).若是有大量的包是轉發的呢?
2).若是有大量的包是UDP的呢?
3).若是是***包呢?
4).若是這些包是接下來要被Netfilter刷掉的呢?
...
這有點像另外一個相似的Direct技術,即socket的busy poll。想得到甚高的性能,你必須對你的系統的行爲足夠理解,好比你知道這臺服務器就是專門處理TCP代理請求的,那麼開啓Direct TCP或者busy poll就是有好處的。
       而我認爲個人這個優化方案是一個更加通用的方案,實際上只有單點微調,且不會涉及別的狀況,只要你能保證數據包到達的中斷行爲是優雅的,後面的處理都是水 到渠成的,徹底自動化。就算是面對了上述4種非正常數據包,一個hash計算的代價也很小,且不須要面臨RCU鎖的內存屏障致使的刷流水問題。現現在,隨 着多隊列網卡以及PCI-E MSI的出現和流行,有不少技術能夠保證:
1).同一個元組標識的流的數據包到達後老是中斷同一個CPU;
2).處理不一樣的流的CPU上的負載很是均衡。

只要作到以上2點,其它的事情就不用管了,對於TCP的鏈接包,數據包會在同一個CPU一路向上一路無鎖到達本地Accept隊列或者全局Accept隊列,接下來會轉向另一條一樣寬擁有一樣多車道的高速公路,這條新的公路由進程操縱。

最終,優化過的TCP鏈接處理框圖以下:


wKioL1YCwPnxSohFAAOlSbiREOI097.jpg


PS:個人這個方案不須要修改應用程序,也無須連接任何庫,徹底替換內核便可。目前對fastopen並不支持。

對於TCP的非握手包的處理,應該怎麼優化呢?

4.Linux TCP數據處理優化

以 前,咱們覺得TCP典型的C/S處理的服務端是一個一對多的模型,所以listen+accept+fork這種流行的socket編程模型幾乎一致持續 到如今。期間衍生出各類MPM技術。這種模型自己就讓人以爲TCP的鏈接處理就是那個樣子,自己就有一個瓶頸在那裏!事實上,無論是 fastsocket,仍是個人這個方案,都解決了一對多問題,模型成了多對多的。
       那麼對於TCP的數據傳輸處理,它該怎麼優化呢?要知道,它可不是一對多的模型,它是一個自然的一對一的模型,而TCP又是一個嚴格按序的協議,爲了保序,採用並行化處理是一個愚蠢的想法。爲了優化它,咱們先給出可能的瓶頸在哪裏。

4.1.一對一TCP鏈接的瓶頸分析

因爲TCP鏈接中會有大量的數據傳輸,所以內存拷貝是一個瓶頸,然而本文不關注這個,這個能夠經過各類技術解決,好比DMA,零拷貝,分散/彙集IO等,本文關注的是CPU親和力的優化。另外,考慮到這種ESTABLISHED狀態的一對一鏈接的數量還有對其的操做。
       考慮持續到來的短鏈接,大量的併發,咱們來看下內核中ESTABLISHED哈希表的壓力
1).持續的新建鏈接(握手完成,客戶socket會插入表中),鎖表,排隊;
2).持續的鏈接釋放,鎖表,排隊;
3).大量的鏈接同時存在,大量的TIME_WAIT,查詢延遲且連累正常鏈接;
4).TIME_WAIT socket也面臨持續新建,釋放的問題。
...
若是是長鏈接,可能會緩解,但病根不除,不絕後患。
       因爲這個是一對一的對稱模型,TCP的按序性很差作並行化本地操做,所以我也就沒有對其進行橫向拆分,沒了橫向拆分,對於縱向拆分也就沒有了意義,由於勢必會出現多個CPU對隊列的爭搶問題。所以與鏈接處理的優化原則徹底不一樣。

4.2.優化一對一TCP鏈接數據傳輸處理部分

如何優化?目標已經很明確,那就是本地化查詢,本地化插入/刪除,儘可能減小RCU的內存屏障的影響。
       和鏈接處理不一樣,咱們無法把CPU當成主角,由於一條鏈接的處理同一個時刻只能在一個CPU上。所以爲了加速處理而且維護cache,創建兩條原則:
1).增長一個ESTABLISHED表的本地cache,該cache能夠無鎖操做;
2).在用戶進程等待數據期間,禁止進程遷移。


個人邏輯以下,事實證實真的不錯:

tcp_user_receive/poll
{
    0.例行RPS操做
    1.設置進程爲不可遷移
      disable中斷
    2.將socket加入當前CPU的本地ESTABLISHED表 (優化點1:若是本地表沒有才加入)
      enable中斷
    3.sleep_wait_for_data_or_event
    4.讀取數據
      disable中斷
    5.將socket從本地ESTABLISHED表中摘除 (優化點1:不須要摘除)
      enable中斷
    6.清除不可遷移位
}

tcp_stack_receive
{
    1.例行tcp_v4_rcv的操做
    2.查找當前CPU的本地ESTABLISHED表
      沒有找到的狀況下,查找系統全局的ESTABLISHED表
    3.例行tcp_v4_rcv的操做
}


很顯然,上述的邏輯基於如下的判斷:既然進程已經在一個CPU上等待數據了,最好的方式就是將這個進程暫時釘死在這個 CPU上,而且告知內核協議棧,將軟中斷上來的數據包交給這個CPU。釘死這個進程是暫時的,只爲接收一輪數據,這個顯然要在task_struct中增 加一個flag或者借一個bit,而通知內核協議棧的軟中斷例程則是經過將該socket加入到本地的ESTABLISHED緩存哈希表中實現的。
       在進程得到了一輪數據以後,爲了避免影響Linux系統全局的域調度行爲(Linux的域調度能夠很好的作到進程的負載均衡同時又不破壞cache的熱 度),須要將釘死的進程釋放,同時注意優化點1,到底需不須要將socket從本地表摘除呢?鑑於進程遷移通常都是小几率事件,因此進程在至關大的機率下 是不會遷移的,所以不摘除socket能夠省下一些mips。

4.3.客戶socket與鏈接處理徹底分開處理

一對一的TCP socket數據傳輸處理和本文第一部分描述的鏈接處理是沒有關係的,accept返回以後,由哪一個進程/線程處理這個客戶socket,是否綁定它到特定的CPU,這些都不會影響鏈接處理的過程。

4.4.Linux原生協議棧的例行優化-prequeue與backlog

事 實上,因爲涉及到數據拷貝,內核協議棧一直都是傾向於讓用戶進程本身來解析,處理一個skb,而內核要作的只是將skb掛在某個隊列中,這是一個很是常規 的優化。Linux協議棧中存在兩個隊列,prequeue和backlog,前者是當協議棧發現沒有任何一個用戶進程佔據某個socket的時候,其嘗 試將skb掛入一個用戶prequeue隊列,當用戶進程調用recv的時候,自行dequeue並處理之,後者則是當協議棧發現當前socket被某個 用戶進程佔據的時候,將skb掛入backlog隊列,待用戶進程釋放該socket的時候,自行dequeue並處理之。

4.5.Linux原生協議棧的例行優化-RPS/RFS優化

這其實是軟件模擬的RSS相似的東西,資料已經很多,其宗旨就是儘可能讓協議棧處理的CPU和用戶進程處理的CPU是同一個CPU,咱們知道,對於鏈接處理,因爲是一對多的關係,這樣並不可行,然而對於一對一的客戶socket,這樣的效果很是好。

4.6.Linux原生協議棧的例行優化-early demux

Linux協議棧指望若是在確認一個數據流的第一個數據包是本地接收的前提下,免除後續路由查找,只實現socket查找便可。這是一種典型的一次查找,短路使用的優化方案,相似的還有一次查找,多方屢次使用,相似於我對nf_conntrack所作的那樣。

4.7.Linux原生協議棧的例行優化-busy poll

顧名思義,就是不等協議棧往上送skb,而是本身直接去底層拿,有點像快遞自提那種。然而因爲在底層沒有通過協議棧處理,根本就不肯定這個數據包是否是發給本身的,所以這會有必定的風險,可是若是在統計意義上你能肯定本身系統的行爲,那麼也是能夠起到優化效果的。

5.總結

5.1.關於TCP鏈接處理的優化

如 果你把一個Listener僅僅看做是一個被內核協議棧處理的socket,那麼你很容易找到不少值得優化的點,好比這裏把鎖拆分紅粒度更細的,那裏判斷 一下搶佔什麼的,而後代碼就會愈來愈複雜和龐大。可是反過來,若是你把一個Listener看做是一個基礎設施,那事情就簡單多了。基礎設施是服務於客戶 的,它有兩個基本的性質:
1).它永遠在那裏
2).它不受客戶的控制
對於一個TCP Listener,難道它不應是一個基礎設施嗎?它應該和CPU是一夥的,做爲資源而後爲鏈接請求服務,它本不該該是搶CPU的傢伙,不該該抱有「它如何才能搶到更多的CPU時間,如何才能拽取其它Listener的資源....」這樣的想法去優化它。
       Linux內核中有不少這樣的例子,好比ksoftirqd/0,ksoftirqd/0/1,像這種kxxxx/n這類內核線程均可以看做是基礎設施, 在每個CPU上提供相同的服務,不干涉別的CPU,永遠都是取Per CPU的管理數據。按照這樣的想法,當須要提供一個TCP服務的時候,很容易想法創建一個相似的基礎設施,它將永遠在那裏,每個CPU上一個,爲新到來 的鏈接提供鏈接服務,它服務的產品就是一個客戶socket,至於說怎麼交給進程,經過兩類Accept隊列提供一個服務窗口!咱們假設一個偵聽進程或者 線程掛掉了,這絲絕不會影響TCP Listener基礎設施,它的進程無關性繼續爲其它的進程提供客戶socket,直到全部的偵聽進程所有掛掉或者主動退出,而這很容易用引用計數來跟 蹤。
       在這個優化版本中,你能夠這樣想,從兩類Accept隊列往下,TCP Listener基礎設施與用戶的偵聽進程無關了,不會受到其影響,即使用戶偵聽進程不斷在CPU間跳躍,綁定,解除綁定,它們影響不到Accept隊列 下面的TCP Listener基礎設施,受影響的也只是它們本身從哪一個Accept隊列裏獲取客戶socket的問題。下面是一個簡圖:


wKiom1YCwQmwR2H9AAD6QtXAk_k938.jpg


一樣,因爲TCP Listener已經基礎設施化了,向上用Accept隊列隔離了用戶進程socket,向下用中斷調度系統隔離了網卡或者網卡隊列,所以也就沒有了不少優化版本中面臨的四種狀況:隊列比CPU多,隊列與CPU相等,隊列比CPU少,根本就沒有隊列。

5.2.關於TCP傳輸處理的優化

本 文針對這個方面說的不是不少,很大一部分緣由在於這部分並非核心瓶頸,Linux自己的調度系統已經能夠很好的應對進程切換,遷移對cache的影響, 所以,你會發現,我作的僅有的優化也是針對這兩方面的,同時,增長一個本地ESTABLISHED哈希緩存會帶來小量的性能提高,這個想法來自於我增長的 nf_conntrack的cache以及slab分級cache。

5.3.與數據包轉發的統一

前段時間搞過一個Linux轉 發性能優化,當時引入了一個虛擬輸出隊列(VOQ),解決加速比的問題,爲了實現線速pps,轉發的狀況存在網卡帶寬的N加速比問題,所以當時建立轉發基 礎設施的時候是把網卡拉過來搭夥合做,而不是CPU,事實上,若是在多隊列網卡上,這兩者是徹底一致的,網卡隊列能夠綁定到CPU,而且還能夠一個CPU 專門處理輸入,一個專門處理輸出。
       配合Per CPU的skb pool(我舉的是卡車運貨的例子),每個Listener在每個CPU上都有一個skb pool,這樣對於本地出發的數據包,能夠作到無鎖化的分配skb。
       如今針對TCP的狀況咱們能夠把轉發和本地接收統一在一塊兒了,對於轉發而言,出口是另外一塊網卡,對於本地接收而言,出口是Accept隊列(握手包)或者 用戶緩衝區(傳輸包)。若是出口處沒有用戶進程,假設不是放入隊列,而是直接丟棄,那麼Per CPU的Accept隊列真的就是一塊網卡了。

5.4.溫州老闆的座椅爆炸

本文受朋友之託,答應了就寫了,決不藏着掖着,今天在飛機上裝X了一路,15A是個大媽在看雜誌,15C是一個少女,跟我搭了幾句話我就沒再理她。寫着寫着又想到一些優化,然則總不能飛機上debug吧,只能loopback啦...溫州老闆欠我一首《卡農》!!!

相關文章
相關標籤/搜索