写点什么

[大厂实践] Netflix 容器平台内核 panic 可观察性实践

作者:俞凡
  • 2023-12-03
    上海
  • 本文字数:3325 字

    阅读完需:约 11 分钟

在某些情况下,K8S 节点和 Pod 会因为出错自动消失,很难追溯原因,其中一种情况就是发生了内核 panic。本文介绍了 Netflix 容器平台针对内核 panic 所做的可观测性增强,使得发生内核 panic 的时候,能够导出信息,帮助排查问题。原文: Kubernetes And Kernel Panics


最近,我们为了减轻容器平台Titus客户(工程师,而不是最终用户)的痛苦,开始调查"孤儿(Orphaned)"pod。有些 pod 从不会结束,只能被垃圾收集,没有真正令人满意的最终状态。我们的服务任务(比如ReplicatSet)所有者不会太在意,但 Batch 用户会非常在意。如果没有真正的返回码,怎么才能知道重试是否安全?


即使只占系统中总 pod 的一小部分,这些孤儿 pod 对用户来说也是真正的痛苦。这些 pod 到底去哪儿了?为什么不见了?


本文展示了如何将最坏情况(内核 panic)与 Kubernetes(k8s)联系起来,并最终与我们的运维人员联系起来,这样我们就可以跟踪 k8s 节点是如何以及为什么消失的。

孤儿 Pod 从何而来?

因为底层 k8s 节点对象消失了,所以孤儿 pod 也消失了。一旦发生这种情况,GC进程将删除该 pod。在 Titus 上,我们运行自定义控制器来存储 Pod 和 Node 对象的历史,这样我们就可以保存一些解释并将其显示给用户。对应的失败模式在我们的 UI 中是这样的:


当 k8s 节点和它的 pod 消失时,用户会看到什么


这是一种解释,但我和用户都不太满意。为什么代理丢失了?

丢失的节点从何而来?

节点可能因为任何原因消失,尤其是在"云"中。当这种情况发生时,通常是云供应商提供的 k8s 云控制器检测到实际的服务器(在我们的例子中是 EC2 实例)已经消失,并反过来删除 k8s 节点对象。这仍然没有真正回答为什么。


如何确保每个消失的实例都有原因,提供解释,并和 pod 关联在一起?这一切都始于一个注释:


{     "apiVersion": "v1",     "kind": "Pod",     "metadata": {          "annotations": {               "pod.titus.netflix.com/pod-termination-reason": "Something really bad happened!",...
复制代码


创建存放这些数据的地方就是一个很好的开始。现在我们所要做的就是让 GC 控制器意识到这个注释,然后将其分发给任何可能导致 pod 或节点意外消失的进程中。添加注释(而不是修补状态)可以保留 pod 的其余部分。(我们还为终止原因添加了注释,并为标记添加了简短的reason-code)


pod-termination-reason注释对于填充人类可读的消息非常有用,例如:


  • "此 pod 被更高优先级的作业($id)抢占了"

  • "由于底层硬件失败,必须终止此 pod ($failuretype) "

  • "这个 pod 必须被终止,因为 $user 在节点上运行 sudo halt "

  • "这个 pod 意外死亡,因为底层节点内核 panic 了!"


但是等等,我们如何为内核 panic 的节点注释 pod 呢?

捕获内核 Panic

当 Linux 内核出现问题时,能做的就不多了。但是,如果可以发出某种"在我的最后一口气中,诅咒 Kubernetes!"UDP 数据包呢?


受这篇Google Spanner论文的启发,Spanner 节点发出"最后一口气"UDP 数据包来释放租约和锁,也可以配置服务器在内核 panic 时使用一个常用的 Linux 模块netconsole来做同样的事情。

配置 Netconsole

事实上,Linux 内核甚至可以发送带有字符串"kernel panic"的 UDP 数据包,而它正在 panic,这有点令人惊讶。能做到这一点是因为 netconsole 需要配置实现填写好的整个 IP 头。没错,必须告诉 Linux 源 MAC、IP 和 UDP 端口是什么,以及目标 MAC、IP 和 UDP 端口是什么,实际上是在为内核构造 UDP 数据包。但是,有了这些准备工作,当时机成熟时,内核可以很容易的构造数据包,并在系统崩溃时将其从(预配置的)网络接口中取出。幸运的是,netconsole-setup命令使设置变得非常简单,所有配置选项可以动态设置,这样当端点发生变化时,就可以指向新的 IP。


一旦设置完成,内核消息将在modprobe之后开始流动。想象一下,整个操作就像执行dmesg | netcat -u $destination 6666,只不过是在内核空间中。

Netconsole"最后的怒吼"数据包

通过netconsole设置,内核 panic 的最后怒吼看起来就像一组 UDP 数据包,就像人们可能期望的那样,其中 UDP 数据包的数据只是内核消息的文本。在内核 panic 的情况下,看起来像这样(每行一个 UDP 数据包):


Kernel panic - not syncing: buffer overrun at 0x4ba4c73e73acce54[ 8374.456345] CPU: 1 PID: 139616 Comm: insmod Kdump: loaded Tainted: G OE[ 8374.458506] Hardware name: Amazon EC2 r5.2xlarge/, BIOS 1.0 10/16/2017[ 8374.555629] Call Trace:[ 8374.556147] <TASK>[ 8374.556601] dump_stack_lvl+0x45/0x5b[ 8374.557361] panic+0x103/0x2db[ 8374.558166] ? __cond_resched+0x15/0x20[ 8374.559019] ? do_init_module+0x22/0x20a[ 8374.655123] ? 0xffffffffc0f56000[ 8374.655810] init_module+0x11/0x1000 [kpanic][ 8374.656939] do_one_initcall+0x41/0x1e0[ 8374.657724] ? __cond_resched+0x15/0x20[ 8374.658505] ? kmem_cache_alloc_trace+0x3d/0x3c0[ 8374.754906] do_init_module+0x4b/0x20a[ 8374.755703] load_module+0x2a7a/0x3030[ 8374.756557] ? __do_sys_finit_module+0xaa/0x110[ 8374.757480] __do_sys_finit_module+0xaa/0x110[ 8374.758537] do_syscall_64+0x3a/0xc0[ 8374.759331] entry_SYSCALL_64_after_hwframe+0x62/0xcc[ 8374.855671] RIP: 0033:0x7f2869e8ee69...
复制代码

连接到 Kubernetes

最后要连接的是 Kubernetes (k8s),需要 k8s 控制器完成以下工作:


  1. 监听端口 6666 上的 netconsole UDP 数据包,观察来自节点的类似内核 panic 的情况。

  2. 在内核出现故障时,查找与传入 netconsole 数据包的 IP 地址相关联的 k8s 节点对象。

  3. 对于该 k8s 节点,找到绑定到它的所有 pod,注释,然后删除这些 pod。

  4. 对于 k8s 节点,注释节点,然后删除。


第 1 步和第 2 步可能是这样的:


for {    n, addr, err := serverConn.ReadFromUDP(buf)    if err != nil {        klog.Errorf("Error ReadFromUDP: %s", err)    } else {        line := santizeNetConsoleBuffer(buf[0:n])        if isKernelPanic(line) {            panicCounter = 20            go handleKernelPanicOnNode(ctx, addr, nodeInformer, podInformer, kubeClient, line)        }    }    if panicCounter > 0 {        klog.Infof("KernelPanic context from %s: %s", addr.IP, line)        panicCounter++    }}
复制代码


然后第 3 和第 4 步:


func handleKernelPanicOnNode(ctx context.Context, addr *net.UDPAddr, nodeInformer cache.SharedIndexInformer, podInformer cache.SharedIndexInformer, kubeClient kubernetes.Interface, line string) {    node := getNodeFromAddr(addr.IP.String(), nodeInformer)    if node == nil {        klog.Errorf("Got a kernel panic from %s, but couldn't find a k8s node object for it?", addr.IP.String())    } else {        pods := getPodsFromNode(node, podInformer)        klog.Infof("Got a kernel panic from node %s, annotating and deleting all %d pods and that node.", node.Name, len(pods))        annotateAndDeletePodsWithReason(ctx, kubeClient, pods, line)        err := deleteNode(ctx, kubeClient, node.Name)        if err != nil {            klog.Errorf("Error deleting node %s: %s", node.Name, err)        } else {            klog.Infof("Deleted panicked node %s", node.Name)        }    }}
复制代码


有了这些代码,一旦检测到内核故障,pod 和节点就会立即消失。不需要等待任何 GC 进程。注释帮助记录发生在节点和 pod 上的事情:


真实的 pod 在真实的 k8s 节点上丢失了,这个节点发生了真实的内核 panic!

结论

将作业标记为由于内核 panic 而失败可能不会让客户满意。但当他们知道我们现在有必要的可观察性工具来开始修复这些内核 panic 时,就会感到满意!




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 5
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
[大厂实践] Netflix容器平台内核panic可观察性实践_Kubernetes_俞凡_InfoQ写作社区