跳转至

Kubernetes、IaC、GitOps 与云事故

CPU limit 与探针耦合导致 Kubernetes 服务重启风暴

影响

核心订单 API1 在 18 分钟内 p992 从 120ms 升至 4.2s,约 7.6% 请求超时;40 个副本中最多 27 个同时 NotReady,12 个发生 liveness3 重启。数据未丢失,但客户端重试使下游数据库 QPS 增加 2.3 倍。

相关技术说明

CPU CFS quota4 可让容器在一个调度周期用尽额度后被 throttled;分钟级 CPU 平均值可能不高。探针也需要 CPU 调度,若 liveness 与业务线程共享受限 cgroup5,压力时探针超时会重启本可自行恢复的进程。重启又会触发 JIT6、缓存预热和连接重建,构成正反馈。

readiness、liveness 和 startup 的决策副作用不同:readiness 摘流但保留进程,liveness 杀进程。应把远端依赖排除出活性判断,并用 cpu.stat、PSI、probe duration、container lastState 和下游连接数验证,而不能以节点总 CPU 作为排除证据。

实践经验

  • 10:02 发布新序列化逻辑,单请求 CPU 增加约 35%;10:07 p99 上升但节点 CPU 仅 58%;10:10 首批 liveness 超时;10:14 HPA 扩容,新副本预热使数据库连接激增;10:20 停止发布;10:28 服务稳定。
  • 证据链:Pod events 明确 Liveness probe failedcpu.stat 的 throttled 时间与延迟同向;线程 dump 中请求线程为 runnable;数据库无先发异常,连接峰值晚于重启峰值。由此排除“数据库先故障”和“节点 CPU 饱和”。

止血

暂停 rollout 并回退序列化版本;暂时提高 CPU limit、扩大健康旧副本,但限制 HPA 最大扩容速率;将 liveness 临时改成本地轻量检查并提高失败阈值,readiness 继续负责摘流;数据库连接池设置硬上限。每项动作分批执行,以错误率与连接数作为继续条件。

根因与促成因素

直接根因是新代码 CPU 成本上升,在 1 核 limit 下频繁 throttling;liveness 超时过短并与业务 CPU 竞争,使延迟故障升级为重启风暴。促成因素包括压测未启用生产 cgroup 限制、HPA 只看平均 CPU、启动预热无速率限制、告警缺少 throttling 与 probe failure。

永久修复

以代表性请求压测确定 request,CPU limit 按邻居隔离需求重新评估;将 startup/readiness/liveness 拆分,活性只查本地事件循环;优化序列化热点与预热,连接池按全局数据库预算分配;新增 throttled ratio、PSI、Ready endpoint 和重启原因告警,发布门禁观察至少一个预热周期。

验证与复盘

在预生产以同样 quota 重放峰值并人为降低 CPU,确认只发生受控 readiness 摘流、不发生 liveness 风暴;生产 canary 5%→20%→50% 验证 p99、throttling、连接数与错误预算。复盘行动项均有 owner、期限和验收指标;同时记录提高 limit 带来的节点超售风险,补 N-1 容量验证。

CoreDNS 5 秒延迟与节点 conntrack 压力复合故障

影响

约 30% 节点上的新建 HTTP 连接间歇多出约 5 秒,登录和支付依赖超时;已有长连接大多正常。总错误率 4.1%,故障持续 36 分钟,集中在刚扩容的两个节点池。

相关技术说明

Pod DNS 查询通常先经集群 DNS Service 到 CoreDNS7,再转发未知域到上游。ndots 和搜索域可能把一个短名称扩展成多次查询;UDP 回包丢失后 resolver 按超时重试,常呈现固定秒级台阶。节点 conntrack8 表接近上限、哈希冲突或 UDP 状态问题可让 DNS 先受影响,但 CoreDNS 本身 QPS/CPU 正常。

排查要对齐 Pod、节点、CoreDNS 和上游四处证据:Pod resolv.conf 与查询序列、源节点抓包、CoreDNS request/rcode/latency、节点 conntrack 指标。只在 CoreDNS Pod 内执行一次 dig 不能复现客户端节点数据面,也不能证明 Service 路径健康。

实践经验

  • 13:40 新节点池开始承载批任务;13:47 应用依赖超时告警;13:55 CoreDNS 扩容无明显效果;14:03 按节点聚合发现异常集中;14:09 抓包显示首次 UDP 响应到节点后未交付 Pod,约 5 秒后重试成功;14:16 限制批任务,14:23 恢复。
  • 证据链:CoreDNS 端到端处理 p99 小于 20ms且 NOERROR;异常节点 conntrack 使用率超过 95%、insert failed 增长;应用短名称产生 4 次搜索查询。由此确定节点连接跟踪压力为主要根因,搜索放大为促成因素。

止血

停止/迁移产生大量短连接的批任务,cordon 异常节点并分批替换;在容量验证后调整 conntrack 上限;关键外部域改用带尾点的 FQDN,扩 CoreDNS 仅作为冗余而非根因修复。避免同时清空所有 conntrack,以免中断现有连接。

根因与促成因素

批任务关闭连接复用且多层重试,使新节点 conntrack 表快速耗尽;节点镜像的 conntrack 参数低于旧池。ndots/搜索域令外部短名称查询放大,监控只看 CoreDNS CPU 和 SERVFAIL,未按节点观察丢包与 insert failure。

永久修复

统一节点镜像网络参数与容量基线,按连接模型而非内存经验设置上限;应用强制连接池和重试预算;优化 DNS 搜索/ndots 使用,外部域使用 FQDN;部署按节点的 DNS 合成探针与 conntrack 饱和告警,新节点池上线前执行短连接压力测试。

验证与复盘

在隔离节点逐步提升短连接率,验证 DNS p99、丢包、conntrack insert failure 与应用成功率;模拟上游 DNS TCP fallback。复盘明确“扩 CoreDNS 无效”是重要反证,更新 runbook 要求先按源节点分组,避免控制面指标掩盖节点数据面故障。

节点维护绕过 PDB 导致有状态服务失去 quorum

影响

三副本配置服务在节点池升级期间丢失两个投票成员,写请求停止 22 分钟,依赖它的 14 个服务无法更新配置;读取依靠缓存维持。没有确认的数据丢失,但有 9 分钟管理面不可用。

相关技术说明

PDB9 只限制通过 eviction API 的自愿中断,并根据健康副本计算 disruptionsAllowed;强制删除、节点突然故障和某些控制器动作不受保护。quorum10 系统三成员必须至少两个可通信,副本数、拓扑分散、PDB 和维护批次共同构成可用性条件,任一项不能替代其他项。

节点 drain 返回 eviction 429 是安全信号,说明当前健康或预算不足;自动化若超时后 --force 删除,就把可控维护变成非自愿中断。有状态服务还需确认成员真正完成日志追赶,readiness 只有确实包含该条件才可作为依据;无论如何,应用级 quorum/lag 检查都应参与维护门禁。

实践经验

  • 01:00 升级首节点,成员 A 被替换后仍在追赶且保持 Ready=False;01:06 第二节点 drain 被 PDB 正确阻止;01:11 自动脚本超时并强制删除成员 B;01:12 quorum 丢失;01:18 停止节点批次;01:24 恢复 B 所在旧节点;01:34 写入恢复。
  • 证据链:维护日志记录 eviction 429 后执行强删;PDB 当时 disruptionsAllowed=0;服务指标显示 A 的 replication lag 尚未归零,Pod condition 同时为 Ready=False;三个副本中两个原本位于同一节点池维护批次。PDB 与 readiness 给出了相互一致的阻断证据,故障来自自动化绕过而非 PDB 误判。

止血

停止所有节点升级与控制器自动重调度,保留数据目录;选择日志最完整的两个成员恢复,确认 cluster ID/term 后形成 quorum,再逐个接入第三成员。禁止在未确认旧成员状态前新建空集群,避免选错权威数据。

根因与促成因素

直接根因是维护脚本把 PDB 阻塞当普通超时并强制删除,忽略了 Ready=False 与 replication lag 的明确证据。促成因素是副本拓扑与节点升级域重合、升级前无独立的 quorum/N-1 容量前置检查、脚本沿用无状态服务模板;PDB 本身按设计工作。

永久修复

维护控制器遇 PDB 阻塞立即停止并升级人工,禁止自动降级为强删;为该服务定义反亲和/拓扑分散和专用维护策略;持续验证 readiness 的成员角色/追赶阈值,升级入口独立检查 quorum、lag、PDB 和目标节点;建立备份恢复与成员替换 runbook。

验证与复盘

在演练集群分别模拟单节点失效、追赶中 drain、PDB 429 和维护脚本中断,证明任何时刻仅一个投票成员不可用。复盘把“强制参数”列为高风险动作,需要双人批准和资源 allowlist;接受维护时长增加,以换取 quorum 安全。

准入 webhook 证书过期导致集群无法发布

影响

生产集群已有工作负载继续运行,但 41 分钟内所有匹配的 Pod、Deployment 和 Job 新建/更新被拒;故障副本无法替换,两个服务容量下降到 60%。平台发布与自动扩容同时停滞。

相关技术说明

admission webhook 位于 API 写路径,API server 按 webhook clientConfig 连接服务并验证 CA bundle。failurePolicy: Fail 在超时、TLS 或服务不可达时拒绝请求,适合安全关键规则,却把 webhook 可用性变成控制面写入依赖。若 webhook 自身 Pod 更新也被规则匹配,会形成自举死锁。

证书有服务端证书、CA bundle 与轮换控制器多个生命周期;仅监控 Pod Ready 不能发现未来过期。可靠设计需要多副本跨故障域、短超时、精准 selector、排除自身/系统恢复路径、证书到期告警和受审计 break-glass。

实践经验

  • 09:00 证书过期;09:02 HPA 扩容创建 Pod 被拒;09:07 发布告警;09:12 API 审计显示 webhook TLS certificate has expired;09:17 尝试重启 webhook 也被自身规则拒绝;09:24 启用预案缩小 match 范围;09:31 更新证书;09:41 恢复全部策略。
  • 证据链:API server 其他读写正常;拒绝仅匹配特定 webhook;endpoint 仍存在但 TLS 握手失败;证书 notAfter 与事故开始一致,排除网络和 etcd。

止血

通过预先授权的 break-glass 身份,将 webhook selector 临时排除其自身 namespace 和受影响恢复工作负载,而非全局改 Ignore;更新证书与 CA bundle,先对测试 namespace 验证,再恢复匹配范围。所有绕过期间创建对象单独审计并事后重新校验。

根因与促成因素

证书轮换 Job 因权限变更连续失败,但只有任务失败告警且无人接收;证书无提前到期告警。failurePolicy=Fail 合理,但 webhook 匹配自身、单一证书流程和无恢复 selector 放大了故障。

永久修复

证书到期 30/14/7 天多级告警,轮换后做真实 AdmissionReview;webhook 跨 AZ 多副本,排除自身与紧急 namespace;策略版本先 audit/canary;break-glass 修改受 Git/IaC 管理并自动过期。为安全关键 webhook 建 SLO 与 API 延迟预算。

验证与复盘

演练证书过期、服务不可达、CA 不匹配和慢响应,验证 fail-closed 期间仍能恢复 webhook 且普通绕过被拒。复盘审计绕过窗口内对象,确认策略重新扫描无违规;将证书轮换从“运维任务”提升为控制面依赖治理。

Terraform state 迁移错误导致生产路由被计划删除

影响

网络模块拆分后,一次生产 apply 删除三条私网路由,其中一条被删除 6 分钟,导致支付服务到数据库跨账号连接中断,约 2.4% 请求失败。另两条因云 API 依赖顺序尚未执行而被及时阻止。

相关技术说明

Terraform/OpenTofu 以资源地址和 state 映射判断对象所有权,并要求一个远端对象只绑定到一个资源地址。目标已 import 而源 state 仍保留映射会形成双重管理;只从源配置删除资源、却没有用无销毁的 removed block 或受控 state rm 释放映射时,源 plan 会计划销毁该对象——无论目标 state 是否也已 import。state 锁只保护单个 state,不能协调两个后端。

跨 state 迁移应冻结两个后端的全部 writer,建立资源地址—云对象 ID 清单,并在克隆 state 中先验证目标配置可无差异接管。生产窗口内,优先让源 state 无销毁地忘记对象、验证云对象仍存在,再立即 import 到目标;若工具明确支持直接跨 state 移动,也须受同一全局锁和清单约束。两端最终都要做完整 plan。云路由等共享资源还应有 deletion policy gate,不能依赖人眼从长计划中发现三行删除。

实践经验

  • 15:00 合并模块拆分;15:08 目标 state import 作业只成功 17/18 条,但脚本错误返回 0;15:12 源配置删除全部 18 条;15:20 审批摘要只显示“18 changes”;15:26 apply 删除首条;15:27 网络 SLI 告警并自动停止流水线;15:33 恢复路由。
  • 证据链:目标 state 缺少目标对象 ID,源 state 仍包含全部资源;plan JSON 明确一条关键路由 delete;云审计显示唯一删除来自 CI 角色。无控制台漂移或 provider 错误。

止血

立即取消 apply 并冻结源、目标两个 state 的 writer;从最后批准配置重建被删路由,验证双向流量;备份两份 state 后逐项对账。没有直接回滚整个 state,因为真实 API 已部分成功,盲目恢复旧文件会再次制造映射差异。

根因与促成因素

迁移脚本采用目标先 import,既制造了 17 个对象的双重映射,又吞掉第 18 个 import 失败;随后仅删除源配置而没有无销毁地释放源映射。源与目标流水线无跨 state 互斥,审批 UI 未将 destroy 单独高亮,共享路由也无删除门禁。时间压力使团队把迁移当代码重构而非资源所有权转移。

永久修复

跨 state 迁移采用显式 manifest、双人确认和全局变更锁:先在克隆 state 验证目标配置;生产中每批执行“源端无销毁释放映射→确认远端对象仍存在→目标 import→两端完整 plan”。工具支持时以 removed block 的 destroy = false 留下可审阅记录,否则使用受控 state 操作;禁止目标先 import 形成双重所有权。政策拒绝生产路由 delete,除非绑定限时批准;plan 摘要显示 delete/replace 与资源 ID,迁移脚本任何部分失败即非零退出。

验证与复盘

在克隆 state 与沙箱账号重放迁移,包括中途终止和重复执行,证明可恢复且不会双重管理;生产最终两份完整 plan 无未解释差异,连接合成测试通过。复盘新增“state 操作不是普通重构”的培训和 runbook,测量政策门禁覆盖率。

GitOps 错误 prune 扩散到多个租户

影响

ApplicationSet 生成器变更后,三个集群对应的 22 个生成式 Application 从期望集合中消失;canary 的 ApplicationSet 控制器先删除 Application 对象,其资源 finalizer 随后级联删除 63 个 Deployment 和 Service,造成 11 分钟局部不可用。组织级删除预算阻止另外两个集群继续执行,PVC 因保留策略未删除,数据保留。

相关技术说明

必须区分两条删除路径:现有 Application 的自动 prune,会删除其渲染期望中已不存在的 live 子资源;ApplicationSet 生成器返回空集合,则可能让控制器删除它先前生成的 Application。后一路径由 ApplicationSet 的 applicationsSync 删除策略约束;若 Application 带资源删除 finalizer 且未启用 preserveResourcesOnDeletion,删除 Application 还会级联删除其工作负载。两者不能统称为同一个 prune 开关。

安全发布需区分 generator/render error 与合法 empty、比较 Application 数和子资源数,并按集群/租户分批。生产 ApplicationSet 可按风险采用禁止删除 Application 的 applicationsSync: create-update,并评估 preserveResourcesOnDeletion: true;现有 Application 的 auto-sync、自愈和 prune 仍需独立门禁。Git revert 能恢复声明对象,却不能保证恢复已删除 PVC、LoadBalancer 地址或外部控制器副作用,紧急手工恢复前要暂停相应 reconcile。

实践经验

  • 11:00 合并目录重命名;11:03 generator 将路径缺失错误当作合法空参数集;11:05 canary 批次中的生成式 Application 开始被删除,资源 finalizer 触发级联删除;11:06 删除数量门禁暂停其他集群;11:08 业务告警;11:11 全局暂停 reconcile;11:16 恢复旧 commit;11:22 canary 核心服务恢复。
  • 证据链:Git diff 只有路径变化;ApplicationSet controller 日志显示生成的 Application 数从 22 降至 0;API 审计先出现 Application 删除,随后是同一控制器身份删除子资源;无租户手工操作。PVC 的保留策略解释了数据未丢失。

止血

暂停 ApplicationSet 删除动作及受影响 Application 的自动同步,但保留只读观测;恢复上一已签名 Git commit 与 generator 版本,离线核对 Application 数和每个 Application 的渲染对象数。先在 canary 重建 Application,再以 prune 关闭的手动同步恢复身份、ConfigMap/Secret 引用和 Deployment,最后恢复入口;保留的 PVC 重新绑定前验证 owner 和数据。

根因与促成因素

生成插件把路径不存在当空成功;ApplicationSet 允许删除生成的 Application,且未保留 Application 管理的资源,finalizer 因而级联删除。重构 PR 未包含 Application 集与子资源集两层 diff。促成因素是多个集群同时追踪同一分支、ApplicationSet 控制器权限过宽;已有删除预算降低了最终爆炸半径。

永久修复

生成失败必须 hard error;CI 同时存档生成的 Application 清单和各 Application 渲染清单,对数量、关键 kind、目标集群和高风险删除设门禁。生产 ApplicationSet 默认使用不允许删除 Application 的策略,删除需独立审批;按数据恢复要求启用资源保留,Application 级 prune 另设保护。环境按 digest 渐进晋级,控制器身份按环境拆分,PVC/namespace/CRD 默认禁止自动删除,并提供受审计恢复清单。

验证与复盘

测试路径缺失、仓库 403、空租户和插件崩溃,确认错误不会被接受为合法空集合;分别演练“ApplicationSet 删除 Application”和“Application prune 子资源”,验证两套开关与审计证据。再从 Git+保留数据恢复 canary 并记录实际 RTO。复盘肯定门禁有效但不以“只影响 canary”结案,继续修复空集合语义和权限半径。

NAT 端口耗尽叠加重试导致跨云 API 全面超时

影响

私有子网内的同步服务访问第三方风控 API 时连接超时,支付授权成功率降至 88%;同一 NAT 出口上的镜像拉取和监控上报也受影响。故障 27 分钟,无数据丢失,但重试队列积压 34 万条。

相关技术说明

NAT 对每个连接维护源地址/端口映射;大量到同一目的地址的短连接会耗尽可用端口或连接跟踪容量,带宽图却可能不高。客户端、sidecar 和网关多层重试会成倍增加连接,超时又让旧映射更久占用。扩带宽不一定增加特定目的的端口可用量。

诊断需关联 NAT 分配失败/连接数、按目的流量、应用 connect time、连接池命中和重试 attempt。缓解优先减少放大、复用连接、隔离关键出口,再扩 IP/网关;目标 API 限流也要区别于本地 NAT 超时,前者通常有 HTTP 响应,后者多停在 connect。

实践经验

  • 16:00 批量风控回查任务启动;16:05 NAT active connections 激增;16:08 首次 connect timeout;16:11 服务 mesh 重试使新连接翻三倍;16:15 业务告警;16:18 停批并关闭一层重试;16:24 连接分配恢复;16:32 支付 SLI 正常。
  • 证据链:第三方未收到失败请求且无 429;NAT 端口分配错误与 connect timeout 一一对应;应用每请求新建 TLS 连接,代理还有两次 retry;CPU、DNS 与第三方状态正常。

止血

暂停非关键回查任务,为支付保留出口并限制并发;关闭代理层写请求重试,应用启用连接复用;扩展独立 NAT 地址前先验证路由。积压按令牌桶逐步释放,防恢复后再次打满。没有全量切到另一区域,因为该区共享同一第三方限额且会造成更大重连。

根因与促成因素

新批任务绕过连接池,对单一目的创建海量短连接;NAT 与在线支付共享故障域;SDK 和 mesh 重试所有权不清。容量测试只按 Mbps,未按连接率、目的热点和映射保持时间建模。

永久修复

在线/批处理使用不同出口与配额,强制 HTTP/2 或 keep-alive;统一重试预算和幂等策略;监控 NAT 端口/连接失败、每目的连接率及池复用率;批任务有速率和熔断。评估 PrivateLink/供应商专线时同时考虑对方能力与成本。

验证与复盘

压测以目标峰值 1.5 倍模拟短连接、慢响应和第三方 429,确认在线流量隔离且积压能有界恢复;验证 NAT 单 AZ 故障的路由。复盘将“连接数/秒”加入容量评审,记录新增出口 IP 的白名单与治理成本。

多区域故障转移出现双主风险且备用备份不可解密

影响

主区域数据库控制面故障后,自动化在备用区提升副本;主区网络局部恢复时旧主仍接受内部写入,形成 4 分钟分叉。团队尝试从备用备份重建仲裁节点时又发现 KMS 权限缺失。对外写入冻结 49 分钟,最终通过事务对账无永久财务损失。

相关技术说明

多区域 failover 的核心是单写权威与 fencing。网络分区时无法仅凭“看不到旧主”证明它已停止写;必须通过独立仲裁、撤销写入租约、存储 fencing 或客户端路由保证只有一个主。异步复制的 lag 定义实际 RPO,提升点之后回切需要处理新写入,不能仅反向 DNS。

备份可恢复性还依赖 KMS key、IAM、网络和软件版本。跨区域复制了密文不代表目标区域身份有解密权;过宽复制 key 又增加攻击面。因此恢复演练要在隔离账号用目标身份完成解密和业务验证。

实践经验

  • 03:10 主区控制面告警;03:16 自动化因连续探测失败提升备用;03:19 主区部分网络恢复,内部批任务仍向旧主写;03:23 写入差异告警并全局冻结;03:31 备用备份恢复报 KMS AccessDenied;03:42 建立受控临时解密授权;03:52 选定新主并合并补偿;03:59 恢复写。
  • 证据链:两边数据库 timeline/LSN 显示共同祖先后分叉;DNS 日志显示外部已切换但内部固定 endpoint 未切;KMS 审计明确目标恢复角色无 decrypt。故障不是单一云区域不可用,而是切换协议和恢复依赖缺陷。

止血

立即冻结所有写路径,包括内部批任务与运维脚本;通过网络与数据库租约 fence 旧主,比较两边事务日志,选择包含已确认外部交易的新主,对旧分支做业务补偿而非块级合并。KMS 临时授权采用双人审批、限定备份 key 和一小时有效,并记录所有解密。

根因与促成因素

自动 failover 只有“连续探测失败”条件,没有独立仲裁和旧主 fencing;内部客户端使用硬编码区域端点。备份复制流程未验证目标恢复角色与 key,演练只检查文件存在。促成因素还包括 RPO/RTO 文档未定义分区时牺牲写可用性的选择。

永久修复

建立唯一灾备编排器与 quorum/租约 fencing,所有写客户端使用可切换代理并支持只读降级;在提升前检查复制位点和数据完整性,在回切前反向复制。跨账号/区域备份使用独立受管 key,自动化每周在隔离环境恢复。业务明确网络分区时选择一致性优先。

验证与复盘

演练完整区域断开、控制面断而数据面通、旧主孤岛、KMS 拒绝和内部 DNS 未切五种场景;验收为任一时刻仅一写主、实际 RPO/RTO达标、备份可解密且关键交易校验通过。复盘把“状态页区域故障”与自身多重促成因素分开,行动项覆盖应用、平台、身份和数据团队。


  1. API 是 Application Programming Interface(应用程序编程接口),定义软件组件之间如何发起请求和交换数据。 

  2. p99 是第 99 百分位数,常用于观察少数最慢请求造成的尾延迟。 

  3. Kubernetes 中 readiness 决定 Pod 是否接收流量,liveness 决定是否重启容器,startup 用于保护尚未完成启动的应用。 

  4. CFS quota 是 Linux 完全公平调度器的周期性 CPU 配额机制,额度耗尽后会暂时限制任务运行。 

  5. cgroup 是 Linux control group(控制组),用于限制并统计一组进程的 CPU、内存与 I/O 等资源。 

  6. JIT 是 Just-In-Time compilation(即时编译),在程序运行期间把代码编译为机器指令,启动或预热阶段可能额外消耗 CPU。 

  7. CoreDNS 是可通过插件扩展的 DNS 服务器,也是 Kubernetes 常用的集群 DNS 实现。参见 CoreDNS 官方文档。 

  8. conntrack 是 Linux 内核的连接跟踪机制,为有状态防火墙和 NAT 保存会话状态。 

  9. PDB 是 Pod Disruption Budget(Pod 中断预算),限制自愿中断期间可同时不可用的 Pod 数量。 

  10. quorum(法定人数)是分布式系统完成一致决策所需的最小参与者数量;多数派系统通常要求超过半数成员可通信。