背景与问题
很多团队刚上 Kubernetes 时,最容易误解的一点是:只要 Pod 配了副本数,集群就算高可用了。真到线上出故障,才发现问题远不止业务 Pod 这么简单。
我自己做过几次集群故障排查,最典型的现场通常长这样:
- 控制面某台节点重启后,
kubectl偶发超时 - API Server 看起来还活着,但调度明显变慢
- 某个工作节点网络抖动,Pod 处于
Terminating或Unknown - DaemonSet 没完全恢复,业务侧开始报连通性异常
- 节点“看似在线”,但 kubelet 已经半失联
这类问题的本质,不是单一组件挂了,而是控制面高可用设计和工作节点故障自愈机制没有形成闭环。
本文我会从 troubleshooting 的角度,把这个问题拆开讲:
- 高可用控制面为什么不只是“多部署几台 master”
- 工作节点故障后,Kubernetes 是如何判断、驱逐、重建的
- 出问题时应该怎么定位
- 给一套可运行的实战配置和排查命令
现象复现
先看几个线上最常见的异常现象,后面排查会围绕它们展开。
现象 1:控制面单点隐患
只有 1 个控制面节点时:
kube-apiserver所在机器宕机,整个集群 API 不可用- 控制器和调度器无法继续执行
- 已运行的 Pod 可能暂时还在,但无法创建、删除、扩缩容
现象 2:工作节点失联后,业务恢复很慢
某个节点断网后:
- 节点状态变成
NotReady - Pod 长时间停留在旧节点上
- Deployment 有副本,但新 Pod 迟迟没补起来
- Service 端点更新不及时,流量还可能打到故障节点
现象 3:etcd 正常率低导致控制面抖动
常见表现:
kubectl get pods -A偶发卡住- API Server 日志出现
etcd request timed out - leader 频繁切换
- 集群并未完全挂,但“哪哪都慢”
核心原理
这一节很关键。很多排查做不下去,是因为不知道 Kubernetes 到底按什么机制在“自愈”。
1. 高可用控制面的基本结构
一个相对标准的生产架构通常包含:
- 3 台及以上控制面节点
- 前置负载均衡器或 VIP
- 每台控制面运行:
kube-apiserverkube-controller-managerkube-scheduleretcd(堆叠式)或外部 etcd 集群
核心目标是:任意 1 台控制面节点故障,不影响 API 对外服务与控制循环继续运转。
flowchart LR
U[运维/CI/CD/kubectl] --> LB[负载均衡器 / VIP]
LB --> CP1[Control Plane 1]
LB --> CP2[Control Plane 2]
LB --> CP3[Control Plane 3]
CP1 --> E1[(etcd 1)]
CP2 --> E2[(etcd 2)]
CP3 --> E3[(etcd 3)]
CP1 --> W1[Worker 1]
CP2 --> W2[Worker 2]
CP3 --> W3[Worker 3]
关键点
- API Server 可多活
- Controller Manager 和 Scheduler 通过 Leader Election 保证单活控制
- etcd 依赖多数派(quorum)
- 3 节点 etcd,允许挂 1 台
- 5 节点 etcd,允许挂 2 台
- 负载均衡器必须做健康检查,不能把流量继续打到失效 API Server
2. 工作节点故障自愈链路
工作节点出问题后,Kubernetes 并不是“立刻重建”,而是按一条链路逐步感知和处理:
- kubelet 周期性向 API Server 上报节点状态
- Node Controller 发现心跳超时
- 节点状态从
Ready变为NotReady或Unknown - 节点被自动打上 taint,例如:
node.kubernetes.io/not-readynode.kubernetes.io/unreachable
- 控制器按 Pod 的容忍时间决定是否驱逐
- Deployment/StatefulSet/DaemonSet 再按各自机制重建
sequenceDiagram
participant K as kubelet
participant A as API Server
participant N as Node Controller
participant S as Scheduler
participant D as Deployment/ReplicaSet
K->>A: 定期上报 NodeStatus/Lease
A->>N: 节点状态更新
Note over K,A: 节点故障或网络中断
N->>A: 标记节点 NotReady/Unknown
N->>A: 添加 unreachable/not-ready taint
D->>A: 发现副本不足
S->>A: 为新 Pod 选择健康节点
A->>D: 新副本运行
这里最容易踩坑的点
很多人以为节点一 NotReady,Pod 就会马上迁走。其实不一定。
因为默认还受这些因素影响:
pod-eviction-timeout- Pod 是否配置了
tolerations - 是否使用本地存储
- PDB(PodDisruptionBudget)是否限制驱逐
- StatefulSet 是否存在有序重建约束
3. 控制面高可用的“真瓶颈”常在 etcd
控制面组件看起来很多,但真正最脆弱、也最容易被忽略的是 etcd。
etcd 决定了:
- API Server 读写延迟
- leader 选举稳定性
- 资源对象一致性
如果 etcd 有这些问题:
- 磁盘延迟高
- 时钟漂移大
- 网络丢包/抖动
- 频繁 compact/defrag 不当
那么表面上看是“API Server 不稳定”,实际上根子在存储层。
stateDiagram-v2
[*] --> Healthy
Healthy --> Degraded: 磁盘/网络延迟升高
Degraded --> LeaderFlap: 选主频繁切换
LeaderFlap --> APISlow: API 请求超时
APISlow --> SchedulingDelay: 调度变慢/控制器积压
SchedulingDelay --> ServiceImpact: 业务恢复变慢
Degraded --> Healthy: 恢复网络/磁盘性能
APISlow --> Healthy: etcd 稳定后恢复
定位路径
下面给一条我更推荐的排查顺序:先判断是控制面问题,还是节点问题,再往下钻。
第一步:确认 API Server 是否稳定
kubectl get --raw='/readyz?verbose'
kubectl get --raw='/livez?verbose'
kubectl get componentstatuses
如果 readyz 都不稳定,先别急着查业务 Pod,优先看:
- 负载均衡器健康检查
- API Server 日志
- etcd 状态
第二步:确认 etcd 健康
如果是 kubeadm 默认堆叠式 etcd,通常在控制面节点本机执行:
export ETCDCTL_API=3
etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key \
--endpoints=https://127.0.0.1:2379 \
endpoint health
etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/peer.crt \
--key=/etc/kubernetes/pki/etcd/peer.key \
--endpoints=https://127.0.0.1:2379 \
endpoint status -w table
重点看:
- 是否所有成员都 healthy
- leader 是否频繁变化
- raft term 是否异常跳变
- db size 是否持续膨胀
第三步:确认节点心跳与 taint
kubectl get nodes -o wide
kubectl describe node <node-name>
kubectl get leases -n kube-node-lease
尤其看:
Ready状态- 最近心跳时间
- 是否出现
node.kubernetes.io/unreachable Conditions中的NetworkUnavailable、MemoryPressure、DiskPressure
第四步:确认 Pod 为什么没被迁走
kubectl get pod -A -o wide | grep <node-name>
kubectl describe pod <pod-name> -n <namespace>
kubectl get pdb -A
排查方向:
- 是否被 PDB 卡住
- 是否有长时间 toleration
- 是否使用本地卷导致无法快速迁移
- 是否存在 finalizer 或 CNI 卸载不完整
实战代码(可运行)
下面给一套偏实战的最小落地方案,目标是:
- 3 控制面节点
- 通过 kubeadm 初始化高可用集群
- 工作负载具备基本反亲和与中断保护
- 故障时能更快完成节点级自愈
1. kubeadm 高可用控制面配置
假设前面有一个负载均衡地址 10.0.0.100:6443。
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.27.6
controlPlaneEndpoint: "10.0.0.100:6443"
networking:
podSubnet: "10.244.0.0/16"
apiServer:
certSANs:
- "10.0.0.100"
- "k8s-api.internal"
controllerManager:
extraArgs:
node-monitor-grace-period: "20s"
pod-eviction-timeout: "30s"
scheduler: {}
etcd:
local:
dataDir: /var/lib/etcd
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "iptables"
初始化首个控制面节点:
kubeadm init --config kubeadm-ha.yaml --upload-certs
加入其他控制面节点:
kubeadm join 10.0.0.100:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--control-plane \
--certificate-key <certificate-key>
加入工作节点:
kubeadm join 10.0.0.100:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
说明:
node-monitor-grace-period和pod-eviction-timeout可以缩短故障发现与驱逐时间,但别调得过激。网络稍微抖一下就误判,也会带来大量无意义迁移。我一般建议先在测试环境压测再定值。
2. 业务工作负载的高可用部署示例
下面这个 Deployment 做了几件事:
- 3 副本
- 节点反亲和,尽量打散
- 就绪探针确保流量不进故障实例
- 存活探针触发容器级自愈
- 搭配 PDB,避免维护时全部一起掉
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-ha
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: web-ha
template:
metadata:
labels:
app: web-ha
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: web-ha
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-ha-pdb
namespace: default
spec:
minAvailable: 2
selector:
matchLabels:
app: web-ha
部署:
kubectl apply -f web-ha.yaml
kubectl get pods -o wide
3. 模拟工作节点故障并验证自愈
先找一个有业务 Pod 的节点:
kubectl get pods -o wide
kubectl get nodes
然后在该节点上模拟 kubelet 停止:
sudo systemctl stop kubelet
观察节点状态:
kubectl get nodes -w
观察 Pod 重建过程:
kubectl get pods -o wide -w
预期现象:
- 节点先变
NotReady - 原节点上的 Pod 不会瞬间消失
- 超过驱逐阈值后,新 Pod 会被调度到其他健康节点
- Service 端点会逐步切换到新 Pod
4. 一个简单的自动检查脚本
这个脚本用于快速检查控制面和节点状态,适合故障时先跑一遍。
#!/usr/bin/env bash
set -euo pipefail
echo "== API Server Readyz =="
kubectl get --raw='/readyz?verbose' || true
echo
echo "== Nodes =="
kubectl get nodes -o wide
echo
echo "== Non-Running Pods =="
kubectl get pods -A --field-selector=status.phase!=Running || true
echo
echo "== Node Leases =="
kubectl get leases -n kube-node-lease
echo
echo "== Events (tail) =="
kubectl get events -A --sort-by=.lastTimestamp | tail -n 30 || true
保存为 cluster-health-check.sh 后执行:
chmod +x cluster-health-check.sh
./cluster-health-check.sh
常见坑与排查
这一节我会把最容易碰到、而且最容易误判的坑直接列出来。
坑 1:有 3 个控制面节点,但入口还是单点
常见配置:
- 前面只放了 1 台 Nginx/HAProxy
- 没有 Keepalived/VIP
- 或 DNS 只指向单 IP
结果是:
- 控制面节点虽然多台
- 但流量入口挂了,集群照样不可用
排查方式
nc -zv 10.0.0.100 6443
curl -k https://10.0.0.100:6443/livez
止血方案
- 临时改
/etc/hosts指向可用 API Server - 或直接用某个健康控制面节点 IP 进行紧急操作
- 后续补齐双 LB 或 VIP 漂移方案
坑 2:节点故障后 Pod 长时间不迁移
常见原因
pod-eviction-timeout太长- Pod 有默认 300 秒
tolerationSeconds - PDB 限制了驱逐
- StatefulSet + PVC 绑定导致切换慢
排查方式
kubectl describe node <node-name>
kubectl describe pod <pod-name> -n <ns>
kubectl get pdb -A
止血方案
如果明确节点已经不可恢复,可手动驱逐或删除 Node:
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --force
kubectl delete node <node-name>
注意:
drain适合节点还通的时候。
如果节点已经彻底失联,通常是delete node让控制平面尽快放弃旧状态。
但涉及本地卷、StatefulSet 时,一定先确认数据一致性。
坑 3:livenessProbe 配太猛,自己把自己打死
这是我踩过的一个坑。应用冷启动需要 20 秒,我把 livenessProbe 配成了 5 秒就检查,结果容器一直重启,看起来像“集群不稳定”,其实是探针误杀。
建议
readinessProbe控流量livenessProbe控重启- 慢启动应用优先加
startupProbe
示例:
apiVersion: v1
kind: Pod
metadata:
name: probe-demo
spec:
containers:
- name: app
image: nginx:1.25
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet:
path: /
port: 80
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 20
periodSeconds: 10
坑 4:etcd 磁盘性能差,所有问题都像“玄学”
表现
- API 请求偶发超时
- 控制器处理慢
- leader 切换频繁
- 但 CPU、内存看起来并不高
排查建议
在 etcd 节点上看磁盘延迟:
iostat -x 1
看 etcd 日志:
journalctl -u etcd -f
如果是容器化静态 Pod:
crictl ps | grep etcd
crictl logs <container-id>
止血方案
- etcd 数据盘用 SSD
- 避免与业务 IO 混跑
- 定期 compact/defrag,但不要在高峰时段做
安全/性能最佳实践
高可用和自愈做得越深,越不能忽视安全和性能边界。否则很容易变成“故障恢复快了,但平时更脆”。
安全最佳实践
1. 控制面入口最小暴露
- API Server 只对内网开放
- 外部访问走堡垒机或专用代理
- 负载均衡器加白名单和审计
2. etcd 一定启用 TLS
etcd 保存的是整个集群状态,基本就是核心资产。证书、网络隔离、访问控制都不能省。
3. RBAC 最小权限
自动化巡检脚本、CI/CD 账号不要直接给 cluster-admin。
很多“误操作导致全局故障”的根源,不是系统自愈差,而是权限过大。
4. 审计日志要保留
至少保留:
- API Server audit log
- 关键节点系统日志
- etcd 日志
- CNI 插件日志
故障后没有日志,基本就是盲飞。
性能最佳实践
1. 不要把故障检测时间调得过低
参数调太小会有副作用:
- 网络轻微抖动就误判节点失联
- Pod 频繁迁移
- 对存储型业务影响很大
建议:
- 延迟敏感无状态业务可激进些
- 有状态业务要保守
- 先压测,再上线
2. 业务副本要跨节点、最好跨可用区
- 至少做
podAntiAffinity - 更进一步做 topology spread constraints
- 如果是云上,多可用区部署收益很大
3. 为关键系统组件预留资源
比如:
- CoreDNS
- kube-proxy
- CNI 组件
- ingress controller
这些组件没资源,业务再多副本也没意义。
4. 监控别只看 Pod,要看控制循环时延
建议重点监控:
- API Server 请求延迟
- etcd fsync 延迟
- controller workqueue 积压
- scheduler latency
- 节点 lease 更新间隔
方案取舍分析
在控制面和自愈策略上,没有“唯一标准答案”,只有更适合当前阶段的取舍。
方案 1:堆叠式 etcd
即 etcd 与控制面在同一批节点上。
优点
- 部署简单
- 成本较低
- kubeadm 支持成熟
缺点
- 控制面和存储耦合
- 故障域不够独立
- 节点资源竞争更明显
适用场景
- 中小规模集群
- 团队运维能力一般
- 追求快速落地
方案 2:外部 etcd 集群
优点
- 控制面与数据存储解耦
- 更容易独立扩展与维护
- 故障隔离更清晰
缺点
- 部署维护复杂度更高
- 网络链路要求更高
- 对团队经验有要求
适用场景
- 多集群共享规范
- 对控制面稳定性要求很高
- 有专门平台团队维护
止血方案
当线上已经出问题时,不要一上来就“彻底修复”,先做止血。
场景 1:控制面入口挂了
- 临时直连健康 API Server 节点
- 校验
/readyz - 恢复或切换 LB/VIP
场景 2:某工作节点故障且不可恢复
- 先确认业务副本是否已补齐
- 删除故障 Node 对象
- 必要时手工清理云主机或虚机残留
场景 3:大面积 Pod 重建慢
- 检查镜像拉取是否受限
- 检查 CNI / CoreDNS 是否正常
- 暂时提升关键业务优先级
- 必要时手工扩容健康节点池
总结
如果把 Kubernetes 高可用和故障自愈浓缩成一句话,我会这样说:
控制面负责“还能不能管”,工作节点自愈负责“业务多久恢复”。
真正可落地的设计,至少要做到这几件事:
- 控制面三节点起步,入口不能单点
- etcd 保证多数派与稳定 IO
- 节点故障检测参数要结合业务特性调优
- 工作负载本身要配合自愈
- 多副本
- 反亲和
- 合理探针
- 合理 PDB
- 排查顺序要对
- 先 API Server
- 再 etcd
- 再 Node
- 最后看 Pod 和业务层
最后给一个很实际的建议:
不要只在文档里写“我们集群支持高可用”,一定要定期做故障演练,比如:
- 关一台控制面节点
- 断一个工作节点网络
- 模拟 etcd 单成员异常
- 演练 drain、delete node、Pod 自动补副本
你只有亲手演练过,才知道你的“高可用”到底是设计出来的,还是想象出来的。