etcd 磁盘 fsync 抖到 300ms,apiserver 大面积 5xx:一次 K8s 控制面写延迟排查实录

举报
yd_243555368 发表于 2026/11/06 01:02:06 2026/11/06
【摘要】 集群 apiserver p99 飙到 10s、kubelet 上报 NodeReady 抖动,看着像网络问题,实际是 etcd 所在云盘 IOPS 不够。本文给出完整排查链:用 etcdctl endpoint status 看 FSYNC 定位慢盘、iostat 与 fio fsync 测试量化落盘延迟、逐个成员 compaction+defrag 从 1.4GB 降到 620MB、分离镜像

故障现场: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 抢同一块盘。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。