性能优化:如何更快地接收数据
從網(wǎng)卡到應(yīng)用程序,數(shù)據(jù)包會(huì)經(jīng)過一系列組件,其中驅(qū)動(dòng)做了什么?內(nèi)核做了什么?為了優(yōu)化,我們又能做些什么?整個(gè)過程中涉及到諸多細(xì)微可調(diào)的軟硬件參數(shù),并且相互影響,不存在一勞永逸的“銀彈”。本文中又拍云系統(tǒng)開發(fā)高級(jí)工程師楊鵬將結(jié)合自己的的實(shí)踐經(jīng)驗(yàn),介紹在深入理解底層機(jī)制的基礎(chǔ)上如何做出“場景化”的最優(yōu)配置。
文章根據(jù)楊鵬在又拍云 Open Talk 技術(shù)沙龍北京站主題演講《性能優(yōu)化:更快地接收數(shù)據(jù)》整理而成,現(xiàn)場視頻及 PPT 可點(diǎn)擊閱讀原文查看。
大家好,我是又拍云開發(fā)工程師楊鵬,在又拍云工作已有四年時(shí)間,期間一直從事 CDN 底層系統(tǒng)開發(fā)的工作,負(fù)責(zé)調(diào)度、緩存、負(fù)載均衡等 CDN 的核心組件,很高興來跟大家分享在網(wǎng)絡(luò)數(shù)據(jù)處理方面的經(jīng)驗(yàn)和感受。今天分享的主題是《如何更快地接收數(shù)據(jù)》,主要介紹加速網(wǎng)絡(luò)數(shù)據(jù)處理的方法和實(shí)踐。希望能幫助大家更好的了解如何在系統(tǒng)的層面,盡量在應(yīng)用程序無感的情況下做到極致的優(yōu)化。言歸正傳,進(jìn)入主題。
首先需要清楚在嘗試做任何優(yōu)化的時(shí)候,想到的第一件事情應(yīng)該是什么?個(gè)人覺得是衡量指標(biāo)。做任何改動(dòng)或優(yōu)化之前,都要明確地知道,是怎樣的指標(biāo)反映出了當(dāng)前的問題。那么在做了相應(yīng)的調(diào)整或改動(dòng)之后,也才能通過指標(biāo)去驗(yàn)證實(shí)際效果與作用。
針對要分享的主題,有一個(gè)圍繞上面指標(biāo)核心的基本原則。在網(wǎng)絡(luò)層面做優(yōu)化,歸根結(jié)底只需要看一點(diǎn),假如可以做到網(wǎng)絡(luò)棧的每個(gè)層次,加入能監(jiān)控到對應(yīng)層次的丟包率,這樣核心的指標(biāo),就可以明確地知道問題出在哪一層。有了明確可監(jiān)控的指標(biāo),之后做相應(yīng)的調(diào)整與實(shí)際效果的驗(yàn)證也就很簡單了。當(dāng)然上述兩點(diǎn)相對有點(diǎn)虛,接下來就是比較干的部分了。
如上圖所示,當(dāng)收到一個(gè)數(shù)據(jù)包,從進(jìn)入網(wǎng)卡,一直到達(dá)應(yīng)用層,總的數(shù)據(jù)流程有很多。在當(dāng)前階段,無需關(guān)注每個(gè)流程,留意其中幾個(gè)核心的關(guān)鍵路徑即可:
-
第一個(gè),數(shù)據(jù)包到達(dá)網(wǎng)卡;
-
第二個(gè),網(wǎng)卡在收到數(shù)據(jù)包時(shí),它需要產(chǎn)生一個(gè)中斷,告訴 CPU 數(shù)據(jù)已經(jīng)到了;
-
第三步,內(nèi)核從這個(gè)時(shí)候開始進(jìn)行接管,把數(shù)據(jù)從網(wǎng)卡中拿出來,交到后面內(nèi)核的協(xié)議棧去處理。
以上是三個(gè)關(guān)鍵的路徑。上圖中右邊的手繪圖指的就是這三個(gè)步驟,并有意區(qū)分了兩個(gè)顏色。之所以這么區(qū)分是因?yàn)?strong>接下來會(huì)按這兩部分進(jìn)行分享,一是上層驅(qū)動(dòng)部分,二是下層涉及到內(nèi)核的部分。 當(dāng)然內(nèi)核比較多,通篇只涉及到內(nèi)核網(wǎng)絡(luò)子系統(tǒng),更具體來說是內(nèi)核跟驅(qū)動(dòng)交互部分的內(nèi)容。
網(wǎng)卡驅(qū)動(dòng)
網(wǎng)卡驅(qū)動(dòng)的部分,網(wǎng)卡是硬件,驅(qū)動(dòng)(driver)是軟件,包括了網(wǎng)卡驅(qū)動(dòng)部分的大部分。這部分可簡單分四個(gè)點(diǎn),依次是初始化、啟動(dòng)、監(jiān)控與調(diào)優(yōu)驅(qū)動(dòng)它的初始化流程。
網(wǎng)卡驅(qū)動(dòng)-初始化
驅(qū)動(dòng)初始化的過程和硬件相關(guān),無需過分關(guān)注。但需注意一點(diǎn)就是注冊 ethool 的一系列操作,這個(gè)工具可以對網(wǎng)卡做各種各樣的操作,不止可以讀取網(wǎng)卡的配置,還可以更改網(wǎng)卡的配置參數(shù),是一個(gè)非常強(qiáng)大的工具。
那它是如何控制網(wǎng)卡的呢?每個(gè)網(wǎng)卡的驅(qū)動(dòng)在初始化時(shí),通過接口,去注冊支持 ethool 工具的一系列操作。ethool 是一套很通用的接口,比如說它支持 100 個(gè)功能,但每個(gè)型號(hào)的網(wǎng)卡,只能支持一個(gè)子集。所以具體支持哪些功能,會(huì)在這一步進(jìn)行聲明。
上圖截取的部分,是在初始化時(shí)結(jié)構(gòu)體的賦值。前面兩個(gè)可以簡單看一下,驅(qū)動(dòng)在初始化的時(shí)候會(huì)告訴內(nèi)核,如果想要操作這塊網(wǎng)卡對應(yīng)的回調(diào)函數(shù),其中最主要的是啟動(dòng)和關(guān)閉,有用 ifconfig 工具操作網(wǎng)卡的應(yīng)該都很熟悉,當(dāng)用 ifconfig up/down 一張網(wǎng)卡的時(shí)候,調(diào)用的都是它初始化時(shí)指定的這幾個(gè)函數(shù)。
網(wǎng)卡驅(qū)動(dòng)-啟動(dòng)
驅(qū)動(dòng)初始化過程之后就是啟動(dòng)(open)中的流程了,一共分為四步:分配 rx/tx 隊(duì)列內(nèi)存、
開啟 NAPI、注冊中斷處理函數(shù)、開啟中斷。其中注冊中斷處理函數(shù)和開啟中斷是理所當(dāng)然的,任何一個(gè)硬件接入到機(jī)器上都需要做這個(gè)操作。當(dāng)后面收到一些事件時(shí),它需要通過中斷去通知系統(tǒng),然后開啟中斷。
第二步的 NAPI 后面會(huì)詳細(xì)說明,這里先重點(diǎn)關(guān)注啟動(dòng)過程中對內(nèi)存的分配。網(wǎng)卡在收到數(shù)據(jù)時(shí),都必須把數(shù)據(jù)從鏈路層拷貝到機(jī)器的內(nèi)存里,而這塊內(nèi)存就是網(wǎng)卡在啟動(dòng)時(shí),通過接口向內(nèi)核、向操作系統(tǒng)申請而來的。內(nèi)存一旦申請下來,地址確定之后,后續(xù)網(wǎng)卡在收到數(shù)據(jù)的時(shí)候,就可以直接通過 DMA 的機(jī)制,直接把數(shù)據(jù)包傳送到內(nèi)存固定的地址中去,甚至不需要 CPU 的參與。
到隊(duì)列內(nèi)存的分配可以看下上圖,很早之前的網(wǎng)卡都是單隊(duì)列的機(jī)制,但現(xiàn)代的網(wǎng)卡大多都是多隊(duì)列的。好處就是機(jī)器網(wǎng)卡的數(shù)據(jù)接收可以被負(fù)載均衡到多個(gè) CPU 上,因此會(huì)提供多個(gè)隊(duì)列,這里先有個(gè)概念后面會(huì)詳細(xì)說明。
下面來詳細(xì)介紹啟動(dòng)過程中的第二步 NAPI,這是現(xiàn)代網(wǎng)絡(luò)數(shù)據(jù)包處理框架中非常重要的一個(gè)擴(kuò)展。之所以現(xiàn)在能支持 10G、20G、25G 等非常高速的網(wǎng)卡,NAPI 機(jī)制起到了非常大的作用。當(dāng)然 NAPI 并不復(fù)雜,其核心就兩點(diǎn):中斷、輪循。一般來說,網(wǎng)卡在接收數(shù)據(jù)時(shí)肯定是收一個(gè)包,產(chǎn)生一個(gè)中斷,然后在中斷處理函數(shù)的時(shí)候?qū)幚淼簟L幵谑瞻⑻幚碇袛?#xff0c;下一個(gè)收包,再處理中斷,這樣的循環(huán)中。而 NAPI 機(jī)制優(yōu)勢在于只需要一次中斷,收到之后就可以通過輪循的方式,把隊(duì)列內(nèi)存中所有的數(shù)據(jù)都拿走,達(dá)到非常高效的狀態(tài)。
網(wǎng)卡驅(qū)動(dòng)-監(jiān)控
接下來就是在驅(qū)動(dòng)這層可以做的監(jiān)控了,需要去關(guān)注其中一些數(shù)據(jù)的來源。
$ sudo ethtool -S eth0 NIC statistics:rx_packets: 597028087tx_packets: 5924278060rx_bytes: 112643393747tx_bytes: 990080156714rx_broadcast: 96tx_broadcast: 116rx_multicast:20294528....首先非常重要的是 ethool 工具,它可以拿到網(wǎng)卡中統(tǒng)計(jì)的數(shù)據(jù)、接收的包數(shù)量、處理的流量等等常規(guī)的信息,而我們更多的是需要關(guān)注到異常信息。
$ cat /sys/class/net/eth0/statistics/rx_dropped 2通過 sysfs 的接口,可以看到網(wǎng)卡的丟包數(shù),這就是系統(tǒng)出現(xiàn)異常的一個(gè)標(biāo)志。
三個(gè)途徑拿到的信息與前面差不多,只是格式有些亂,僅做了解即可。
上圖是要分享的一個(gè)線上案例。當(dāng)時(shí)業(yè)務(wù)上出現(xiàn)異常,經(jīng)過排查最后是懷疑到網(wǎng)卡這層,為此需要做進(jìn)一步的分析。通過 ifconfig 工具可以很直觀的查看到網(wǎng)卡的一些統(tǒng)計(jì)數(shù)據(jù),圖中可以看到網(wǎng)卡的 errors 數(shù)據(jù)指標(biāo)非常高,明顯出現(xiàn)了問題。但更有意思的一點(diǎn)是, errors 右邊最后的 frame 指標(biāo)數(shù)值跟它完全相同。因?yàn)?errors 指標(biāo)是網(wǎng)卡中很多錯(cuò)誤累加之后的指標(biāo),與它相鄰的 dropped、overruns 這倆個(gè)指標(biāo)都是零,也就是說在當(dāng)時(shí)的狀態(tài)下,網(wǎng)卡的錯(cuò)誤大部分來自 frame。
當(dāng)然這只是瞬時(shí)的狀態(tài),上圖中下面部分是監(jiān)控?cái)?shù)據(jù),可以明顯看到波動(dòng)的變化,確實(shí)是某一臺(tái)機(jī)器異常了。frame 錯(cuò)誤一般是在網(wǎng)卡收到數(shù)據(jù)包,進(jìn)行 RCR 校驗(yàn)時(shí)失敗導(dǎo)致的。當(dāng)收到數(shù)據(jù)包,會(huì)對該包中的內(nèi)容做校驗(yàn),當(dāng)發(fā)現(xiàn)跟已經(jīng)存下來的校驗(yàn)不匹配,說明包是損壞的,因此會(huì)直接將其丟掉。
這個(gè)原因是比較好分析的,兩點(diǎn)一線,機(jī)器的網(wǎng)卡通過網(wǎng)線接到上聯(lián)交換機(jī)。當(dāng)這里出現(xiàn)問題,不是網(wǎng)線就是機(jī)器本身的網(wǎng)卡問題,或者是對端交換機(jī)的端口,也就是上聯(lián)交換機(jī)端口出現(xiàn)問題。當(dāng)然按第一優(yōu)先級(jí)去分析,協(xié)調(diào)運(yùn)維去更換了機(jī)器對應(yīng)的網(wǎng)線,后面的指標(biāo)情況也反映出了效果,指標(biāo)直接突降直到完全消失,錯(cuò)誤也就不復(fù)存在了,對應(yīng)上層的業(yè)務(wù)也很快恢復(fù)了正常。
網(wǎng)卡驅(qū)動(dòng)-調(diào)優(yōu)
說完監(jiān)控之后來看下最后的調(diào)優(yōu)。在這個(gè)層面能調(diào)整的東西不多,主要是針對網(wǎng)卡多隊(duì)列的調(diào)整,比較直觀。調(diào)整隊(duì)列數(shù)目、大小,各隊(duì)列間的權(quán)重,甚至是調(diào)整哈希的字段,都是可以的。
$ 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上圖是針對多隊(duì)列的調(diào)整。為了說明剛才的概念,舉個(gè)例子,比如有個(gè) web server 綁定到了 CPU2,而機(jī)器有多個(gè) CPU,這個(gè)機(jī)器的網(wǎng)卡也是多隊(duì)列的,其中某個(gè)隊(duì)列會(huì)被 CPU2 處理。這個(gè)時(shí)候就會(huì)有一個(gè)問題,因?yàn)榫W(wǎng)卡有多個(gè)隊(duì)列,所以 80 端口的流量只會(huì)被分配到其中一個(gè)隊(duì)列上去。假如這個(gè)隊(duì)列不是由 CPU2 處理的,就會(huì)涉及到一些數(shù)據(jù)的騰挪。底層把數(shù)據(jù)接收上來后再交給應(yīng)用層的時(shí)候,需要把這個(gè)數(shù)據(jù)移動(dòng)一下。如果本來在 CPU1 處理的,需要挪到 CPU2 去,這時(shí)會(huì)涉及到 CPU cache 的失效,這對高速運(yùn)轉(zhuǎn)的 CPU 來說是代價(jià)很高的操作。
那么該怎么做呢?我們可以通過前面提到的工具,特意把 80 端口 tcp 數(shù)據(jù)流量導(dǎo)向到對應(yīng) CPU2 處理的網(wǎng)卡隊(duì)列。這么做的效果是數(shù)據(jù)包從到達(dá)網(wǎng)卡開始,到內(nèi)核處理完再到送達(dá)應(yīng)用層,都是同一個(gè) CPU。這樣最大的好處就是緩存,CPU 的 cache 始終是熱的,如此整體下來,它的延遲、效果也會(huì)非常好。當(dāng)然這個(gè)例子并不實(shí)際,主要是為了說明能做到的一個(gè)效果。
內(nèi)核網(wǎng)絡(luò)子系統(tǒng)
說完了整個(gè)網(wǎng)卡驅(qū)動(dòng)部分,接下來是講解內(nèi)核子系統(tǒng)部分,這塊會(huì)分為軟中斷與網(wǎng)絡(luò)子系統(tǒng)初始化兩部分來分享。
軟中斷
上圖的 NETDEV 是 linux 網(wǎng)絡(luò)子系統(tǒng)每年都會(huì)開的一個(gè)分會(huì),其中比較有意思的點(diǎn)是每年大會(huì)舉辦的屆數(shù)會(huì)以一個(gè)特殊字符來表示。圖中是辦到了 0X15 屆,想必也都發(fā)現(xiàn)這是 16 進(jìn)制的數(shù)字,0X15 剛好就是 21 年,也是比較極客范。對網(wǎng)絡(luò)子系統(tǒng)感興趣的可以去關(guān)注一下。
言歸正傳,內(nèi)核延時(shí)任務(wù)有多種機(jī)制,而軟中斷只是其中一種。上圖是 linux 的基本結(jié)構(gòu),上層是用戶態(tài),中間是內(nèi)核,下層是硬件,很抽象的一個(gè)分層。用戶態(tài)和內(nèi)核態(tài)之間會(huì)有兩種交互的方式:通過系統(tǒng)調(diào)用,或者通過異常可以陷入到內(nèi)核態(tài)里面。那底層的硬件跟內(nèi)核又是怎么交互的呢?答案是中斷,硬件跟內(nèi)核交互的時(shí)候必須通過中斷,處理任何事件都需要產(chǎn)生一個(gè)中斷信號(hào)來告知 CPU 與內(nèi)核。
不過這樣的機(jī)制一般情況下也許沒有問題,但是對網(wǎng)絡(luò)數(shù)據(jù)來說,一個(gè)數(shù)據(jù)報(bào)一個(gè)中斷,這樣會(huì)有很明顯的兩個(gè)問題。
問題一:中斷在處理期間,會(huì)屏蔽之前的中斷信號(hào)。當(dāng)一個(gè)中斷處理的時(shí)間很長,在處理期間收到的中斷信號(hào)都會(huì)丟掉。 如果處理一個(gè)包用了十秒,在這十秒期間又收到了五個(gè)數(shù)據(jù)包,但因?yàn)橹袛嘈盘?hào)丟了,即便前面的處理完了,后面的數(shù)據(jù)包也不會(huì)再處理了。對應(yīng)到 tcp 這邊,假如客戶端給服務(wù)端發(fā)了一個(gè)數(shù)據(jù)包,幾秒后處理完了,但在處理期間客戶端又發(fā)了后續(xù)的三個(gè)包,但是服務(wù)端后面并不知道,以為只收到了一個(gè)包,這時(shí)客戶端又在等待服務(wù)端的回包,如此會(huì)導(dǎo)致兩邊都卡住了,也說明了信號(hào)丟失是一個(gè)極其嚴(yán)重的問題。
問題二:一個(gè)數(shù)據(jù)包觸發(fā)一次中斷處理的話,當(dāng)有大量的數(shù)據(jù)包到來后,就會(huì)產(chǎn)生非常大量的中斷。 如果達(dá)到了 10 萬、50 萬、甚至百萬的 pps,那 CPU 就需要處理大量的網(wǎng)絡(luò)中斷,也就不用干其他事情了。
而針對以上兩點(diǎn)問題的解決方法就是讓中斷處理盡可能的短。 具體來說,不能在中斷處理函數(shù),只能把它揪出來,交到軟中斷機(jī)制里。這樣之后的實(shí)際結(jié)果是硬件的中斷處理做的事情就很少了,將接收數(shù)據(jù)等一些必須的事情交到軟中斷去完成,這也是軟中斷存在的意義。
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);軟中斷機(jī)制是通過內(nèi)核的線程來實(shí)現(xiàn)的。圖中是對應(yīng)的一個(gè)內(nèi)核線程。服務(wù)器 CPU 都會(huì)有一個(gè) ksoftirqd 這樣的內(nèi)核線程,多 CPU 的機(jī)器會(huì)相對應(yīng)的有多個(gè)線程。圖中結(jié)構(gòu)體最后一個(gè)成員 ksoftirqd/,如果有三個(gè) CPU 對應(yīng)就會(huì)有 /0/1/2 三個(gè)內(nèi)核線程。
軟中斷機(jī)制的信息在 softirqs 下面可以看到。軟中斷并不多只有幾種,其中需要關(guān)注的,跟網(wǎng)絡(luò)相關(guān)的就是 NET-TX 和 NET-RX,網(wǎng)絡(luò)數(shù)據(jù)收發(fā)的兩種場景。
內(nèi)核初始化
鋪墊完軟中斷之后,下面來看內(nèi)核初始化的流程。主要為兩步:
-
針對每個(gè) CPU,創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu),這上面掛了非常多的成員,與后面的處理密切相關(guān);
-
注冊一個(gè)軟中斷處理函數(shù),對應(yīng)上面看到的 NET-TX 和 NET-RX 這兩個(gè)軟中斷的處理函數(shù)。
上圖是手繪的一個(gè)數(shù)據(jù)包的處理流程:
-
第一步網(wǎng)卡收到了數(shù)據(jù)包;
-
第二步把數(shù)據(jù)包通過 DMA 拷到了內(nèi)存里面;
-
第三步產(chǎn)生了一個(gè)中斷告訴 CPU 并開始處理中斷。重點(diǎn)的中斷處理可分為兩步:一是將中斷信號(hào)屏蔽了,二是喚醒 NAPI 機(jī)制。
上面的代碼是 igb 網(wǎng)卡驅(qū)動(dòng)中斷處理函數(shù)做的事情。如果省略掉開始的變量聲明和后面的返回,這個(gè)中斷處理函數(shù)只有兩行代碼,非常短。需要關(guān)注的是第二個(gè),在硬件中斷處理函數(shù)中,只用激活外部 NIPA 軟中斷處理機(jī)制,無需做其他任何事情。因此這個(gè)中斷處理函數(shù)會(huì)返回的非常快。
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 的激活也很簡單,主要為兩步。內(nèi)核網(wǎng)絡(luò)系統(tǒng)在初始化的時(shí)每個(gè) CPU 都會(huì)有一個(gè)結(jié)構(gòu)體,它會(huì)把隊(duì)列對應(yīng)的信息插入到結(jié)構(gòu)體的鏈表里。換句話說,每個(gè)網(wǎng)卡隊(duì)列在收到數(shù)據(jù)的時(shí)候,需要把自己的隊(duì)列信息告訴對應(yīng)的 CPU,將這兩個(gè)信息綁定起來,保證某個(gè) CPU 處理某個(gè)隊(duì)列。
除此之外,還要與觸發(fā)硬中斷一樣,需要觸發(fā)軟中斷。下圖將很多步驟放到了一塊,前面講過的就不再贅述了。圖中要關(guān)注的是軟中斷是怎么觸發(fā)的。與硬中斷差不多,軟中斷也有中斷的向量表。每個(gè)中斷號(hào),都會(huì)對應(yīng)一個(gè)處理函數(shù),當(dāng)需要處理某個(gè)中斷,只需要在對應(yīng)的中斷向量表里找就好了,跟硬中斷的處理是一模一樣的。
數(shù)據(jù)接收-監(jiān)控
說完了運(yùn)作機(jī)制,再來看看有哪些地方可以做監(jiān)控。在 proc 下面有很多東西,可以看到中斷的處理情況。第一列就是中斷號(hào),每個(gè)設(shè)備都有獨(dú)立的中斷號(hào),這是寫死的。對網(wǎng)絡(luò)來說只需要關(guān)注網(wǎng)卡對應(yīng)的中斷號(hào),圖中是 65、66、67、68 等。當(dāng)然看實(shí)際的數(shù)字并沒有意義,而是需要看它的分布情況,中斷是不是被不同 CPU 在處理,如果所有的中斷都是被一個(gè) CPU 處理,那么就需要做些調(diào)整,把它分散開。
數(shù)據(jù)接收-調(diào)優(yōu)
中斷可以做的調(diào)整有兩個(gè):一是中斷合并,二是中斷親和性。
自適應(yīng)中斷合并
-
rx-usecs: 數(shù)據(jù)幀到達(dá)后,延遲多長時(shí)間產(chǎn)生中斷信號(hào),單位微秒
-
rx-frames: 觸發(fā)中斷前積累數(shù)據(jù)幀的最大個(gè)數(shù)
-
rx-usecs-irq: 如果有中斷處理正在執(zhí)行,當(dāng)前中斷延遲多久送達(dá) CPU
-
rx-frames-irq: 如果有中斷處理正在執(zhí)行,最多積累多少個(gè)數(shù)據(jù)幀
上面列的都是硬件網(wǎng)卡支持的功能。NAPI 本質(zhì)上也是中斷合并的機(jī)制,假如有很多包的到來,NAPI 就可以做到只產(chǎn)生一個(gè)中斷,因此不需要硬件來幫助做中斷合并,實(shí)際效果是跟 NAPI 是相同的,都是減少了總的中斷數(shù)量。
中斷親和性
$ sudo bash -c ‘echo 1 > /proc/irq/8/smp_affinity’這個(gè)與網(wǎng)卡多隊(duì)列是密切相關(guān)的。如果網(wǎng)卡有多個(gè)隊(duì)列,就能手動(dòng)來明確指定由哪個(gè) CPU 來處理,均衡的把數(shù)據(jù)處理的負(fù)載分散到機(jī)器的可用 CPU 上。配置也比較簡單,只需把數(shù)字寫入到 /proc 對應(yīng)的這個(gè)文件中就可以了。這是個(gè)位數(shù)組,轉(zhuǎn)成二進(jìn)制后就會(huì)有對應(yīng)的 CPU 去處理。如果寫個(gè) 1,可能就是 CPU0 來處理;如果寫個(gè) 4,轉(zhuǎn)化成二進(jìn)制是 100,那么就會(huì)交給 CPU2 去處理。
另外有個(gè)小問題需要注意,很多發(fā)行版可能會(huì)自帶一個(gè) irqbalance 的守護(hù)進(jìn)程(http://irqbalance.github.io/irqbalance),會(huì)將手動(dòng)中斷均衡的設(shè)置給覆蓋掉。這個(gè)程序做的核心事情就是把上面手動(dòng)設(shè)置文件的操作放到程序里,有興趣可以去看下它的代碼(https://github.com/Irqbalance/irqbalance/blob/master/activate.c),也是把這個(gè)文件打開,寫對應(yīng)的數(shù)字進(jìn)去就可以了。
內(nèi)核-數(shù)據(jù)處理
最后是數(shù)據(jù)處理部分了。當(dāng)數(shù)據(jù)到達(dá)網(wǎng)卡,進(jìn)入隊(duì)列內(nèi)存后,就需要內(nèi)核從隊(duì)列內(nèi)存中將數(shù)據(jù)拉出來。如果機(jī)器的 PPS 達(dá)到了十萬甚至百萬,而 CPU 只處理網(wǎng)絡(luò)數(shù)據(jù)的話,那其他基本的業(yè)務(wù)邏輯也就不用干了,因此不能讓數(shù)據(jù)包的處理獨(dú)占整個(gè) CPU,而核心點(diǎn)是怎么去做限制。
針對上述問題主要有兩方面的限制:整體的限制和單次的限制
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;整體限制很好理解,就是一個(gè) CPU 對應(yīng)一個(gè)隊(duì)列。如果 CPU 的數(shù)量比隊(duì)列數(shù)量少,那么一個(gè) CPU 可能需要處理多個(gè)隊(duì)列。
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;單次限制則是限制一個(gè)隊(duì)列在一輪里處理包的數(shù)量。達(dá)到限制之后就停下來,等待下一輪的處理。
softnet_break:sd->time_squeeze++;_raise_softirq_irqoff(NET_RX_SOFTIRQ);goto out;而停下來就是很關(guān)鍵的節(jié)點(diǎn),幸運(yùn)的是有對應(yīng)的指標(biāo)記錄,有 time-squeeze 這樣中斷的計(jì)數(shù),拿到這個(gè)信息就可以判斷出機(jī)器的網(wǎng)絡(luò)處理是否有瓶頸,被迫中斷的頻率高低。
上圖是監(jiān)控 CPU 指標(biāo)的數(shù)據(jù),格式很簡單,每行對應(yīng)一個(gè) CPU,數(shù)值之間用空格分割,輸出格式為 16 進(jìn)制。那么每一列數(shù)值又代表什么呢?很不幸,這個(gè)沒有文檔,只能通過檢查使用的內(nèi)核版本,然后去看對應(yīng)的代碼。
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);下面說明了文件中每個(gè)字段都是怎么來的,實(shí)際情況可能會(huì)有所不同,因?yàn)殡S著內(nèi)核版本的迭代,字段的數(shù)量以及字段的順序都有可能發(fā)生變化,其中與網(wǎng)絡(luò)數(shù)據(jù)處理被中斷次數(shù)相關(guān)的就是 squeeze 字段:
-
sd->processed 處理的包數(shù)量(多網(wǎng)卡 bond 模式可能多于實(shí)際的收包數(shù)量)
-
sd->dropped 丟包數(shù)量,因?yàn)殛?duì)列滿了
-
sd->time_spueeze 軟中斷處理 net_rx_action 被迫打斷的次數(shù)
-
sd->cpu_collision 發(fā)送數(shù)據(jù)時(shí)獲取設(shè)備鎖沖突,比如多個(gè) CPU 同時(shí)發(fā)送數(shù)據(jù)
-
sd->received_rps 當(dāng)前 CPU 被喚醒的次數(shù)(通過處理器間中斷)
-
sd->flow_limit_count 觸發(fā) flow limit 的次數(shù)
下圖是業(yè)務(wù)中遇到相關(guān)問題的案例,最后排查到 CPU 層面。圖一是 TOP 命令的輸出,顯示了每個(gè) CPU 的使用量,其中紅框標(biāo)出的 CPU4 的使用率存在著異常,尤其是倒數(shù)第二列的 SI 占用達(dá)到了 89%。SI 是 softirq 的縮寫,表示 CPU 花在軟中斷處理上的時(shí)間占比,而圖中 CPU4 在時(shí)間占比上明顯過高。圖二則是對應(yīng)圖一的輸出結(jié)果,CPU4 對應(yīng)的是第五行,其中第三列數(shù)值明顯高于其他 CPU,表明它在處理網(wǎng)絡(luò)數(shù)據(jù)的時(shí)被頻繁的打斷。
針對上面的問題推斷 CPU4 存在一定的性能衰退,也許是質(zhì)量不過關(guān)或其他的原因。為了驗(yàn)證是否是性能衰退,寫了一個(gè)簡單的 python 腳本,一個(gè)一直去累加的死循環(huán)。每次運(yùn)行時(shí),把這段腳本綁定到某個(gè) CPU 上,然后觀察不同 CPU 耗時(shí)的對比。最后對比結(jié)果也顯示 CPU4 的耗時(shí)比其他的 CPU 高了幾倍,也驗(yàn)證了之前的推斷。之后協(xié)調(diào)運(yùn)維更換了 CPU,意向指標(biāo)也就恢復(fù)正常了。
總結(jié)
以上所有操作都只是在數(shù)據(jù)包從網(wǎng)卡到了內(nèi)核層,還沒到常見的協(xié)議,只是完成了萬里長征第一步,后面還有一系列的步驟,例如數(shù)據(jù)包的壓縮(GRO)、網(wǎng)卡多隊(duì)列軟件(RPS)還有 RFS 在負(fù)載均衡的基礎(chǔ)上考慮流的特征,就是 IP 端口四元組的特征,最后才是把數(shù)據(jù)遞交到 IP 層,以及到熟悉的 TCP 層。
總的來說,今天的分享都是圍繞驅(qū)動(dòng)來做的,我想強(qiáng)調(diào)的性能優(yōu)化的核心點(diǎn)在于指標(biāo),不能測量也就很難去改善,要有指標(biāo)的存在,這樣一切的優(yōu)化才有意義。
)
總結(jié)
以上是生活随笔為你收集整理的性能优化:如何更快地接收数据的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 灵魂画手:漫画图解 SSH
- 下一篇: 加密的艺术