etcd 磁盘 fsync 抖到 300ms,apiserver 大面积 5xx:一次 K8s 控制面写延迟排查实录
故障现场:apiserver 突然大面积 5xx
凌晨两点,K8s 集群的告警群里连刷三条:apiserver request latency p99 > 10s、etcd request failed (timeout)、kubelet 上报 NodeReady 抖动。同时业务侧开始零星报 dial tcp 10.96.0.1:443: i/o timeout。这类症状看着像网络问题,但真正的凶手往往在 etcd 的磁盘上。
本文按“先看 etcd 自身指标 → 再看磁盘 fsync → 最后回头修配置”的顺序给一套可直接复用的排查路径。集群版本:K8s v1.26,etcd v3.5.9,3 节点堆叠部署(etcd 与 kubelet 同机)。
第一步:确认 etcd 是瓶颈而不是网络
先用 endpoint status 看各成员的健康度和延迟。重点不是 healthy,而是 fsync 与 db size:
ETCDCTL_API=3 etcdctl \
--endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
endpoint status --write-out=table
输出里看到 FSYNC 一列:正常在 1~5ms,healthy 的集群 fsync 很少超过 10ms。我们那台 10.0.0.12 显示 FSYNC 312ms,另两台 4ms 左右,IS LEADER 那一列恰好也指向 10.0.0.12 —— 写请求全压在慢盘上,全集群一起抖。
顺便确认没有 NOSPACE 告警、db 没有到配额上限:
etcdctl ... endpoint status -w json | jq -r '.[] | "\(.Endpoint) db=\(.Status.dbSize/1024/1024|floor)MB leader=\(.Status.leader)"'
etcdctl ... alarm list
db 只有 1.4GB、quota 默认 2GB、alarm 为空,说明不是容量问题,也不是“写了太多历史版本”。
第二步:落盘看磁盘到底慢在哪
上机器看块设备队列与写延迟,重点关注 await、aqu-sz 和 %util:
iostat -xmd 1 10 /dev/vdb
# 关键列示例(故障机)
Device r/s w/s rkB/s wkB/s r_await w_await aqu-sz %util
vdb 12.0 980.0 480 15680 18.2 241.6 12.4 99.8
w_await 241ms、aqu-sz 12.4、%util 99.8 —— 盘已经满了,队列堆着 12 个请求。接着确认 fsync 的真实耗时分布,这才是 etcd 最敏感的指标:
fio --name=fsync-test --filename=/var/lib/etcd/fsync.bin --size=1G \
--rw=randwrite --bs=4k --iodepth=1 --fsync=1 --runtime=30 --time_based
结果 fdatasync/s=38,单次约 26ms,但 p99 抖动到 400ms+。这台机器用的是网络盘(云盘 PL1 级别),基线只有 120 IOPS,etcd 的 WAL 顺序写 + fsync 正好撞在它的弱点上。
还发现一个放大器:noatime 没加,且节点上同时跑着镜像仓库的 imagefs,GC 时大量写把 IO 抢走。
第三步:止血动作(按影响从低到高排)
1)给 etcd 数据盘降噪。把容器运行时目录与 /var/lib/etcd 分开,并把 emptyDir/日志盘隔到另一块盘;fstab 加 noatime:
/dev/vdb /var/lib/etcd ext4 defaults,noatime,nodiratime 0 2
mount -o remount,noatime,nodiratime /var/lib/etcd
2)清理历史碎片,减小每次落盘量。先 compaction 再 defrag,且必须 逐个成员 执行、不要同时跑:
REV=$(etcdctl ... endpoint status -w json | jq -r '.[0].Status.header.revision')
etcdctl ... compact $REV
etcdctl ... defrag --command-timeout=300s
etcdctl ... endpoint status -w table # 确认 dbSize 是否下降、fsync 是否回落
我们这次 db 从 1.4GB 降到 620MB,fsync 从 312ms 降到 38ms,apiserver 的 p99 立刻从 12s 回落到 800ms 量级,先救住了集群。
3)限速引发雪崩的写路径。临时把 --max-requests-inflight 与 --max-mutating-requests-inflight 调低(如 400 / 200),并开启 APF(APIPriorityAndFairness)默认特性,避免一个批量控制器把 etcd 队列打满:
# /etc/kubernetes/manifests/kube-apiserver.yaml
- --max-requests-inflight=400
- --max-mutating-requests-inflight=200
4)怀疑 WAL 盘太小时,把 WAL 单独放到低延迟盘(--wal-dir),这是 IO 争抢最有效的隔离手段;顺便调大 --quota-backend-bytes=8589934592(8GB,需先确认 etcd 内存可承受)。
第四步:用指标长期盯着,别等它再炸一次
Prometheus 里这几条足够:
- alert: EtcdFsyncSlow
expr: histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])) > 0.05
for: 5m
labels: {severity: warning}
annotations: {summary: "etcd WAL fsync p99 > 50ms({{ $labels.instance }})"}
- alert: EtcdBackendCommitSlow
expr: histogram_quantile(0.99, rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])) > 0.1
for: 5m
- alert: EtcdDbSizeHigh
expr: etcd_mvcc_db_total_size_in_bytes / etcd_server_quota_backend_bytes > 0.8
for: 10m
再加一条 etcd_server_leader_changes_seen_total > 3(15 分钟内),能提前发现即将发生的选主风暴。
补充:用日志和审计确认“到底是谁在猛写”
止血之后还要回答一个问题:为什么 WAL 会突然变大。两个低成本的取证手段:
# 1) etcd 慢请求日志(kubeadm 部署一般在 /var/log/pods 或 kubelet 的容器日志里)
journalctl -u etcd --since "-30min" | grep -i "took too long" | awk '{print $NF}' | sort | uniq -c | sort -rn | head
# 2) apiserver 审计日志里按 verb 统计写放大
grep '"verb":"\(create\|update\|patch\)"' /var/log/kubernetes/audit.log \
| jq -r '.objectRef.resource' | sort | uniq -c | sort -rn | head -10
我们抓到的 Top 1 是某个自研控制器每 30 秒全量 update 一次 ConfigMap 状态,单对象 380KB,外加 1200 个 Pod 的 status 高频 patch。把状态改成事件驱动 + 加 resourceVersion 乐观锁、把大对象拆小之后,WAL 写入量下降 60%,fsync 峰值彻底消失。这一步比换盘更省钱,也更治本。
复盘:三个必须改的认知
- healthy=true 不等于健康。etcd 会把慢请求也标记成功,只有 fsync / backend commit 的分位数能暴露问题。
- etcd 是延迟敏感型负载,不是吞吐型。给它一块 IOPS 够低延迟的本地盘,比多给 4 核 CPU 有用得多;云盘选型看 p99,不要只看平均 IOPS。
- compaction 不等于监控。真正的告警要落在“把 IO 隔离好、apiserver 精准限流、Prometheus 分位报警”三件事上,而不是每次出事做一次 defrag。
顺手提醒:defrag 期间该成员会阻塞读,务必逐个成员执行并确认 endpoint status 回到 healthy 再做下一个;如果 etcd 与 kubelet 同机,考虑把 etcd 迁到独立节点,避免 kubelet 的镜像 GC 与 etcd 的 WAL 抢同一块盘。
- 点赞
- 收藏
- 关注作者
评论(0)