Kubernetes 6443连接拒绝:kube-apiserver控制平面排查

发布时间:2026/10/27 5:14:18
Kubernetes 6443连接拒绝:kube-apiserver控制平面排查

1. 先把这行报错读薄:它到底在说什么

The connection to the server 10.0.0.21:6443 was refused - did you specify the right host or port?这行字我在过去几年里见过不下一百次,每次的根因都不一样,但报错的字面意思其实非常克制:kubectl 拿着 kubeconfig 里的地址去连 6443 端口,TCP 握手阶段就被对方一脚踢了回来,内核返回ECONNREFUSED。说白了不是"连得慢",也不是"没有权限",而是"这个门牌号上根本没有人在听"。很多人第一反应是去查网络、查防火墙、查云厂商安全组,查了半天发现全通,最后才发现是控制平面自己没起来。这个方向的偏差,本质上是对 6443 这个端口背后是谁在服务没有建立清晰的认识。

6443 是 kube-apiserver 的 secure port,是整个集群唯一的入口。你敲的每一条kubectl get、每一次kubelet上报节点状态、每一个 controller 的 watch 请求,最终都落到这个端口上。它一旦没人监听,整个集群对外就是"死的"——哪怕节点上的业务 Pod 还在欢快地跑着,哪怕 etcd 里的数据一个字节都没丢。所以这个报错不是网络故障的同义词,它是一个症状,指向的是"apiserver 这个进程此刻不在"或者"它在了但你够不着"。这两件事的排查路径完全不同,先把它们分开,能省掉一大半无用功。

我习惯用一个很土但极其有效的类比:6443 就像一栋楼的大门。报错说"门打不开",可能是一楼大厅压根没人上班(apiserver 没起来),可能是门卫把你拦在外面(防火墙 REJECT),也可能是你走错了楼(kubeconfig 里的 IP 写错了)。这三种情况的现场表现有细微差别,而refused这个词恰恰是最"诚实"的一种——它明确告诉你包到了、对方回了 RST,说明路由是通的,问题出在门本身。

1.1 refused、timeout、no route to host 是三件不同的事

新手最容易犯的错,是把所有"连不上"都当成一回事。实际上这三种报错对应的排查方向差得很远,先看一张对照表:

报错关键词底层发生了什么典型根因第一优先排查
connection refusedTCP SYN 到达目标,对方回 RST端口没监听 / 被 REJECT / 进程崩了目标机上ss -lntp | grep 6443
i/o timeoutSYN 发出后没有任何回应,超时防火墙 DROP / 路由不通 / 安全组丢弃中间链路与安全组策略
no route to host收到 ICMP 不可达目标机宕机 / 网段不通 / ARP 失败目标机是否存活、网卡配置
x509: certificate is valid for ...TLS 握手阶段证书校验失败节点 IP 变更、证书 SAN 不匹配证书与 kubeconfig 里的地址
Unauthorized/Forbidden已经连上了,但认证授权失败证书过期、RBAC 配置kubeadm certs check-expiration

这张表里最值得说的是最后两行。很多人在看到refused之后,凭记忆去搜"k8s 连不上 apiserver",搜到的却是证书过期的文章,然后跑去 renew 证书,折腾一圈发现没用——因为证书问题根本不会给你refused,它会给你x509开头的一长串。报错文本的第一个词,基本就决定了你该敲的第一条命令,这个判断习惯比记住任何一条命令都值钱。

提示:refused说明网络层大概率是通的。如果你在 A 机器上telnet B 6443得到Connection refused,那就别再怀疑交换机、路由表、云网络了,直接去 B 机器上看进程。

1.2 一次 kubectl 请求要穿过哪几层

要把排查做得有条理,得先知道一条kubectl get nodes从敲下回车到出结果,中间经过了哪些关卡。我把它拆成五层,从下往上分别是:

第一层是物理与网络,包括网卡、路由、安全组、宿主机防火墙。这一层出问题给的是timeout或no route to host,偶尔因为防火墙配了 REJECT 也会给refused,这是唯一会干扰判断的情况。第二层是端口监听,内核的 socket 表里有没有0.0.0.0:6443或者特定IP:6443。第三层是进程存活,也就是 kube-apiserver 这个容器有没有在跑。第四层是依赖可用,apiserver 启动时要连 etcd,etcd 挂了它自己会反复重启。第五层才是认证授权,kubeconfig 里的证书、token、context 对不对。

refused这个报错,绝大多数情况下卡在第二层和第三层。所以我的排查顺序永远是:先在目标机上确认端口有没有人听,再确认是谁该听这个端口、它为什么没听。而不是反过来,从 kubectl 这头一路 ping 过去。因为 apiserver 如果压根没起来,你在客户端做再多网络测试都是白费。

这里还有一个非常容易被忽略的细节:kubeconfig 里的 server 地址到底是哪个。集群刚搭好的时候,/etc/kubernetes/admin.conf里的 server 可能是https://10.0.0.21:6443,而你本机~/.kube/config里可能抄的是另一个网段的地址,或者抄的是已经下线的旧机器。我见过一次特别典型的:某开发者把测试集群的 kubeconfig 拷到笔记本上,改了半天的防火墙,最后发现kubectl config view里那行 server 指向的是一台早就被回收的机器。先看 kubeconfig,再看网络,这个顺序能救你很多时间。

2. 排查顺序设计:为什么我坚持从 kubelet 查起

大部分人查这个问题的直觉是"apiserver 挂了,那就去重启 apiserver"。这个思路在传统部署里是对的,在 kubeadm 部署的集群里会撞墙——因为你根本没有systemctl restart kube-apiserver这个服务。控制平面的四个核心组件(apiserver、controller-manager、scheduler、etcd)在 kubeadm 体系里全是静态 Pod,它们的生命周期由 kubelet 直接管理,不走 Deployment、不走 ReplicaSet,甚至连 ReplicationController 都不走。kubelet 启动时会扫描/etc/kubernetes/manifests/目录,把里面的 YAML 文件翻译成容器直接拉起来。

这个设计带来一个非常反直觉的结论:apiserver 挂了,第一嫌疑人是 kubelet,不是 apiserver 自己。因为 apiserver 的"启动脚本"就是 kubelet。kubelet 没起来,manifests 目录里的东西一个都不会被实例化;kubelet 起来了但容器运行时没起来,kubelet 也只会干着急;容器运行时起来了但拉镜像失败、挂载点不存在、cgroup 配置错误,静态 Pod 同样起不来。这就是为什么我在前面强调"排查起点"——顺着 kubelet 往下摸,路径最短。

还有一层更隐蔽的依赖:kubelet 自己也需要 apiserver 才能正常工作(上报节点状态、拉取 PodSpec),但它启动的早期阶段并不依赖 apiserver。kubelet 可以先把自己的静态 Pod 拉起来,等 apiserver 起来之后再开始和它通信。所以这里不存在"鸡生蛋"的死锁,只要 kubelet 和容器运行时的链路是通的,控制平面就有机会自愈。

2.1 静态 Pod 这个设计决定了排查起点

我把控制平面节点的启动链路画成文字版的层级关系,方便对照着查:

  • 内核与 systemd 正常 → 这是地基,systemctl status能看出大部分问题
  • containerd(或其它运行时)已启动 →systemctl status containerd
  • kubelet 已启动且 enable →systemctl status kubelet,注意enabled还是disabled
  • kubelet 读取/etc/kubernetes/manifests/→ 目录里应该有 4 个 YAML
  • kubelet 通过 CRI 让运行时创建容器 →crictl ps -a | grep kube-apiserver
  • apiserver 容器内进程启动 → 失败的话看crictl logs或journalctl -u kubelet
  • apiserver 连接 etcd(2379)→ etcd 容器必须在跑
  • apiserver 监听 6443 →ss -lntp | grep 6443

这条链路上任何一环断了,你在客户端看到的都是同一句refused。区别只在于你在服务端能多快定位到是哪一环。

有一点必须说清楚:kubectl报错并不代表集群塌了。如果只是你本地网络到控制面不通,集群内部的业务 Pod、Service、Ingress 可能完全正常。反过来,如果控制面挂了,已经在运行的业务 Pod 也不会立刻挂掉——kubelet 会继续维持容器运行,只是没法感知新的调度指令。这个特性在故障时是好事,它给了你足够的修复窗口,不用一边救火一边担心业务全崩。

注意:kubelet 服务如果是disabled状态,机器重启后不会自启,而 containerd 往往是enabled的。这就造成一个很容易误判的现象:容器运行时活着,但控制平面没了。这个组合我在真实环境里遇到过不止一次,排查时先看systemctl is-enabled kubelet。

2.2 一条从客户端到服务端的排查路径

我把这套排查固化成了一个固定流程,按顺序执行,基本不会走回头路。整个过程分两段:客户端侧确认"我连的是谁",服务端侧确认"谁该在听"。

客户端侧先做三件事:确认 kubeconfig 指向的地址和端口、确认能否到达目标网络、确认目标端口是否有响应。命令很简单:

# 1. 看当前上下文和 server 地址 kubectl config view --minify | grep server # 2. 看有几个 context,会不会切错了集群 kubectl config get-contexts # 3. 测端口连通性(nc 比 telnet 更干净) nc -zv 10.0.0.21 6443

nc -zv的输出只有三行,但信息量很足:succeeded说明端口通,直接跳到认证问题;Connection refused说明目标机活着但端口没监听;timed out说明包被丢在路上。这三个结果对应三条完全不同的后续动作。

服务端侧则按前面那条链路自上而下查。我的习惯是先一条命令把关键状态一次性拉出来:

systemctl is-active containerd kubelet && \ ss -lntp | grep -E '6443|2379|10250' && \ crictl ps --name kube-apiserver

这三行里任何一行返回空或者报错,问题范围就基本锁定了。containerd和kubelet都是active,6443没有监听,crictl ps看不到 apiserver 容器——那就是 kubelet 在创建静态 Pod 时失败了,去看journalctl -u kubelet。如果6443有监听但客户端仍然refused,那就要怀疑 bind 地址绑错了网卡,或者中间有防火墙规则。

3. 动手实操:一次完整的现场排查记录

下面这段是我记忆比较深的一次经历,环境是某开发者自己维护的一套三节点测试集群,版本是近两年的某个稳定版,部署方式是 kubeadm。现象是早上到工位kubectl get nodes直接甩出refused,前一天晚上还好好的,中间没有人动过配置——至少当事人是这么说的。整个排查花了二十多分钟,过程我尽量还原,包括走过的弯路。

3.1 第一轮:确认 apiserver 到底有没有在跑

第一反应是回到控制平面节点,一条命令看端口:

ss -lntp | grep 6443

输出是空的。没有LISTEN,没有kube-apiserver进程。这一步直接排除了"防火墙把包拦了"的可能性——端口压根没开,客户端能收到 RST 再正常不过。接着看静态 Pod:

crictl ps -a --name kube-apiserver

输出里有一个状态是Exited的容器,退出码 255。这说明 kubelet 至少曾经尝试过拉起 apiserver,但容器启动后没多久就挂了。看日志:

crictl logs <容器ID> 2>&1 | tail -50

日志最后几行是连接 etcd 超时。到这里线索就转移了:apiserver 不是"没起来",是"起来了但连不上 etcd,于是自己退出,kubelet 反复重启它,形成 CrashLoop"。而 etcd 连不上,可能是 etcd 自己挂了,也可能是磁盘、证书、网络的问题。

回头看ss的输出再确认一遍 etcd:

ss -lntp | grep 2379

同样为空。crictl ps -a --name etcd显示容器在反复重启。crictl logs里刷屏的是disk space相关的错误。到这一步答案基本出来了:

df -h /var/lib/etcd

使用率 100%。某次调试留下的镜像和日志把这个分区塞满了,etcd 写 WAL 失败,直接拒绝启动,apiserver 跟着殉葬,最终在客户端表现为 6443 refused。

3.2 第二轮:清理磁盘与恢复 etcd

找到根因之后的处理反而简单,但有几个细节不能省。先确认是什么占满了空间:

du -sh /var/lib/containerd/* 2>/dev/null | sort -rh | head -5 du -sh /var/log/* 2>/dev/null | sort -rh | head -5

结果是 containerd 的镜像层和几个没做轮转的日志文件。清理动作分三步走:先删掉不用的镜像释放空间,再对日志做截断(注意是截断不是直接删,删了文件句柄还占着空间),最后确认 etcd 的数据目录没有被误伤。

# 清理未使用的镜像,这一步最安全 crictl rmi --prune # 日志截断,不要 rm(进程还持有文件句柄) truncate -s 0 /var/log/pods/*/*/*.log # 确认空间回来了 df -h /var/lib/etcd

空间腾出来之后,etcd 容器会由 kubelet 自动重建,不需要手动干预。等它健康起来(ss -lntp | grep 2379有监听),apiserver 也会跟着起来,因为这个重启是 kubelet 的 backoff 机制在自动重试。整个过程大概等了四十秒。最后验证:

ss -lntp | grep 6443 curl -k https://127.0.0.1:6443/healthz

healthz返回ok,客户端kubectl get nodes恢复正常。

这个案例里有个值得记住的点:apiserver 容器退出的日志,比 kubelet 的日志更直接。很多人只看journalctl -u kubelet,看到一堆SyncLoop相关的信息就晕了,其实真正的原因在容器自己的 stdout 里。crictl logs和journalctl -u kubelet | grep -i error一起用,效率高很多。

3.3 第三轮:如果 apiserver 容器都不存在怎么办

上面那次是容器存在但反复重启。还有一种更彻底的情况:crictl ps -a里连 apiserver 容器的影子都没有,说明 kubelet 根本没去创建它。这时候问题一定在 kubelet 或更下层,去看 kubelet 的状态:

systemctl status kubelet --no-pager -l

如果看到failed或者反复activating,基本上就是 kubelet 自己启动失败了。再拉详细日志:

journalctl -u kubelet -n 200 --no-pager | grep -iE 'error|fail|swap|cgroup'

这里能捞到的典型错误有几类,我按出现频率排一下:

日志关键词含义处理方向
Running with swap on is not supported交换分区没关swapoff -a并注释 fstab
failed to run Kubelet: misconfiguration: kubelet cgroup drivercgroup 驱动不一致统一为 systemd
failed to load kubelet config file配置文件缺失或语法错检查/var/lib/kubelet/config.yaml
failed to get runtime...connection refused连不上容器运行时先修 containerd
node not found循环刷节点对象被删清理后重新 join

如果 kubelet 状态是inactive (dead)而且is-enabled返回disabled,那更简单——机器重启过,kubelet 压根没自启。这种在云主机上尤其常见,因为很多人搭完集群只start没enable。

systemctl enable --now kubelet

然后再观察静态 Pod 是否被拉起来。这一步之后如果还是不行,才需要往容器运行时和 manifests 目录上查。

3.4 第四轮:端口通了但仍然报 refused 的情况

还有一种相对少见但很坑的情况:服务端ss能看到 6443 在监听,但客户端依然refused。这时候要检查监听地址:

ss -lntp | grep 6443 # 输出可能是 127.0.0.1:6443 而不是 0.0.0.0:6443

如果绑的是127.0.0.1,那么从本机kubectl是通的,从别的机器过来必然是refused。这个通常是因为kube-apiserver.yaml里被加了--bind-address=127.0.0.1,或者是通过 kubeadm 的--apiserver-bind-address参数指定的。修复方式就是改回0.0.0.0或者明确的网卡地址,然后等 kubelet 检测到 manifests 变更自动重建(一般十几秒)。

还有一种情况是端口被别的进程占了。kube-apiserver 启动时如果发现 6443 被占用,行为在不同版本下不完全一致,有的会直接退出,有的会尝试退到 6444。这就造成一个迷惑现象:ss里有 6443 在监听,但连上去不是 apiserver。查一下是谁:

ss -lntp | grep 6443 # 最后一列会显示进程名,如果是个陌生进程,直接处理它

4. 七类高频根因逐个拆解与修复

前面那套流程解决的是"怎么找",这一节讲"找到了之后怎么修"。我把这些年遇到过的情况归成七类,基本覆盖了 90% 以上的场景。每一类我都会说清楚触发条件和修复动作,方便你直接对号入座。

4.1 kubelet 起不来:swap、cgroup 与配置文件

swap 是最经典的一个。kubelet 默认要求关闭交换分区,因为 Kubernetes 的调度和资源计算模型假设内存是不可超卖的,swap 会让这个假设失效。机器重启后如果/etc/fstab里的 swap 行没注释掉,交换分区会自动挂上,kubelet 随即拒绝启动。

# 临时关闭 swapoff -a # 永久关闭,注释掉 fstab 里的 swap 行 sed -i '/ swap / s/^/#/' /etc/fstab # 验证 free -h

cgroup 驱动不一致是第二个高频问题。containerd 默认用cgroupfs,而 kubelet 在新版本里推荐systemd。两边不一致时,kubelet 会直接报错退出,或者出现"Pod 能起来但资源限制不生效"的诡异现象。修复分两步,改 containerd 配置并重启:

# /etc/containerd/config.toml [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true systemctl restart containerd

改完之后注意 kubelet 那边/var/lib/kubelet/config.yaml里的cgroupDriver也要是systemd,两边必须对齐,单改一边等于没改。

配置文件本身出问题的情况也不少,比如/var/lib/kubelet/config.yaml被误删、被误改,或者kubeadm join过程中断留下半截配置。这类问题的表现是 kubelet 报failed to load kubelet config file,通常用kubeadm reset重来一遍比手工修更快,前提是你接受这个节点的状态被清空。

4.2 容器运行时没起来,kubelet 只能干瞪眼

systemctl status kubelet显示 active,但crictl ps报connection refused,这是运行时的问题。kubelet 通过 CRI 接口和运行时通信,默认走 containerd 的 socket/run/containerd/containerd.sock。运行时挂了,kubelet 会一直重试,日志里刷failed to get runtime status。

排查顺序是:先看服务状态,再看 socket 文件在不在,最后看运行时自己的日志。

systemctl status containerd --no-pager ls -l /run/containerd/containerd.sock journalctl -u containerd -n 100 --no-pager

containerd 起不来的常见原因包括:配置文件语法错误(改了config.toml没验证)、存储驱动问题、/var/lib/containerd权限错乱、磁盘满。其中磁盘满是最隐蔽的,因为 containerd 不会明确告诉你"磁盘满了",只会报一些看起来无关的错。

提示:crictl需要配置 endpoint,默认会读/etc/crictl.yaml。如果这个文件被改过或者没配,crictl本身就会报连接错误,容易被误判成运行时挂了。先crictl info确认一下。

4.3 etcd 的磁盘、数据与证书

etcd 是控制平面的数据底座,它一出问题,apiserver 必然跟着出问题,只是报错形式不同。etcd 几个高发的故障点:磁盘空间不足、数据目录权限错、WAL 文件损坏、证书过期、时钟偏移。

磁盘问题最好查,df -h一眼就看出来。但要注意不只看空间,还要看 inode:

df -h /var/lib/etcd df -i /var/lib/etcd

inode 耗尽的情况在大量小文件的场景下会出现,表现和空间耗尽很像但df -h显示还有余量,容易看漏。

数据损坏的处理要谨慎得多。etcd 启动失败时日志里会明确写corrupt或者invalid database。这时候第一件事不是删数据,而是先备份:

cp -a /var/lib/etcd /var/lib/etcd.bak.$(date +%s)

如果是单节点测试集群,最省事的方案是清空数据目录让它重新初始化,代价是集群状态全丢,需要重新kubeadm init。如果是有备份的集群,用etcdctl snapshot restore恢复。生产环境这两个动作都要慎重,我个人的习惯是:任何对 etcd 数据目录的写操作之前,先做一次冷备份,哪怕磁盘已经满了也要腾出空间来备份。

证书过期是另一个大坑。etcd 的证书默认有效期是一年(某些版本是更长),过期后 apiserver 连不上 etcd,表现同样是 6443 refused。检查命令:

kubeadm certs check-expiration openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -noout -dates

续期用kubeadm certs renew all,续完之后需要重启控制平面组件让新证书生效。因为它们是静态 Pod,重启方式是让 kubelet 重建容器——把 manifest 临时移出目录再移回来,或者直接重启 kubelet:

kubeadm certs renew all mv /etc/kubernetes/manifests /etc/kubernetes/manifests.bak sleep 20 mv /etc/kubernetes/manifests.bak /etc/kubernetes/manifests

这个"移出去再移回来"的手法,本质上是给 kubelet 一个明确的信号:目录内容变了,重新扫描。比systemctl restart kubelet温和一些,因为重启 kubelet 会影响到这个节点上所有 Pod 的同步逻辑。

4.4 机器 IP 变了,kubeconfig 和证书都要跟着改

云主机被关机再开机之后 IP 变了,这在按量计费的测试环境里是家常便饭。IP 一变,两个地方立刻失效:/etc/kubernetes/admin.conf里的 server 地址,以及 apiserver 证书里的 SAN 列表。

前者改起来简单,直接编辑 kubeconfig:

kubectl config set-cluster kubernetes --server=https://新IP:6443

后者需要重新生成证书,kubeadm init phase certs apiserver可以只重建 apiserver 证书,但要先把旧证书挪走。注意这一步之后所有依赖旧证书的客户端配置都要同步更新,包括 kubelet 的 kubeconfig。

还有一种"IP 没变但地址写错"的情况,就是 kubeconfig 里写的是内网 IP,而你从外网连。这种情况下的报错通常是timeout而不是refused,因为包被丢在路上了。但如果中间有设备回了 RST,也会看到refused,所以不能只凭报错下结论,nc -zv测一下更踏实。

4.5 防火墙与安全组:最容易被冤枉的一层

说实话,在refused这个报错上,防火墙是嫌疑最大但定罪最少的。因为 firewalld 的默认拒绝策略多数是 DROP(表现为 timeout),配成 REJECT 才会给 refused。不过还是要把检查动作列出来,因为确实存在。

# firewalld firewall-cmd --state firewall-cmd --list-all # ufw ufw status verbose # iptables 直接看(注意 k8s 自己会写大量规则,别乱清) iptables -L INPUT -n --line-numbers

kubeadm 部署时通常会自动放行必要端口,但如果你后来手动开过防火墙、装过安全软件、或者云厂商的安全组被改过,就可能把 6443 挡在外面。云上环境还要额外看一层安全组规则,这个在机器内部是看不到的。

这里有个实操建议:排查时先关掉防火墙做一次对照测试,确认是不是它的问题,再决定要不要去精细化配置规则。systemctl stop firewalld之后立刻测,测完不管结果如何都记得恢复,别把安全策略留在关闭状态。

4.6 资源压力:内存、conntrack 与 PID

资源类问题最阴险,因为它不给你明确的报错,只是让进程莫名其妙地起不来或者被杀。几个重点看的地方:

free -m dmesg -T | grep -i 'out of memory' cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max

OOM 是最常见的。apiserver 和 etcd 都是内存大户,小内存机器(比如 2G)跑控制平面很容易在负载上来时被内核杀掉。dmesg里能直接看到Killed process ... kube-apiserver这样的记录。这种情况下的处理不是简单重启,而是要么加内存,要么限制其它进程的资源占用。

conntrack 表满会导致新连接无法建立,表现可能是 refused 也可能是 timeout。node 数量多、Service 数量大的集群容易出现这个问题,调大nf_conntrack_max可以缓解,但根子上还是要看是不是有异常的连接数增长。

PID 耗尽相对少见,但容器密集的节点上会遇到。ps -eLf | wc -l看一下线程数,和kernel.pid_max对比。这类问题的处理思路是清理僵尸进程、调整内核参数。

4.7 manifests 被改坏:一个字符就能让控制平面消失

/etc/kubernetes/manifests/下的四个 YAML 是控制平面的"源代码",任何一处语法错误都会让对应的组件起不来。而 YAML 对缩进极其敏感,一个 tab 和一个空格的区别,就能让 kubelet 解析失败。

出问题时 kubelet 日志里会有明确提示,比如failed to parse或者cannot unmarshal。最快的验证方式是直接看目录和文件状态:

ls -l /etc/kubernetes/manifests/ # 用任意 yaml 解析器验证语法 python3 -c "import yaml,sys; yaml.safe_load(open('/etc/kubernetes/manifests/kube-apiserver.yaml'))"

改坏之后的恢复策略取决于你有没有备份。我的建议是每次改 manifest 之前先 cp 一份带时间戳的副本,这个习惯几乎零成本,但能省掉大量重建集群的时间。如果已经改坏了又没有备份,可以尝试从 kubeadm 的 ConfigMap 或者另一台控制平面节点上取回原始文件——多 master 的集群在这里优势明显。

5. 排查速查表与避坑清单

写到这里,主体方法论已经齐了。这一节我把它压缩成可以直接贴在显示器边上的形式,出问题时按图索骥,比从头回忆快得多。

5.1 症状到命令的对照表

现象优先执行观察重点
客户端 refusednc -zv <ip> 6443通/拒/超时,三选一
服务端无监听ss -lntp | grep 6443有没有 LISTEN
有监听但连不上看监听地址是 0.0.0.0 还是 127.0.0.1bind-address
容器不存在systemctl status kubeletactive / failed / disabled
容器反复重启crictl logs <id>最后 30 行
疑似 etcd 问题ss -lntp | grep 2379有无监听
疑似磁盘问题df -h加df -i两项都要看
疑似内存问题dmesg -T | grep -i oom有没有被杀记录
疑似证书问题kubeadm certs check-expiration剩余天数
机器重启过systemctl is-enabled kubelet containerd是否自启

这张表的用法是从上往下逐行排除,不要跳。因为上面的行成本低、信息量大,下面的行成本高、容易误判。我见过太多人一上来就去kubeadm certs renew,结果证书续了三遍问题还在,最后发现是 kubelet 没启动。

5.2 我踩过的几个坑

第一个坑:把 timeout 当成 refused 处理。报错文本只看了一眼"连不上"就开始查进程,实际上nc测出来是超时,根子在安全组。这个坑的教训是:永远用工具复现一次再下结论,不要凭印象。

第二个坑:改完配置不重启对应服务。改完 containerd 的config.toml忘了systemctl restart containerd,然后对着 kubelet 的日志查了半小时,越查越迷惑。现在我的习惯是改完任何配置文件,立刻在同一条命令里跟一个重启动作,不给遗忘留空间。

第三个坑:日志只看到第一屏。journalctl -u kubelet默认只显示最近的内容,但真正的原因可能在更早的位置,被后续的重试日志淹没了。用--since和grep组合能有效过滤:

journalctl -u kubelet --since "10 min ago" --no-pager | grep -iE 'error|fatal|failed'

第四个坑:在多集群环境里切错 context。本地~/.kube/config里堆了五六个集群的配置,kubectl默认用当前 context。报错之后第一件事应该是kubectl config current-context,确认自己连的是哪个集群。这个错误低级但高发,我自己也犯过。

第五个坑:直接删/var/lib/etcd。有一次图省事,觉得是测试集群就无所谓,结果删完之后发现集群里还有一些需要保留的配置,只能重建。从那以后我的原则是:删数据之前先 cp,哪怕只花十秒钟。

第六个坑:忽略时间同步。证书校验依赖系统时间,机器时间偏移过大时会出现各种奇怪的 TLS 错误。虽然它一般不给refused,但在排查证书问题时这是个必须先排除的干扰项。timedatectl status一眼就能看出来。

6. 把故障挡在发生之前:几个低成本习惯

排查能力再强也不如不出故障。这个报错背后的根因,绝大多数都是可以提前预防的,而且预防的成本远低于事后救火。

6.1 证书与磁盘的例行检查

证书过期是唯一一个"时间一到必然发生"的故障,没有任何随机性,所以完全可以用定期检查来消除。我的做法是在控制平面节点上放一个简单的检查脚本,输出剩余天数,超过阈值就提醒。核心就两行:

kubeadm certs check-expiration for f in /etc/kubernetes/pki/*.crt /etc/kubernetes/pki/etcd/*.crt; do echo "$f: $(openssl x509 -in $f -noout -enddate)" done

磁盘同理,但要多看一项 inode。控制平面节点的磁盘增长主要来自容器日志和镜像,所以把日志轮转配好是治本之策。kubelet 的containerLogMaxSize和containerLogMaxFiles两个参数值得花五分钟配置一下,能免掉大量后期清理工作。

6.2 重启演练与自启确认

"机器重启后集群起不来"是个非常常见的场景,但它完全可以提前发现。花十分钟做一次重启演练,看看控制平面能不能自己恢复,比事故现场临时抱佛脚强得多。演练之后重点确认三件事:kubelet和containerd都是enabled、swap 在 fstab 里被注释掉、/etc/kubernetes/manifests/目录完整。

systemctl is-enabled kubelet containerd grep -i swap /etc/fstab ls -l /etc/kubernetes/manifests/

这三条命令我建议放在集群搭建完成后的验证清单里,也放在每次变更之后的复查清单里。它们的执行成本是几秒钟,但能拦住的故障可能是几个小时。

还有一个容易忽略的点:云主机的"关机再开机"和"重启"在 IP 保留上行为不同。有些环境下关机后重新开机会分配新 IP,这会直接导致前面说的证书 SAN 不匹配。如果你的测试环境是这种计费模式,要么改用固定 IP,要么在开机后第一时间检查 kubeconfig 和证书。这个坑踩一次就够记一辈子。

我个人在实际操作中的体会是,refused这个报错看着吓人,但它其实是 k8s 所有报错里最"讲道理"的一个——它从不撒谎,端口没开就是没开,进程没起就是没起。真正让人头疼的是那些看起来连上了但行为诡异的故障。所以每次看到这行字,我反而会松一口气,因为它意味着问题范围是收敛的,只要按顺序把 kubelet、运行时、etcd、端口这几层过一遍,答案一定在其中某一层。把这几层的检查固化成肌肉记忆,比背下几十条命令有用得多。