最近,在调查OOM(内记忆中)问题的同时,Twitter工程师发现板坯缓存始终增加,但页面缓存始终持续减少。仔细观察表明,板式缓存的最高消耗是凹陷高速缓存,并且将凹凸缓存充电到一个存储器控制组(CGROUP)。似乎Linux内核的内存再生家只回收了页面缓存,但根本没有回收凹陷缓存。
通过调试问题,我们发现收缩器代码中存在罕见的竞争条件。
要完全理解可防止板块缓存的丢弃,我们需要仔细查看Linux内核内部,特别是以下内存管理子系统:
机器的内存资源不是无限的。如果我们同时运行多个程序或运行非常大的程序,则可能比内存资源提供更多的需求。一旦可用内存低,内存再生机(AKA KSWAPD或直接回收)试图撤销“旧”内存以使足够的空间释放足够的空间来满足分配器。回收器扫描列表(其中大多数最近使用的列表,AKA LRU)以驱动最近使用的内存。列表包括匿名存储器列表,文件存储器列表和板坯缓存(图1)。
除非不可检测的列表,回收器扫描上述所有列表。但是,有时候收集者只会扫描其中一些。例如,如果交换分区不可用,则回收器不会扫描匿名存储器列表。
当回收家正试图回收平板缓存时,需要调用缩减器。板坯缓存是具有不同子系统管理的不同大小的内存对象。例如,这包括inode缓存和凹陷缓存。一些板坯缓存是可回收的。由于这些缓存保持了特定子系统的数据并具有特定状态,因此可以通过专用的缩减器来回收,了解如何管理缓存。
文件系统元数据缓存为每个元数据段具有专用收缩器。可重选文件系统元数据缓存处于内存中的CGROUP感知和非统一内存访问(NUMA)感知LRU列表。收缩器扫描这些列表以在击中内存压力时回收一些缓存。
对于现代数据中心,CGroup通常用于管理资源并实现更好的资源隔离。内存CGROUP用于管理内存资源。
在存在内存CGroup之前,上述LRU列表附加到NUMA节点。
当存在内存CGroup时,LRU列表处于每个存储器 - CGROUP和每个节点级别。每个内存cgroup都有自己的专用LRU列表,用于存储器回收器图3。
在回收内存时,内存再生器遍历所有内存CGroups以扫描LRU列表。
对于典型的生产部署,可能存在大约40个收缩器,包括每个超级块的文件系统元数据收缩器。
当存在许多内存CGroups和许多收缩器时,如果调用所有收缩器,可能会出现严重的可扩展性问题,因为所有的收缩器都被调用所有存储器CGroups。即使某些收缩者没有一个可替代的对象,也会发生这些问题。
上游内核引入了一个叫做Shrinker_maps的优化。它是一个位图,指示哪些收缩器具有可回收物体,以便调用它们。当新对象添加到LRU时,将设置位图,并在LRU为空时清除。优化使上述可扩展性问题消失了。当一个存储器CGROUP被剥离时(例如,当目录被删除时)其LRU被拼接到其父存储器CGROUP并且相应地设置父存储器CGROUP的位图。
以下命令遍历所有LRU列表,从所有内存cgroups缩小所有可替换对象。
通过检查内存相关的统计信息(例如,使用Slabtop导出的内核和检查板坯使用的/ proc / meminfo和/ proc / slabinfo),似乎凹陷缓存在一个存储器cgroup下围绕2gb内存消耗。这意味着应该有大约1000万个凹陷对象,因为通常凹陷对象大小是192个字节。丢弃缓存应该能够缩小大部分时间。
但是,可能存在丢弃缓存不会丢弃平板缓存的几个原因。例如,LRU列表可能是短暂的,或者对象可能是在扫描时引用或锁定,因此暂时无法激起。下一步是对收缩者进行一些洞察力。
Linux内核提供了大量的方法来观察内核运行方式。内置跟踪点是最常用的trakepoints之一。关于收缩员有两个Tracepoints:
有几种不同的工具可以启用跟踪点并获取跟踪日志。例如,直接在DebugFS,PERF或Trace-CMD下操作文件。我们个人更喜欢Trace-CMD,这是一个支持大量命令的内核跟踪命令的包。该包装也支持几乎所有的发行版。有关Trace-CMD的使用,请参阅该操作。
KSWAPD1-474 [066] .... 508610.111172:mm_shrink_slab_start:super_cac He_scan + 0x0 / 0x1a0 000000000043230d:n:n:n:n:1缩小0 gfp_flags gfp_kernel缓存项38084993 delta 18596 total_scan 18596优先级 KSWAPD1-474 [066] .... 508610.139951:mm_shrink_slab_end:super_cache _can + 0x0 / 0x1a0 000000000043230d:n:1:1未使用扫描计数0新扫描计数164 TOLL_SCAN 164最后收缩器返回VAL 18432
通过分析跟踪日志,似乎在一些消耗最凹陷的缓存中的凹凸缓存根本没有扫描。这意味着收缩率甚至没有被称为。它肯定看起来很奇怪!
如上所述,如果已清除Shrinker_Map位图,则可以防止收缩器被调用的唯一方法是。但LRU列表必须是空的。即使LRU列表上仍有对象,是否有可能清除收缩器位图可能?我们需要检查与收缩相关的内核数据结构,以了解正在发生的内容。
我们可以通过几个工具检查内核数据结构,例如众所周知的崩溃实用程序。在这里,我们使用了Drgn。 DRGN支持使用Python和Live调试的编程。
使用下面的Python脚本,我们可以轻松检查LRU列表和Shrinker_Map位图的长度,用于特定内存cgroup:
对于CSS_FOR_EACH_CHILD(PROG [' root_mem_cgroup']。CSS)的用户: if(user.cgroup.kn.name.string _()。解码()==" user.slice"): 休息 memcg = container_of(用户,' struct mem_cgroup&#39 ;,' css') nr_items = 0. 对于list_for_each_entry的sb(' struct super_block' prog [' super_blocks']。地址_of_(),' s_list'): nr_items + = sb.s_dentry_lru.node.memcg_lrus.lru [9] .nr_items Bitmap = memcg.nodeinfo [0] .shrinker_map.map [0]
NR_ITEMS显示了DENTRY LRU列表的长度,位图显示了SHRINKER_MAP位图。
结果表明,LRU列表中有大约1000万个凹陷缓存对象,大多数是可替代的。所以,LRU列表绝对不是空的。但是shrinker_map位图显示相应的位清除。
即使有丰富的可替代的高速缓存,这真的很奇怪。它闻起来像仁虫。
通过读取内核代码,似乎内核似乎且仅当相应的LRU列表为空时才清除位图。内核设置了以下位置:
由于我们的用例非常频繁地创建和删除内存CGroups,因此通常经常发生重新处理。它似乎可能有一些重新定位问题。
一旦比赛条件所在,修复似乎很容易。它实际上只有几条线条:
diff --git a / mm / list_lru.c b / mm / list_lrru.c 索引8DE5E37..1E61161 100644 --- a / mm / list_lru.c +++ b / mm / list_lru.c @@ -534,7 + 534,6 @@静态void memcg_drain_list_lru_node(struct list_lru * lru,int nid, struct list_lru_node * nlru =& lru->节点[nid]; int dst_idx = dst_memcg-> kmemcg_id; struct list_lru_one * src,* dst; - BOOL设置; / * *由于List_lru_ {add,del}可以在IRQ安全锁下调用, @@ -546,11 + 545,12 @@静态void memcg_drain_list_lru_node(struct list_lru * lru,int nid, dst = list_lru_from_memcg_idx(nlru,dst_idx); list_splice_init(& src-> list,& dst-> list); - set =(!dst-> nr_items&& src-> nr_items); - dst-> nr_items + = src-> nr_items; - 如果(设置) + + if(src-> nr_items){ + dst-> nr_items + = src-> nr_items; memcg_set_shrinker_bit(dst_memcg,nid,lru_shrinker_id(lru)); - src-> nr_items = 0; + src-> nr_items = 0; +} spin_unlock_irq(& nlru->锁定); }
我们将修补程序源到上游内核,它已被合并到主线内核树中。请参阅git.kernel.org提交。
要调查该问题,我们使用了深入的分析方法和使用方法(利用率,饱和度和错误)。我们的过程如下:
监控:这用于随着时间的推移不断记录统计数据。可以拉动历史数据,以便可以识别基于时间的使用趋势。有时数据源用于监视与统计工具的重叠。
统计工具:内核有很多内置统计数据,可以通过读取/ proc,/ sys或第三方工具(如iostat)进行检查。它们通常用于检查资源利用率。通常,检查这些统计数据是分析性能问题或内核错误的第一步。
trapepoints:内核也配有大量内置的trapepoints。它们可以使用各种手段打开或熄灭。跟踪是非常强大的,可以显示有关内部内部活动的大量细节。它适用于更深层次的检查。但是Trapepoints绝对不在任何地方使用。可以使用动态跟踪,其中Trapepoints不可用。
内核数据结构检查员:有时甚至跟踪点和动态跟踪都没有透露足够的信息来弄清楚根本原因。在这种情况下,内核开发人员必须检查内核数据结构。通常,这种检查员提供内置命令和高级语言编程接口,以使检查更容易。但是,开发人员必须深入了解内核internals来使用它们。
检查代码:要确定错误的位置并提出适当的修复,是必要的代码检查。
内存填充是Linux内核最复杂的部分之一。它充满了启发式算法,复杂的角落案例,复杂的数据结构和与其他子系统的复杂交互。内存填写是Linux内核的核心部分,并由其他子系统依赖于此。但是,内存填海的错误或次优行为可能需要很长时间才能发现。修复程序可能非常微妙,验证可能需要实质性的努力来保证没有回归。