背景
原本穩(wěn)定的環(huán)境也因?yàn)檎埱罅康纳蠞q帶來了很多不穩(wěn)定的因素衬吆,其中一直困擾我們的就是網(wǎng)卡丟包問題梁钾。起初線上存在部分Redis節(jié)點(diǎn)還在使用千兆網(wǎng)卡的老舊服務(wù)器,而緩存服務(wù)往往需要承載極高的查詢量逊抡,并要求毫秒級的響應(yīng)速度姆泻,如此一來千兆網(wǎng)卡很快就出現(xiàn)了瓶頸。經(jīng)過整治冒嫡,我們將千兆網(wǎng)卡服務(wù)器替換為了萬兆網(wǎng)卡服務(wù)器拇勃,本以為可以高枕無憂,但是沒想到孝凌,在業(yè)務(wù)高峰時段方咆,機(jī)器也竟然出現(xiàn)了丟包問題,而此時網(wǎng)卡帶寬使用還遠(yuǎn)遠(yuǎn)沒有達(dá)到瓶頸蟀架。
定位網(wǎng)絡(luò)丟包的原因
從異常指標(biāo)入手
首先瓣赂,我們在系統(tǒng)監(jiān)控的net.if.in.dropped
指標(biāo)中,看到有大量數(shù)據(jù)丟包異常片拍,那么第一步就是要了解這個指標(biāo)代表什么煌集。
這個指標(biāo)的數(shù)據(jù)源,是讀取/proc/net/dev
中的數(shù)據(jù)穆碎,監(jiān)控Agent做簡單的處理之后上報牙勘。以下為/proc/net/dev
的一個示例职恳,可以看到第一行Receive代表in所禀,Transmit代表out,第二行即各個表頭字段放钦,再往后每一行代表一個網(wǎng)卡設(shè)備具體的值色徘。
其中各個字段意義如下:
字段 | 解釋 |
---|---|
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. |
通過上述字段解釋,我們可以了解丟包發(fā)生在網(wǎng)卡設(shè)備驅(qū)動層面操禀;但是想要了解真正的原因褂策,需要繼續(xù)深入源碼。
/proc/net/dev
的數(shù)據(jù)來源颓屑,根據(jù)源碼文件net/core/net-procfs.c
斤寂,可以知道上述指標(biāo)是通過其中的dev_seq_show()
函數(shù)和dev_seq_printf_stats()
函數(shù)輸出的:
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數(shù)據(jù)部分 */
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;
/* 數(shù)據(jù)源從下面的函數(shù)中取得 */
const struct rtnl_link_stats64 *stats = dev_get_stats(dev, &temp);
/* /proc/net/dev 各個字段的數(shù)據(jù)算法 */
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()
函數(shù)里,對應(yīng)drop輸出的部分揪惦,能看到由兩塊組成:stats-
>rx_dropped+stats
->rx_missed_errors
遍搞。
繼續(xù)查找dev_get_stats
函數(shù)可知,rx_dropped
和rx_missed_errors
都是從設(shè)備獲取的器腋,并且需要設(shè)備驅(qū)動實(shí)現(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;
}
結(jié)構(gòu)體 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ū)空間不足導(dǎo)致的丟包,而rx_missed_errors
則在注釋中寫的比較籠統(tǒng)诊县。有資料指出讲弄,rx_missed_errors
是fifo隊(duì)列(即rx ring buffer
)滿而丟棄的數(shù)量,但這樣的話也就和rx_fifo_errors
等同了依痊。后來公司內(nèi)網(wǎng)絡(luò)內(nèi)核研發(fā)大牛王偉給了我們點(diǎn)撥:不同網(wǎng)卡自己實(shí)現(xiàn)不一樣避除,比如Intel的igb網(wǎng)卡rx_fifo_errors
在missed
的基礎(chǔ)上,還加上了RQDPC
計數(shù)胸嘁,而ixgbe
就沒這個統(tǒng)計驹饺。RQDPC計數(shù)是描述符不夠的計數(shù),missed
是fifo
滿的計數(shù)缴渊。所以對于ixgbe
來說赏壹,rx_fifo_errors
和rx_missed_errors
確實(shí)是等同的。
通過命令ethtool -S eth0
可以查看網(wǎng)卡一些統(tǒng)計信息衔沼,其中就包含了上文提到的幾個重要指標(biāo)rx_dropped
蝌借、rx_missed_errors
、rx_fifo_errors
等指蚁。但實(shí)際測試后菩佑,我發(fā)現(xiàn)不同網(wǎng)卡型號給出的指標(biāo)略有不同,比如Intel ixgbe
就能取到凝化,而Broadcom bnx2/tg3
則只能取到rx_discards
(對應(yīng)rx_fifo_errors
)稍坯、rx_fw_discards
(對應(yīng)rx_dropped
)。這表明搓劫,各家網(wǎng)卡廠商設(shè)備內(nèi)部對這些丟包的計數(shù)器瞧哟、指標(biāo)的定義略有不同,但通過驅(qū)動向內(nèi)核提供的統(tǒng)計數(shù)據(jù)都封裝成了struct rtnl_link_stats64
定義的格式枪向。
在對丟包服務(wù)器進(jìn)行檢查后勤揩,發(fā)現(xiàn)rx_missed_errors
為0,丟包全部來自rx_dropped
秘蛔。說明丟包發(fā)生在Linux內(nèi)核的緩沖區(qū)中陨亡。接下來,我們要繼續(xù)探索到底是什么緩沖區(qū)引起了丟包問題深员,這就需要完整地了解服務(wù)器接收數(shù)據(jù)包的過程负蠕。
了解接收數(shù)據(jù)包的流程
接收數(shù)據(jù)包是一個復(fù)雜的過程,涉及很多底層的技術(shù)細(xì)節(jié)倦畅,但大致需要以下幾個步驟:
- 網(wǎng)卡收到數(shù)據(jù)包遮糖。
- 將數(shù)據(jù)包從網(wǎng)卡硬件緩存轉(zhuǎn)移到服務(wù)器內(nèi)存中。
- 通知內(nèi)核處理滔迈。
- 經(jīng)過TCP/IP協(xié)議逐層處理止吁。
- 應(yīng)用程序通過
read()
從socket buffer
讀取數(shù)據(jù)被辑。
將網(wǎng)卡收到的數(shù)據(jù)包轉(zhuǎn)移到主機(jī)內(nèi)存(NIC與驅(qū)動交互)
NIC在接收到數(shù)據(jù)包之后,首先需要將數(shù)據(jù)同步到內(nèi)核中敬惦,這中間的橋梁是rx ring buffer
盼理。它是由NIC和驅(qū)動程序共享的一片區(qū)域,事實(shí)上俄删,rx ring buffer
存儲的并不是實(shí)際的packet數(shù)據(jù)宏怔,而是一個描述符,這個描述符指向了它真正的存儲地址畴椰,具體流程如下:
- 驅(qū)動在內(nèi)存中分配一片緩沖區(qū)用來接收數(shù)據(jù)包臊诊,叫做
sk_buffer
; - 將上述緩沖區(qū)的地址和大行敝(即接收描述符)抓艳,加入到
rx ring buffer
。描述符中的緩沖區(qū)地址是DMA使用的物理地址帚戳; - 驅(qū)動通知網(wǎng)卡有一個新的描述符玷或;
- 網(wǎng)卡從
rx ring buffer
中取出描述符,從而獲知緩沖區(qū)的地址和大衅巍偏友; - 網(wǎng)卡收到新的數(shù)據(jù)包;
- 網(wǎng)卡將新數(shù)據(jù)包通過DMA直接寫到
sk_buffer
中对供。
當(dāng)驅(qū)動處理速度跟不上網(wǎng)卡收包速度時位他,驅(qū)動來不及分配緩沖區(qū),NIC接收到的數(shù)據(jù)包無法及時寫到sk_buffer
产场,就會產(chǎn)生堆積鹅髓,當(dāng)NIC內(nèi)部緩沖區(qū)寫滿后,就會丟棄部分?jǐn)?shù)據(jù)涝动,引起丟包迈勋。這部分丟包為rx_fifo_errors
炬灭,在/proc/net/dev
中體現(xiàn)為fifo字段增長醋粟,在ifconfig中體現(xiàn)為overruns指標(biāo)增長。
通知系統(tǒng)內(nèi)核處理(驅(qū)動與Linux內(nèi)核交互)
這個時候重归,數(shù)據(jù)包已經(jīng)被轉(zhuǎn)移到了sk_buffer
中米愿。前文提到,這是驅(qū)動程序在內(nèi)存中分配的一片緩沖區(qū)鼻吮,并且是通過DMA寫入的育苟,這種方式不依賴CPU直接將數(shù)據(jù)寫到了內(nèi)存中,意味著對內(nèi)核來說椎木,其實(shí)并不知道已經(jīng)有新數(shù)據(jù)到了內(nèi)存中违柏。那么如何讓內(nèi)核知道有新數(shù)據(jù)進(jìn)來了呢博烂?答案就是中斷,通過中斷告訴內(nèi)核有新數(shù)據(jù)進(jìn)來了漱竖,并需要進(jìn)行后續(xù)處理禽篱。
提到中斷,就涉及到硬中斷和軟中斷馍惹,首先需要簡單了解一下它們的區(qū)別:
- 硬中斷: 由硬件自己生成躺率,具有隨機(jī)性,硬中斷被CPU接收后万矾,觸發(fā)執(zhí)行中斷處理程序悼吱。中斷處理程序只會處理關(guān)鍵性的、短時間內(nèi)可以處理完的工作良狈,剩余耗時較長工作后添,會放到中斷之后,由軟中斷來完成薪丁。硬中斷也被稱為上半部分吕朵。
- 軟中斷: 由硬中斷對應(yīng)的中斷處理程序生成,往往是預(yù)先在代碼里實(shí)現(xiàn)好的窥突,不具有隨機(jī)性努溃。(除此之外,也有應(yīng)用程序觸發(fā)的軟中斷阻问,與本文討論的網(wǎng)卡收包無關(guān)梧税。)也被稱為下半部分。
當(dāng)NIC把數(shù)據(jù)包通過DMA復(fù)制到內(nèi)核緩沖區(qū)sk_buffer
后称近,NIC立即發(fā)起一個硬件中斷第队。CPU接收后,首先進(jìn)入上半部分刨秆,網(wǎng)卡中斷對應(yīng)的中斷處理程序是網(wǎng)卡驅(qū)動程序的一部分凳谦,之后由它發(fā)起軟中斷,進(jìn)入下半部分衡未,開始消費(fèi)sk_buffer
中的數(shù)據(jù)缓醋,交給內(nèi)核協(xié)議棧處理。
通過中斷送粱,能夠快速及時地響應(yīng)網(wǎng)卡數(shù)據(jù)請求褪贵,但如果數(shù)據(jù)量大槽卫,那么會產(chǎn)生大量中斷請求,CPU大部分時間都忙于處理中斷效览,效率很低。為了解決這個問題,現(xiàn)在的內(nèi)核及驅(qū)動都采用一種叫NAPI(new API)的方式進(jìn)行數(shù)據(jù)處理椅挣,其原理可以簡單理解為 中斷+輪詢攻谁,在數(shù)據(jù)量大時对雪,一次中斷后通過輪詢接收一定數(shù)量包再返回,避免產(chǎn)生多次中斷雏赦。
整個中斷過程的源碼部分比較復(fù)雜,并且不同驅(qū)動的廠商及版本也會存在一定的區(qū)別梆造。 以下調(diào)用關(guān)系基于Linux-3.10.108及內(nèi)核自帶驅(qū)動drivers/net/ethernet/intel/ixgbe
:
注意到,enqueue_to_backlog
函數(shù)中源祈,會對CPU的softnet_data
實(shí)例中的接收隊(duì)列(input_pkt_queue
)進(jìn)行判斷煎源,如果隊(duì)列中的數(shù)據(jù)長度超過netdev_max_backlog
,那么數(shù)據(jù)包將直接丟棄香缺,這就產(chǎn)生了丟包手销。netdev_max_backlog
是由系統(tǒng)參數(shù)net.core.netdev_max_backlog
指定的,默認(rèn)大小是 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);
/* 判斷接收隊(duì)列是否滿锋拖,隊(duì)列長度為 netdev_max_backlog */
if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {
if (skb_queue_len(&sd->input_pkt_queue)) {
enqueue:
/* 隊(duì)列如果不會空,將數(shù)據(jù)包添加到隊(duì)列尾 */
__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
*/
/* 隊(duì)列如果為空祸轮,回到 ____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;
}
/* 隊(duì)列滿則直接丟棄,對應(yīng)計數(shù)器 +1 */
sd->dropped++;
rps_unlock(sd);
local_irq_restore(flags);
atomic_long_inc(&skb->dev->rx_dropped);
kfree_skb(skb);
return NET_RX_DROP;
}
內(nèi)核會為每個CPU Core
都實(shí)例化一個softnet_data
對象适袜,這個對象中的input_pkt_queue
用于管理接收的數(shù)據(jù)包柄错。假如所有的中斷都由一個CPU Core
來處理的話,那么所有數(shù)據(jù)包只能經(jīng)由這個CPU的input_pkt_queue
苦酱,如果接收的數(shù)據(jù)包數(shù)量非常大售貌,超過中斷處理速度,那么input_pkt_queue
中的數(shù)據(jù)包就會堆積疫萤,直至超過netdev_max_backlog
颂跨,引起丟包。這部分丟包可以在cat /proc/net/softnet_stat
的輸出結(jié)果中進(jìn)行確認(rèn):
其中每行代表一個CPU扯饶,第一列是中斷處理程序接收的幀數(shù)恒削,第二列是由于超過 netdev_max_backlog
而丟棄的幀數(shù)池颈。 第三列則是在net_rx_action
函數(shù)中處理數(shù)據(jù)包超過netdev_budge
指定數(shù)量或運(yùn)行時間超過2個時間片的次數(shù)。在檢查線上服務(wù)器之后蔓同,發(fā)現(xiàn)第一行CPU饶辙。硬中斷的中斷號及統(tǒng)計數(shù)據(jù)可以在/proc/interrupts
中看到蹲诀,對于多隊(duì)列網(wǎng)卡斑粱,當(dāng)系統(tǒng)啟動并加載NIC設(shè)備驅(qū)動程序模塊時,每個RXTX隊(duì)列會被初始化分配一個唯一的中斷向量號脯爪,它通知中斷處理程序該中斷來自哪個NIC隊(duì)列则北。在默認(rèn)情況下,所有隊(duì)列的硬中斷都由CPU 0處理痕慢,因此對應(yīng)的軟中斷邏輯也會在CPU 0上處理尚揣,在服務(wù)器 TOP 的輸出中,也可以觀察到 %si 軟中斷部分掖举,CPU 0的占比比其他core高出一截快骗。
到這里其實(shí)有存在一個疑惑,我們線上服務(wù)器的內(nèi)核版本及網(wǎng)卡都支持NAPI塔次,而NAPI的處理邏輯是不會走到enqueue_to_backlog
中的方篮,enqueue_to_backlog
主要是非NAPI的處理流程中使用的。對此励负,我們覺得可能和當(dāng)前使用的Docker架構(gòu)有關(guān)藕溅,事實(shí)上,我們通過net.if.dropped
指標(biāo)獲取到的丟包继榆,都發(fā)生在Docker虛擬網(wǎng)卡上巾表,而非宿主機(jī)物理網(wǎng)卡上,因此很可能是Docker虛擬網(wǎng)橋轉(zhuǎn)發(fā)數(shù)據(jù)包之后略吨,虛擬網(wǎng)卡層面產(chǎn)生的丟包集币,這里由于涉及虛擬化部分,就不進(jìn)一步分析了翠忠。
驅(qū)動及內(nèi)核處理過程中的幾個重要函數(shù):
(1)注冊中斷號及中斷處理程序鞠苟,根據(jù)網(wǎng)卡是否支持MSI/MSIX
,結(jié)果為: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政溃,調(diào)用 ixgbe_request_msix_irqs 設(shè)置中斷處理程序*/
if (adapter->flags & IXGBE_FLAG_MSIX_ENABLED)
err = ixgbe_request_msix_irqs(adapter);
/* 支持MSI趾访,直接設(shè)置 ixgbe_intr 為中斷處理程序 */
else if (adapter->flags & IXGBE_FLAG_MSI_ENABLED)
err = request_irq(adapter->pdev->irq, &ixgbe_intr, 0,
netdev->name, adapter);
/* 都不支持的情況,直接設(shè)置 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];
/* 設(shè)置中斷處理入口函數(shù)為 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)線上的多隊(duì)列網(wǎng)卡均支持MSIX董虱,中斷處理程序入口為ixgbe_msix_clean_rings
扼鞋,里面調(diào)用了函數(shù)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)之后經(jīng)過一些列調(diào)用,直到發(fā)起名為NET_RX_SOFTIRQ
的軟中斷云头。到這里完成了硬中斷部分捐友,進(jìn)入軟中斷部分,同時也上升到了內(nèi)核層面溃槐。
/**
* 文件: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))
/* 注意下面調(diào)用的這個函數(shù)名字前是兩個下劃線 */
__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);
/* 注意下面調(diào)用的這個函數(shù)名字前是四個下劃線,傳入的 softnet_data 是當(dāng)前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
對應(yīng)的軟中斷處理程序接口是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功能就是輪詢調(diào)用poll方法坐桩,這里就是ixgbe_poll侣集。一次輪詢的數(shù)據(jù)包數(shù)量不能超過內(nèi)核參數(shù)net.core.netdev_budget指定的數(shù)量(默認(rèn)值300)暂筝,并且輪詢時間不能超過2個時間片紊馏。這個機(jī)制保證了單次軟中斷處理不會耗時太久影響被中斷的程序。
/* 文件: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.
*/
/* 判斷處理包數(shù)是否超過 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
之后的一系列調(diào)用就不一一詳述了姻几,有興趣的同學(xué)可以自行研究宜狐,軟中斷部分有幾個地方會有類似if (static_key_false(&rps_needed))
這樣的判斷,會進(jìn)入前文所述有丟包風(fēng)險的enqueue_to_backlog
函數(shù)蛇捌。 這里的邏輯為判斷是否啟用了RPS機(jī)制抚恒,RPS是早期單隊(duì)列網(wǎng)卡上將軟中斷負(fù)載均衡到多個CPU Core
的技術(shù),它對數(shù)據(jù)流進(jìn)行hash并分配到對應(yīng)的CPU Core
上豁陆,發(fā)揮多核的性能柑爸。不過現(xiàn)在基本都是多隊(duì)列網(wǎng)卡,不會開啟這個機(jī)制盒音,因此走不到這里表鳍,static_key_false
是針對默認(rèn)為false
的static key
的優(yōu)化判斷方式。這段調(diào)用的最后祥诽,deliver_skb
會將接收的數(shù)據(jù)傳入一個IP層的數(shù)據(jù)結(jié)構(gòu)中譬圣,至此完成二層的全部處理。
/**
* 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機(jī)制 */
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
/* 獲取對應(yīng)的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é)議棧逐層處理雄坪,最終交給用戶空間讀取
數(shù)據(jù)包進(jìn)到IP層之后厘熟,經(jīng)過IP層、TCP層處理(校驗(yàn)维哈、解析上層協(xié)議绳姨,發(fā)送給上層協(xié)議),放入socket buffer
阔挠,在應(yīng)用程序執(zhí)行read() 系統(tǒng)調(diào)用時飘庄,就能從socket buffer中將新數(shù)據(jù)從內(nèi)核區(qū)拷貝到用戶區(qū),完成讀取购撼。
這里的socket buffer
大小即TCP接收窗口跪削,TCP由于具備流量控制功能谴仙,能動態(tài)調(diào)整接收窗口大小,因此數(shù)據(jù)傳輸階段不會出現(xiàn)由于socket buffer
接收隊(duì)列空間不足而丟包的情況(但UDP及TCP握手階段仍會有)碾盐。涉及TCP/IP協(xié)議的部分不是此次丟包問題的研究重點(diǎn)晃跺,因此這里不再贅述。
網(wǎng)卡隊(duì)列
查看網(wǎng)卡型號
# 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
可以看出毫玖,網(wǎng)卡的中斷機(jī)制是MSI-X掀虎,即網(wǎng)卡的每個隊(duì)列都可以分配中斷(MSI-X支持2048個中斷)。
網(wǎng)卡隊(duì)列
...
#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;
}
...
根據(jù)網(wǎng)卡型號確定驅(qū)動中定義的網(wǎng)卡隊(duì)列孕豹,可以看到X540網(wǎng)卡驅(qū)動中定義最大支持的IRQ Vector為0x40(數(shù)值: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());
通過加載網(wǎng)卡驅(qū)動十气,獲取網(wǎng)卡型號和網(wǎng)卡硬件的隊(duì)列數(shù)励背;但是在初始化misx vector的時候,還會結(jié)合系統(tǒng)在線CPU的數(shù)量砸西,通過Sum = Min(網(wǎng)卡隊(duì)列叶眉,CPU Core) 來激活相應(yīng)的網(wǎng)卡隊(duì)列數(shù)量,并申請Sum個中斷號芹枷。
如果CPU數(shù)量小于64衅疙,會生成CPU數(shù)量的隊(duì)列,也就是每個CPU會產(chǎn)生一個external IRQ鸳慈。
我們線上的CPU一般是48個邏輯core饱溢,就會生成48個中斷號,由于我們是兩塊網(wǎng)卡做了bond走芋,也就會生成96個中斷號绩郎。
驗(yàn)證與復(fù)現(xiàn)網(wǎng)絡(luò)丟包
通過霸爺?shù)?a target="_blank">一篇文章,我們在測試環(huán)境做了測試翁逞,發(fā)現(xiàn)測試環(huán)境的中斷確實(shí)有集中在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
的結(jié)果:
==irq number:dev_name
87,eth0-0->1693
90,eth0-3->1263
95,eth1-3->746
92,eth1-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
對應(yīng)的符號信息:
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)
這個函數(shù)挖函,而這個函數(shù)正是前文提到的状植,NET_RX_SOFTIRQ
對應(yīng)的軟中斷處理程序。因此可以確認(rèn)網(wǎng)卡的軟中斷在機(jī)器上分布非常不均怨喘,而且主要集中在CPU 0
上津畸。通過/proc/interrupts
能確認(rèn)硬中斷集中在CPU 0
上,因此軟中斷也都由CPU 0
處理必怜,如何優(yōu)化網(wǎng)卡的中斷成為了我們關(guān)注的重點(diǎn)肉拓。
優(yōu)化策略
CPU親緣性
前文提到,丟包是因?yàn)殛?duì)列中的數(shù)據(jù)包超過了netdev_max_backlog
造成了丟棄棚赔,因此首先想到是臨時調(diào)大netdev_max_backlog
能否解決燃眉之急帝簇,事實(shí)證明徘郭,對于輕微丟包調(diào)大參數(shù)可以緩解丟包,但對于大量丟包則幾乎不怎么管用丧肴,內(nèi)核處理速度跟不上收包速度的問題還是客觀存在残揉,本質(zhì)還是因?yàn)閱魏颂幚碇袛嘤衅款i,即使不丟包芋浮,服務(wù)響應(yīng)速度也會變慢抱环。因此如果能同時使用多個CPU Core
來處理中斷,就能顯著提高中斷處理的效率纸巷,并且每個CPU都會實(shí)例化一個softnet_data
對象镇草,隊(duì)列數(shù)也增加了。
中斷親緣性設(shè)置
通過設(shè)置中斷親緣性瘤旨,可以讓指定的中斷向量號更傾向于發(fā)送給指定的CPU Core
來處理梯啤,俗稱“綁核”。命令grep eth /proc/interrupts
的第一列可以獲取網(wǎng)卡的中斷號存哲,如果是多隊(duì)列網(wǎng)卡因宇,那么就會有多行輸出:
中斷的親緣性設(shè)置可以在cat /proc/irq/${中斷號}/smp_affinity 或 cat /proc/irq/${中斷號}/smp_affinity_list
中確認(rèn),前者是16進(jìn)制掩碼形式祟偷,后者是以CPU Core
序號形式察滑。例如下圖中,將16進(jìn)制的400轉(zhuǎn)換成2進(jìn)制后修肠,為 10000000000贺辰,“1”在第10位上,表示親緣性是第10個CPU Core
嵌施。
那為什么中斷號只設(shè)置一個CPU Core
呢饲化?而不是為每一個中斷號設(shè)置多個CPU Core
平行處理。我們經(jīng)過測試艰管,發(fā)現(xiàn)當(dāng)給中斷設(shè)置了多個CPU Core
后滓侍,它也僅能由設(shè)置的第一個CPU Core
來處理,其他的CPU Core
并不會參與中斷處理牲芋,原因猜想是當(dāng)CPU可以平行收包時撩笆,不同的核收取了同一個queue的數(shù)據(jù)包,但處理速度不一致缸浦,導(dǎo)致提交到IP層后的順序也不一致夕冲,這就會產(chǎn)生亂序的問題,由同一個核來處理可以避免了亂序問題裂逐。
但是歹鱼,當(dāng)我們配置了多個Core處理中斷后,發(fā)現(xiàn)Redis的慢查詢數(shù)量有明顯上升卜高,甚至部分業(yè)務(wù)也受到了影響弥姻,慢查詢增多直接導(dǎo)致可用性降低南片,因此方案仍需進(jìn)一步優(yōu)化。
Redis進(jìn)程親緣性設(shè)置
如果某個CPU Core
正在處理Redis的調(diào)用庭敦,執(zhí)行到一半時產(chǎn)生了中斷疼进,那么CPU不得不停止當(dāng)前的工作轉(zhuǎn)而處理中斷請求,中斷期間Redis也無法轉(zhuǎn)交給其他core繼續(xù)運(yùn)行秧廉,必須等處理完中斷后才能繼續(xù)運(yùn)行伞广。Redis本身定位就是高速緩存,線上的平均端到端響應(yīng)時間小于1ms疼电,如果頻繁被中斷嚼锄,那么響應(yīng)時間必然受到極大影響。容易想到蔽豺,由最初的CPU 0
單核處理中斷区丑,改進(jìn)到多核處理中斷,Redis進(jìn)程被中斷影響的幾率增大了茫虽,因此我們需要對Redis進(jìn)程也設(shè)置CPU親緣性刊苍,使其與處理中斷的Core互相錯開,避免受到影響濒析。
使用命令taskset
可以為進(jìn)程設(shè)置CPU親緣性,操作十分簡單啥纸,一句taskset -cp cpu-list pid
即可完成綁定号杏。經(jīng)過一番壓測,我們發(fā)現(xiàn)使用8個core處理中斷時斯棒,流量直至打滿雙萬兆網(wǎng)卡也不會出現(xiàn)丟包盾致,因此決定將中斷的親緣性設(shè)置為物理機(jī)上前8個core,Redis進(jìn)程的親緣性設(shè)置為剩下的所有core荣暮。調(diào)整后庭惜,確實(shí)有明顯的效果,慢查詢數(shù)量大幅優(yōu)化穗酥,但對比初始情況护赊,仍然還是高了一些些,還有沒有優(yōu)化空間呢砾跃?
通過觀察骏啰,我們發(fā)現(xiàn)一個有趣的現(xiàn)象,當(dāng)只有CPU 0處理中斷時抽高,Redis進(jìn)程更傾向于運(yùn)行在CPU 0判耕,以及CPU 0同一物理CPU下的其他核上。于是有了以下推測:我們設(shè)置的中斷親緣性翘骂,是直接選取了前8個核心壁熄,但這8個core卻可能是來自兩塊物理CPU的帚豪,在/proc/cpuinfo
中,通過字段processor
和physical id
能確認(rèn)這一點(diǎn)草丧,那么響應(yīng)慢是否和物理CPU有關(guān)呢志鞍?物理CPU又和NUMA架構(gòu)關(guān)聯(lián),每個物理CPU對應(yīng)一個NUMA node
方仿,那么接下來就要從NUMA角度進(jìn)行分析固棚。
NUMA
SMP 架構(gòu)
隨著單核CPU的頻率在制造工藝上的瓶頸,CPU制造商的發(fā)展方向也由縱向變?yōu)闄M向:從CPU頻率轉(zhuǎn)為每瓦性能仙蚜。CPU也就從單核頻率時代過渡到多核性能協(xié)調(diào)此洲。
SMP(對稱多處理結(jié)構(gòu)):即CPU共享所有資源,例如總線委粉、內(nèi)存呜师、IO等。
SMP 結(jié)構(gòu):一個物理CPU可以有多個物理Core贾节,每個Core又可以有多個硬件線程汁汗。即:每個HT有一個獨(dú)立的L1 cache,同一個Core下的HT共享L2 cache栗涂,同一個物理CPU下的多個core共享L3 cache知牌。
下圖(摘自內(nèi)核月談)中,一個x86 CPU有4個物理Core斤程,每個Core有兩個HT(Hyper Thread)角寸。
NUMA 架構(gòu)
在前面的FSB(前端系統(tǒng)總線)結(jié)構(gòu)中,當(dāng)CPU不斷增長的情況下忿墅,共享的系統(tǒng)總線就會因?yàn)橘Y源競爭(多核爭搶總線資源以訪問北橋上的內(nèi)存)而出現(xiàn)擴(kuò)展和性能問題扁藕。
在這樣的背景下,基于SMP架構(gòu)上的優(yōu)化疚脐,設(shè)計出了NUMA(Non-Uniform Memory Access)非均勻內(nèi)存訪問亿柑。
內(nèi)存控制器芯片被集成到處理器內(nèi)部,多個處理器通過QPI鏈路相連棍弄,DRAM也就有了遠(yuǎn)近之分望薄。(如下圖所示:摘自CPU Cache)
CPU 多層Cache的性能差異是很巨大的,比如:L1的訪問時長1ns照卦,L2的時長3ns…跨node的訪問會有幾十甚至上百倍的性能損耗式矫。
NUMA 架構(gòu)下的中斷優(yōu)化
這時我們再回歸到中斷的問題上,當(dāng)兩個NUMA節(jié)點(diǎn)處理中斷時役耕,CPU實(shí)例化的softnet_data
以及驅(qū)動分配的sk_buffer
都可能是跨Node的采转,數(shù)據(jù)接收后對上層應(yīng)用Redis來說,跨Node訪問的幾率也大大提高,并且無法充分利用L2故慈、L3 cache板熊,增加了延時。
同時察绷,由于Linux wake affinity
特性干签,如果兩個進(jìn)程頻繁互動,調(diào)度系統(tǒng)會覺得它們很有可能共享同樣的數(shù)據(jù)拆撼,把它們放到同一CPU核心或NUMA Node
有助于提高緩存和內(nèi)存的訪問性能容劳,所以當(dāng)一個進(jìn)程喚醒另一個的時候,被喚醒的進(jìn)程可能會被放到相同的CPU core
或者相同的NUMA節(jié)點(diǎn)上闸度。此特性對中斷喚醒進(jìn)程時也起作用竭贩,在上一節(jié)所述的現(xiàn)象中,所有的網(wǎng)絡(luò)中斷都分配給CPU 0
去處理莺禁,當(dāng)中斷處理完成時留量,由于wakeup affinity
特性的作用,所喚醒的用戶進(jìn)程也被安排給CPU 0
或其所在的numa節(jié)點(diǎn)上其他core哟冬。而當(dāng)兩個NUMA node
處理中斷時楼熄,這種調(diào)度特性有可能導(dǎo)致Redis進(jìn)程在CPU core
之間頻繁遷移,造成性能損失浩峡。
綜合上述可岂,將中斷都分配在同一NUMA Node
中,中斷處理函數(shù)和應(yīng)用程序充分利用同NUMA下的L2红符、L3緩存青柄、以及同Node下的內(nèi)存,結(jié)合調(diào)度系統(tǒng)的wake affinity
特性预侯,能夠更進(jìn)一步降低延遲。
END
如發(fā)現(xiàn)文章有錯誤峰锁、對內(nèi)容有疑問萎馅,給我留言哦~
彩蛋小福利
點(diǎn)擊免費(fèi)獲取Java學(xué)習(xí)筆記,面試虹蒋,文檔以及視頻