驯服Linux OOM Killer(优质英文资料翻译)
在极低的内存条件下, OOM(out-of-memory) Killer会启动并使用一组随时间演变的启发式方法来选择要杀死的进程。对于可能希望终止不同进程的用户来说,这可能非常烦人。从系统的角度来看,被杀死的进程也可能很重要。为了避免错误进程的过早消亡,许多开发人员认为需要对 OOM Killer的活动进行更大程度的控制。
为什么是 OOM Killer?
主要发行版内核设置/proc/sys/vm/overcommit_memory的默认值 为零,这意味着进程可以请求比系统中当前可用的内存更多的内存。这是基于试探法完成的,即分配的内存不会立即使用,并且进程在其生命周期内也不会使用它们分配的所有内存。如果没有过量使用,系统将无法充分利用其内存,从而浪费一些内存。过量使用内存允许系统以更有效的方式使用内存,但存在 OOM 情况的风险。内存占用程序会耗尽系统的内存,使整个系统陷入停顿。这会导致这样一种情况,当内存太低时,甚至单个页面都无法分配给用户进程,从而允许管理员终止适当的任务,或让内核执行诸如释放内存之类的重要操作。在这样的情况下,
用户和系统管理员经常询问如何控制 OOM Killer的行为。为了方便控制,引入了 /proc/ <pid> /oom_adj旋钮,以防止系统中的重要进程被杀死,并定义要杀死的进程的顺序。oom_adj的可能值范围从 -17 到 +15。分数越高,相关进程就越有可能被OOM-killer杀死。如果 oom_adj设置为 -17,则不考虑该进程进行 OOM-killing。
谁是坏人?
在内存不足的情况下要杀死的进程是根据其不良评分来选择的。不良分数反映在 /proc/ <pid> /oom_score 中。这个值是根据系统丢失最少完成的工作量,恢复大量内存,不杀死任何消耗大量内存的无辜进程,并杀死最少数量的进程(如果可能限制为一个)的基础上确定的)。badness 分数是使用进程的原始内存大小、它的 CPU 时间 (utime + stime)、运行时间 (uptime - start time) 和它的oom_adj值来计算的。进程使用的内存越多,分数就越高。进程在系统中存活的时间越长,分数越小。
任何不幸进入swapoff()系统调用(从系统中删除交换文件)的进程都将被选择首先杀死。对于其余的,初始内存大小成为进程的原始不良分数。如果他们不共享相同的记忆,则每个孩子的记忆大小的一半被添加到父母的分数中。因此,分叉服务器是被杀死的主要候选人。只有一个“饥饿”的孩子会让父母不如孩子好。最后,应用以下启发式方法来保存重要进程:
- 如果任务的好值高于零,则其分数翻倍
- 超级用户或直接硬件访问任务(CAP_SYS_ADMIN、CAP_SYS_RESOURCE 或 CAP_SYS_RAWIO)的分数除以 4。这是累积的,即具有硬件访问权限的超级用户任务的分数除以 16。
- 如果 OOM 条件发生在一个 cpuset 并且被检查的任务不属于该集合,则其分数除以 8。
- 得到的分数乘以 2 的 oom_adj 次方(即 点 <<= oom_adj为正, 点 >>= -(oom_adj) 否则)。
然后选择坏度最高的任务并杀死它的子任务。当它没有子进程时,进程本身会在 OOM 情况下被杀死。
将 OOM-killing 策略转移到用户空间
/proc/ <pid> /oom_score是一个随时间变化的动态值,对于管理员要求的不同和动态策略不灵活。很难确定在 OOM 情况下哪个进程将被杀死。管理员必须为每个创建的进程和退出的每个进程调整分数。在具有快速生成进程的系统中,这可能是一项艰巨的任务。为了让 OOM Killer政策的实施更容易, Evgeniy Polyakov 提出了一种基于名称的解决方案。使用他的补丁,最先死亡的进程是运行名称在 /proc/sys/vm/oom_victim 中的程序的进程。基于名称的解决方案有其局限性:
- 任务名称不是真实名称的可靠指标,在进程名称字段中被截断。此外,执行二进制文件的符号链接,但具有不同的名称将不适用于这种方法
- 这种方法一次只能指定一个名称,排除了层次结构的可能性
- 可能有多个同名但来自不同二进制文件的进程。
- 如果/proc/sys/vm/oom_victim定义的名称没有进程,则行为归结为默认的当前实现 。这增加了查找受害进程所需的扫描次数。
艾伦·考克斯不喜欢这个解决方案,这表明该容器是控制问题的最适当方式。为了响应这个建议,由 Nikanth Karthikesan 贡献的oom_killer 控制器提供了在系统内存不足时要杀死的进程序列的控制。该补丁引入了一个带有oom.priority字段的 OOM 控制组 (cgroup) 。要杀死的进程是从具有最高oom.priority值的进程中选择的。
要控制 OOM Killer,请挂载补丁引入的 cgroup OOM 伪文件系统:
mount -t cgroup -o oom oom /mnt/oom-killer
OOM-killer 目录包含文件tasks中所有进程的列表 ,以及它们在oom.priority 中的 OOM 优先级。默认情况下, oom.priority设置为 1。
如果要创建一个特殊的控制组,其中包含应该首先受到 OOM 杀手注意的进程列表,请在/mnt/oom-killer下创建一个目录来表示它:
mkdir lambs
将oom.priority设置为足够高的值:
echo 256 > /mnt/oom-killer/lambs/oom.priority
oom.priority是一个 64 位无符号整数,并且可以具有一个无符号 64 位数字可以容纳的最大值。在扫描要杀死的进程时,OOM-killer 从具有最高 oom.priority 值的任务列表中选择一个进程。
添加要添加到任务列表的进程的PID:
echo > /mnt/oom-killer/lambs/tasks
要创建一个不会被 OOM-killer 杀死的进程列表,请创建一个目录来包含这些进程:
mkdir invincibles
将oom.priority设置为零会使此 cgroup 中的所有进程从要杀死的目标进程列表中排除。
echo 0 > /mnt/oom-killer/invincibles/oom.priority
要向该组添加更多进程,请将任务的pid添加到无敌组的任务列表中:
echo > /mnt/oom-killer/invincibles/tasks
重要的进程,例如数据库进程及其控制器,可以加入到这个组中,所以当OOM-killer搜索要杀死的进程时,它们会被忽略。任务中列出的进程的所有子进程都会自动添加到同一个控制组并继承父进程的 oom.priority。当多个任务的oom.priority最高时,OOM Killer根据oom_score和oom_adj选择进程。
不过,这种方法对 cpuset 用户没有吸引力。考虑两个cpuset,A和B。如果cpuset A中的进程有一个高oom.priority 值,如果cpuset B内存不足,即使cpuset A中有足够的内存,它也会被杀死。 这需要不同的设计驯服 OOM Killer。
讨论的一个有趣结果是处理用户空间中的 OOM 情况。内核向用户空间发送通知,应用程序通过丢弃它们的用户空间缓存来响应。如果用户空间进程不能释放足够的内存,或者进程忽略内核释放内存的请求,内核就会使用杀死进程的老方法。 mem_notify,通过Kosaki基宏开发的,是过去做一个这样的尝试。但是 mem_notify补丁 不能应用于2.6.28以上的版本,因为内存管理回收顺序发生了变化,但设计原则和目标可以重用。David Rientjes建议采用以下两种混合解决方案之一:
另一个是 /dev/mem_notify,它允许您对设备文件进行 poll() 并通知低内存事件。这可以包括当一组任务完全耗尽内存时的 cgroup oom 通知程序行为,但也可以在这种情况可能迫在眉睫时发出警告。我建议将其作为 cgroups 的客户端来实现,以便不同的处理程序可以负责不同的任务聚合。
大多数开发人员更喜欢让/dev/mem_notify成为控制组的客户端。这可以进一步扩展以与建议的 oom-controller 合并。
嵌入式系统中的低内存
Android 开发人员需要对低内存情况进行更大程度的控制,因为 OOM Killer直到低内存情况的后期才会启动,即直到所有缓存都被清空。Android 想要一种解决方案,可以在空闲内存耗尽时尽早启动。所以他们引入了“lowmemory”驱动,它有多个低内存阈值。在内存不足的情况下,当满足第一个阈值时,后台进程会收到问题通知。它们不会退出,而是保存它们的状态。这会影响切换应用程序时的延迟,因为应用程序必须在激活时重新加载。在进一步的压力下,低内存Killer会杀死状态已保存在前一个阈值中的非关键后台进程,最后杀死前台应用程序。
保持多个低内存触发器为进程提供了足够的时间从它们的缓存中释放内存,因为在 OOM 情况下,用户空间进程可能根本无法运行。它所需要的只是内核内部结构的一次分配,或者一个页面错误,使系统耗尽内存。在响应低内存通知的用户空间应用程序的帮助下,较早的低内存情况通知可以避免 OOM 情况。
基于内核启发式杀死进程不是最佳解决方案,这些在选择进程作为牺牲羔羊时为用户提供更好控制的新举措是实现稳健设计的步骤,以便为用户提供更多控制。然而,就最终控制解决方案达成共识可能需要一些时间。