主题
第六部分 大规模故障案例与最佳实践
前五部分解决了"怎么建、怎么配、怎么管、怎么看、怎么调"的问题,本部分解决最后一个问题:出事的时候怎么办,以及怎么让事情不出。
当集群规模从几十节点增长到数千节点,故障的形态会发生质变:小问题会被放大(一个控制器的 bug 可以制造 event 风暴)、小概率事件会变成必然事件(数千块磁盘每天总有几块出问题)、人为操作会成为最大风险源(一次批量升级可以同时打挂几百个节点)。
本部分的全部案例均来自真实生产环境复盘(部分案例的完整记录可参考本仓库
k8s-issues/目录),每个案例按 现象 → 根因 → 排查过程 → 解决方案 → 经验教训 五段式复盘。读完本部分,你应该能建立起大规模集群的"故障直觉":看到某类指标异动,就能立刻锁定排查方向。
第 1 章 大规模典型故障案例库
本章共 15 个案例,按故障域分为五组:
| 分组 | 案例编号 | 主题 |
|---|---|---|
| etcd | 1.1 ~ 1.4 | DB 打满只读、碎片过多、quorum 丢失、leader 频繁切换 |
| kube-apiserver | 1.5 ~ 1.8 | inflight 打满、event 风暴、审计日志打爆磁盘、全量 list 拖垮 |
| 节点 | 1.9 ~ 1.11 | CNI 升级事故、证书轮换事故、conntrack/PID/FD 耗尽 |
| 网络 | 1.12 ~ 1.13 | CoreDNS 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 触发配额上限:
- 某平台团队的 Operator 存在 bug,每 30 秒全量更新一次 CR 对象的 status(内容几乎不变,但每次 update 都产生新 revision);
- etcd 是 MVCC 存储,历史 revision 不会被自动删除,只有 compact 才清理;
- RKE2 默认开启自动 compaction(
etcd-auto-compaction-retention),但 compact 只释放逻辑空间,物理文件大小不会缩小; - 大量 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,集群恢复正常写入。
根治措施:
- 修复 Operator 的"无变化也 update status"的 bug(改用
UpdateStatus前先做 DeepEqual 判断); - 对该 CRD 启用 API Priority and Fairness 限流;
- 配置告警:
etcd_mvcc_db_total_size_in_bytes / etcd_server_quota_backend_bytes > 0.8提前告警,不要等只读; - 建立每月一次定期 defrag 的运维例行任务(详见附录速查命令)。
经验教训
- etcd 的 8GB 不是"可用容量",是"红线"。水位监控必须按 80% 阈值提前介入。
DB SIZE和DB 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_secondsP99 纳入趋势告警(如连续 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解决方案
- 报表程序改造三选一:
- 改用 Pagination:
limit=500+continuetoken 分页拉取; - 改用
resourceVersion=0从 watch cache 读(不写 etcd,但仍有大内存开销); - 最优:通过 informer 本地缓存 + label selector 只取需要的字段/命名空间。
- 改用 Pagination:
- apiserver 侧防护:
yaml
kube-apiserver-arg:
- "enable-priority-and-fairness=true"
# 配合 APF 将低优先级业务 list 限流,并设置 --max-resource-list-bytes 思路:
# 通过 admission 或网关层禁止不带 limit 的全量 LIST Pod- 在 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 副本仍无法消化。
根因
双重问题叠加:
- DNS 风暴源头:新服务的 HTTP 客户端未启用连接复用,每个请求新建连接,每次都触发 DNS 解析;且代码中对同一个域名的解析失败无退避重试(每秒重试上千次)。单 Pod 产生 4000 QPS DNS 查询。
- 放大器效应:应用 Pod 的
resolv.conf默认ndots:5,查询api.example.com(3 个点 < 5)会先依次拼接 4 个 search 域后缀逐个查询,1 次业务查询放大为 5 次 DNS 查询,实际打到 CoreDNS 是 20000 QPS。 - 无 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
- 切换 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(Δ)",同步延迟降到亚秒级。
- 辅助措施:开启 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
- 该 Deployment 有 8000 副本,滚动更新
maxSurge配置过大,数千 Pod 几乎同时在数千节点上创建; - 新镜像从未预热,所有节点本地均无缓存,全部回源 registry;
- containerd 的镜像拉取重试机制在 registry 超时时指数退避过短,失败重试进一步放大请求量;
- 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 缓存,分批放行拉取。
长期(多管齐下):
- 镜像预热:发布流水线前置一步"预热 job",把新镜像提前 24 小时通过 DaemonSet 拉取到全部节点(
imagePullPolicy: IfNotPresent+ sleep 容器); - P2P 分发:部署 Dragonfly/Kraken,节点间 P2P 分发镜像层,registry 只承担种子回源;
- 发布节奏控制:大副本量 Deployment 用
maxSurge: 5%+ 分批发布,杜绝全量同时拉取; - 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 和预热就是裸奔。
latesttag 在大规模集群是万恶之源:不可追溯、不可缓存校验、与 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
concurrency: 50意味着任意时刻 2% 的节点在 drain,但驱逐产生的 Pod 重建压力是全局的:50 节点 × 平均每节点 40 Pod = 2000 Pod 同时调度,超出调度器吞吐;- 多个核心工作负载未配 PodDisruptionBudget,drain 直接打穿最小可用副本;
- 升级波次之间无"健康检查门禁",前一波未完全恢复就进入下一波;
- 控制平面与 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 条)
| # | 军规 | 反面案例 |
|---|---|---|
| A1 | 3 节点 etcd 跨机柜/跨 PDU 部署,严禁同机柜同电源 | 案例三 |
| A2 | etcd 不跨机房拉长部署,容灾靠异地快照备份 | 案例四 |
| 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 | 变更窗口内冻结一切无关变更 | 案例十五 |
| D9 | CNI/CSI/ingress 等基础设施升级按 单节点→机柜→可用区→全量 四波灰度 | 案例九 |
| D10 | 所有自动化下发(policy/配置)带爆炸半径检查与审批卡点 | 案例十三 |
2.3 运维维度(10 条)
| # | 军规 | 说明 |
|---|---|---|
| O1 | etcd 快照定时 + 异地备份 + 每季度恢复演练 | 案例三 |
| O2 | 每月低峰期 etcd 逐台 defrag,碎片率纳入巡检 | 案例二 |
| O3 | 证书有效期全量登记,到期前 30 天告警 | 案例十 |
| O4 | 节点故障先冻结自动化(autoscaler/自愈/升级)再处置 | 案例九 |
| O5 | 批量清理对象(event/pod)分批限速,禁止一把梭 delete | 案例六 |
| O6 | 控制平面磁盘全挂载点监控,审计日志独立分区 | 案例七 |
| O7 | 变更必有回滚方案,回滚方案必须演练过 | 案例十五 |
| O8 | 建立故障复盘文化:无指责、重流程、产出可执行的改进项 | 本部分全部 |
| O9 | 运维操作命令进 runbook,禁止"靠老师傅记忆" | 案例三 |
| O10 | 值班 on-call 双备份,重大故障 15 分钟内升级 | SRE 通识 |
2.4 观测维度(10 条)
| # | 军规 | 说明 |
|---|---|---|
| M1 | SLI/SLO 先行:先定义"健康长什么样",再谈告警 | 案例二 |
| M2 | etcd 必监控:DB SIZE 与 IN USE 双指标、backend commit P99、leader 切换频率 | 案例一/二/四 |
| M3 | apiserver 必监控:inflight 水位、LIST 延迟、pprof 常态化可用 | 案例五/八 |
| M4 | 告警按"频率+趋势"设计,而非仅绝对阈值 | 案例二/四 |
| M5 | 周期性故障优先怀疑 cronjob/定时任务 | 案例八 |
| M6 | DNS 三层监控:CoreDNS、NodeLocal、节点 conntrack | 案例十二 |
| M7 | kube-proxy 同步延迟 P99 > 5s 即告警 | 案例十三 |
| M8 | event 写入 QPS > 50/s 告警 | 案例六 |
| M9 | 节点侧三水位:conntrack / PID / FD,全部可视化 | 案例十一 |
| M10 | 监控本身要高可用:监控挂了等于瞎了,降级期间禁止变更 | 第四部分 |
2.5 优化维度(10 条)
| # | 军规 | 说明 |
|---|---|---|
| P1 | APF(Priority and Fairness)大规模集群必开,节点心跳最高优先级 | 案例五 |
| P2 | 客户端 list 规范:selector + 分页 + resourceVersion=0 | 案例八 |
| P3 | 审计策略分级:读操作只记 Metadata,禁全量 RequestResponse | 案例七 |
| P4 | audit-log-mode=batch,可用性优先于审计实时性 | 案例七 |
| P5 | ndots 收敛(默认 5 → 2)+ FQDN 加点 | 案例十二 |
| P6 | 数千节点必上 NodeLocal DNSCache | 案例十二 |
| P7 | etcd quota 调大(8GB)+ 水位 80% 提前告警 | 案例一 |
| P8 | election-timeout 按网络质量调整,跨机房 5s | 案例四 |
| P9 | Operator/控制器上线前做写放大评估(status 更新频率) | 案例一/六 |
| P10 | event 聚合键设计:message 不嵌入时间戳/随机值 | 案例六 |
第 3 章 生产就绪检查清单(上线前 50 条)
使用方法:集群上线 / 重大版本发布前,逐项打钩并签字。任何一项为 ✗ 均不应上线。
3.1 容量(8 条)
| # | 检查项 | 通过标准 |
|---|---|---|
| C1 | 节点规模与控制平面规格匹配 | 符合容量估算公式卡(见附录) |
| C2 | etcd DB 配额与当前水位 | quota ≥ 8GB,当前水位 < 50% |
| C3 | Pod/Service CIDR 余量 | 可用 IP ≥ 规划规模的 3 倍 |
| C4 | apiserver inflight 参数 | 按规模上调并通过压测验证 |
| C5 | registry 带宽与 P2P 分发 | 支撑"镜像×节点数÷发布时长"峰值 |
| C6 | CoreDNS 副本数与容量 | 峰值 QPS × 3 冗余 + NodeLocal 就绪 |
| C7 | 调度器吞吐压测 | 满足最大批量重建场景(升级 storm) |
| C8 | 存储/CSI 容量与 IOPS 规划 | 峰值 IO 余量 ≥ 50% |
3.2 高可用(8 条)
| # | 检查项 | 通过标准 |
|---|---|---|
| H1 | etcd 三成员机柜/电源反亲和 | 任意单点故障不丢 quorum |
| H2 | control-plane 负载均衡健康检查 | VIP/LB 摘除故障 apiserver < 10s |
| H3 | 核心组件多副本反亲和部署 | scheduler/controller-manager/coredns |
| H4 | PDB 覆盖率 | 核心服务 100%,全业务 ≥ 90% |
| H5 | 节点批量故障演练 | 模拟 5% 节点失联,业务 SLO 不破 |
| H6 | 控制平面全灭恢复演练 | 近一季度内演练过,runbook 验证有效 |
| H7 | kube-proxy 模式 | ipvs/nftables(Service > 5000 强制) |
| H8 | 拓扑分布约束 | 跨可用区业务全部配置 |
3.3 安全(9 条)
| # | 检查项 | 通过标准 |
|---|---|---|
| S1 | RBAC 最小权限审计 | 无 cluster-admin 滥用,SA 按职责划分 |
| S2 | NetworkPolicy 基线 | default-deny + 基础放行模板 + 下发审批流 |
| S3 | 审计策略分级 + batch 模式 + 独立分区 | 磁盘满不阻塞 API |
| S4 | 证书台账与到期告警 | 全部证书登记,30 天预警 |
| S5 | 镜像安全扫描门禁 | 高危漏洞镜像禁止入库/上线 |
| S6 | Pod 安全标准(PSS) | baseline 集群级 enforce,restricted 业务灰度 |
| S7 | API 准入控制链评审 | webhook 超时与 failurePolicy 逐条评审 |
| S8 | etcd 加密与静态加密(secrets encryption) | 已启用并演练密钥轮换 |
| S9 | 节点基线安全加固 | ssh 限制、防火墙端口白名单(见附录端口表) |
3.4 监控(9 条)
| # | 检查项 | 通过标准 |
|---|---|---|
| N1 | SLI/SLO 定义并接入看板 | 核心链路(创建 Pod、DNS、API)全覆盖 |
| N2 | etcd 黄金指标告警 | DB 水位 80%、commit P99、leader 切换频率 |
| N3 | apiserver 黄金指标告警 | inflight 80%、LIST P99、5xx 率 |
| N4 | 节点三水位告警 | conntrack/PID/FD ≥ 80% |
| N5 | DNS 三层告警 | CoreDNS + NodeLocal + conntrack |
| N6 | event/审计写入速率告警 | event > 50/s 持续 5min |
| N7 | kube-proxy 同步延迟告警 | P99 > 5s |
| N8 | 证书到期告警 | 全部证书 < 30 天触发 |
| N9 | 监控系统自身高可用与降级预案 | 监控故障时冻结变更流程明确 |
3.5 备份(8 条)
| # | 检查项 | 通过标准 |
|---|---|---|
| B1 | etcd 定时快照 | cron 已配置(如 0 */6 * * *),retention ≥ 5 |
| B2 | 快照异地备份 | 跨机房/对象存储,加密传输 |
| B3 | 快照恢复演练 | 近一季度成功恢复过 |
| B4 | quorum 丢失 runbook | force-new-cluster 流程文档化并演练 |
| B5 | 关键声明式资产 Git 化 | 全部 workload/config 可从 Git 重建 |
| B6 | 镜像仓库备份 | registry 存储可恢复 |
| B7 | PV 数据备份策略 | 有状态业务全部覆盖(Velero/存储快照) |
| B8 | RTO/RPO 指标定义并验证 | 与业务方书面确认 |
3.6 应急(8 条)
| # | 检查项 | 通过标准 |
|---|---|---|
| E1 | on-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 先行,看见才能管好 |
| 第五部分 | 性能优化调优 | 默认值是中小集群的,逐层找天花板 |
| 第六部分 | 故障案例与最佳实践 | 故障是最好的老师,复盘是最好的教材 |
最后一句话:大规模集群运维的本质,不是"不出故障",而是"控制爆炸半径、缩短恢复时间、让同样的故障不发生第二次"。愿本书的每一个案例,都帮你少熬一个通宵。