如何识别并解决复杂的dcache问题

背景:这个是在centos7.6的环境上复现的,但该问题其实在不少内核版本上都有,如何作好对linux一些缓存的监控和控制,一直是云计算方向的热点,但这些热点属于细分场景,很难合入到linux主基线,随着ebpf的逐渐稳定,对通用linux内核的编程,观测,可能会有新的收获。本文将分享咱们是怎么排查并解决这个问题的。

1、故障现象

oppo云内核团队发现集群的snmpd的cpu消耗冲高, snmpd几乎长时间占用一个核,perf发现热点以下:node

+   92.00%     3.96%  [kernel]    [k]    __d_lookup 
-   48.95%    48.95%  [kernel]    [k] _raw_spin_lock 
     20.95% 0x70692f74656e2f73                       
        __fopen_internal                              
        __GI___libc_open                              
        system_call                                   
        sys_open                                       
        do_sys_open                                    
        do_filp_open                                   
        path_openat                                    
        link_path_walk                                 
      + lookup_fast                                    
-   45.71%    44.58%  [kernel]    [k] proc_sys_compare 
   - 5.48% 0x70692f74656e2f73                          
        __fopen_internal                               
        __GI___libc_open                               
        system_call                                    
        sys_open                                       
        do_sys_open                                    
        do_filp_open                                   
        path_openat                                    
   + 1.13% proc_sys_compare

几乎都消耗在内核态 __d_lookup的调用中,而后strace看到的消耗为:linux

open("/proc/sys/net/ipv4/neigh/kube-ipvs0/retrans_time_ms", O_RDONLY) = 8 <0.000024>------v4的比较快
open("/proc/sys/net/ipv6/neigh/ens7f0_58/retrans_time_ms", O_RDONLY) = 8 <0.456366>-------v6很慢

进一步手工操做,发现进入ipv6的路径很慢:docker

time cd /proc/sys/net

real 0m0.000s user 0m0.000s sys 0m0.000s编程

time cd /proc/sys/net/ipv6

real 0m2.454s user 0m0.000s sys 0m0.509s后端

time cd /proc/sys/net/ipv4

real 0m0.000s user 0m0.000s sys 0m0.000s 能够看到,进入ipv6的路径的时间消耗远远大于ipv4的路径。centos

2、故障现象分析

咱们须要看一下,为何perf的热点显示为__d_lookup中proc_sys_compare消耗较多,它的流程是怎么样的 proc_sys_compare只有一个调用路径,那就是d_compare回调,从调用链看:缓存

__d_lookup--->if (parent->d_op->d_compare(parent, dentry, tlen, tname, name))
struct dentry *__d_lookup(const struct dentry *parent, const struct qstr *name)
{
.....
	hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {

		if (dentry->d_name.hash != hash)
			continue;

		spin_lock(&dentry->d_lock);
		if (dentry->d_parent != parent)
			goto next;
		if (d_unhashed(dentry))
			goto next;

		/*
		 * It is safe to compare names since d_move() cannot
		 * change the qstr (protected by d_lock).
		 */
		if (parent->d_flags & DCACHE_OP_COMPARE) {
			int tlen = dentry->d_name.len;
			const char *tname = dentry->d_name.name;
			if (parent->d_op->d_compare(parent, dentry, tlen, tname, name))
				goto next;//caq:返回1则是不相同
		} else {
			if (dentry->d_name.len != len)
				goto next;
			if (dentry_cmp(dentry, str, len))
				goto next;
		}
		....
next:
		spin_unlock(&dentry->d_lock);//caq:再次进入链表循环
 	}		

.....
}

集群同物理条件的机器,snmp流程应该同样,因此很天然就怀疑,是否是hlist_bl_for_each_entry_rcu 循环次数过多,致使了parent->d_op->d_compare不停地比较冲突链, 进入ipv6的时候,是否比较次数不少,由于遍历list的过程当中确定会遇到了比较多的cache miss,当遍历了 太多的链表元素,则有可能触发这种状况,下面须要验证下:网络

static inline long hlist_count(const struct dentry *parent, const struct qstr *name)
{
  long count = 0;
  unsigned int hash = name->hash;
  struct hlist_bl_head *b = d_hash(parent, hash);
  struct hlist_bl_node *node;
  struct dentry *dentry;

  rcu_read_lock();
  hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) {
    count++;
  }
  rcu_read_unlock();
  if(count >COUNT_THRES)
  {
     printk("hlist_bl_head=%p,count=%ld,name=%s,hash=%u\n",b,count,name,name->hash);
  }
  return count;
}

kprobe的结果以下:函数

[20327461.948219] hlist_bl_head=ffffb0d7029ae3b0 count = 799259,name=ipv6/neigh/ens7f1_46/base_reachable_time_ms,hash=913731689
[20327462.190378] hlist_bl_head=ffffb0d7029ae3b0 count = 799259,name=ipv6/neigh/ens7f0_51/retrans_time_ms,hash=913731689
[20327462.432954] hlist_bl_head=ffffb0d7029ae3b0 count = 799259,name=ipv6/conf/ens7f0_51/forwarding,hash=913731689
[20327462.675609] hlist_bl_head=ffffb0d7029ae3b0 count = 799259,name=ipv6/neigh/ens7f0_51/base_reachable_time_ms,hash=913731689

从冲突链的长度看,确实进入了dcache的hash表中里面一条比较长的冲突链,该链的dentry个数为799259个, 并且都指向ipv6这个dentry。 了解dcache原理的同窗确定知道,位于冲突链中的元素确定hash值是同样的,而dcache的hash值是用的parent 的dentry加上那么的hash值造成最终的hash值:云计算

static inline struct hlist_bl_head *d_hash(const struct dentry *parent,
					unsigned int hash)
{
	hash += (unsigned long) parent / L1_CACHE_BYTES;
	hash = hash + (hash >> D_HASHBITS);
	return dentry_hashtable + (hash & D_HASHMASK);
}
高版本的内核是:
static inline struct hlist_bl_head *d_hash(unsigned int hash)
{
	return dentry_hashtable + (hash >> d_hash_shift);
}

表面上看,高版本的内核的dentry->dname.hash值的计算变化了,实际上是 hash存放在dentry->d_name.hash的时候,已经加了helper,具体能够参考 以下补丁:

commit 8387ff2577eb9ed245df9a39947f66976c6bcd02
Author: Linus Torvalds <torvalds@linux-foundation.org>
Date:   Fri Jun 10 07:51:30 2016 -0700

    vfs: make the string hashes salt the hash
    
    We always mixed in the parent pointer into the dentry name hash, but we
    did it late at lookup time.  It turns out that we can simplify that
    lookup-time action by salting the hash with the parent pointer early
    instead of late.

问题分析到这里,有两个疑问以下:

  1. 冲突链虽然长,那也可能咱们的dentry在冲突链前面啊 不必定每次都比较到那么远;

  2. proc下的dentry,按道理都是常见和固定的文件名, 为何会这么长的冲突链呢?

要解决这两个疑问,有必要,对冲突链里面的dentry进一步分析。 咱们根据上面kprobe打印的hash头,能够进一步分析其中的dentry以下:

crash> list dentry.d_hash -H 0xffff8a29269dc608 -s dentry.d_sb
ffff89edf533d080
  d_sb = 0xffff89db7fd3c800
ffff8a276fd1e3c0
  d_sb = 0xffff89db7fd3c800
ffff8a2925bdaa80
  d_sb = 0xffff89db7fd3c800
ffff89edf5382a80
  d_sb = 0xffff89db7fd3c800
.....

因为链表很是长,咱们把对应的分析打印到文件,发现全部的这条冲突链中全部的dentry 都是属于同一个super_block,也就是 0xffff89db7fd3c800,

crash> list super_block.s_list -H super_blocks -s super_block.s_id,s_nr_dentry_unused >/home/caq/super_block.txt

# grep ffff89db7fd3c800 super_block.txt  -A 2 
ffff89db7fd3c800
  s_id = "proc\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"

0xffff89db7fd3c800 是 proc 文件系统,他为何会建立这么多ipv6的dentry呢? 继续使用命令看一下dentry对应的d_inode的状况:

...
ffff89edf5375b00
  d_inode = 0xffff8a291f11cfb0
ffff89edf06cb740
  d_inode = 0xffff89edec668d10
ffff8a29218fa780
  d_inode = 0xffff89edf0f75240
ffff89edf0f955c0
  d_inode = 0xffff89edef9c7b40
ffff8a2769e70780
  d_inode = 0xffff8a291c1c9750
ffff8a2921969080
  d_inode = 0xffff89edf332e1a0
ffff89edf5324b40
  d_inode = 0xffff89edf2934800
...

咱们发现,这些同名的,d_name.name均为 ipv6 的dentry,他的inode是不同的,说明这些proc 下的文件不存在硬连接,因此这个是正常的。 咱们继续分析ipv6路径的造成。 /proc/sys/net/ipv6路径的造成,简单地说分为了以下几个步骤:

start_kernel-->proc_root_init()//caq:注册proc fs
因为proc是linux系统默认挂载的,因此查找 kern_mount_data 函数
pid_ns_prepare_proc-->kern_mount_data(&proc_fs_type, ns);//caq:挂载proc fs
proc_sys_init-->proc_mkdir("sys", NULL);//caq:proc目录下建立sys目录
net_sysctl_init-->register_sysctl("net", empty);//caq:在/proc/sys下建立net
对于init_net:
ipv6_sysctl_register-->register_net_sysctl(&init_net, "net/ipv6", ipv6_rotable);
对于其余net_namespace,通常是系统调用触发建立
ipv6_sysctl_net_init-->register_net_sysctl(net, "net/ipv6", ipv6_table);//建立ipv6

有了这些基础,接下来,咱们盯着最后一个,ipv6的建立流程。 ipv6_sysctl_net_init 函数 ipv6_sysctl_register-->register_pernet_subsys(&ipv6_sysctl_net_ops)--> register_pernet_operations-->__register_pernet_operations--> ops_init-->ipv6_sysctl_net_init 常见的调用栈以下:

:Fri Mar  5 11:18:24 2021,runc:[1:CHILD],tid=125338.path=net/ipv6
 0xffffffffb9ac66f0 : __register_sysctl_table+0x0/0x620 [kernel]
 0xffffffffb9f4f7d2 : register_net_sysctl+0x12/0x20 [kernel]
 0xffffffffb9f324c3 : ipv6_sysctl_net_init+0xc3/0x150 [kernel]
 0xffffffffb9e2fe14 : ops_init+0x44/0x150 [kernel]
 0xffffffffb9e2ffc3 : setup_net+0xa3/0x160 [kernel]
 0xffffffffb9e30765 : copy_net_ns+0xb5/0x180 [kernel]
 0xffffffffb98c8089 : create_new_namespaces+0xf9/0x180 [kernel]
 0xffffffffb98c82ca : unshare_nsproxy_namespaces+0x5a/0xc0 [kernel]
 0xffffffffb9897d83 : sys_unshare+0x173/0x2e0 [kernel]
 0xffffffffb9f76ddb : system_call_fastpath+0x22/0x27 [kernel]

在dcache中,咱们/proc/sys/下的各个net_namespace中的dentry都是一块儿hash的, 那怎么保证一个net_namespace 内的dentry隔离呢?咱们来看对应的__register_sysctl_table函数:

struct ctl_table_header *register_net_sysctl(struct net *net,
	const char *path, struct ctl_table *table)
{
	return __register_sysctl_table(&net->sysctls, path, table);
}

struct ctl_table_header *__register_sysctl_table(
	struct ctl_table_set *set,
	const char *path, struct ctl_table *table)
{
	.....
	for (entry = table; entry->procname; entry++)
		nr_entries++;//caq:先计算该table下有多少个项

	header = kzalloc(sizeof(struct ctl_table_header) +
			 sizeof(struct ctl_node)*nr_entries, GFP_KERNEL);
....
	node = (struct ctl_node *)(header + 1);
	init_header(header, root, set, node, table);
....
	/* Find the directory for the ctl_table */
	for (name = path; name; name = nextname) {
....//caq:遍历查找到对应的路径
	}

	spin_lock(&sysctl_lock);
	if (insert_header(dir, header))//caq:插入到管理结构中去
		goto fail_put_dir_locked;
....
}

具体代码不展开,每一个sys下的dentry经过 ctl_table_set 来区分是否可见 而后在查找的时候,比较以下:

static int proc_sys_compare(const struct dentry *parent, const struct dentry *dentry,
		unsigned int len, const char *str, const struct qstr *name)
{
....
	return !head || !sysctl_is_seen(head);
}

static int sysctl_is_seen(struct ctl_table_header *p)
{
	struct ctl_table_set *set = p->set;//获取对应的set
	int res;
	spin_lock(&sysctl_lock);
	if (p->unregistering)
		res = 0;
	else if (!set->is_seen)
		res = 1;
	else
		res = set->is_seen(set);
	spin_unlock(&sysctl_lock);
	return res;
}

//不是同一个 ctl_table_set 则不可见
static int is_seen(struct ctl_table_set *set)
{
	return &current->nsproxy->net_ns->sysctls == set;
}

由以上代码能够看出,当前去查找的进程,若是它归属的net_ns的set 和dentry 中归属的set不一致,则会返回失败,而snmpd归属的 set实际上是init_net的sysctls,而通过查看冲突链中的各个前面绝大多数dentry 的sysctls,都不是归属于init_net的,因此前面都比较失败。

那么,为何归属于init_net的/proc/sys/net的这个dentry会在冲突链的末尾呢? 那个是由于下面的代码致使的:

static inline void hlist_bl_add_head_rcu(struct hlist_bl_node *n,
					struct hlist_bl_head *h)
{
	struct hlist_bl_node *first;

	/* don't need hlist_bl_first_rcu because we're under lock */
	first = hlist_bl_first(h);

	n->next = first;//caq:每次后面添加的时候,是加在链表头
	if (first)
		first->pprev = &n->next;
	n->pprev = &h->first;

	/* need _rcu because we can have concurrent lock free readers */
	hlist_bl_set_first_rcu(h, n);
}

已经知道了snmp对冲突链表比较须要遍历到很后的位置的缘由,接下来,须要弄 明白,为何会有这么多dentry。根据打点,咱们发现了,若是docker不停地 建立pause容器并销毁,这些net下的ipv6的dentry就会累积, 累积的缘由,一个是dentry在没有触发内存紧张的状况下,不会自动销毁, 能缓存则缓存,另外一个则是咱们没有对冲突链的长度进行限制。

那么问题又来了,为何ipv4的dentry就没有累积呢?既然ipv6和ipv4的父parent 都是同样的,那么查看一下这个父parent有多少个子dentry呢?

而后看 hash表里面的dentry,d_parent不少都指向 0xffff8a0a7739fd40 这个dentry。
crash> dentry.d_subdirs 0xffff8a0a7739fd40 ----查看这个父dentry有多少child
  d_subdirs = {
    next = 0xffff8a07a3c6f710, 
    prev = 0xffff8a0a7739fe90
  }
crash> list 0xffff8a07a3c6f710 |wc -l
1598540----------竟然有159万个child

159万个子目录,去掉前面冲突链较长的799259个,还有差很少79万个,那既然进入ipv4路径很快, 说明在net目录下,应该还有其余的dentry有不少子dentry,会不会是一个共性问题?

而后查看集群其余机器,也发现类型现象,截取的打印以下:

count=158505,d_name=net,d_len=3,name=ipv6/conf/all/disable_ipv6,hash=913731689,len=4
hlist_bl_head=ffffbd9d5a7a6cc0,count=158507
 count=158507,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
hlist_bl_head=ffffbd9d429a7498,count=158506

能够看到,ffffbd9d429a7498有着和ffffbd9d5a7a6cc0几乎同样长度的冲突链。 先分析ipv6 链,core链的分析实际上是同样的,挑取冲突链的数据分析以下:

crash> dentry.d_parent,d_name.name,d_lockref.count,d_inode,d_subdirs ffff9b867904f500
  d_parent = 0xffff9b9377368240
  d_name.name = 0xffff9b867904f538 "ipv6"-----这个是一个ipv6的dentry
  d_lockref.count = 1
  d_inode = 0xffff9bba4a5e14c0
  d_subdirs = {
    next = 0xffff9b867904f950, 
    prev = 0xffff9b867904f950
  }

d_child偏移0x90,则0xffff9b867904f950减去0x90为 0xffff9b867904f8c0
crash> dentry 0xffff9b867904f8c0
struct dentry {
......
  d_parent = 0xffff9b867904f500, 
  d_name = {
    {
      {
        hash = 1718513507, 
        len = 4
      }, 
      hash_len = 18898382691
    }, 
    name = 0xffff9b867904f8f8 "conf"------名称为conf
  }, 
  d_inode = 0xffff9bba4a5e61a0, 
  d_iname = "conf\000bles_names\000\060\000.2\000\000pvs.(*Han", 
  d_lockref = {
......
        count = 1----------------引用计数为1,说明还有人引用
......
  }, 
 ......
  d_subdirs = {
    next = 0xffff9b867904fb90, 
    prev = 0xffff9b867904fb90
  }, 
......
}
既然引用计数为1,则继续往下挖:
crash> dentry.d_parent,d_lockref.count,d_name.name,d_subdirs 0xffff9b867904fb00
  d_parent = 0xffff9b867904f8c0
  d_lockref.count = 1
  d_name.name = 0xffff9b867904fb38 "all"
  d_subdirs = {
    next = 0xffff9b867904ef90, 
    prev = 0xffff9b867904ef90
  }
  再往下:
crash> dentry.d_parent,d_lockref.count,d_name.name,d_subdirs,d_flags,d_inode -x 0xffff9b867904ef00
  d_parent = 0xffff9b867904fb00
  d_lockref.count = 0x0-----------------------------挖到引用计数为0为止
  d_name.name = 0xffff9b867904ef38 "disable_ipv6"
  d_subdirs = {
    next = 0xffff9b867904efa0, --------为空
    prev = 0xffff9b867904efa0
  }
  d_flags = 0x40800ce-------------下面重点分析这个
  d_inode = 0xffff9bba4a5e4fb0

能够看到,ipv6的dentry路径为ipv6/conf/all/disable_ipv6,和probe看到的同样, 针对 d_flags ,分析以下:

#define DCACHE_FILE_TYPE        0x04000000 /* Other file type */

#define DCACHE_LRU_LIST     0x80000--------这个表示在lru上面

#define DCACHE_REFERENCED   0x0040  /* Recently used, don't discard. */
#define DCACHE_RCUACCESS    0x0080  /* Entry has ever been RCU-visible */

#define DCACHE_OP_COMPARE   0x0002
#define DCACHE_OP_REVALIDATE    0x0004
#define DCACHE_OP_DELETE    0x0008

咱们看到,disable_ipv6的引用计数为0,可是它是有 DCACHE_LRU_LIST 标志的, 根据以下函数:

static void dentry_lru_add(struct dentry *dentry)
{
	if (unlikely(!(dentry->d_flags & DCACHE_LRU_LIST))) {
		spin_lock(&dcache_lru_lock);
		dentry->d_flags |= DCACHE_LRU_LIST;//有这个标志说明在lru上
		list_add(&dentry->d_lru, &dentry->d_sb->s_dentry_lru);
		dentry->d_sb->s_nr_dentry_unused++;//caq:放在s_dentry_lru是空闲的
		dentry_stat.nr_unused++;
		spin_unlock(&dcache_lru_lock);
	}
}

到此,说明它是能够释放的,因为是线上业务,咱们不敢使用 echo 2 >/proc/sys/vm/drop_caches 而后编写一个模块去释放,模块的主代码以下,参考 shrink_slab:

spin_lock(orig_sb_lock);
        list_for_each_entry(sb, orig_super_blocks, s_list) {
                if (memcmp(&(sb->s_id[0]),"proc",strlen("proc"))||\
                   memcmp(sb->s_type->name,"proc",strlen("proc"))||\
                    hlist_unhashed(&sb->s_instances)||\
                    (sb->s_nr_dentry_unused < NR_DENTRY_UNUSED_LEN) )
                        continue;
                sb->s_count++;
                spin_unlock(orig_sb_lock);
                printk("find proc sb=%p\n",sb);
                shrinker = &sb->s_shrink;
                
               count = shrinker_one(shrinker,&shrink,1000,1000);
               printk("shrinker_one count =%lu,sb=%p\n",count,sb);
               spin_lock(orig_sb_lock);//caq:再次持锁
                if (sb_proc)
                        __put_super(sb_proc);
                sb_proc = sb;

         }
         if(sb_proc){
             __put_super(sb_proc);
             spin_unlock(orig_sb_lock);
         }
         else{
            spin_unlock(orig_sb_lock);
            printk("can't find the special sb\n");
         }

就发现确实两条冲突链都被释放了。 好比某个节点在释放前:

[3435957.357026] hlist_bl_head=ffffbd9d5a7a6cc0,count=34686
[3435957.357029] count=34686,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
[3435957.457039] IPVS: Creating netns size=2048 id=873057
[3435957.477742] hlist_bl_head=ffffbd9d429a7498,count=34686
[3435957.477745] count=34686,d_name=net,d_len=3,name=ipv6/conf/all/disable_ipv6,hash=913731689,len=4
[3435957.549173] hlist_bl_head=ffffbd9d5a7a6cc0,count=34687
[3435957.549176] count=34687,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
[3435957.667889] hlist_bl_head=ffffbd9d429a7498,count=34687
[3435957.667892] count=34687,d_name=net,d_len=3,name=ipv6/conf/all/disable_ipv6,hash=913731689,len=4
[3435958.720110] find proc sb=ffff9b647fdd4000-----------------------开始释放
[3435959.150764] shrinker_one count =259800,sb=ffff9b647fdd4000------释放结束

单独释放后:

[3436042.407051] hlist_bl_head=ffffbd9d466aed58,count=101
[3436042.407055] count=101,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
[3436042.501220] IPVS: Creating netns size=2048 id=873159
[3436042.591180] hlist_bl_head=ffffbd9d466aed58,count=102
[3436042.591183] count=102,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
[3436042.685008] hlist_bl_head=ffffbd9d4e8af728,count=101
[3436042.685011] count=101,d_name=net,d_len=3,name=ipv6/conf/all/disable_ipv6,hash=913731689,len=4
[3436043.957221] IPVS: Creating netns size=2048 id=873160
[3436044.043860] hlist_bl_head=ffffbd9d466aed58,count=103
[3436044.043863] count=103,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
[3436044.137400] hlist_bl_head=ffffbd9d4e8af728,count=102
[3436044.137403] count=102,d_name=net,d_len=3,name=ipv6/conf/all/disable_ipv6,hash=913731689,len=4
[3436044.138384] IPVS: Creating netns size=2048 id=873161
[3436044.226954] hlist_bl_head=ffffbd9d466aed58,count=104
[3436044.226956] count=104,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4
[3436044.321947] hlist_bl_head=ffffbd9d4e8af728,count=103

上面能够看出两个细节:

  1. 释放前,hlist也是在增加的,释放后,hlist仍是在增加。

  2. 释放后,net的dentry变了,因此hashlist的位置变化了。

综上所述,咱们遍历热点慢,是由于snmpd所要查找init_net的ctl_table_set 和dcache中的其余dentry 归属的 ctl_table_set 不一致致使,而链表的长度则 是由于有人在销毁net_namespace的时候,还在访问ipv6/conf/all/disable_ipv6 以及 core/somaxconn 致使的,这两个dentry 都被放在了归属的super_block的 s_dentry_lru 上。 最后一个疑问,是什么调用访问了这些dentry呢?触发的机制以下:

pid=16564,task=exe,par_pid=366883,task=dockerd,count=1958,d_name=net,d_len=3,name=ipv6/conf/all/disable_ipv6,hash=913731689,len=4,hlist_bl_head=ffffbd9d429a7498
hlist_bl_head=ffffbd9d5a7a6cc0,count=1960

pid=16635,task=runc:[2:INIT],par_pid=16587,task=runc,count=1960,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4,hlist_bl_head=ffffbd9d5a7a6cc0
hlist_bl_head=ffffbd9d429a7498,count=1959

能够看到,其实就是 dockerd和runc 触发了这个问题,k8调用docker不停建立pause容器, cni的网络参数填写不对,致使建立的net_namespace 很快被销毁,虽然销毁时调用了 unregister_net_sysctl_table,但同时 runc 和exe 访问了 该net_namespace下的两个dentry,致使这两个dentry被缓存在了 super_block的 s_dentry_lru链表上。再由于总体内存比较充足,因此一直会增加。 注意到对应的路径就是:ipv6/conf/all/disable_ipv6以及 core/somaxconn,ipv4路径下的dentry由于没有 当时在访问的,因此ctl_table可以当时就清理掉。 而倒霉的snmpd由于一直要访问对应的链, cpu就冲高了,使用手工 drop_caches 以后,马上恢复,注意,线上的机器不能使用 drop_caches ,这个会致使sys 冲高,影响一些时延敏感型的业务。

3、故障复现

  1. 内存空余的状况下,没有触发slab的内存回收,k8调用docker建立不一样net_namespace 的pause容器,但由于cni的参数不对,会马上销毁刚建立的net_namespace,若是你在dmesg 中频繁地看到以下日志:
IPVS: Creating netns size=2048 id=866615

则有必要关注一下 dentry的缓存状况。

4、故障规避或解决

可能的解决方案是:

  1. 经过rcu的方式,读取 dentry_hashtable 的各个冲突链,大于必定程度,抛出告警。

  2. 经过一个proc参数,设置缓存的dentry的个数。

  3. 全局能够关注 /proc/sys/fs/dentry-state

  4. 局部的,能够针对super_block,读取s_nr_dentry_unused,超过必定数量,则告警, 示例代码能够参考shrink_slab函数的实现。

  5. 注意与 negative-dentry-limit 的区别。

  6. 内核中使用hash桶的地方不少,咱们该怎么监控hash桶冲突链的长度呢?作成模块扫描,或者找地方保存一个链表长度。

5、做者简介

Anqing OPPO高级后端工程师

目前在oppo混合云负责linux内核及容器,虚拟机等虚拟化方面的工做。

获取更多精彩内容:关注[OPPO互联网技术]公众号

相关文章
相关标签/搜索