背景:這個是在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.
問題分析到這裏,有兩個疑問以下:
-
衝突鏈雖然長,那也可能咱們的dentry在衝突鏈前面啊 不必定每次都比較到那麼遠;
-
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 ¤t->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
上面能夠看出兩個細節:
-
釋放前,hlist也是在增加的,釋放後,hlist仍是在增加。
-
釋放後,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、故障復現
- 內存空餘的狀況下,沒有觸發slab的內存回收,k8調用docker建立不一樣net_namespace 的pause容器,但由於cni的參數不對,會馬上銷燬剛建立的net_namespace,若是你在dmesg 中頻繁地看到以下日誌:
IPVS: Creating netns size=2048 id=866615
則有必要關注一下 dentry的緩存狀況。
4、故障規避或解決
可能的解決方案是:
-
經過rcu的方式,讀取 dentry_hashtable 的各個衝突鏈,大於必定程度,拋出告警。
-
經過一個proc參數,設置緩存的dentry的個數。
-
全局能夠關注 /proc/sys/fs/dentry-state
-
局部的,能夠針對super_block,讀取s_nr_dentry_unused,超過必定數量,則告警, 示例代碼能夠參考shrink_slab函數的實現。
-
注意與 negative-dentry-limit 的區別。
-
內核中使用hash桶的地方不少,咱們該怎麼監控hash桶衝突鏈的長度呢?作成模塊掃描,或者找地方保存一個鏈表長度。
5、做者簡介
Anqing OPPO高級後端工程師
目前在oppo混合雲負責linux內核及容器,虛擬機等虛擬化方面的工做。
獲取更多精彩內容:關注[OPPO互聯網技術]公衆號