K8s etcd 性能调优慢查询日志、压缩策略与碎片整理一、你的 kube-apiserver 请求延迟直升 10 倍根因在 etcd 没做过 compactionK8s 集群跑了一年以后运维团队发现一个规律每隔 2-3 个月kubectl get pods 的响应时间从 200ms 涨到 2-3 秒。重启 apiserver 无效、重启 etcd 暂时缓解但几天后复现。最终定位到 etcd 的存储文件膨胀到 8GB而实际数据只有 1GB——其余 7GB 都是未 compact 的历史版本。etcd 的 MVCC多版本并发控制机制会保留每次写入的历史版本。每次 watch 事件、lease 续租、status 更新——所有操作都会产生一个新的 revision。如果不定期 compact历史版本会无限累积导致两方面问题存储空间膨胀和查询扫描范围增大。etcd 性能调优的核心三板斧compaction压缩历史版本、defrag碎片整理和慢查询日志定位慢操作。这三件事不做集群越大性能退化越快。二、底层机制与原理剖析etcd 使用 BoltDB 作为底层存储引擎BoltDB 是一个 BTree 实现的嵌入式 KV 数据库。其写入流程始于提案到 Raft日志复制到 Follower 后应用到 BoltDB。在 BoltDB 内部BTree 节点更新会将新数据页写入文件尾旧数据页标记为可回收导致文件持续增长。当 compaction 触发时系统清理超过保留期的旧 revision 并释放被标记的页但文件物理大小不会缩小从而产生碎片。随后 defrag 触发在线碎片整理将文件缩小回实际数据大小。这一过程中若未进行 compactMVCC 扫描范围会变大导致 range query 变慢同时存储文件增大也会增加磁盘 IO。三个关键操作的理解Compaction删除指定 revision 之前的所有历史版本数据。不会减小 BoltDB 文件大小——只是标记这些数据块为可回收。没有 compaction任何 range 查询都需要扫描大量的历史 revisionO(n) 的扫描复杂度在 n 增大时性能急剧劣化。Defrag碎片整理。Compaction 释放了逻辑空间但 BoltDB 的文件在物理上并没有缩小。Defrag 操作重新组织数据页把分散的有效数据紧凑排列截断文件末尾的空洞。这是一个在线操作但会短暂阻塞 etcd 的写入通常 1-5 秒因此在生产环境执行时需要谨慎。慢查询日志etcd 3.4 支持--experimental-slow-request-log默认 500ms。记录了哪些 key 的 range 操作耗时过长。对于定位哪个 controller 在 List 所有的 Pod 并导致 etcd 压力过大这类问题非常有用。三、生产级代码实现etcd compaction 和 defrag 的自动化运维脚本#!/bin/bash # # etcd 自动化运维脚本 # 功能定时 compaction 碎片整理 性能检查 # 设计决策 # 1. compaction 每小时执行保留期 1 小时 # 2. defrag 仅在碎片率 2x 时执行 # 3. defrag 逐节点执行每次间隔 30s 等待集群恢复 # set -euo pipefail # 配置 ETCD_ENDPOINTS${ETCD_ENDPOINTS:-https://127.0.0.1:2379} ETCD_CERT${ETCD_CERT:-/etc/kubernetes/pki/etcd/server.crt} ETCD_KEY${ETCD_KEY:-/etc/kubernetes/pki/etcd/server.key} ETCD_CA${ETCD_CA:-/etc/kubernetes/pki/etcd/ca.crt} ETCDCTLetcdctl --endpoints${ETCD_ENDPOINTS} \ --cacert${ETCD_CA} \ --cert${ETCD_CERT} \ --key${ETCD_KEY} COMPACTION_RETENTION_HOURS1 # 保留 1 小时的历史版本 DEFRAG_FRAG_THRESHOLD2.0 # 碎片率 2 时执行 defrag HEALTH_CHECK_RETRIES10 log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* } # 1. 检查集群健康状态 check_health() { log 检查 etcd 集群健康状态... if ! $ETCDCTL endpoint health --cluster 2/dev/null; then log ERROR: etcd 集群不健康跳过运维操作 exit 1 fi log 集群健康状态正常 } # 2. 执行 Compaction run_compaction() { log 开始执行 compaction... # 获取当前 revision CURRENT_REV$($ETCDCTL endpoint status --write-outjson | \ python3 -c import sys,json; datajson.load(sys.stdin); print(max(d[Status][header][revision] for d in data))) log 当前 revision: ${CURRENT_REV} # 计算 compact revision保留最近 N 小时 # Compact 语法保留到指定 revision 为止 COMPACT_REV$((CURRENT_REV - 1000)) # 简化的保留策略 log 执行 compactrevision: ${COMPACT_REV} $ETCDCTL compact ${COMPACT_REV} # 检查 compaction 是否完成 # 等待所有节点完成 compact sleep 2 log Compaction 完成 } # 3. 检查碎片率并执行 Defrag run_defrag() { log 检查 etcd 碎片率... # 获取各节点的状态 STATUS$($ETCDCTL endpoint status --write-outjson) # 解析每个节点的碎片率 MEMBERS$(echo $STATUS | python3 -c import sys, json data json.load(sys.stdin) for d in data: ep d[Endpoint] db_size int(d[Status][dbSize]) db_in_use int(d[Status][dbSizeInUse]) frag_ratio db_size / max(db_in_use, 1) print(f{ep}|{db_size}|{db_in_use}|{frag_ratio:.2f}) ) need_defragfalse for member in $MEMBERS; do IFS| read -r ep db_size db_use frag $member log 节点: $ep, DB大小: $((db_size/1024/1024))MB, 使用: $((db_use/1024/1024))MB, 碎片率: $frag if (( $(echo $frag $DEFRAG_FRAG_THRESHOLD | bc -l) )); then log 节点 $ep 碎片率 $frag $DEFRAG_FRAG_THRESHOLD, 需要 defrag need_defragtrue fi done if ! $need_defrag; then log 所有节点碎片率均在阈值以下跳过 defrag return fi # 逐节点执行 defrag log 开始逐节点 defrag每个节点间隔 30 秒... ENDPOINTS_LIST($(echo $STATUS | python3 -c import sys, json data json.load(sys.stdin) for d in data: print(d[Endpoint]) )) for ep in ${ENDPOINTS_LIST[]}; do log Defrag 节点: $ep # 逐节点执行不指定 --cluster 避免全集群同时阻塞 $ETCDCTL --endpoints$ep defrag log Defrag 完成: $ep等待集群恢复... # 等待节点恢复健康 for i in $(seq 1 $HEALTH_CHECK_RETRIES); do if $ETCDCTL --endpoints$ep endpoint health 2/dev/null | grep -q is healthy; then log 节点 $ep 已恢复健康 break fi sleep 3 done # 节点间等待 30 秒让 raft 日志同步 sleep 30 done log 所有节点 defrag 完成 } # 4. 性能检查 check_performance() { log 执行性能检查... # 检查慢查询日志需要 etcd 开启 experimental-slow-request-log # 检查 etcd 的 metrics METRICS$($ETCDCTL endpoint status --write-outjson) DB_SIZE$($ETCDCTL endpoint status --write-outjson | \ python3 -c import sys, json data json.load(sys.stdin) sizes [int(d[Status][dbSize]) for d in data] print(fDB总大小: {sum(sizes)/1024/1024:.0f}MB, 最大单节点: {max(sizes)/1024/1024:.0f}MB) ) log $DB_SIZE # 检查是否有 leader 频繁切换 LEADER_COUNT$($ETCDCTL endpoint status --write-outjson | \ python3 -c import sys, json data json.load(sys.stdin) leaders sum(1 for d in data if d[Status][leader] d[Status][header][member_id]) print(leaders) ) if [ $LEADER_COUNT -ne 1 ]; then log WARNING: Leader 数量异常: $LEADER_COUNT期望 1 fi } # 5. etcd 配置优化建议 show_recommendations() { cat EOF etcd 配置优化建议 1. 自动 compaction必须开启: etcd --auto-compaction-modeperiodic \\ --auto-compaction-retention1h 2. 慢查询日志推荐开启: etcd --experimental-slow-request-log \\ --experimental-slow-request-duration500ms 3. 存储配额防止磁盘爆满: etcd --quota-backend-bytes8589934592 # 8GB 4. 使用 SSDetcd 对磁盘延迟敏感: fio --rwwrite --bs4k --iodepth1 --runtime60 | grep 99th percentile # 99th 延迟应 10ms 5. 独立磁盘避免和 kubelet/容器运行时共享 IO: mount /var/lib/etcd 到独立磁盘分区 EOF } # 主流程 main() { check_health run_compaction run_defrag check_performance show_recommendations log etcd 运维任务完成 } main $etcd 配置建议通过 kubeadm 修改# /etc/kubernetes/manifests/etcd.yaml (关键参数) spec: containers: - command: - etcd # 自动 compaction # 每 1 小时自动压缩历史版本 - --auto-compaction-modeperiodic - --auto-compaction-retention1h # 慢查询日志 # 超过 500ms 的请求记录日志 - --experimental-slow-request-log - --experimental-slow-request-duration500ms # 存储配额 # 防止 BoltDB 文件无限增长 - --quota-backend-bytes8589934592 # 快照数限制 # 保留最近 5 个快照旧的自动清理 - --max-snapshots5 - --max-wals5四、边界分析与架构权衡Compaction 的副作用Compaction 后 watch API 的历史 event 会丢失。如果你的应用依赖 etcd watch 来追历史事件如 Kubernetes 的 Informer 机制compaction 后 Informer 的 cache 需要重新 List 全量数据。这也是为什么 Kubernetes 默认要求 compaction retention 至少 1 小时——给 Informer 足够的时间窗口来追数据。Defrag 的阻塞风险Defrag 不是无锁操作。在 defrag 期间该节点会短暂阻塞写入。对于 8GB 的 BoltDB 文件defrag 可能需要 5-30 秒。在这期间Raft 日志会积压。因此必须逐节点 defrag每次间隔足够长让日志追回来。如果集群只有 3 个节点但不巧已经有一个 down 了不要执行 defrag——此时只剩 2 个节点再阻塞一个会导致集群不可用。适用边界任何运行超过 3 个月的 K8s 集群都需要定期执行 compaction 和 defrag。集群规模越大Pod 数 1000、Node 数 50etcd 性能对 compaction 的敏感度越高。禁用场景无。有 etcd 就要做 compaction。五、总结etcd 性能退化不是 etcd 的 bug是没有做 compaction 和 defrag 的必然结果。Compaction 每小时执行一次保留 1 小时历史版本解决 MVCC 堆积导致的扫描性能下降。Defrag 在碎片率 2x 时逐节点执行解决 BoltDB 文件膨胀和磁盘 IO 增加。开启慢查询日志定位对 etcd 不友好的查询模式如不带 limit 的 List 操作。这三件事是 etcd 运维的底线。