最新消息:

如何准确识别K8S中的PodOOMKilled并发出报警

IT技术 ipcpu 211浏览 0评论

如何准确识别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

转载请注明:IPCPU-网络之路 » 如何准确识别K8S中的PodOOMKilled并发出报警

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址