用 Consul 做配置和分布式锁:KV 的 CAS 怎么防止丢更新?阻塞查询会不会漏事件?session 的 TTL、lock-delay、行为各是什么?持有者所在节点挂了,别人多久能拿到锁?这篇用 3 个 server 加 2 个 client agent 逐项量出来。
操作 | 结果 -------------------------------+------------- 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 会合并掉中间值。
操作 | 结果
-----------------+------------------------------------
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 去向不同。
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 则持有者死了锁也不放。
持有者 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。