bigywater.tech
微服务 · 53

Consul KV 与 session 锁

用 Consul 做配置和分布式锁:KV 的 CAS 怎么防止丢更新?阻塞查询会不会漏事件?session 的 TTL、lock-delay、行为各是什么?持有者所在节点挂了,别人多久能拿到锁?这篇用 3 个 server 加 2 个 client agent 逐项量出来。

66 → 400
8 个线程各加 50 次的计数器,无保护的 GET+PUT 最终值,和加上 ?cas=ModifyIndex 之后的最终值
20.0 s
TTL 10 s 的 session 不续约,到失效的用时(TTL 的两倍)
20.3 s
持有锁的节点被 SIGKILL 后,默认 lock-delay 下另一个客户端拿到锁的用时(lock-delay 1s 时 6.9 s)

KV 与 CAS

实测 · 本机执行输出
  操作                             | 结果          
  -------------------------------+-------------
  PUT cas=0(key 不存在)             | 成功          
  PUT cas=0(key 已存在)             | 失败(返回 false)
  PUT cas=1024(不是当前 ModifyIndex) | 失败(返回 false)
  PUT cas=24(当前 ModifyIndex)     | 成功          
  CAS 成功之后 ModifyIndex 从 24 变到 27,CreateIndex 保持 24(= 24),值 'v4'
  写法                         | 最终值(应得 400) | 一共读-写了多少次 | 用时 s
  ---------------------------+-------------+-----------+-----
  GET 后 PUT(没有保护)            | 66          | 400       | 0.71
  GET 后 PUT ?cas=ModifyIndex | 400         | 2359      | 4.88
  没有 CAS 时读-改-写互相覆盖,丢更新;CAS 保证了最终值正确,代价是冲突的线程要重试(读-写次数比 400 多的部分就是白做的)

ModifyIndex 是全局 Raft 索引;?cas 让读-改-写不丢更新,冲突的线程要重试。

阻塞查询

实测 · 本机执行输出
  无变化:wait=2s,5 次实际返回用时 2.03, 2.09, 2.06, 2.01, 2.09 s;返回的 X-Consul-Index 与请求的相同
  写入发生在 1 s 之后(经另一个 agent 写):3 次返回用时 1.01, 1.02, 1.02 s,索引都变大且写入成功:True
  顺序写,每 100 ms 一次,共 20 次:watcher 看到 20 次变化,最后看到的值 'r100-19'(最后写入的是 r100-19)
  顺序写,每 2 ms 一次,共 200 次:watcher 看到 200 次变化,最后看到的值 'r2-199'(最后写入的是 r2-199)
  8 个线程并发写,每个 50 次,共 400 次:watcher 看到 203 次变化(写入 400 次),最后看到的值是 key 里最终的值:True
  一个事务(/v1/txn)里对同一个 key 连续 set 10 次:HTTP 200,watcher 看到 1 次变化,值 ['txn-9']
  阻塞查询返回的是“返回那一刻的最新值和索引”,不是变化的流水:能不能看到每个中间值,取决于写入和 watcher 重新发起查询的相对速度(etcd 的 watch 是事件流,见第 42 篇)

返回的是当前值和索引,不是事件流:并发写得快时 watcher 会合并掉中间值。

session 锁的规则

实测 · 本机执行输出
  操作               | 结果                                 
  -----------------+------------------------------------
  A acquire        | 成功                                 
  B acquire(A 持有时) | 失败(返回 false)                       
  GET lock         | Session 是 A: True,LockIndex 1,值 'A'
  B release(不是持有者) | 失败(返回 false)                       
  A release        | 成功                                 
  GET lock         | Session None,LockIndex 1           
  B acquire(释放之后)  | 成功                                 
  C acquire        | 成功                                 
  GET lock         | LockIndex 3                        
  LockIndex 当围栏令牌用的演示(资源自己记住见过的最大令牌,我写的几行,不是 Consul 的功能):[('A', 1, True), ('B', 2, True), ('A(已经被换掉)', 1, False)]
  session 被销毁后     | key 的去向                              
  -----------------+--------------------------------------
  Behavior=delete  | GET 返回 HTTP 404(key 被删除)             
  Behavior=release | key 还在,Session=None,LockIndex 1,值 'x'

acquire 持有期间别人 false;LockIndex 随每次被拿到加 1,可以当围栏令牌;销毁 session 时行为 delete 与 release 的 key 去向不同。

TTL

实测 · 本机执行输出
  session              | 结果                  | 锁             
  ---------------------+---------------------+---------------
  (a)TTL 10 s,不续约      | 20.0 s 后 session 消失 | 锁 Session=None
  (b)TTL 10 s,每 3 s 续约 | 45 s 内一直在           | 锁仍被持有:True    
  (c)没有 TTL,持有者死了      | 45 s 内一直在           | 锁仍被持有:True    
  不设 TTL 的 session 只和节点的存活挂钩:持有者进程死了而 agent 还活着,锁就一直占着;所以要用锁的话应该设 TTL 并让持有者续约

TTL 10 s 不续约要 20.0 s 才失效;不设 TTL 则持有者死了锁也不放。

lock-delay

实测 · 本机执行输出
  持有者 session 的 lock-delay | 销毁后 B 第一次拿到锁的时间
  -------------------------+----------------
  0s                       | 0.01 s         
  3s                       | 3.03 s         
  不写(默认)                   | 15.04 s        
  lock-delay 是给“前任持有者可能还没发现自己失效”留的缓冲:没有它,新持有者和前任可能同时认为自己在持锁。它保护的是前任,代价是切换变慢

session 非自愿失效后,默认 15 s 内别人拿不到锁。

节点故障切换

实测 · 本机执行输出
  A 的 lock-delay | 杀掉节点到 A 的锁被释放 | 杀掉节点到 B 拿到锁
  ---------------+---------------+------------
  不写(默认)         | 5.4 s         | 20.3 s     
  1s             | 5.9 s         | 6.9 s      
  节点被杀之后要先等 gossip 判它 failed(serfHealth 变 critical,第 52 篇量过),session 才被作废、锁才释放,然后还要等 lock-delay;总时间 = 这两段相加
  (附注)经 client agent 的请求遇到 HTTP 500 “rpc error making call: EOF” 并重试的次数:0(重试后都成功了才继续;没有成功的会让实验直接中止)
ERRORS {}

总时间 = gossip 发现节点没了 + lock-delay。

分步演示

Consul KV 与 session 锁
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。Consul 是 Homebrew 的 2.0.4,3 个 server + 2 个 client agent(127.0.0.1);客户端是我写的几行 Python(HTTP API),不是 consul lock 命令也不是任何 SDK;围栏令牌的演示资源是我写的几行代码;计数器、lock-delay 各测 1 轮,TTL 与故障切换各 1 次(没有重复取分布);开发中的几次运行里经 client agent 偶尔遇到 HTTP 500 “rpc error making call: EOF”(同一时间服务端日志有 “server cannot decode request: i/o deadline reached”),我的客户端遇到就重试,最后这次运行没有出现,原因我没有深究
    • 没有验证:consul lock 命令与各语言 SDK 的封装(含它们自己的续约和 lock-delay 处理);Consul 文档里对 TTL 失效时间、lock-delay 设计意图的说法(这里只有测到的 20.0 s 和 15.04 s);session 在服务端 leader 切换时的行为;更大规模的 KV(大值、大量 key);ACL;事务 /v1/txn 除了合并事件之外的语义;阻塞查询在 stale 模式与跨数据中心时的行为
    京ICP备2026067502号-1京公网安备11010502063487号