Kubernetes Out-of-memory (OOM)简介

OOM的困扰

在生产环境中,很多时候会出现运行在Kubernetes容器中的应用进程莫名终止,常见的有应用状态不正常引发的Kubernetes杀掉pod,此外非常多的情况是出现了所谓的 OOM Killed

备注

Kubernetes为现代部署带来了适应各种环境的标准化,但是也带来了复杂的架构,这导致 DevOps Atlas 的开发者和运维者之间持续的拉锯:要定位是应用原因还是Kubernetes原因导致的异常需要高超的技术能力。

内存过量使用(overcommit)

内存overcommit是非常常见常见的操作系统技术,这意味着操作系统为进程分配了超过实际能够分配的内存量。这是因为几乎没有程序会同时使用所有分配给它的内存,类似于银行,除非出现 挤兑 否则内存overcommit完全不会影响应用程序运行,而且使得内存能够更为有效利用。

Linux内核通过 过度使用记账(Overcommit Accounting) 实现内存过量使用,默认是 启发式 overcommit ( heuristic overcommit )

备注

Kubernetes默认的overcommit策略是 接受任何分配请求 而不检查提交限制

oom-killer

Out Of Memory (OOM) 管理器 负责确保内存,在检查可用内存、判断OOM状态(调用 out_of_memory() )和选择哪个进程( select_bad_process() )有复杂的逻辑。

注意,如果出现 oom-killer 现象,应该想办法解决OOM问题的根因,而不是禁止``oom-killer``

一种排查思路: 如果系统内存充足,不足以触发 oom-killer ,可以采用先配置 vm.overcommit_memory=0 ,在运行一段时间后,改为 vm.overcommit_memory=2 ,并且 vm.overcommit_ratio 配置为比当前内存分配更小一些。此时就会出现系统分配状态 /proc/meminfoCommitted_AS 大于 CommitLimit 。此时运行任何程序都会出现报错 -bash: fork: Cannot allocate memory

cgroups

Kernel Cgroup 是容器技术( 容器术语解析 )的核心,也是Kubernetes计算节点内存依据(根据内存 根cgroup 统计信息计算)

cgrouops和kubernetes

cgroups v2

Kubernetes已经开始支持 Control Group v2 ,但是需要明确配置,例如我在 z-k8s高可用Kubernetes集群准备 启用了 Control Group v2

cgroups 和 OOM killer

cgroup的内存使用量超过配置限制是,就会可能触发 OOM killer :

  • 首先尝试从中回收内存 cgroup 以便为 cgroup 触及的新页面(达到cgroup限制)腾出空间

  • 如果回收不成功,则会调用 OOM 例程来选择并终止 cgroup 中最庞大的任务(即各个cgroup中使用内存最多的进程)

Kubernetes和OOM killer

OOM killer是内核组件,所以并不是Kubernetes决定调用OOM killer,而是OOM killer在cgroup中所有进程累计内存使用超出了cgroup限制时自动触发。这里带来一个问题,就是OOM killer并不理解Kubernetes pod概念,可能会杀掉容器中非常重要的进程但是由于没有触及 pid 1 进程,可能会导致Kubernetes依然判断pod无需终止。

备注

避免关键进程不被误杀的方法是使用 oom_score_adj 保护进程不被OOM killer

OOM killer支持 soft limits ,不过Kubernetes尚未使用它

OOM相关的运行时( runtime )影响

对于使用 .NET 或 Java程序,是使用运行时的编程语言,除了操作系统的内存分配限制以及cgroup内存分配限制,程序运行时也会对应用内存进行限制。运行时相当于应用程序和操作系统之间的中间人,运行时runtime负责跟踪使用了哪些对象,以及对象周围的内存在不使用时可以被GC(回收)。

运行时runtime有设置选项来限制应用程序可以分配的最大内存,所以在排查应用程序无法分配内存时,我们还需要检查runtime限制。此外需要关注runtime的默认限制,也就是即使你不配置runtime限制参数,但是实际上应用程序运行还是会受到runtime限制。

.NET运行时内存限制:

  • .NET GC使用容器内存(已分配但不一定使用的内存)限制的 75% 作为默认分配限制

  • .NET 运行时 System.GC.HeapHardLimitPercent (75%) 机制只在容器设置限制时生效

Go语言没有实际运行时,而是将GC功能放到运行时库(runtime library)中。Go 当前缺少(截至 2021 年 12 月)用于指定堆最大限制的设置。

Kubernetes资源请求和限制

在Kubernetes中 Pod和Container的资源管理 对资源请求和限制提供支持:

  • 当为一个Pod中的容器指定资源请求时, kube-scheduler 使用这个信息来决定将pod调度到哪个节点

  • 当为一个容器指定资源限制时, kubelet 会强制执行这些限制,以便确保运行的容器使用资源不超过设置的限制

pod的QoS:

  • 决定了当节点上内存变得稀缺时pod被驱逐的顺序

  • 决定了Linux上根内存cgroup /sys/fs/cgroup/memory/ 层级结构中的位置: node将放置pod以及container的cgroup( 当前版本默认配置了 --cgroups-per-qos=true 标志给 kubelet )

Pod的QoS不是手工配置的,而是Kubernetes根据分配给pod中的容器的请求(request)和限制(limit)来决定的。本质上,Kubernetes通过算法查看pod中容器上的设置和限制,并最终为pod分配相应的QoS:

  • Guaranteed 保证Pod一定有指定资源提供(CPU和内存)

  • Burstable 则指Pod中至少有一个容器有CPU和内存的request,但是不能确保(guaranteed)

  • BestEffort 指没有一个容器指定至少一个CPU或内存限制以及请求

参考