大规模 K8S 集群中,90% 的控制平面雪崩源于野蛮的 List 请求击穿 APIServer 缓存并耗尽 etcd 磁盘 IO。本文通过配置 APF 阻断高频穿透请求,结合 etcd WAL 磁盘物理隔离与参数调优,彻底解决控制平面高延迟与假死问题。
案发现场:慢如老牛的 APIServer 与崩溃的 etcd
某次集群(K8S v1.26.5, etcd v3.5.7)规模扩容至 500+ Node、20000+ Pod 后,控制平面出现剧烈抖动。具体表现为:kubectl 响应极慢甚至经常 Timeout,新 Pod 处于 ContainerCreating 状态长达数分钟无法调度。
直切要害,先看 APIServer 报错日志:
W0824 10:12:35.123456 1 request.go:1085] Request takes too long: type=list, resource=pods, user=system:serviceaccount:monitoring:custom-operator...
转头去拉 etcd 的日志,标准的重载现象:
{"level":"warn","ts":"...","caller":"etcdserver/server.go:872","msg":"apply request took too long","took":"543.2ms","expected-duration":"100ms","prefix":"k8s.io/pods/..."}
{"level":"warn","ts":"...","caller":"wal/wal.go:783","msg":"sync duration of file 485.4ms, expected duration is <10ms"}
通过 PromQL 看一眼核心指标:
# 查看 etcd WAL fsync 99线延迟
histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))
查询结果显示 fsync 99线延迟竟然飙到了 600ms 以上。正常基于 NVMe SSD 的集群,这个值不该超过 10ms。控制面板的瓶颈已经很清晰了:底层 etcd 的 IO 被彻底打爆,导致 Quorum 写入超时,上层 APIServer 出现堆积。
为什么一个外围的 Operator 能轻易干碎底层 etcd?
在排查过程中,通过开启 APIServer 的审计日志(Audit Log),发现元凶是某个业务团队自己写的 custom-operator。它每隔几秒钟就在全局范围内发起针对 Pod 和 ConfigMap 的全量 List 操作。
这里必须讲一下 K8S APIServer 处理 List 请求的底层逻辑。很多人以为 APIServer 有本地 Cache,所有的读请求都不会对 etcd 造成压力。这是典型的只知其一不知其二。
当客户端发起 List 请求时,决定是否命中 APIServer 缓存的关键在于 ResourceVersion 和 Limit 参数:
-
ResourceVersion="0":直接从 APIServer 本地 Cache 读取数据,对 etcd 无影响,速度最快。 -
ResourceVersion=""(未设置):默认行为,要求保证强一致性(Quorum Read)。APIServer 必须穿透缓存,向 etcd 发起请求以获取最新数据。在数据量庞大的集群中,这种全量拉取不仅消耗 etcd CPU 和内存,还会挤占网络带宽。 -
未设置分页参数 (
Limit/Continue):如果单次拉取的数据集达到数百 MB,APIServer 在反序列化时会造成巨大的 CPU 飙升和内存消耗(OOM 诱因)。
当时的那个 custom-operator,用的是旧版 client-go,且写法极其粗暴,未走 Informer 机制(基于 Watch 维护本地 Cache),而是直接调用原生 Client 的 List 方法,并且未带任何缓存容忍参数。这就是典型的“一脚油门把 etcd 踹进火葬场”。
调优实战:防穿透与底层 IO 隔离
既然找到了问题,处理思路就很直接:上层限流,底层扩容 IO。
1. APIServer 侧:启用 APF(API Priority and Fairness)进行流控
绝对不要指望业务开发能立刻改掉拉垮的代码,运维必须从架构层面自保。K8S 自带的 API 优先级和公平性(APF)就是用来防这类 DDoS 的。
针对这个惹祸的 Operator,我们专门下发一个 FlowSchema 和 PriorityLevelConfiguration 来压制它的并发数:
# 1. 定义并发等级:限制最多只能有 2 个并发,超出直接拒绝或排队
apiVersion: flowcontrol.apiserver.k8s.io/v1beta3
kind: PriorityLevelConfiguration
metadata:
name: limit-custom-operator
spec:
type: Limited
limited:
assuredConcurrencyShares: 5
limitResponse:
type: Reject # 超过限额直接拒绝,不排队,快速失败
---
# 2. 匹配肇事的 ServiceAccount 规则
apiVersion: flowcontrol.apiserver.k8s.io/v1beta3
kind: FlowSchema
metadata:
name: restrict-custom-operator
spec:
priorityLevelConfiguration:
name: limit-custom-operator
matchingPrecedence: 100
rules:
- subjects:
- kind: ServiceAccount
serviceAccount:
name: custom-operator
namespace: monitoring
resourceRules:
- apiGroups: ["*"]
resources: ["pods", "configmaps"]
verbs: ["list"]
应用该策略后,该 Operator 的高频穿透读被直接按死在 APIServer 层,返回 429 Too Many Requests,etcd 的负载曲线立刻呈断崖式下降。
2. etcd 侧:WAL 与数据盘的物理隔离
虽然拦住了异常流量,但 etcd fsync 延迟对磁盘波动的敏感度依然极高。默认情况下,etcd 的 WAL(预写日志)和 db 数据文件都在同一块盘上。 etcd 处理一次写请求的路径是:收到请求 -> Append WAL -> fsync 落盘 -> 应用到状态机 -> 返回。如果 fsync 慢,整个集群的写入就慢。
在生产环境中,必须将 WAL 剥离到单独的极速盘(最好是基于 PCIe 的 NVMe SSD,不与其他任何 IO 混用)。
操作步骤:
假设新的高性能盘挂载点为 /data/etcd-wal。
-
停止 etcd 进程。
-
迁移原有的 WAL 目录:
bash mv /var/lib/etcd/member/wal/* /data/etcd-wal/ rm -rf /var/lib/etcd/member/wal ln -s /data/etcd-wal /var/lib/etcd/member/wal - 调整文件系统挂载参数。在
/etc/fstab中,确保存储 etcd 数据的磁盘禁用 atime 记录,减少无用元数据更新:text /dev/nvme1n1 /data/etcd-wal ext4 defaults,noatime,nodiratime,barrier=0 0 0 - 启动 etcd。
3. etcd 参数调优(缓解大对象写入)
除了存储隔离,对于 v3.5 版本的 etcd,我们还需调整以下参数,提升其在高并发场景下的生命力:
-
--snapshot-count=10000:默认 100000 次修改才做一次快照。将其调低,减少每次构建快照的内存消耗和 IO 瞬时突增。 -
--quota-backend-bytes=8589934592:默认 2G,大集群极易触顶导致alarm:NOSPACE,直接拉满到 8G(官方建议最大上限)。 -
开启自动压缩:
--auto-compaction-retention=1/--auto-compaction-mode=periodic,每小时清理一次历史版本,防止库文件无限膨胀。
常见问题
Q: APF 配置把业务请求拦掉了,业务跑异常了怎么办?
A: 运维的底线是保证控制平面的可用性,而不是为烂代码买单。如果是 List 被限流返回 429,业务应该在代码中实现退避重试(Exponential Backoff),最根本的解决方法是改写代码,使用 client-go 的 SharedInformerFactory,基于 List-Watch 机制消费本地内存数据,绝不允许将 APIServer 当作通用数据库高频乱查。
Q: 为什么 etcd 报 NOSPACE,但我看了下磁盘空间还有很多剩余?
A: 这是个经典的认知误区。etcd 的 NOSPACE 通常指的不是宿主机的磁盘满了,而是 etcd 的 DB 文件大小达到了 --quota-backend-bytes 设置的硬上限(默认 2GB)。解决办法:首先用 etcdctl compact 压缩历史版本,然后执行 etcdctl defrag 释放存储碎片,最后视情况修改启动参数提高 Quota 值。
Q: APIServer 的参数配置里,--max-requests-inflight 和 APF 有什么区别?
A: --max-requests-inflight(及其相关的 mutating 参数)是全局并发限制,属于一刀切的限流。一旦触发阈值,不论是关键的 Controller 还是无用的旁路脚本,都会被无差别丢弃。而 APF 是精细化流控,支持根据资源类型、User、Namespace 等对请求进行分类、排队和熔断。在较新的 K8S 版本中,APF 是更推荐且更核心的防灾手段。