本來穩定的環境也由於請求量的上漲帶來了不少不穩定的因素,其中一直困擾咱們的就是網卡丟包問題。起初線上存在部分Redis節點還在使用千兆網卡的老舊服務器,而緩存服務每每須要承載極高的查詢量,並要求毫秒級的響應速度,如此一來千兆網卡很快就出現了瓶頸。通過整治,咱們將千兆網卡服務器替換爲了萬兆網卡服務器,本覺得能夠高枕無憂,可是沒想到,在業務高峯時段,機器也居然出現了丟包問題,而此時網卡帶寬使用還遠遠沒有達到瓶頸。html
首先,咱們在系統監控的net.if.in.dropped
指標中,看到有大量數據丟包異常,那麼第一步就是要了解這個指標表明什麼。前端
這個指標的數據源,是讀取/proc/net/dev
中的數據,監控Agent作簡單的處理以後上報。如下爲/proc/net/dev
的一個示例,能夠看到第一行Receive表明in,Transmit表明out,第二行即各個表頭字段,再日後每一行表明一個網卡設備具體的值。node
其中各個字段意義以下:linux
字段 | 解釋 |
---|---|
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()
函數輸出的:api
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
都是從設備獲取的,而且須要設備驅動實現。bash
/**
* 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內核的緩衝區中。接下來,咱們要繼續探索究竟是什麼緩衝區引發了丟包問題,這就須要完整地瞭解服務器接收數據包的過程。
接收數據包是一個複雜的過程,涉及不少底層的技術細節,但大體須要如下幾個步驟:
read()
從socket buffer
讀取數據。NIC在接收到數據包以後,首先須要將數據同步到內核中,這中間的橋樑是rx ring buffer
。它是由NIC和驅動程序共享的一片區域,事實上,rx ring buffer
存儲的並非實際的packet數據,而是一個描述符,這個描述符指向了它真正的存儲地址,具體流程以下:
sk_buffer
;rx ring buffer
。描述符中的緩衝區地址是DMA使用的物理地址;rx ring buffer
中取出描述符,從而獲知緩衝區的地址和大小;sk_buffer
中。當驅動處理速度跟不上網卡收包速度時,驅動來不及分配緩衝區,NIC接收到的數據包沒法及時寫到sk_buffer
,就會產生堆積,當NIC內部緩衝區寫滿後,就會丟棄部分數據,引發丟包。這部分丟包爲rx_fifo_errors
,在/proc/net/dev
中體現爲fifo字段增加,在ifconfig中體現爲overruns指標增加。
這個時候,數據包已經被轉移到了sk_buffer
中。前文提到,這是驅動程序在內存中分配的一片緩衝區,而且是經過DMA寫入的,這種方式不依賴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,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
對應的符號信息:
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訪問的概率也大大提升,而且沒法充分利用L二、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下的L二、L3緩存、以及同Node下的內存,結合調度系統的wake affinity
特性,可以更進一步下降延遲。