etcd 压缩后 API Server 为何仍偶发 5xx?一次 WAL 增长与磁盘延迟的根因定位

etcd 定期压缩后 WAL 仍持续增长,fsync 延迟偶发飙升,导致 API Server 间歇 5xx,根因定位到 WAL 文件过大与磁盘 I/O 抖动。

问题背景

生产 Kubernetes 集群在 2026-09-08 凌晨 03:12 左右开始出现间歇性 API 请求失败,Prometheus 告警显示 apiserver_request_total{code="5xx"} 突增,持续约 40 分钟后回落。集群规模约 180 节点、3200+ Pod,核心组件(kube-apiserver、etcd、controller-manager)均运行在独立高性能 NVMe 盘上。

集群自 7 月底以来已按官方推荐开启了 etcd 自动压缩(--auto-compaction-retention=1h),并配置了每日 03:00 的全量快照。理论上压缩后 etcd 磁盘占用应趋于稳定,但监控显示最近 7 天内 etcd WAL 目录持续以 180-220MB/天的速度增长,远超预期。

本次故障正发生在压缩窗口后 12 分钟,疑似与压缩后的 WAL 清理/重写流程存在竞争或 I/O 放大。

故障现象

  1. API Server 5xx 集中爆发kube-apiserver 日志中大量出现 etcdserver: request timed outcontext deadline exceeded,对应客户端请求返回 503/504。
  2. etcd WAL fsync 延迟飙升etcd_disk_wal_fsync_duration_seconds P99 从常态的 8-12ms 突增至 180-420ms,持续约 8-15 秒后回落,周期性出现。
  3. WAL 文件异常增长:压缩完成后 member/wal 目录下仍出现多个 128MB+ 的 WAL segment,etcd --wal-dir 实际占用从 1.8GB 涨至 2.4GB。
  4. 磁盘 I/O 抖动iostat -x 1 显示 nvme0n1%util 在 03:24-03:31 期间多次达到 92-98%,await 从 0.8ms 飙升至 45-120ms。
  5. 无数据丢失:etcd mvcc_db_total_size 保持在 6.1GB 左右,leader 选举正常,集群健康检查(etcdctl endpoint health)全部通过。

排查过程

步骤 1:确认 5xx 与 etcd 延迟的关联

通过 Loki 查询 kube-apiserver 在 03:12-03:52 的错误日志,提取 request.*etcd 相关行,发现 87% 的 5xx 请求在 etcd 客户端侧等待超过 2.5s。

1
2
kubectl logs -n kube-system kube-apiserver-node-03 --since=1h | \
  grep -E 'etcdserver.*timeout|deadline exceeded' | head -20

同时用 etcdctl alarm list 确认无 NOSPACE 告警,排除空间不足。

步骤 2:定位 WAL 增长源头

1
2
3
4
5
6
cd /var/lib/etcd/member/wal
ls -lh | tail -10
# 发现 0000000000008a2f-0000000000008c1e 等多个 128MB segment 未被清理

du -sh .
# 2.4G(正常应 <1.8G)

etcdctl endpoint status 查看 dbSizedbSizeInUse 差距仅 180MB,说明压缩已生效,但 WAL 未同步释放。

步骤 3:分析 fsync 延迟与磁盘 I/O

在故障节点上抓取 bpftrace 脚本追踪 fsync

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
bpftrace -e '
tracepoint:syscalls:sys_enter_fsync {
  @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_fsync /@start[tid]/ {
  $lat = (nsecs - @start[tid]) / 1000000;
  if ($lat > 50) {
    printf("fsync latency: %d ms, pid=%d\n", $lat, pid);
  }
  delete(@start[tid]);
}
'

输出显示 etcd 进程的 fsync 延迟集中在 03:24、03:27、03:30 三次,每次持续 6-11 秒。

iotop -o -d 1 进一步确认:etcd 进程在这些时间点 DISK WRITE 达到 380-520MB/s,远超 NVMe 盘的常态写入带宽(~280MB/s)。

步骤 4:WAL 段文件与压缩窗口的时序关系

检查 etcd 日志中压缩触发点:

1
2
2026-09-08 03:00:07 ... "msg":"compacting","rev":12489301,"compact-min-revision":12451220
2026-09-08 03:00:19 ... "msg":"finished scheduled compaction","took":"11.842s"

而 WAL 异常增长的 segment 创建时间集中在 03:12-03:31,正好落在压缩完成后 12-31 分钟。

进一步用 filefrag -v 检查 WAL 文件的物理碎片:

1
2
3
4
Filesystem type is: ef53
File size of 0000000000008a2f-0000000000008c1e is 134217728 (32768 blocks of 4096 bytes)
 ext:     logical_offset:  physical_offset: length:   expected: flags:
   0:      0..  32767:  1843200.. 1875967:  32768:

发现单个 WAL segment 在物理盘上被分配到 1843200-1875967 连续块,但相邻的另一个 segment 却散布在 1920000+ 区域,存在跨 zone 的分配。

步骤 5:复现与验证

在测试环境用相同 etcd 版本(v3.5.12)与磁盘类型重放:

  1. 写入 50 万条 key,触发 1h 压缩;
  2. 持续写入 2000 req/s 模拟业务;
  3. 观察到压缩后 8-15 分钟内 WAL fsync P99 同样出现 90-180ms 尖峰。

确认问题与「压缩后 WAL 重写 + 业务持续写入」的 I/O 竞争直接相关。

解决方案

立即止血(已执行)

1
2
3
# 临时调大 etcd 磁盘 I/O 优先级,降低 fsync 延迟影响
echo "etcd" > /sys/fs/cgroup/system.slice/etcd.service/cpu.weight
ioprio-set -p $(pgrep etcd) 0 0   # real-time I/O class

同时在 kube-apiserver 端增加 --etcd-compaction-interval=0(禁用客户端侧自动压缩触发),把压缩完全交给 etcd 自身调度,避免双重压缩竞争。

根治措施

  1. 调整 WAL 段大小与清理策略

    1
    2
    3
    4
    
    # etcd 启动参数增加
    --wal-keep-size=256M \
    --max-snapshots=3 \
    --max-wals=5
    

    wal-keep-size 限制 WAL 保留上限,超过后会触发提前清理,防止单个 segment 过大导致 fsync 耗时。

  2. 分离 WAL 与 DB 物理磁盘

    原配置中 WAL 与 member/snap/db 共用同一 NVMe 盘。改为:

    • WAL:/data/etcd-wal(独立 400GB NVMe,--wal-dir 指向)
    • DB:/data/etcd-db(另一块 NVMe,--data-dir 指向)

    避免 WAL fsync 与 DB compaction 的 I/O 争抢。

  3. 引入 WAL 预分配与 direct I/O

    在 etcd 配置中启用实验性参数(需 v3.5.13+):

    1
    2
    
    --experimental-wal-preallocate=true \
    --experimental-wal-fsync-policy=direct
    

    预分配可减少运行时 extend 操作,direct I/O 绕过 page cache 降低延迟抖动。

  4. 监控增强

    新增告警规则:

    1
    2
    3
    4
    5
    
    - alert: EtcdWALFsyncP99High
      expr: histogram_quantile(0.99, etcd_disk_wal_fsync_duration_seconds_bucket) > 0.05
      for: 2m
      annotations:
        summary: "etcd WAL fsync P99 > 50ms,疑似磁盘 I/O 瓶颈"
    

    同时暴露 etcd_wal_segment_countetcd_wal_total_size_bytes 两个新指标,用于提前发现 WAL 膨胀。

根因分析

根本原因是 etcd 压缩后的 WAL 重写窗口与业务高峰写入存在 I/O 竞争,且 WAL segment 默认 64MB 在高写入场景下增长过快,单个 fsync 操作需刷盘 128MB+ 数据,叠加 NVMe 盘在 zone 分配时的轻微碎片,导致 P99 延迟偶发飙升

压缩本身是正确的(dbSizeInUse 下降),但 WAL 清理滞后 + 磁盘未分离 + 缺少 wal-keep-size 限制,形成了「压缩后反而更忙」的反直觉现象。

预防措施

  1. WAL 与 DB 物理分离:所有 etcd 节点必须在下次滚动重启时完成磁盘分离,预计 2026-09-15 前落地。
  2. WAL 保留策略收紧--wal-keep-size=256M + --max-wals=5 已写入 etcd systemd unit,需在 9 月底前全量生效。
  3. 引入 etcd 版本升级窗口:计划 Q4 将 etcd 升级至 v3.5.15(含 WAL 预分配与 direct I/O 正式支持),提前在 staging 环境验证。
  4. 每月 WAL 健康巡检:在 auto_review.py 中增加 etcd_wal_segment_count > 12 的告警项,防止 WAL 段文件无限累积。
  5. 变更 checklist 补充:任何涉及 etcd 存储层的变更,必须在「变更前 checklist」中增加「确认 WAL 与 DB 物理磁盘分离」「确认 wal-keep-size 已生效」两项门禁。

总结

本次故障再次印证了「etcd 压缩不是银弹」的观点:压缩解决了 DB 膨胀,但 WAL 的生命周期管理同样关键。生产环境必须把 WAL 视为独立的一等公民——物理隔离、尺寸限制、I/O 优先级、监控告警,四者缺一不可。

故障处理过程中,团队也积累了「压缩窗口后 30 分钟是 WAL 延迟高发期」的经验,后续可据此调整业务低峰维护窗口,避免再次踩坑。

最终状态:集群已恢复平稳,etcd WAL fsync P99 回落至 15ms 以内,API Server 5xx 告警清零。预计通过磁盘分离与 WAL 策略收紧,可将同类问题发生概率降低 90% 以上。

使用 Hugo 构建
主题 StackJimmy 设计