写点什么

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

发布于: 刚刚

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

一、故障现象

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


+   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 看到的消耗为:


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 的路径很慢:

time cd /proc/sys/net

real 0m0.000suser 0m0.000ssys 0m0.000s

time cd /proc/sys/net/ipv6

real 0m2.454suser 0m0.000ssys 0m0.509s

time cd /proc/sys/net/ipv4

real 0m0.000suser 0m0.000ssys 0m0.000s 可以看到,进入 ipv6 的路径的时间消耗远远大于 ipv4 的路径。

二、故障现象分析

我们需要看一下,为什么 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 8387ff2577eb9ed245df9a39947f66976c6bcd02Author: 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_sbffff89edf533d080  d_sb = 0xffff89db7fd3c800ffff8a276fd1e3c0  d_sb = 0xffff89db7fd3c800ffff8a2925bdaa80  d_sb = 0xffff89db7fd3c800ffff89edf5382a80  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 = 0xffff8a291f11cfb0ffff89edf06cb740  d_inode = 0xffff89edec668d10ffff8a29218fa780  d_inode = 0xffff89edf0f75240ffff89edf0f955c0  d_inode = 0xffff89edef9c7b40ffff8a2769e70780  d_inode = 0xffff8a291c1c9750ffff8a2921969080  d_inode = 0xffff89edf332e1a0ffff89edf5324b40  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 fsproc_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 -l1598540----------居然有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=4hlist_bl_head=ffffbd9d5a7a6cc0,count=158507 count=158507,d_name=net,d_len=3,name=core/somaxconn,hash=1701998435,len=4hlist_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为 0xffff9b867904f8c0crash> dentry 0xffff9b867904f8c0struct 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=ffffbd9d429a7498hlist_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=ffffbd9d5a7a6cc0hlist_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 冲高,影响一些时延敏感型的业务。

三、故障复现

  1. 内存空余的情况下,没有触发 slab 的内存回收,k8 调用 docker 创建不同 net_namespace 的 pause 容器,但因为 cni 的参数不对,会立刻销毁刚创建的 net_namespace,如果你在 dmesg 中频繁地看到如下日志:


IPVS: Creating netns size=2048 id=866615
复制代码


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

四、故障规避或解决

可能的解决方案是:


  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 桶冲突链的长度呢?做成模块扫描,或者找地方保存一个链表长度。

五、作者简介

Anqing OPPO 高级后端工程师


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


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


发布于: 刚刚阅读数: 5
用户头像

还未添加个人签名 2019.12.23 加入

OPPO数智技术干货及技术活动分享平台

评论

发布
暂无评论
如何识别并解决复杂的dcache问题