性能優化:如何更快地接收數據


從網卡到應用程序,數據包會經過一系列組件,其中驅動做了什么?內核做了什么?為了優化,我們又能做些什么?整個過程中涉及到諸多細微可調的軟硬件參數,並且相互影響,不存在一勞永逸的“銀彈”。本文中又拍雲系統開發高級工程師楊鵬將結合自己的的實踐經驗,介紹在深入理解底層機制的基礎上如何做出“場景化”的最優配置。

文章根據楊鵬在又拍雲 Open Talk 技術沙龍北京站主題演講《性能優化:更快地接收數據》整理而成,現場視頻及 PPT 可點擊閱讀原文查看。

大家好,我是又拍雲開發工程師楊鵬,在又拍雲工作已有四年時間,期間一直從事 CDN 底層系統開發的工作,負責調度、緩存、負載均衡等 CDN 的核心組件,很高興來跟大家分享在網絡數據處理方面的經驗和感受。今天分享的主題是《如何更快地接收數據》,主要介紹加速網絡數據處理的方法和實踐。希望能幫助大家更好的了解如何在系統的層面,盡量在應用程序無感的情況下做到極致的優化。言歸正傳,進入主題。

首先需要清楚在嘗試做任何優化的時候,想到的第一件事情應該是什么?個人覺得是衡量指標。做任何改動或優化之前,都要明確地知道,是怎樣的指標反映出了當前的問題。那么在做了相應的調整或改動之后,也才能通過指標去驗證實際效果與作用。

針對要分享的主題,有一個圍繞上面指標核心的基本原則。在網絡層面做優化,歸根結底只需要看一點,假如可以做到網絡棧的每個層次,加入能監控到對應層次的丟包率,這樣核心的指標,就可以明確地知道問題出在哪一層。有了明確可監控的指標,之后做相應的調整與實際效果的驗證也就很簡單了。當然上述兩點相對有點虛,接下來就是比較干的部分了。

如上圖所示,當收到一個數據包,從進入網卡,一直到達應用層,總的數據流程有很多。在當前階段,無需關注每個流程,留意其中幾個核心的關鍵路徑即可:

  • 第一個,數據包到達網卡;

  • 第二個,網卡在收到數據包時,它需要產生一個中斷,告訴 CPU 數據已經到了;

  • 第三步,內核從這個時候開始進行接管,把數據從網卡中拿出來,交到后面內核的協議棧去處理。

以上是三個關鍵的路徑。上圖中右邊的手繪圖指的就是這三個步驟,並有意區分了兩個顏色。之所以這么區分是因為接下來會按這兩部分進行分享,一是上層驅動部分,二是下層涉及到內核的部分。 當然內核比較多,通篇只涉及到內核網絡子系統,更具體來說是內核跟驅動交互部分的內容。

網卡驅動

網卡驅動的部分,網卡是硬件,驅動(driver)是軟件,包括了網卡驅動部分的大部分。這部分可簡單分四個點,依次是初始化、啟動、監控與調優驅動它的初始化流程。

網卡驅動-初始化

驅動初始化的過程和硬件相關,無需過分關注。但需注意一點就是注冊 ethool 的一系列操作,這個工具可以對網卡做各種各樣的操作,不止可以讀取網卡的配置,還可以更改網卡的配置參數,是一個非常強大的工具。

那它是如何控制網卡的呢?每個網卡的驅動在初始化時,通過接口,去注冊支持 ethool 工具的一系列操作。ethool 是一套很通用的接口,比如說它支持 100 個功能,但每個型號的網卡,只能支持一個子集。所以具體支持哪些功能,會在這一步進行聲明。

上圖截取的部分,是在初始化時結構體的賦值。前面兩個可以簡單看一下,驅動在初始化的時候會告訴內核,如果想要操作這塊網卡對應的回調函數,其中最主要的是啟動和關閉,有用 ifconfig 工具操作網卡的應該都很熟悉,當用 ifconfig up/down 一張網卡的時候,調用的都是它初始化時指定的這幾個函數。

網卡驅動-啟動

驅動初始化過程之后就是啟動(open)中的流程了,一共分為四步:分配 rx/tx 隊列內存、

開啟 NAPI、注冊中斷處理函數、開啟中斷。其中注冊中斷處理函數和開啟中斷是理所當然的,任何一個硬件接入到機器上都需要做這個操作。當后面收到一些事件時,它需要通過中斷去通知系統,然后開啟中斷。

第二步的 NAPI 后面會詳細說明,這里先重點關注啟動過程中對內存的分配。網卡在收到數據時,都必須把數據從鏈路層拷貝到機器的內存里,而這塊內存就是網卡在啟動時,通過接口向內核、向操作系統申請而來的。內存一旦申請下來,地址確定之后,后續網卡在收到數據的時候,就可以直接通過 DMA 的機制,直接把數據包傳送到內存固定的地址中去,甚至不需要 CPU 的參與。

到隊列內存的分配可以看下上圖,很早之前的網卡都是單隊列的機制,但現代的網卡大多都是多隊列的。好處就是機器網卡的數據接收可以被負載均衡到多個 CPU 上,因此會提供多個隊列,這里先有個概念后面會詳細說明。

下面來詳細介紹啟動過程中的第二步 NAPI,這是現代網絡數據包處理框架中非常重要的一個擴展。之所以現在能支持 10G、20G、25G 等非常高速的網卡,NAPI 機制起到了非常大的作用。當然 NAPI 並不復雜,其核心就兩點:中斷、輪循。一般來說,網卡在接收數據時肯定是收一個包,產生一個中斷,然后在中斷處理函數的時候將包處理掉。處在收包、處理中斷,下一個收包,再處理中斷,這樣的循環中。而 NAPI 機制優勢在於只需要一次中斷,收到之后就可以通過輪循的方式,把隊列內存中所有的數據都拿走,達到非常高效的狀態。

網卡驅動-監控

接下來就是在驅動這層可以做的監控了,需要去關注其中一些數據的來源。


$ sudo ethtool -S eth0
NIC statistics:
     rx_packets: 597028087
     tx_packets: 5924278060
     rx_bytes: 112643393747
     tx_bytes: 990080156714
     rx_broadcast: 96
     tx_broadcast: 116
     rx_multicast:20294528
     .... 

首先非常重要的是 ethool 工具,它可以拿到網卡中統計的數據、接收的包數量、處理的流量等等常規的信息,而我們更多的是需要關注到異常信息。


$ cat /sys/class/net/eth0/statistics/rx_dropped
2

通過 sysfs 的接口,可以看到網卡的丟包數,這就是系統出現異常的一個標志。

三個途徑拿到的信息與前面差不多,只是格式有些亂,僅做了解即可。

上圖是要分享的一個線上案例。當時業務上出現異常,經過排查最后是懷疑到網卡這層,為此需要做進一步的分析。通過 ifconfig 工具可以很直觀的查看到網卡的一些統計數據,圖中可以看到網卡的 errors 數據指標非常高,明顯出現了問題。但更有意思的一點是, errors 右邊最后的 frame 指標數值跟它完全相同。因為 errors 指標是網卡中很多錯誤累加之后的指標,與它相鄰的 dropped、overruns 這倆個指標都是零,也就是說在當時的狀態下,網卡的錯誤大部分來自 frame。

當然這只是瞬時的狀態,上圖中下面部分是監控數據,可以明顯看到波動的變化,確實是某一台機器異常了。frame 錯誤一般是在網卡收到數據包,進行 RCR 校驗時失敗導致的。當收到數據包,會對該包中的內容做校驗,當發現跟已經存下來的校驗不匹配,說明包是損壞的,因此會直接將其丟掉。

這個原因是比較好分析的,兩點一線,機器的網卡通過網線接到上聯交換機。當這里出現問題,不是網線就是機器本身的網卡問題,或者是對端交換機的端口,也就是上聯交換機端口出現問題。當然按第一優先級去分析,協調運維去更換了機器對應的網線,后面的指標情況也反映出了效果,指標直接突降直到完全消失,錯誤也就不復存在了,對應上層的業務也很快恢復了正常。

網卡驅動-調優

說完監控之后來看下最后的調優。在這個層面能調整的東西不多,主要是針對網卡多隊列的調整,比較直觀。調整隊列數目、大小,各隊列間的權重,甚至是調整哈希的字段,都是可以的。

$ sudo ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:   0
TX:   0
Other:    0
Combined: 8
Current hardware settings:
RX:   0
TX:   0
Other:    0
Combined: 4

上圖是針對多隊列的調整。為了說明剛才的概念,舉個例子,比如有個 web server 綁定到了 CPU2,而機器有多個 CPU,這個機器的網卡也是多隊列的,其中某個隊列會被 CPU2 處理。這個時候就會有一個問題,因為網卡有多個隊列,所以 80 端口的流量只會被分配到其中一個隊列上去。假如這個隊列不是由 CPU2 處理的,就會涉及到一些數據的騰挪。底層把數據接收上來后再交給應用層的時候,需要把這個數據移動一下。如果本來在 CPU1 處理的,需要挪到 CPU2 去,這時會涉及到 CPU cache 的失效,這對高速運轉的 CPU 來說是代價很高的操作。

那么該怎么做呢?我們可以通過前面提到的工具,特意把 80 端口 tcp 數據流量導向到對應 CPU2 處理的網卡隊列。這么做的效果是數據包從到達網卡開始,到內核處理完再到送達應用層,都是同一個 CPU。這樣最大的好處就是緩存,CPU 的 cache 始終是熱的,如此整體下來,它的延遲、效果也會非常好。當然這個例子並不實際,主要是為了說明能做到的一個效果。

內核網絡子系統

說完了整個網卡驅動部分,接下來是講解內核子系統部分,這塊會分為軟中斷與網絡子系統初始化兩部分來分享。

軟中斷

上圖的 NETDEV 是 linux 網絡子系統每年都會開的一個分會,其中比較有意思的點是每年大會舉辦的屆數會以一個特殊字符來表示。圖中是辦到了 0X15 屆,想必也都發現這是 16 進制的數字,0X15 剛好就是 21 年,也是比較極客范。對網絡子系統感興趣的可以去關注一下。

言歸正傳,內核延時任務有多種機制,而軟中斷只是其中一種。上圖是 linux 的基本結構,上層是用戶態,中間是內核,下層是硬件,很抽象的一個分層。用戶態和內核態之間會有兩種交互的方式:通過系統調用,或者通過異常可以陷入到內核態里面。那底層的硬件跟內核又是怎么交互的呢?答案是中斷,硬件跟內核交互的時候必須通過中斷,處理任何事件都需要產生一個中斷信號來告知 CPU 與內核。

不過這樣的機制一般情況下也許沒有問題,但是對網絡數據來說,一個數據報一個中斷,這樣會有很明顯的兩個問題。

問題一:中斷在處理期間,會屏蔽之前的中斷信號。當一個中斷處理的時間很長,在處理期間收到的中斷信號都會丟掉。 如果處理一個包用了十秒,在這十秒期間又收到了五個數據包,但因為中斷信號丟了,即便前面的處理完了,后面的數據包也不會再處理了。對應到 tcp 這邊,假如客戶端給服務端發了一個數據包,幾秒后處理完了,但在處理期間客戶端又發了后續的三個包,但是服務端后面並不知道,以為只收到了一個包,這時客戶端又在等待服務端的回包,如此會導致兩邊都卡住了,也說明了信號丟失是一個極其嚴重的問題。

問題二:一個數據包觸發一次中斷處理的話,當有大量的數據包到來后,就會產生非常大量的中斷。 如果達到了 10 萬、50 萬、甚至百萬的 pps,那 CPU 就需要處理大量的網絡中斷,也就不用干其他事情了。

而針對以上兩點問題的解決方法就是讓中斷處理盡可能的短。 具體來說,不能在中斷處理函數,只能把它揪出來,交到軟中斷機制里。這樣之后的實際結果是硬件的中斷處理做的事情就很少了,將接收數據等一些必須的事情交到軟中斷去完成,這也是軟中斷存在的意義。

static struct smp_hotplug_thread softirq_threads = {
  .store              = &ksoftirqd,
  .thread_should_run  = ksoftirqd_should_run,
  .thread_fn          = run_ksoftirqd,
  .thread-comm        = “ksoftirqd/%u”,
};

static _init int spawn_ksoftirqd(void)
{
  regiter_cpu_notifier(&cpu_nfb);
  
  BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

  return 0;
}
early_initcall(spawn_ksoftirqd);

軟中斷機制是通過內核的線程來實現的。圖中是對應的一個內核線程。服務器 CPU 都會有一個 ksoftirqd 這樣的內核線程,多 CPU 的機器會相對應的有多個線程。圖中結構體最后一個成員 ksoftirqd/,如果有三個 CPU 對應就會有 /0/1/2 三個內核線程。

軟中斷機制的信息在 softirqs 下面可以看到。軟中斷並不多只有幾種,其中需要關注的,跟網絡相關的就是 NET-TX 和 NET-RX,網絡數據收發的兩種場景。

內核初始化

鋪墊完軟中斷之后,下面來看內核初始化的流程。主要為兩步:

  • 針對每個 CPU,創建一個數據結構,這上面掛了非常多的成員,與后面的處理密切相關;

  • 注冊一個軟中斷處理函數,對應上面看到的 NET-TX 和 NET-RX 這兩個軟中斷的處理函數。

上圖是手繪的一個數據包的處理流程:

  • 第一步網卡收到了數據包;

  • 第二步把數據包通過 DMA 拷到了內存里面;

  • 第三步產生了一個中斷告訴 CPU 並開始處理中斷。重點的中斷處理可分為兩步:一是將中斷信號屏蔽了,二是喚醒 NAPI 機制。


static irqreturn_t igb_msix_ring(int irq, void *data)
{
  struct igb_q_vector *q_vector = data;
  
  /* Write the ITR value calculated from the previous interrupt. */
  igb_write_itr(q_vector);
  
  napi_schedule(&q_vector->napi);
  
  return IRO_HANDLED;
}

上面的代碼是 igb 網卡驅動中斷處理函數做的事情。如果省略掉開始的變量聲明和后面的返回,這個中斷處理函數只有兩行代碼,非常短。需要關注的是第二個,在硬件中斷處理函數中,只用激活外部 NIPA 軟中斷處理機制,無需做其他任何事情。因此這個中斷處理函數會返回的非常快。

NIPI 激活


/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi)
{
  list_add_tail(&napi->poll_list, &sd->poll_list);
  _raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

NIPI 的激活也很簡單,主要為兩步。內核網絡系統在初始化的時每個 CPU 都會有一個結構體,它會把隊列對應的信息插入到結構體的鏈表里。換句話說,每個網卡隊列在收到數據的時候,需要把自己的隊列信息告訴對應的 CPU,將這兩個信息綁定起來,保證某個 CPU 處理某個隊列。

除此之外,還要與觸發硬中斷一樣,需要觸發軟中斷。下圖將很多步驟放到了一塊,前面講過的就不再贅述了。圖中要關注的是軟中斷是怎么觸發的。與硬中斷差不多,軟中斷也有中斷的向量表。每個中斷號,都會對應一個處理函數,當需要處理某個中斷,只需要在對應的中斷向量表里找就好了,跟硬中斷的處理是一模一樣的。

數據接收-監控

說完了運作機制,再來看看有哪些地方可以做監控。在 proc 下面有很多東西,可以看到中斷的處理情況。第一列就是中斷號,每個設備都有獨立的中斷號,這是寫死的。對網絡來說只需要關注網卡對應的中斷號,圖中是 65、66、67、68 等。當然看實際的數字並沒有意義,而是需要看它的分布情況,中斷是不是被不同 CPU 在處理,如果所有的中斷都是被一個 CPU 處理,那么就需要做些調整,把它分散開。

數據接收-調優

中斷可以做的調整有兩個:一是中斷合並,二是中斷親和性。

自適應中斷合並

  • rx-usecs: 數據幀到達后,延遲多長時間產生中斷信號,單位微秒

  • rx-frames: 觸發中斷前積累數據幀的最大個數

  • rx-usecs-irq: 如果有中斷處理正在執行,當前中斷延遲多久送達 CPU

  • rx-frames-irq: 如果有中斷處理正在執行,最多積累多少個數據幀

上面列的都是硬件網卡支持的功能。NAPI 本質上也是中斷合並的機制,假如有很多包的到來,NAPI 就可以做到只產生一個中斷,因此不需要硬件來幫助做中斷合並,實際效果是跟 NAPI 是相同的,都是減少了總的中斷數量。

中斷親和性

$ sudo bash -c ‘echo 1 > /proc/irq/8/smp_affinity’

這個與網卡多隊列是密切相關的。如果網卡有多個隊列,就能手動來明確指定由哪個 CPU 來處理,均衡的把數據處理的負載分散到機器的可用 CPU 上。配置也比較簡單,只需把數字寫入到 /proc 對應的這個文件中就可以了。這是個位數組,轉成二進制后就會有對應的 CPU 去處理。如果寫個 1,可能就是 CPU0 來處理;如果寫個 4,轉化成二進制是 100,那么就會交給 CPU2 去處理。

另外有個小問題需要注意,很多發行版可能會自帶一個 irqbalance 的守護進程(http://irqbalance.github.io/irqbalance),會將手動中斷均衡的設置給覆蓋掉。這個程序做的核心事情就是把上面手動設置文件的操作放到程序里,有興趣可以去看下它的代碼(https://github.com/Irqbalance/irqbalance/blob/master/activate.c),也是把這個文件打開,寫對應的數字進去就可以了。

內核-數據處理

最后是數據處理部分了。當數據到達網卡,進入隊列內存后,就需要內核從隊列內存中將數據拉出來。如果機器的 PPS 達到了十萬甚至百萬,而 CPU 只處理網絡數據的話,那其他基本的業務邏輯也就不用干了,因此不能讓數據包的處理獨占整個 CPU,而核心點是怎么去做限制。

針對上述問題主要有兩方面的限制:整體的限制和單次的限制

while (!list_empty(&sd->poll_list)){
  struct napi_struct *n;
  int work,weight;
  
  /* If softirq window is exhausted then punt.
   * Allow this to run for 2 jiffies since which will allow
   * an average latency of 1.5/HZ.
   */
   if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
   goto softnet_break;

整體限制很好理解,就是一個 CPU 對應一個隊列。如果 CPU 的數量比隊列數量少,那么一個 CPU 可能需要處理多個隊列。

weight = n->weight;

work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
        work = n->poll(n,weight);
        trace_napi_poll(n);
}

WARN_ON_ONCE(work > weight);

budget -= work;

單次限制則是限制一個隊列在一輪里處理包的數量。達到限制之后就停下來,等待下一輪的處理。

softnet_break:
  sd->time_squeeze++;
  _raise_softirq_irqoff(NET_RX_SOFTIRQ);
  goto out;

而停下來就是很關鍵的節點,幸運的是有對應的指標記錄,有 time-squeeze 這樣中斷的計數,拿到這個信息就可以判斷出機器的網絡處理是否有瓶頸,被迫中斷的頻率高低。

上圖是監控 CPU 指標的數據,格式很簡單,每行對應一個 CPU,數值之間用空格分割,輸出格式為 16 進制。那么每一列數值又代表什么呢?很不幸,這個沒有文檔,只能通過檢查使用的內核版本,然后去看對應的代碼。

seq_printf(seq,
     "%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n",
     sd->processed, sd->dropped, sd->time_squeeze, 0,
     0, 0, 0, 0, /* was fastroute */
     sd->cpu_collision, sd->received_rps, flow_limit_count);

下面說明了文件中每個字段都是怎么來的,實際情況可能會有所不同,因為隨着內核版本的迭代,字段的數量以及字段的順序都有可能發生變化,其中與網絡數據處理被中斷次數相關的就是 squeeze 字段:

  • sd->processed 處理的包數量(多網卡 bond 模式可能多於實際的收包數量)

  • sd->dropped 丟包數量,因為隊列滿了

  • sd->time_spueeze 軟中斷處理 net_rx_action 被迫打斷的次數

  • sd->cpu_collision 發送數據時獲取設備鎖沖突,比如多個 CPU 同時發送數據

  • sd->received_rps 當前 CPU 被喚醒的次數(通過處理器間中斷)

  • sd->flow_limit_count 觸發 flow limit 的次數

下圖是業務中遇到相關問題的案例,最后排查到 CPU 層面。圖一是 TOP 命令的輸出,顯示了每個 CPU 的使用量,其中紅框標出的 CPU4 的使用率存在着異常,尤其是倒數第二列的 SI 占用達到了 89%。SI 是 softirq 的縮寫,表示 CPU 花在軟中斷處理上的時間占比,而圖中 CPU4 在時間占比上明顯過高。圖二則是對應圖一的輸出結果,CPU4 對應的是第五行,其中第三列數值明顯高於其他 CPU,表明它在處理網絡數據的時被頻繁的打斷。

針對上面的問題推斷 CPU4 存在一定的性能衰退,也許是質量不過關或其他的原因。為了驗證是否是性能衰退,寫了一個簡單的 python 腳本,一個一直去累加的死循環。每次運行時,把這段腳本綁定到某個 CPU 上,然后觀察不同 CPU 耗時的對比。最后對比結果也顯示 CPU4 的耗時比其他的 CPU 高了幾倍,也驗證了之前的推斷。之后協調運維更換了 CPU,意向指標也就恢復正常了。

總結

以上所有操作都只是在數據包從網卡到了內核層,還沒到常見的協議,只是完成了萬里長征第一步,后面還有一系列的步驟,例如數據包的壓縮(GRO)、網卡多隊列軟件(RPS)還有 RFS 在負載均衡的基礎上考慮流的特征,就是 IP 端口四元組的特征,最后才是把數據遞交到 IP 層,以及到熟悉的 TCP 層。

總的來說,今天的分享都是圍繞驅動來做的,我想強調的性能優化的核心點在於指標,不能測量也就很難去改善,要有指標的存在,這樣一切的優化才有意義。

推薦閱讀

MySQL 那些常見的錯誤設計規范

全站 HTTPS 就一定安全了嗎?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM