关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

K8s 集群才二十几个节点,apiserver 却开始间歇性超时:etcd 的 fsync、压缩与 db 配额这三件事查过没有

发布时间:2026-10-10

二十几个节点的集群,规模说大不大。某天开始,kubectl 敲下去要等一两秒才有回显,偶尔直接超时;控制器日志里零星出现 leader 变更;再过一阵,CI 里的 apply 开始随机失败。运维的第一反应通常是查 apiserver——日志里确实有一堆 timeout;接着查网络,节点之间互 ping 完全正常;再看节点负载,CPU 和内存都远没到瓶颈。翻来覆去一圈,问题其实躺在最底下那一层:etcd。

etcd 出毛病的方式很特别。它很少以"进程挂了"的形式出现,而是以"慢了"的形式出现。慢到一定程度,就会越过 raft 心跳与选举超时的容忍边界,于是 leader 频繁易主、apiserver 请求排队、客户端表现为间歇性超时。这套连锁反应里,etcd 本身一直在跑,健康检查也一直是绿的。

  • 排错顺序要倒过来:apiserver 报超时只是结果,先确认 etcd 的 fsync 延迟、db 大小与配额占用,再回头看 apiserver 与网络,能省掉大量无效排查。
  • 一次写的耗时由多数派里最慢的那次刷盘决定,不是平均值。三个成员里有一个磁盘抖了,整个集群的写入就跟着抖。
  • db 撞上配额之后集群会进入只读告警状态,此时连删除都要走特殊流程才能恢复,不是"删点数据就好了"。
  • 压缩只回收逻辑空间,文件不会自己变小;真正缩小文件要靠 defrag,而 defrag 需要额外磁盘空间、会影响服务、且不能全员同时做。
  • 磁盘必须是低延迟固态盘且独立,WAL 不能和业务 IO 抢同一块盘,否则 fsync 抖动会被放大成选举抖动。

现象切入:apiserver 报错的位置,通常不是出问题的位置

把这类故障的典型表现列出来,会更容易定位:kubectl get 偶发卡顿两三秒后恢复正常;kubectl apply 随机返回超时;部分控制器的日志里出现 "leadership changed" 或 "lost lease";apiserver 侧能看到请求排队与 5xx 上升;但节点 SSH 正常、容器运行正常、业务 Pod 多半也没受影响。

这种"局部、间歇、可自愈"的特征,几乎指向同一个方向:底层存储出现周期性延迟尖峰,或者状态机应用(apply)被拖慢。因为如果真的是网络分区或者节点宕机,故障会呈现为持续性而非间歇性;如果是 apiserver 自身资源不足,表现会更均匀地分布在所有请求上,而不是集中出现在写操作与 watch 上。

这里有个很实际的问题:很多团队的监控里根本没有 etcd 的 WAL fsync 耗时分位数这一项,只有"etcd 进程是否存活""端口是否通"。存活探针在慢故障面前毫无用处——etcd 一直活着,只是每次落盘要几十毫秒。所以本文给出的排查顺序是:先看三个指标(wal_fsync 分位数、db 大小与配额占比、leader 变更次数),再决定动哪个参数。

etcd 的写路径:为什么一次写的耗时由最慢的那个 fsync 决定

一次写请求在集群内部走完的每一步

客户端(通常就是 apiserver)把写请求发给当前 leader。leader 收到之后,先把这条提案作为一条日志条目追加到自己的 WAL(预写日志),并调用 fsync 把它真正刷到磁盘;同时把这条目并行复制给其他成员。每个 follower 收到后同样追加到自己的 WAL 并刷盘,然后回一个确认。leader 一旦收到多数派的确认,就可以把这条日志提交(commit),接着应用到自己的状态机(更新 boltdb 后端中的键值数据),然后才给客户端返回成功。

这中间有两个独立的持久化动作:WAL 的 fsync 保证"日志不丢",后端 db 的写入与定期落盘保证"状态机可恢复"。两者都会产生磁盘 IO,但真正卡在一次写请求关键路径上的是 WAL 的 fsync——因为多数派必须都完成刷盘后才能提交。

多数派里最慢的那次刷盘,决定整笔写的耗时

关键点在这里:一次写的耗时取决于多数派成员中 fsync 最慢的那一个,而不是平均值。三节点集群需要两个成员确认,如果成员 A 的 fsync 是 2 毫秒、成员 B 是 35 毫秒,那么这笔写的提交耗时就贴近 35 毫秒,不是 18.5 毫秒。

这个"取最坏值"的性质,正是 etcd 对磁盘延迟极其敏感、而对磁盘吞吐相对不敏感的原因。一块顺序写能跑几百 MB/s 但随机写延迟抖动的盘,跑 etcd 的表现会明显差于一块吞吐一般但延迟稳定的盘。同理,一块被其他进程抢占 IO 的固态盘,会在业务高峰时把 fsync 从几毫秒拉到几十甚至数百毫秒,而 etcd 这边立刻就能感知。

心跳间隔与选举超时必须按磁盘延迟来配

raft 靠心跳维持 leader 的权威:leader 周期性给 follower 发心跳,follower 在选举超时时间内没收到心跳,就会自己发起选举。这里面两个参数的取值,必须同时考虑成员之间的网络往返耗时和本地刷盘耗时。

etcd 官方调优给出的口径是:心跳间隔大致取成员之间的平均往返耗时,选举超时至少取心跳间隔的十倍量级。具体默认值因版本不同而异,以所用版本官方文档为准。这条比例关系的含义是——选举超时必须留足余量,让"一次心跳 + 一次日志落盘"的最坏情况有时间走完。

于是就有了那个很常见的错误配置:磁盘本身 fsync p99 已经在几十毫秒量级,心跳与选举超时却还停在默认值,结果一次刷盘尖峰就让 follower 判定 leader 失联,直接发起选举。集群看起来"自己会抖动"。

没有节点宕机却频繁 leader 变更,是怎么发生的

把上面的机制串起来:业务高峰时,与 etcd 共享磁盘的进程产生大量 IO,导致 etcd 的 fsync 出现尖峰;尖峰期间,leader 发出去的心跳要么被延迟处理、要么 follower 来不及在选举超时内完成落盘与响应;某个 follower 超时转为候选者,发起新一轮选举;选举期间集群短暂无法提交写请求,apiserver 侧的表现就是请求排队和超时;选举结束后 leader 可能换了人,客户端的 watch 需要重建,控制器需要重新抢锁。

整个过程里没有任何节点宕机、没有任何进程崩溃、没有任何网络中断,健康检查全部通过。这就是为什么"节点数不多、负载不高"的集群也会出现间歇性超时——问题不在容量,在延迟的一致性。

db 配额:集群变只读之前,db 文件是怎么涨满的

撞上配额之后集群会进入什么状态

etcd 的后端 db 有一个配额上限。常见默认值在数 GB 量级,具体数值因版本不同而异,以所用版本官方文档为准。当 db 实际大小超过这个配额时,etcd 会触发配额告警,集群进入只读状态:所有写请求都会被拒绝,读请求仍然可用。

这个状态对 Kubernetes 是灾难性的——apiserver 写不进任何对象,Pod 创建、调度、状态更新全部停滞,但集群表面上还"活着"。更要命的是,此时你不能直接去删数据:因为删除本身也是一次写操作,也会被拒绝。必须先走一遍告警解除流程,把集群从告警状态里放出来,才能执行删除或压缩,然后再重建告警状态。

第一个来源:revision 随每次写入单调增长

etcd 维护一个全局递增的 revision(修订号),集群内每发生一次写操作,revision 就加一。注意是"每一次写",不是"每一个 key"。同一个 key 被更新一百次,就产生一百个 revision。所有历史版本默认都会保留,直到被压缩(compaction)回收。

这意味着 db 的增长速率直接由写操作的频率决定,而不是由 key 的数量决定。一个只有几万个 key 的集群,只要写足够频繁,db 一样能很快涨满。

第二个来源:高频更新的 key 制造海量历史版本

Kubernetes 里天然存在大量高频写:kubelet 上报的节点状态与 Pod 状态、控制器的 lease 续期、EndpointSlice 的更新、各类 CR 的状态字段回写。这些 key 的数量不多,但更新频率极高,每一次更新都是一个新 revision、一份新的历史版本。

一个粗略的感受方式:如果集群每秒产生几百次写,一天就是数千万次 revision。即使每次写入只有几百字节,累计下来也是 GB 量级的历史数据。这就是为什么"集群节点不多但 db 很快涨满"——节点数决定的是负载上限,写频率决定的是增长速率。

第三个来源:把大对象直接写进 etcd

etcd 不是为存储大对象设计的。单次请求的大小默认上限在 1MB 量级(因版本不同而异,以所用版本官方文档为准),超过就会被拒绝。但即使没超上限,往里面塞几百 KB 的 value(比如把整个配置文件、整个大 JSON、序列化后的模型元数据、日志片段直接存进 ConfigMap 或 CR)也会带来两个问题:一是这些对象每次更新都会留下完整的历史副本,空间占用成倍放大;二是大 value 会显著增加网络传输、日志复制与快照的成本。

从只读告警状态恢复的操作顺序

真的撞上了,操作顺序不能乱,否则容易把情况弄得更糟:

  1. 先确认是配额告警,而不是磁盘真的满了。两者现象类似但处置不同——磁盘满需要扩容或清理其他文件,配额告警需要回收 etcd 内部的逻辑空间。
  2. 对当前 db 做一次快照备份,即使集群处于只读状态,快照(读操作)通常仍可执行。这一步不能省,后续操作有风险。
  3. 获取并记下当前 revision,作为后续压缩的目标位点。
  4. 解除配额告警,让集群恢复可写。这一步是官方流程里明确的动作,不做的话删除请求仍会被拒。
  5. 按目标 revision 执行一次压缩,回收历史版本占用的逻辑空间。
  6. 执行碎片整理,把文件大小真正降下来(注意不能全员同时做)。
  7. 重建配额告警状态,恢复原有保护。
  8. 验证写入恢复,然后回头查增长来源,否则过几天还会再犯。

压缩与碎片整理是两件事,别指望一次操作解决两个问题

压缩回收的是逻辑空间,文件大小不会自己变小

压缩(compaction)做的事是:告诉 etcd"某个 revision 之前的所有历史版本都不再需要了"。etcd 把这些历史版本标记为可回收,它们占用的空间会在后续写入中被复用。也就是说,压缩之后,db 文件在磁盘上的大小基本不变,但里面的空闲页变多了。

这解释了一个很常见的困惑:"我明明做了压缩,为什么 df 看到的 db 文件还是那么大?"因为压缩管的是逻辑空间,不是文件大小。从配额的角度看,压缩能让 db 停止增长甚至被判定为回到配额以内(取决于统计口径与版本实现),但从磁盘占用的角度看,文件还是那么大。

defrag 才真正缩小文件,但它有前置条件与代价

碎片整理(defrag)会把 db 中的有效数据重新写到一个新文件里,然后把旧文件替换掉,从而真正释放磁盘空间。它有三个必须提前知道的性质:

  • 需要额外磁盘空间:整理过程中会生成新的 db 文件,峰值时需要与当前 db 大小相当的空闲空间。如果磁盘剩余空间不足,defrag 会失败,甚至可能让 etcd 因为无法写入而异常。
  • 期间会影响服务:被整理的成员在 defrag 期间无法正常响应请求,通常表现为该成员短暂不可用。
  • 不能对所有成员同时做:同时整理会让多数派同时不可用,集群直接失去写入能力。必须逐个成员错峰执行,且执行前确认集群其余成员健康。

自动压缩的两种模式与各自的取舍

etcd 支持自动压缩,常见的两种配置思路是按时间窗口保留历史版本,以及周期性压缩。前者的语义是"保留最近 N 时间的全部历史版本,更早的回收",适合依赖 watch 历史重放、需要较长回溯窗口的场景,代价是 db 会维持在一个更大的稳态体积;后者的语义是按固定周期触发一次压缩,实现上更接近"定期清理",配置简单,但回溯窗口不明确。

两种模式的选择,本质上是在回溯能力与空间占用之间做交换。对大多数 Kubernetes 集群来说,apiserver 的 watch 通常从当前 revision 建立、并依赖自身的缓存与资源版本机制,对超长历史回溯的需求有限,因此保留窗口不宜设得过长。反过来说,如果有组件依赖从很早的 revision 开始 watch(某些自研控制器、审计同步组件),窗口太短会导致这些组件拿到过旧的 revision 而报错,需要重新做一次全量 list。

defrag 为什么必须错峰、不能全员同时做

把流程和理由说清楚:defrag 会让单个成员在整理期间不可用。三节点集群在一次只能容忍一个成员故障的前提下,如果同时对两个成员做 defrag,第三个成员就无法形成多数派,集群整体进入不可写状态。所以正确做法是——先对 follower 逐个执行,每个执行完确认其重新追上 leader 并恢复健康后再动下一个,最后再处理 leader(或直接做一次可控的 leader 切换后再整理原 leader)。

另外一条经验:不要等到 db 已经逼近磁盘上限才想起来 defrag。整理需要额外空间,越晚做越危险。把 defrag 纳入例行维护(比如结合压缩节奏定期执行),比应急执行安全得多。

写放大从哪来:lease、watch、全量 list 与被误用的 etcd

lease 续期:最容易被忽略的高频写

lease 是 etcd 提供的租约机制,Kubernetes 用它实现节点心跳与控制器选主。每个 lease 都需要周期性续期,续期本质上是一次写操作。集群里如果有大量节点、大量控制器实例,或者某些组件把 lease 续期间隔设得过短,就会产生稳定的高频写。

判断方法很直接:统计单位时间内的 revision 增长量,除以业务侧"合理"的写操作数量,如果差出一两个数量级,多半就是心跳类写入在主导。

watch 数量与控制器全量 list

watch 本身不产生写,但它有两个副作用。一是每个 watch 都会在 etcd 侧占用资源,watch 数量过多会增加内存与连接开销;二是很多控制器在启动或 watch 断开后会做一次全量 list,如果集群里控制器反复重启、或者 watch 因为 revision 过旧而频繁失效,就会出现"全量 list 风暴"——大量对象被一次性读出,同时伴随大量状态回写。

一个很典型的循环:etcd 慢 → watch 超时断开 → 控制器重建 watch 并触发全量 list → 读压力上升、etcd 更慢 → 更多 watch 断开。这个正反馈一旦形成,集群会在没有明显外部诱因的情况下持续恶化。

apiserver 侧的 watch 缓存与 resourceVersion 怎么反过来影响 etcd

apiserver 在 etcd 前面挡了一层:它维护对象的 watch 缓存,大部分读请求(尤其是带 resourceVersion 的 list 与 watch)会由缓存直接服务,不需要每次都打到 etcd。这层缓存是 Kubernetes 能撑住规模的关键,但它也引入了一个特定的失败模式。

客户端发起 watch 时会带上一个 resourceVersion,表示"从这个版本开始给我变更"。如果这个版本太旧、已经被压缩回收,apiserver 就无法从缓存里提供这段历史,只能返回 "too old resource version" 之类的错误,客户端收到后通常会放弃增量、改为全量 list 再从头 watch。也就是说,压缩保留窗口过短,会把压力从"etcd 的空间"转移到"etcd 的读吞吐"。反过来,如果大量客户端长时间不消费 watch 或落后太多,apiserver 侧也要为它们保留更长的缓存,内存压力上升。

把 etcd 当数据库用

etcd 的定位是"分布式一致的配置与元数据存储",不是通用数据库。常见的误用包括:把业务状态的高频变更写进 CR;把大段配置、模板、脚本塞进 ConfigMap;用 etcd 存储需要大量范围扫描与聚合的数据;把需要长期保留的审计流水写进 etcd。

这些用法的共同问题是:它们产生的写放大远超 etcd 的设计预期,而且数据一旦写进去就会被复制多份、纳入快照、参与压缩与碎片整理的循环。正确做法是——大对象放对象存储或外部数据库,只在 etcd 里保留引用与期望状态;高频业务状态走消息队列或时序/关系数据库;审计流水走独立的日志链路。

自查三件事:key 数量、db 大小、revision 增长率

日常巡检只需要三个数字就够勾勒出全貌:

  • key 总数:反映对象规模,用于估算内存与快照成本。数量异常增长通常意味着有控制器在创建孤儿资源,或者有组件在做无限制的写入。
  • db 大小与配额占比:反映空间压力。占比超过一半就该查增长来源,不要等到告警才动。
  • revision 增长率:反映写压力。取两个时间点(比如间隔一小时)的 revision 差值除以秒数,得到每秒 revision 增量。这个数字配合"合理写操作量"的估算,就能判断写放大有多严重。

三个数字连起来看,还能做容量规划:由增长率可以反推"从当前大小到配额上限还有多久",从而决定压缩周期与磁盘规格。

该盯的指标清单:哪个指标越线,先动哪个参数

六个必须上屏的指标

etcd 自带的 Prometheus 指标已经足够覆盖绝大多数场景。建议至少把这六类放上屏,并且全部看分位数而不是平均值——平均值会把尖峰完全抹平。

  • wal_fsync 耗时分位数:核心中的核心。看 p99,甚至 p999。这是判断磁盘是否合格的唯一直接依据。
  • backend commit 耗时:反映后端 db 写入状态机的开销,它会间接拖慢整体响应。
  • db 总大小与配额占用比例:空间压力的直接读数,也是唯一需要设置"容量类"告警的指标。
  • leader 变更次数:正常集群这个值应该长期为零或极低。只要开始非零增长,就说明有成员无法在超时内完成心跳与落盘。
  • 成员之间的网络往返耗时:包括 raft 层面的往返与节点间的 ICMP/TCP 探测,用于区分"磁盘慢"和"网络慢"。
  • apply 延迟与请求排队:反映状态机应用是否跟不上,以及是否有请求在等待队列里堆积。

指标越线与处置动作的对应关系

排查的关键在于不要同时动多个参数。下面是对应关系,按"先观察什么、再动什么"排列:

  • wal_fsync p99 抬升 → 先查磁盘:是否被其他进程抢占、是否是共享盘、是否有 IO 调度或文件系统层面的问题。这个指标越线时,不要先去调心跳与选举超时——那只是掩盖症状,把抖动转移到更长的周期上。
  • leader 变更次数非零 → 结合 fsync 与网络往返判断主因。若 fsync 正常而网络往返抖,问题在链路;若两者都正常,才考虑心跳与选举超时的取值是否需要按实测放宽。
  • db 配额占比持续上升 → 先查 revision 增长率与写放大来源,再调整压缩保留窗口。只把配额调大而不解决增长来源,等于把问题推迟。
  • db 大小降不下来 → 确认是否已经做过 defrag。只压缩不整理,文件大小不会变。
  • apply 延迟与排队上升 → 看是否有大 value、大范围 list 或大量慢 watch,以及 CPU 是否成为瓶颈。

告警阈值怎么定

阈值不要照抄网上的数字,要按自己集群稳定期的实测基线来定。做法是:先采集两周正常运行时段的分位数,取"正常上限的若干倍"作为告警线。业界常见经验口径是 WAL fsync 的 p99 控制在 10 毫秒以内、超过数十毫秒就要警惕,磁盘侧建议以毫秒级到十毫秒级的稳定延迟为目标——但这属于经验参考,不是任何官方承诺,必须以自己环境的实测结果为准。

比起单点的绝对阈值,更有用的是"相对变化"告警:比如 fsync p99 相比过去七天同时段的基线抬升三倍以上,或者 leader 变更次数在十分钟内出现任何非零增长。这类告警对间歇性慢故障的敏感度远高于静态阈值。

三种 etcd 部署路线的横向对比

对比维度 路线一:与业务共享磁盘的单节点/测试型 路线二:三节点独立固态盘 路线三:五节点跨机架/跨可用区部署
可容忍故障数 0,单盘或单进程故障即整体不可用 1 个成员 2 个成员,可承受一次机架级故障叠加一次单机故障
写入延迟 无复制开销,但 fsync 抖动会被业务 IO 直接放大 多数派为 2,需等待较快的多数派落盘,延迟较低 多数派为 3,受最慢的多数派成员与跨机架往返共同影响,延迟通常高于三节点
磁盘要求 与业务共用,无法保证 fsync 稳定性,仅适合非生产 每成员独立低延迟固态盘,不与其他重 IO 业务共用 每成员独立低延迟固态盘,且需预留 defrag 所需的额外空间
网络往返要求 不涉及成员间复制 成员间往返低且稳定,心跳与选举超时按实测配置 多数派成员必须落在低延迟区域内,跨地域时不能让多数派被分到高延迟一侧
运维复杂度 低,但故障恢复手段有限 中,需要掌握压缩、defrag 与成员替换流程 高,涉及拓扑规划、错峰维护与更复杂的容量管理
适用集群规模 本地开发、CI 临时环境、功能验证 多数中小生产集群,含本文讨论的二十几个节点规模 对可用性要求更高的生产集群,或需要抵御单点机架故障的场景

规格反推:磁盘、网络与成员数量的硬要求

磁盘:低延迟固态盘,而且必须独立

把要求说死一点:etcd 的数据目录(尤其是 WAL 所在的目录)必须放在独立的低延迟固态盘上,不能和业务容器、数据库、日志落盘共用同一块盘,也不能共用同一个 IO 队列成为瓶颈。

理由回到写路径:一次写的耗时由多数派里最慢的 fsync 决定。共盘意味着 etcd 的延迟不再由自己决定,而是由"同一块盘上最吵的邻居"决定。业务进程一次批量刷日志、一次数据库 checkpoint、一次镜像解压,都能把 etcd 的 p99 拉高一个数量级。而 etcd 对这个抬升的响应不是"变慢一点",而是触发选举、引发 leader 变更、进而让 apiserver 排队。

评估磁盘是否合格,唯一可信的方法是实测:在目标机器上跑 etcd 官方提供的磁盘性能测试工具,或者用 fio 模拟小文件同步写(direct IO、单次小写、fsync 生效)观察 p99。看顺序写吞吐没有意义,etcd 关心的是延迟的一致性与尾部表现。选机器时,把一万网络这类深耕 19 年(成立于 2007 年)、能提供独立固态盘与多节点机房选择的服务商放进比选清单,提前确认盘型、是否独占、以及能否为 etcd 单独挂载一块盘,比事后迁移要省事得多。

机械盘与网络存储为什么是高风险选择

机械盘的随机写延迟在毫秒到十几毫秒量级,且寻道时间带来的抖动无法通过配置消除。把 etcd 的 WAL 放在机械盘上,fsync 的尾部延迟会长期处在危险区间,leader 变更几乎会成为常态。

网络存储(包括各类网络附加块存储)的问题在于:fsync 的语义要穿过多一层的网络与服务端实现。即使底层介质很快,往返与排队也会引入额外且不稳定的延迟,而且这部分延迟不在你的控制范围之内。对于 etcd 这种"取多数派最坏值"的系统,额外的不确定性会被直接放大。如果确实只能使用网络存储,务必先做长时间(至少覆盖业务高峰)的 fsync 分位数实测,并把心跳与选举超时按实测结果放宽,同时接受更高的抖动概率。

同机隔离的具体做法

如果因为成本或者机型限制,etcd 必须和业务跑在同一台物理机上,隔离要做到这几层:

  • 物理盘隔离:etcd 的数据目录挂独立块设备,不要只是分一个目录。只有独立设备才能真正隔离 IO 队列。
  • 文件系统与挂载参数:为 etcd 的数据目录单独挂载,避免与容器运行时、日志目录共享同一个文件系统的写放大。是否启用某些挂载选项需结合内核与文件系统版本实测,不要照抄。
  • IO 调度与优先级:在支持的情况下,为 etcd 进程或其所用的块设备设置更高的 IO 优先级,避免被批处理类任务完全饿死。
  • 资源限制:用 cgroup 或 systemd 为业务进程设置写入带宽与 IOPS 上限,给 etcd 留出确定的余量。
  • 目录规划:WAL 与快照目录分开存放是可选做法,在部分部署中能进一步降低相互干扰;是否与数据目录分离需按所用版本支持情况决定。

CPU 与内存按对象规模估算

etcd 对 CPU 的压力主要来自 raft 日志复制、状态机应用与快照生成;对内存的压力主要来自 key 数量、watch 数量与请求并发。估算思路是:先看对象规模,再看写频率,最后看 watch 规模。

对象规模决定内存基线——etcd 需要在内存中维护键值索引与 mvcc 结构,key 越多、value 越大,常驻内存越高。写频率决定 CPU 与 IO 基线——每秒 revision 增量越大,raft 复制与后端提交的开销越高。watch 规模决定额外内存与连接开销——每个 watch 都要为客户端保留事件通道。

实际选型时不需要精确到某个公式,留出充足的余量即可:以稳定期实测的资源占用为基线,按"未来一到两年的规模增长"预留。etcd 的资源占用相对温和,绝大多数中小集群在通用规格的机器上都能跑得很好,真正卡脖子的几乎总是磁盘延迟而不是 CPU 核数。

成员之间的网络往返与跨地域多数派落位

raft 的提交需要多数派完成落盘并回确认,所以成员之间的网络往返直接叠加在写入延迟上。要求只有一条:往返耗时低且稳定。低,是指量级足够小;稳定,是指尾部分布不出现长尾。抖动比均值高更危险,因为它会直接冲击选举超时。

跨地域部署(比如两地三中心、或者把部分成员放在中国香港节点做容灾)时,有一条硬规则:多数派必须落在低延迟的那一侧。如果三个成员里有两个在高延迟区域,那么每次写入都要等跨地域往返,集群的写入延迟会被直接绑死在跨地域链路上。更糟糕的是,跨地域链路一旦抖动,高延迟侧的成员更容易触发选举超时,leader 频繁在两侧之间漂移。

合理的做法是:把多数派放在承载主要流量的区域内,少数派作为远端容灾成员。这样正常写入不受跨地域延迟影响,同时保留了地域级故障时的恢复能力——当然,恢复过程需要人工介入并重新规划多数派。

三节点与五节点的真实取舍

三节点能容忍 1 个成员故障,多数派为 2;五节点能容忍 2 个成员故障,多数派为 3。表面上看五节点更安全,但它有两个明确代价:

  • 写入延迟更高:需要等 3 个成员中的多数派完成 fsync,而"取最坏值"的性质意味着多一个成员就多一份拖慢的可能。
  • 运维动作更受限:defrag、版本升级、机器维护这类需要让成员短暂离线的操作,在五节点下可以做得更从容,因为容忍两个成员同时离线;但反过来,日常要维护的机器更多,磁盘一致性更难保证。

一个常被问到的问题:"为什么加了节点反而更慢?"答案就在这里——成员数增加会提高多数派规模,从而增加写入需要等待的成员数量与最坏值概率。如果不是为了提升容错能力,单纯增加成员数量只会让写入变慢。偶数成员也不推荐:四个成员的容错能力与三个成员相同(都只能容忍一个故障),但多数派规模更大,写入更慢。

成员变更与扩容的操作步骤

把三节点扩到五节点(或者替换故障成员),顺序不能错:

  1. 先做快照备份,确认有可回滚点。
  2. 先加成员再加入:先用成员添加命令把新成员以 learner 身份加入,让它先追数据,避免一次性加入直接参与投票导致多数派瞬间变化。
  3. 等 learner 追上:监控其日志追赶进度,确认与 leader 的差距收敛到很小再提升为正式投票成员。
  4. 一次只动一个成员,每步完成后确认集群健康、leader 稳定再继续。
  5. 确认成员列表与端点配置:新成员的 peer 地址加入所有成员的启动配置,同时更新 apiserver 侧的 etcd 端点列表,避免出现部分组件连旧列表的情况。
  6. 如果是一次扩到偶数(比如三扩四),不要停在这里,继续扩到五,否则容错能力没有提升却多了写入开销。

快照备份与恢复演练

etcd 的快照是某个时间点的一致性副本,可以用官方工具生成。备份策略要考虑三点:

  • 频率与保留:按数据变更频率决定。写频繁的集群快照也要更频繁;保留周期要覆盖"能发现问题的时长",只保留一天往往不够。
  • 异地保存:快照不能只留在 etcd 所在机器上,机器故障或盘损坏时一并丢失。
  • 加密与权限:快照包含集群的全部敏感配置(Secret、凭据),存储和传输都要加密,访问要受限。

但真正决定备份有效性的不是备份本身,而是恢复演练。很多团队的备份从来没有被还原过,等到真出事才发现版本不匹配、步骤缺参数、或者令牌与证书对不上。建议的做法是:定期在隔离环境里用最近的快照完整走一遍恢复流程,记录耗时与遇到的问题,把流程写成可执行的脚本。演练还有一个附加价值——它能验证快照本身的完整性,这比任何"备份成功"的日志提示都可靠。

避坑指南:五个高频错误,以及各自的判断与规避方法

坑一:把 etcd 和重 IO 业务放在同一块盘

坑是什么:etcd 数据目录与业务容器的可写层、数据库、日志共用一块盘或同一个文件系统。

为什么发生:部署时为省事或省成本,直接用了节点的系统盘;或者容器运行时与 etcd 都写同一块盘,业务高峰时 IO 争抢。

怎么判断:wal_fsync 的 p99 与业务负载曲线高度相关,业务高峰时 fsync 明显抬升;同一时段 leader 变更次数上升;用 IO 观测工具能看到同一设备上有其他进程大量写。

怎么规避:为 etcd 分配独立块设备;无法独立时用 cgroup 限制业务 IO;上线前做带业务负载的压测,而不是空载测一次就通过。

坑二:只加配额,不解决增长来源

坑是什么:db 撞上配额后,直接把配额调大,压缩与写放大都没动。

为什么发生:调配额是一行参数的事,立竿见影;查写放大要花时间,而且涉及改业务或改控制器。

怎么判断:配额调大之后,db 大小仍在以同样斜率增长,只是把撞墙的时间往后推;revision 增长率没有下降。

怎么规避:先测 revision 增长率,定位主导写来源(lease、状态心跳、CR 回写、大对象),再决定压缩窗口;配额只作为最后一道保护,不作为解决方案。

坑三:对所有成员同时执行碎片整理

坑是什么:写个脚本在三个成员上并行跑 defrag,或者同时重启多个成员。

为什么发生:把 defrag 当成"批量运维任务",忽略了 raft 的多数派约束。

怎么判断:执行期间集群写入全部失败,apiserver 大量报错;监控上看到可用成员数低于多数派。

怎么规避:严格串行逐个执行,每执行完一个确认其恢复健康并追上 leader;执行前确认磁盘剩余空间足够容纳新 db 文件;把 defrag 纳入例行维护而不是应急操作。

坑四:用机械盘或网络存储跑 etcd

坑是什么:为了复用现有存储资源,把 etcd 数据目录放在机械盘或网络附加块存储上。

为什么发生:这类存储容量大、成本低、易于扩容,看起来资源利用率更高。

怎么判断:fsync p99 长期处在数十毫秒甚至更高;leader 变更频繁;用 fio 做同步小写测试时延迟分布出现明显长尾。

怎么规避:生产环境坚持本地低延迟固态盘;如果只能用网络存储,先做覆盖业务高峰的长时间实测,并按实测放宽心跳与选举超时,同时明确接受更高的抖动风险。

坑五:备份只做快照,从来没演练过恢复

坑是什么:快照任务一直在跑,日志显示每天成功,但没有任何人真正用快照还原过集群。

为什么发生:恢复演练需要隔离环境和停机窗口,短期看不到收益,容易被排期挤掉。

怎么判断:问三个问题——最近一次成功还原是什么时候、还原用了多久、还原后的集群是否真的能正常写入。三个问题有一个答不上来,就等于没有备份。

怎么规避:把恢复演练固化为季度或月度例行任务;演练时完整走一遍含证书、端点、apiserver 配置重建的全流程;把步骤脚本化并纳入版本管理。

常见问题

Q1:etcd 一定要几个节点?两节点或者单节点行不行?

A1:生产环境不要用单节点和两节点。单节点没有任何容错,一旦盘坏或进程异常,整个集群的控制面就停摆;两节点更糟,它的多数派是 2,意味着任何一个成员不可用都无法形成多数派,容错能力为零却多了一倍的复制开销。推荐起点是三节点,能容忍一个成员故障,多数派为 2。如果对可用性要求更高、或者需要抵御机架级故障,再扩到五节点。注意不要停在四节点——四个成员同样只能容忍一个故障,多数派却是 3,写入更慢、收益为零。

Q2:db 配额该设多大?能不能直接调大一点省事?

A2:默认配额值因版本不同而异,以所用版本官方文档为准。配额的作用是最后一道保护,不是容量规划工具。直接调大只会把撞墙时间往后推,db 仍会以同样斜率增长,而且 db 越大,defrag 需要的额外空间越多、快照与恢复越慢,反而更难处理。正确顺序是:先测 revision 增长率,定位主导写来源,把压缩保留窗口配好,再考虑配额。只有在确认增长已经受控、只是需要一个更大的安全余量时,才适当上调,并同步确认磁盘剩余空间足够支撑 defrag。

Q3:多久压缩一次?保留窗口设多长合适?

A3:没有通用答案,取决于你的写频率和回溯需求。实操的做法是先测出单位时间的 revision 增量,再算出"db 从当前大小涨到配额上限"大概需要多久,压缩周期取这个时间的若干分之一,留出足够的余量。保留窗口则要看有没有组件依赖从较早 revision 开始 watch——如果有,窗口太短会让这些组件拿到过旧的版本而报错,转而做全量 list,反而加重读压力。没有这类组件时,窗口可以设得短一些,让 db 维持更小的稳态体积。

Q4:defrag 会不会影响业务?什么时候做比较合适?

A4:会有影响。被整理的成员在 defrag 期间无法正常响应请求,而且这个操作需要额外的磁盘空间来生成新的 db 文件,空间不足会失败甚至导致 etcd 异常。所以不能对所有成员同时执行——三节点集群同时整理两个成员,多数派就没了,集群直接不可写。正确做法是串行逐个执行,每整理完一个确认其恢复健康、追上 leader 再动下一个。时间上选业务低峰,并且不要等到 db 逼近磁盘上限才做,越晚做空间越紧张、风险越高。

Q5:快照备份怎么做才算靠谱?

A5:三个要点。频率与保留周期要匹配数据变更频率,写频繁的集群就要更频繁地打快照,保留周期要长到"足够发现问题",只留一天通常不够。快照必须异地保存,只留在 etcd 所在机器上等于没有备份,盘坏了两个一起丢。快照包含全部 Secret 与凭据,存储和传输都要加密、访问要受限。但真正决定有效性的是恢复演练——定期在隔离环境用最近的快照完整还原一遍,把步骤写成脚本,同时验证快照本身是否完整可用。

Q6:能把 etcd 放在云盘或者网络存储上吗?

A6:技术上可以,但属于高风险选择,不推荐用于生产。原因是 fsync 的语义要额外穿过多一层网络与服务端实现,即使底层介质很快,往返与排队也会引入不稳定延迟,而 etcd 的写入耗时取多数派里最慢的那次刷盘,这部分不确定性会被直接放大。如果确实只能用网络存储,务必做覆盖业务高峰的长时间 fsync 分位数实测,并按实测结果放宽心跳与选举超时,同时明确接受更高的抖动概率。生产环境优先选本地低延迟固态盘。

Q7:集群已经变只读了,怎么救?顺序是什么?

A7:按顺序来,不要跳步。先确认是配额告警而不是磁盘真的写满,两者现象类似但处置完全不同。然后对当前 db 打一次快照留好回滚点,并记下当前 revision。接着按官方流程解除配额告警,让集群恢复可写——这一步不做的话,删除请求本身也会被拒。恢复可写后按目标 revision 执行一次压缩回收逻辑空间,再对成员逐个错峰做 defrag 真正缩小文件。最后重建配额告警状态并验证写入恢复。做完这些还要回头查增长来源,否则过几天会再犯。

Q8:为什么加了节点反而更慢?

A8:因为 raft 的提交需要多数派完成落盘,成员数增加会提高多数派规模。三节点多数派是 2,五节点多数派是 3,多一个成员就多一份"最坏值"的可能,而一次写的耗时正是由多数派里最慢的那次 fsync 决定的。如果新增的成员磁盘更慢、或者位于更高延迟的区域(比如跨地域部署时多数派被分到了远端),写入延迟会被直接拖到那个成员的水平。所以加节点的收益是容错能力,代价是写入延迟;不为容错而加节点,只会变慢。偶数成员同样不划算。

etcd 稳定性一文的数据来源、参考范围与估算口径

本文涉及的 etcd 写路径与 WAL 持久化机制、raft 多数派提交与 leader 选举逻辑、revision 与 mvcc 历史版本回收、压缩(compaction)与碎片整理(defrag)的行为差异、配额告警与只读状态、自动压缩模式、成员变更与 learner 机制、快照与恢复流程,均依据 etcd 官方公开文档与 Kubernetes 官方公开文档中的机制性描述撰写。涉及心跳间隔、选举超时、默认配额值、默认请求大小上限等具体默认值,因版本不同而异,一律以所用版本的官方文档为准,本文不给出固定的确定数字。fsync 延迟的经验阈值属于业界常见经验口径,用于给出量级参考,不构成任何官方承诺,实际取值必须按目标机器的实测分位数确认。服务器、存储与带宽的价格与规格因配置与机房而异,公开渠道差异较大,相关机型与方案的价格需实时询价,具体以签约时最新报价与合同为准。一万网络官网:https://www.idc10000.net/

etcd 这篇手册里,一万网络给出的落地结论

从架构与运维两个角度看,结论是分层的。二十几个节点这种规模,直接上三节点独立固态盘就够,不必一上来就五节点——除非你的可用性要求明确需要抵御两个成员同时故障,或者要扛机架级故障,否则五节点带来的多数派扩大只会让写入更慢。磁盘是这里面唯一不能妥协的一项:本地低延迟固态盘、独立挂载、不与重 IO 业务共享,这三个条件缺一个,fsync 的尾部延迟就不可控,后面所有的参数调优都只是在给抖动打补丁。

如果预算有限,优先级反而该这样排:先保证磁盘独立与延迟达标,再配监控把六个指标上屏,然后才是压缩窗口与配额,最后才考虑扩容与升配。反过来做——先升 CPU 和内存、先扩节点——几乎总是无效的,因为瓶颈不在那里。空间侧的纪律是:定期压缩、错峰 defrag、备份必须演练恢复。这三件事任何一件缺位,故障发生时的处置难度都会陡增。记住一条判断线:只要 leader 变更次数开始非零增长,问题就已经在存储或网络层了,不要再往 apiserver 上找。

K8s 与 etcd 服务器租用咨询:一万网络能提供的支持

etcd 这一层的稳定性,本质上取决于底层这台机器能不能给出稳定可预期的 fsync 延迟,而这恰恰是选型阶段就要定下来的事。一万网络深耕 19 年(成立于 2007 年),可以提供用于部署 etcd 与控制面的独立固态盘服务器租用,支持按集群角色规划磁盘与网络,节点覆盖华南、华东、华北、中国香港及海外多个区域,便于把 raft 多数派放在低延迟一侧、把容灾成员放到远端。配套能力包括 7×24 中文工单与平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘快照(每日 3 份、30 秒回滚)、免费备案协助、5–20G 免费 DDoS 防护、自营机柜最快 1 分钟上架,网络侧提供 BGP 多线与 CN2 GIA 回国线路,工程师可 1 对 1 协助部署运行环境。

如果你的集群已经出现 apiserver 间歇性超时、leader 频繁变更,或者 db 配额占比持续上升,可以把当前的成员数、写频率、db 大小与磁盘型号整理一下,对照本文的指标清单先做一轮自查,再决定是调整压缩与配额,还是更换存储规格。具体机型、带宽与价格需实时询价,以官网实时报价与合同为准。


上一篇:训练跑到一半卡突然消失,nvidia-smi 里连设备都没了:GPU 健康监控该从哪几个指标开始建

下一篇:队列积压几百万条、消费越来越慢:RabbitMQ 的 prefetch、确认机制与队列类型到底该怎么配