溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Redis 高負載下的中斷優(yōu)化

發(fā)布時間:2020-06-26 21:07:03 來源:網絡 閱讀:2591 作者:Java月亮呀 欄目:編程語言

背景

原本穩(wěn)定的環(huán)境也因為請求量的上漲帶來了很多不穩(wěn)定的因素,其中一直困擾我們的就是網卡丟包問題。起初線上存在部分Redis節(jié)點還在使用千兆網卡的老舊服務器,而緩存服務往往需要承載極高的查詢量,并要求毫秒級的響應速度,如此一來千兆網卡很快就出現(xiàn)了瓶頸。經過整治,我們將千兆網卡服務器替換為了萬兆網卡服務器,本以為可以高枕無憂,但是沒想到,在業(yè)務高峰時段,機器也竟然出現(xiàn)了丟包問題,而此時網卡帶寬使用還遠遠沒有達到瓶頸。

定位網絡丟包的原因

從異常指標入手

首先,我們在系統(tǒng)監(jiān)控的net.if.in.dropped指標中,看到有大量數據丟包異常,那么第一步就是要了解這個指標代表什么。

Redis 高負載下的中斷優(yōu)化cdn.xitu.io/2019/6/18/16b69caa0edce2ac?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">

這個指標的數據源,是讀取/proc/net/dev中的數據,監(jiān)控Agent做簡單的處理之后上報。以下為/proc/net/dev的一個示例,可以看到第一行Receive代表in,Transmit代表out,第二行即各個表頭字段,再往后每一行代表一個網卡設備具體的值。

Redis 高負載下的中斷優(yōu)化

其中各個字段意義如下:

字段解釋
bytesThe total number of bytes of data transmitted or received by the interface.
packetsThe total number of packets of data transmitted or received by the interface.
errsThe total number of transmit or receive errors detected by the device driver.
dropThe total number of packets dropped by the device driver.
fifoThe number of FIFO buffer errors.
frameThe number of packet framing errors.
collsThe number of collisions detected on the interface.
compressedThe number of compressed packets transmitted or received by the device driver. (This appears to be unused in the 2.2.15 kernel.)
carrierThe number of carrier losses detected by the device driver.
multicastThe number of multicast frames transmitted or received by the device driver.

通過上述字段解釋,我們可以了解丟包發(fā)生在網卡設備驅動層面;但是想要了解真正的原因,需要繼續(xù)深入源碼。

/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。

繼續(xù)查找dev_get_stats函數可知,rx_droppedrx_missed_errors都是從設備獲取的,并且需要設備驅動實現(xiàn)。

/**??*??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中的緩沖區(qū)空間不足導致的丟包,而rx_missed_errors則在注釋中寫的比較籠統(tǒng)。有資料指出,rx_missed_errors是fifo隊列(即rx ring buffer)滿而丟棄的數量,但這樣的話也就和rx_fifo_errors等同了。后來公司內網絡內核研發(fā)大牛王偉給了我們點撥:不同網卡自己實現(xiàn)不一樣,比如Intel的igb網卡rx_fifo_errorsmissed的基礎上,還加上了RQDPC計數,而ixgbe就沒這個統(tǒng)計。RQDPC計數是描述符不夠的計數,missedfifo滿的計數。所以對于ixgbe來說,rx_fifo_errorsrx_missed_errors確實是等同的。

通過命令ethtool -S eth0可以查看網卡一些統(tǒng)計信息,其中就包含了上文提到的幾個重要指標rx_dropped、rx_missed_errorsrx_fifo_errors等。但實際測試后,我發(fā)現(xiàn)不同網卡型號給出的指標略有不同,比如Intel ixgbe就能取到,而Broadcom bnx2/tg3則只能取到rx_discards(對應rx_fifo_errors)、rx_fw_discards(對應rx_dropped)。這表明,各家網卡廠商設備內部對這些丟包的計數器、指標的定義略有不同,但通過驅動向內核提供的統(tǒng)計數據都封裝成了struct rtnl_link_stats64定義的格式。

在對丟包服務器進行檢查后,發(fā)現(xiàn)rx_missed_errors為0,丟包全部來自rx_dropped。說明丟包發(fā)生在Linux內核的緩沖區(qū)中。接下來,我們要繼續(xù)探索到底是什么緩沖區(qū)引起了丟包問題,這就需要完整地了解服務器接收數據包的過程。

了解接收數據包的流程

接收數據包是一個復雜的過程,涉及很多底層的技術細節(jié),但大致需要以下幾個步驟:

  1. 網卡收到數據包。

  2. 將數據包從網卡硬件緩存轉移到服務器內存中。

  3. 通知內核處理。

  4. 經過TCP/IP協(xié)議逐層處理。

  5. 應用程序通過read()socket buffer讀取數據。


將網卡收到的數據包轉移到主機內存(NIC與驅動交互)

NIC在接收到數據包之后,首先需要將數據同步到內核中,這中間的橋梁是rx ring buffer。它是由NIC和驅動程序共享的一片區(qū)域,事實上,rx ring buffer存儲的并不是實際的packet數據,而是一個描述符,這個描述符指向了它真正的存儲地址,具體流程如下:

  1. 驅動在內存中分配一片緩沖區(qū)用來接收數據包,叫做sk_buffer;

  2. 將上述緩沖區(qū)的地址和大?。唇邮彰枋龇?,加入到rx ring buffer。描述符中的緩沖區(qū)地址是DMA使用的物理地址;

  3. 驅動通知網卡有一個新的描述符;

  4. 網卡從rx ring buffer中取出描述符,從而獲知緩沖區(qū)的地址和大?。?/p>

  5. 網卡收到新的數據包;

  6. 網卡將新數據包通過DMA直接寫到sk_buffer中。


當驅動處理速度跟不上網卡收包速度時,驅動來不及分配緩沖區(qū),NIC接收到的數據包無法及時寫到sk_buffer,就會產生堆積,當NIC內部緩沖區(qū)寫滿后,就會丟棄部分數據,引起丟包。這部分丟包為rx_fifo_errors,在/proc/net/dev中體現(xiàn)為fifo字段增長,在ifconfig中體現(xiàn)為overruns指標增長。

通知系統(tǒng)內核處理(驅動與Linux內核交互)

這個時候,數據包已經被轉移到了sk_buffer中。前文提到,這是驅動程序在內存中分配的一片緩沖區(qū),并且是通過DMA寫入的,這種方式不依賴CPU直接將數據寫到了內存中,意味著對內核來說,其實并不知道已經有新數據到了內存中。那么如何讓內核知道有新數據進來了呢?答案就是中斷,通過中斷告訴內核有新數據進來了,并需要進行后續(xù)處理。

提到中斷,就涉及到硬中斷和軟中斷,首先需要簡單了解一下它們的區(qū)別:

  • 硬中斷: 由硬件自己生成,具有隨機性,硬中斷被CPU接收后,觸發(fā)執(zhí)行中斷處理程序。中斷處理程序只會處理關鍵性的、短時間內可以處理完的工作,剩余耗時較長工作,會放到中斷之后,由軟中斷來完成。硬中斷也被稱為上半部分。

  • 軟中斷: 由硬中斷對應的中斷處理程序生成,往往是預先在代碼里實現(xiàn)好的,不具有隨機性。(除此之外,也有應用程序觸發(fā)的軟中斷,與本文討論的網卡收包無關。)也被稱為下半部分。

當NIC把數據包通過DMA復制到內核緩沖區(qū)sk_buffer后,NIC立即發(fā)起一個硬件中斷。CPU接收后,首先進入上半部分,網卡中斷對應的中斷處理程序是網卡驅動程序的一部分,之后由它發(fā)起軟中斷,進入下半部分,開始消費sk_buffer中的數據,交給內核協(xié)議棧處理。


通過中斷,能夠快速及時地響應網卡數據請求,但如果數據量大,那么會產生大量中斷請求,CPU大部分時間都忙于處理中斷,效率很低。為了解決這個問題,現(xiàn)在的內核及驅動都采用一種叫NAPI(new API)的方式進行數據處理,其原理可以簡單理解為 中斷+輪詢,在數據量大時,一次中斷后通過輪詢接收一定數量包再返回,避免產生多次中斷。

整個中斷過程的源碼部分比較復雜,并且不同驅動的廠商及版本也會存在一定的區(qū)別。 以下調用關系基于Linux-3.10.108及內核自帶驅動drivers/net/ethernet/intel/ixgbe


注意到,enqueue_to_backlog函數中,會對CPU的softnet_data實例中的接收隊列(input_pkt_queue)進行判斷,如果隊列中的數據長度超過netdev_max_backlog ,那么數據包將直接丟棄,這就產生了丟包。netdev_max_backlog是由系統(tǒng)參數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輪詢部分,并重新發(fā)起軟中斷??*/??????????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個時間片的次數。在檢查線上服務器之后,發(fā)現(xiàn)第一行CPU。硬中斷的中斷號及統(tǒng)計數據可以在/proc/interrupts中看到,對于多隊列網卡,當系統(tǒng)啟動并加載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指標獲取到的丟包,都發(fā)生在Docker虛擬網卡上,而非宿主機物理網卡上,因此很可能是Docker虛擬網橋轉發(fā)數據包之后,虛擬網卡層面產生的丟包,這里由于涉及虛擬化部分,就不進一步分析了。

驅動及內核處理過程中的幾個重要函數:

(1)注冊中斷號及中斷處理程序,根據網卡是否支持MSI/MSIX,結果為:MSIXixgbe_msix_clean_ringsMSIixgbe_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)之后經過一些列調用,直到發(fā)起名為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用來保存中斷狀態(tài),并禁止中斷?*/?????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);????????/*?發(fā)起軟中斷?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上,發(fā)揮多核的性能。不過現(xiàn)在基本都是多隊列網卡,不會開啟這個機制,因此走不到這里,static_key_false是針對默認為falsestatic key的優(yōu)化判斷方式。這段調用的最后,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;?}?復制代碼
TCP/IP協(xié)議棧逐層處理,最終交給用戶空間讀取

數據包進到IP層之后,經過IP層、TCP層處理(校驗、解析上層協(xié)議,發(fā)送給上層協(xié)議),放入socket buffer,在應用程序執(zhí)行read() 系統(tǒng)調用時,就能從socket buffer中將新數據從內核區(qū)拷貝到用戶區(qū),完成讀取。

這里的socket buffer大小即TCP接收窗口,TCP由于具備流量控制功能,能動態(tài)調整接收窗口大小,因此數據傳輸階段不會出現(xiàn)由于socket buffer接收隊列空間不足而丟包的情況(但UDP及TCP握手階段仍會有)。涉及TCP/IP協(xié)議的部分不是此次丟包問題的研究重點,因此這里不再贅述。

網卡隊列

查看網卡型號

??#?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的時候,還會結合系統(tǒng)在線CPU的數量,通過Sum = Min(網卡隊列,CPU Core) 來激活相應的網卡隊列數量,并申請Sum個中斷號。

如果CPU數量小于64,會生成CPU數量的隊列,也就是每個CPU會產生一個external IRQ。

我們線上的CPU一般是48個邏輯core,就會生成48個中斷號,由于我們是兩塊網卡做了bond,也就會生成96個中斷號。

驗證與復現(xiàn)網絡丟包

我們在測試環(huán)境做了測試,發(fā)現(xiàn)測試環(huán)境的中斷確實有集中在CPU 0的情況,下面使用systemtap診斷測試環(huán)境軟中斷分布的方法:

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("~")?}?復制代碼

下面執(zhí)行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?復制代碼

打開這個文件,我們發(fā)現(xiàn)它是在執(zhí)行static void net_rx_action(struct softirq_action *h)這個函數,而這個函數正是前文提到的,NET_RX_SOFTIRQ對應的軟中斷處理程序。因此可以確認網卡的軟中斷在機器上分布非常不均,而且主要集中在CPU 0上。通過/proc/interrupts能確認硬中斷集中在CPU 0上,因此軟中斷也都由CPU 0處理,如何優(yōu)化網卡的中斷成為了我們關注的重點。

優(yōu)化策略

CPU親緣性

前文提到,丟包是因為隊列中的數據包超過了netdev_max_backlog造成了丟棄,因此首先想到是臨時調大netdev_max_backlog能否解決燃眉之急,事實證明,對于輕微丟包調大參數可以緩解丟包,但對于大量丟包則幾乎不怎么管用,內核處理速度跟不上收包速度的問題還是客觀存在,本質還是因為單核處理中斷有瓶頸,即使不丟包,服務響應速度也會變慢。因此如果能同時使用多個CPU Core來處理中斷,就能顯著提高中斷處理的效率,并且每個CPU都會實例化一個softnet_data對象,隊列數也增加了。

中斷親緣性設置

通過設置中斷親緣性,可以讓指定的中斷向量號更傾向于發(fā)送給指定的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平行處理。我們經過測試,發(fā)現(xiàn)當給中斷設置了多個CPU Core后,它也僅能由設置的第一個CPU Core來處理,其他的CPU Core并不會參與中斷處理,原因猜想是當CPU可以平行收包時,不同的核收取了同一個queue的數據包,但處理速度不一致,導致提交到IP層后的順序也不一致,這就會產生亂序的問題,由同一個核來處理可以避免了亂序問題。

但是,當我們配置了多個Core處理中斷后,發(fā)現(xiàn)Redis的慢查詢數量有明顯上升,甚至部分業(yè)務也受到了影響,慢查詢增多直接導致可用性降低,因此方案仍需進一步優(yōu)化。


Redis進程親緣性設置

如果某個CPU Core正在處理Redis的調用,執(zhí)行到一半時產生了中斷,那么CPU不得不停止當前的工作轉而處理中斷請求,中斷期間Redis也無法轉交給其他core繼續(xù)運行,必須等處理完中斷后才能繼續(xù)運行。Redis本身定位就是高速緩存,線上的平均端到端響應時間小于1ms,如果頻繁被中斷,那么響應時間必然受到極大影響。容易想到,由最初的CPU 0單核處理中斷,改進到多核處理中斷,Redis進程被中斷影響的幾率增大了,因此我們需要對Redis進程也設置CPU親緣性,使其與處理中斷的Core互相錯開,避免受到影響。

使用命令taskset可以為進程設置CPU親緣性,操作十分簡單,一句taskset -cp cpu-list pid即可完成綁定。經過一番壓測,我們發(fā)現(xiàn)使用8個core處理中斷時,流量直至打滿雙萬兆網卡也不會出現(xiàn)丟包,因此決定將中斷的親緣性設置為物理機上前8個core,Redis進程的親緣性設置為剩下的所有core。調整后,確實有明顯的效果,慢查詢數量大幅優(yōu)化,但對比初始情況,仍然還是高了一些些,還有沒有優(yōu)化空間呢?


通過觀察,我們發(fā)現(xiàn)一個有趣的現(xiàn)象,當只有CPU 0處理中斷時,Redis進程更傾向于運行在CPU 0,以及CPU 0同一物理CPU下的其他核上。于是有了以下推測:我們設置的中斷親緣性,是直接選取了前8個核心,但這8個core卻可能是來自兩塊物理CPU的,在/proc/cpuinfo中,通過字段processorphysical id 能確認這一點,那么響應慢是否和物理CPU有關呢?物理CPU又和NUMA架構關聯(lián),每個物理CPU對應一個NUMA node,那么接下來就要從NUMA角度進行分析。


NUMA

SMP 架構

隨著單核CPU的頻率在制造工藝上的瓶頸,CPU制造商的發(fā)展方向也由縱向變?yōu)闄M向:從CPU頻率轉為每瓦性能。CPU也就從單核頻率時代過渡到多核性能協(xié)調。

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)。

Redis 高負載下的中斷優(yōu)化

NUMA 架構

在前面的FSB(前端系統(tǒng)總線)結構中,當CPU不斷增長的情況下,共享的系統(tǒng)總線就會因為資源競爭(多核爭搶總線資源以訪問北橋上的內存)而出現(xiàn)擴展和性能問題。

在這樣的背景下,基于SMP架構上的優(yōu)化,設計出了NUMA(Non-Uniform Memory Access)非均勻內存訪問。

內存控制器芯片被集成到處理器內部,多個處理器通過QPI鏈路相連,DRAM也就有了遠近之分。(如下圖所示:摘自CPU Cache)

CPU 多層Cache的性能差異是很巨大的,比如:L1的訪問時長1ns,L2的時長3ns…跨node的訪問會有幾十甚至上百倍的性能損耗。

Redis 高負載下的中斷優(yōu)化

NUMA 架構下的中斷優(yōu)化

這時我們再回歸到中斷的問題上,當兩個NUMA節(jié)點處理中斷時,CPU實例化的softnet_data以及驅動分配的sk_buffer都可能是跨Node的,數據接收后對上層應用Redis來說,跨Node訪問的幾率也大大提高,并且無法充分利用L2、L3 cache,增加了延時。

同時,由于Linux wake affinity特性,如果兩個進程頻繁互動,調度系統(tǒng)會覺得它們很有可能共享同樣的數據,把它們放到同一CPU核心或NUMA Node有助于提高緩存和內存的訪問性能,所以當一個進程喚醒另一個的時候,被喚醒的進程可能會被放到相同的CPU core或者相同的NUMA節(jié)點上。此特性對中斷喚醒進程時也起作用,在上一節(jié)所述的現(xiàn)象中,所有的網絡中斷都分配給CPU 0去處理,當中斷處理完成時,由于wakeup affinity特性的作用,所喚醒的用戶進程也被安排給CPU 0或其所在的numa節(jié)點上其他core。而當兩個NUMA node處理中斷時,這種調度特性有可能導致Redis進程在CPU core之間頻繁遷移,造成性能損失。

綜合上述,將中斷都分配在同一NUMA Node中,中斷處理函數和應用程序充分利用同NUMA下的L2、L3緩存、以及同Node下的內存,結合調度系統(tǒng)的wake affinity特性,能夠更進一步降低延遲。

Redis 高負載下的中斷優(yōu)化

彩蛋小福利

點擊免費獲取Java學習筆記,面試,文檔以及視頻

部分資料如下:

Redis 高負載下的中斷優(yōu)化


向AI問一下細節(jié)
推薦閱讀:
  1. redis阻塞分析
  2. redis集群

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI