集群状态为什么该交给 etcd:用 Raft 从根上消除脑裂

Posted on 一 12 10月 2026 in Tech

Abstract 集群状态为什么该交给 etcd:用 Raft 从根上消除脑裂
Authors Walter Fan
Category Tech
Version v1.0
Updated 2026-10-12
License CC-BY-NC-ND 4.0

大纲

展开看看
  • 问题:用 Redis/MySQL 存"谁是主、锁归谁、配置版本几",网络一分区就两边各自称主,脑裂、双写、数据打架
  • 根因:Redis 异步复制 + 哨兵、MySQL 主从,都没把"分区的少数派必须退位"写进协议——它们是为"存数据"设计的,不是为"下结论"设计的
  • 解法:etcd 底层 Raft,用多数派(quorum)做决策。拿不到多数派票就当不上 leader、提交不了写,少数派那半边自动哑火,脑裂在协议层被堵死
  • 一个例子:把全集群切进维护模式(cluster_maintenance_state=1),分区时本地内存 / Redis / etcd 三种做法各自什么下场
  • 拓扑:节点数取奇数(3/5),quorum = ⌊N/2⌋+1;为什么不是越多越好;跨机房摆法与见证节点
  • 方法:选主(campaign/lease)、分布式锁(concurrency)、配置一致下发(watch + revision)、服务注册(lease + keepalive)
  • 最佳实践:etcd 只放协调元数据、给 lease 设合理 TTL、认 revision 别认时间、写要带 compare、监控 quorum 健康、定期 defrag
  • 坑:脑裂没了但"假主"还在(旧 leader 不自知)、fencing token、lease 续约抖动、单 key 热点、把业务数据往里塞、跨机房 RTT 拖垮写延迟
  • 边界:etcd 不扛高吞吐、不存大 value;它是裁判不是球员

上一篇讲异地双活的数据层选型时,etcd 只占了一段话——"协调层:etcd 不是业务库,是裁判"。发出去之后被追问最多的是同一个意思的两种问法:

"我已经有 Redis / MySQL 了,存个'谁是主'而已,为什么非得再引一个 etcd?"

因为存"谁是主"这件事,和存业务数据,是两类完全不同的问题。前者要的是在网络分区时也只有一个答案,后者要的是吞吐和容量。Redis 和 MySQL 是为后者设计的,拿它们来干前者,网络一抖就会出现两个"主"同时认为自己是主——这就是脑裂(split-brain)。

这篇想把一件事讲透:集群状态(选主、锁、配置、注册)该交给 etcd,而不是 Redis 或 MySQL。不是因为 etcd 更快更新,恰恰相反,是因为它慢得有原则——它把"分区时谁该闭嘴"写进了协议,而不是留给运维去祈祷。

  • 面向做过分布式后端 / SRE 的同行,Raft、quorum、lease、revision 这些词直接用。
  • 下面的命令和参数是示意,讲清机制用,不是生产模板。

一、问题:你存"谁是主"的地方,决定了分区时有几个主

先把场景摆具体。一个常见的集群,有若干工作节点,但某些活全局只能有一个节点干——比如定时任务调度、写入某个外部系统的唯一出口、或者一把全局锁。于是你需要一个"谁是主"的记录。很多团队顺手就用手头已有的库:

用 Redis + 哨兵(Sentinel)选主的写法,大概是抢一个 key:

SET leader "node-A" NX PX 10000

谁抢到谁当主,拿 PX 做租期,定时续。看着挺干净。

用 MySQL 选主,通常是往一张单行表里 UPDATE ... WHERE 抢,或者靠一个唯一键。

这两种写法在网络好的时候都能跑。问题出在网络分区那一刻。

假设机房内部交换机抖了一下,或者跨机房专线断了几秒,集群被切成了两半:A 这半边联系不上 B 这半边。接下来会发生什么?

  • Redis 主挂在 B 那半边,哨兵在 A 那半边凑够票,把 A 侧的从提升为新主。于是 A 侧有一个主在服务,B 侧原来那个主还没死、也没人通知它退位,它还在接受写。——两个 Redis 主。
  • 上层抢锁的逻辑分别连到两个 Redis 主,各自都 SET NX 成功,于是 A 侧有个节点认为自己是 leader,B 侧也有个节点认为自己是 leader。——两个业务主。

这就是脑裂:同一个"全局唯一"的角色,在分区两侧各存在一份,而且都认为自己是正版。 两个主同时写外部系统、同时跑调度、同时持锁,轻则数据打架,重则把下游写乱、把钱算错。等分区恢复,两份互相矛盾的状态还得人工去和解。

关键在于:脑裂不是你 SET NX 写错了,是你选的那个"记录谁是主的地方"本身不解决分区。 换个更大的库、加个更复杂的哨兵配置,都绕不过这一点。

网络分区下,Redis/MySQL 选主出现两个主(脑裂),etcd 的少数派自动退位


二、根因:复制模型里,没人规定"少数派必须闭嘴"

为什么 Redis 和 MySQL 会这样?不是实现有 bug,是它们的复制模型从设计上就没管这件事。

Redis 主从是异步复制。 主收到写就立刻返回成功,再慢慢把数据推给从。这意味着主已经返回"OK"的那笔写,从可能还没收到。一旦主挂、从被提升,那笔已经告诉客户端"成功了"的写就丢了。哨兵负责的是"发现主挂了、选个从顶上",它关心的是可用性,不是"保证全局只有一个主"。分区时两边哨兵各看各的,各提各的主,协议层没有任何机制强制某一边退位。

MySQL 主从同理。 默认异步,半同步(semi-sync)能把 RPO 压小一点(主至少等一个从确认),但半同步也不保证分区时少数派主动下线——它退化回异步继续服务是常见配置。主从切换靠外部工具(MHA、orchestrator 之类)判断,判断逻辑在库之外,分区时一样会误判成"两个主"。

把这两类库的本质说白了:

Redis 和 MySQL 是"存数据"的库,它们的第一目标是把你的写收下、尽量别丢、尽量快。它们不是"下结论"的库——"现在全局谁是主"这种需要在分区时也唯一的结论,不在它们的职责里。

而"下结论"恰恰是一个共识(consensus)问题。共识问题有个被研究了几十年的标准答案:多数派(quorum)。核心就一句话——

任何决策,必须拿到超过半数节点的同意才算数。因为两个互不相交的多数派不可能同时存在,所以分区时最多只有一侧能凑齐多数派,另一侧自动失去决策权。

这句话是整篇文章的题眼。它不是优化,是数学:3 个节点里要 2 个点头,5 个里要 3 个点头;网络把集群切成 2+1,那个只剩 1 个节点的少数派,无论如何也凑不齐 2 票,于是它当不了主、也提交不了写——不是因为它"懂事",是因为它在协议上根本做不到。

etcd 就是把这套多数派共识(具体是 Raft 算法)做成了一个现成的、你拿来即用的协调存储。


三、解法:etcd 用 Raft 把"少数派退位"写进协议

etcd 底层跑 Raft。你不用自己实现 Raft,但得理解它凭什么能堵死脑裂。三个要点:

1. 选主要多数派投票。 Raft 里一个节点想当 leader,得向其他节点拉票,拿到多数派的票才能上任。分区后少数派那侧拉不到多数票,当不了 leader;更关键的是它会发现自己联系不上多数节点,于是主动降级成 follower,停止一切写。多数派那侧选出唯一 leader,继续服务。全程最多一个 leader。

2. 写入要多数派确认。 leader 收到一笔写(Raft 里叫 log entry),得复制给多数派并确认后,才算 committed、才返回成功。这跟 Redis 的"主收到就返回"正相反——etcd 是宁可慢、宁可在分区时拒绝写,也不返回一个可能站不住的成功。这是典型的 CP 取舍:分区时少数派那侧宁可不可用,也不给你一个假的"成功"。

3. 已提交的写不会丢、不会被覆盖成另一个版本。 因为每笔提交都过了多数派,新 leader 上任时必然包含所有已提交的日志(Raft 的选举限制保证了这点)。不会出现 Redis 那种"主返回成功的写,切主后没了"。

这三条合起来,结论是:

用 etcd 存集群状态,网络分区时你最多失去少数派那侧的服务(它会拒绝读写而不是乱答),但你永远不会得到两个主、不会得到两份打架的状态。脑裂从"运维要靠监控和祈祷去防"变成了"协议层根本不允许发生"。

代价也要说清楚:etcd 慢,吞吐低,容量小。 每笔写都要跨多数派节点走一轮网络往返,还要落盘。所以它不是用来存业务数据的——它是用来存那些量很小、但必须绝对唯一的元信息:谁是主、锁在谁手上、配置是第几版、哪些服务还活着。裁判一场球只需要吹几次哨,不需要像球员那样满场跑。


四、一个具体例子:把全集群切进维护模式

抽象讲完,落到一个你多半干过的活。集群里有一批业务节点,某天要做滚动维护,希望它们同时进入维护态——停止接新请求、把 cluster_maintenance_state 从 0 置成 1。维护完再统一置回 0。

这个状态有两个硬要求:全集群看到的值必须一致(不能一半节点在维护、一半还在接客),而且切换这个动作本身得有唯一的裁决(谁说了算、以哪一版为准)。我们用三种常见做法,各自过一遍网络分区。

场景固定:6 个业务节点被分区切成 A 侧 4 个、B 侧 2 个。运维在 A 侧下达"进入维护"。

做法一:只用节点本地内存

每个节点内存里存一个 cluster_maintenance_state,靠广播/gossip 互相同步。

分区一来,运维的"置 1"广播只到得了 A 侧 4 个节点,B 侧 2 个节点压根没收到,还停留在 0。于是:

  • A 侧 4 个节点:维护态,停服
  • B 侧 2 个节点:正常态,继续接客

这不是脑裂,是更朴素的"状态根本没对齐"。没有任何单一事实源,每个节点的内存就是它自己的真相。分区恢复后,B 侧要么一直没进维护(维护做了个寂寞),要么等某次重新广播才慢半拍跟上——而这期间你以为全停了,其实一直有流量打在 B 侧的两台上。本地内存连"大家看到同一个值"这个最低要求都保证不了。

做法二:节点内存 + Redis 当共享状态

进一步,把 cluster_maintenance_state 放 Redis,各节点定时读 Redis 刷新本地缓存。看着有了"单一事实源"。但 Redis 本身要高可用,于是上主从 + 哨兵——脑裂的病根又回来了。

分区把 Redis 也切成两半:A 侧哨兵把 A 侧的从提升为新主,B 侧原主还活着、没退位。现在有两个 Redis 主:

# A 侧(运维在这边操作,连到 A 侧新主)
SET cluster_maintenance_state 1   # A 侧 Redis 主:1

# B 侧(原主还在服务,没人改它)
GET cluster_maintenance_state     # B 侧 Redis 主:仍是 0

结果和做法一殊途同归,但更有欺骗性:

  • A 侧 4 个节点读 A 侧主 → 1 → 进维护
  • B 侧 2 个节点读 B 侧主 → 0 → 继续接客

你明明用了"共享存储",以为全集群会一致,但共享存储自己脑裂成了两份。更糟的是分区恢复:两个 Redis 主的数据要合并,cluster_maintenance_state 到底听谁的?异步复制按谁后写谁赢,很可能 B 侧那个陈旧的 0 反而把 A 侧的 1 覆盖掉——维护信号凭空消失。Redis 异步复制 + 哨兵,给了你"单一事实源"的错觉,没给你它的保证。

做法三:节点内存 + etcd 当共享状态

把状态放 etcd:运维用一次带条件的事务把它从 0 改成 1,各节点 watch 这个 key,收到变更就刷新本地缓存。

# 运维侧:基于"当前是 0"这个前提,原子地改成 1(带 fencing 版本)
etcdctl txn <<'EOF'
value("cluster_maintenance_state") = "0"

put cluster_maintenance_state "1"

EOF

这里的 etcd 自己也是个集群,它的节点也被同一场分区切开,假设 quorum 落在 A 侧:

  • A 侧(含 etcd 多数派):这笔 txn 跨多数派确认成功,cluster_maintenance_state=1 落定,revision +1。A 侧 4 个业务节点通过 watch 收到变更,进维护。
  • B 侧(etcd 少数派):B 侧的 etcd 节点凑不齐多数派,拒绝读写。B 侧 2 个业务节点此刻去读 etcd 会失败——它们读不到一个过期的值,而是读不到值。

差别就在这最后一句。做法一和二里,B 侧读到的是一个看起来正常、实则过期的 0,于是心安理得地继续接客;做法三里,B 侧读到的是错误,这逼着它面对一个诚实的选择:既然连集群状态都确认不了,我到底还敢不敢服务?

这正是 CP 的价值——宁可告诉你"我现在不知道",也不给你一个可能错的"0"。 你可以据此设计 B 侧的兜底策略:读不到 etcd 就自动进入保守态(比如也停止接新请求),或者干脆自我隔离。无论选哪条,前提是 etcd 没有骗你说"一切正常"。

分区恢复后更是干净利落:B 侧 etcd 节点重新接上多数派,直接同步到最新 revision,cluster_maintenance_state=1,没有任何"两份值打架、谁覆盖谁"的合并噩梦——因为从头到尾,全局只有一个被多数派承认的值。

三种做法对比

维度 本地内存 内存 + Redis 内存 + etcd
分区时 B 侧看到的值 过期的 0(没收到广播) 过期的 0(读到另一个脑裂主) 读不到(少数派拒绝服务)
B 侧的行为 以为正常,继续接客 以为正常,继续接客 知道异常,可兜底保守
本质问题 没有单一事实源 事实源自己脑裂成两份 唯一事实源,少数派诚实失效
分区恢复 慢慢广播补齐,可能漏 两份值合并,可能被旧值覆盖 同步到最新 revision,无冲突
结论 ✗ 状态不对齐 ✗ 脑裂假象更危险 ✓ 要么一致,要么明确不可用

一句话收这个例子:

本地内存和 Redis 在分区时都会让 B 侧"错误地相信自己是对的";etcd 的价值不是让 B 侧也看到 1,而是让 B 侧诚实地知道"我现在确认不了"——把一个静默的错误,换成一个你能处理的明确失败。


五、拓扑:节点取奇数,quorum = ⌊N/2⌋+1

etcd 集群怎么摆,就一个核心参数:节点数 N,以及由它决定的 quorum(多数派大小)。

节点数 N quorum(要几票) 能容忍几台挂 说明
1 1 0 无容错,仅开发用
3 2 1 生产最常见,性价比最高
5 3 2 对可用性要求更高时用
4 3 1 反例:成本比 3 高、容错和 3 一样,纯亏
7 4 3 很少需要;写延迟明显上升

两条规律记住:

节点数一定取奇数。 偶数毫无好处:4 节点的 quorum 是 3,容错能力和 3 节点一样(都只能挂 1 台),却多养一台机器、还把写延迟拖高了(每笔写要等更多节点)。加一台偶数节点,只增加成本不增加容错。

不是越多越好。 节点越多,quorum 越大,每笔写要等的确认越多,写延迟越高。etcd 的瓶颈是写延迟,不是读。3 节点是绝大多数场景的甜点,只有当"同时挂两台也要服务"是硬需求时才上 5 节点。

跨机房怎么摆,这是最容易踩坑的地方:

  • 别把 3 节点平摊成 1+1+1 再加一个脑补的"主机房"概念——跨机房 RTT 会让每笔写都变慢(写要等多数派,而多数派里必然包含远端节点)。
  • 2+1 摆法要小心:机房 A 放 2 个、机房 B 放 1 个,看着 A 是多数派。但如果 A 整个挂了,B 剩 1 个节点永远凑不齐 2 票,整个 etcd 不可用。这不是脑裂(不会出两个主),但是全局停摆。
  • 真要跨机房高可用,用三机房 1+1+1,任何一个机房挂了,剩两个机房还能凑 2 票。代价是写延迟 = 到第二近机房的 RTT。
  • 成本敏感时,可以在第三个机房放一个 etcd 节点只做投票不扛读写流量(配得很轻,充当见证/仲裁角色),用最小成本换"第三张票"。

一句话:etcd 的拓扑设计,本质是在"能容忍几台/几个机房挂"和"写延迟多高"之间做取舍,而取舍的单位永远是 quorum。

etcd 三节点与跨三机房拓扑,以及 quorum 如何决定容错


六、方法:选主、锁、配置下发、服务注册

etcd 的四个典型用法,底层都靠两个原语:lease(租约) 和 revision(全局单调递增的版本号)。先把这两个讲清楚,后面全好懂。

  • lease:一个带 TTL 的"活着"凭证。你创建一个 lease,定期 keepalive 续约;一旦不续(进程挂了、网断了),TTL 到期后 etcd 自动把挂在这个 lease 上的所有 key 删掉。这是"自动清理死节点"的基础。
  • revision:etcd 每次写都让全局 revision +1。它是一个全局唯一、单调递增的逻辑时钟。认 revision,不要认墙上时钟——分布式系统里时钟不可信,revision 可信。

选主(leader election)。 etcd 的 concurrency 库直接提供了 Election。语义是:多个候选人 Campaign(竞选),etcd 保证同一时刻只有一个赢家,赢家把自己的身份写进一个挂了 lease 的 key。赢家进程挂掉或网断,lease 到期,这个 key 消失,其他候选人自动接替。

# 概念示意:拿一个 lease,用它竞选
etcdctl lease grant 10          # 拿到 lease ID,TTL 10s
etcdctl elect my-service node-A # 竞选,赢了才返回,赢家身份绑在 lease 上

这里的关键是:赢家身份是由 etcd 的多数派共识决定的,不是某个客户端自己 SET NX 出来的。分区时少数派那侧根本竞选不成功。

分布式锁。 同理用 concurrency.Mutex,Lock 时在 etcd 里按 revision 排队,revision 最小的那个拿到锁,其余的 watch 前一个、排队等待。锁也绑在 lease 上,持锁进程挂了锁自动释放——不会出现 Redis 锁那种"持锁进程死了、锁靠 TTL 硬等过期、这期间谁都拿不到"再叠加"TTL 设短了还没干完就被别人抢走"的两难。

配置一致下发。 把配置写进一个 key,所有消费方 Watch 这个 key。etcd 保证每个 watcher 都会按 revision 顺序收到变更,不漏、不乱序。想做灰度或原子切换,可以用 etcd 的事务(Txn):compare revision == 预期 then put,保证"基于我看到的那一版去改,没人在中间插队"。

服务注册与发现。 每个实例启动时把自己的地址写进 /services/xxx/实例ID,绑一个 lease 并持续 keepalive。实例挂了,lease 到期,key 自动消失。消费方 watch /services/xxx/ 前缀,实时拿到活着的实例列表。不健康的节点会自己消失,不需要额外的健康检查去摘除。

etcd 选主 / 锁 / 配置下发 / 服务注册的工作机制(lease + revision + watch)


七、最佳实践

把上面的机制用对,有几条反复验证过的原则:

  • 只放协调元数据。 谁是主、锁、配置、服务列表、少量 feature flag——这些量小、必须一致。业务数据(用户表、消息、日志、会话内容)一律别进 etcd。一旦你发现往 etcd 里写的 value 超过几 KB、或者 QPS 上千,几乎可以肯定你用错地方了。

  • lease TTL 要配得当,而且要和故障检测窗口对齐。 TTL 太短,网络抖一下还没来得及续约就误判死亡,主频繁切换(抖动);太长,真死了也要等很久才被清理,故障恢复慢。常见做法是 TTL 设成 keepalive 间隔的 3 倍左右,给网络抖动留冗余。

  • 写要带 compare(乐观并发)。 用 Txn 的 compare ... then ... else ...,基于 revision 做条件写,而不是无脑覆盖。这是 etcd 替你做并发控制的正确姿势——"我看到的是第 N 版,如果还是第 N 版就改,否则告诉我有人先改了"。

  • 认 revision,不认时间。 所有"谁先谁后""是不是同一版"的判断都用 revision。墙上时钟会漂移,在分布式里是毒药。

  • 监控 quorum 健康,而不只是进程存活。 一个 3 节点集群挂了 1 个,还能服务,但你已经没有容错余量了——再挂一个就全停。所以监控要盯的是"还剩几个健康节点、离失去 quorum 还有几步",而不是简单的"进程在不在"。

  • 定期 defrag + 设置自动压缩(compaction)。 etcd 保留历史 revision,不压缩会越积越大,最终把磁盘配额撑爆、集群变只读。开 auto-compaction、定期 defrag 回收空间,是 etcd 运维的必修课,跑着跑着突然只读的事故大多栽在这。

  • 客户端要处理"leader 切换"的瞬断。 etcd leader 切换(几百毫秒级)期间写会短暂失败,客户端要有重试和幂等,别把一次正常的选举当成故障。


八、坑:脑裂没了,但别以为就高枕无忧

etcd 从协议层消灭了"两个 leader 同时被 etcd 承认"。但分布式系统的坑是分层的,etcd 保证了它那一层,不等于你整条链路就安全了。几个真能半夜把你叫起来的坑:

1. "假主"问题——旧 leader 不知道自己已经被废。 这是最隐蔽的。假设 node-A 是业务主,它和 etcd 断了连接(但它自己没挂,还在跑)。etcd 这边 lease 到期,把主的位置给了 node-B。现在 node-B 是新主。但 node-A 不知道自己已经不是主了,它可能还在往下游写!etcd 保证了"谁在 etcd 里登记为主"是唯一的,但管不住一个已经失联的旧进程继续干活。

解法是 fencing token(隔离令牌):每次选主,etcd 给新主发一个单调递增的 token(revision 天生就是),业务每次写下游都带上这个 token,下游只接受 token 更大的写、拒绝更小的。这样旧主即便还在写,它手里的 token 是旧的,会被下游拒掉。没有 fencing,etcd 的选主再可靠,也挡不住失联旧主把下游写脏。

2. lease 续约抖动导致频繁切主。 TTL 配太短,或者客户端的 keepalive 实现不robust(比如和业务逻辑挤在一个 goroutine 里,业务一卡续约就迟到),网络正常也会被误判成死亡,主来回切。切主本身有成本(下游要感知、缓存要失效),频繁切会放大成雪崩。keepalive 一定要走独立的、不被业务阻塞的路径。

3. 单 key 热点。 把一个被疯狂读写的 key(比如全局计数器)放进 etcd,每次写都走多数派共识,延迟和吞吐都扛不住。etcd 不是干这个的。高频计数去 Redis,etcd 只存"现在谁有权改这个计数器"。

4. 跨机房 RTT 拖垮写延迟。 前面拓扑那节说过:写要等多数派,跨机房部署意味着每笔写至少要等一次跨机房 RTT。如果你的协调操作很频繁(比如锁抢得很凶),跨机房 etcd 的延迟会直接变成业务延迟。要么把频繁的协调收敛到机房内,要么降低协调频率。

5. 把 etcd 当通用 KV 库塞业务数据。 这是最常见的误用。etcd 好用、API 友好、还带 watch,于是有人把它当 Redis 用。结果:value 过大、历史 revision 爆炸、defrag 跟不上、磁盘配额撑爆、集群变只读、连带"谁是主"也查不了——协调层挂了,整个集群跟着瘫。etcd 的 value 大小和总容量都有硬限制,这个限制就是在提醒你:它是裁判,不是仓库。

6. 别忘了 etcd 自己也要容灾。 它是整个集群的"真相源",它一挂,上面所有依赖它选主/拿锁/读配置的服务全瘫。所以 etcd 的备份(snapshot)、恢复演练、跨机房高可用,优先级要排在业务库前面。


总结:裁判要用对的规则,不是用现成的球员

把三件事收一下:

  1. 问题:用 Redis/MySQL 存"谁是主、锁归谁",网络分区时会出现两个主——脑裂。
  2. 根因:它们是存数据的库,异步复制 + 外部哨兵,协议层没有"少数派必须退位"这条规则。脑裂是结构性的,不是配置能根治的。
  3. 解法:etcd 底层 Raft,用多数派共识把"分区时少数派自动失效、全局最多一个主"写进了协议。它慢、容量小,但在"下结论"这件事上是对的。

一句话落地:

集群状态(选主、锁、配置、注册)交给 etcd;业务数据交给 Redis/MySQL/Cassandra/NewSQL。别让存数据的库去下结论,也别让下结论的库去扛数据。

但请记住最后那个反直觉的点:etcd 消灭的是"两个 leader 被同时承认",不是"一个失联旧主还在偷偷写下游"。 选主可靠只是第一步,fencing token 才让这份可靠一直延伸到下游。下次你给集群加选主时,先问自己一句:旧主失联但没死的那几秒,谁拦得住它? 答得上来,你才算真用对了 etcd。

关于这个话题的上下文,可以接着看再论 CAP:会议和实时通信为什么要选 AP(为什么 P 躲不掉、怎么在 C 和 A 之间选边),以及从容灾到异地双活:数据层怎么选(四类数据各配什么库,etcd 在其中扮演裁判)。

全文思维导图

@startmindmap
<style>
mindmapDiagram {
  node {
    BackgroundColor #F8F9FA
    RoundCorner 10
    Padding 10
    FontSize 13
  }
  :depth(0) {
    BackgroundColor #1E3A5F
    FontColor white
    FontSize 18
    FontStyle bold
  }
  :depth(1) {
    FontSize 15
    FontStyle bold
  }
  :depth(2) {
    FontSize 13
  }
}
</style>

* etcd 存集群状态
** 问题
*** Redis/MySQL 选主
*** 分区即两个主
*** 脑裂/双写/数据打架
** 根因
*** 异步复制
*** 外部哨兵判主
*** 没有"少数派退位"规则
** 解法:Raft
*** 选主要多数派投票
*** 写入要多数派确认
*** 已提交不丢不覆盖
** 拓扑
*** 节点取奇数 3/5
*** quorum=⌊N/2⌋+1
*** 三机房 1+1+1
*** 见证/仲裁节点
** 方法
*** 选主 Election
*** 锁 Mutex
*** 配置 Watch+Txn
*** 注册 lease+keepalive
** 最佳实践
*** 只放协调元数据
*** lease TTL 对齐检测窗口
*** 写带 compare
*** 认 revision 不认时间
*** 监控 quorum 余量
*** 定期 defrag/compaction
** 坑
*** 假主(旧 leader 不自知)
*** fencing token
*** 续约抖动频繁切主
*** 单 key 热点
*** 跨机房 RTT
*** 别塞业务数据
@endmindmap

集群状态为什么该交给 etcd - 思维导图


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。 欢迎在我的个人网站 https://www.fanyamin.com 访问原文并评论。