bigywater.tech
微服务 · 43

etcd 事务 Txn 与 Leader 选举:比较并交换实现锁,concurrency.Election

用 etcd 做分布式锁和 Leader 选举,CAS 锁和排队锁差多少?持有者崩溃或被暂停会怎样?fencing token 怎么救场?选举的故障转移时间由什么决定?这篇用 etcd 3.7.2 的 Txn 和 lease 逐项量出来。

1798 对 3807
20 个线程抢 300 次锁时,排队锁与单 key CAS 锁发给 etcd 的请求数
A(1218)被拒绝
锁持有者被暂停 3 秒后醒来继续写:没有 fencing 时写入成功,共享资源检查 fencing token 后被拒绝
1.92 到 2.20 s 对 0.01 s
TTL 2 s 的选举里,leader 崩溃与主动 resign 时新 leader 出现用的时间

CAS 锁与排队锁

实测 · 本机执行输出
  锁                        | 同一时刻最多持有者 | 300 次总用时 | 发给 etcd 的请求数 | 等待 p50 | 等待 p99 | 相邻两次获取锁按 create_revision 递增的比例(FIFO 的话应为 100%)
  -------------------------+-----------+----------+--------------+--------+--------+-----------------------------------------------
  (a) 单 key CAS 锁 + 随机退避重试 | 1         | 4.4 s    | 3807         | 157 ms | 936 ms | (不适用)                                         
  (b) 排队锁 + 监听前一个 key      | 1         | 3.5 s    | 1798         | 210 ms | 263 ms | 100%                                          
  两种锁都做到了互斥(最多 1 个持有者)。CAS 锁在竞争激烈时,大量请求浪费在失败的重试上;排队锁每个竞争者只监听前一个,请求数少、没有惊群,并且按 create_revision 先到先得,最坏等待更可预期

排队锁每个竞争者只监听前一个,请求数少、先到先得;CAS 锁请求多、等待波动大。

崩溃与暂停

实测 · 本机执行输出
  情形                           | 结果                                                    
  -----------------------------+-------------------------------------------------------
  (a) 持有者崩溃,等待者拿到锁用了           | 2.51 s / 2.54 s / 2.42 s                              
  (b) 暂停的持有者恢复后继续写(没有 fencing) | A(token 1214):写入 → B(token 1216):写入 → A(token 1214):写入
  (c) 同上,共享资源检查 fencing token  | A(token 1218):写入 → B(token 1220):写入 → A(token 1218):拒绝
  锁靠 lease 保活,持有者崩溃之后要等 lease 过期(约 TTL 再加一点)别人才能拿到;但 lease 过期而持有者自己不知道时(进程被暂停、时钟跳变),它醒来之后仍然以为持有锁。锁服务本身无法阻止这件事,要靠共享资源一侧检查 fencing token(拿锁时 etcd 返回的单调递增 revision)拒绝旧持有者的写入

崩溃靠 lease 过期释放锁;暂停后醒来的旧持有者要靠 fencing token 在资源一侧拒绝。

选举的故障转移

实测 · 本机执行输出
  lease TTL | 失效方式       | 新 leader 出现用了多久(3 次)     | 前任 → 新 leader        
  ----------+------------+--------------------------+----------------------
  2 s       | (a) 崩溃     | 1.92 s / 2.13 s / 2.20 s | c0→c1 / c0→c1 / c0→c1
  2 s       | (b) resign | 0.01 s / 0.01 s / 0.01 s | c0→c1 / c0→c1 / c0→c1
  5 s       | (a) 崩溃     | 5.09 s / 5.16 s / 5.21 s | c0→c1 / c0→c1 / c0→c1
  5 s       | (b) resign | 0.00 s / 0.00 s / 0.01 s | c0→c1 / c0→c1 / c0→c1
  崩溃的故障转移时间与 TTL 相当(看上表:TTL 2 s 约 2 s,TTL 5 s 约 5 s,里面包括 etcd 检查 lease 到期的延迟);主动 resign 几乎是立刻的。TTL 越短故障转移越快,但网络抖动时 leader 被误判失效(丢掉领导权)的概率越高——和第 40 篇里心跳与 TTL 的关系是同一个取舍。新 leader 总是 create_revision 次小的候选者,所以顺序是确定的(先来的先当)

崩溃时约等于 TTL,主动 resign 几乎立刻。

分步演示

etcd 事务与选举
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。etcd 3.7.2(Homebrew,单节点自己起的进程,每次运行前清空数据目录),通过 HTTP/JSON 网关操作 Txn / lease / watch;锁和选举是我按 etcd concurrency 包的做法手写的 Python 实现,不是 clientv3 的 concurrency 包本身;fencing token 用的是拿锁时的 create_revision(第一节 CAS 锁用的是 Txn 的 revision);崩溃与暂停是让续约线程停止来模拟的,没有真的 kill 进程;每个场景的次数见各节
    • 没有验证:clientv3 concurrency.Session / Mutex / Election 的真实实现和它们的 keepalive 重试;etcd 集群 leader 切换期间锁的行为;lease TTL 更长时的故障转移;大量竞争者(几百个)时排队锁的开销;锁的可重入;读写锁
    京ICP备2026067502号-1京公网安备11010502063487号