关于后端:如何识别并解决复杂的dcache问题

4次阅读

共计 15880 个字符,预计需要花费 40 分钟才能阅读完成。

背景:这个是在 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.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 的门路。

二、故障景象剖析

咱们须要看一下,为什么 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 冲高,影响一些时延敏感型的业务。

三、故障复现

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 互联网技术] 公众号

正文完
 0