从 kubeadm 部署一个 Docker + cri-dockerd 组合的集群开始,控制面组件是以静态 Pod 方式运行的。你只要经历过一次"服务器重启后 kubectl 连不上 apiserver"的现场,就会明白我为什么对这个故障印象这么深:不是某个组件真的坏了,而是把 docker、cri-dockerd、kubelet 这三层的启动顺序和自启动状态全部赌在了系统默认配置上。这篇文章就把这个问题的定位思路、根因和完整修复过程拆开讲,顺带记录一些我在实际运维中踩过的坑。
1. 故障表现与定位思路
1.1 控制面组件到底是怎么跑起来的
很多人会把"kube-apiserver、kube-controller-manager、kube-scheduler"理解成和 nginx、mysql 一样的系统服务,想着服务挂了就systemctl restart kube-apiserver。但 kubeadm 集群里根本没有 kube-apiserver.service 这个 unit,它们都是静态 Pod。
静态 Pod 的 YAML 文件放置在/etc/kubernetes/manifests目录下,由 kubelet 进程负责监听并创建。kubelet 健康时,会周期性扫描这个目录,把 YAML 提交给容器运行时。这意味着控制面的生命周期完全绑在 kubelet 上:kubelet 起不来,apiserver 就根本不存在。
而 kubelet 要创建容器,又依赖容器运行时。当你使用 Docker 作为运行时,且集群版本在 1.24 及以上时,kubelet 不能直接连 Docker 的 unix socket 调用容器接口,必须经过 cri-dockerd 这个适配层。cri-dockerd 负责把 kubelet 发出的 CRI 调用转换成 Docker Engine API。
于是启动链路就变成了:
systemd 启动 docker → systemd 启动 cri-dockerd → systemd 启动 kubelet → kubelet 连接 /var/run/cri-dockerd.sock → kubelet 读取 /etc/kubernetes/manifests → 拉起 etcd、apiserver、controller-manager、scheduler这条链路上只要有一环在开机时没有就绪,后面所有环节都会卡住。
1.2 重启后的两条关键排查线索
服务器重启后,问题表现通常有两种形态。第一种是kubectl get nodes直接报连接超时或拒绝,说明 apiserver 端口没有监听。第二种是能连上大致集群但 master 节点显示 NotReady,说明某个控制面容器处于 CrashLoopBackOff 或 ContainerCreating。
我一般不会先去翻 kubectl,而是先按顺序检查三个核心服务:
systemctl status docker --no-pager systemctl status cri-dockerd --no-pager systemctl status kubelet --no-pager然后确认两个 socket 文件是否真实存在:
ls -l /var/run/docker.sock /var/run/cri-dockerd.sock如果cri-dockerd.sock不存在,那 kubelet 再怎么重试也连不上运行时,apiserver 必然起不来。这一步能帮你快速区分问题出在运行时这一层,还是出在 kubelet 与静态 Pod 这一层。
2. 控制面不能自恢复的三个根因
2.1 服务自启没配全:docker、cri-dockerd、kubelet
第一个根因看起来简单,但我在很多环境里都见过。kubeadm 安装文档通常只提醒你systemctl enable kubelet,docker 是发行版包管理器装的,默认大概率已经 enable,但 cri-dockerd 如果是从 GitHub release 下载 tar 包手动部署的,service 文件不一定存在,更不一定 enable。
自启检查三条命令:
systemctl is-enabled docker systemctl is-enabled cri-dockerd systemctl is-enabled kubelet输出只要不是enabled,就补上:
systemctl enable docker systemctl enable cri-dockerd systemctl enable kubelet systemctl daemon-reload还有一个容易被忽略的环节:docker 的 socket 激活。某些发行版上 docker.service 依赖 docker.socket,如果 socket 没有 enable,Docker daemon 可能不会被唤醒。建议一起检查:
systemctl is-enabled docker.socket2.2 systemd 依赖缺失,启动顺序整个乱掉
即使三个服务全部 enable,systemd 在开机时也是并行拉起服务。kubelet 完全有可能比 cri-dockerd 先启动,cri-dockerd 也可能比 docker 先启动。kubeadm 生成的 kubelet.service 文件默认内容非常"干净":
[Unit] Description=kubelet: The Kubernetes Node Agent Documentation=https://kubernetes.io/docs/ Wants=network-online.target After=network-online.target里面没有After=cri-dockerd.service,也没有Requires=cri-dockerd.service。cri-dockerd 的 service 文件如果是从某份文档里复制来的,依赖配置也可能不完整。结果就是开机时容器运行时的 unix socket 还没有创建,kubelet 就试图连接,不断报 connection refused。
我排查时喜欢看开机以来的日志,把时间顺序拉出来:
journalctl -b -u kubelet --no-pager | head -50 journalctl -b -u cri-dockerd --no-pager | head -50如果 kubelet 的第一条连接错误发生在 cri-dockerd 的 "Started CRI Interface" 之前,基本可以断定就是启动顺序问题。
2.3 静态 Pod 机制把命脉全绑在 kubelet 上
第三个根因是架构层面的。apiserver、controller-manager、scheduler 没有独立的守护进程,所以你不能像修普通服务那样单独拉起它们。它们被创建的前提是 kubelet 已经成功连接容器运行时,并且/etc/kubernetes/manifests目录下有完整的 YAML 文件。
如果异常关机导致 etcd 的 YAML 文件损坏,或者/etc/kubernetes目录权限被改乱,kubelet 即使活着也无法正确创建静态 Pod。这里有一个我自己的检查顺序:
ls -l /etc/kubernetes/manifests/ cat /etc/kubernetes/manifests/kube-apiserver.yaml | head -20确认文件存在、权限为 644 或至少可读,并且 YAML 结构没有被截断。静态 Pod 的 YAML 丢失或损坏时,kubelet 不会报特别醒目的错误,只会静默地跳过创建,这比显式报错更容易误导人。
3. 完整修复过程:让控制面重启后自动恢复
3.1 第一刀:把三件套全部设为开机自启
修复的第一步是把 docker、cri-dockerd、kubelet 全部设为开机自启。这一步能解决"什么都没启动"的问题。
systemctl enable docker systemctl enable cri-dockerd systemctl enable kubelet systemctl daemon-reload确认结果:
systemctl is-enabled docker cri-dockerd kubelet如果某台机器之前被人为关闭过服务自启,这里会一并还原。我还会顺手确认network-online.target的状态,因为 kubelet 的 service 依赖它,而某些系统上这个 target 可能没有 enabled:
systemctl is-enabled network-online.target3.2 第二刀:给 cri-dockerd 和 kubelet 补齐依赖
自启解决"要不要启动",依赖解决"谁先谁后"。找到 cri-dockerd 的 service 文件路径:
systemctl cat cri-dockerd通常位于/etc/systemd/system/cri-dockerd.service或/usr/lib/systemd/system/cri-dockerd.service,修改后内容如下:
[Unit] Description=CRI Interface for Docker Application Container Engine After=network-online.target docker.service Wants=network-online.target Requires=docker.service [Service] Type=notify ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint fd:// Restart=always RestartSec=10 StartLimitInterval=0 [Install] WantedBy=multi-user.target关键配置是After=docker.service和Requires=docker.service。After控制顺序,Requires控制硬依赖:如果 docker 启动失败,cri-dockerd 也不会启动。StartLimitInterval=0是为了避免反复重启触发 systemd 失败上限。
再看 kubelet.service,同样补充 cri-dockerd 依赖:
[Unit] Description=kubelet: The Kubernetes Node Agent Documentation=https://kubernetes.io/docs/ Wants=network-online.target After=network-online.target cri-dockerd.service Requires=cri-dockerd.service StartLimitInterval=0 [Service] ExecStart=/usr/bin/kubelet Restart=always RestartSec=10 StartLimitInterval=0 [Install] WantedBy=multi-user.target修改完以后执行:
systemctl daemon-reload这里提醒一句:Requires搭配After才是完整表达。只写After不写Requires,只能保证顺序,不能保证 docker 失败时阻止 cri-dockerd 启动;只写Requires不写After,docker 可能还在启动中 cri-dockerd 就开始连接了。如果你用的是容器化部署的 kubelet,操作方式会不同,但 kubeadm 场景下这套写法是稳妥的。
3.3 第三刀:手动拉起一轮做全局验证
如果当前集群已经处于起不来的状态,改完配置后不建议直接systemctl restart kubelet。要按依赖顺序从底层开始拉起:
systemctl start docker systemctl start cri-dockerd systemctl start kubelet每启动一个环节停一下,确认状态:
systemctl status docker --no-pager | head -20 systemctl status cri-dockerd --no-pager | head -20 systemctl status kubelet --no-pager | head -20kubelet 起来后,静态 Pod 创建需要一段时间。等待 30 秒到 2 分钟,然后查看容器:
crictl pods --namespace kube-system如果你没装 crictl,也可以用 docker CLI 过滤:
docker ps --filter label=io.kubernetes.pod.name --format "table {{.Names}}\t{{.Status}}"正常情况下应该依次看到 etcd、kube-apiserver、kube-controller-manager、kube-scheduler 的容器。如果 etcd 未就绪,apiserver 会一直等待连接 2379/tcp;如果 apiserver 未就绪,controller-manager 和 scheduler 即使被创建也会不断重试连接。
3.4 验证下次重启不会再跪
手动恢复之后,我习惯做一次"演练式验证"。这是我认为整套修复中最重要的一步:
reboot等两三分钟重新登录,依次检查:
systemctl is-active docker cri-dockerd kubelet docker ps --filter label=io.kubernetes.pod.name | head kubectl get nodes这一次要确认在没有人工干预的情况下,控制面自动恢复到 Ready 状态。如果还不行,再用journalctl -b看本次开机日志,比上次多了什么、少了什么。
4. 实操中容易忽视的坑:etcd、网络插件、时序
4.1 镜像缺失和数据盘没有挂载
控制面组件的容器镜像通常在初始化时已经拉到本地,但离线环境、镜像仓库地址变更、或者清理脚本误删镜像,都可能导致重启后 kubelet 无法创建容器。检查方式:
docker images | grep kube-apiserver docker images | grep etcd如果镜像缺失,需要手动拉取对应版本。这一步不起眼,但我在排查"apiserver 一直 ContainerCreating"时遇到过不止一次。
另一个坑是 etcd 数据目录。kubeadm 默认把数据放在/var/lib/etcd,但有人会把数据迁到独立数据盘。重启后挂载盘没有自动挂载,etcd 容器创建失败,apiserver 也起不来。检查方式:
mount | grep /var/lib/etcd如果挂载点不对,修复/etc/fstab后重新挂载,再重启 kubelet。这类问题表面上是 apiserver 起不来,根源却在存储层,不挂数据盘之前怎么重启都是白费。
4.2 kubelet 日志里的几类典型错误
排查控制面无法恢复时,我最常用的是这条命令:
journalctl -u kubelet --since "10 minutes ago" --no-pager | grep -i error | tail -30常见错误可以归成几类,我用一个速查表说明:
| 日志中的关键词 | 大概率原因 | 优先排查动作 |
|---|---|---|
| connection refused to /var/run/cri-dockerd.sock | cri-dockerd 没就绪或没启动 | 检查 docker 与 cri-dockerd 状态 |
| Container runtime is down | kubelet 与运行时连接断开 | 按顺序重启 docker 与 cri-dockerd |
| Failed to create pod sandbox | cri-dockerd 与 Docker 通信异常 | 查看 cri-dockerd 日志 |
| connection refused to 2379 | etcd 未就绪 | 检查 etcd 容器和 /var/lib/etcd |
| failed to ensure lease exists | kubelet 与 apiserver 通信异常 | 确认 apiserver 容器、证书、网络 |
如果错误集中在连接拒绝,先按依赖顺序重启;如果错误集中在镜像拉取,赶紧去查镜像;如果错误集中在权限,重点检查/etc/kubernetes目录和 kubelet 运行用户。
4.3 网络插件不恢复,node 永远 NotReady
控制面三件套都恢复后,如果kubectl get nodes显示 NotReady,问题往往不在控制面,而在网络插件。自建集群常用的 flannel 或 calico 以 DaemonSet 方式运行,控制面挂掉期间,这些 Pod 会被反复重建,恢复周期可能很长。
排查网络插件 Pod 的日志:
kubectl logs -n kube-flannel ds/kube-flannel --tail=20如果插件配置里的 apiserver 地址、token、或节点网络接口名有问题,会出现频繁 CrashLoopBackOff。网络插件不恢复,就算 apiserver 活着,node 的 condition 依然是 False,业务 Pod 也创建不出来。所以我把这个排查放进"控制面自动恢复"的检查单里。
4.4 时间偏移与会话过期的隐性坑
虚拟机上恢复过快照的节点,开机后系统时间可能偏差很大。etcd 对时钟漂移非常敏感,apiserver 与 etcd、kubelet 与 apiserver 之间的证书校验也依赖时间窗口。时间偏移会导致 TLS 证书验证失败、令牌失效之类让人摸不着头脑的报错。
排查方法:
date timedatectl status如果时间偏差大,先配置 chronyd 或 systemd-timesyncd 同步时间,再重启 kubelet 和相关容器。这个问题在物理机上少见,但我在虚拟化环境里踩到过,而且症状和"apiserver 无法自动恢复"完全一致。
5. 一套可复用的自愈检查脚本
与其每次重启后手动敲命令,不如把检查逻辑写成一个脚本,放在 master 节点上开机自动执行。下面是我在实际环境里使用的简化版本:
#!/bin/bash # 作用:重启后检查 docker/cri-dockerd/kubelet 和控制面静态Pod状态 set -e for svc in docker cri-dockerd kubelet; do if systemctl is-enabled "$svc" >/dev/null 2>&1; then echo "[OK] $svc enabled" else echo "[WARN] $svc NOT enabled, enable it now" systemctl enable "$svc" fi if systemctl is-active "$svc" >/dev/null 2>&1; then echo "[OK] $svc active" else echo "[WARN] $svc inactive, starting" systemctl start "$svc" fi sleep 2 done for sock in /var/run/docker.sock /var/run/cri-dockerd.sock; do if [ -S "$sock" ]; then echo "[OK] socket $sock exists" else echo "[ERR] socket $sock missing" fi done echo "static pod yaml files:" ls -l /etc/kubernetes/manifests/这个脚本解决不了所有问题,但能把重启后最常见的三件事一次确认清楚:服务是否自启、服务是否存活、socket 是否就位。如果这三样都正常,控制面组件绝大多数情况下能自己拉起来;如果还不正常,就去查 etcd 和网络插件。
最后分享一个我踩过多次坑后换来的习惯:每次对 master 节点做内核升级、docker 升级、挂载盘调整之前,先对/etc/kubernetes/manifests目录做一次备份,再对/var/lib/etcd做快照备份。控制面恢复这件事,七分靠 systemd 依赖,三分靠关键时刻的手动介入,而如果你连静态 Pod 的 YAML 都丢了,再完美的启动顺序也无济于事。