专栏首页皮振伟的专栏[linux][perf]list过长导致CPU消耗过高的问题分析

[linux][perf]list过长导致CPU消耗过高的问题分析

前言

某机器上网络出现时断时续的问题,网络的同事发现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 = %ld\n", 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 %d\n", ret);
    return ret;
  }

  pr_info("[probe-tcf]Planted kprobe at %p\n", kp.addr);
  return 0;
}

static void __exit kprobe_exit(void)
{
  unregister_kprobe(&kp);
  pr_info("[probe-unregistered]kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("pizhenwei pizhenwei@bytedance.com");

得到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 %s\n", block→q→dev_queue→dev→name);  //打印出来异常的netdev的名字

  printk("[always][0]count = %ld\t", tpc[0]);
  printk("[always][1]count = %ld\t", tpc[1]);
  printk("[always][2]count = %ld\t", tpc[2]);
  printk("[always][4]count = %ld\n", tpc[3]);

  printk("[always][0]action_ref = %ld\t", action_ref[0]);
  printk("[always][1]action_ref = %ld\t", action_ref[1]);
  printk("[always][2]action_ref = %ld\t", action_ref[2]);
  printk("[always][4]action_ref = %ld\n", action_ref[3]);

  printk("[always][0]ref = %ld\t", ref[0]);
  printk("[always][1]ref = %ld\t", ref[1]);
  printk("[always][2]ref = %ld\t", ref[2]);
  printk("[always][4]ref = %ld\n", ref[3]);

  printk("[always]actrefonly = %ld\n", 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 %d\n", ret);
    return ret;
  }

  pr_info("[probe-tcf]Planted kprobe at %p\n", kp.addr);
  return 0;
}

static void __exit kprobe_exit(void)
{
  unregister_kprobe(&kp);
  pr_info("[probe-unregistered]kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("pizhenwei pizhenwei@bytedance.com");

得到dump出来的结果

至此,我们已经找到了vxlan_sys_4789上的元素比较多。

后面,经过网络同事的验证(通过tc命令dump出来结果),vxlan_sys_4789上的chain确实过多。脚本删除空的chain后,ovs的cpu消耗下降到10%以内,网络恢复正常。

本文分享自微信公众号 - AlwaysGeek(gh_d0972b1eeb60)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • MySQL之数据库基本查询语句

    最后是今天的分享:Author、Article、ArticleDetail三张表一键建表SQL语句

    ITester软件测试小栈
  • Selenium自动化测试-JavaScript定位

    做自动化过程中,会发现有的按钮点击不了,或者点击没有反应,也没有报错,或者不能处理滚动条等场景,我们可以通过JavaScript定位来解决这些问题。

    ITester软件测试小栈
  • 经典算法之动态规划

    动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。在面试笔试中动态规划也...

    用户3467126
  • “中台”都在讲什么?一文聊聊中台技术

    2019年,中台这个概念非常热门,由于这种模式有助于提高效率、降低成本、保证质量,一线互联网大厂,如阿里,腾讯,网易,滴滴,纷纷入坑中台。

    DevOps时代
  • 经典算法之bitmap算法

    本文将讲述Bit-Map算法的相关原理,Bit-Map算法的一些利用场景,例如BitMap解决海量数据寻找重复、判断个别元素是否在海量数据当中等问题.最后说说B...

    用户3467126
  • 相爱相杀的运维之殤:苏宁消费金融超大规模 IT 系统 DevOps 实践

    今天跟大家分享的一个主题,就是苏宁消费金融超大规模IT系统DevOps的落地实践。下面分四个部分:

    DevOps时代
  • 经典算法之稀疏矩阵

    在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵;与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵...

    用户3467126
  • 数据预处理有哪些方法?

    数据清理(data cleaning) 的主要思想是通过填补缺失值、光滑噪声数据,平滑或删除离群点,并解决数据的不一致性来“清理“数据。

    加米谷大数据
  • TensorFlow是什么?怎么用?终于有人讲明白了

    导读:在开始使用TensorFlow之前,必须了解它背后的理念。该库很大程度上基于计算图的概念,除非了解它们是如何工作的,否则无法理解如何使用该库。本文将简要介...

    用户1621951
  • 二叉树-LeetCode 235、236、226、230(中序,LCA,DFS)

    给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 ...

    算法工程师之路

扫码关注云+社区

领取腾讯云代金券