蜗窝科技

一个较复杂dcache问题

安庆 · · Linux内核分析 · 阅读 8448 · 2021年6月存档

一个较复杂dcache问题

背景:这个是在centos7.6的环境上复现的,但问题其实在很多内核版本上都有

下面列一下我们是怎么排查并解这个问题的。

一、故障现象

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链表上。再因为整体内存比较充足,所以一直会增长。

而倒霉的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桶冲突链的长度呢?做成模块

扫描,或者找地方保存一个链表长度。

五、感谢

在这个问题的解决过程中,k8的美女伟伟同学和网络的高工郑翔同学提供了很多帮助,在

此表示感谢。

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(3 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • nswcfd@cu

    感谢分享!
  • nswcfd@cu

    请问为什么ipv4的hash链并没有那么长呢?
    • 安庆

      @nswcfd@cu:因为当销毁net_namespace的时候,exe和runc两个进程并没有访问对应的ipv4目录下的dentry。所以能够顺利清理掉对应ctl_table的dentry。