kubernetes-guide/troubleshooting/cases/runtime/broken-system-time-causing-...

117 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 系统时间被修改导致 sandbox 冲突
## 问题描述
节点重启后,节点上的存量 pod 出现无法正常 running容器sandbox在不断重启的现象。
查看事件,提示是 sandbox 的 name 存在冲突 (`The container name xxx is already used by yyy`),具体事件如下:
![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925153743.png)
## 结论先行
这个问题的根因是节点的时间问题,节点重启前的系统时间比节点重启后的系统时间提前,影响了 kubelet 内部缓存 cache 中的 sandbox 的排序,导致 kubelet 每次起了一个新 sandbox 之后,都只会拿到旧的 sandbox导致了 sandbox 的不断创建和 name 冲突。
## 排查日志
先来看下 kubelet 的日志,部分截图如下:
![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925153751.png)
![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925153759.png)
截图上是同一个 podkube-proxy最近的两次 podWorker 逻辑截图,都抛出了同一个提示:`No ready sandbox for pod <pod-name> can be found, Need to start a new one`。这个应该就是造成容器冲突的来源,每次沉浸到 podWorker 的逻辑之后podWorker 都要尝试去创建一个新的sandbox进而造成容器冲突。
疑问:为啥 podWorker 每次都去创建一个新的 sandbox?
接下来继续调大 kubelet 的日志级别k8s v1.16已经支持动态调整这里调大日志级别到V(6)),这里主要是尝试拿到某个 pod 所关联的所有 sandbox截图如下
![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925153810.png)
通过配合节点上执行 docker inspectps相关命令发现异常的 podkube-proxy存在两个 sandbox重启前的+重启后的),并且在 sandboxID 数组中的排序为 `[重启前的sandbox, 重启后的 sandbox]` (这里先 mark 一下)。
## 相关知识
在进一步分析之前,我们先介绍下相关背景知识。
### Pod 创建流程
先来一起熟悉下 pod 创建流程:
![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925153823.png)
### PLEG 组件
再看下 `PLEG` 的工作流程。kubelet 启动之后,会运行起 `PLEG` 组件,定期的缓存 pod 的信息(包括 pod status。在 `PLEG` 的每次 relist 逻辑中,会对比 `old pod``new pod`,检查是否存在变化,如果新旧 pod 之间存在变化,则开始执行下面两个逻辑:
1. 生成 event 事件,比如 containerStart 等,最后再投递到 `eventChannel` 中,供 podWorker 来消费。
2. 更新内部缓存 cache。在跟新缓存 `updateCache` 的逻辑中,会调用 runtime 的相关接口获取到与 pod 相关的 status 状态信息,然后并缓存到内部缓存 cache中最后发起通知 ( podWorker 会发起订阅) 。
podStatus的数据结构如下
```go
# podStatus
type PodStatus struct {
// ID of the pod.
ID types.UID
...
...
// Only for kuberuntime now, other runtime may keep it nil.
SandboxStatuses []*runtimeapi.PodSandboxStatus
}
# SandboxStatus
// PodSandboxStatus contains the status of the PodSandbox.
type PodSandboxStatus struct {
// ID of the sandbox.
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
...
// Creation timestamp of the sandbox in nanoseconds. Must be > 0.
CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
...
}
```
podStatus 会保存 pod 的一些基础信息,再加上 containerStatus 和 sandboxStatus 信息。
这里重点关注下 SandboxStatus 的排序问题,配合代码可以发现,排序是按照 sandbox 的 Create time 来执行的,并且时间越新,位置越靠前。排序相关的代码部分如下:
```go
// Newest first.
type podSandboxByCreated []*runtimeapi.PodSandbox
func (p podSandboxByCreated) Len() int { return len(p) }
func (p podSandboxByCreated) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p podSandboxByCreated) Less(i, j int) bool { return p[i].CreatedAt > p[j].CreatedAt }
```
### podWorker 组件
最后再看下 podWorker 的工作流程。podWorker 的工作就是负责 pod 在节点上的正确运行(比如挂载 volume新起 sandbox新起 container 等),一个 pod 对应一个 podWorker直到 pod 销毁。当节点重启后kubelet 会收到 `type=ADD` 的事件来创建 pod 对象。
当 pod 更新之后,会触发 `event=containerStart` 事件的投递,然后 kubelet 就会收到 `type=SYNC` 的事件,来更新 pod 对象。在每次 podWorker 的内部逻辑中(`managePodLoop()`) 中,会存在一个 podStatus内部缓存的订阅如下
```go
// This is a blocking call that would return only if the cache
// has an entry for the pod that is newer than minRuntimeCache
// Time. This ensures the worker doesn't start syncing until
// after the cache is at least newer than the finished time of
// the previous sync.
status, err := p.podCache.GetNewerThan(podUID, lastSyncTime)
```
来等待内部 cache 中的 podStatus 更新,然后再操作后续动作(是否重新挂载 volume、是否重建 sandbox是否重建 container 等)。
## 复现问题,定位根因
接下来,我们一起来模拟复现下问题现场。
![](https://image-host-1251893006.cos.ap-chengdu.myqcloud.com/2023%2F09%2F25%2F20230925153835.png)
在节点重启之前,由于是新建节点后,所以对于 pod 来说status 中只有一个命名以 `_0` 结尾的 sandbox。当操作重启节点之后kubelet 收到 `type=ADD` 的事件podWorker 开始创建 pod由于之前以 `_0` 命名结尾的 sandbox 已经 died 了,所以会新建一个新的以 `_1` 命名结尾的 sandbox当新的以 `_1` 命名结尾的 sandbox 运行之后containerStarted就会投递一个 `type=SYNC` 的事件给到 kubelet然后 podWorker 会被再次触发(内部 cache 也更新了通知也发出了。正常情况下podWorker 会拿到 podStatus 中新的 sandbox`_1` 命名结尾的),就不会再创建 sandbox 了,也就是不会发生 name 冲突的问题。而用户的环境却是,此时拿到了以 `_0` 命名结尾的旧的 sandbox所以再新一轮的 podWorker 逻辑中,会再次创建一个新的以 `_1` 命名的 sandbox从而产生冲突。
而这里的根因就是时间问题节点重启前的sandbox`_0` 命名结尾的)的 `create time` 比节点重启后的sandbox`_1` 命名结尾的)的 `create time` 还要提前,所以导致了内部 cache 中 sandbox 的排序发生了错乱,从而触发 name 冲突问题。
## 解决方案
根据上面的排查发现kubelet 的内部缓存中sandbox 的排序是有系统时间来决定的,所以,尽量保证 k8s 集群中的时间有正确同步,或者不要乱改节点上的时间。