作为一个常年跟Kubernetes 和 KubeSphere 打交道的人,我最怕看到的告警其实不是“节点离线”,而是控制台突然打不开、kubectl 敲什么都是connection refused。这种故障表面上看是网络问题,但排查到最后,十次里有六七次都不是网络本身的问题。KubeSphere 无法连接、API Server 崩溃,再加上磁盘满,这三个词凑在一起,基本就是一个非常典型的控制面雪崩事故。
这篇文章不打算写那种“官方文档复读机”,就结合我实际踩过的一个案例,把从Connection Refused到磁盘写满、再到 API Server 崩溃的完整链路、排查手法和恢复操作都捋一遍。如果你也管着 K8s 集群,这篇文章值得花十分钟读完,关键时候能救命。
1. 故障表象拆解:Connection Refused 到底在说什么
1.1 同样是连不上,refused 和 timeout 是两码事
遇到“无法连接”,第一步不是慌,是分清楚报错类型。很多人看到connection refused就以为是防火墙、安全组、负载均衡的问题,这是最常见的误判。
connection refused这个报错的意思是:TCP 请求到达了对端,但对端没有进程在监听这个端口,于是内核直接回了 RST。也就是说,网络链路大概率是通的,问题出在“端口后面根本没有活着的服务”。
而如果是timeout,说明数据包发出去就石沉大海了,可能被防火墙丢弃、路由不可达、负载均衡后端全挂,或者源和目标之间出现了网络黑洞。
拿 kubectl 来说,如果你执行:
kubectl get nodes报错是:
Unable to connect to the server: dial tcp 192.168.10.101:6443: connect: connection refused这就说明:你的客户端能到192.168.10.101,但是 6443 端口上已经没有 kube-apiserver 在监听了。
而 KubeSphere 控制台通常是靠 NodePort 30880 暴露的,如果控制台打不开,报错可能是502 Bad Gateway或者干脆拒绝连接。这时候先别去折腾 ingress、防火墙,先确认后端服务是不是已经挂了。
这里有个快速分层判断的技巧:
| 报错类型 | 排查方向 |
|---|---|
| connection refused | 进程未监听、服务崩溃、端口被占用 |
| timeout | 网络链路、防火墙、负载均衡后端状态 |
| 502/504 | 上游服务存在但不可用、服务崩溃或超时 |
| 证书错误 | 证书过期、CA 不匹配(K8s 组件间常见) |
1.2 KubeSphere 的“连接”不是只有一个入口
KubeSphere 的用户侧入口确实有三个层级,排查的时候别只盯一个。
第一层是浏览器访问的控制台入口,走 30880 NodePort,流量路径是:浏览器 -> NodeIP:30880 -> ks-console Service -> ks-apiserver -> Kubernetes API Server。
第二层是 kubectl 直连 API Server 的入口,走 6443,流量路径是:kubectl -> MasterIP:6443 -> kube-apiserver -> etcd。
第三层是在集群内访问的 Service ClusterIP,比如各命名空间里的服务通过 K8s Service DNS 名互相调用。
这三个入口如果同时失效,或者至少控制台和 kubectl 同时失效,那基本可以断定问题出在公共的“底座”,也就是 kube-apiserver 或者它后面的 etcd,而不是某一个上层服务的问题。
那次故障的现场就是这种状况:控制台打不开,kubectl 报connection refused,同时集群里其他业务也开始出现超时。当多个入口同时不正常,就别再纠结网络了,赶紧把视角转向控制面本身。
2. 从“端口不通”到定位 API Server 崩溃的排查链路
2.1 第一步:直接探活本机 API Server
既然公网和 NodePort 都不行,那就先绕过网络链路,直接在 Master 节点上看 API Server 到底还活着没有。
登录任意 Master 节点,先测试本机的 HTTPS 健康检查接口:
curl -k https://127.0.0.1:6443/healthz注意,这个链接是 HTTPS 的,-k是忽略证书校验。如果返回:
ok说明 kube-apiserver 进程在本机是活着的,那问题大概率出在负载均衡、浮点 IP 或者前置代理上。
但如果返回:
curl: (7) Failed to connect to 127.0.0.1 port 6443: Connection refused那就实锤了:6443 端口根本没有进程在监听,API Server 已经退出或处于 CrashLoopBackOff 状态。
当时我用ss -lntp | grep 6443时发现端口被释放,再用systemctl status kube-apiserver一看,服务处于failed状态,下面写着kube-apiserver.service: Main process exited, code=exited, status=1/FAILURE。
到了这一步,网络问题的嫌疑基本洗清了,接下来就是看进程为什么退出的问题。
2.2 第二步:翻系统日志找到崩溃原因
高版本的 K8s 集群如果在二进制部署模式下,kube-apiserver 是 systemd 管理的,日志直接进 journald。
排查命令:
journalctl -u kube-apiserver --since "30 minutes ago" -n 200这里强烈建议先看最后 100-200 行,重点找panic、fatal、error、disk、etcd这些关键词。如果日志滚得特别快,可以用:
journalctl -u kube-apiserver -f实时追一下。
在我经历的那次故障中,日志里反复出现这么几行:
storagebackend: etcd3: failed to watch ...: rpc error: code = ResourceExhausted desc = etcdserver: mvcc: database space exceeded以及:
etcdserver: server is likely overloaded还有更直接的:
error: failed to open file /var/lib/etcd/member/snap/db: no space left on device看到no space left on device的时候,我心里其实已经清楚了一半:这不是 API Server 自己写代码写崩了,是底层存储爆了,etcd 写不进去数据,进而把 API Server 拉下了水。
2.3 第三步:确认 etcd 健康状态和磁盘水位
Kubernetes 的所有状态都存放在 etcd 里,API Server 是 etcd 唯一的客户端(严格说是主要的)。如果 etcd 落不了盘,API Server 的所有读写请求都会失败,包括它自己最核心的 List-Watch 机制。
检查 etcd 服务:
systemctl status etcd或者直接用 etcdctl 探活:
ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/server.pem --key=/etc/etcd/ssl/server-key.pem endpoint health注意:不同集群的证书路径不一样,实际环境以你自己的部署为准。
与此同时,敲下这两个命令看水位:
df -h df -idf -h看的是容量占用,df -i看的是 inode 占用。我当时跑完df -h的结果是:
/dev/sda2 50G 50G 12K 100% /根分区 100% 满,etcd 数据目录如果在根分区下面,基本等于被判了死刑。
这里有个坑:df -i显示的 inode 满了也会导致写入失败,但df -h看着还有剩余空间。两者都要看,任何一项满都会产生连锁反应。
3. 磁盘满导致的 API Server 崩溃机制复盘
3.1 为什么磁盘满,先死的是 etcd 和 API Server
先说结论:磁盘满 -> etcd 无法写 WAL 和快照 -> API Server 读写 etcd 全部失败 -> API Server 探活失败、panic、被重启 -> 控制面瘫痪。
etcd 是一个强一致性的 KV 存储,它写数据的时候必须调用 fsync 把日志落盘。如果磁盘满了,fsync 就返回ENOSPC,etcd 会立刻把这次写入标记为失败,并且对客户端返回错误。
API Server 对 etcd 的模型基本是“所有操作都要过 etcd”,包括创建 Pod、更新 Deployment、发放证书、记录 Node 状态。etcd 一拒绝写,API Server 的很多核心循环就会出问题。最典型的是事件记录器的 Event 写不进去,这会导致kubectl describe查不到新事件,但其实 API Server 的 apiserver loop 已经开始畸形运行。
更致命的是 kubelet 和 controller-manager 都依赖 API Server 来心跳上报。API Server 一卡,kubelet 的NodeHeartbeat发不出去,节点就会被标记为 NotReady,然后触发 workload 驱逐,形成更大的雪崩。
这里有一个容易误会的点:API Server 本身可能并没有“崩”,而是因为 etcd 的database space exceeded错误导致大量 List-Watch 直接报错,最终 API Server 进入apierrors.NewInternalError的循环,然后触发 panic。此时 systemd 会尝试把它重启,但重启后它又去连 etcd,结果还是失败,于是进入 CrashLoopBackOff。从外部看,就是端口没人监听,表现成connection refused。
3.2 磁盘里到底被什么塞满了
定位到磁盘满之后,接下来的一步就是找出“凶手”。执行:
du -x --max-depth=1 -h / 2>/dev/null | sort -hr | head -20-x参数很关键,它表示不跨越文件系统,避免把 /proc、/sys 也扫进去,导致误判和卡死。
那次环境里扫出来的结果,常见的大头基本集中在这么几个地方:
| 目录 | 常见原因 |
|---|---|
| /var/log/journal | systemd 日志堆积 |
| /var/lib/docker/containers | 容器标准输出日志无限制增长 |
| /var/lib/docker/overlay2 | 镜像层和容器可写层堆积 |
| /var/lib/etcd/member | etcd 数据、快照、WAL 文件膨胀 |
| /tmp | 临时文件未被清理 |
| /var/lib/docker/tmp | Docker 构建缓存或临时文件 |
印象最深的两个元凶:一个是某个服务的容器日志一晚上写了几十 GB,另一个是 etcd 快照文件因为长期没做 compaction,整个/var/lib/etcd目录膨胀得非常夸张。
容器日志之所以可怕,是因为 Docker 默认只是把容器 stdout 输出写到/var/lib/docker/containers/{container_id}/*-json.log,如果应用代码里有大段大段的日志打印,这个文件会以惊人的速度增长,不做轮转就是灾难。
3.3 清理前必须知道的安全边界
磁盘满了以后,很多人会急眼,找到大文件就rm -rf。这个习惯在 K8s 环境里特别危险,有三个坑必须避开:
第一个坑是删了文件但空间不释放。很多日志文件正被进程持有句柄,你rm掉之后,文件系统里看不到这个文件了,但进程还在写,空间要等进程关闭文件句柄才真正释放。正确做法是用truncate清空文件内容而不是删除文件:
truncate -s 0 /var/lib/docker/containers/xxx/xxx-json.log第二个坑是不能乱删 overlay2 里的东西。overlay2 目录下面的 layer 和容器可写层,跟运行中的容器强关联,直接删会导致容器异常甚至启动失败。能不碰就不碰。
第三个坑是 etcd 数据目录不能直接删文件。etcd 的数据文件有自己的格式和管理方式,你哪怕手贱删了某个snap文件,都可能直接破坏整个 etcd 集群的数据一致性。要清理 etcd 的空间,得靠它自己的 compaction 和 defrag,不是靠rm。
4. 紧急恢复实操:三步把集群从死亡线上拉回来
4.1 止血:优先释放根分区空间
当时的第一目标是让根分区从 100% 降到 80% 以下,给 etcd 一点喘息的空间。
优先处理最安全的日志文件,因为日志数据是纯文本,丢一点不影响业务完整性(顶多查不到历史日志)。
容器日志逐个清:
find /var/lib/docker/containers -name "*-json.log" -type f -exec truncate -s 0 {} \;journald 日志做一次收缩清理:
journalctl --vacuum-size=500M这条命令会把老的 journal 日志删到只保留最近 500MB,在清理日志的同时不影响 systemd 服务的当前日志查看。执行完后再看:
df -h只要根分区从 100% 掉下来,尤其是留出至少 10%-20% 的空余空间,etcd 就有机会恢复写入。
这里还要补一个操作:如果发现/var/log或者/var/lib/docker是单独挂载的分区,而满的是根分区,要把/var/log下的历史归档文件也清理一下,比如log.1、压缩包之类。还有/tmp目录下的临时文件,可以安心清理。
4.2 复苏:让 etcd 和 API Server 恢复读写
磁盘空间释放后,要观察 etcd 是否自动恢复。先用 etcdctl 再看一次健康状态,如果还是 unhealthy,考虑重启 etcd 服务:
systemctl restart etcd这里有一个动作我是强烈建议先做再重启的:检查 etcd 的磁盘空间告警是否被触发过。
ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/server.pem --key=/etc/etcd/ssl/server-key.pem alarm list如果返回 NOSPACE,单纯重启不会消除告警,需要执行:
ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/server.pem --key=/etc/etcd/ssl/server-key.pem alarm disarm然后再重启 etcd。
之后重启 kube-apiserver:
systemctl restart kube-apiserver并且立刻看端口:
ss -lntp | grep 6443正常情况下,这时候 6443 应该恢复监听。再用本机 curl:
curl -k https://127.0.0.1:6443/healthz返回ok,说明 API Server 已经活过来了。
此时在 Master 节点上执行:
kubectl get nodes如果节点状态还是 NotReady,大概率是 kubelet 的心跳积压导致的,需要手动重启每个 NotReady 节点上的 kubelet:
systemctl restart kubelet4.3 恢复应用:等 Pod 调度和 KubeSphere 控制台
API Server 和节点都恢复正常后,接下来就是让 Kubernetes 自己把状态收敛回来。
用kubectl get pods -A看一下,会发现不少 Pod 处于Pending、ImagePullBackOff或者OOMKilled状态。Pending 的原因多半是节点资源不足或者污点未消除;ImagePullBackOff 可能是磁盘空间不够导致镜像拉取失败。
重点检查 KubeSphere 相关组件:
kubectl get pods -n kubesphere-systemks-apiserver、ks-controller-manager、ks-console 这几个 Pod 是控制台的骨干,确认它们经过重启后处于 Running 状态。然后再访问一次 30880 端口,控制台就能正常打开。
这里要注意,KubeSphere 控制台的 Session 存储、Redis 缓存如果也被磁盘满影响,会出现能打开但提示异常的情况,一般重启对应 Pod 就能自愈。个别情况下需要清掉浏览器端的 Cookie,重新登录。
4.4 现场救活后的第一个动作:备份所有组件状态
集群恢复运行后,我最想提醒你的是:别急着收工,立刻对关键配置做一次快照备份。
哪怕集群只有一个 Master,也建议用 etcd 的快照功能备份一次完整状态:
ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/server.pem --key=/etc/etcd/ssl/server-key.pem snapshot save /backup/etcd-snapshot.db同时把所有静态 Pod 配置、systemd 服务文件、证书目录做一次备份,用 tar 打包存放。这个动作成本很低,但能让你在后续操作里多一道保险。
5. 预防复盘:磁盘告警与日志轮转的落地配置
5.1 为什么监控里有磁盘指标,却没拦住这场事故
故障解决后,我让同事去查监控平台,发现磁盘使用率其实早就超过 90% 了,但告警阈值设的是 95%,而且告警只发到了某个没人看的群里,错过了抢救窗口。
这其实比故障本身更值得警惕。磁盘空间不像流量突增那样“秒级变化”,它是一个缓慢侵蚀的过程。真正有效的做法是分层设防:
| 层级 | 阈值 | 动作 |
|---|---|---|
| 预警 | 磁盘使用率 70% | 检查大文件,启动日志压缩 |
| 告警 | 磁盘使用率 85% | 立即通知运维值班人员 |
| 严重 | 磁盘使用率 92% | 自动清理可安全回收的日志和临时文件 |
| 紧急 | 磁盘使用率 97% | 页面值班 + 电话告警,强制人工介入 |
如果你的监控平台支持节点级告警,那就把node_filesystem_avail_bytes或者类似指标接进来,至少要把告警发给两个以上的人,不能只挂一个接受人。
5.2 Docker 容器日志轮转配置
这是所有 K8s 集群里性价比最高的配置之一。在/etc/docker/daemon.json里加上:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "5" } }max-size=100m表示单个容器日志文件最大 100MB,max-file=5表示最多保留 5 个文件,超过就轮转。注意,这个配置对已存在的容器不生效,需要重启容器或重启 Docker 才能生效,所以最好是配合发布流程逐步滚动重启。
5.3 systemd 日志限制
在/etc/systemd/journald.conf里:
SystemMaxUse=500M MaxFileSec=7day然后重启 journald:
systemctl restart systemd-journald这样 journal 日志会被限制在 500MB,超过 7 天的自动清理。这个配置我们自己集群都加了,效果立竿见影,实测几个月没再出现根分区被 journald 塞满的情况。
5.4 etcd 的压缩与碎片整理
etcd 的存储空间问题,除了物理磁盘满,还有一个隐藏坑:逻辑空间不够。如果你看到报错是mvcc: database space exceeded,但df -h显示磁盘还有空间,那八成是 etcd 的 compaction 策略设置得太保守,历史版本太多。
推荐开启自动压缩:
ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/server.pem --key=/etc/etcd/ssl/server-key.pem compaction --physical 600000这样每10分钟的历史版本会被压缩。压缩后数据库内部还有碎片,需要 defrag 才能把空间真正还给磁盘:
ETCDCTL_API=3 etcdctl --cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/server.pem --key=/etc/etcd/ssl/server-key.pem defrag注意:defrag 会阻塞 etcd 的写能力,建议在业务低峰期执行,或者逐个节点轮流执行。
6. 常见问题排查速查与个人心得
6.1 故障排查速查表
| 现象 | 排查命令 | 常见原因 | 处理建议 |
|---|---|---|---|
| kubectl connection refused | ss -lntp | grep 6443 | API Server 进程未运行 | 查看 kube-apiserver 日志 |
| API Server CrashLoopBackOff | journalctl -u kube-apiserver -n 200 | etcd 连接失败 / 磁盘满 | 先恢复磁盘空间 |
| etcd 报 database space exceeded | etcdctl alarm list | etcd 历史版本积压 | compaction + defrag |
| 节点 NotReady | journalctl -u kubelet -n 100 | kubelet 心跳中断 | 重启 kubelet |
| 控制台 502 | kubectl get pods -n kubesphere-system | ks-apiserver 异常 | 查看并重启相关 Pod |
| 磁盘满了但不见大文件 | df -i | inode 耗尽 | 清理小文件数量 |
6.2 个人踩坑记录:几个“早知道”的瞬间
第一次遇到这种故障时,我犯了一个错误:一直盯着防火墙和负载均衡排查,因为在界面上看到的是Connection Refused。后来被有经验的同事点了一句:“本机 curl 一下 healthz 看看”,这才少走了半小时弯路。现在我的习惯是遇到 refused 永远先做本机探活,能瞬间区分“网络层问题”和“服务层问题”。
第二个踩过的坑是直接对 json 日志文件执行rm。当时节点上的 Docker 还在持续向这个文件写日志,删了之后空间没有立刻释放,我一度以为清理没生效,折腾了半天才想起来是文件句柄没释放,被迫用truncate -s 0解决。如果当时用lsof | grep deleted查看,一眼就能发现问题。
第三个印象深刻的教训,是那次以为随便删个 etcd 的备份文件不影响什么,结果导致一个节点上的 etcd 数据不一致,花了更长时间来恢复。k8s 控制面的文件,尤其是 etcd 目录下的东西,最好只通过官方工具操作,绝不要手痒直接rm。
6.3 最后再分享一个实用小习惯
现在我管理的每个集群,都会在节点上部署一个简单的巡检脚本,每 15 分钟检查一次根分区和 etcd 分区的磁盘使用率,超过 85% 就往运维群的机器人推一条消息。这个脚本大约 30 行,效果却比很多重量级监控系统更直接。
判断集群健康不能只看“网页能打开”,磁盘、网络、etcd、kubelet 任何一个环节出了小问题,都可能演变成控制面雪崩。先看磁盘,再看 etcd,最后才查网络,这个顺序在排查 Kubernetes 连接类故障时基本不会错。