跳转到内容
123xiao | 无名键客

《集群架构实战:基于 Kubernetes 的高可用服务部署与故障自动恢复设计》

字数: 0 阅读时长: 1 分钟

背景与问题

在 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. 高可用不是单点配置,而是一条链路

一条完整的高可用链路大概是:

  1. 多副本运行
  2. 副本分散到不同节点/可用区
  3. 只有健康 Pod 才接流量
  4. Pod 异常后自动重建
  5. 节点异常后快速迁移
  6. 发布过程中保持最小可用实例数
  7. 终止时优雅摘流,避免请求中断
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

但如果没有合理设置:

  • maxUnavailable
  • maxSurge
  • minReadySeconds
  • terminationGracePeriodSeconds
  • preStop

就很容易在更新时出现“旧 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
  • 是否有 UnhealthyBack-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 的摘流与业务进程退出不同步。

建议

至少配置:

  • terminationGracePeriodSeconds
  • preStop
  • 应用捕获 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”,那大概率会在发布、节点异常、流量波动时吃亏。

更靠谱的设计思路应该是:

  1. 多副本只是起点,不是终点
  2. readiness 决定流量安全,liveness 决定重启策略
  3. startupProbe 用来保护慢启动应用
  4. 反亲和性、拓扑分散、PDB 决定你能不能扛住节点级故障
  5. 优雅终止决定发布时是否真能做到低中断
  6. 排障时先止血,再沿 Pod → Endpoint → Deployment → Node 的链路定位

如果你现在维护的是中型业务服务,我的执行建议是:

  • 先从 探针、滚动更新、优雅终止 三件事改起
  • 然后补上 反亲和性 + PDB
  • 最后再根据监控完善 HPA 与资源模型

边界条件也要说清楚:
如果你的业务是强状态、有长事务、依赖本地磁盘,Kubernetes 的“自动恢复”不等于“无损恢复”。这时还要结合应用层状态转移、会话迁移和数据一致性设计。

一句话总结:Kubernetes 能帮你恢复实例,但高可用真正成立,靠的是你把“健康判定、流量切换、调度分散、优雅下线”这几环都补齐。


分享到:

上一篇
《微服务架构中分布式事务的实战选型与落地:Seata、可靠消息最终一致性与补偿机制对比》
下一篇
《微服务架构中的分布式事务落地实践:基于 Saga 模式的设计与排错指南》