本文讲述作者如何解决客户集群中出现的OOM(Out of Memory)和Pod驱逐问题。文章不仅详细记录了问题的发生背景、现象特征,还深入探讨了排查过程中的关键步骤和技术细节。
人在工位坐,锅又双叒叕从天上来:
某日下午,
正当我在工位勤恳工作时,我沉寂已久的电话铃声突然响起,刚接起来就听见对面哭喊着:“牧原老哥,救救我啊!”。
原来某TAM同学拜访客户,恰逢客户集群多个节点和业务出现OOM以及驱逐pod的情况,需要我们快速救援,那咱必须救兄弟姐妹于金木水火土之中啊
问题特征有几个....:
-
多集群,多业务,多节点出现驱逐
-
不同的业务pod的内存都相当大,独占节点(节点16c32G ,pod limit 15c 31G-主业务container)
-
既有podOOM又有节点内存不足的驱逐 "reason":"Evicted","message":"The node was low on resource: memory
-
调小kubelet保留内存(调整前可用26G,调整后29G),依然约26G触发OOM
闻弦歌而知雅意,这个问题不高级~:
这现象看起来就是业务正常的增长导致内存用完了,还要啥自行车啊?
用户表示,我业务实际没有使用那么多内存,
并提供了内存使用的监控截图。
对于这类问题,如果我们想要细致的分析,是需要采集一些关键信息来佐证和分析。
a) 找到驱逐状态的pod查看下驱逐的原因,可以看到这个pod的驱逐原因是节点内存不足,同时输出了这个container使用了约27G的内存,初始的request内存是24G。
b) 我们看下pod的yaml对资源这块的设置,主业务确实是31G,以及当前是否有大内存使用的pod,发现多个25G左右的内存使用率pod。
c) 查看节点如top node,节点的可分配资源,以及运行的pod,超卖信息等,这里面满满都是信息啊。
d) 客户反馈调小过系统的资源保留,因此我们也要看下kubelet的参数是否生效,可以看到,reserved的内存已经降低到了100m,说明调整已经生效了。
既然基础信息采集完毕,开始整活:
触发了OOM,那么系统会输出oom的相关信息,且会输出具体的进程占用的内存开销,我们来一起看看系统日志吧。
OOM日志分析必备小知识之看图说话:
OOM日志之开始:
OOM日志之关键信息:
-
触发OOM的task是在这个cgroup路径里面。
-
当前的内存使用以及limit上限,failecnt代表申请失败的次数(申请失败不会立刻oom,会先尝试回收内存 )。
-
同时输出了很多cgroup的统计信息,每个cgroup使用的内存统计,可以先忽略。
这里有个知识点,rss是page数,因此计算进程的内存开销应该用rss*4kb去计算。
通过计算rss的page值得出,内存还远远没达到系统的最大内存:
echo "scale=2; 6901788 * 4 / 1024/1024" |bc
26.32G
少年,有没有感觉哪里不对劲?
这个cgroup它不正经啊,兄弟:
是的,你的感觉没有错,cgroup的limit是 27968328 kB。
节点的Allocatable: 31273800 Ki,
按理说cgroup的limit应该是Allocatable差不多的值,这里偏差有点大,拿出我当年尝试论证「哥德巴赫猜想」的数学能力,经过一番缜密的计算得出,cgroup的limit小于节点的Allocatable,这个cgroup它不正经啊,兄弟
。
cgroup limit:
% echo "scale=2; 27968328 / 1024/1024" |bc
26.67
node Allocatable:
% echo "scale=2; 31273800 / 1024/1024" |bc
29.82
cgroup设置的总得limit在这个文件中设置,它是kubelet初始化的时候设置的。
我们看下ack的初始化日志,读取自定义配置文件,计算要保留的内存。
可以看下大概的时间点,在17:34:26分的时候完成的计算。
客户通过自定义kubelet参数修改了保留内存,那我们得看下文件/etc/kubernetes/kubelet-customized-args.conf 的变动时间,以及kubelet加载的时间,文件是17:35:18左右修改的,难道是文件修改的延迟太大,导致没有覆盖?
不然,kubelet监测到配置文件变更会有一次reload的行为,因此我们可以继续看下日志,是否有重新加载。
可以看到kubelet的参数被配置成了100m。
配置难道不成功?
kubelet刷新cgroup的配置也是有日志记录的,我们可以通过关键词“Updated Node Allocatable limit across pods”看下同步的时间。
17:35:18是有刷新的日志的,且该日志表明强制配置cgroup成功后返回,兜兜转转又双叒叕来到了我最不喜欢的翻代码环节。
此处省略一万字
if len(cm.cgroupRoot) > 0 {
go func() {
for {
err := cm.cgroupManager.Update(cgroupConfig)
if err == nil {
cm.recorder.Event(nodeRef, v1.EventTypeNormal, events.SuccessfulNodeAllocatableEnforcement, "Updated Node Allocatable limit across pods")
return
}
message := fmt.Sprintf("Failed to update Node Allocatable Limits %q: %v", cm.cgroupRoot, err)
cm.recorder.Event(nodeRef, v1.EventTypeWarning, events.FailedNodeAllocatableEnforcement, message)
time.Sleep(time.Minute)
}
}()
}
if nc.EnforceNodeAllocatable.Has(kubetypes.SystemReservedEnforcementKey) {
klog.V(2).Infof("Enforcing System reserved on cgroup %q with limits: %+v", nc.SystemReservedCgroupName, nc.SystemReserved)
if err := enforceExistingCgroup(cm.cgroupManager, cm.cgroupManager.CgroupName(nc.SystemReservedCgroupName), nc.SystemReserved); err != nil {
message := fmt.Sprintf("Failed to enforce System Reserved Cgroup Limits on %q: %v", nc.SystemReservedCgroupName, err)
cm.recorder.Event(nodeRef, v1.EventTypeWarning, events.FailedNodeAllocatableEnforcement, message)
return fmt.Errorf(message)
}
cm.recorder.Eventf(nodeRef, v1.EventTypeNormal, events.SuccessfulNodeAllocatableEnforcement, "Updated limits on system reserved cgroup %v"