Redis 高負載下的中斷優化

背景

本來穩定的環境也由於請求量的上漲帶來了不少不穩定的因素,其中一直困擾咱們的就是網卡丟包問題。起初線上存在部分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_droppedrx_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_errorsmissed的基礎上,還加上了RQDPC計數,而ixgbe就沒這個統計。RQDPC計數是描述符不夠的計數,missedfifo滿的計數。因此對於ixgbe來講,rx_fifo_errorsrx_missed_errors確實是等同的。網絡

經過命令ethtool -S eth0能夠查看網卡一些統計信息,其中就包含了上文提到的幾個重要指標rx_droppedrx_missed_errorsrx_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內核的緩衝區中。接下來,咱們要繼續探索究竟是什麼緩衝區引發了丟包問題,這就須要完整地瞭解服務器接收數據包的過程。

瞭解接收數據包的流程

接收數據包是一個複雜的過程,涉及不少底層的技術細節,但大體須要如下幾個步驟:

  1. 網卡收到數據包。
  2. 將數據包從網卡硬件緩存轉移到服務器內存中。
  3. 通知內核處理。
  4. 通過TCP/IP協議逐層處理。
  5. 應用程序經過read()socket buffer讀取數據。

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

NIC在接收到數據包以後,首先須要將數據同步到內核中,這中間的橋樑是rx ring buffer。它是由NIC和驅動程序共享的一片區域,事實上,rx ring buffer存儲的並非實際的packet數據,而是一個描述符,這個描述符指向了它真正的存儲地址,具體流程以下:

  1. 驅動在內存中分配一片緩衝區用來接收數據包,叫作sk_buffer
  2. 將上述緩衝區的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的緩衝區地址是DMA使用的物理地址;
  3. 驅動通知網卡有一個新的描述符;
  4. 網卡從rx ring buffer中取出描述符,從而獲知緩衝區的地址和大小;
  5. 網卡收到新的數據包;
  6. 網卡將新數據包經過DMA直接寫到sk_buffer中。

當驅動處理速度跟不上網卡收包速度時,驅動來不及分配緩衝區,NIC接收到的數據包沒法及時寫到sk_buffer,就會產生堆積,當NIC內部緩衝區寫滿後,就會丟棄部分數據,引發丟包。這部分丟包爲rx_fifo_errors,在/proc/net/dev中體現爲fifo字段增加,在ifconfig中體現爲overruns指標增加。

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

這個時候,數據包已經被轉移到了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,結果爲: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)以後通過一些列調用,直到發起名爲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是針對默認爲falsestatic 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;
}
複製代碼

TCP/IP協議棧逐層處理,最終交給用戶空間讀取

數據包進到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處理,如何優化網卡的中斷成爲了咱們關注的重點。

優化策略

CPU親緣性

前文提到,丟包是由於隊列中的數據包超過了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的慢查詢數量有明顯上升,甚至部分業務也受到了影響,慢查詢增多直接致使可用性下降,所以方案仍需進一步優化。

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中,經過字段processorphysical id 能確認這一點,那麼響應慢是否和物理CPU有關呢?物理CPU又和NUMA架構關聯,每一個物理CPU對應一個NUMA node,那麼接下來就要從NUMA角度進行分析。

NUMA

SMP 架構

隨着單核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)。

NUMA 架構

在前面的FSB(前端系統總線)結構中,當CPU不斷增加的狀況下,共享的系統總線就會由於資源競爭(多核爭搶總線資源以訪問北橋上的內存)而出現擴展和性能問題。

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

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

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

NUMA 架構下的中斷優化

這時咱們再回歸到中斷的問題上,當兩個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特性,可以更進一步下降延遲。

相關文章
相關標籤/搜索