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

背景

原本穩(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)代表什么煌集。

image

這個指標(biāo)的數(shù)據(jù)源,是讀取/proc/net/dev中的數(shù)據(jù)穆碎,監(jiān)控Agent做簡單的處理之后上報牙勘。以下為/proc/net/dev的一個示例职恳,可以看到第一行Receive代表in所禀,Transmit代表out,第二行即各個表頭字段放钦,再往后每一行代表一個網(wǎng)卡設(shè)備具體的值色徘。

image

其中各個字段意義如下:

字段 解釋
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_droppedrx_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_errorsmissed的基礎(chǔ)上,還加上了RQDPC計數(shù)胸嘁,而ixgbe就沒這個統(tǒng)計驹饺。RQDPC計數(shù)是描述符不夠的計數(shù),missedfifo滿的計數(shù)缴渊。所以對于ixgbe來說赏壹,rx_fifo_errorsrx_missed_errors確實(shí)是等同的。

通過命令ethtool -S eth0可以查看網(wǎng)卡一些統(tǒng)計信息衔沼,其中就包含了上文提到的幾個重要指標(biāo)rx_dropped蝌借、rx_missed_errorsrx_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é)倦畅,但大致需要以下幾個步驟:

  1. 網(wǎng)卡收到數(shù)據(jù)包遮糖。
  2. 將數(shù)據(jù)包從網(wǎng)卡硬件緩存轉(zhuǎn)移到服務(wù)器內(nèi)存中。
  3. 通知內(nèi)核處理滔迈。
  4. 經(jīng)過TCP/IP協(xié)議逐層處理止吁。
  5. 應(yīng)用程序通過read()socket buffer讀取數(shù)據(jù)被辑。
image

將網(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ù)宏怔,而是一個描述符,這個描述符指向了它真正的存儲地址畴椰,具體流程如下:

  1. 驅(qū)動在內(nèi)存中分配一片緩沖區(qū)用來接收數(shù)據(jù)包臊诊,叫做sk_buffer
  2. 將上述緩沖區(qū)的地址和大行敝(即接收描述符)抓艳,加入到rx ring buffer。描述符中的緩沖區(qū)地址是DMA使用的物理地址帚戳;
  3. 驅(qū)動通知網(wǎng)卡有一個新的描述符玷或;
  4. 網(wǎng)卡從rx ring buffer中取出描述符,從而獲知緩沖區(qū)的地址和大衅巍偏友;
  5. 網(wǎng)卡收到新的數(shù)據(jù)包;
  6. 網(wǎng)卡將新數(shù)據(jù)包通過DMA直接寫到sk_buffer中对供。
image

當(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é)議棧處理。

image

通過中斷送粱,能夠快速及時地響應(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

image

注意到,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):

image

其中每行代表一個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é)果為:MSIXixgbe_msix_clean_rings负间,MSIixgbe_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)為falsestatic 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)卡因宇,那么就會有多行輸出:

image

中斷的親緣性設(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嵌施。

image

那為什么中斷號只設(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)化。

image

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)化空間呢砾跃?

image

通過觀察骏啰,我們發(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中,通過字段processorphysical 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)行分析固棚。

image

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

image

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的訪問會有幾十甚至上百倍的性能損耗式矫。

image

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)一步降低延遲。

image

END

如發(fā)現(xiàn)文章有錯誤峰锁、對內(nèi)容有疑問萎馅,給我留言哦~

彩蛋小福利

點(diǎn)擊免費(fèi)獲取Java學(xué)習(xí)筆記,面試虹蒋,文檔以及視頻

部分資料如下:

15178719-828b0315c84c0bf0.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末糜芳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子魄衅,更是在濱河造成了極大的恐慌峭竣,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晃虫,死亡現(xiàn)場離奇詭異皆撩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門扛吞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呻惕,“玉大人,你說我怎么就攤上這事滥比⊙谴啵” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵盲泛,是天一觀的道長濒持。 經(jīng)常有香客問我,道長寺滚,這世上最難降的妖魔是什么柑营? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮玛迄,結(jié)果婚禮上由境,老公的妹妹穿的比我還像新娘。我一直安慰自己蓖议,他們只是感情好虏杰,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著勒虾,像睡著了一般纺阔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上修然,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天笛钝,我揣著相機(jī)與錄音,去河邊找鬼愕宋。 笑死玻靡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的中贝。 我是一名探鬼主播囤捻,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼邻寿!你這毒婦竟也來了蝎土?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤绣否,失蹤者是張志新(化名)和其女友劉穎誊涯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒜撮,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡暴构,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丹壕。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡庆械,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菌赖,到底是詐尸還是另有隱情缭乘,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布琉用,位于F島的核電站堕绩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏邑时。R本人自食惡果不足惜奴紧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望晶丘。 院中可真熱鬧黍氮,春花似錦、人聲如沸浅浮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滚秩。三九已至专执,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間郁油,已是汗流浹背本股。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桐腌,地道東北人拄显。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像案站,于是被迫代替她去往敵國和親凿叠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容