[linux][perf]list過長導致CPU消耗過高的問題分析

  • 2019 年 12 月 15 日
  • 筆記

前言

某機器上網絡出現時斷時續的問題,網絡的同事發現ovs進程的CPU消耗很高,硬件offload的規則下發卡住的問題。即通過netlink向內核發送消息卡住。

分析

perf分析熱點函數

可見,CPU的主要消耗是在__tcf_chain_get函數和__mutex_lock上。

為什麼鎖消耗那麼高

rtnetlink_rcv_msg函數(去掉參數檢查等等,保留關鍵邏輯),通過這段邏輯我們可以知道,在執行每個netlink命令的時候,都會先執行鎖操作,在執行具體的回調函數(即link->doit),再解鎖的過程。

如果其中某一個回調函數執行時間過長,就會長時間佔用鎖,造成其他的link->doit回調函數block住更長的時間,那麼鎖的消耗也會更高。

再結合其他的代碼邏輯可以發現,__tcf_chain_get函數就剛好在某一個回調函數的路徑上。

Annotate __tcf_chain_get

分析上面的熱點函數__tcf_chain_get

__tcf_chain_get比較長,理論上來說,一個cmp指令只要一個cycle即可,那麼在這裡為什麼一個cmp指令使用了99.13%的CPU時間呢?可以從彙編中看到,需要取rax偏移0x18的地址上數值和esi進行比較。這裡存在一次讀取內存的操作,如果是鏈表的話,前後兩個鏈表元素未必在內存上相鄰,容易造成CPU的cache miss。

計算熱點代碼的路徑

ffffffff8161ab40+1d= ffffffff8161ab5d

所以執行addr2line -e /usr/lib/debug/vmlinux-4.19 -a 0xffffffff8161ab5d

0xffffffff8161ab5d

可以得到/linux/net/sched/cls_api.c:268

可以看到268行是對chain->index和chain_index進行對比。

繼續看tcf_chain結構體

index結構在tcf_chain結構體中偏移0x20,為什麼反彙編的代碼在0x18上?

結合上下文我們可以看到,使用list來遍歷:

chain的地址是在list的地址-0x8,index的地址是在chain+0x20,那麼index的地址相對於list的地址就是+0x18,計算chain的過程都被編譯器優化掉了,只需要使用list的地址+0x18即可完成代碼邏輯中的遍歷過程。

綜上,可以證實,__tcf_chain_get消耗過高的原因是:遍歷list的過程中遇到了比較多的cache miss;遍歷了太多的鏈表元素的導致的。

計算鏈表的長度

基於kprobe實現kmod,來dump出來鏈表的長度。

#include <linux/kernel.h>  #include <linux/module.h>  #include <linux/kprobes.h>  #include <linux/uaccess.h>  #include <net/sch_generic.h>      static long filter_count(struct tcf_chain *chain)  {    struct tcf_proto *tp;    long count = 0;      for (tp = rtnl_dereference(chain->filter_chain);        tp; tp = rtnl_dereference(tp->next)) {      count++;    }    return count;  }      static int kp_do_tcf_handler(struct kprobe *p, struct pt_regs *regs)  {    struct tcf_block *block = (void __user *)regs->di;    struct tcf_chain *chain;    long count = 0;      list_for_each_entry(chain, &block->chain_list, list) {      count++;    }      printk("[always]count = %ldn", count);      return 0;  }    static struct kprobe kp = {    .symbol_name  = "__tcf_chain_get",    .pre_handler = kp_do_tcf_handler,  };    static int __init kprobe_init(void)  {    int ret;      ret = register_kprobe(&kp);    if (ret < 0) {      pr_err("register_kprobe failed, returned %dn", ret);      return ret;    }      pr_info("[probe-tcf]Planted kprobe at %pn", kp.addr);    return 0;  }    static void __exit kprobe_exit(void)  {    unregister_kprobe(&kp);    pr_info("[probe-unregistered]kprobe at %p unregisteredn", kp.addr);  }    module_init(kprobe_init)  module_exit(kprobe_exit)  MODULE_LICENSE("GPL");  MODULE_AUTHOR("pizhenwei [email protected]");

得到dump的結果

可以知道list的元素多少不等,有的不到100,有的到達了接近25W個元素。由此可以論證上面的猜測,鏈表元素太多。

找到哪個dev的鏈表元素過多

再進一步完善kprobe的邏輯,

#include <linux/kernel.h>  #include <linux/module.h>  #include <linux/kprobes.h>  #include <linux/uaccess.h>  #include <net/sch_generic.h>    static long filter_count(struct tcf_chain *chain)  {    struct tcf_proto *tp;    long count = 0;      for (tp = rtnl_dereference(chain->filter_chain);        tp; tp = rtnl_dereference(tp->next)) {      count++;    }      return count;  }    static int kp_do_tcf_handler(struct kprobe *p, struct pt_regs *regs)  {    struct tcf_block *block = (void __user *)regs->di;    struct tcf_chain *chain;    long count = 0, tps;    long tpc[4] = {0};  //繪製filter count分佈圖    long ref[4] = {0};  //繪製refcnt的分佈圖    long action_ref[4] = {0};  //繪製action_refcnt的分佈圖    long actrefonly = 0;  //過濾出來action_refcnt等於refcnt的分佈圖      list_for_each_entry(chain, &block->chain_list, list) {      count++;        tps = filter_count(chain);      if (tps == 0)        tpc[0]++;      else if (tps > 0 && tps <= 1)        tpc[1]++;      else if (tps > 1 && tps <= 2)        tpc[2]++;      else        tpc[3]++;        if (chain->refcnt == 0)        ref[0]++;      else if (chain->refcnt > 0 && chain->refcnt <= 1)        ref[1]++;      else if (chain->refcnt > 1 && chain->refcnt <= 2)        ref[2]++;      else        ref[3]++;        if (chain->action_refcnt == 0)        action_ref[0]++;      else if (chain->action_refcnt > 0 && chain->action_refcnt <= 1)        action_ref[1]++;      else if (chain->action_refcnt > 1 && chain->action_refcnt <= 2)        action_ref[2]++;      else        action_ref[3]++;          if (chain->action_refcnt == chain->refcnt)        actrefonly++;    }      #if 1    if (count < 1000)  //過濾鏈表元素少於1000的情況      return 0;  #endif      printk("[always]DEV %sn", block→q→dev_queue→dev→name);  //打印出來異常的netdev的名字      printk("[always][0]count = %ldt", tpc[0]);    printk("[always][1]count = %ldt", tpc[1]);    printk("[always][2]count = %ldt", tpc[2]);    printk("[always][4]count = %ldn", tpc[3]);      printk("[always][0]action_ref = %ldt", action_ref[0]);    printk("[always][1]action_ref = %ldt", action_ref[1]);    printk("[always][2]action_ref = %ldt", action_ref[2]);    printk("[always][4]action_ref = %ldn", action_ref[3]);      printk("[always][0]ref = %ldt", ref[0]);    printk("[always][1]ref = %ldt", ref[1]);    printk("[always][2]ref = %ldt", ref[2]);    printk("[always][4]ref = %ldn", ref[3]);      printk("[always]actrefonly = %ldn", actrefonly);      return 0;  }    static struct kprobe kp = {    .symbol_name  = "__tcf_chain_get",    .pre_handler = kp_do_tcf_handler,  };    static int __init kprobe_init(void)  {    int ret;      ret = register_kprobe(&kp);    if (ret < 0) {      pr_err("register_kprobe failed, returned %dn", ret);      return ret;    }      pr_info("[probe-tcf]Planted kprobe at %pn", kp.addr);    return 0;  }    static void __exit kprobe_exit(void)  {    unregister_kprobe(&kp);    pr_info("[probe-unregistered]kprobe at %p unregisteredn", kp.addr);  }    module_init(kprobe_init)  module_exit(kprobe_exit)  MODULE_LICENSE("GPL");  MODULE_AUTHOR("pizhenwei [email protected]");

得到dump出來的結果

至此,我們已經找到了vxlan_sys_4789上的元素比較多。

後面,經過網絡同事的驗證(通過tc命令dump出來結果),vxlan_sys_4789上的chain確實過多。腳本刪除空的chain後,ovs的cpu消耗下降到10%以內,網絡恢復正常。