背景与问题
在 Kubernetes 里做高可用部署,很多团队一开始的理解都比较“理想化”:
- Deployment 副本数改成 3,就算高可用了
- Pod 挂了会自动拉起,就算故障自动恢复了
- Service 前面挂个 Ingress,请求就不会丢了
但我在实际排障时发现,真正线上出问题的地方,往往不是“有没有副本”,而是:
- Pod 明明是 Running,但业务已经不可用
- 滚动发布时新 Pod 还没准备好,流量就切过去了
- 节点宕机后,服务恢复慢,甚至出现长时间 502
- 探针配置不合理,导致服务在“抖动—重启—再抖动”之间循环
- Pod 被驱逐、调度失败、镜像拉取失败,自动恢复链路断在半路
所以这篇文章不只是讲“怎么部署”,而是从 故障视角 来设计一套更靠谱的 Kubernetes 高可用服务方案,并给出可运行的 YAML 和排查路径。
现象复现
先看几个典型线上现象,很多人都遇到过:
现象 1:服务偶发 502/超时
明明 Deployment 有 3 个副本,但发布时或节点重启后,Ingress/Nginx 还是报错。
常见原因:
- readinessProbe 未通过前流量已进入
- Pod 终止时没有优雅下线
- Service Endpoint 更新有延迟
- 应用自己启动慢,但存活探针过早触发重启
现象 2:Pod 频繁重启
kubectl get pods 能看到 CrashLoopBackOff,或者 Pod 看起来不断被拉起。
常见原因:
- livenessProbe 配错,把“启动慢”误判为“进程挂了”
- 应用依赖外部数据库/Redis,启动阶段卡住
- 内存 limit 太小,触发 OOMKilled
- 容器退出码非 0,Kubernetes 只是不断重试
现象 3:节点故障后恢复不及时
节点 NotReady 了,但业务并没有快速切走,或者被切走后容量不够。
常见原因:
- 副本分布过于集中
- 没有 PodDisruptionBudget
- 没有反亲和性与拓扑分散
- HPA/Cluster Autoscaler 配合不完整
核心原理
先把几个核心机制串起来,否则排障时容易“只盯着 Pod”。
1. 高可用不是单点配置,而是一条链路
一条完整的高可用链路大概是:
- 多副本运行
- 副本分散到不同节点/可用区
- 只有健康 Pod 才接流量
- Pod 异常后自动重建
- 节点异常后快速迁移
- 发布过程中保持最小可用实例数
- 终止时优雅摘流,避免请求中断
flowchart TD
A[用户流量进入 Ingress/Service] --> B[Service 选择 Ready Pod]
B --> C1[Pod A]
B --> C2[Pod B]
B --> C3[Pod C]
C1 --> D[健康检查失败]
D --> E[Kubelet 标记 Unready]
E --> F[Service 摘除 Endpoint]
F --> G[Deployment/ReplicaSet 维持副本数]
G --> H[重新调度新 Pod]
2. 三种探针各自解决不同问题
livenessProbe
判断“是不是应该重启容器”。
适合检测:
- 进程死锁
- 服务线程卡死
- 应用进入不可恢复状态
不适合检测:
- 启动慢
- 短时依赖抖动
- 临时负载高导致响应慢
readinessProbe
判断“能不能接流量”。
如果 readiness 失败:
- Pod 还活着
- 但会从 Service Endpoints 中摘掉
- 不再接收新流量
这才是线上高可用的关键。
startupProbe
判断“是否仍在启动阶段”。
它解决的是一个老问题:有些应用启动很慢,livenessProbe 先下手为强,把容器提前重启了。
3. Deployment 的滚动更新不等于零中断
很多人只写:
replicas: 3
strategy:
type: RollingUpdate
但如果没有合理设置:
maxUnavailablemaxSurgeminReadySecondsterminationGracePeriodSecondspreStop
就很容易在更新时出现“旧 Pod 退了,新 Pod 还没真准备好”的空窗期。
4. 节点级故障恢复依赖调度策略
Pod 自动拉起只是第一层,真正能否抗节点故障,还取决于:
- podAntiAffinity:避免副本扎堆同一台机器
- topologySpreadConstraints:尽量打散到不同节点/域
- PodDisruptionBudget:限制自愿驱逐导致的可用性下降
- 资源 requests/limits:避免新 Pod 调度不上去
下面这张图更适合理解节点故障时的恢复路径:
sequenceDiagram
participant User as 用户
participant LB as Service/Ingress
participant Node1 as 节点1
participant Node2 as 节点2
participant CP as Kubernetes 控制面
User->>LB: 发起请求
LB->>Node1: 转发到 Pod-1
LB->>Node2: 转发到 Pod-2
Node1-->>CP: 节点故障/NotReady
CP->>LB: 摘除 Node1 上异常 Pod Endpoint
CP->>Node2: 保留健康副本继续服务
CP->>Node2: 或调度新 Pod 到其他健康节点
User->>LB: 后续请求继续访问健康副本
实战代码(可运行)
下面我给一套可以直接落地的示例,目标是部署一个高可用的 Web 服务,并具备较完整的自动恢复能力。
1. 示例应用
这里用一个简单的 nginx 容器模拟 Web 服务,探针直接检查 HTTP。
deployment-ha.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ha-web
labels:
app: ha-web
spec:
replicas: 3
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: ha-web
template:
metadata:
labels:
app: ha-web
spec:
terminationGracePeriodSeconds: 30
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: ha-web
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: ha-web
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 30
periodSeconds: 2
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
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
2. Service
service.yaml
apiVersion: v1
kind: Service
metadata:
name: ha-web-svc
spec:
selector:
app: ha-web
ports:
- name: http
port: 80
targetPort: 80
type: ClusterIP
3. PodDisruptionBudget
pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ha-web-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: ha-web
4. HPA
如果业务流量有波动,建议给它加上水平扩缩容。
hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ha-web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ha-web
minReplicas: 3
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
5. 一键部署
kubectl apply -f deployment-ha.yaml
kubectl apply -f service.yaml
kubectl apply -f pdb.yaml
kubectl apply -f hpa.yaml
6. 验证部署状态
kubectl get deploy,po,svc,pdb,hpa -l app=ha-web
kubectl rollout status deployment/ha-web
kubectl get endpoints ha-web-svc
止血方案:出故障时先保证服务
排障时别一上来就改一堆 YAML。我的习惯是先止血,再修结构性问题。
场景 1:发布后 502 激增
可以先做:
kubectl rollout undo deployment/ha-web
kubectl rollout status deployment/ha-web
如果确认是 readiness 不合理,先临时扩大副本数:
kubectl scale deployment ha-web --replicas=5
场景 2:探针导致频繁重启
先看最近事件:
kubectl describe pod <pod-name>
kubectl logs <pod-name> --previous
如果是启动慢导致 liveness 误杀,可以先临时移除或放宽探针参数,再发版修复。
场景 3:节点故障后容量不够
先确认 Pod 是否集中在单节点:
kubectl get pods -o wide -l app=ha-web
短期止血:
- 提高副本数
- 清理资源不足节点
- 检查是否存在 Pending Pod
- 必要时手动 cordon/drain 故障节点
kubectl cordon <node-name>
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
定位路径
线上故障最怕“东一榔头西一棒子”。下面给一条比较稳的排查路径。
flowchart LR
A[业务报错/超时] --> B{Pod 是否 Ready?}
B -- 否 --> C[检查 probe / events / logs]
B -- 是 --> D{Endpoints 是否正常?}
D -- 否 --> E[检查 Service selector 与标签]
D -- 是 --> F{节点是否异常?}
F -- 是 --> G[检查 Node 状态、驱逐、调度]
F -- 否 --> H[检查资源瓶颈/应用线程池/依赖服务]
第一步:看 Pod 状态
kubectl get pods -o wide
kubectl describe pod <pod-name>
重点看:
Ready是否为True- 是否有
CrashLoopBackOff - 是否
OOMKilled - 是否有
Unhealthy、Back-off restarting failed container
第二步:看 Endpoint 是否挂上
kubectl get endpoints ha-web-svc
kubectl describe svc ha-web-svc
如果 Pod 是 Running,但 endpoints 为空,通常是:
- readiness 失败
- label selector 不匹配
- Pod 不在 Ready 状态
第三步:看 Deployment 滚动更新状态
kubectl rollout status deployment/ha-web
kubectl describe deployment ha-web
重点关注:
Available副本数Unavailable副本数- 是否卡在 ProgressDeadlineExceeded
第四步:看节点与调度
kubectl get nodes
kubectl describe node <node-name>
kubectl get events --sort-by=.lastTimestamp
重点看:
- 节点是否
NotReady - 是否有
Insufficient cpu/memory - 是否被 taint
- Pod 是否因亲和性/反亲和性而调度失败
第五步:看资源与历史日志
kubectl top pod
kubectl top node
kubectl logs <pod-name> --previous
如果 --previous 日志里能看到应用崩溃前输出,往往比实时日志更有价值。
常见坑与排查
下面这些坑,我基本都见过,甚至踩过。
1. 把 livenessProbe 当成“健康检查总开关”
这是最常见的误区。
症状
- 容器反复重启
- 服务一直起不来
- 日志里没明显报错,但 Pod 不断重建
根因
应用启动慢,liveness 提前触发。
建议
- 慢启动服务优先用
startupProbe livenessProbe只检测“不可恢复”状态- 不要把外部依赖波动直接当成 liveness 失败条件
2. readinessProbe 成功条件太宽松
症状
- Pod 很快 Ready
- 但接流量后大量超时
根因
探针只检查端口是否打开,没有检查应用是否真正可服务。
建议
如果应用有 /healthz、/ready 之类接口,应该区分:
/live:进程是否存活/ready:缓存、配置、连接池、核心依赖是否就绪
3. 忘了优雅终止
症状
- 发布时偶发请求失败
- 长连接中断
- 网关日志里出现 upstream reset
根因
容器一收到终止信号就退出,Service/Ingress 的摘流与业务进程退出不同步。
建议
至少配置:
terminationGracePeriodSecondspreStop- 应用捕获
SIGTERM并停止接新请求
4. 只有多副本,没有分散部署
症状
- 明明 3 个副本
- 但全跑在同一个节点
- 节点一挂,全军覆没
根因
没有设置反亲和性或拓扑分散约束。
建议
- 用
podAntiAffinity - 用
topologySpreadConstraints - 多可用区环境下按 zone 打散
5. 资源 request 配置过低
症状
- 平时看起来能跑
- 高峰期抖动严重
- 新 Pod 被调度后性能很差
根因
request 太低导致调度过度乐观,节点实际资源被打满。
建议
- request 基于真实监控设置
- limit 不要过小,避免无谓 OOM
- 关键服务预留资源
6. PDB 配太死,导致运维操作卡住
症状
- drain 节点时一直失败
- 升级集群卡住
根因
minAvailable 设置过高,而实际副本数不足。
建议
比如 3 副本服务,minAvailable: 2 通常比较稳;但如果你只有 2 副本,还设成 2,很多操作都会很难做。
安全/性能最佳实践
高可用不是只盯可用性,安全和性能也会直接影响恢复能力。
安全最佳实践
1. 以非 root 运行容器
securityContext:
runAsNonRoot: true
runAsUser: 1001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
如果容器因为权限问题频繁失败,排障时也更容易界定问题边界。
2. 限制 ServiceAccount 权限
很多服务根本不需要访问 Kubernetes API,不要默认挂过大的 RBAC 权限。
3. 镜像固定版本,不要直接用 latest
nginx:latest 在恢复、回滚、跨环境排查时都容易造成不一致。建议固定到明确版本号。
4. 配合 NetworkPolicy 做最小访问面
高可用服务不是谁都能访问谁。网络边界清晰后,异常更容易被定位。
性能最佳实践
1. 给探针留出合理超时
CPU 高时,探针本身就可能超时。不要把 timeoutSeconds 配得过小,否则高峰期会出现“应用没挂,探针先挂”。
2. 让 HPA 和 requests 配套
HPA 依赖资源指标,如果 requests 失真,扩缩容判断也会失真。
3. 预热机制要和 readiness 联动
如果应用需要:
- JIT 预热
- 本地缓存加载
- 大量配置拉取
- 建立连接池
那么 预热完成前不要 Ready。
4. 区分“快速失败”和“自动重试”
Kubernetes 会帮你重建 Pod,但不会修复你的业务雪崩。应用侧仍需要:
- 请求超时
- 熔断
- 限流
- 指数退避重试
一份更稳的验证清单
上线前,我一般会人工过一遍下面这些点:
- Deployment 至少 2~3 副本
- readiness / liveness / startup 探针职责明确
-
maxUnavailable=0或符合业务容忍度 - 设置了
minReadySeconds - 配置了优雅终止
- 副本已分散到不同节点
- PDB 与副本数匹配
- requests/limits 基于监控数据
- 可通过
rollout undo快速回滚 - 节点故障时至少还能保留最小服务能力
总结
如果把 Kubernetes 高可用部署只理解成“多起几个 Pod”,那大概率会在发布、节点异常、流量波动时吃亏。
更靠谱的设计思路应该是:
- 多副本只是起点,不是终点
- readiness 决定流量安全,liveness 决定重启策略
- startupProbe 用来保护慢启动应用
- 反亲和性、拓扑分散、PDB 决定你能不能扛住节点级故障
- 优雅终止决定发布时是否真能做到低中断
- 排障时先止血,再沿 Pod → Endpoint → Deployment → Node 的链路定位
如果你现在维护的是中型业务服务,我的执行建议是:
- 先从 探针、滚动更新、优雅终止 三件事改起
- 然后补上 反亲和性 + PDB
- 最后再根据监控完善 HPA 与资源模型
边界条件也要说清楚:
如果你的业务是强状态、有长事务、依赖本地磁盘,Kubernetes 的“自动恢复”不等于“无损恢复”。这时还要结合应用层状态转移、会话迁移和数据一致性设计。
一句话总结:Kubernetes 能帮你恢复实例,但高可用真正成立,靠的是你把“健康判定、流量切换、调度分散、优雅下线”这几环都补齐。