K8s etcd 性能调优:慢查询日志、压缩策略与碎片整理
一、你的 kube-apiserver 请求延迟直升 10 倍,根因在 etcd 没做过 compaction
K8s 集群跑了一年以后,运维团队发现一个规律:每隔 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 是一个 B+Tree 实现的嵌入式 KV 数据库。其写入流程始于提案到 Raft,日志复制到 Follower 后应用到 BoltDB。在 BoltDB 内部,B+Tree 节点更新会将新数据页写入文件尾,旧数据页标记为可回收,导致文件持续增长。当 compaction 触发时,系统清理超过保留期的旧 revision 并释放被标记的页,但文件物理大小不会缩小,从而产生碎片。随后 defrag 触发在线碎片整理,将文件缩小回实际数据大小。这一过程中,若未进行 compact,MVCC 扫描范围会变大导致 range query 变慢,同时存储文件增大也会增加磁盘 IO。
三个关键操作的理解:
Compaction:删除指定 revision 之前的所有历史版本数据。不会减小 BoltDB 文件大小——只是标记这些数据块为可回收。没有 compaction,任何 range 查询都需要扫描大量的历史 revision,O(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}"
ETCDCTL="etcdctl –endpoints=${ETCD_ENDPOINTS} \\
–cacert=${ETCD_CA} \\
–cert=${ETCD_CERT} \\
–key=${ETCD_KEY}"
COMPACTION_RETENTION_HOURS=1 # 保留 1 小时的历史版本
DEFRAG_FRAG_THRESHOLD=2.0 # 碎片率 > 2 时执行 defrag
HEALTH_CHECK_RETRIES=10
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-out=json | \\
python3 -c "import sys,json; data=json.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 "执行 compact,revision: ${COMPACT_REV}"
$ETCDCTL compact ${COMPACT_REV}
# 检查 compaction 是否完成
# 等待所有节点完成 compact
sleep 2
log "Compaction 完成"
}
# ===== 3. 检查碎片率并执行 Defrag =====
run_defrag() {
log "检查 etcd 碎片率…"
# 获取各节点的状态
STATUS=$($ETCDCTL endpoint status –write-out=json)
# 解析每个节点的碎片率
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_defrag=false
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_defrag=true
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-out=json)
DB_SIZE=$($ETCDCTL endpoint status –write-out=json | \\
python3 -c "
import sys, json
data = json.load(sys.stdin)
sizes = [int(d['Status']['dbSize']) for d in data]
print(f'DB总大小: {sum(sizes)/1024/1024:.0f}MB, 最大单节点: {max(sizes)/1024/1024:.0f}MB')
")
log "$DB_SIZE"
# 检查是否有 leader 频繁切换
LEADER_COUNT=$($ETCDCTL endpoint status –write-out=json | \\
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-mode=periodic \\\\
–auto-compaction-retention=1h
2. 慢查询日志(推荐开启):
etcd –experimental-slow-request-log \\\\
–experimental-slow-request-duration=500ms
3. 存储配额(防止磁盘爆满):
etcd –quota-backend-bytes=8589934592 # 8GB
4. 使用 SSD(etcd 对磁盘延迟敏感):
fio –rw=write –bs=4k –iodepth=1 –runtime=60 | 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-mode=periodic
– –auto-compaction-retention=1h
# === 慢查询日志 ===
# 超过 500ms 的请求记录日志
– –experimental-slow-request-log
– –experimental-slow-request-duration=500ms
# === 存储配额 ===
# 防止 BoltDB 文件无限增长
– –quota-backend-bytes=8589934592
# === 快照数限制 ===
# 保留最近 5 个快照,旧的自动清理
– –max-snapshots=5
– –max-wals=5
四、边界分析与架构权衡
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 数 > 50),etcd 性能对 compaction 的敏感度越高。
禁用场景:
无。有 etcd 就要做 compaction。
五、总结
etcd 性能退化不是 etcd 的 bug,是没有做 compaction 和 defrag 的必然结果。Compaction 每小时执行一次(保留 1 小时历史版本),解决 MVCC 堆积导致的扫描性能下降。Defrag 在碎片率 > 2x 时逐节点执行,解决 BoltDB 文件膨胀和磁盘 IO 增加。开启慢查询日志定位对 etcd 不友好的查询模式(如不带 limit 的 List 操作)。这三件事是 etcd 运维的底线。


