如何准确识别K8S中的PodOOMKilled并发出报警
一、引子
领导要求K8S系统中只要发现Pod容器被OOMKilled,就要发出报警,原因是啥呢,是因为领导发现有些故障总是会伴随着PodOOM,两者之间有一定的关联,所以才要求报警发出来,给大家参考下。
好吧,咱们姑且不去讨论PodOOM和故障之间的契合度,单说如何实现发生PodOOM就报警把。
二、试水,从K8S自带的监控开始
K8S自带监控除了Master节点上的组件和Etcd组件,就只剩下
job="kubelet"
job="kube-state-metrics"
kubelet是运行在Node节点,直接控制容器生命周期的,可惜这里面没有OOM相关的监控项。
kube-state-metrics是一个独立的组件,用来统计Service相关的信息,这里面发现有OOM相关内容
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}
于是我们把这个监控加到监控系统里,结果运行了一段时间发现问题了,当K8S出现问题时,这个报警不会触发,或者十次OOM只能触发一次吧。
一开始我们以为是我们的采集周期过长,这个状态变化太快,我们还没有采集,他的状态就变了,后来翻阅了一些资料发现,并不是如此,而是 [参考资料1]
只有当pid为1的程序为OOM-killer杀死时,Containers才会被标记为OOM killed
好吧,这条路走不通。
三、返工,从OOM原理入手
我们只能从其他方面考虑,我们从OOM原理入手了:
OOM Killer是Linux内核在系统内存严重不足时,强行释放进程内存的一种机制,这个是系统级别的。在引入cgroup以后,cgroup也有类似的OOM Killer,但是这个只能控制cgroup组内的OOM Killer,不能kill其他group选节点上的进程。
当我们创建1个服务时,kubelet会在Node节点创建1个容器Pod,并将limit内存限制写入到cgroup中,当这个Pod使用的内存超过了cgroup中的限制值,就会触发cgroup组内的OOM Killer,将进程杀死。
由于这个机制时cgroup的,因此这个操作不会通知kubelet,kubelet也无法感知到,但是kubelet本身有机制会自动拉起。
四、深挖,dmesg的报警内容
当Pod被OOM Killer杀死以后,会在系统日志中写一些日志,默认是写入到 /dev/kmsg 中,这个文件需要用dmesg 命令读取,使用dmesg -T参数可以在首行打印时间信息。
这个OOM Killer的报错信息会随着系统内核版本的变化,也在不断的变化,目前我们收集到了以下几个内核版本的OOM信息。
## Kernel 3.10版本OOM报错
Task in /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf01c1049_ebf4_4b9d_b04c_345e8da5d1a8.slice/docker-15bbd7bed86af66837bb4e3b6d2e08802a23f31df45aefc60daa81f5a12896b5.scope killed as a result of limit of /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf01c1049_ebf4_4b9d_b04c_345e8da5d1a8.slice
## Kernel 4.19版本OOM报错
Task in /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod9693eb91_d071_4564_895b_fc504afa8f00.slice/docker-1d4c8e1115f9c33504d278db21e17189f8cdd1ad6b794a4557b5f910cb78ea4b.scope killed as a result of limit of /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod9693eb91_d071_4564_895b_fc504afa8f00.slice
## Kernel 5.4版本OOM报错
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=a2172e8d1101b4393d934a3a3fb902260c945c360ed278bbdef80cb45fbb9419,mems_allowed=0-1,oom_memcg=/kubepods/burstable/pod3962896b-ab69-4572-8f1d-ee756f9f3589,task_memcg=/kubepods/burstable/pod3962896b-ab69-4572-8f1d-ee756f9f3589/a2172e8d1101b4393d934a3a3fb902260c945c360ed278bbdef80cb45fbb9419,task=memc,pid=387602,uid=0
## Kernel 5.10版本OOM报错
oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-db82d879ba5185c931261d7544272174ec1368150C41457418C844974.scope,mems_allowed=0,oom_memcg=/kubends.slice/kubepods-burstable.slice/kubepods-bustable-pod748bebc7_7fde_4fef_b2a3_fe45e29392df.slice,task_memcg=/kubends.slice/kubepods-burstable.slice/kubepods-bustable-pod748bebc7_7fde_4fef_b2a3_fe45e29392df.slice/cri-containerd-ab82a879ba5185c93126175448c72174ec1368158c4d45718cfb8b84df97d6.scope,task=memc,pid=26036,uid=8
在这个信息里面,我们可以拿到被Kill的进程PID,进程名称等信息,但其实最重要的是POD容器的UUID信息(3962896b-ab69-4572-8f1d-ee756f9f3589)也在这里面,后面的章节会介绍,但是需要注意的是不同版本的UUID描述不一样,有的是下划线连接,有的是中横线连接。
五、实现,但有所遗憾的NPD
对于Node节点系统 /dev/kmsg 文件的监控,Kubenertes官方是有工具的,叫NPD(node-problem-detector),链接如下
https://github.com/kubernetes/node-problem-detector
这个工具会监控/dev/kmsg 文件的报错信息、Docker进程是否存在等信息,然后将发现的问题以Event形式上报给 K8S的apiserver,这样使用 kubectl get event 就可以看到对应的信息了。
但是呢,官方的这个npd有很大的缺陷,那就是拿不到被Kill的容器名称,这个很重要,如果没有容器名称,只有pid的话,根本无法排查,并且POD被Kill了,根本找不对对应关系。
好在阿里云在它自身的Kubernetes服务中提供了增强版npd,可以实现这个获取容器名称的功能。
六、关联,UUID如何关联Pod名称
我们在第四部分说,OOM的报错信息是包含POD容器的UUID信息的。
在 K8s 系统中,每个实例对象都有自己的 UID(其实就是UUID),用于唯一标识自己,比如 Pod、ConfigMap 等对象的 metadata 内都有一个叫做 uid 的字段。为什么要有这个字段呢?因为仅靠 kind/namespace/name 是没办法确定实例还是那个实例的,比如实例被重建,kind/namespace/name 等信息并不会变化,这时候就需要使用 UID 来判断「你还是不是原来的你了」。
在K8S中可以使用如下命令查看POD的UUID信息
# kubectl get pods -n project-1780 -o custom-columns=PodName:.metadata.name,PodUID:.metadata.uid
PodName PodUID
service-12520-988868dfd-gzjsw af5181dd-e9bd-4b8f-ac41-dc857c3db9d0
当然这些信息可以通过apiserver获得,我们翻看了阿里云npd的代码
https://github.com/AliyunContainerService/node-problem-detector/tree/alibabacloud-release/v0.8.12
// pkg/systemlogmonitor/log_monitor.go 第370行开始
// listPodAndCache list pods on this node, find pod with pod uuid.
func (l *logMonitor) listPodAndCache() error {
doneChan := make(chan bool)
defer close(doneChan)
statisticStartTime := time.Now().UnixNano()
pl, err := k8sClient.CoreV1().Pods("").List(metav1.ListOptions{
ResourceVersion: "0",
FieldSelector: fmt.Sprintf("spec.nodeName=%s", nodeName),
})
statisticEndListPodTime := time.Now().UnixNano()
glog.Infof("listPod spend time: %v ms, startTime: %v nanoTimestamp, endTime: %v nanoTimestamp", (statisticEndListPodTime-statisticStartTime)/1e6, statisticStartTime, statisticEndListPodTime)
if err != nil {
glog.Error("Error in listing pods, error: %v", err.Error())
return err
}
然后还发现了,使用UUID去匹配POD名称的代码。
// 1.1 cache dirty, try re cache
err := l.listPodAndCache()
if err != nil {
glog.Errorf("pod oom found, list and cache pod list error. pod uuid: %v, error: %v, cache value: %v", uuid, err, cacheVal)
}
if cacheVal, ok := l.cache.Get(uuid); ok {
podName, namespace := parseCache(uuid, cacheVal.(string))
glog.V(9).Infof("pod oom hit pod list cache. podName: %v, namespace: %v", podName, namespace)
if podName != "" {
return generatePodOOMEventMessage(podName, uuid, namespace, nodeName)
} else {
glog.Errorf("pod oom found, but pod parse cache error. pod uuid: %v, cache value: %v", uuid, cacheVal)
}
} else {
glog.Errorf("pod oom found, but pod get cache error. pod uuid: %v, cache value: %v", uuid, cacheVal)
}
但是需要注意的地方就是刚刚说的,UUID的获取,当前阿里的版本没有考虑到使用中横线连接UUID的情况,导致5.4内核版本拿不到UUID,不仅如此,阿里的版本在5.4也存在正则匹配的问题,如果你不幸使用了5.4版本,都需要自行修改代码适配。
我把我的NPD部署的YAML放在了github,大家可以参考下。
https://github.com/ipcpu/npd-yaml/blob/main/npd.yaml
七、报警,水到渠成
有了NPD模块,我们使用K8S的eventrouter功能就可以发到统一汇总的地址了,这里我们是输出到kafka,然后扔进了阿里的SLS,使用了阿里云SLS报警系统。
这个地方还有一个需要注意,k8s的Event有ADDED、UPDATED两种,第一次产生的event就是ADDED,我们查询的时候只关注ADDED类型就可以,如果考虑UPDATED的会造成一些重复。
终于实现了报警,过程太曲折了,特别是不同的内核版本OOM报错信息不一样,给我们造成了很多困难。
参考资料
https://zhoushuke.github.io/2023/02/09/Kubernetes-Out-Of-Memory-1/
https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E5%AE%B9%E5%99%A8%E5%AE%9E%E6%88%98%E9%AB%98%E6%89%8B%E8%AF%BE/08%20%E5%AE%B9%E5%99%A8%E5%86%85%E5%AD%98%EF%BC%9A%E6%88%91%E7%9A%84%E5%AE%B9%E5%99%A8%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A2%AB%E6%9D%80%E4%BA%86%EF%BC%9F.md