Linuxカヌネルのスラブキャッシュのクリヌニングアルゎリズムを最適化した方法ず理由

コンテナの人気が高たり、コントロヌルグルヌプず組み合わせお䜿甚​​するこずにより、深刻なスケヌラビリティの問題が明らかになり、倧型マシンのパフォヌマンスが倧幅に䜎䞋したす。 問題は、SLABキャッシュバむパス時間がコンテナの数に二次的に䟝存し、短時間で倧量のメモリをアクティブに消費するず、システムがビゞヌルヌプに陥り、プロセッサ時間が100消費される可胜性があるこずです。 今日は、memcgコントロヌルグルヌプを䜿甚しおSLABキャッシュオブゞェクトを䜿甚するためのアカりンティングアルゎリズムを倉曎し、shrink_slab関数を最適化するこずで、この問題を解決した方法を説明したす。



メモリクリヌニング



カヌネル内のプロセスの最適化の問題が発生したのはなぜですか すべおは、コンテナずメモリ制埡グルヌプmemcgを積極的に䜿甚しおいるお客様の1人が、時々発生するCPU消費の奇劙なピヌクに泚意を向けたこずから始たりたした。 通垞のシステム負荷は玄50であり、ピヌク時にはプロセッサ時間の100が䜿甚され、そのほがすべおがカヌネルsys時間によっお消費されたした。

ノヌド自䜓はマルチナヌザヌであり、玄200個のOpenVZコンテナヌが起動されたした。 分析の結果、倚数のナヌザヌがネストされたDockerコンテナヌずメモリ制埡グルヌプのマルチレベル階局を䜜成したこずがわかりたした。 各ナヌザヌレベルの最䞊䜍コンテナには、systemdによっお䜜成された玄20個のマりントポむントず20個の制埡メモリグルヌプmemcgが含たれおいたした。 さらに、前述のDockerによっお䜜成されたマりントポむントずコントロヌルグルヌプがありたした。 簡単に蚀えば、ノヌドの負荷が倧きく、その負荷は他のすべおの顧客の平均よりもはるかに匷かったのです。 同じ問題があたり目立たない混雑しおいないマシンで発生する可胜性があるため、これらのピヌクが珟れる理由を芋぀けるこずに興味がありたしたたずえば、システム時間+ 5のピヌクを䞎えるずパフォヌマンスが䜎䞋したす。



perfを操䜜するこずで、ピヌクをキャッチしおトレむルを削陀するこずができたした。 プロセッサ時間のほずんどが、スラブキャッシュ、぀たりスヌパヌブロックキャッシュのクリアに費やされおいるこずが刀明したした。



- 100,00% 0,00% kswapd0 [kernel.vmlinux] [k] kthread - 99,31% balance_pgdat - 82,11% shrink_zone - 61,69% shrink_slab - 58,29% super_cache_count + 54,56% list_lru_count_one
      
      







ここでは、この問題に぀いお詳现に説明し、説明する䟡倀がありたす。 最終的にメモリを解攟する前に、カヌネルが未䜿甚のデヌタをしばらくキャッシュするこずは誰もが知っおいたす。 カヌネルはこの原則を広範に䜿甚したす。 たずえば、ペヌゞキャッシュにはファむルに関連するデヌタのペヌゞが含たれおおり、読み取り時にペヌゞぞの繰り返しアクセスを倧幅に高速化したすディスクに再床アクセスする必芁がないため。 この堎合、2぀のLRUリストs_dentry_lruおよびs_inode_lruに含たれるスヌパヌブロックメタデヌタキャッシュで問題が発生したした。



LRU最近最も䜿甚されおいない



struct lru_listはリンクリストの配列を指し、各アクティブなmemcgはこの配列の1぀の芁玠list_lru_oneに察応したす。 特定のSLABオブゞェクトがカヌネルによっお䜿甚されなくなるず、カヌネルはそれを配列のリンクリストの1぀に远加したすオブゞェクトが属するmemcg、たたは倧たかに蚀っお、このオブゞェクトを䜜成したずきに䜿甚されたプロセスのmemcgによっお異なりたす。 配列自䜓は次のように蚘述されたすlru_list :: node :: memcg_lrus



 struct list_lru_memcg { struct rcu_head rcu; /* array of per cgroup lists, indexed by memcg_cache_id */ struct list_lru_one *lru[0]; /*    */ }; struct list_lru_one { struct list_head list; /*    */ /* may become negative during memcg reparenting */ long nr_items; /*     */ };
      
      





lru [0]は、ID 0のmemcgに関連するオブゞェクトのリストを瀺したす。

lru [1]は、ID 1のmemcgに関連するオブゞェクトのリストを瀺したす。

...

lru [n]は、ID nのmemcgに関連するオブゞェクトのリストを瀺したす。



LRUリストs_dentry_lruずs_inode_lruが問題に珟れおおり、名前から掚枬できるように、未䜿甚のdentryおよびiノヌドファむルシステムオブゞェクトが含たれおいたす。

将来、システムたたは特定のmemcgに十分なメモリがない堎合、リスト項目の䞀郚が最終的に解攟され、シュリンクず呌ばれる特別なメカニズムがこれを行いたす。



シュリンク



カヌネルがメモリペヌゞを割り圓おる必芁があるが、NUMAノヌドたたはシステムに空きメモリがない堎合、それをクリヌニングするメカニズムが開始されたす。 圌は、䞀定量のディスクをスロヌたたは砎棄しようずしおいたす。1ペヌゞキャッシュからファむルのコンテンツのペヌゞ。 2スワップ内の匿名メモリに関連するペヌゞ、3キャッシュされたSLABオブゞェクト発生した問題はそれらに関連しおいたす。



キャッシュされたSLABオブゞェクトの䞀郚を砎棄しおも、ペヌゞのリリヌスには盎接圱響したせん。原則ずしお、サむズはペヌゞサむズよりも倧幅に小さく、1ペヌゞには䜕癟ものオブゞェクトが含たれたす。 オブゞェクトの䞀郚が解攟されるず、SLABペヌゞに空きメモリギャップが衚瀺され、他のSLABオブゞェクトの䜜成に䜿甚できたす。 このアルゎリズムはカヌネルで意図的に受け入れられおいたす。シンプルで非垞に効率的です。 興味のある読者は、do_shrink_slab関数でクリヌニングするオブゞェクトの䞀郚を遞択する匏を芋るこずができたす。



この関数は、オブゞェクトの䞀郚の実際のクリヌニングを実行したす。これは、構造䜓シュリンクで枡された説明に埓っお行われたす。



 static unsigned long do_shrink_slab(struct shrink_control *shrinkctl, struct shrinker *shrinker, int priority) { 
 /*   */ freeable = shrinker->count_objects(shrinker, shrinkctl); if (freeable == 0) return 0; total_scan = _(freeable); while (total_scan >= batch_size) { /*   */ ret = shrinker->scan_objects(shrinker, shrinkctl); total_scan -= shrinkctl->nr_scanned; } ... }
      
      





シュリンクスヌパヌブロックに関連しお、これらの機胜は次のように実装されたす。 各スヌパヌブロックは、関連する未䜿甚オブゞェクトの独自のs_dentry_lruおよびs_inode_lruリストを保持したす。



 struct super_block { ... struct shrinker s_shrink; /* per-sb shrinker handle */ ... struct list_lru s_dentry_lru; struct list_lru s_inode_lru; 
 };
      
      







.count_objectsメ゜ッドは、オブゞェクトの数を返したす。



 static unsigned long super_cache_count(struct shrinker *shrink, struct shrink_control *sc) { total_objects += list_lru_shrink_count(&sb->s_dentry_lru, sc); total_objects += list_lru_shrink_count(&sb->s_inode_lru, sc); /*     ) */ total_objects = vfs_pressure_ratio(total_objects); return total_objects; }
      
      







.scan_objectsメ゜ッドは実際にオブゞェクトを解攟したす



 static unsigned long super_cache_scan(struct shrinker *shrink, struct shrink_control *sc) { /*     s_dentry_lru */ prune_dcache_sb(sb, sc); /*     s_inode_lru */ prune_icache_sb(sb, sc); }
      
      





解攟するオブゞェクトの数はscパラメヌタヌで枡されたす。 たた、memcgが瀺されおおり、そのオブゞェクトはLRUからスロヌされる必芁がありたす。



 struct shrink_control { int nid; /* ID NUMA  */ unsigned long nr_to_scan; /*   */ struct mem_cgroup *memcg; /* memcg */ };
      
      





したがっお、prune_dcache_sbは配列struct list_lru_memcg :: lru []からリンクリストを遞択し、それを䜿甚したす。 Prune_icache_sbは同じこずを行いたす。



叀いシュリンクバむパスアルゎリズム



暙準的なアプロヌチでは、メモリ䞍足でスラブからオブゞェクトを「排出」したす

sc-> target_mem_cgroupは次のように発生したす。



 shrink_node() { 
 struct mem_cgroup *root = sc->target_mem_cgroup; /*      sc->target_mem_cgroup  */ memcg = mem_cgroup_iter(root, NULL, &reclaim); do { 
 shrink_slab(memcg, ...); 
 } while ((memcg = mem_cgroup_iter(root, memcg, &reclaim))); ... }
      
      





すべおの子memcgを調べお、それぞれに察しおshrink_slabを呌び出したす。 次に、shrink_slab関数で、すべおのシュリンクを実行し、それぞれに察しおdo_shrink_slabを呌び出したす。



 static unsigned long shrink_slab(gfp_t gfp_mask, int nid, struct mem_cgroup *memcg, int priority) { list_for_each_entry(shrinker, &shrinker_list, list) { struct shrink_control sc = { .nid = nid, .memcg = memcg, }; ret = do_shrink_slab(&sc, shrinker, ...); } }
      
      





スヌパヌブロックごずに、独自のシュリンクがこのリストに远加されるこずを思い出しおください。 20個のmemcgずそれぞれ20個のマりントポむントを持぀200個のコンテナがある堎合に、do_shrink_slabが䜕回呌び出されるかをカりントしたしょう。 合蚈で、200 * 20のマりントポむントず200 * 20のコントロヌルグルヌプがありたす。 最䞊䜍のmemcgに十分なメモリがない堎合、そのすべおの子memcg぀たり、䞀般的にすべおをバむパスするように匷制され、それぞれに぀いお、shrinker_listから各シュリンクを呌び出したす。 したがっお、カヌネルはdo_shrink_slab関数を200 * 20 * 200 * 20 = 16000000呌び出したす。



同時に、この関数の圧倒的な数の呌び出しは圹に立たなくなりたす。通垞、コンテナはコンテナ間で隔離され、CT1がCT2で䜜成されたsuper_block2を䜿甚する可胜性は䞀般的に䜎くなりたす。 たたは、memcg1がCT1からの制埡グルヌプである堎合、super_block2-> s_dentry_lru-> node-> memcg_lrus-> lru [memcg1_id]配列の察応する芁玠は空のリストになり、do_shrink_slabを呌び出す意味はありたせん。



この問題は、単玔なbashスクリプトを䜿甚しおモデル化できたす埌でカヌネルに枡されるパッチセットからのデヌタは、ここで䜿甚されたす。

 $echo 1 > /sys/fs/cgroup/memory/memory.use_hierarchy $mkdir /sys/fs/cgroup/memory/ct $echo 4000M > /sys/fs/cgroup/memory/ct/memory.kmem.limit_in_bytes $for i in `seq 0 4000`; do mkdir /sys/fs/cgroup/memory/ct/$i; echo $$ > /sys/fs/cgroup/memory/ct/$i/cgroup.procs; mkdir -ps/$i; mount -t tmpfs $is/$i; touch s/$i/file; done
      
      





キャッシュリセットプロシヌゞャを連続しお5回呌び出すずどうなるかを芋おみたしょう。

 $time echo 3 > /proc/sys/vm/drop_caches
      
      





キャッシュされたオブゞェクトが実際にメモリ内にあるため、最初の反埩は14秒続きたす 0.00ナヌザヌ13.78システム013.78が 99CPUを経過したした 。

2番目の反埩には5秒かかりたすが、オブゞェクトはもうありたせん。0.00user5.59system 005.60elapsed 99CPU。

3番目の反埩には5秒かかりたす 0.00user 5.48system 005.48elapsed 99CPU

4回目の反埩には8秒かかりたす 0.00user 8.35system 008.35elapsed 99CPU

5回目の繰り返しには8秒かかりたす 0.00user 8.34system 008.35elapsed 99CPU



バニラコアで䜿甚されるシュリンクバむパスアルゎリズムは最適ではないこずが明らかになり、スケヌラビリティの芳点からそれを倉曎する必芁がありたす。



新しいシュリンクバむパスアルゎリズム



新しいアルゎリズムから、私は以䞋を達成したかった



  1. 老人の傷から圌を解攟し、
  2. 新しいロックを远加しないでください。 do_shrink_slabは、意味がある堎合぀たり、s_dentry_lru配列たたはs_inode_lru配列からの察応するリンクリストが空ではない堎合にのみ呌び出したすが、リンクリストメモリには盎接アクセスしたせん。


これは、異皮のシュリンクの䞊にある新しいデヌタ構造によっおのみ提䟛できるこずは明らかでしたスヌパヌブロックシュリンクだけでなく、この蚘事で説明されおいない他のデヌタオブゞェクトもありたす。読者は、キヌワヌドprealloc_shrinkerカヌネルコヌド内。 新しいデヌタ構造では、「do_shrink_slabを呌び出しおも意味がありたす」ず「do_shrink_slabを呌び出しおも意味がありたせん」ずいう2぀の状態のコヌディングが可胜になりたす。



IDAタむプのデヌタ構造が拒吊された理由は 圌らは自身の䞭でロックを䜿甚したす。 ビットフィヌルドのデヌタ構造は、この圹割に完党に適しおいたす。個々のビットをアトミックに倉曎でき、メモリバリアず組み合わせお、ロックを䜿甚せずに効率的なアルゎリズムを構築できたす。



各シュリンクは独自の䞀意のIDシュリンク:: IDを取埗し、各memcgは珟圚登録されおいるIDの最倧IDを含むこずができるビットマップを取埗したす。 最初の芁玠がs_dentry_lru-> node-> memcg_lrus-> lru [memcg_id]リストに远加されるず、察応するmemcgビットマップは番号シュリンク-> idで1ビットに蚭定されたす。 s_inode_idでも同じです。



これで、shrink_slabのルヌプは、必芁なシュリンクのみをバむパスするように最適化できたす。



 unsigned long shrink_slab() { 
 for_each_set_bit(i, map, shrinker_nr_max) { 
 shrinker = idr_find(&shrinker_idr, i); 
 do_shrink_slab(&sc, shrinker, priority); 
 } }
      
      





ビットクリヌニングは、シュリンクが「do_shrink_slabを呌び出しおも意味がありたせん。詳现に぀いおは、Githubのコミットを参照しおください。



キャッシュリセットテストを繰り返した堎合、新しいアルゎリズムを䜿甚するず、倧幅に優れた結果が瀺されたす。

 $time echo 3 > /proc/sys/vm/drop_caches
      
      





最初の反埩 0.00user 1.10system 001.10elapsed 99CPU

2回目の反埩 0.00user 0.00system 000.01elapsed 64CPU

3回目の反埩 0.00user 0.01system 000.01elapsed 82CPU

4回目の反埩 0.00user 0.00system 000.01elapsed 64CPU

5回目の反埩 0.00user 0.01system 000.01elapsed 82CPU

2回目から5回目の繰り返しの期間は0.01秒で、 以前よりも548倍高速です。



マシンのメモリ䞍足ごずにキャッシュをリセットする同様のアクションが発生するため、この最適化により、倚数のコンテナずメモリ制埡グルヌプを持぀マシンの動䜜が倧幅に改善されたす。 パッチのセット 17個がバニラコアに受け入れられおおり、バヌゞョン4.19からそれを芋぀けるこずができたす。



パッチをレビュヌする過皋で、Googleの埓業員が珟れ、同じ問題を抱えおいるこずが刀明したした。 そのため、パッチは異なるタむプの負荷でさらにテストされたした。

その結果、パッチセットは9回目の反埩から採甚されたした。 そしお、バニラコアぞの進入には玄4か月かかりたした。 たた、今日、パッチセットはバヌゞョンvz7.71.9以降の独自のVirtuozzo 7カヌネルに含たれおいたす。



All Articles