如何看到 Pod 的失败原因

在生产环境中,如果没有配置合适的告警,可能会发现某个 Pod 有 restartCount,但是由于已经重启了超过一小时,没有相关的 Events,导致无法看到响应的 Pod 重启原因。想要捕获每一次 Pod 退出异常,K8s 为我们提供了 Pod.spec.container[0].terminationMessagePath field ,来记录 Pod 异常退出时的信息。

本文不对 terminationMessagePath 的具体配置以及用法做相关介绍,可参考官方文档;本文会记录一些文档中没有提及但实际使用中会遇到的一些小 tips。

如何在 Pod 重启一小时后查看 Pod 退出原因?

通过 kubectl describe pod 可以看到 Pod 上次的退出状态

1
2
3
4
5
Last State:     Terminated
Reason: Error
Exit Code: 2
Started: Wed, 17 Nov 2021 14:51:42 +0800
Finished: Wed, 17 Nov 2021 14:51:42 +0800

其中 Exit Code 为程序退出的错误码,有一定参考含义,Reason 和 Last State 为 Pod 退出时在 K8s 集群中表现的状态。

但以上信息对于判断 Pod 退出的真正原因是远远不够的。

在 Pod 宿主机上获取 Pod 异常退出的日志

先说结论,对于配置了 terminationMessagePath 和 terminationMessagePolicy 的 Pod,Kubelet 会添加一个对于用户不可见的 hostPath volume,用于将退出信息保存在宿主机上,该目录为/var/log/pods/{pod-name}/{container-name},该目录下会记录 Pod 标准输出的日志,每个日志文件的前缀编号为 Pod 重启次数。根据该日志就能清晰的显示出 Pod 退出时的 log,从而判断异常原因。

接下来我们可以看下 kubelet (v1.20.4)的源码,看看其内在的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// makeMounts generates container volume mounts for kubelet runtime v1.
func (m *kubeGenericRuntimeManager) makeMounts(opts *kubecontainer.RunContainerOptions, container *v1.Container) []*runtimeapi.Mount {
volumeMounts := []*runtimeapi.Mount{}

for idx := range opts.Mounts {
v := opts.Mounts[idx]
selinuxRelabel := v.SELinuxRelabel && selinux.SELinuxEnabled()
mount := &runtimeapi.Mount{
HostPath: v.HostPath,
ContainerPath: v.ContainerPath,
Readonly: v.ReadOnly,
SelinuxRelabel: selinuxRelabel,
Propagation: v.Propagation,
}

volumeMounts = append(volumeMounts, mount)
}

// The reason we create and mount the log file in here (not in kubelet) is because
// the file's location depends on the ID of the container, and we need to create and
// mount the file before actually starting the container.
// we can only mount individual files (e.g.: /etc/hosts, termination-log files) on Windows only if we're using Containerd.
supportsSingleFileMapping := m.SupportsSingleFileMapping()
if opts.PodContainerDir != "" && len(container.TerminationMessagePath) != 0 && supportsSingleFileMapping {
// Because the PodContainerDir contains pod uid and container name which is unique enough,
// here we just add a random id to make the path unique for different instances
// of the same container.
cid := makeUID()
containerLogPath := filepath.Join(opts.PodContainerDir, cid)
fs, err := m.osInterface.Create(containerLogPath)
if err != nil {
utilruntime.HandleError(fmt.Errorf("error on creating termination-log file %q: %v", containerLogPath, err))
} else {
fs.Close()

// Chmod is needed because ioutil.WriteFile() ends up calling
// open(2) to create the file, so the final mode used is "mode &
// ~umask". But we want to make sure the specified mode is used
// in the file no matter what the umask is.
if err := m.osInterface.Chmod(containerLogPath, 0666); err != nil {
utilruntime.HandleError(fmt.Errorf("unable to set termination-log file permissions %q: %v", containerLogPath, err))
}

// Volume Mounts fail on Windows if it is not of the form C:/
containerLogPath = volumeutil.MakeAbsolutePath(goruntime.GOOS, containerLogPath)
terminationMessagePath := volumeutil.MakeAbsolutePath(goruntime.GOOS, container.TerminationMessagePath)
selinuxRelabel := selinux.SELinuxEnabled()
volumeMounts = append(volumeMounts, &runtimeapi.Mount{
HostPath: containerLogPath,
ContainerPath: terminationMessagePath,
SelinuxRelabel: selinuxRelabel,
})
}
}

return volumeMounts
}

pkg/kubelet/kuberuntime/kuberuntime_container.go 下可以看到,if opts.PodContainerDir != "" && len(container.TerminationMessagePath) != 0 && supportsSingleFileMapping 就会触发TerminationMessage 的挂载,hostPath 为 containerLogPath = volumeutil.MakeAbsolutePath(goruntime.GOOS, containerLogPath)

containerLogPath 的路径为 containerLogPath := filepath.Join(opts.PodContainerDir, cid)cid 是一个随机的 UID。opts.PodContainerDir 的默认值为/var/lib/kubelet/pods/{pod-uid}/containers/{container-name} 。在宿主机该目录下,你同样可以看到 Pod 的所有输出,包括上述的退出日志,但是由于文件名是 uid,不是那么直观,只能通过时间来判断具体哪份日志。

至于如何将 /var/lib/kubelet/pods/{pod-uid}/containers/{container-name} 的日志转换到 /var/log/pods/{pod-name}/{container-name} 下,简单看了下貌似是 CRI 做的,所以我猜测 不同的 CRI 可能有不同的表现,笔者的环境 CRI 为 containerd,这一块如有错误麻烦大家指正。