1. 架构定位与组件分工
1.1 为什么高可用集群里最终选了 containerd
聊到 Kubernetes 高可用(HA)集群的容器运行时,很多人第一反应还是 Docker。其实从 k8s 1.24 版本开始,dockershim 被正式移除之后,containerd 就成了绝大多数生产集群的默认选择。我之前在维护某跨平台系统的时候,还见过不少老集群里硬生生跑着 Docker 模式的操作,后来升级到 1.26 的时候踩了一堆坑,才彻底明白当年 Kubernetes 团队做这个决定背后的逻辑。
containerd 和 Docker 的关系,通俗一点说就是 Docker 把 containerd 包了一层,提供了更上层的镜像构建、命令行交互和一整套开发者体验。但在 Kubernetes 的运行环境里,真正干活的其实是 containerd。去掉 Docker 这层壳,直接让 kubelet 通过 CRI(Container Runtime Interface)和 containerd 通信,链路更短,组件更少,占用的系统资源也小得多。生产环境里这意味着一件事:故障点少了,排查问题的路径短了,性能浪费也明显降低了。
在 HA 架构下,这一点尤其重要。高可用集群往往有多个控制平面节点和多个工作节点,如果每个节点上还额外跑着一个 Docker daemon 进程,无疑增加了内存占用和崩溃概率。用 containerd 作为底座,每个节点上只需要管理一个轻量的容器运行时服务,配合 kubelet 工作,逻辑上就清晰很多。还有一个很实在的因素:containerd 的崩溃恢复速度比 Docker daemon 快不少,这在节点故障切换的场景里非常关键。
1.2 kubelet 和容器运行时之间的 CRI 桥梁
kubelet 是 Kubernetes 里负责管理节点上容器生命周期的核心组件,但它自己并不直接创建容器。它通过 CRI 接口向容器运行时发指令,让 containerd 去实际执行创建、启动、停止、删除这些操作。这个过程有点像你去餐厅点菜,kubelet 是服务员,而后厨的厨师就是 containerd。服务员不需要知道菜是怎么炒出来的,只需要把订单递进去,再把菜端出来。
在 containerd 这个方案里,kubelet 默认通过一个 Unix Socket 和 containerd 通信。这个 Socket 的路径一般是/run/containerd/containerd.sock,kubelet 启动的时候通过--container-runtime-endpoint这个参数指定。很多人配置的时候会忽略一个细节:不同版本的 Kubernetes 对这个参数的默认值不一样。有些版本默认走的是 Docker 的 Socket,如果不显式指定,kubelet 就一直连不上 containerd,然后疯狂报错。
CRI 的设计初衷就是让 kubelet 和具体的运行时实现解耦。你换上 containerd,kubelet 的代码不需要改动;将来如果出现更好的运行时,理论上也可以无缝切换。但在 HA 集群里,这个抽象层也带来一个问题:你必须在所有节点上保证 kubelet 和 containerd 的版本兼容性。我见过一个事故,某团队某个节点上 containerd 版本很老,kubelet 版本很新,结果那个节点一直注册不上集群,后来发现是 CRI 版本协商失败导致的。
1.3 HA 架构下的特殊考量
高可用 Kubernetes 集群和单节点测试环境最大的区别在于:每一个组件都不能是单点。控制平面的 kubelet 要配合静态 Pod 把 apiserver、etcd、controller-manager、scheduler 这些组件拉起来;工作节点的 kubelet 要负责调度上来的业务 Pod 的生命周期。而 containerd 则是所有这些 Pod 的栖身之所。
在 HA 架构下,kubelet 配置里有一个非常关键的参数:--register-with-taints。控制平面节点默认会打上污点,让普通业务 Pod 不会被调度到这些节点上。你如果配置 kubelet 启动参数的时候不小心把这个选项搞错了,可能会出现业务 Pod 被调度到控制平面节点上、和 apiserver 抢占资源的情况。这个问题在单节点环境里不明显,但在三控制节点的 HA 集群里就是一个比较严重的隐患。
另外,HA 集群里各节点的/etc/hosts往往会配置多个 apiserver 的地址,kubelet 需要通过--kubeconfig指向一个包含集群访问信息的文件。如果集群里有多个 apiserver 做了负载均衡,kubelet 只需要指向负载均衡器的虚拟 IP 就行。但如果用了 keepalived 之类的方案,虚拟 IP 切换的时候,kubelet 的长连接可能会短暂中断,这就需要 kubelet 能自动重连。好在 kubelet 默认就有这个能力,只是重连期间的日志会刷得比较吓人,习惯了就好。
2. containerd 核心配置解析
2.1 config.toml 里的关键参数
装好 containerd 之后,通常系统会生成一个默认的配置文件,路径在/etc/containerd/config.toml。生产环境中我们一般不会直接使用默认配置,而是执行containerd config default > /etc/containerd/config.toml先生成一份完整的配置模板,再基于它做修改。
我第一次做这块的时候,对着那个动辄几百行的 TOML 文件有点懵。后来发现,真正在生产环境中需要手工调整的其实就那么几个核心参数。第一个是version,当前 containerd 主流版本用的是 2,老版本可能还在用 1,这个字段决定了后续配置项的解析方式。第二个是root和state,分别对应 containerd 的数据目录和运行目录,默认分别是/var/lib/containerd和/run/containerd。一般不建议去改动它们,除非磁盘分区有特殊规划。
大部分初学的人会在plugins."io.containerd.grpc.v1.cri"这一段里翻车。这个段落是 containerd 内置的 CRI 插件的配置区,kubelet 通过 CRI 创建的所有 Pod 都走这个插件。如今大部分发行版提供的默认 containerd 已经内置了 CRI Plugin,所以重启 containerd 服务之后,那个containerd.sock就会自动出现。如果你的 containerd 版本较老或者发行版特殊,还需要手动确认这个插件是否有启用,否则后面 kubelet 会一直连不上 Socket。
2.2 systemd cgroup driver 的一致性
在 Kubernetes 的环境里,cgroup 驱动的一致性是最容易出错、又最难排查的问题之一。kubelet 有一堆 cgroup 相关的参数,containerd 也有一堆 cgroup 相关的参数,它们俩必须配置成同一个驱动,否则节点虽然能注册成功,但 Pod 运行一段时间之后就会出现各种资源统计错乱、稳定性问题。
目前主流的做法是用systemdcgroup driver。原因很简单:systemd 是现代 Linux 发行版的默认初始化系统,它本身就接管了 cgroup 的一部分管理逻辑。如果容器运行时绕过 systemd 自己直接操作 cgroup,就会和 systemd 的预期状态不一致,在资源紧张的时候容易出现进程被错误杀掉的情况。
containerd 侧需要在 config.toml 里找到SystemdCgroup这个布尔值,把它设为true。很多老版本默认是false,如果你用的是 Docker 时代的习惯,很可能会忽略这一步。kubelet 侧的配置也简单,就是启动参数里加一行--cgroup-driver=systemd。两边设置完之后,还可以用一个简单的方法验证一致性:在节点上执行kubectl describe node,看看输出的System Info里的Container Runtime Version部分是否正常,同时观察节点上的容器是否能正确显示 CPU 和内存使用量。
还有一个细节:如果你使用的是云服务器,某些供应商会给内核打一些特殊的 cgroup 补丁,这时候你还要注意 cgroupfs 版本的问题。CentOS 7 用的是 cgroup v1,比较新的 Ubuntu 22.04 已经默认启用 cgroup v2。containerd 和 kubelet 都能同时支持 v1 和 v2,但你最好确认一下操作系统实际挂载的 cgroup 版本,再把配置保持一致。这个问题在集群混合节点的时候尤其明显,A 节点和 B 节点系统版本不一致,就会导致同样的配置在一个节点上正常、另一个节点上报警。
2.3 sandbox_image 与 pause 容器
Kubernetes 里的每个 Pod 其实都有一个看不见的"地基"容器——pause 容器(也叫 infra 容器)。它负责hold住 Pod 的网络命名空间和一部分资源,其他业务容器通过共享这个 pause 容器的命名空间来实现网络互通。这个 pause 镜像是由 kubelet 或者 containerd 的 CRI 插件负责拉取的,配置项就在 containerd 的 config.toml 里,字段名是sandbox_image。
默认的 sandbox_image 指向的是 Kubernetes 官方的镜像仓库地址,比如registry.k8s.io/pause:3.9。但是在某些网络环境下,直接访问这个地址可能会超时,导致 Pod 一直处于ContainerCreating状态。这里就体现出配置镜像加速的必要性了。你可以把 sandbox_image 换成国内可访问的镜像地址,或者自建的私有镜像仓库里的 pause 镜像。关键是镜像的 tag 版本要和 Kubernetes 主版本匹配,不匹配的话虽然大概率也能跑,但有些新特性的行为会不一致。
换 sandbox_image 的时候记住一个原则:不要只改 containerd 的配置而忽略了 kubelet 侧可能有运行的旧 Pod。改完配置之后,systemctl restart containerd只能让新创建的 Pod 使用新的 pause 镜像,已经存在的 Pod 还是会继续用旧的。要想全部切换,需要先 drain 节点,把旧 Pod 全部驱逐掉,再在节点上重启 containerd,最后 uncordon 恢复调度。这个过程在高可用集群里是可以无缝进行的,因为你还有其他节点在承接业务流量。
2.4 registry mirror 配置
生产环境里大概率需要访问自有镜像仓库,或者对官方仓库做镜像加速。containerd 的 config.toml 里提供了[plugins."io.containerd.grpc.v1.cri".registry.mirrors]这个配置段来管理镜像仓库访问。这里有个常见的坑:很多照着旧文档来的人,会以为配置方式是docker.io = ["https://mirror.example.com"],但 containerd 的 TOML 解析对 key 名的格式要求很严格,URL 里的冒号和斜杠需要特别注意转义的处理方式。
我建议的写法是在 config.toml 里这样定义一个 mirror:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://mirror.example.com", "https://registry-1.docker.io"]这样做的含义是:当 kubelet 指令 containerd 去拉取docker.io/library/nginx:1.25时,containerd 会优先访问第一个 endpoint 也就是你的镜像加速地址,如果失败再回退到后面的官方地址。endpoint 列表的优先级是从上到下的,所以把最快的镜像源放在最前面是有道理的。
还有一个容易让人困惑的点:如果你使用的是私有仓库,且没有配置 TLS 证书,需要在 config.toml 里额外增加[plugins."io.containerd.grpc.v1.cri".registry.configs."myregistry.example.com".tls]这段配置,把insecure_skip_verify设为true。生产环境我不建议直接这么干,但如果内网测试环境临时用,这种做法能省掉繁琐的证书分发流程。
3. kubelet 核心配置详解
3.1 systemd unit 文件里的启动参数
Kubernetes 集群里的 kubelet 一般都用 systemd 管理,unit 文件路径通常是/etc/systemd/system/kubelet.service.d/10-kubeadm.conf,如果你用的是 kubeadm 部署的话。这个文件里包含了 kubelet 的全部启动参数。很多人在配置的时候会忽略一个细节:kubelet 加载配置的优先级顺序,命令行参数会覆盖配置文件里的内容,所以如果在 unit 文件里写了参数,又在kubelet config.yaml里配置了同样的字段,最终生效的实际上是命令行里的那一个。
我在实际项目中习惯把 kubelet 的参数尽量集中在 systemd drop-in 文件里管理。这样每个节点上的行为高度一致,排查问题的时候只需要把 unit 文件拉出来看一遍,就能理解这个节点的 kubelet 是什么配置。要改参数的话也简单,修改完执行systemctl daemon-reload && systemctl restart kubelet即可。
kubelet 最核心的几个启动参数,总结起来包括:--kubeconfig指定连接 apiserver 用的凭据文件;--config指定 kubelet 自身的配置文件路径;--container-runtime-endpoint指定容器运行时 Socket;--pod-infra-container-image指定 pause 镜像。后面两个参数在 containerd 模式下尤其重要,如果你集群里用了私有镜像仓库,需要在--pod-infra-container-image里显式指定和 containerd sandbox_image 一致的镜像。
3.2 节点注册、认证与 apiserver 通信
kubelet 启动之后要做的第一件事就是向 apiserver 注册节点信息,同时建立后续通信用的心跳连接。这个注册过程依赖--kubeconfig指向的文件里的 client 证书和 token 信息。用 kubeadm 部署时,这个文件通常是/etc/kubernetes/kubelet.conf,里面有一次性的 bootstrap token,用于完成初始认证。
有个细节值得说一说:kubelet 在完成 bootstrap 认证之后,会自动生成一份属于自己的证书,路径在/var/lib/kubelet/pki/kubelet-client-current.pem,后续的通信都用这份证书。如果你因为某种原因删除了这个文件,kubelet 会重新走一遍 bootstrap 流程。在 HA 集群里,如果控制平面节点的证书过期,kubelet 会反复尝试重新联系 apiserver,日志里能看到大量的认证失败信息。这种情况的恢复方法是重新分发集群的 CA 证书,并在各节点上更新 kubelet.conf,一定不要图省事直接把kubelet.conf删掉,否则可能触发更复杂的证书签发流程。
然后还有一个容易被忽略的配置项:--node-ip。在有多个网卡的节点上,kubelet 默认会选择一个内网 IP 作为节点通信地址。如果选错了网卡,Pod 与节点之间的网络通信就会出现问题。比如节点上有 docker0 网桥,又有物理网卡,kubelet 很可能会误选 docker0 的 IP。解决办法是在 unit 文件里显式指定--node-ip,或者在 kubelet 的 config.yaml 里设置nodeIP字段。这一点在 HA 集群中需要特别关注,因为节点如果注册了两个 IP,集群里的 Service 转发就会出现路由混乱的情况。
3.3 静态 Pod 与控制平面自举
在 Kubernetes HA 架构里,控制平面组件(apiserver、etcd、controller-manager、scheduler)不是直接以 systemd 服务方式运行的,而是以静态 Pod 的方式由 kubelet 拉起。所谓静态 Pod,就是直接放在节点上某个目录下的 Pod 定义文件,kubelet 会定期扫描这个目录,根据文件内容自行创建和更新对应 Pod。这个目录一般是/etc/kubernetes/manifests。
这种设计的高明之处在于:控制平面组件的生命周期完全由 kubelet 管理,kubelet 挂了组件就挂,kubelet 活着组件就能自动恢复。对于 HA 集群来说,这就避免了手动启动 apiserver 等服务的繁琐操作。但如果你的静态 Pod 配置有问题,表现出的症状会非常诡异——比如 apiserver 反复重启、etcd 集群成员频繁变化。
我在实际操作中发现,静态 Pod 目录下的 YAML 文件命名以及内容格式特别敏感。文件后缀必须是.yaml或者.json,写成.yml可能会让 kubelet 无法识别。文件内的kind字段必须是Pod,而且metadata.name必须填好,如果留空,kubelet 会把文件名当作 Pod 名。另外,静态 Pod 的镜像拉取策略默认是IfNotPresent,如果你改了镜像标签但节点上还缓存着旧版本,那 kubelet 不会主动拉取新镜像,这是一个隐蔽的坑。我遇到过某团队升级控制平面版本时,改了 manifests 目录下的 YAML,但 kubelet 硬是用了旧镜像跑了几个小时,最后手动删掉节点上的旧镜像才恢复正常。
4. 启动顺序与联合健康检查
4.1 为什么先 containerd 后 kubelet
每个节点在开机之后,systemd 会按照依赖关系拉起各种服务。kubelet 的 unit 文件里通常有一行After=containerd.service,这个声明是为了保证在 containerd 起来之后 kubelet 才会启动。否则 kubelet 一开始就会连接 containerd 的 Socket 失败,虽然它会一直重试,但前面的报错日志会误导排查方向。
这里我建议额外注意 systemd 的依赖关系不仅仅是After,还可能需要Requires或者Wants。有些团队为了让 kubelet 在 containerd 崩溃时也能重启,会配置PartOf=containerd.service之类的指令。但生产环境我不太推荐这样强绑定,因为 containerd 出故障时如果强制重启 kubelet,可能会造成所有 Pod 的大规模重启,对业务的影响范围反而更大了。
比较稳妥的做法是:containerd 和 kubelet 各自独立管理,containerd 崩溃的时候有 systemd 自动拉起,kubelet 则能通过 CRI 重新建立通信。kubelet 在失去与容器运行时的连接后,会在日志里打印类似Failed to get sandbox image或者GRPC error的提示,但不会自杀。等 containerd 恢复之后,kubelet 会自动重新建立连接并继续工作。这个自动恢复的过程在 HA 集群里尤其重要,之前我遇到过 containerd 因为磁盘问题短暂僵死,之后自行恢复,节点上的业务 Pod 虽然经历了几分钟的Unknown状态,但最终都完好地活了下来。
4.2 containerd 常用检查命令
containerd 没有docker ps这种直观的命令,它提供的是ctr和crictl两个命令行工具。ctr是 containerd 的原生命令,功能相对底层,一般用于管理镜像和运行时的底层操作。而crictl是 Kubernetes 社区提供的 CRI 兼容命令行工具,可以理解为专门为 kubelet 场景定制的"docker ps",日常排查用它就够了。
判断 containerd 是否正常,别光看服务状态,而是要实际调一下 Socket 看看响应。我常用的几组命令是这样的:用ctr version检查 containerd 版本信息,用ctr plugins list查看 CRI 插件和各个功能模块的加载状态,用crictl ps -a查看节点上所有容器的状态,用crictl images查看已拉取的镜像列表。
这里面最容易踩坑的是crictl本身就依赖一个配置文件才能知道该连哪个 Socket。如果你在节点上执行crictl ps报错说failed to connect,先检查/etc/crictl.yaml里的runtime-endpoint字段填的是不是unix:///run/containerd/containerd.sock。我遇到不少同事在 Kubernetes 环境里直接敲crictl却连不上,以为 containerd 挂了,实际上只是没有配置连接端点、默认还在找 Docker 的 Socket 路径。
4.3 判断 kubelet 与 containerd 协作是否健康
最直观的检查方式当然是kubectl get nodes看所有节点是不是Ready。如果节点不是Ready,第一步就需要去节点上看journalctl -u kubelet -f和journalctl -u containerd -f的日志输出。这里有一个经验:kubelet 的日志信息量很大,光靠肉眼扫可能找不到重点,可以使用grep -i error先过滤出明显错误级别的内容,然后再逐条分析。
还有一个实用技巧是查看/var/log/pods目录下各 Pod 的日志,Kubernetes 默认会把这个目录以卷的方式挂载到每个节点上。当你发现某个 Pod 长时间处于ContainerCreating,先检查这个 Pod 对应的容器是否在 containerd 里存在。如果存在但一直起不来,可以执行crictl logs看容器日志;如果压根不存在,那就是 kubelet 给 containerd 下达的创建指令没有执行成功,问题大概率出在镜像拉取或者配置错误上。
健康状态下,kubelet 的日志应该是相对安静的,偶尔有SyncLoop或者Pulling image之类的正常记录,定期会向 apiserver 上报节点状态心跳。如果你在日志里看到反复出现的PLEG is not healthy,这是一个信号,说明节点的 Pod 生命周期事件循环出现卡顿。这个问题的根源经常不是因为 kubelet 本身,而是 containerd 在反复重新加载配置、或者镜像拉取超时导致事件队列堆积。遇到这种情况,先观察 CPU 和磁盘 IO 是否异常,再考虑重启 containerd 还是重启 kubelet。
5. 常见问题与排查技巧实录
5.1 kubelet 一直报 CRI 连接失败
现象描述:节点上 kubelet 服务状态显示不断重启,日志里反复出现failed to connect to containerd或者failed to determine if the container runtime is already running。
排查思路先分三步走:第一步看 containerd 是否真的活着,执行systemctl status containerd;第二步确认 Socket 文件是否存在,执行ls -l /run/containerd/containerd.sock;第三步检查 kubelet 启动参数里的--container-runtime-endpoint是否指向了这个 Socket。
如果 containerd 活着但 Socket 文件不存在,多半是 containerd 配置里的 CRI 插件没启用。这个问题在 containerd 1.7 版本之后比较少见,老版本里就需要检查是不是存在disabled_plugins数组包含cri字样。处理方法是在 config.toml 里去掉对 CRI 插件的禁用,重启 containerd 服务。
还有一个小概率情况:Socket 文件存在但权限不对。kubelet 通常以 root 运行,而 containerd 的 Socket 需要具备相应的读写权限,正常 root 用户可以直接访问,但如果配置过User=限定 kubelet 运行用户,就需要注意 Socket 是否对那个用户开放了访问权限。我见过一个节点上 containerd 和 kubelet 都以 root 运行,但 containerd 的 Socket 目录权限被某些安全策略设置成0750,导致问题的例子,排查了半天最后发现是权限多了一位数。
5.2 cgroup driver 不匹配导致的资源异常
如果你的节点注册成功,但运行一些容器后kubectl top node显示的 CPU 内存用量异常高或者全为 0,那很有可能是 cgroup driver 不一致。kubelet 和 containerd 一个用systemd一个用cgroupfs,就会出现这种错乱的现象。原因在于两套驱动会把进程归属到不同的 cgroup 层级里,统计数据自然对不上。
验证方法:在节点上执行ps -ef | grep kubelet看启动参数里的--cgroup-driver,用cat /etc/containerd/config.toml | grep SystemdCgroup看 containerd 的配置,再结合节点操作系统的 cgroup 版本来综合判断。通常如果操作系统是使用 systemd 的现代发行版,推荐两边都是systemd;如果你的发行版用的是 cgroupfs 年轻时的那一套配置,那还是得根据实际情况来,而不是无脑照着网上的教程抄。
这个问题的修复不难,难的是发现过程。它不像连接失败那样在日志里直接报错,更多是以一个"各种功能看起来都正常,但就是资源数据不对"的形式存在。我通常会在部署完一个节点之后,立刻用kubectl top和crictl stats做对比,看两组数据是否接近。如果不一致,马上检查驱动配置,不要等项目上运行业务之后再来查,那时候数据全线飘红,很难定位是哪一层出的问题。
5.3 sandbox 镜像拉取失败导致 Pod 卡在 ContainerCreating
这是 containerd 模式下最经典的问题了。现象就是kubectl get pods显示状态ContainerCreating很久不动,节点上crictl ps -a只能看到零星的业务容器、或者是没有任何容器存在,进一步用crictl images查看,发现 pause 镜像缺失或者 tag 不对。
排查方法:journalctl -u containerd日志里多半会有一条镜像拉取超时的记录。如果你配置了镜像加速,先去确认加速地址是否可用;如果没配置,尝试直接拉一次ctr images pull registry.k8s.io/pause:3.9看能否成功,以此判断网络链路是否正常。
另外要注意一个细节:kubelet 在创建 Pod 的时候用的是--pod-infra-container-image参数指定的镜像,而 containerd 创建沙箱时用的却是 config.toml 里的sandbox_image。两者的值如果不一致,可能会出现 "kubelet 认为已经拉好了,containerd 却找不到" 的怪现象。所以在 containerd 方案里,建议把这三个地方联动起来看:kubelet 的启动参数、containerd 的 config.toml、以及节点上实际缓存的镜像列表。
5.4 节点反复 Ready-NotReady 抖动
另一个让运维人员头疼的问题是节点状态不稳定,一会儿 Ready 一会儿 NotReady。这种抖动大多不是单一故障,而是多种因素叠加的结果。常见的原因是 kubelet 心跳上报超时,而心跳超时的背后可能是镜像拉取频繁失败导致 kubelet 事件处理阻塞,或者 containerd 频繁 OOM 重启。
排查这类抖动问题的思路是收集时间线:从journalctl -u kubelet和 containerd 日志里找出从 Ready 到 NotReady 的时间点,再看那段时间内节点上发生了什么。我之前处理过一个案例,某节点上部署了大量带有 hostPath 卷的 Pod,磁盘 inode 被日志文件塞满,containerd 无法写数据,导致所有容器操作都超时,最终 kubelet 判定节点失联。
如果你的节点使用了本地 SSD 且开启了 fstrim 之类的定时任务,也要小心 trim 操作和容器运行时的文件层操作争抢 IO。这种问题在日志上表现不明显,但节点会隔一段时间就无规律抖动。解决办法是在系统层面错开 trim 时间和业务高峰,或者调整 containerd 的 IO 优先级。
5.5 常见问题速查表
| 现象 | 可能原因 | 快速排查命令 | 参考解法 |
|---|---|---|---|
| kubelet 反复重启,日志报连接失败 | kubelet 未指定 containerd Socket | ls -l /run/containerd/containerd.sock | 修改 kubelet 启动参数并重启 |
| Pod 长时间 ContainerCreating | sandbox 镜像拉取失败 | crictl images查看 pause 镜像 | 配置镜像加速或手动 pull |
| 节点 Ready 但 kubectl top 数据异常 | cgroup 驱动不一致 | ps -ef | grep kubelet | 统一 kubelet 和 containerd 的 cgroup driver |
| 节点反复 Ready-NotReady 抖动 | 磁盘 IO 阻塞或 inode 耗尽 | df -i、journalctl -u kubelet -n 200 | 清理磁盘空间、调整日志策略 |
| crictl 命令无法连接 Socket | 缺少/etc/crictl.yaml配置 | 查看/etc/crictl.yaml | 添加 runtime-endpoint 字段 |
| 修改 kubelet 配置不生效 | systemd 未重载 | systemctl daemon-reload | 重载 systemd 配置再重启服务 |
6. 实操过程中的个人体会
6.1 踩过几次坑之后的总结
从我维护过的那几套 Kubernetes 高可用集群来看,containerd 和 kubelet 这套组合只要配置对了,真的可以长期稳定运行不去管它。但前提是你要理解每一条配置背后的"为什么"。比如为什么要统一 cgroup driver,因为 systemd 接管了 cgroup 资源控制,你绕过它就会打架;为什么要指定 node-ip,因为 Linux 多网卡环境下默认选择策略不一定是你要的那个地址。
还有一个体会比较深:排查问题时要先确认底层,再去上层。节点的底层是系统,再往上是 containerd,再往上是 kubelet,之后才是 Kubernetes 集群对象。很多人在 Pod 起不来的时候直接去翻 apiserver 日志,绕了一大圈才发现是 containerd 的 Socket 没监听。从底层往上逐层排查,定位问题的效率会高很多。具体来说,就是先看 containerd 服务是否健康、再看 kubelet 能否连上运行时、最后才去看集群层面的调度和网络策略。
6.2 建议养成的几个好习惯
第一,每次修改 containerd 或 kubelet 配置之前,先把原配置做备份,最好是加个时间戳拷贝到临时目录。改坏了随时能回滚,不用重新配置一遍。我见过有人改完丢了个参数,又得靠回忆恢复现场,效率很低。第二,在节点上始终保存一个可以独立运行的 crictl 配置,这样即使 kubelet 配置出错,你也能用 crictl 检查底层容器状态。第三,养成查看完整日志的习惯,不要只用systemctl status看那一页的摘录,journalctl -u kubelet -f -n 500这样的命令才是排查问题的真实工具。真的哪一天出了故障,可用的日志越详细,恢复的时间就越短。
至于后续的扩展方向,如果你已经把这套 containerd 加 kubelet 的基础配置吃透了,下一步可以继续研究 kubelet 的配置热更新能力,也就是通过 config.yaml 管理 kubelet 参数而不是靠命令行一堆参数堆砌,这在多节点统一管理时能省不少事。也可以探索 containerd 的快照插件机制、镜像加速的使用技巧,以及节点层面的安全加固方案。基础打牢了,后面这些扩展点学起来都会很快。