您好,登錄后才能下訂單哦!
原本穩定的環境也因為請求量的上漲帶來了很多不穩定的因素,其中一直困擾我們的就是網卡丟包問題。起初線上存在部分Redis節點還在使用千兆網卡的老舊服務器,而緩存服務往往需要承載極高的查詢量,并要求毫秒級的響應速度,如此一來千兆網卡很快就出現了瓶頸。經過整治,我們將千兆網卡服務器替換為了萬兆網卡服務器,本以為可以高枕無憂,但是沒想到,在業務高峰時段,機器也竟然出現了丟包問題,而此時網卡帶寬使用還遠遠沒有達到瓶頸。
首先,我們在系統監控的net.if.in.dropped
指標中,看到有大量數據丟包異常,那么第一步就是要了解這個指標代表什么。
cdn.xitu.io/2019/6/18/16b69caa0edce2ac?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
這個指標的數據源,是讀取/proc/net/dev
中的數據,監控Agent做簡單的處理之后上報。以下為/proc/net/dev
的一個示例,可以看到第一行Receive代表in,Transmit代表out,第二行即各個表頭字段,再往后每一行代表一個網卡設備具體的值。
其中各個字段意義如下:
字段 | 解釋 |
---|---|
bytes | The total number of bytes of data transmitted or received by the interface. |
packets | The total number of packets of data transmitted or received by the interface. |
errs | The total number of transmit or receive errors detected by the device driver. |
drop | The total number of packets dropped by the device driver. |
fifo | The number of FIFO buffer errors. |
frame | The number of packet framing errors. |
colls | The number of collisions detected on the interface. |
compressed | The number of compressed packets transmitted or received by the device driver. (This appears to be unused in the 2.2.15 kernel.) |
carrier | The number of carrier losses detected by the device driver. |
multicast | The number of multicast frames transmitted or received by the device driver. |
通過上述字段解釋,我們可以了解丟包發生在網卡設備驅動層面;但是想要了解真正的原因,需要繼續深入源碼。
/proc/net/dev
的數據來源,根據源碼文件net/core/net-procfs.c
,可以知道上述指標是通過其中的dev_seq_show()
函數和dev_seq_printf_stats()
函數輸出的:
static?int?dev_seq_show(struct?seq_file?*seq,?void?*v)?{?????if?(v?==?SEQ_START_TOKEN)?????????/*?輸出/proc/net/dev表頭部分???*/?????????seq_puts(seq,?"Inter-|???Receive????????????????????????????"???????????????????"????????????????????|??Transmit\n"???????????????????"?face?|bytes????packets?errs?drop?fifo?frame?"???????????????????"compressed?multicast|bytes????packets?errs?"???????????????????"drop?fifo?colls?carrier?compressed\n");?????else?????????/*?輸出/proc/net/dev數據部分???*/?????????dev_seq_printf_stats(seq,?v);?????return?0;?}????static?void?dev_seq_printf_stats(struct?seq_file?*seq,?struct?net_device?*dev)?{?????struct?rtnl_link_stats64?temp;????????/*?數據源從下面的函數中取得???*/?????const?struct?rtnl_link_stats64?*stats?=?dev_get_stats(dev,?&temp);???????/*?/proc/net/dev?各個字段的數據算法???*/?????seq_printf(seq,?"%6s:?%7llu?%7llu?%4llu?%4llu?%4llu?%5llu?%10llu?%9llu?"????????????"%8llu?%7llu?%4llu?%4llu?%4llu?%5llu?%7llu?%10llu\n",????????????dev->name,?stats->rx_bytes,?stats->rx_packets,????????????stats->rx_errors,????????????stats->rx_dropped?+?stats->rx_missed_errors,????????????stats->rx_fifo_errors,????????????stats->rx_length_errors?+?stats->rx_over_errors?+?????????????stats->rx_crc_errors?+?stats->rx_frame_errors,????????????stats->rx_compressed,?stats->multicast,????????????stats->tx_bytes,?stats->tx_packets,????????????stats->tx_errors,?stats->tx_dropped,????????????stats->tx_fifo_errors,?stats->collisions,????????????stats->tx_carrier_errors?+?????????????stats->tx_aborted_errors?+?????????????stats->tx_window_errors?+?????????????stats->tx_heartbeat_errors,????????????stats->tx_compressed);?}?復制代碼
dev_seq_printf_stats()
函數里,對應drop輸出的部分,能看到由兩塊組成:stats-
>rx_dropped+stats
->rx_missed_errors
。
繼續查找dev_get_stats
函數可知,rx_dropped
和rx_missed_errors
都是從設備獲取的,并且需要設備驅動實現。
/**??*??dev_get_stats???-?get?network?device?statistics??*??@dev:?device?to?get?statistics?from??*??@storage:?place?to?store?stats??*??*??Get?network?statistics?from?device.?Return?@storage.??*??The?device?driver?may?provide?its?own?method?by?setting??*??dev->netdev_ops->get_stats64?or?dev->netdev_ops->get_stats;??*??otherwise?the?internal?statistics?structure?is?used.??*/?struct?rtnl_link_stats64?*dev_get_stats(struct?net_device?*dev,?????????????????????struct?rtnl_link_stats64?*storage)?{?????const?struct?net_device_ops?*ops?=?dev->netdev_ops;?????if?(ops->ndo_get_stats64)?{?????????memset(storage,?0,?sizeof(*storage));?????????ops->ndo_get_stats64(dev,?storage);?????}?else?if?(ops->ndo_get_stats)?{?????????netdev_stats_to_stats64(storage,?ops->ndo_get_stats(dev));?????}?else?{?????????netdev_stats_to_stats64(storage,?&dev->stats);?????}????????storage->rx_dropped?+=?(unsigned?long)atomic_long_read(&dev->rx_dropped);?????storage->tx_dropped?+=?(unsigned?long)atomic_long_read(&dev->tx_dropped);?????storage->rx_nohandler?+=?(unsigned?long)atomic_long_read(&dev->rx_nohandler);?????return?storage;?}?復制代碼
結構體 rtnl_link_stats64
的定義在 /usr/include/linux/if_link.h
中:
/*?The?main?device?statistics?structure?*/?struct?rtnl_link_stats64?{?????__u64???rx_packets;?????/*?total?packets?received???*/?????__u64???tx_packets;?????/*?total?packets?transmitted????*/?????__u64???rx_bytes;???????/*?total?bytes?received?????*/?????__u64???tx_bytes;???????/*?total?bytes?transmitted??*/?????__u64???rx_errors;??????/*?bad?packets?received?????*/?????__u64???tx_errors;??????/*?packet?transmit?problems?*/?????__u64???rx_dropped;?????/*?no?space?in?linux?buffers????*/?????__u64???tx_dropped;?????/*?no?space?available?in?linux??*/?????__u64???multicast;??????/*?multicast?packets?received???*/?????__u64???collisions;???????/*?detailed?rx_errors:?*/?????__u64???rx_length_errors;?????__u64???rx_over_errors;?????/*?receiver?ring?buff?overflow??*/?????__u64???rx_crc_errors;??????/*?recved?pkt?with?crc?error????*/?????__u64???rx_frame_errors;????/*?recv'd?frame?alignment?error?*/?????__u64???rx_fifo_errors;?????/*?recv'r?fifo?overrun??????*/?????__u64???rx_missed_errors;???/*?receiver?missed?packet???*/???????/*?detailed?tx_errors?*/?????__u64???tx_aborted_errors;?????__u64???tx_carrier_errors;?????__u64???tx_fifo_errors;?????__u64???tx_heartbeat_errors;?????__u64???tx_window_errors;???????/*?for?cslip?etc?*/?????__u64???rx_compressed;?????__u64???tx_compressed;?};?復制代碼
至此,我們知道rx_dropped
是Linux中的緩沖區空間不足導致的丟包,而rx_missed_errors
則在注釋中寫的比較籠統。有資料指出,rx_missed_errors
是fifo隊列(即rx ring buffer
)滿而丟棄的數量,但這樣的話也就和rx_fifo_errors
等同了。后來公司內網絡內核研發大牛王偉給了我們點撥:不同網卡自己實現不一樣,比如Intel的igb網卡rx_fifo_errors
在missed
的基礎上,還加上了RQDPC
計數,而ixgbe
就沒這個統計。RQDPC計數是描述符不夠的計數,missed
是fifo
滿的計數。所以對于ixgbe
來說,rx_fifo_errors
和rx_missed_errors
確實是等同的。
通過命令ethtool -S eth0
可以查看網卡一些統計信息,其中就包含了上文提到的幾個重要指標rx_dropped
、rx_missed_errors
、rx_fifo_errors
等。但實際測試后,我發現不同網卡型號給出的指標略有不同,比如Intel ixgbe
就能取到,而Broadcom bnx2/tg3
則只能取到rx_discards
(對應rx_fifo_errors
)、rx_fw_discards
(對應rx_dropped
)。這表明,各家網卡廠商設備內部對這些丟包的計數器、指標的定義略有不同,但通過驅動向內核提供的統計數據都封裝成了struct rtnl_link_stats64
定義的格式。
在對丟包服務器進行檢查后,發現rx_missed_errors
為0,丟包全部來自rx_dropped
。說明丟包發生在Linux內核的緩沖區中。接下來,我們要繼續探索到底是什么緩沖區引起了丟包問題,這就需要完整地了解服務器接收數據包的過程。
接收數據包是一個復雜的過程,涉及很多底層的技術細節,但大致需要以下幾個步驟:
網卡收到數據包。
將數據包從網卡硬件緩存轉移到服務器內存中。
通知內核處理。
經過TCP/IP協議逐層處理。
應用程序通過read()
從socket buffer
讀取數據。
NIC在接收到數據包之后,首先需要將數據同步到內核中,這中間的橋梁是rx ring buffer
。它是由NIC和驅動程序共享的一片區域,事實上,rx ring buffer
存儲的并不是實際的packet數據,而是一個描述符,這個描述符指向了它真正的存儲地址,具體流程如下:
驅動在內存中分配一片緩沖區用來接收數據包,叫做sk_buffer
;
將上述緩沖區的地址和大小(即接收描述符),加入到rx ring buffer
。描述符中的緩沖區地址是DMA使用的物理地址;
驅動通知網卡有一個新的描述符;
網卡從rx ring buffer
中取出描述符,從而獲知緩沖區的地址和大小;
網卡收到新的數據包;
網卡將新數據包通過DMA直接寫到sk_buffer
中。
當驅動處理速度跟不上網卡收包速度時,驅動來不及分配緩沖區,NIC接收到的數據包無法及時寫到sk_buffer
,就會產生堆積,當NIC內部緩沖區寫滿后,就會丟棄部分數據,引起丟包。這部分丟包為rx_fifo_errors
,在/proc/net/dev
中體現為fifo字段增長,在ifconfig中體現為overruns指標增長。
這個時候,數據包已經被轉移到了sk_buffer
中。前文提到,這是驅動程序在內存中分配的一片緩沖區,并且是通過DMA寫入的,這種方式不依賴CPU直接將數據寫到了內存中,意味著對內核來說,其實并不知道已經有新數據到了內存中。那么如何讓內核知道有新數據進來了呢?答案就是中斷,通過中斷告訴內核有新數據進來了,并需要進行后續處理。
提到中斷,就涉及到硬中斷和軟中斷,首先需要簡單了解一下它們的區別:
硬中斷: 由硬件自己生成,具有隨機性,硬中斷被CPU接收后,觸發執行中斷處理程序。中斷處理程序只會處理關鍵性的、短時間內可以處理完的工作,剩余耗時較長工作,會放到中斷之后,由軟中斷來完成。硬中斷也被稱為上半部分。
軟中斷: 由硬中斷對應的中斷處理程序生成,往往是預先在代碼里實現好的,不具有隨機性。(除此之外,也有應用程序觸發的軟中斷,與本文討論的網卡收包無關。)也被稱為下半部分。
當NIC把數據包通過DMA復制到內核緩沖區sk_buffer
后,NIC立即發起一個硬件中斷。CPU接收后,首先進入上半部分,網卡中斷對應的中斷處理程序是網卡驅動程序的一部分,之后由它發起軟中斷,進入下半部分,開始消費sk_buffer
中的數據,交給內核協議棧處理。
通過中斷,能夠快速及時地響應網卡數據請求,但如果數據量大,那么會產生大量中斷請求,CPU大部分時間都忙于處理中斷,效率很低。為了解決這個問題,現在的內核及驅動都采用一種叫NAPI(new API)的方式進行數據處理,其原理可以簡單理解為 中斷+輪詢,在數據量大時,一次中斷后通過輪詢接收一定數量包再返回,避免產生多次中斷。
整個中斷過程的源碼部分比較復雜,并且不同驅動的廠商及版本也會存在一定的區別。 以下調用關系基于Linux-3.10.108及內核自帶驅動drivers/net/ethernet/intel/ixgbe
:
注意到,enqueue_to_backlog
函數中,會對CPU的softnet_data
實例中的接收隊列(input_pkt_queue
)進行判斷,如果隊列中的數據長度超過netdev_max_backlog
,那么數據包將直接丟棄,這就產生了丟包。netdev_max_backlog
是由系統參數net.core.netdev_max_backlog
指定的,默認大小是 1000。
?/*??*?enqueue_to_backlog?is?called?to?queue?an?skb?to?a?per?CPU?backlog??*?queue?(may?be?a?remote?CPU?queue).??*/?static?int?enqueue_to_backlog(struct?sk_buff?*skb,?int?cpu,???????????????????unsigned?int?*qtail)?{?????struct?softnet_data?*sd;?????unsigned?long?flags;???????sd?=?&per_cpu(softnet_data,?cpu);???????local_irq_save(flags);???????rps_lock(sd);????????/*?判斷接收隊列是否滿,隊列長度為?netdev_max_backlog??*/??????if?(skb_queue_len(&sd->input_pkt_queue)?<=?netdev_max_backlog)?{??????????????????????if?(skb_queue_len(&sd->input_pkt_queue))?{?enqueue:?????????????/*??隊列如果不會空,將數據包添加到隊列尾??*/?????????????__skb_queue_tail(&sd->input_pkt_queue,?skb);?????????????input_queue_tail_incr_save(sd,?qtail);?????????????rps_unlock(sd);?????????????local_irq_restore(flags);?????????????return?NET_RX_SUCCESS;?????????}??????????????/*?Schedule?NAPI?for?backlog?device??????????*?We?can?use?non?atomic?operation?since?we?own?the?queue?lock??????????*/?????????/*??隊列如果為空,回到?____napi_schedule加入poll_list輪詢部分,并重新發起軟中斷??*/??????????if?(!__test_and_set_bit(NAPI_STATE_SCHED,?&sd->backlog.state))?{?????????????if?(!rps_ipi_queued(sd))?????????????????____napi_schedule(sd,?&sd->backlog);?????????}????????????goto?enqueue;?????}???????/*?隊列滿則直接丟棄,對應計數器?+1?*/??????sd->dropped++;?????rps_unlock(sd);???????local_irq_restore(flags);???????atomic_long_inc(&skb->dev->rx_dropped);?????kfree_skb(skb);?????return?NET_RX_DROP;?}?復制代碼
內核會為每個CPU Core
都實例化一個softnet_data
對象,這個對象中的input_pkt_queue
用于管理接收的數據包。假如所有的中斷都由一個CPU Core
來處理的話,那么所有數據包只能經由這個CPU的input_pkt_queue
,如果接收的數據包數量非常大,超過中斷處理速度,那么input_pkt_queue
中的數據包就會堆積,直至超過netdev_max_backlog
,引起丟包。這部分丟包可以在cat /proc/net/softnet_stat
的輸出結果中進行確認:
其中每行代表一個CPU,第一列是中斷處理程序接收的幀數,第二列是由于超過 netdev_max_backlog
而丟棄的幀數。 第三列則是在net_rx_action
函數中處理數據包超過netdev_budge
指定數量或運行時間超過2個時間片的次數。在檢查線上服務器之后,發現第一行CPU。硬中斷的中斷號及統計數據可以在/proc/interrupts
中看到,對于多隊列網卡,當系統啟動并加載NIC設備驅動程序模塊時,每個RXTX隊列會被初始化分配一個唯一的中斷向量號,它通知中斷處理程序該中斷來自哪個NIC隊列。在默認情況下,所有隊列的硬中斷都由CPU 0處理,因此對應的軟中斷邏輯也會在CPU 0上處理,在服務器 TOP 的輸出中,也可以觀察到 %si 軟中斷部分,CPU 0的占比比其他core高出一截。
到這里其實有存在一個疑惑,我們線上服務器的內核版本及網卡都支持NAPI,而NAPI的處理邏輯是不會走到enqueue_to_backlog
中的,enqueue_to_backlog
主要是非NAPI的處理流程中使用的。對此,我們覺得可能和當前使用的Docker架構有關,事實上,我們通過net.if.dropped
指標獲取到的丟包,都發生在Docker虛擬網卡上,而非宿主機物理網卡上,因此很可能是Docker虛擬網橋轉發數據包之后,虛擬網卡層面產生的丟包,這里由于涉及虛擬化部分,就不進一步分析了。
驅動及內核處理過程中的幾個重要函數:
(1)注冊中斷號及中斷處理程序,根據網卡是否支持MSI/MSIX
,結果為:MSIX
→ ixgbe_msix_clean_rings
,MSI
→ ixgbe_intr
,都不支持 → ixgbe_intr
。
/**??*?文件:ixgbe_main.c??*?ixgbe_request_irq?-?initialize?interrupts??*?@adapter:?board?private?structure??*??*?Attempts?to?configure?interrupts?using?the?best?available??*?capabilities?of?the?hardware?and?kernel.??**/?static?int?ixgbe_request_irq(struct?ixgbe_adapter?*adapter)?{?????struct?net_device?*netdev?=?adapter->netdev;?????int?err;???????/*?支持MSIX,調用?ixgbe_request_msix_irqs?設置中斷處理程序*/?????if?(adapter->flags?&?IXGBE_FLAG_MSIX_ENABLED)?????????err?=?ixgbe_request_msix_irqs(adapter);?????/*?支持MSI,直接設置?ixgbe_intr?為中斷處理程序?*/?????else?if?(adapter->flags?&?IXGBE_FLAG_MSI_ENABLED)?????????err?=?request_irq(adapter->pdev->irq,?&ixgbe_intr,?0,???????????????????netdev->name,?adapter);?????/*?都不支持的情況,直接設置?ixgbe_intr?為中斷處理程序?*/?????else??????????err?=?request_irq(adapter->pdev->irq,?&ixgbe_intr,?IRQF_SHARED,???????????????????netdev->name,?adapter);???????if?(err)?????????e_err(probe,?"request_irq?failed,?Error?%d\n",?err);???????return?err;?}????/**??*?文件:ixgbe_main.c??*?ixgbe_request_msix_irqs?-?Initialize?MSI-X?interrupts??*?@adapter:?board?private?structure??*??*?ixgbe_request_msix_irqs?allocates?MSI-X?vectors?and?requests??*?interrupts?from?the?kernel.??**/?static?int?(struct?ixgbe_adapter?*adapter)?{?????…?????for?(vector?=?0;?vector?<?adapter->num_q_vectors;?vector++)?{?????????struct?ixgbe_q_vector?*q_vector?=?adapter->q_vector[vector];?????????struct?msix_entry?*entry?=?&adapter->msix_entries[vector];???????????/*?設置中斷處理入口函數為?ixgbe_msix_clean_rings?*/?????????err?=?request_irq(entry->vector,?&ixgbe_msix_clean_rings,?0,???????????????????q_vector->name,?q_vector);?????????if?(err)?{?????????????e_err(probe,?"request_irq?failed?for?MSIX?interrupt?'%s'?"???????????????????"Error:?%d\n",?q_vector->name,?err);?????????????goto?free_queue_irqs;?????????}?????…?????}?}?復制代碼
(2)線上的多隊列網卡均支持MSIX,中斷處理程序入口為ixgbe_msix_clean_rings
,里面調用了函數napi_schedule(&q_vector->napi)
。
/**??*?文件:ixgbe_main.c??**/?static?irqreturn_t?ixgbe_msix_clean_rings(int?irq,?void?*data)?{?????struct?ixgbe_q_vector?*q_vector?=?data;???????/*?EIAM?disabled?interrupts?(on?this?vector)?for?us?*/???????if?(q_vector->rx.ring?||?q_vector->tx.ring)?????????napi_schedule(&q_vector->napi);???????return?IRQ_HANDLED;?}?復制代碼
(3)之后經過一些列調用,直到發起名為NET_RX_SOFTIRQ
的軟中斷。到這里完成了硬中斷部分,進入軟中斷部分,同時也上升到了內核層面。
/**??*?文件:include/linux/netdevice.h??*??napi_schedule?-?schedule?NAPI?poll??*??@n:?NAPI?context??*??*?Schedule?NAPI?poll?routine?to?be?called?if?it?is?not?already??*?running.??*/?static?inline?void?napi_schedule(struct?napi_struct?*n)?{?????if?(napi_schedule_prep(n))?????/*??注意下面調用的這個函數名字前是兩個下劃線?*/?????????__napi_schedule(n);?}???/**??*?文件:net/core/dev.c??*?__napi_schedule?-?schedule?for?receive??*?@n:?entry?to?schedule??*??*?The?entry's?receive?function?will?be?scheduled?to?run.??*?Consider?using?__napi_schedule_irqoff()?if?hard?irqs?are?masked.??*/?void?__napi_schedule(struct?napi_struct?*n)?{?????unsigned?long?flags;???????/*??local_irq_save用來保存中斷狀態,并禁止中斷?*/?????local_irq_save(flags);?????/*??注意下面調用的這個函數名字前是四個下劃線,傳入的?softnet_data?是當前CPU?*/?????____napi_schedule(this_cpu_ptr(&softnet_data),?n);?????local_irq_restore(flags);?}????/*?Called?with?irq?disabled?*/?static?inline?void?____napi_schedule(struct?softnet_data?*sd,??????????????????????struct?napi_struct?*napi)?{?????/*?將?napi_struct?加入?softnet_data?的?poll_list?*/?????list_add_tail(&napi->poll_list,?&sd->poll_list);????????/*?發起軟中斷?NET_RX_SOFTIRQ?*/?????__raise_softirq_irqoff(NET_RX_SOFTIRQ);?}?復制代碼
(4)NET_RX_SOFTIRQ
對應的軟中斷處理程序接口是net_rx_action()
。
/*??*??文件:net/core/dev.c??*??Initialize?the?DEV?module.?At?boot?time?this?walks?the?device?list?and??*??unhooks?any?devices?that?fail?to?initialise?(normally?hardware?not??*??present)?and?leaves?us?with?a?valid?list?of?present?and?active?devices.??*??*/???/*??*???????This?is?called?single?threaded?during?boot,?so?no?need??*???????to?take?the?rtnl?semaphore.??*/?static?int?__init?net_dev_init(void)?{?????…?????/*??分別注冊TX和RX軟中斷的處理程序?*/?????open_softirq(NET_TX_SOFTIRQ,?net_tx_action);?????open_softirq(NET_RX_SOFTIRQ,?net_rx_action);?????…?}?復制代碼
(5)net_rx_action功能就是輪詢調用poll方法,這里就是ixgbe_poll。一次輪詢的數據包數量不能超過內核參數net.core.netdev_budget指定的數量(默認值300),并且輪詢時間不能超過2個時間片。這個機制保證了單次軟中斷處理不會耗時太久影響被中斷的程序。
/*?文件:net/core/dev.c??*/?static?void?net_rx_action(struct?softirq_action?*h)?{?????struct?softnet_data?*sd?=?&__get_cpu_var(softnet_data);?????unsigned?long?time_limit?=?jiffies?+?2;?????int?budget?=?netdev_budget;?????void?*have;???????local_irq_disable();???????while?(!list_empty(&sd->poll_list))?{?????????struct?napi_struct?*n;?????????int?work,?weight;???????????/*?If?softirq?window?is?exhuasted?then?punt.??????????*?Allow?this?to?run?for?2?jiffies?since?which?will?allow??????????*?an?average?latency?of?1.5/HZ.??????????*/????????????/*?判斷處理包數是否超過?netdev_budget?及時間是否超過2個時間片?*/?????????if?(unlikely(budget?<=?0?||?time_after_eq(jiffies,?time_limit)))?????????????goto?softnet_break;???????????local_irq_enable();???????????/*?Even?though?interrupts?have?been?re-enabled,?this??????????*?access?is?safe?because?interrupts?can?only?add?new??????????*?entries?to?the?tail?of?this?list,?and?only?->poll()??????????*?calls?can?remove?this?head?entry?from?the?list.??????????*/?????????n?=?list_first_entry(&sd->poll_list,?struct?napi_struct,?poll_list);???????????have?=?netpoll_poll_lock(n);???????????weight?=?n->weight;???????????/*?This?NAPI_STATE_SCHED?test?is?for?avoiding?a?race??????????*?with?netpoll's?poll_napi().??Only?the?entity?which??????????*?obtains?the?lock?and?sees?NAPI_STATE_SCHED?set?will??????????*?actually?make?the?->poll()?call.??Therefore?we?avoid??????????*?accidentally?calling?->poll()?when?NAPI?is?not?scheduled.??????????*/?????????work?=?0;?????????if?(test_bit(NAPI_STATE_SCHED,?&n->state))?{?????????????work?=?n->poll(n,?weight);?????????????trace_napi_poll(n);?????????}???????????……?????}????}?復制代碼
(6)ixgbe_poll
之后的一系列調用就不一一詳述了,有興趣的同學可以自行研究,軟中斷部分有幾個地方會有類似if (static_key_false(&rps_needed))
這樣的判斷,會進入前文所述有丟包風險的enqueue_to_backlog
函數。 這里的邏輯為判斷是否啟用了RPS機制,RPS是早期單隊列網卡上將軟中斷負載均衡到多個CPU Core
的技術,它對數據流進行hash并分配到對應的CPU Core
上,發揮多核的性能。不過現在基本都是多隊列網卡,不會開啟這個機制,因此走不到這里,static_key_false
是針對默認為false
的static key
的優化判斷方式。這段調用的最后,deliver_skb
會將接收的數據傳入一個IP層的數據結構中,至此完成二層的全部處理。
/**??*??netif_receive_skb?-?process?receive?buffer?from?network??*??@skb:?buffer?to?process??*??*??netif_receive_skb()?is?the?main?receive?data?processing?function.??*??It?always?succeeds.?The?buffer?may?be?dropped?during?processing??*??for?congestion?control?or?by?the?protocol?layers.??*??*??This?function?may?only?be?called?from?softirq?context?and?interrupts??*??should?be?enabled.??*??*??Return?values?(usually?ignored):??*??NET_RX_SUCCESS:?no?congestion??*??NET_RX_DROP:?packet?was?dropped??*/?int?netif_receive_skb(struct?sk_buff?*skb)?{?????int?ret;???????net_timestamp_check(netdev_tstamp_prequeue,?skb);???????if?(skb_defer_rx_timestamp(skb))?????????return?NET_RX_SUCCESS;???????rcu_read_lock();???#ifdef?CONFIG_RPS?????/*?判斷是否啟用RPS機制?*/?????if?(static_key_false(&rps_needed))?{?????????struct?rps_dev_flow?voidflow,?*rflow?=?&voidflow;?????????/*?獲取對應的CPU?Core?*/?????????int?cpu?=?get_rps_cpu(skb->dev,?skb,?&rflow);???????????if?(cpu?>=?0)?{?????????????ret?=?enqueue_to_backlog(skb,?cpu,?&rflow->last_qtail);?????????????rcu_read_unlock();?????????????return?ret;?????????}?????}?#endif?????ret?=?__netif_receive_skb(skb);?????rcu_read_unlock();?????return?ret;?}?復制代碼
數據包進到IP層之后,經過IP層、TCP層處理(校驗、解析上層協議,發送給上層協議),放入socket buffer
,在應用程序執行read() 系統調用時,就能從socket buffer中將新數據從內核區拷貝到用戶區,完成讀取。
這里的socket buffer
大小即TCP接收窗口,TCP由于具備流量控制功能,能動態調整接收窗口大小,因此數據傳輸階段不會出現由于socket buffer
接收隊列空間不足而丟包的情況(但UDP及TCP握手階段仍會有)。涉及TCP/IP協議的部分不是此次丟包問題的研究重點,因此這里不再贅述。
查看網卡型號
??#?lspci?-vvv?|?grep?Eth?01:00.0?Ethernet?controller:?Intel?Corporation?Ethernet?Controller?10-Gigabit?X540-AT2?(rev?03)?????????Subsystem:?Dell?Ethernet?10G?4P?X540/I350?rNDC?01:00.1?Ethernet?controller:?Intel?Corporation?Ethernet?Controller?10-Gigabit?X540-AT2?(rev?03)?????????Subsystem:?Dell?Ethernet?10G?4P?X540/I350?rNDC????#?lspci?-vvv?07:00.0?Ethernet?controller:?Intel?Corporation?I350?Gigabit?Network?Connection?(rev?01)?????????Subsystem:?Dell?Gigabit?4P?X540/I350?rNDC?????????Control:?I/O-?Mem+?BusMaster+?SpecCycle-?MemWINV-?VGASnoop-?ParErr-?Stepping-?SERR-?FastB2B-?DisINTx+?????????Status:?Cap+?66MHz-?UDF-?FastB2B-?ParErr-?DEVSEL=fast?>TAbort-?<TAbort-?<MAbort-?>SERR-?<PERR-?INTx-?????????Latency:?0,?Cache?Line?Size:?128?bytes?????????Interrupt:?pin?D?routed?to?IRQ?19?????????Region?0:?Memory?at?92380000?(32-bit,?non-prefetchable)?[size=512K]?????????Region?3:?Memory?at?92404000?(32-bit,?non-prefetchable)?[size=16K]?????????Expansion?ROM?at?92a00000?[disabled]?[size=512K]?????????Capabilities:?[40]?Power?Management?version?3?????????????????Flags:?PMEClk-?DSI+?D1-?D2-?AuxCurrent=0mA?PME(D0+,D1-,D2-,D3hot+,D3cold+)?????????????????Status:?D0?NoSoftRst+?PME-Enable-?DSel=0?DScale=1?PME-?????????Capabilities:?[50]?MSI:?Enable-?Count=1/1?Maskable+?64bit+?????????????????Address:?0000000000000000??Data:?0000?????????????????Masking:?00000000??Pending:?00000000?????????Capabilities:?[70]?MSI-X:?Enable+?Count=10?Masked-?????????????????Vector?table:?BAR=3?offset=00000000?????????????????PBA:?BAR=3?offset=00002000?復制代碼
可以看出,網卡的中斷機制是MSI-X,即網卡的每個隊列都可以分配中斷(MSI-X支持2048個中斷)。
網卡隊列
?...??#define?IXGBE_MAX_MSIX_VECTORS_82599????0x40?...????????u16?ixgbe_get_pcie_msix_count_generic(struct?ixgbe_hw?*hw)??{??????u16?msix_count;??????u16?max_msix_count;??????u16?pcie_offset;?????????switch?(hw->mac.type)?{??????case?ixgbe_mac_82598EB:??????????pcie_offset?=?IXGBE_PCIE_MSIX_82598_CAPS;??????????max_msix_count?=?IXGBE_MAX_MSIX_VECTORS_82598;??????????break;??????case?ixgbe_mac_82599EB:??????case?ixgbe_mac_X540:??????case?ixgbe_mac_X550:??????case?ixgbe_mac_X550EM_x:??????case?ixgbe_mac_x550em_a:??????????pcie_offset?=?IXGBE_PCIE_MSIX_82599_CAPS;??????????max_msix_count?=?IXGBE_MAX_MSIX_VECTORS_82599;??????????break;??????default:??????????return?1;??????}??...?復制代碼
根據網卡型號確定驅動中定義的網卡隊列,可以看到X540網卡驅動中定義最大支持的IRQ Vector為0x40(數值:64)。
?static?int?ixgbe_acquire_msix_vectors(struct?ixgbe_adapter?*adapter)??{??????struct?ixgbe_hw?*hw?=?&adapter->hw;??????int?i,?vectors,?vector_threshold;?????????/*?We?start?by?asking?for?one?vector?per?queue?pair?with?XDP?queues???????*?being?stacked?with?TX?queues.???????*/??????vectors?=?max(adapter->num_rx_queues,?adapter->num_tx_queues);??????vectors?=?max(vectors,?adapter->num_xdp_queues);?????????/*?It?is?easy?to?be?greedy?for?MSI-X?vectors.?However,?it?really???????*?doesn't?do?much?good?if?we?have?a?lot?more?vectors?than?CPUs.?We'll???????*?be?somewhat?conservative?and?only?ask?for?(roughly)?the?same?number???????*?of?vectors?as?there?are?CPUs.???????*/??????vectors?=?min_t(int,?vectors,?num_online_cpus());?復制代碼
通過加載網卡驅動,獲取網卡型號和網卡硬件的隊列數;但是在初始化misx vector的時候,還會結合系統在線CPU的數量,通過Sum = Min(網卡隊列,CPU Core) 來激活相應的網卡隊列數量,并申請Sum個中斷號。
如果CPU數量小于64,會生成CPU數量的隊列,也就是每個CPU會產生一個external IRQ。
我們線上的CPU一般是48個邏輯core,就會生成48個中斷號,由于我們是兩塊網卡做了bond,也就會生成96個中斷號。
我們在測試環境做了測試,發現測試環境的中斷確實有集中在CPU 0
的情況,下面使用systemtap
診斷測試環境軟中斷分布的方法:
global?hard,?soft,?wq????probe?irq_handler.entry?{?hard[irq,?dev_name]++;?}????probe?timer.s(1)?{?println("==irq?number:dev_name")?foreach(?[irq,?dev_name]?in?hard-?limit?5)?{?printf("%d,%s->%d\n",?irq,?kernel_string(dev_name),?hard[irq,?dev_name]);???????}???println("==softirq?cpu:h:vec:action")?foreach(?[c,h,vec,action]?in?soft-?limit?5)?{?printf("%d:%x:%x:%s->%d\n",?c,?h,?vec,?symdata(action),?soft[c,h,vec,action]);???????}??????println("==workqueue?wq_thread:work_func")?foreach(?[wq_thread,work_func]?in?wq-?limit?5)?{?printf("%x:%x->%d\n",?wq_thread,?work_func,?wq[wq_thread,?work_func]);??}????println("\n")?delete?hard?delete?soft?delete?wq?}????probe?softirq.entry?{?soft[cpu(),?h,vec,action]++;?}????probe?workqueue.execute?{?wq[wq_thread,?work_func]++?}???????probe?begin?{?println("~")?}?復制代碼
下面執行i.stap
的結果:
==irq?number:dev_name?87,eth0-0->1693?90,eth0-3->1263?95,eth2-3->746?92,eth2-0->703?89,eth0-2->654?==softirq?cpu:h:vec:action?0:ffffffff81a83098:ffffffff81a83080:0xffffffff81461a00->8928?0:ffffffff81a83088:ffffffff81a83080:0xffffffff81084940->626?0:ffffffff81a830c8:ffffffff81a83080:0xffffffff810ecd70->614?16:ffffffff81a83088:ffffffff81a83080:0xffffffff81084940->225?16:ffffffff81a830c8:ffffffff81a83080:0xffffffff810ecd70->224?==workqueue?wq_thread:work_func?ffff88083062aae0:ffffffffa01c53d0->10?ffff88083062aae0:ffffffffa01ca8f0->10?ffff88083420a080:ffffffff81142160->2?ffff8808343fe040:ffffffff8127c9d0->2?ffff880834282ae0:ffffffff8133bd20->1?復制代碼
下面是action
對應的符號信息:
addr2line?-e?/usr/lib/debug/lib/modules/2.6.32-431.20.3.el6.mt20161028.x86_64/vmlinux?ffffffff81461a00?/usr/src/debug/kernel-2.6.32-431.20.3.el6/linux-2.6.32-431.20.3.el6.mt20161028.x86_64/net/core/dev.c:4013?復制代碼
打開這個文件,我們發現它是在執行static void net_rx_action(struct softirq_action *h)
這個函數,而這個函數正是前文提到的,NET_RX_SOFTIRQ
對應的軟中斷處理程序。因此可以確認網卡的軟中斷在機器上分布非常不均,而且主要集中在CPU 0
上。通過/proc/interrupts
能確認硬中斷集中在CPU 0
上,因此軟中斷也都由CPU 0
處理,如何優化網卡的中斷成為了我們關注的重點。
前文提到,丟包是因為隊列中的數據包超過了netdev_max_backlog
造成了丟棄,因此首先想到是臨時調大netdev_max_backlog
能否解決燃眉之急,事實證明,對于輕微丟包調大參數可以緩解丟包,但對于大量丟包則幾乎不怎么管用,內核處理速度跟不上收包速度的問題還是客觀存在,本質還是因為單核處理中斷有瓶頸,即使不丟包,服務響應速度也會變慢。因此如果能同時使用多個CPU Core
來處理中斷,就能顯著提高中斷處理的效率,并且每個CPU都會實例化一個softnet_data
對象,隊列數也增加了。
通過設置中斷親緣性,可以讓指定的中斷向量號更傾向于發送給指定的CPU Core
來處理,俗稱“綁核”。命令grep eth /proc/interrupts
的第一列可以獲取網卡的中斷號,如果是多隊列網卡,那么就會有多行輸出:
中斷的親緣性設置可以在cat /proc/irq/${中斷號}/smp_affinity 或 cat /proc/irq/${中斷號}/smp_affinity_list
中確認,前者是16進制掩碼形式,后者是以CPU Core
序號形式。例如下圖中,將16進制的400轉換成2進制后,為 10000000000,“1”在第10位上,表示親緣性是第10個CPU Core
。
那為什么中斷號只設置一個CPU Core
呢?而不是為每一個中斷號設置多個CPU Core
平行處理。我們經過測試,發現當給中斷設置了多個CPU Core
后,它也僅能由設置的第一個CPU Core
來處理,其他的CPU Core
并不會參與中斷處理,原因猜想是當CPU可以平行收包時,不同的核收取了同一個queue的數據包,但處理速度不一致,導致提交到IP層后的順序也不一致,這就會產生亂序的問題,由同一個核來處理可以避免了亂序問題。
但是,當我們配置了多個Core處理中斷后,發現Redis的慢查詢數量有明顯上升,甚至部分業務也受到了影響,慢查詢增多直接導致可用性降低,因此方案仍需進一步優化。
如果某個CPU Core
正在處理Redis的調用,執行到一半時產生了中斷,那么CPU不得不停止當前的工作轉而處理中斷請求,中斷期間Redis也無法轉交給其他core繼續運行,必須等處理完中斷后才能繼續運行。Redis本身定位就是高速緩存,線上的平均端到端響應時間小于1ms,如果頻繁被中斷,那么響應時間必然受到極大影響。容易想到,由最初的CPU 0
單核處理中斷,改進到多核處理中斷,Redis進程被中斷影響的幾率增大了,因此我們需要對Redis進程也設置CPU親緣性,使其與處理中斷的Core互相錯開,避免受到影響。
使用命令taskset
可以為進程設置CPU親緣性,操作十分簡單,一句taskset -cp cpu-list pid
即可完成綁定。經過一番壓測,我們發現使用8個core處理中斷時,流量直至打滿雙萬兆網卡也不會出現丟包,因此決定將中斷的親緣性設置為物理機上前8個core,Redis進程的親緣性設置為剩下的所有core。調整后,確實有明顯的效果,慢查詢數量大幅優化,但對比初始情況,仍然還是高了一些些,還有沒有優化空間呢?
通過觀察,我們發現一個有趣的現象,當只有CPU 0處理中斷時,Redis進程更傾向于運行在CPU 0,以及CPU 0同一物理CPU下的其他核上。于是有了以下推測:我們設置的中斷親緣性,是直接選取了前8個核心,但這8個core卻可能是來自兩塊物理CPU的,在/proc/cpuinfo
中,通過字段processor
和physical id
能確認這一點,那么響應慢是否和物理CPU有關呢?物理CPU又和NUMA架構關聯,每個物理CPU對應一個NUMA node
,那么接下來就要從NUMA角度進行分析。
隨著單核CPU的頻率在制造工藝上的瓶頸,CPU制造商的發展方向也由縱向變為橫向:從CPU頻率轉為每瓦性能。CPU也就從單核頻率時代過渡到多核性能協調。
SMP(對稱多處理結構):即CPU共享所有資源,例如總線、內存、IO等。
SMP 結構:一個物理CPU可以有多個物理Core,每個Core又可以有多個硬件線程。即:每個HT有一個獨立的L1 cache,同一個Core下的HT共享L2 cache,同一個物理CPU下的多個core共享L3 cache。
下圖(摘自內核月談)中,一個x86 CPU有4個物理Core,每個Core有兩個HT(Hyper Thread)。
在前面的FSB(前端系統總線)結構中,當CPU不斷增長的情況下,共享的系統總線就會因為資源競爭(多核爭搶總線資源以訪問北橋上的內存)而出現擴展和性能問題。
在這樣的背景下,基于SMP架構上的優化,設計出了NUMA(Non-Uniform Memory Access)非均勻內存訪問。
內存控制器芯片被集成到處理器內部,多個處理器通過QPI鏈路相連,DRAM也就有了遠近之分。(如下圖所示:摘自CPU Cache)
CPU 多層Cache的性能差異是很巨大的,比如:L1的訪問時長1ns,L2的時長3ns…跨node的訪問會有幾十甚至上百倍的性能損耗。
這時我們再回歸到中斷的問題上,當兩個NUMA節點處理中斷時,CPU實例化的softnet_data
以及驅動分配的sk_buffer
都可能是跨Node的,數據接收后對上層應用Redis來說,跨Node訪問的幾率也大大提高,并且無法充分利用L2、L3 cache,增加了延時。
同時,由于Linux wake affinity
特性,如果兩個進程頻繁互動,調度系統會覺得它們很有可能共享同樣的數據,把它們放到同一CPU核心或NUMA Node
有助于提高緩存和內存的訪問性能,所以當一個進程喚醒另一個的時候,被喚醒的進程可能會被放到相同的CPU core
或者相同的NUMA節點上。此特性對中斷喚醒進程時也起作用,在上一節所述的現象中,所有的網絡中斷都分配給CPU 0
去處理,當中斷處理完成時,由于wakeup affinity
特性的作用,所喚醒的用戶進程也被安排給CPU 0
或其所在的numa節點上其他core。而當兩個NUMA node
處理中斷時,這種調度特性有可能導致Redis進程在CPU core
之間頻繁遷移,造成性能損失。
綜合上述,將中斷都分配在同一NUMA Node
中,中斷處理函數和應用程序充分利用同NUMA下的L2、L3緩存、以及同Node下的內存,結合調度系統的wake affinity
特性,能夠更進一步降低延遲。
點擊免費獲取Java學習筆記,面試,文檔以及視頻
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。