
本文记录我处理过的一个 Kubernetes 生产集群故障,目的是梳理问题分析的思路和排查策略,希望下次再遇到类似问题时,能更有针对性的解决问题,尽量少熬夜。
Kubernetes v1.27(托管版,三可用区),Containerd 运行时NGINX Ingress Controller,上游为若干 Deployment 的 ClusterIP Serviceweb(无状态),常态副本数 replicas=5,跨 AZ 均衡调度HPA 按 CPU 指标自动扩缩,minReplicas 当时未设置(疏忽)Deployment 滚动更新,默认 maxUnavailable=25%,maxSurge=25%PodDisruptionBudget(PDB)Kubernetes 的 PDB 用来约束自愿性中断期间允许离线的副本上限,在节点维护、滚动升级、集群缩容等动作里扮演保险丝的角色。
我司的运维在某一可用区对一批节点执行 kubectl drain 做内核补丁,配合集群自动修复能力,这在我们的标准变更流程里被认为是低风险动作。
因为 没有 PDB 的上限保护,这一 AZ 内 web 的 5 个副本在短时间内被逐出到 0,再新调度上来的副本尚未全部 Ready、Endpoints 还未恢复时,对外流量已打到 Ingress,用户侧开始出现 502/503。
对外暴露的典型错误:
nginx 错误日志:no live upstreams while connecting to upstream(下图为类似报错的日志截屏,标红处为关键字)
错误截图:Istio/Envoy 的团队里,同类现象常体现为 no healthy upstream。这类报错的本质含义是网关找不到任何健康的后端。 这些错误并非 Ingress 本身坏了,而是后端副本在该 AZ 被清空或未就绪,导致上游列表一度为空。社区与厂商文章都将其归因于后端不可达或健康检查失败。
事后复盘时,我们在另一套演练环境复现了当时的关键观测点,以确定结论具有普遍性。
kubectl get endpoints web -n prod 在故障窗口返回 subsets: [](空列表)kubectl drain <node> --ignore-daemonsets --delete-emptydir-data 在没有 PDB 的前提下不会被 API Server 阻止,副本可以被快速驱逐nginx:no live upstreams while connecting to upstream(反向代理层无法找到任何上游)(Server Fault)Istio/Envoy:no healthy upstream(服务网格侧同义错误) 如果 存在 PDB 且设置合理,在 drain 期间常见的命令行报错会变成阻断式的:
error when evicting pods/<pod-name>: Cannot evict pod as it would violate the pod's disruption budget.这是一个真实而经典的错误字符串,出现在试图驱逐会突破 PDB 下限时。
web 配置 PDB + 滚动策略 + HPA 兜底为了让读者可以亲手跑一遍,我们整理了一份可运行的 YAML 套件。在一个干净的命名空间下部署 Deployment、Service、HPA 与 PDB,并演示 drain 前后行为差异。
目标:副本数为 5 的服务,任何自愿性中断期间 至少保留 4 个可用副本。
PDB与Deployment的滚动策略、HPA的minReplicas共同构成三道防线。PDB的写法与行为可参考官方任务文档。
apiVersion: v1
kind: Namespace
metadata:
name: prod
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: prod
spec:
replicas: 5
revisionHistoryLimit: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # 关键:更新时不允许任何不可用
maxSurge: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: ghcr.io/nginxinc/nginx-unprivileged:1.27-alpine
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 3
failureThreshold: 2
livenessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: prod
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080
protocol: TCP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
namespace: prod
spec:
minReplicas: 5 # 兜底:即使缩容也不跌破 5
maxReplicas: 10
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
namespace: prod
spec:
minAvailable: 4 # 关键:自愿性中断时至少保留 4 个可用副本
selector:
matchLabels:
app: web关于
minAvailable与maxUnavailable的选择,社区文章分享过将maxUnavailable用于更直观地表达允许中断的上限,在小副本场景下尤其清晰;两者的取整规则也有说明。
kubectl apply -f suite.yaml,待 Deployment 全部 Ready。drain:针对某一节点执行 kubectl drain <node> --ignore-daemonsets --delete-emptydir-data。kubectl get pdb -n prod 中 web-pdb 的 ALLOWED DISRUPTIONS 会随 Ready 副本动态变化。drain 试图把某个 AZ 的 Ready 数压到 3 时,驱逐会被 API Server 拒绝,并在 kubectl 端可见错误
Cannot evict pod as it would violate the pod's disruption budget.(真实错误串) Endpoints 在任何时刻都不会变成空集合,Ingress 不再出现 no live upstreams/no healthy upstream。 Deployment 的滚动参数只对发布生效;而 drain、节点替换、集群缩容这类动作属于自愿性中断,需要通过 Eviction API 走 PDB 检查。官方 PDB 文档与 OpenShift 的 PDB API 说明都明确了驱逐判定基于 minAvailable/maxUnavailable。
再补一层 HPA minReplicas 的原因也很现实:当业务低谷时 HPA 将 replicas 自动降到很小的值,如果没有 minReplicas 的硬阈,夜间维护配合 drain 仍然可能把可用副本压到临界乃至 0。关于 HPA 行为与建议,官方文档与最佳实践文章都有覆盖。
这类事故里,最有辨识度的字符串与现象如下,可作为 SRE 值班排查的优先断面:
nginx:no live upstreams while connecting to upstream,说明反向代理拿不到任何上游。常见于后端全部不可用或健康检查未通过。 Istio/Envoy:no healthy upstream,含义相同。 kubectl drain 在有 PDB 时的阻断提示:Cannot evict pod as it would violate the pod's disruption budget.(我们在演练环境复刻,命令端确实会返回这一错误) 若要进一步从实现层面理解 nginx 的 no live upstreams,一些解析文章会直接引用源码路径,点出该错误是在上游选择器返回 NGX_BUSY 时写入日志的,这能帮助你将问题迅速归因到上游列表为空或不可用。
Deployment/StatefulSet 必须有 PDB>=5 的 web/api 类服务,建议 minAvailable: 4 或 maxUnavailable: 1。maxUnavailable: 0 更直观。maxUnavailable 的语义见文档与实践文。maxUnavailable: 0,避免发布过程中叠加削减可用副本。配合 PDB 才覆盖到更多场景(发布以外的自愿性中断)。 HPA 兜底minReplicas 不低于 PDB 的 minAvailable。社区经验也强调 minReplicas > minAvailable 的直觉规则。 drain 流程的预检kubectl get pdb -A 检查目标命名空间是否缺少 PDB。--disable-eviction=false(默认)执行 drain,避免跳过 PDB 审查。Cannot evict pod...,说明 PDB 生效,这是安全的阻断信号,不要强制 --force。 nginx 的 no live upstreams、5xx 比例做细粒度告警,扩充到 Istio/Envoy 的 no healthy upstream。根因多在后端不可用而非网关配置本身。 minAvailable 与 maxUnavailable 何时选谁两者都能表达可用性下限,但在小副本时 maxUnavailable: 1 往往更贴合直觉(比如 replicas=2 的控制面 Deployment),运维更容易推算可被中断的上限;社区的经验贴也给出了做法与取整细节。
Kubernetes 给了我们强大的自动化与弹性,但可用性这道题从不自动及格。一个简短的 PDB 清单,配上稳妥的滚动窗口与 HPA 的最小副本,就能在夜间维护时把风险从整面墙倒降到最多倒一块砖。事故之后,我们把 PDB 纳入了上架检查清单,让 drain 在没有保险丝时根本发不出去,也把 HPA minReplicas 调整到和业务 SLO 一致的安全下限。
PDB 与相关资料索引Kubernetes 官方:为你的应用指定 PodDisruptionBudget(含 minAvailable/maxUnavailable 行为与示例) OpenShift 文档:PDB policy/v1 的判定语义与字段说明(适用于理解取整与判定过程) HPA 官方文档与演练(建议结合 minReplicas 使用) PDB 选 maxUnavailable 的思考与经验分享(在小副本时更直观) Istio/Envoy 的 no healthy upstream 与 nginx 的 no live upstreams(本质都是后端不可用) web 设置 PDB: apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
namespace: prod
spec:
minAvailable: 4
selector:
matchLabels:
app: webDeployment 滚动策略设为 maxUnavailable: 0,将 HPA minReplicas 设为 >= PDB.minAvailable。drain 前先看 kubectl get pdb -n prod;在缺失 PDB 的命名空间禁止执行变更。drain 时若看到
Cannot evict pod as it would violate the pod's disruption budget.
说明保护起效,不要用 --force 绕过。 只要还在
Kubernetes上跑关键业务,用一张PDB的小纸条把门拴上,值回票价。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。