为什么不建议开发把OOM当成一种应用程序的重启特性使用

现在越来越多应用云原生化跑在k8s上面,k8s为应用提供了自动限制、自动重启、服务发现等各种能力。这些能力让开发减少了对运维相关属性的关注,但也让一些开发把一些错误当成了特性来使用,比如针对一些无状态的服务,利用 OOM 和自动重启来恢复。这看起来大多数时候似乎没有问题,借助自动恢复,OOM的应用会被重新来起来工作。但这种坏习惯会让系统在某些时候变得更不稳定,比如 OOM Killer 导致的死锁问题。

一次OOM导致的k8s节点卡死

最初的现象:节点进入假死状态,登录节点上后ps等系统命令无法使用,节点监控看CPU、内存、负载情况:



可以看到节点的CPU,内存没什么变化,但负载和IO都变大了的,可以推断大概率是内核和 IO 引起的问题。查看system journal log 看到当时容器发生了一次OOM记录:

查看 dockerd、khugepaged、jdb2 进程处于D状态(TASK_UNINTERRUPTIBLE,不可中断的睡眠状态)。说明都在等待 IO,持续这么长时间看状况应该是死锁了,结合容器 OOM 和ext4/jbd2 死锁找到一条相关的 bug 记录:https://bugs.centos.org/view.php?id=17982

最后检验是和这个 bug 完全吻合的。
简单说下原理:

  • 系统内有两个用户进程,位于同一个 cgroup 中,cgroup 上限制最大可用的内存;
  • 进程 1,分配大量内存,使得 cgroup 内存使用量超过限制,OOM Killer 选择该进程杀死;该进程正在执行 ext4_sync_file(),在 jbd2_log_wait_commit() 等待进程 2 的 Transaction 完成;此时该进程处于不可中断状态,无法被 Kill,内存无法被释放;
  • 进程 2,处于 ext4_create()(或者其他 ext4_xxx 函数)中,在 __getblk() 上等待可用的内存;只有拿到足够内存,才能完成 ext4_journal_stop(),将 t_updates 递减,其对应的内核 Journal 线程才能完成 jbd2_journal_commit_transaction();
  • (死锁条件 1)进程 1 在等待进程 2 的 Transaction Commit 完成才能被 Kill 而释放内存;
  • (死锁条件 2)进程 2 在等待进程 1 的释放内存才能拿到内存完成 Transaction Commit

这个问题其实只在 CentOS 7 的内核版本中出现,算 Centos 的一个内核bug,大家可能觉得系统人员去维护解决bug就行了,但是其实 OOM 引起的系统故障的bug 在 linux 各版本上都十分的多,比如下面这个案例提到的/proc/sys/kernel/printk和OOM Killer 引起的一个死锁问题

结语

虽然容器的自动重启和恢复可以帮助我们很大程度的解决异常,cgroups帮我们做了命名空间隔离,我们可以看到 OOM Killer 作为内核的一个功能,如果把他本身作为一种特性去对待是十分危险的。当容器进程遇到 OOM 内存泄漏的时候,我们应该去尝试解决他,而不是无视他。

参考文献

shikanon wechat
欢迎您扫一扫,订阅我滴↑↑↑的微信公众号!