
先还原一个真实场景。上周帮一个朋友排查节点故障业务 Pod 卡在 ContainerCreating 出不来kubectl describe也看不到有效事件。登上节点之后我第一件事就是ls -l /var/lib/kubelet因为 Kubernetes 里所有和节点本地状态相关的数据几乎都堆在这个目录里。整理这些年的排障笔记我发现很多人对它的了解只停留在“pods 目录很大、吃磁盘”——但其实/var/lib/kubelet的目录结构直接决定了静态 Pod 能不能起来、GPU 能不能被识别、节点重启后 CPU 绑核状态会不会丢。这篇文章就把这块讲透。内容分成两大部分先按目录逐个拆解每个子目录的真实角色再讲静态 Pod 从 manifest 文件到运行容器的完整管理链路最后补上几个我实际踩过的坑和对应的排查路径。无论是刚接触 Kubernetes 的运维还是已经写过不少 YAML 但还是搞不清节点上状态的开发者这篇文章都值得存一下。1. 先把 /var/lib/kubelet 在节点上的角色讲透1.1 它到底存了什么先解一个常见误解很多教程会把/var/lib/kubelet说成“kubelet 的工作目录”这句话没错但太笼统了。更准确地说这个目录是 kubelet在本机的一切可变状态的落盘位置。它可以分成三类内容运行时状态比如每个 Pod 在本机的 UUID 目录、容器元数据、挂载卷的具体文件、Pod 的 hosts 文件、CPU 管理器的分配状态。通信端点kubelet 与各种插件之间通过 Unix Socket 通信这些 socket 文件也放在这个目录下比如plugins、device-plugins、pod-resources。配置与身份kubelet 自身配置的快照、客户端证书、静态 Pod 的清单文件清单文件本身通常在/etc/kubernetes/manifests但 kubelet 解析后产生的状态会落到这里。理解了这个定位你就知道为什么排障时先翻这个目录——不管是容器起不来、磁盘告警、证书过期还是设备插件不上报资源最终都能在这个目录里找到直接证据。1.2 核心子目录速查表排障时照着找我整理了一张速查表基本覆盖了一个正常 kubeadm 集群节点上常见的子目录。注意不是所有目录在所有版本都会出现比如memory_manager_state只有你显式开启内存管理策略才会生成。子目录/文件作用什么时候最该看它pods/每个 Pod 在本机的状态目录以 Pod UUID 命名Pod 起不来、卷挂载异常、删除后空间不释放plugins/CSI 驱动 Socket、FlexVolume 插件目录CSI 挂卷失败、FlexVolume 找不到驱动device-plugins/kubelet 与设备插件通信的 SocketGPU 等硬件资源没有出现在节点上pod-resources/对外开放的 Pod 资源查询 gRPC Socket需要查 Pod 与设备映射关系时cpu_manager_stateCPU 管理器状态文件节点重启后绑核失效、CPU 分配错乱memory_manager_state内存管理器状态文件使用内存管理策略时分配异常config-hash/kubelet 从--config读到的配置快照kubelet 实际生效配置和预期不一致pki/kubelet 客户端/服务端证书kubelet 报证书过期、无法连 API Server提示不管用的是 containerd 还是 CRI-O/var/lib/kubelet都是 kubelet 的默认根目录。这个路径是可以被--root-dir参数改掉的但生产环境里我几乎没见过有人改改了反而会让很多默认排查命令失效。1.3 根目录被改的代价--root-dir插一句--root-dir因为理解它有助于理解这个目录的边界。kubelet 启动时如果指定了--root-dir/data/kubelet那么 pods、plugins、device-plugins 这些子目录都会跑到新路径下。看起来是好事比如可以把状态放到更大磁盘上但代价是crictl查到的沙箱信息里挂载路径可能仍然指向/var/lib/kubelet/pods一些脚本和监控也会失准。更麻烦的是如果 kubelet 版本升级时把--root-dir改回了默认值旧节点上的 Pod 会被当成全新 Pod 来处理容器被重建状态归零。所以我的建议是没有充足理由就别改 root-dir磁盘不够应该先排查日志和残留目录而不是把一个核心状态目录搬走。2. pods 目录逐个拆一个 Pod UUID 下的完整线索2.1 为什么用 UUID 而不是 Pod 名进入/var/lib/kubelet/pods你会看到一堆 64 位十六进制字符串的目录它们不是随机生成的而是每个 Pod 的 UID。静态 Pod 的 UID 是 kubelet 基于 manifest 内容生成的哈希值普通 Pod 的 UID 则来自 API Server。为什么不用 Pod 名原因很直接Pod 名在命名空间内唯一但跨命名空间、跨节点会重复而且 Pod 删除重建后名字可以一样UUID 一定是新的。如果目录按名字存删除重建后 kubelet 根本分不清新旧状态卷挂载、沙箱清理全都会乱。所以记住在节点上Pod 的唯一标识就是这一串 UUID。2.2 containers 子目录容器元数据与日志的落点打开任意一个 UUID 目录通常会看到类似下面的结构/var/lib/kubelet/pods/uuid/ ├── containers/ │ ├── business-app/ │ └── sandbox-nginx/ ├── volumes/ ├── etc-hosts ├── hostname └── hostscontainers/下每个子目录对应一个容器。这里存放的是 kubelet 视角的容器元数据——容器在运行时里的唯一 ID、日志路径的关联信息等。这里有个常见误区容器的日志文件并不直接躺在containers/name/里。实际日志在/var/log/pods/namespace_podName_uuid/containerName/下而containers/name/里面一般只有元数据和用来定位日志的软链/占位文件。所以你在节点上想看某个容器的完整日志第一反应应该是ls /var/log/pods/namespace_podName_uuid/而不是去/var/lib/kubelet/pods里翻。这个区分很重要。我见过有人为了清理磁盘空间直接删了/var/lib/kubelet/pods/uuid/containers/下的文件结果容器还在跑但 kubelet 重启后元数据对不上容器状态变得非常诡异。正确清理路径应该聚焦/var/log/pods下的日志由日志轮转或自己清理。2.3 volumes 与 hosts 文件卷挂载的本机路径volumes/目录是卷挂载在本机的真实落点。拿一个挂载了 Secret 和 ConfigMap 的 Pod 举例你会在里面看到/var/lib/kubelet/pods/uuid/volumes/ ├── kubernetes.io~configmap/ │ └── app-config/ ├── kubernetes.io~secret/ │ └── app-token/ └── kubernetes.io~empty-dir/ └── tmp-cache/目录名很有意思kubernetes.io~卷类型这种命名是从 FlexVolume 时代继承下来的约定。每个卷子目录里就是注入到容器里的实际文件。如果你想确认某个 Secret 是否真的下发到了节点来这里看是最快的。etc-hosts和hostname这两个文件也值得注意。容器内的/etc/hosts实际上 bind mount 到了这个etc-hosts文件所以你在宿主机上改这个文件容器内立刻能看到。但永远不要手工去改它——kubelet 在 Pod 沙箱重建、网络配置变化时都会重写这个文件你的改动会静默消失。正确的做法是使用spec.hostAliases。3. 容易被忽略的三个通信目录plugins / device-plugins / pod-resources排障排多了你会发现Kubernetes 节点上“目录即接口”的设计非常普遍。插件和 kubelet 之间不通过 HTTP 到网络端口通信而是通过 Unix Socket 落在这些目录下。3.1 pluginsCSI 驱动与 FlexVolume 的 Unix Socket/var/lib/kubelet/plugins/下最核心的内容是 CSI 驱动的 Socket 文件。举个具体例子如果你用了某个云厂商的块存储 CSI 驱动那么节点上就会出现/var/lib/kubelet/plugins/driver-name/csi.sockkubelet 会作为客户端连接到这个 Socket向 CSI 驱动发起NodePublishVolume、NodeUnstageVolume等调用。如果这个 Socket 文件不存在或者权限不对表现就是 Pod 一直 ContainerCreating事件里报failed to find plugin或者rpc error。FlexVolume 时代则是直接把可执行驱动放在plugins/下的子目录里比如plugins/vendor/driver/。现在 FlexVolume 基本被 CSI 取代但老集群里还能看到这种结构。排障经验当你看到卷挂载失败时别急着怀疑 CSI 驱动先ls /var/lib/kubelet/plugins/确认驱动 socket 存在。如果 socket 在但不可写还要看权限如果压根没有大概率是 CSI DaemonSet 没调度到这个节点或者插件容器 CrashLoopBackOff。3.2 device-pluginsGPU 等硬件资源如何上报给 kubeletdevice-plugins/目录是硬件资源上报的关键路径。kubelet 启动时会在这个目录下创建一个kubelet.sock各类设备插件GPU、FPGA、某种网卡会连接这个 Socket 向 kubelet 注册自己然后在device-plugins/下暴露自己独有的 Socket/var/lib/kubelet/device-plugins/ ├── kubelet.sock └── nvidia.com/gpu.sockkubelet 会通过后者的 Socket 持续获取设备列表和健康状态然后上报到 API Server 的 Node 对象上。如果 GPU 节点上kubectl describe node看不到nvidia.com/gpu资源多半是这里出了问题。一个我实际踩过的坑设备插件 Socket 存在但插件版本和驱动不匹配kubelet 一直报Failed to allocate device。我一度以为是驱动问题重装了两次驱动才注意到/var/lib/kubelet/device-plugins/下同时存在两个版本的插件 Socket互相抢注册。后来把所有插件 Pod 清掉只保留一个版本资源立刻正常上报。所以排查 GPU 资源问题时先看这个目录下的 socket 数量和状态。3.3 pod-resources资源分配查询的 gRPC 接口/var/lib/kubelet/pod-resources/往往被忽略但它在现代 Kubernetes 里越来越重要。这里有一个kubelet.sock对外提供 gRPC 服务用于查询节点上所有 Pod 的资源分配情况Pod 与设备的绑定关系比如哪些 GPU 分配给了哪个容器可分配资源总量。拓扑感知调度、设备资源追踪这类组件都是通过这个 Socket 拿数据的。如果你在做资源调度相关的二次开发或者遇到 Pod 已分配设备和实际设备不一致的问题可以试着调用这个 Socket 里的 gRPC 接口来核对。3.4 config 快照与 pki 证书不常用但关键时刻救命config-hash/当 kubelet 通过--config参数指定配置文件启动时它会在这里落一份解析后的配置快照文件名通常就是kubelet。你可以把它理解成“kubelet 实际启动用的配置备份”。如果改了 CM 但 kubelet 行为没变先对比这里和预期配置。pki/存放 kubelet 的证书常见的是kubelet-client-current.pem。证书轮换失败、kubelet 无法连接 API Server 时看这个目录基本都是必选项。注意这里只动 kubelet 自己的证书不要和/etc/kubernetes/pki下的 CA、apiserver 证书混在一起。4. 静态 Pod 生命周期从 manifest 文件到运行容器的完整链路4.1 为什么 kubeadm 用静态 Pod 托管控制平面组件先回答一个很多人都问过的问题为什么 kubeadm 部署的集群里apiserver、etcd、controller-manager、scheduler 不是 systemd 服务而是躺在/etc/kubernetes/manifests/下的几个 YAML/etc/kubernetes/manifests/ ├── etcd.yaml ├── kube-apiserver.yaml ├── kube-controller-manager.yaml └── kube-scheduler.yaml原因很聪明控制平面组件是集群最基础的部分在它们可用之前集群是没有 API Server 来接收“创建 Pod”这个指令的。静态 Pod 由 kubelet 直接管理kubelet 又是节点上的独立进程它不依赖 API Server 就能把 manifest 文件里的容器拉起来。这就形成了一个鸡生蛋问题的优雅解——先用静态 Pod 把 API Server 跑起来剩下的交给 API Server 自己管。同时静态 Pod 天然具备“文件即配置”的运维模型。你要升级 etcd直接替换 yaml 里的镜像kubelet 会自动完成滚动重启节点挂了静态 Pod 会跟着 kubelet 一起消失不会像 Deployment 管理的 Pod 被调度到别的节点。这对控制平面来说反而是特性控制平面组件就该跟节点绑定在一起。4.2 kubelet 怎么发现并加载 manifest 文件kubelet 扫描静态 Pod 清单有两个途径。老一点的版本用--pod-manifest-path参数直接指定目录现在更常见的是在KubeletConfiguration里配置staticPodPath默认值就是/etc/kubernetes/manifests。kubelet 对这个目录的监听策略是“文件事件监听 定时全量扫描”双保险。文件创建、修改、删除会触发即时更新同时还会周期性地全量扫描目录防止漏掉文件事件。这也是为什么你删除一个 manifest 后 Pod 不会立刻消失——kubelet 需要先感知到文件变化然后按正常的优雅终止流程处理通常要等terminationGracePeriod结束才真正杀掉容器。每个 manifest 文件对应一个静态 Pod 的定义。kubelet 加载后会为它在自己的内存里建立一个 Pod 状态机然后走和普通 Pod 一样的 CRI 调用链创建沙箱sandbox- 拉取镜像 - 创建业务容器 - 把卷挂载进容器。这一步的本质是静态 Pod 只是“来源不同”运行链路和普通 Pod 完全一致。4.3 清单文件变化、删除时会发生什么这部分是很多人容易搞混的点。修改 manifestkubelet 会发现文件哈希变化然后判断改动类型。只换镜像这类改动会走“杀死旧容器、启动新容器”的路径如果改了卷、端口这类影响沙箱定义的字段kubelet 会重建整个 Pod其实是先停旧沙箱再起新沙箱。重建过程中容器会有短暂中断控制平面组件会表现为请求失败或 etcd 短暂不可用因此操作前最好评估影响。删除 manifestkubelet 按普通 Pod 删除逻辑执行优雅终止并不存在“立即消失”的说法。如果容器不响应 SIGTERM最终会被 SIGKILL 强杀。这个行为和你kubectl delete pod完全一样只是触发源换成了本地文件。新增 manifestkubelet 发现新文件后会直接创建对应的静态 Pod。这个特性在你临时想在一个节点上跑一个不大动干戈的调试 Pod 时非常有用。5. 镜像 PodMirror Pod机制静态 Pod 与 API Server 的映射关系5.1 mirror pod 是怎么生成的名字为什么带节点名静态 Pod 没有经过 API Server 创建所以理论上 API Server 根本不知道它的存在。但 kubelet 会做一件很重要的事把静态 Pod 在 API Server 上“镜像”一份出来这就是镜像 PodMirror Pod。看一个实际例子。kubeadm 集群里查 Pod你会看到kubectl get pod -n kube-system NAME READY STATUS RESTARTS kube-apiserver-master01 1/1 Running 0注意这个名字kube-apiserver-master01规则是静态Pod名-节点名。如果 manifest 里的 metadata.name 是kube-apiserver节点名是master01那镜像 Pod 就是kube-apiserver-master01。kubelet 创建镜像 Pod 时会给它打上引用该节点的 ownerReferences所以你在kubectl describe里能看到它归属于某个 Node。“名字带节点名”还有一个隐藏细节静态 Pod 如果不显式设置spec.hostname容器内的 hostname 默认就是这个带节点名的镜像 Pod 名字。这就是为什么控制平面容器里执行hostname会看到一个很长的名字而不是干净的kube-apiserver。5.2 为什么 kubectl delete 总是“删不掉”静态 Pod这个问题我几乎每个月都能在社区看到kubectl delete pod kube-apiserver-master01删掉之后Pod 一两秒内又回来了怀疑集群有灵异事件。这不是 Bug是机制。kubectl delete删的是 API Server 上的镜像 Pod 对象但这个对象的真正所有者是 kubelet 的同步循环。kubelet 检查到镜像 Pod 被删除后会立刻重新 POST 一份新的镜像 Pod把它“补”回来。只要/etc/kubernetes/manifests/下对应文件还在静态 Pod 就不可能被 API Server 侧删除。反过来官方文档里明确说过静态 Pod 提交到 API Server 的镜像 Pod 不能被删除、不能被更新。这保证了控制平面的稳定性——你想删掉 apiserver必须去节点上操作 manifest 文件而不是对着 API Server 发指令。5.3 静态 Pod 的配置更新边界ConfigMap/Secret 的限制静态 Pod 有一个官方文档明确写出的限制不支持通过 ConfigMap 或 Secret 的更新来触发 Pod 更新。原因是静态 Pod 的 spec 来自本地文件kubelet 不会像 Deployment 那样 watch 相关 ConfigMap 的版本变化。即使你kubectl edit configmap改了配置静态 Pod 里的挂载文件也不会自动更新Pod 更不会因此重建。要对静态 Pod 应用新的 ConfigMap 内容只能手动修改 manifest 文件来触发 Pod 重建或者干脆不要用 ConfigMap 给静态 Pod 动态喂配置。这个限制对控制平面组件影响不大因为它们的配置基本都通过启动参数和本地文件来管理。但如果你在别的场景下尝试用静态 Pod 跑业务应用就要特别注意静态 Pod 适合“节点本地、配置静态、随 kubelet 存亡”的工作负载不适合需要动态配置管理的应用。想用 Deployment 就老老实实用 Deployment静态 Pod 不是万能的。5.4 维护静态 Pod 的正确姿势改文件 触发重建基于上面的机制安全维护静态 Pod 的完整链路是确认要操作的静态 Pod 在哪个节点kubectl get pod -A -o wide | grep name。SSH 到对应节点编辑/etc/kubernetes/manifests/name.yaml。保存文件后kubelet 自动感知变化并执行滚动更新或重建。观察镜像 Pod 状态如果重建失败kubelet 会保留旧 Pod 继续运行直到新版 ready。这个“失败保留旧版本”的行为对控制平面非常关键它避免了修改一个 yaml 导致 apiserver 直接挂掉的灾难。当然前提是你不要手滑把整个文件删了——一旦文件不存在Pod 就会被终止。6. 排障实录目录结构与静态 Pod 的常见问题复盘6.1 Pod 一直 ContainerCreating从 device-plugins 目录顺藤摸瓜有一次一个 GPU 推理服务在某个新增节点上一直 ContainerCreating报错信息里出现了nvidia.com/gpu资源无法分配。当时我列出了完整排查链路kubectl describe pod name查看事件发现调度到了新节点但没有任何设备分配成功的记录。登到节点上ls /var/lib/kubelet/device-plugins/发现nvidia.com/gpu.sock不存在。查 GPU 插件的 DaemonSet发现它没被调度到这个新节点原因是一个你没见过的污点被加上了。修复调度后socket 出现但 Pod 仍然 ContainerCreating。再看ls -l /var/lib/kubelet/device-plugins/发现 socket 的属主、权限不对kubelet 无法读取。重启该节点的插件 Pod 后 socket 权限恢复正常Pod 启动成功。这个案例想说明的是/var/lib/kubelet/device-plugins/目录里的文件状态是设备插件和 kubelet 之间健康度的直接探测器。出问题时先看文件在不在、权限对不对、socket 有几个比一开始就扑到驱动重装效率高得多。6.2 节点重启后 CPU 绑核失效cpu_manager_state 的恢复过程另一个高频问题是集群开启了cpuManagerPolicy: static某个 Guaranteed 且 requests.cpu 为整数的应用本来被绑到了 0-3 号核节点一重启应用性能明显下降。taskset -p pid一看发现 Cpus_allowed_list 变成了全部核心。问题基本出在/var/lib/kubelet/cpu_manager_state上。这个文件用 JSON 记录了哪些 Pod 绑到了哪些核。节点正常下线时 kubelet 会正确清理它但如果是断电、强制重启文件可能没来得及更新或者记录已经和实际不一致。社区常见的处理方式是cd /var/lib/kubelet cp cpu_manager_state /tmp/cpu_manager_state.bak rm -f cpu_manager_state systemctl restart kubeletkubelet 重启后会重新评估节点上现存 Pod 的 CPU 需求生成一份新的分配状态。这样做能把绑核状态“洗白”但也意味着一部分 Pod 的核会被重新分配所以动手前必须确认业务可以接受短时间 CPU 拓扑变化。注意如果有人告诉你直接删/var/lib/kubelet/pods/uuid就能清掉绑核残留千万别照做。这会连卷挂载元数据一起删掉引发的问题远大于解决的那一个。删除 cpu_manager_state 之前也必须确认当前集群里没有更严重的状态依赖。6.3 磁盘告警在 /var/lib/kubelet从 pods 目录里找出真凶磁盘告警是节点运维里的常客。正确的第一步不是直接rm -rf而是先定位大块头du -sh /var/lib/kubelet/* 2/dev/null | sort -rh | head -20这个命令会告诉你大的子目录是谁。如果是pods/异常大那就再细化到具体 UUIDdu -sh /var/lib/kubelet/pods/* 2/dev/null | sort -rh | head -20最常见的元凶有两类。第一类是容器日志但它们通常在/var/log/pods对应pods/uuid/containers/name/里只是日志的软链或占位文件。清理思路是配好日志轮转而不是直接删 kubelet 状态目录。第二类是残留的 Pod UUID 目录Pod 已经从 API Server 删除但节点上由于以前手动操作过 CRI 或 kubelet 同步异常目录没被回收。残留目录的清理要非常克制。标准流程是crictl ps -a | grep uuid确认这个 UUID 对应的容器已经不存在。在 API Server 上确认这个 Pod 真的已经删除。只删对应 UUID 的目录不要全量清理。千万不要执行rm -rf /var/lib/kubelet/pods/*这会把还在运行的 Pod 状态一并清掉的结果是大量容器失联比磁盘告警严重得多。6.4 目录操作红线哪些文件能碰、哪些碰了会出事最后把我心里的目录操作红线列出来这些都是真实教训换来的不要手工编辑pods/uuid/etc-hostskubelet 会在沙箱重建、网络变更时重写这个文件手工改动会丢失想加 hosts 映射用hostAliases。不要随意删除pki/下的证书因为证书轮换机制会在文件不匹配时尝试重新签发但如果 CA 不可用kubelet 直接起不来。先备份再操作。不要随手删除cpu_manager_state、memory_manager_state这是有状态文件删除相当于让 kubelet 忘记所有 CPU/内存分配影响面是全部拓扑敏感 Pod。不要清理config-hash/目录如果 kubelet 还在运行它可能还在读取这个快照清理后配置状态就说不清了。不要对整个目录chmod -R或chown -Rsocket 文件、证书文件、状态文件的权限要求各不相同一把梭会把通信和认证一起搞坏。实际上对/var/lib/kubelet绝大多数操作的正确姿势是“先备份后修改观察再决定”。比如要动 state 文件先cp一份要删残留目录先用crictl确认无容器要改 manifest先看集群里镜像 Pod 状态。记住这几条红线能帮你避开大部分节点级事故。这篇文章的由来本质上是把我多年排障经验里和/var/lib/kubelet这个目录有关的坑都串了起来。如果你遇到节点上的怪问题先从速查表定位目录再按静态 Pod 的机制推理基本都能找到答案。最后再分享一个小技巧每次要动节点的状态目录前先在终端里敲一句date记录时间再ls /var/lib/kubelet留个快照后续想回溯操作时你会感谢自己这个习惯的。