2个不稳定版本
0.2.0 | 2024年7月24日 |
---|---|
0.1.0 | 2024年7月24日 |
#70 在 并发
每月259次下载
23KB
283 行
putex
进程互斥锁
用于管理至少一次执行系统的锁定和时间组件。
NATS
目前仅使用NATS进行锁定,但有可能将其扩展到使用其他锁定。
NATS通过乐观锁定kv存储桶条目使用。如果它能在当前所有者通过续期或另一个代理成为所有者之前更改之前写入锁,它将成为所有者。代理遵循一组规则和共同执行计划,确保它们将始终选举单个领导者,并且永远不会意外重叠执行(只要操作者也遵循规则!)
变量
R
- 锁续期间隔
活跃代理多久运行一次健康检查并声明其活跃性。
F
- 故障阈值
在代理可以采取锁之前,必须经过多少个没有更新锁的R
。这是健康检查的最大超时时间,因为返回这个时间将导致预期故障转移。
C
- 确认计数
从新代理变为活跃到允许它封锁其他节点并激活其服务之间,必须经过多少个R
。
规则
操作员(您)
- 健康检查
- 健康检查将获得一个参数为
active
或standby
的值,表示锁是否由该代理持有。如果是active
,则应执行更彻底的检查,如果可能的话。如果是standby
,健康检查应指示我们是否相信在确认周期之后healthcheck active
将通过 - 在正常情况下,健康检查不应超过R,但在异常情况下,可能会超过R,但这些情况不需要故障转移,例如,某个地方的电源开关循环造成2秒的断电,您有R=1000和F=3(因此您预留了3秒,健康检查需要2秒来返回结果,因为数据包丢失--这大于R但小于R*F,所以会产生警告,但不会发生故障转移。
- 健康检查必须不超过 R
时间运行,否则它将失去与其他代理的锁定。如果在可能不会进行故障转移的情况下,健康检查可能需要超过 R 的时间,则应增加 R、F 或两者,其中 F 增加了整体的故障转移窗口,而 R 慢化了系统总体速度,以便健康检查能够跟上。
- 健康检查将获得一个参数为
- 激活
- 激活脚本首先应该隔离其他主机,如果可能并且对于用例来说是必要的。激活脚本将在至少 C*R 后被调用,因此这应该紧接着服务启动立即发生。
- 停用
- 停用必须在 C*R 内完成。如果停用需要比分配的时间更长,则可以通过增加 C 或 R 来实现,其中 R 影响系统的所有方面并普遍减慢速度。
代理操作理论
使用 NATS 集群中特定桶的特定密钥来锁定特定进程的运行者。
my-hostname:~$ natslock nats.cluster.local locks spof-service my-hostname
每个令牌应该是唯一的,通常是主机名。这些也会在 kv 存储中,并可以用来记录系统的当前状态。
实现应确保时间偏移不会影响同步。
R = 锁定更新间隔 F = 在获取锁定之前锁定更新的失败次数 C = 在接管后等待更新之前更新的次数 T = 锁定超时 = R * F 考虑 F=1 的例子以确保逻辑合理。如果 R = 1s 且 F=1,则更新必须在从第一次检查到我们想要获取锁定的时间的 1s 内发生。在这些参数下,1s 没有更新后,我们预计将立即获取锁定,所以 T=R*F。
客户端拥有锁,并且当其令牌存在于键的 C 个完整更新间隔中时或如果这是第一次修订或当前令牌为空时,应运行服务。
- 接管后完成此操作,这给以前系统的时间来关闭,如果需要的话。C 应该通常是 1,关闭/隔离机制应该是瞬时的(iptables/kill -9)。
客户端通过将令牌写入键来获取锁。
客户端可以在 T 秒内没有写入或在键不存在时写入键。
客户端只能在键不存在或自上次读取以来没有更新时(除协议的其他规则外)写入键。不存在:使用 create,如果键已被创建则失败。存在:使用带有修订的 update 来确保读取的修订也是被替换的修订。
这两个可以并行尝试,因为可能只有一个会成功。
任何写入键的失败都必须导致底层应用程序立即被杀死或以其他方式隔离(在 R 个间隔内)。
客户端在运行其健康检查后更新其锁。健康检查应该小于 R,但必须小于 T,否则客户端将失去其锁。由于它 必须 失去锁并 必须 认识到这种情况已经发生,健康检查将在 T 后始终超时,并且锁将立即放弃。
非初始化
此解决方案旨在向服务发出启动或停止的信号,而不是作为自己的初始化系统。因此,您应该执行类似于 systemctl start database.service
的操作,而不是 redis-server
。隔离也应通过 systemd 单元 DAG 处理。
泵
在显式无状态跳转执行器(除了tokio异步执行器)中运行,代码中的操作为“泵”。每次运行都在开始时建立其状态,可能循环有限次数,但必须偶尔返回外部循环,以便进行清理以及清除栈深度,以便当尾调用优化失败时(据报道Rust当前不可靠)。换句话说,泵操作也是一个具有单个入口和单个出口的有向无环图(DAG)。
图中的单次泵送从开始到结束。
每次泵送至少持续R。这是通过等待与泵送泵并行运行的计时器来实现的(即,它不是泵函数的一部分,而是泵的重要实现细节)。
状态机
flowchart TD
Start(((Start))) --> GetLock
GetLock[lk = Lock Value\nrev = Revision] --> Empty{"lk empty or mine?\n(or failed to retrieve\ntimeout = 1R)"}
Empty -- yes --> HCSunnyLoop
Empty -- no --> WaitKill
WaitKill[Kill Process] --> HCWait
HCWrite[Run Healthcheck] --> HCWriteSuccess{Success?}
HCWriteSuccess -- yes --> Write
HCWriteSuccess -- no --> WaitR
Write[Write Lock] --> WriteSuccess{Success?}
WriteSuccess -- yes --> WriteWaitCR
WriteSuccess -- no ---> WaitR
WriteWaitCR[["Wait R\n(C-1 times):\n->Write Lock\n->Wait R"]] --> WaitR
style WriteWaitCR text-align:left
HCWait[Run Healthcheck] --> HCWaitSuccess{Success?}
HCWaitSuccess -- yes --> WaitFR
HCWaitSuccess -- no --> WaitR
WaitR[Wait remainder of R, if any]
WaitFR[Wait F*R for takeover] --> HCWrite
HCSunnyLoop[Run Healthcheck] --> HCSunnyLoopSuccess{Success?}
HCSunnyLoopSuccess -- yes --> SunnyLoopWrite
SunnyLoopWrite[Renew lock\nMay attempt for\nup to F*R seconds] --> SunnyLoopWriteSuccess{Success?}
SunnyLoopWriteSuccess -- yes --> SunnyStartProcess
SunnyLoopWriteSuccess -- no --> SunnyLoopAbort
SunnyStartProcess[Unfence Self\nFence Others\nEnable Process] --> WaitR
HCSunnyLoopSuccess -- no --> SunnyLoopAbort
SunnyLoopAbort[Attempt to write blank to lock\nOne attempt only.] --> SunnyKill
SunnyKill[Kill Process] --> WaitR
WaitR --> End((End)) --> ToStart[Back to start]
依赖关系
~22–34MB
~637K SLoC