Skip to content

第六部分 大规模故障案例与最佳实践

前五部分解决了"怎么建、怎么配、怎么管、怎么看、怎么调"的问题,本部分解决最后一个问题:出事的时候怎么办,以及怎么让事情不出

当集群规模从几十节点增长到数千节点,故障的形态会发生质变:小问题会被放大(一个控制器的 bug 可以制造 event 风暴)、小概率事件会变成必然事件(数千块磁盘每天总有几块出问题)、人为操作会成为最大风险源(一次批量升级可以同时打挂几百个节点)。

本部分的全部案例均来自真实生产环境复盘(部分案例的完整记录可参考本仓库 k8s-issues/ 目录),每个案例按 现象 → 根因 → 排查过程 → 解决方案 → 经验教训 五段式复盘。读完本部分,你应该能建立起大规模集群的"故障直觉":看到某类指标异动,就能立刻锁定排查方向。


第 1 章 大规模典型故障案例库

本章共 15 个案例,按故障域分为五组:

分组案例编号主题
etcd1.1 ~ 1.4DB 打满只读、碎片过多、quorum 丢失、leader 频繁切换
kube-apiserver1.5 ~ 1.8inflight 打满、event 风暴、审计日志打爆磁盘、全量 list 拖垮
节点1.9 ~ 1.11CNI 升级事故、证书轮换事故、conntrack/PID/FD 耗尽
网络1.12 ~ 1.13CoreDNS DNS 风暴、NetworkPolicy 误配置全网隔离
镜像与升级1.14 ~ 1.15镜像拉取惊群、批量升级 storm

1.1 案例一:etcd DB 打满 8GB,集群进入"假性只读"

现象

某 3000 节点生产集群,凌晨 4 点告警群同时收到:

  • etcdBackendQuotaExceeded:etcd 后端存储用量超过配额 95%
  • KubeAPIDown 间歇性触发:apiserver 读写超时
  • 业务侧反馈:kubectl apply 大量返回 etcdserver: mvcc: database space exceeded

诡异的是:读请求大部分正常,写请求全部失败,集群看起来"还活着"但任何变更都无法落地。

根因

etcd 默认后端存储配额(quota-backend-bytes)为 2GB,该集群按大规模实践调大到 8GB。但由于以下因素叠加,DB 在三个月内从 3GB 缓慢增长到 8GB 触发配额上限:

  1. 某平台团队的 Operator 存在 bug,每 30 秒全量更新一次 CR 对象的 status(内容几乎不变,但每次 update 都产生新 revision);
  2. etcd 是 MVCC 存储,历史 revision 不会被自动删除,只有 compact 才清理;
  3. RKE2 默认开启自动 compaction(etcd-auto-compaction-retention),但 compact 只释放逻辑空间,物理文件大小不会缩小
  4. 大量 CR(每个 200KB+)叠加高频 update,revision 增长速度快于 compaction 清理速度。

排查过程

bash
# 1. 查看 etcd DB 实际大小(关键指标)
etcdctl --cacert /var/lib/rancher/rke2/server/tls/etcd/server-ca.crt \
  --cert /var/lib/rancher/rke2/server/tls/etcd/server-client.crt \
  --key  /var/lib/rancher/rke2/server/tls/etcd/server-client.key \
  --endpoints https://127.0.0.1:2379 endpoint status -w table

输出(表格关键列):

text
+--------------------+------------------+---------+-------------------+-----------+------------+
| ENDPOINT           | VERSION          | DB SIZE | DB SIZE IN USE    | IS LEADER | RAFT INDEX |
+--------------------+------------------+---------+-------------------+-----------+------------+
| https://10.0.1.11:2379 | 3.5.x       | 8.1 GB  | 4.2 GB            | true      | 912834712  |
| https://10.0.1.12:2379 | 3.5.x       | 8.1 GB  | 4.2 GB            | false     | 912834712  |
| https://10.0.1.13:2379 | 3.5.x       | 8.1 GB  | 4.2 GB            | false     | 912834712  |
+--------------------+------------------+---------+-------------------+-----------+------------+

关键判读DB SIZE 8.1GB 已超 8GB 配额,但 DB SIZE IN USE 只有 4.2GB —— 一半是碎片/历史空间,这是典型的"虚胖"。

bash
# 2. 看 Prometheus 指标确认增长曲线
etcd_server_quota_backend_bytes          # 配额:8589934592 (8GB)
etcd_mvcc_db_total_size_in_bytes         # 实际大小:贴着配额线
etcd_mvcc_db_total_size_in_use_in_bytes  # 在用大小:仅一半

# 3. 找出"谁写得多":按对象类型统计
kubectl get --raw /metrics | grep apiserver_storage_objects | sort -k2 -n -r | head -20

发现某 CRD 的 apiserver_storage_objects 数量不算最大,但配合 apiserver 审计日志确认该 CRD 的 UPDATE QPS 高达 30+/s,锁定了肇事 Operator。

解决方案

紧急处置(30 分钟内恢复写入):

bash
# 1. 临时提高配额(治标,先让集群能写)
# 修改 /etc/rancher/rke2/config.yaml,追加:
#   etcd-arg:
#     - "quota-backend-bytes=17179869184"   # 临时提到 16GB
systemctl restart rke2-server   # 逐台滚动,严禁同时重启

# 2. 手动 compact + defrag 回收物理空间
REV=$(etcdctl ... endpoint status --write-out="json" | jq '.[0].Status.header.revision')
etcdctl ... compact $REV
etcdctl ... defrag --cluster    # 注意:defrag 会短暂阻塞该成员,逐台执行

defrag 后 DB SIZE 从 8.1GB 降到 4.3GB,集群恢复正常写入。

根治措施:

  1. 修复 Operator 的"无变化也 update status"的 bug(改用 UpdateStatus 前先做 DeepEqual 判断);
  2. 对该 CRD 启用 API Priority and Fairness 限流;
  3. 配置告警:etcd_mvcc_db_total_size_in_bytes / etcd_server_quota_backend_bytes > 0.8 提前告警,不要等只读;
  4. 建立每月一次定期 defrag 的运维例行任务(详见附录速查命令)。

经验教训

  • etcd 的 8GB 不是"可用容量",是"红线"。水位监控必须按 80% 阈值提前介入。
  • DB SIZEDB SIZE IN USE 两个指标必须同时监控:前者涨、后者不涨 = 碎片问题;两者一起涨 = 真实数据增长,要找写入源头。
  • 高频 update status 的 Operator 是 etcd 的头号隐形杀手,上线前应做"写放大评估"。
  • defrag 会锁单成员,3 节点 etcd 必须逐台做,且避开业务高峰。

1.2 案例二:etcd 碎片过多,全集群"变慢"但无人告警

现象

某 1500 节点集群,用户陆续反馈"kubectl 偶尔卡 2~3 秒",Pod 创建延迟 P99 从 800ms 涨到 4s。但所有常规告警(节点 NotReady、apiserver down、etcd down)全部静默,Dashboard 上 CPU/内存/磁盘一切正常。

根因

etcd 长期运行后产生大量内部碎片,bbolt 的 freelist 膨胀,导致每次写事务要扫描更多页面,写延迟和 apply 延迟缓慢劣化。这类故障没有阈值可告警,纯靠 P99 延迟趋势才能发现。

排查过程

bash
# 1. 看 apiserver 的 etcd 请求延迟分层
histogram_quantile(0.99, rate(apiserver_storage_duration_seconds_bucket[5m]))
# 发现 PUT 操作 P99 从 50ms 涨到 900ms,而 GET 基本正常 —— 指向 etcd 写路径

# 2. 看 etcd 自身延迟指标
histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))   # 正常
histogram_quantile(0.99, rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])) # 涨到 1.2s,异常!

# 3. 对比 DB SIZE 与 DB SIZE IN USE
etcdctl ... endpoint status -w table
# DB SIZE 6.8GB,IN USE 只有 2.1GB —— 碎片率 69%

判读要点backend_commit_duration 高 + wal_fsync 正常 = 瓶颈在 bbolt 后端而非磁盘 IO,结合碎片率即可确诊。

解决方案

bash
# 逐台 defrag(follower 先做,leader 最后)
etcdctl ... --endpoints https://10.0.1.12:2379 defrag
etcdctl ... --endpoints https://10.0.1.13:2379 defrag
etcdctl ... --endpoints https://10.0.1.11:2379 defrag   # leader 最后做

defrag 后 backend commit P99 从 1.2s 回落到 30ms,Pod 创建延迟恢复正常。

经验教训

  • 大规模集群必须把 etcd_disk_backend_commit_duration_seconds P99 纳入趋势告警(如连续 3 天环比增长 50%),而不是只看绝对阈值。
  • 建立例行维护:每月低峰期逐台 defrag,可写成 systemd timer + 脚本自动化。
  • "集群变慢但无告警"是第一类需要靠 SLO 劣化(而非组件 down)来发现的故障,这正是第四部分可观测性体系强调 SLI/SLO 的原因。

1.3 案例三:三 control-plane 节点同时宕机,etcd quorum 丢失灾后恢复

现象

某 RKE2 集群(v1.35.6+rke2r1,Rocky Linux 9.8)因机房电力事故,3 个 control-plane 节点(192.168.122.78 / .69 / .136同时断电。电力恢复后三个节点的 rke2-server 均无法启动,反复报错:

text
error "etcdserver: request timed out"
transport: authentication handshake failed: context deadline exceeded
dial tcp 127.0.0.1:2379: connect: connection refused

集群 API 完全不可用,所有 kubectl 命令返回 connection refused。这是大规模集群最严重的故障形态:控制平面全灭。

完整恢复记录见本仓库:k8s-issues/rke2-disaster-recovery-control-plane-quorum-loss.md

根因

etcd 基于 Raft,3 节点集群需要至少 2 个成员存活才能达成 quorum。三个成员同时掉线后,各自磁盘上的 raft 状态停留在不同进度,重启后互相无法选主,quorum 永久丢失,只能强制以单节点模式重建集群。

排查过程

bash
# 1. 确认三台 rke2-server 状态与日志
systemctl status rke2-server
journalctl -u rke2-server -n 200 --no-pager | grep -iE "etcd|raft|quorum"

# 2. 确认 etcd 数据目录完整性(挑选数据最新的节点作为恢复源)
ls -lh /var/lib/rancher/rke2/server/db/etcd/member/snap/
# 对比三台节点的 wal/snap 时间戳与文件大小,选 raft index 最高的节点(本例为 78)

解决方案

恢复流程(完整步骤见上述仓库文档,此处给出主干):

bash
# 1. 三台节点全部清理残留进程
rke2-killall.sh

# 2. 在恢复源节点(78)备份 etcd 数据
mkdir -p /var/lib/rancher/rke2/server/db/etcd.bak.$(date +%Y%m%d%H%M%S)
cp -a /var/lib/rancher/rke2/server/db/etcd/* /var/lib/rancher/rke2/server/db/etcd.bak.*/

# 3. 编辑 /var/lib/rancher/rke2/server/db/etcd/config,追加:
#    force-new-cluster: true
#    该配置忽略旧成员信息,强制将数据降级为单节点 etcd 集群

# 4. 重启 rke2-server,确认单节点 etcd 恢复
systemctl restart rke2-server

# 5. 用 etcdctl 移除 69、136 的旧成员记录
etcdctl --cacert .../etcd/server-ca.crt --cert .../etcd/server-client.crt \
  --key .../etcd/server-client.key --endpoints https://127.0.0.1:2379 \
  member list -w table
etcdctl ... member remove <旧成员ID>

# 6. 清理 69/136 节点的 etcd 数据后,以全新成员身份重新 join
#    (清空 /var/lib/rancher/rke2/server/db/etcd,重启 rke2-server 走正常 join 流程)

恢复后验证:kubectl get nodes 正常、etcdctl endpoint health 三成员全 healthy、raft index 追平。

经验教训

  • 三节点 etcd 必须做机房级反亲和:本次事故的直接诱因是三台 control-plane 在同一机柜同一 PDU。跨机柜/跨可用区部署是硬性要求。
  • force-new-cluster 是最后的救命稻草,但操作前必须先冷备份整个 etcd 目录,一旦操作失误连回滚的机会都没有。
  • RKE2 的 etcd-snapshot-schedule-cron + etcd-snapshot-retention(见 rke2/server-config.yaml)必须配置并把快照异地备份;有快照时优先走 snapshot restore 而非 force-new-cluster。
  • 恢复源的选取标准是 raft index 最高(数据最新)的成员,选错会丢数据。
  • 定期演练!建议每季度在影子环境演练一次 quorum 丢失恢复,本案例的恢复耗时 4 小时,其中 2 小时花在"没人记得命令"上。

1.4 案例四:跨机房网络抖动导致 etcd leader 频繁切换

现象

某集群为容灾将 3 节点 etcd 跨两个机房部署(2+1),近一周 Prometheus 频繁触发 etcdLeaderChanges 告警:etcd_server_leader_changes_seen_total 一天内增长 20+ 次。每次 leader 切换瞬间,apiserver 出现约 1~2 秒的写入超时毛刺,业务侧表现为间歇性的"创建 Pod 偶尔超时"。

根因

etcd 默认 election timeout 为 1000ms、heartbeat 为 100ms。跨机房链路 RTT 约 8ms,但近期运营商链路抖动,间歇性出现 1~3 秒的丢包/延迟尖峰,超过 election timeout 后 follower 判定 leader 失联,发起新一轮选举。leader 在两个机房间来回"漂移"。

排查过程

bash
# 1. 确认 leader 切换频率与时间点
etcd_server_leader_changes_seen_total   # 一天 +23

# 2. 看 raft 网络相关指标
histogram_quantile(0.99, rate(etcd_network_peer_round_trip_time_seconds_bucket[5m]))
# peer RTT P99 平时 10ms,切换发生时间点飙到 1.5s+

# 3. 在 etcd 节点上直接观测链路质量
mtr -n --report --report-cycles 100 10.0.2.13   # 对端机房 etcd peer
# 发现间歇性 2%~5% 丢包,时间点与 leader 切换完全吻合

解决方案

yaml
# /etc/rancher/rke2/config.yaml —— 放宽 etcd 心跳与选举超时,容忍网络抖动
etcd-arg:
  - "heartbeat-interval=500"        # 默认 100ms → 500ms
  - "election-timeout=5000"         # 默认 1000ms → 5000ms(保持 10 倍关系)

同时推动网络团队修复链路,并长期方案:将第三个 etcd 成员迁回同机房第三机柜,改为"同机房 3 节点 + 异地快照备份"的容灾模型。

经验教训

  • etcd 对网络质量的敏感度远高于对机器规格的敏感度。跨机房部署 etcd 前,必须用至少一周的链路质量数据(RTT P99、丢包率)论证可行性。
  • 调大 election-timeout 是"用恢复速度换稳定性":超时越久,leader 真死时恢复越慢。5s 是跨机房场景的常用平衡点。
  • leader 切换告警要设"频率阈值"(如 1 小时 >3 次),单次切换在抖动环境下属正常自愈。
  • 三节点 etcd 的容灾正确姿势:同地域三机柜 + 异地快照,而不是把 etcd 本身拉跨机房。

1.5 案例五:apiserver inflight 打满,全集群 API 超时

现象

某 5000 节点集群,某天上午 10 点(业务发布高峰),突然所有 kubectl 命令超时,controller-manager、scheduler 全部报 Timeout: request did not complete within the allotted duration,节点批量出现 NotReady 抖动(kubelet 上报心跳超时)。但 apiserver 进程没死,CPU 也只用了 60%。

根因

kube-apiserver 有并发保护机制:--max-requests-inflight(默认 400,非 list 读以外的变更类请求)和 --max-mutating-requests-inflight(默认 200)。发布高峰时数百个 CI/CD 流水线同时创建/更新工作负载,变更类 inflight 达到 200 上限后开始排队,排队请求挤占 HTTP 连接,最终连 kubelet 的心跳(Node status update,也是变更类请求)都排不上队 → 节点误报 NotReady → 触发驱逐 → 更多变更请求 → 恶性循环。

排查过程

bash
# 1. 看 inflight 水位(最直接证据)
apiserver_current_inflight_requests{request_kind="mutating"}   # 顶死 200
apiserver_current_inflight_requests{request_kind="readOnly"}   # 380/400

# 2. 看请求丢弃与延迟
rate(apiserver_dropped_requests_total[5m])                     # 持续增长
histogram_quantile(0.99, rate(apiserver_request_duration_seconds_bucket{verb="POST"}[5m]))  # >30s

# 3. 用 pprof 看 apiserver 在干什么(需要鉴权,端口 6443)
go tool pprof https://localhost:6443/debug/pprof/goroutine?debug=1
# 大量 goroutine 阻塞在 flowcontrol 的队列等待上

# 4. 确认流量来源
kubectl get --raw /metrics | grep apiserver_request_total | sort -k2 -n -r | head
# 发现 userAgent="ci-runner/v2.3" 的请求占变更类 70%

解决方案

紧急: 暂停 CI/CD 发布队列,apiserver 逐台滚动重启清空积压队列,节点心跳恢复后 NotReady 自动消除。

长期:

yaml
# /etc/rancher/rke2/config.yaml
kube-apiserver-arg:
  - "max-requests-inflight=800"           # 按控制平面规格上调
  - "max-mutating-requests-inflight=400"
  - "enable-priority-and-fairness=true"   # APF,给 kubelet 心跳等高优先级请求保留通道

同时配置 APF 的 PriorityLevelConfiguration,将 system:nodes 组的请求划入高优先级队列,保证节点心跳永不被业务流量饿死。

经验教训

  • inflight 打满的典型特征是"apiserver 活着但所有请求超时",CPU 并不高——瓶颈在排队而非算力。
  • APF(API Priority and Fairness)在大规模集群不是可选项:它保证控制面组件(kubelet、controller-manager)的请求永远优先于业务客户端。
  • CI/CD 系统必须做客户端限流(client-go 的 QPS/Burst),数百流水线同时 apply 是最常见的 inflight 杀手。
  • 节点心跳走变更类请求通道,inflight 打满会次生灾害为节点批量 NotReady,排查时要意识到这不是节点问题。

1.6 案例六:控制器 bug 制造 event 风暴,etcd 被 event 淹没

现象

某集群升级某个自研 Operator 后 6 小时,etcd DB 大小从 2GB 暴涨到 6GB,apiserver 写入延迟全面升高,kubectl describe 任何对象都返回成千上万条重复 event。

根因

新版 Operator 的 reconcile 循环存在 bug:对一个处于 Failed 状态的 CR,每轮 reconcile(间隔 1s)都调用 EventRecorder.Event() 记录同一条事件。K8s 的 event 聚合机制(EventCorrelator + spam filter)默认对同源同消息 event 做计数合并,但该 Operator 每条 event 的 message 中嵌入了时间戳,导致聚合键每次不同,无法合并,每小时产生约 3600 × N 个独立 Event 对象写入 etcd。

排查过程

bash
# 1. 确认 etcd 增长主力对象
kubectl get --raw /metrics | grep apiserver_storage_objects | grep events
# events.k8s.io/events: 1,842,331 个(正常集群 < 1 万)

# 2. 找出 event 来源
kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -100
# 全部来自 reportingController="my-operator",message 仅时间戳不同

# 3. 看 apiserver 写入热点
rate(apiserver_request_total{resource="events",verb="POST"}[5m])   # 510/s

解决方案

bash
# 1. 紧急止血:缩容肇事 Operator 到 0
kubectl scale deploy/my-operator -n platform --replicas=0

# 2. 批量清理存量 event(注意分批,避免 delete 风暴)
kubectl get events -A -o json | \
  jq -r '.items[] | select(.reportingController=="my-operator") | "\(.metadata.namespace) \(.metadata.name)"' | \
  head -10000 | xargs -n2 -P4 sh -c 'kubectl delete event -n $0 $1'

# 3. compact + defrag 回收 etcd 空间(同案例一)

长期:修复 Operator(event message 去时间戳化,让聚合生效);对 Event 对象启用 apiserver 的 --event-ttl(默认 1h 已存在,但风暴速度远超 TTL 清理速度)以及 APF 限流。

经验教训

  • Event 是 etcd 里"单价低、总量可怕"的对象,event 写入 QPS 应设告警(如 >50/s 持续 5 分钟)。
  • event 聚合依赖 message 完全一致,任何在 message 里嵌入变量(时间戳、随机 ID)的代码都会绕过聚合,Code Review 时应作为检查点。
  • 清理百万级对象要分批限速,一次 kubectl delete events --all 本身就是一次对 apiserver 的 DoS。
  • 大规模集群可考虑用第三方 event 导出(如 eventrouter → Kafka/ES)替代 etcd 留存,缩短 event-ttl 到 10 分钟。

1.7 案例七:审计日志打爆控制平面磁盘

现象

某集群三台 control-plane 的 /var/log 分区先后写满(100%),kube-apiserver 因无法写审计日志而拒绝所有请求(这是 --audit-log-mode=blocking 的默认行为),集群 API 中断 40 分钟。

根因

审计策略配置为对所有请求 RequestResponse 级别(最详细级别,记录完整请求体和响应体),且 audit-log-maxsize 配置为 1000(MB)× audit-log-maxbackup 100 份 = 理论上限 100GB,而 /var/log 分区只有 50GB。某次批量 list 大 ConfigMap 的操作(单次响应 5MB)让审计日志每小时增长 8GB,两天打满磁盘。

排查过程

bash
# 1. 磁盘水位定位
df -h /var/log
du -sh /var/log/rancher/rke2/audit* 2>/dev/null | sort -h | tail
ls -lhS /var/log/*.log | head

# 2. 确认 apiserver 报错
journalctl -u rke2-server | grep -i "audit"
# "unable to write audit log: no space left on device"

# 3. 评估审计日志增长速率
ls -lh --time-style=full-iso /var/log/rancher/rke2/audit/ | head -20

解决方案

yaml
# /etc/rancher/rke2/config.yaml
kube-apiserver-arg:
  - "audit-log-path=/data/logs/kube-apiserver-audit.log"   # 迁到大分区
  - "audit-log-maxage=7"
  - "audit-log-maxbackup=10"
  - "audit-log-maxsize=200"        # 200MB × 10 = 2GB 上限
  - "audit-log-mode=batch"         # 关键:改异步,磁盘满时不再阻塞 API
  - "audit-policy-file=/etc/rancher/rke2/audit-policy.yaml"

配套精简审计策略:对 get/list/watch 只记 Metadata 级;对 secrets 只记 Metadata(安全合规要求);仅对写操作记 Request 级,全面禁用 RequestResponse

经验教训

  • audit-log-mode 在生产必须设为 batch:blocking 模式意味着"磁盘满 = 集群死",这是用可用性换审计完整性的错误取舍。
  • 审计日志的体量由"策略 × 流量"决定,上线前必须做容量估算:单条大小 × QPS × 保留时长。
  • 审计日志应落到独立大分区/独立磁盘,并用 logrotate 或 apiserver 自带轮转双保险。
  • 磁盘水位告警必须覆盖控制平面节点的所有挂载点,本案例中监控只覆盖了 //var/lib/rancher,漏掉了 /var/log

1.8 案例八:cronjob 定时全量 list Pod,每天 9 点准时拖垮 apiserver

现象

某 4000 节点集群每天上午 9:00~9:10,apiserver 内存从 12GB 飙升到 45GB(节点物理内存 64GB),期间 API P99 延迟从 100ms 涨到 8s,10 分钟后自动恢复。故障"准时"到可以拿来对表。

根因

某数据团队部署的 cronjob 每天 9:00 运行一个报表程序,该程序使用 client-go 不带任何筛选条件地 list 全部命名空间的 Pod(集群当时约 28 万个 Pod,单次 list 响应体约 1.8GB)。三台 apiserver 各自为该请求在内存中拼装完整响应,叠加 protobuf→JSON 转换开销,内存瞬间膨胀。由于未启用 resourceVersion=0 + Pagination(limit 分页),每次全量拉取都是一次"内存炸弹"。

排查过程

bash
# 1. 内存增长时间点精确对应 9:00,查 apiserver 请求日志/审计日志
grep 'verb="list"' /data/logs/kube-apiserver-audit.log | grep '09:0' | \
  jq -r 'select(.objectRef.resource=="pods" and .objectRef.namespace==null) | .userAgent' | sort | uniq -c | sort -rn | head
# 定位到 userAgent="report-job/v0.1"

# 2. 指标佐证
rate(apiserver_request_total{verb="LIST",resource="pods"}[1m])   # 9:00 尖峰
process_resident_memory_bytes{job="apiserver"}                   # 同步尖峰

# 3. pprof 抓取内存画像确认响应拼装占大头
go tool pprof --inuse_space https://localhost:6443/debug/pprof/heap

解决方案

  1. 报表程序改造三选一:
    • 改用 Paginationlimit=500 + continue token 分页拉取;
    • 改用 resourceVersion=0 从 watch cache 读(不写 etcd,但仍有大内存开销);
    • 最优:通过 informer 本地缓存 + label selector 只取需要的字段/命名空间。
  2. apiserver 侧防护:
yaml
kube-apiserver-arg:
  - "enable-priority-and-fairness=true"
# 配合 APF 将低优先级业务 list 限流,并设置 --max-resource-list-bytes 思路:
# 通过 admission 或网关层禁止不带 limit 的全量 LIST Pod
  1. 在 CI 门禁中加入静态检查:禁止业务镜像中出现无 selector、无 limit 的全量 list 调用模式。

经验教训

  • 大规模集群中,一个不带分页的 LIST 就是一枚定时炸弹。对象数 × 单对象大小超过百 MB 时,list 请求比写请求危险得多。
  • "准时故障"优先怀疑 cronjob/定时任务,排查时先看故障时间点的周期性。
  • 对所有 controller/自研客户端建立 list 规范:必须带 label selector、必须分页、必须 resourceVersion=0(走 cache)。
  • apiserver 内存监控要结合 apiserver_request_duration_seconds{verb="LIST"} 做关联看板,一眼定位大 list 来源。

1.9 案例九:节点批量 NotReady —— CNI 升级事故

现象

某集群例行升级 Calico(v3.26 → v3.28),升级 DaemonSet 滚动更新到第 3 批(约 150 台节点)时,这批节点全部 NotReady,且 NotReady 节点上的业务 Pod 网络中断。由于默认的滚动更新没有按机柜/可用区分批,影响面集中在一个可用区,该区业务大面积超时。

根因

新版 Calico 的 CNI 配置文件 schema 变更,calico-node 启动时重写 /etc/cni/net.d/10-calico.conflist,但新配置与节点上残留的旧版 CNI 二进制(/opt/cni/bin/calico)不兼容,CNI 插件调用失败 → kubelet 的 PodSandbox 健康检查失败 → 节点上报 NetworkUnavailable=true → NotReady。

排查过程

bash
# 1. 确认 NotReady 节点的 condition
kubectl describe node <node> | grep -A5 Conditions
# NetworkUnavailable  True   ...   CalicoIsNotReady

# 2. 上节点看 CNI 实际调用错误
journalctl -u rke2-agent | grep -i cni | tail -20
# "failed to find plugin calico in path /opt/cni/bin" / "incompatible CNI config version"

# 3. 对比新旧文件
ls -la /opt/cni/bin/          # 旧版二进制时间戳
cat /etc/cni/net.d/10-calico.conflist   # 已被新版重写

# 4. 确认影响批次与升级进度的相关性
kubectl get pods -n kube-system -l k8s-app=calico-node -o wide | awk '{print $7}' | sort | uniq -c

解决方案

bash
# 1. 紧急:回滚 DaemonSet 镜像版本,并暂停滚动
kubectl rollout undo ds/calico-node -n kube-system

# 2. 对受影响节点修复 CNI 二进制(重新下发匹配版本)
#    并通过 node 重启 kubelet 触发 sandbox 重建
systemctl restart rke2-agent

# 3. 恢复后按新策略重升:maxUnavailable 改为按波次
kubectl patch ds calico-node -n kube-system --type=json -p='[
  {"op":"replace","path":"/spec/updateStrategy/rollingUpdate/maxUnavailable","value":"2%"}]'

经验教训

  • CNI 升级是节点批量故障的头号高危操作,必须先单节点灰度 → 单机柜 → 单可用区 → 全量,每波次观察至少 30 分钟。
  • DaemonSet 的 maxUnavailable 绝对不能用默认值 1(对数千节点集群太保守)也不能用 100%(等于裸奔),推荐按百分比(1%~2%)+ 人工卡点分批。
  • 升级前检查"配置文件 schema + CNI 二进制版本"的兼容矩阵,CNI 插件是文件系统级别的耦合,K8s 层面看不到。
  • 节点批量 NotReady 的第一反应应该是冻结一切自动化(cluster-autoscaler、升级流水线、自愈脚本),防止次生灾害(如自动重建节点把故障扩大化)。

1.10 案例十:节点批量 NotReady —— 证书轮换事故

现象

某运行满一年的 RKE2 集群,在某日凌晨开始出现整批 agent 节点陆续 NotReady,每小时掉 50~100 台,呈"滚雪球"态势。kubectl get nodes 看到的 NotReady 节点数量与时间呈线性关系。

根因

RKE2/K3s 体系中 kubelet 的 client 证书默认有效期 1 年,依赖 kubelet 的证书自动轮换(rotateCertificates: true)续期。该集群的 kubelet server 证书轮换被显式关闭(kubelet-arg: rotate-server-certificates=false 误配置覆盖了默认),且部分节点上 kubelet client 证书 CSR 的自动批准控制器(csr-approver)因权限问题失效。证书在满一年时陆续到期,到期节点 kubelet 无法通过 apiserver 认证,批量掉线。到期时间分散在各节点首次加入集群的时间点,所以呈线性扩散。

排查过程

bash
# 1. 在 NotReady 节点上看 kubelet 日志
journalctl -u rke2-agent | grep -iE "cert|x509|unauthorized" | tail
# "x509: certificate has expired or is not yet valid"
# "Unauthorized" (apiserver 拒绝)

# 2. 确认证书有效期
openssl x509 -in /var/lib/rancher/rke2/agent/client-kubelet.crt -noout -dates
# notAfter=2026-07-10 —— 已过期

# 3. 检查 CSR 批准是否堆积
kubectl get csr | grep Pending | head
# 大量 system:node:* 的 CSR 卡在 Pending

解决方案

bash
# 1. 手动批准堆积的 CSR(让未过期节点先完成轮换)
kubectl get csr -o name | xargs -I{} kubectl certificate approve {}

# 2. 已过期节点:证书已失效无法自动续期,需重新下发凭证
#    RKE2 中删除过期 client 证书并重启 agent 触发重新 bootstrap(需 supervisor 侧可达)
rm /var/lib/rancher/rke2/agent/client-kubelet.crt
systemctl restart rke2-agent

# 3. 修复根因配置
# /etc/rancher/rke2/config.yaml(server 与 agent 分别核对):
kubelet-arg:
  - "rotate-certificates=true"
  - "rotate-server-certificates=true"

经验教训

  • 证书有效期是集群的"定时炸弹日历":所有证书(etcd peer/server/client、apiserver、kubelet、front-proxy)的到期时间必须纳入监控,kubelet_certificate_manager_client_ttl 和文件级 openssl x509 -dates 巡检缺一不可。
  • 集群"满周岁""满两年"是证书事故高发时间点,周年庆前先查证书。
  • 任何 *-arg 覆盖默认值的操作都要评审:本案例根源就是一条"看似无害"的 rotate-server-certificates=false
  • 证书轮换的完整链路(kubelet → CSR → 自动批准 → 新证书落盘)每环节都要监控,CSR Pending 堆积是最佳早期信号。

1.11 案例十一:节点资源耗尽三连 —— conntrack 打满、PID/FD 耗尽与内核 netfilter bug

现象 A(conntrack)

某高 QPS 微服务集群节点,业务间歇性出现随机 TCP 超时(无规律、跨 Pod、跨节点),ping 正常、带宽充足。同一批节点后续又陆续出现"无法创建新进程"(现象 B)与"文件句柄耗尽"(现象 C)。

根因 A:conntrack 表打满

节点默认 net.netfilter.nf_conntrack_max = 262144,高 QPS 短连接服务(每秒 2 万新建连接)叠加 DNS(UDP 双 entry)很快耗尽 conntrack 表,新连接被静默丢弃,表现为随机丢包。

排查过程 A

bash
# 1. 看内核日志有无 conntrack 表满
dmesg -T | grep conntrack
# "nf_conntrack: table full, dropping packet"

# 2. 看当前水位
cat /proc/sys/net/netfilter/nf_conntrack_count   # 262144(顶满)
cat /proc/sys/net/netfilter/nf_conntrack_max

# 3. Prometheus 指标
node_nf_conntrack_entries / node_nf_conntrack_entries_limit   # = 100%

解决方案 A

bash
# /etc/sysctl.d/90-rke2-large-scale.conf
net.netfilter.nf_conntrack_max = 4194304
net.netfilter.nf_conntrack_tcp_timeout_established = 600
net.netfilter.nf_conntrack_udp_timeout = 60
net.netfilter.nf_conntrack_udp_timeout_stream = 180
sysctl --system

注意:conntrack 表每条约 300+ 字节内核内存,4M 条约占 1.2GB+,调大时同步评估内存。

根因 B/C:PID 与 FD 耗尽

同批节点上某 Java 应用线程泄漏(每请求新建线程池不关闭),线程数触达 kernel.pid_max = 4194304 上限前,先触达了 systemd 对 kubelet 的 TasksMax 与应用的 fd 上限 nofile=65535,表现为 fork 失败与 "too many open files"。

排查过程 B/C

bash
# PID 水位
cat /proc/sys/kernel/pid_max
ps -eLf | wc -l                      # 当前线程总数
systemd-cgtop | grep kubepods        # 定位异常 cgroup

# FD 水位(按进程排名)
for p in /proc/[0-9]*; do echo "$(ls $p/fd 2>/dev/null | wc -l) $p"; done | sort -rn | head
cat /proc/sys/fs/file-nr             # 已分配 / 0 / 上限

# 指标
node_filefd_allocated / node_filefd_maximum
process_max_fds / process_open_fds   # 按进程

解决方案 B/C

bash
# sysctl
kernel.pid_max = 4194304
fs.file-max = 20971520
fs.nr_open = 20971520

# /etc/systemd/system/rke2-agent.service.d/override.conf
[Service]
LimitNOFILE=1048576
TasksMax=infinity

# containerd 侧 /var/lib/rancher/rke2/agent/etc/containerd/config.toml 模板中
# 同步调高,业务侧修复线程泄漏

顺带案例:内核 netfilter bug

另一次类似"随机丢包"最终定位为内核版本 bug(某 5.14.x 内核在 ipvs + conntrack 高并发下出现 race,连接跟踪条目异常提前老化)。通过 perf + 内核 dropwatch 定位后,升级内核到修复版本解决。

bash
# 定位工具链
dropwatch -l kas          # 观察内核丢包点
perf record -a -g sleep 30 && perf report
uname -r                  # 与内核 changelog 比对确认 bug 修复版本

经验教训

  • "随机丢包"排查三板斧:conntrack 水位 → 内核 dmesg/dropwatch → 内核版本 bug 检索。
  • conntrack、pid_max、file-max 是节点侧三大"隐形天花板",全部要在节点基线中显式配置并纳入监控,默认值是为通用服务器设计的,不是为 K8s 高密度节点设计的。
  • 内核版本要纳入集群基线管理:全集群统一内核版本,升级内核视同升级 K8s 一样走灰度流程。
  • 节点资源耗尽类故障的共性:应用层的 bug 以内核参数的形式爆发,治标(调参)和治本(修应用)缺一不可。

1.12 案例十二:CoreDNS 被打爆 —— 某应用 DNS 风暴与未启用 NodeLocal 的双重暴击

现象

某集群上线一个新微服务后,全集群 DNS 解析延迟 P99 从 2ms 涨到 300ms,大量应用报 lookup xxx on 10.13.0.10:53: read udp: i/o timeout,CoreDNS Pod CPU 全部打满(8C 100%),HPA 扩容到 20 副本仍无法消化。

根因

双重问题叠加:

  1. DNS 风暴源头:新服务的 HTTP 客户端未启用连接复用,每个请求新建连接,每次都触发 DNS 解析;且代码中对同一个域名的解析失败无退避重试(每秒重试上千次)。单 Pod 产生 4000 QPS DNS 查询。
  2. 放大器效应:应用 Pod 的 resolv.conf 默认 ndots:5,查询 api.example.com(3 个点 < 5)会先依次拼接 4 个 search 域后缀逐个查询,1 次业务查询放大为 5 次 DNS 查询,实际打到 CoreDNS 是 20000 QPS。
  3. 无 NodeLocal DNSCache:所有 DNS 走 ClusterIP → conntrack 做 DNAT,UDP 高并发下 conntrack 出现竞争与丢包(Linux 内核 conntrack 对 UDP 的 race 是著名问题),进一步推高超时与重试。

排查过程

bash
# 1. CoreDNS 查询量与热点域名
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=1000 | \
  awk '{print $6}' | sort | uniq -c | sort -rn | head
# api.example.com. 占 90%

# 2. 定位来源 Pod(CoreDNS 日志含客户端 IP)
kubectl logs -n kube-system -l k8s-app=kube-dns | grep "api.example.com" | \
  awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn | head

# 3. 指标
coredns_dns_requests_total                      # QPS 曲线
coredns_dns_request_duration_seconds_bucket     # 延迟
node_nf_conntrack_entries                       # UDP 条目激增

# 4. 检查是否部署 NodeLocal DNSCache
kubectl get ds -n kube-system node-local-dns    # 不存在

解决方案

紧急:

bash
# 1. 修复应用 resolv.conf:注入 ndots 配置,消除 search 域放大
# Pod spec:
#   dnsConfig:
#     options:
#       - name: ndots
#         value: "2"
# 或将业务域名写成 FQDN(结尾加点):api.example.com.

# 2. 临时给肇事 Deployment 限副本,止血

长期:部署 NodeLocal DNSCache(大规模集群标配)

bash
# 部署 node-local-dns DaemonSet(每节点一个本地缓存,监听 169.254.20.10)
# kubelet 配置 node-local-dns 为首选 nameserver
# /etc/rancher/rke2/config.yaml
kubelet-arg:
  - "cluster-dns=169.254.20.10"

NodeLocal 的价值:本地命中不走 conntrack(消除 UDP race 丢包);TCP 长连接上游(消除 UDP 不可靠);节点级缓存削掉 70%+ 重复查询。

经验教训

  • DNS 是集群中最容易被低估的"中枢服务":CoreDNS 挂 = 全集群业务挂,其容量规划要按峰值 QPS × 3 冗余。
  • ndots:5 是 K8s DNS 的第一性能杀手,任何外部域名解析都被放大 5 倍,务必通过 dnsConfig 收敛。
  • 数千节点集群必须上 NodeLocal DNSCache,它同时解决容量、延迟、conntrack race 三个问题(参考本仓库 metaLB/coredns-nodelocaldncache-from-beginner-to-master.md)。
  • DNS 告警要覆盖三层:CoreDNS QPS/延迟/错误率、NodeLocal 本地命中率、节点 conntrack 水位。
  • 应用侧 DNS 治理:客户端连接复用、解析失败退避重试、NDots 配置,应纳入上线检查单。

1.13 案例十三:NetworkPolicy 误配置导致全网隔离 + Service iptables 规则更新延迟

现象 A(NetworkPolicy 事故)

某平台团队为落实"默认 deny"安全基线,向集群下发一条"兜底" NetworkPolicy 后 30 秒,全集群跨命名空间流量全部中断:业务 500 错误率飙升到 80%,连 CoreDNS 的解析都失败(policy 未放行 kube-system 的 UDP 53 出口)。

根因 A

运维人员将本该下发到单个业务命名空间的 default-deny 策略,通过 CI 流水线的变量错误渲染到了所有命名空间for ns in $(kubectl get ns ...) 的脚本 bug)。NetworkPolicy 语义是"一旦被任何 policy 选中即进入白名单模式",default-deny 选中所有 Pod 后,未被显式放行的流量全部丢弃。

排查过程 A

bash
# 1. 确认 NetworkPolicy 数量异常
kubectl get networkpolicy -A | wc -l     # 从 47 暴涨到 400+(每个 ns 一条)

# 2. 查看策略内容
kubectl get networkpolicy -A -o yaml | grep -B2 "podSelector: {}"

# 3. CNI 侧确认 policy 生效
# calico: calicoctl get networkpolicy -o wide
# 结合业务超时时间点与 policy 创建时间对齐

解决方案 A

bash
# 紧急:批量删除误下发策略(保留原有的 47 条)
kubectl get networkpolicy -A -o json | \
  jq -r '.items[] | select(.metadata.labels["app.kubernetes.io/managed-by"]=="policy-ci") |
         "\(.metadata.namespace) \(.metadata.name)"' | \
  xargs -n2 sh -c 'kubectl delete networkpolicy -n $0 $1'

流量在 CNI 同步删除规则后 1 分钟内恢复。长期措施:policy 下发流水线加入"爆炸半径检查"(单次变更影响 ns 数 > 5 需人工审批);default-deny 必须配套"基础放行模板"(DNS、监控、apiserver)。

现象 B(Service iptables 规则更新延迟)

同一集群(4000 节点、3 万 Service、12 万 Endpoints)还长期存在一个慢性问题:新创建的 Service/Endpoints 在部分节点上最长 90 秒才能通,业务滚动更新时流量打到尚未更新规则的节点上产生 1%~2% 错误。

根因 B

kube-proxy 使用 iptables 模式(该集群历史原因未切 ipvs),每次 Service/Endpoints 变更,kube-proxy 用 iptables-restore 全量重写规则。3 万 Service 对应约 30 万条 iptables 规则,单次全量同步耗时 40~80 秒,期间 iptables-restore 持有 xtables 锁,新规则生效存在分钟级延迟窗口。规则越多 → 同步越慢 → 变更期间不一致窗口越大,恶性循环。

排查过程 B

bash
# 1. 看 kube-proxy 同步延迟指标
kubeproxy_sync_proxy_rules_duration_seconds_bucket      # P99 75s(正常 < 1s)
kubeproxy_sync_proxy_rules_last_timestamp_seconds       # 距上次同步时间

# 2. 数规则规模
iptables -t nat -L KUBE-SERVICES -n | wc -l
iptables-save | wc -l                                   # 312,000 条

# 3. 抓一次同步过程
kubectl logs -n kube-system kube-proxy-xxx | grep -i "syncProxyRules took"

解决方案 B

  1. 切换 kube-proxy 到 ipvs 模式(参考本仓库 metaLB/rke2-ipvs-iptables-principle.md):
yaml
# /etc/rancher/rke2/config.yaml
kube-proxy-arg:
  - "proxy-mode=ipvs"
  - "ipvs-scheduler=rr"

切换后规则同步从"全量重写 O(n)"变为"增量更新 O(Δ)",同步延迟降到亚秒级。

  1. 辅助措施:开启 iptables 模式的 minSyncInterval 限速不合理时只能缓解,根治靠 ipvs 或 nftables 模式。

经验教训

  • NetworkPolicy 是"声明即生效"的高危资源,任何批量下发 policy 的流水线都要带爆炸半径检查与金丝雀命名空间
  • default-deny 落地三件套:先监控模式(CNI 支持时)、配套基础放行清单(DNS/监控/控制面)、单 ns 灰度。
  • Service/Endpoints 规模超过 5000 时,iptables 模式的 kube-proxy 就是定时炸弹,大规模集群必须用 ipvs 或 nftables 模式
  • kubeproxy_sync_proxy_rules_duration_seconds 是必配告警:P99 > 5s 即应介入,不要等业务报错。

1.14 案例十四:数千节点同时拉镜像打爆 registry(thundering herd)+ 镜像 GC 与业务冲突

现象 A(镜像拉取惊群)

某 4000 节点集群进行一次核心业务的全量大版本发布(镜像 2.1GB,Deployment 滚动更新),发布开始后 3 分钟,私有 Harbor registry 的带宽打满 10Gbps、连接数超过 5 万,registry 开始返回 503/超时。镜像拉取失败导致大量 Pod 卡在 ImagePullBackOff,发布卡住,且由于重试风暴,registry 陷入"越忙越慢、越慢越重试"的死亡螺旋。

根因 A

  1. 该 Deployment 有 8000 副本,滚动更新 maxSurge 配置过大,数千 Pod 几乎同时在数千节点上创建;
  2. 新镜像从未预热,所有节点本地均无缓存,全部回源 registry;
  3. containerd 的镜像拉取重试机制在 registry 超时时指数退避过短,失败重试进一步放大请求量;
  4. registry 前端无分级缓存/无 P2P 分发,单点扛全集群回源。

排查过程 A

bash
# 1. 确认拉取失败规模
kubectl get pods -A --field-selector=status.phase=Pending | grep ImagePullBackOff | wc -l

# 2. registry 侧指标
harbor_core_http_request_total / 网卡带宽 / 连接数
ss -s                                       # 连接数 5w+

# 3. 节点侧确认回源行为
crictl images | grep myapp                  # 各节点均无缓存
journalctl -u rke2-agent | grep -i "pull" | tail

# 4. 看节点本地并发拉取限制(默认很低但乘以节点数依然恐怖)
grep max-concurrent-downloads /var/lib/rancher/rke2/agent/etc/containerd/config.toml

解决方案 A

紧急: 暂停发布(kubectl rollout pause),registry 临时扩容 + 前置 CDN 缓存,分批放行拉取。

长期(多管齐下):

  1. 镜像预热:发布流水线前置一步"预热 job",把新镜像提前 24 小时通过 DaemonSet 拉取到全部节点(imagePullPolicy: IfNotPresent + sleep 容器);
  2. P2P 分发:部署 Dragonfly/Kraken,节点间 P2P 分发镜像层,registry 只承担种子回源;
  3. 发布节奏控制:大副本量 Deployment 用 maxSurge: 5% + 分批发布,杜绝全量同时拉取;
  4. registry 分级:核心集群按可用区部署 registry 代理缓存(registries.yaml 配置 mirror,参考 rke2/registries.yaml)。

现象 B(镜像 GC 与业务冲突)

另一次事故中,某批 GPU 节点业务周期性失败,报错 failed to create containerd container: image not found,但 Pod spec 里的镜像明明存在。

根因 B

kubelet 镜像 GC(image-gc-high-threshold=85)在磁盘紧张时回收"未被使用"的镜像。业务方一个批处理框架采用"预拉镜像、数小时后创建容器"的模式,预拉完成到容器创建之间,镜像处于"已存在但无容器引用"状态,恰逢磁盘水位超过 85%,GC 把刚预热好的镜像删了,创建容器时镜像已不在本地且 registry 侧该 tag 已被覆盖更新(latest 滥用),拉到的镜像 digest 与预期不符导致失败。

排查过程 B

bash
# 1. kubelet 日志确认 GC 行为
journalctl -u rke2-agent | grep -i "image garbage collection"
# "image garbage collection: removing image sha256:xxx to free bytes"

# 2. 对比镜像拉取/删除/容器创建三个时间点,还原冲突时序
crictl images --digests | grep myapp

解决方案 B

yaml
kubelet-arg:
  - "image-gc-high-threshold=90"
  - "image-gc-low-threshold=80"
# 根因修复:业务镜像引用全部改为 digest(@sha256:...)而非 tag;
# 预拉与创建之间的窗口期通过 pinned image(containerd pin)或缩短窗口规避

经验教训

  • 镜像分发的容量规划 = 镜像大小 × 同时拉取节点数 ÷ 可接受拉取时长,4000 节点 × 2GB 的级别,没有 P2P 和预热就是裸奔。
  • latest tag 在大规模集群是万恶之源:不可追溯、不可缓存校验、与 GC 冲突,必须全面 digest 化。
  • kubelet 镜像 GC 的阈值要与节点系统盘/数据盘大小联动规划,GPU/AI 节点(镜像动辄 10GB+)应单独配置更大磁盘或更高阈值。
  • 发布系统要把"镜像已预热到目标节点"作为发布前置门禁,而不是发布后才发现。

1.15 案例十五:批量升级 storm 与升级后 kubelet 不兼容

现象 A(批量升级 storm)

某 2500 节点集群做 RKE2 版本升级(v1.28 → v1.30,跨两个 minor),使用 system-upgrade-controller 下发升级 Plan,concurrency: 50。升级开始后:50 台节点同时 drain → 数百 Pod 同时驱逐重建 → 调度器与 apiserver 压力激增 → 部分有状态服务(未配 PDB)副本数跌破 quorum → 业务告警爆发 → 运维恐慌中手工干预,进一步加剧混乱。整个升级窗口 6 小时的计划实际花了 26 小时才收尾。

根因 A

  1. concurrency: 50 意味着任意时刻 2% 的节点在 drain,但驱逐产生的 Pod 重建压力是全局的:50 节点 × 平均每节点 40 Pod = 2000 Pod 同时调度,超出调度器吞吐;
  2. 多个核心工作负载未配 PodDisruptionBudget,drain 直接打穿最小可用副本;
  3. 升级波次之间无"健康检查门禁",前一波未完全恢复就进入下一波;
  4. 控制平面与 agent 混在同一个 Plan 里同时推进。

排查过程 A

bash
# 1. 看升级进度与驱逐压力
kubectl get plans -n system-upgrade -o wide
kubectl get nodes -l plan.upgrade.cattle.io/... 
rate(scheduler_pod_scheduling_attempts_total[5m])          # 飙升 10 倍
rate(apiserver_request_total{verb="POST",resource="pods",subresource="eviction"}[5m])

# 2. 找出被 drain 打穿的服务
kubectl get pdb -A | awk '$4<$5'        # ALLOWED DISRUPTIONS < 期望
kubectl get evictions -A 2>/dev/null | head

解决方案 A

yaml
# 修正后的 Plan:控制平面先行、严格串行波次、小并发
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
  name: rke2-agent-upgrade
  namespace: system-upgrade
spec:
  concurrency: 10              # 2500 节点集群先 10,观察后逐步放大
  nodeSelector:
    matchExpressions:
      - {key: node-role.kubernetes.io/control-plane, operator: DoesNotExist}
  prepare:
    image: rancher/kubectl:v1.30.x
    args: ["cordons-before", "health-check"]   # 波次前置健康检查(通过 initJob 实现)
  upgrade:
    image: rancher/system-upgrade-controller:latest

配套:升级前巡检 PDB 覆盖率(核心服务 100% 覆盖);波次间强制观察期 15 分钟 + 自动化健康门禁(节点 Ready 率、API P99、业务错误率三项全绿才放行下一波)。

现象 B(升级后 kubelet 不兼容)

同一轮升级中,第一批 agent 升级到 v1.30 后,部分节点上旧版 device-plugin(某厂商 GPU 插件,两年未更新)调用已被移除的 kubelet PodResources API 旧版本,插件 panic 循环重启 → 节点 GPU 资源从 allocatable 消失 → 排队中的 GPU Pod 全部 Pending,AI 训练业务中断 3 小时。

根因 B

K8s 的版本偏差政策(kubelet 与 apiserver 允许 ±1~2 minor)只覆盖核心组件,不覆盖生态组件(device-plugin、CNI、CSI、监控 agent)。升级前只验证了 RKE2 自身,未验证节点上运行的全部 DaemonSet/插件对新 kubelet 的兼容性。

排查过程 B

bash
journalctl -u rke2-agent | grep -i podresources
kubectl logs -n gpu-system device-plugin-xxx --previous | tail -20
# "unknown service v1alpha1.PodResourcesLister" / "method not implemented"

kubectl describe node gpu-node-01 | grep -A10 Allocatable
# nvidia.com/gpu: 0  ← 资源消失

解决方案 B

回滚该批节点 kubelet 版本止血;升级 device-plugin 到支持新版 API 的版本后重升。长期:建立"升级兼容性矩阵",把集群中全部 DaemonSet/插件列为升级检查的强制项。

经验教训

  • 大规模升级的正确姿势:控制平面与节点分开、小并发起步、波次间健康门禁、随时可回滚。concurrency 的决策依据是"驱逐重建压力 vs 调度器吞吐",不是节点总数百分比。
  • 升级前巡检三件套:PDB 覆盖率、生态组件兼容矩阵、快照备份有效性(恢复演练过的快照才算数)。
  • 版本偏差政策只保核心组件;device-plugin/CNI/CSI 等生态组件的兼容性是升级事故的重灾区。
  • 升级窗口内冻结一切其他变更(发布、弹性伸缩、配置变更),多变更叠加是故障定位的噩梦。
  • 第一批永远是"金丝雀节点"(5~10 台承载真实业务但非核心),观察 24 小时再推进。

第 2 章 大规模最佳实践总纲:五个维度各十条军规

以下 50 条军规是全书前五部分与本部分案例库的浓缩,每一条背后都有至少一个真实事故。建议打印贴在运维工位上。

2.1 架构维度(10 条)

#军规反面案例
A13 节点 etcd 跨机柜/跨 PDU 部署,严禁同机柜同电源案例三
A2etcd 不跨机房拉长部署,容灾靠异地快照备份案例四
A3集群规模超 2000 节点前完成 kube-proxy ipvs/nftables 切换案例十三
A4控制平面节点独占,taints 隔离一切业务负载第二部分
A5集群 CIDR 规划预留 3 倍余量,Pod/Service 网段永不复用第一部分
A6单集群不超 5000 节点/15 万 Pod,超限拆多集群第一部分
A7关键基础设施(registry、DNS、镜像仓库)与业务集群解耦独立部署案例十四
A8所有核心工作负载必须配 PDB,覆盖率纳入巡检案例十五
A9多可用区业务按区做拓扑分布约束(topologySpreadConstraints)案例九
A10建立"爆炸半径"意识:任何全局性配置(policy/准入/webhook)默认假设它会误伤全集群案例十三

2.2 部署维度(10 条)

#军规说明
D1节点基线镜像化/自动化,新节点 30 分钟内全自动就绪Ansible/云镜像
D2内核参数、conntrack、pid_max、file-max 全部纳入基线案例十一
D3全集群统一内核版本,内核升级走灰度案例十一
D4镜像引用全面 digest 化,禁用 latest案例十四
D5大镜像发布前强制预热 + P2P 分发案例十四
D6升级并发从小开始(10 台),波次间健康门禁案例十五
D7升级前执行 PDB 覆盖率与生态组件兼容性检查案例十五
D8变更窗口内冻结一切无关变更案例十五
D9CNI/CSI/ingress 等基础设施升级按 单节点→机柜→可用区→全量 四波灰度案例九
D10所有自动化下发(policy/配置)带爆炸半径检查与审批卡点案例十三

2.3 运维维度(10 条)

#军规说明
O1etcd 快照定时 + 异地备份 + 每季度恢复演练案例三
O2每月低峰期 etcd 逐台 defrag,碎片率纳入巡检案例二
O3证书有效期全量登记,到期前 30 天告警案例十
O4节点故障先冻结自动化(autoscaler/自愈/升级)再处置案例九
O5批量清理对象(event/pod)分批限速,禁止一把梭 delete案例六
O6控制平面磁盘全挂载点监控,审计日志独立分区案例七
O7变更必有回滚方案,回滚方案必须演练过案例十五
O8建立故障复盘文化:无指责、重流程、产出可执行的改进项本部分全部
O9运维操作命令进 runbook,禁止"靠老师傅记忆"案例三
O10值班 on-call 双备份,重大故障 15 分钟内升级SRE 通识

2.4 观测维度(10 条)

#军规说明
M1SLI/SLO 先行:先定义"健康长什么样",再谈告警案例二
M2etcd 必监控:DB SIZE 与 IN USE 双指标、backend commit P99、leader 切换频率案例一/二/四
M3apiserver 必监控:inflight 水位、LIST 延迟、pprof 常态化可用案例五/八
M4告警按"频率+趋势"设计,而非仅绝对阈值案例二/四
M5周期性故障优先怀疑 cronjob/定时任务案例八
M6DNS 三层监控:CoreDNS、NodeLocal、节点 conntrack案例十二
M7kube-proxy 同步延迟 P99 > 5s 即告警案例十三
M8event 写入 QPS > 50/s 告警案例六
M9节点侧三水位:conntrack / PID / FD,全部可视化案例十一
M10监控本身要高可用:监控挂了等于瞎了,降级期间禁止变更第四部分

2.5 优化维度(10 条)

#军规说明
P1APF(Priority and Fairness)大规模集群必开,节点心跳最高优先级案例五
P2客户端 list 规范:selector + 分页 + resourceVersion=0案例八
P3审计策略分级:读操作只记 Metadata,禁全量 RequestResponse案例七
P4audit-log-mode=batch,可用性优先于审计实时性案例七
P5ndots 收敛(默认 5 → 2)+ FQDN 加点案例十二
P6数千节点必上 NodeLocal DNSCache案例十二
P7etcd quota 调大(8GB)+ 水位 80% 提前告警案例一
P8election-timeout 按网络质量调整,跨机房 5s案例四
P9Operator/控制器上线前做写放大评估(status 更新频率)案例一/六
P10event 聚合键设计:message 不嵌入时间戳/随机值案例六

第 3 章 生产就绪检查清单(上线前 50 条)

使用方法:集群上线 / 重大版本发布前,逐项打钩并签字。任何一项为 ✗ 均不应上线。

3.1 容量(8 条)

#检查项通过标准
C1节点规模与控制平面规格匹配符合容量估算公式卡(见附录)
C2etcd DB 配额与当前水位quota ≥ 8GB,当前水位 < 50%
C3Pod/Service CIDR 余量可用 IP ≥ 规划规模的 3 倍
C4apiserver inflight 参数按规模上调并通过压测验证
C5registry 带宽与 P2P 分发支撑"镜像×节点数÷发布时长"峰值
C6CoreDNS 副本数与容量峰值 QPS × 3 冗余 + NodeLocal 就绪
C7调度器吞吐压测满足最大批量重建场景(升级 storm)
C8存储/CSI 容量与 IOPS 规划峰值 IO 余量 ≥ 50%

3.2 高可用(8 条)

#检查项通过标准
H1etcd 三成员机柜/电源反亲和任意单点故障不丢 quorum
H2control-plane 负载均衡健康检查VIP/LB 摘除故障 apiserver < 10s
H3核心组件多副本反亲和部署scheduler/controller-manager/coredns
H4PDB 覆盖率核心服务 100%,全业务 ≥ 90%
H5节点批量故障演练模拟 5% 节点失联,业务 SLO 不破
H6控制平面全灭恢复演练近一季度内演练过,runbook 验证有效
H7kube-proxy 模式ipvs/nftables(Service > 5000 强制)
H8拓扑分布约束跨可用区业务全部配置

3.3 安全(9 条)

#检查项通过标准
S1RBAC 最小权限审计无 cluster-admin 滥用,SA 按职责划分
S2NetworkPolicy 基线default-deny + 基础放行模板 + 下发审批流
S3审计策略分级 + batch 模式 + 独立分区磁盘满不阻塞 API
S4证书台账与到期告警全部证书登记,30 天预警
S5镜像安全扫描门禁高危漏洞镜像禁止入库/上线
S6Pod 安全标准(PSS)baseline 集群级 enforce,restricted 业务灰度
S7API 准入控制链评审webhook 超时与 failurePolicy 逐条评审
S8etcd 加密与静态加密(secrets encryption)已启用并演练密钥轮换
S9节点基线安全加固ssh 限制、防火墙端口白名单(见附录端口表)

3.4 监控(9 条)

#检查项通过标准
N1SLI/SLO 定义并接入看板核心链路(创建 Pod、DNS、API)全覆盖
N2etcd 黄金指标告警DB 水位 80%、commit P99、leader 切换频率
N3apiserver 黄金指标告警inflight 80%、LIST P99、5xx 率
N4节点三水位告警conntrack/PID/FD ≥ 80%
N5DNS 三层告警CoreDNS + NodeLocal + conntrack
N6event/审计写入速率告警event > 50/s 持续 5min
N7kube-proxy 同步延迟告警P99 > 5s
N8证书到期告警全部证书 < 30 天触发
N9监控系统自身高可用与降级预案监控故障时冻结变更流程明确

3.5 备份(8 条)

#检查项通过标准
B1etcd 定时快照cron 已配置(如 0 */6 * * *),retention ≥ 5
B2快照异地备份跨机房/对象存储,加密传输
B3快照恢复演练近一季度成功恢复过
B4quorum 丢失 runbookforce-new-cluster 流程文档化并演练
B5关键声明式资产 Git 化全部 workload/config 可从 Git 重建
B6镜像仓库备份registry 存储可恢复
B7PV 数据备份策略有状态业务全部覆盖(Velero/存储快照)
B8RTO/RPO 指标定义并验证与业务方书面确认

3.6 应急(8 条)

#检查项通过标准
E1on-call 排班与升级路径双人备份,15 分钟升级机制
E2故障分级标准(P0~P3)书面定义,全员知晓
E3核心故障 runbook本部分 15 案例对应的处置手册就绪
E4一键冻结自动化能力autoscaler/升级/自愈可全局一键暂停
E5应急演练频率每季度至少一次红蓝对抗/故障注入
E6备件与弹性资源池5% 备用节点容量常驻
E7沟通模板与对外通报流程业务方/管理层通报模板就绪
E8复盘机制无指责复盘 + 改进项跟踪闭环

第 4 章 常见面试题(大规模方向)

以下 10 题是大规模 K8s/RKE2 岗位面试高频题,附答案要点。回答时建议结合本部分案例,用"我遇到过……"的方式讲,远胜于背概念。

Q1:集群规模从 500 节点扩到 5000 节点,控制平面需要做什么?

答案要点:① etcd:quota 调大(≥8GB)、独立高性能 SSD、跨机柜反亲和、定期 defrag;② apiserver:多副本 + LB、inflight 上调、开启 APF、event-ttl 与审计策略收敛;③ scheduler:开 percentageOfNodesToScore 降低打分开销;④ kube-proxy 切 ipvs/nftables;⑤ CoreDNS 扩容 + NodeLocal DNSCache;⑥ 监控指标基数治理(drop 高基数 label);⑦ 全部客户端 list 规范化(分页+selector)。核心思想:默认参数是为中小集群设计的,规模增长后每一层都有隐形天花板,要逐层评估

Q2:etcd 打满 8GB 只读了怎么办?

答案要点:立即临时调大 quota-backend-bytes 恢复写入(逐台滚动重启)→ 找出写入源头(apiserver_storage_objects + 审计日志)→ 止血肇事方 → compact 历史 revision → 逐台 defrag 回收物理空间 → 恢复配额。预防:DB SIZE 与 IN USE 双指标 80% 水位告警、Operator 写放大评估、定期 defrag。(案例一完整流程)

Q3:节点批量 NotReady,你的排查顺序是什么?

答案要点:先冻结自动化(autoscaler/升级/自愈)防次生灾害 → 看 NotReady 节点 condition 分布(NetworkUnavailable?KubeletNotReady?)→ 按比例与时间分布判断是"点"还是"面":全网格状发散多为控制面/CNI/证书问题,单机柜集中多为基础设施问题 → 上节点看 kubelet 日志(证书过期?CNI 调用失败?conntrack/PID 耗尽?)→ 对照近期变更记录(90% 的批量故障与变更相关)。(案例九/十/十一)

Q4:如何保证升级数千节点不出事?

答案要点:升级前:PDB 覆盖率巡检、生态组件兼容矩阵、快照备份验证、金丝雀节点;升级中:控制平面与节点分离、小并发(10 台起)、波次间健康门禁(节点 Ready 率/API P99/业务错误率)、冻结其他变更;升级后:观察期 + 回滚预案。关键认知:concurrency 由调度器吞吐决定,不是拍脑袋的百分比。(案例十五)

Q5:DNS 解析偶发超时,可能的原因有哪些?

答案要点:① ndots:5 导致的 search 域放大(5 倍查询量);② 无 NodeLocal 时 UDP 经 conntrack DNAT 的 race 丢包;③ CoreDNS 容量不足/被打爆(DNS 风暴);④ 上游 DNS(forward)故障;⑤ 节点 conntrack 表满。排查:CoreDNS QPS/延迟/错误率 → 热点域名与来源 Pod → 节点 conntrack 水位 → 是否部署 NodeLocal。根治:ndots 收敛 + NodeLocal + 客户端连接复用。(案例十二)

Q6:什么是镜像拉取惊群(thundering herd),如何解?

答案要点:数千节点同时拉取同一新镜像打爆 registry。解法四层:发布前镜像预热(DaemonSet 预拉);P2P 分发(Dragonfly/Kraken)让 registry 只做种子回源;发布节奏控制(maxSurge 百分比 + 分批);registry 分级缓存(按可用区代理)。容量公式:镜像大小 × 并发节点数 ÷ 目标拉取时长 = 所需带宽。(案例十四)

Q7:APF(API Priority and Fairness)是什么,为什么大规模集群必须开?

答案要点:APF 把 API 请求按 PriorityLevel 分队列、按 FlowSchema 分类限流,保证高优先级请求(kubelet 心跳、控制面组件)不被业务流量饿死。不开 APF 时,inflight 打满会让节点心跳排队 → 节点批量误报 NotReady → 驱逐风暴。开启后要配置 system:nodes 高优先级队列,并对 CI/CD 等高频客户端单独限流。(案例五)

Q8:etcd 跨机房部署三节点(2+1)为什么是错误的?

答案要点:① etcd 对网络 RTT 和丢包极度敏感,跨机房抖动导致 leader 频繁切换,每次切换伴随 1~2s 写入不可用;② 2+1 布局下,"2"所在机房整体故障 = 丢 quorum,而"1"所在机房故障也会让多数派受压;③ 正确容灾模型:同地域三机柜反亲和部署 etcd + 快照异地备份,机房间容灾靠多集群联邦/应用层双活,而不是拉长 etcd。若必须跨机房,election-timeout 调到 5s 并用链路质量数据论证。(案例三/四)

Q9:一个不带分页的全量 LIST Pod 为什么能拖垮 apiserver?

答案要点:LIST 全量 Pod 时 apiserver 要在内存中拼装完整响应(28 万 Pod ≈ 1.8GB),还要做 protobuf→JSON 转换与权限过滤,单请求内存开销巨大;并发几个这样的请求就能打爆内存、触发 GC 风暴、拖慢所有其他请求。防御:客户端规范(selector + limit 分页 + resourceVersion=0 走 watch cache);服务端 APF 限流 + 大 list 来源监控告警 + CI 静态检查。(案例八)

Q10:如何设计大规模集群的告警体系,避免告警风暴又不错过真故障?

答案要点:① SLI/SLO 先行:基于错误预算告警(burn rate),而非堆阈值;② 分层:症状告警(用户视角:API P99、Pod 创建延迟)Pager,原因告警(组件指标)进看板;③ 趋势+频率告警补充绝对阈值(etcd 碎片、leader 切换频率);④ 聚合抑制:节点批量故障按机柜/可用区聚合为一条;⑤ 每条告警必须绑定 runbook,无 runbook 不上线;⑥ 定期告警审计:删除无人响应的告警,演练验证关键告警链路。核心:告警的价值 = 有人响应 × 有手册可循,数量不是目标。(案例二、第四部分)


全书总结

至此,《RKE2 数千节点:方案、配置、运维、观测及优化》六个部分全部完成:

部分主题一句话回顾
第一部分架构设计与容量规划规模决定架构,先算清楚再动手
第二部分配置与自动化部署一切基线化、自动化、可重建
第三部分运维体系变更管理是大规模运维的生命线
第四部分可观测性体系SLI/SLO 先行,看见才能管好
第五部分性能优化调优默认值是中小集群的,逐层找天花板
第六部分故障案例与最佳实践故障是最好的老师,复盘是最好的教材

最后一句话:大规模集群运维的本质,不是"不出故障",而是"控制爆炸半径、缩短恢复时间、让同样的故障不发生第二次"。愿本书的每一个案例,都帮你少熬一个通宵。