fencing token 让过期的旧主节点失效
租约到期时,持有者可能只是暂停,并没有真正停止。旧进程恢复后,仍会发送迟到写入。存储服务需要一种方法拒绝已经被取代的所有权;围栏令牌提供有序的所有权世代编号,并随每次变更传递。保护来自资源侧实际检查,而不是锁服务仅仅发出了一个数字。理解这个执行位置,才能判断旧主是否真的被挡住。 光有锁还不够 客户端可以先检查租约,然后因垃圾回收暂停,等另一个客户端取得资源后才恢复。即使在发送前立即检查,也无法消除检查与使用之间的空隙。网络延迟同样可能让已经发送的请求很晚才到达。资源必须在操作真正到达时决定是否接受,不能只相信客户端记忆中的持有状态仍然有效。 租约超时是故障检测机制,不证明先前进程已经停止。旧进程的凭据可能仍能通过身份认证,但它的所有权已经过期。因此应区分身份认证、权限与所有权世代。围栏不替代访问控制,也不能允许恶意客户端随意声称一个极大的编号,把合法所有者永久挡在外面。合法编号的来源同样属于协议需要保证的边界。 把递增编号绑定到所有权 协调者在取得所有权时发出递增令牌。暂停的持有者仍只有旧编号,不能在没有重新取得所有权时,随便再申请一个新数字继续写。发号序列必须在故障后避免回退和复用;墙上时钟、分区节点各自维护的计数器,都不会自动具备这个契约。时间戳看起来更大,不等于它证明了有效的所有权交接。 被保护资源保存已接受的最大世代,拒绝更小编号,接受更大编号时推进高水位。比较和对应的数据变更必须在同一个原子、持久化边界内完成。如果先单独检查,再无条件写入,就重新制造了竞争窗口。复制资源还需要一致的执行边界,不能让每个副本独立比较、各自接受不同所有者,之后再声称全部操作已经被围栏保护。 相等编号需要单独定义策略 一个租约持有者通常会在同一世代里执行多次写入。如果拒绝所有等于高水位的编号,该持有者就只能成功写一次。常见的所有权世代规则拒绝较小值,相等值仍可按应用正常权限处理。每次操作都使用严格递增编号,是另一种协议,不能与每次取得所有权才发一枚令牌的机制混为一谈。 相等编号也不能说明某次操作是否重复。需要防止重试重复产生效果时,应使用操作标识、幂等更新或其他明确的顺序规则。围栏区分不同所有权世代,不会自动排列同一世代的全部操作,也不会自动去重,更不能单独保证业务效果恰好发生一次。把这些责任写清楚,可以避免用一个编号替代所有并发控制。 坏掉的样子 发出新令牌,不会立即通知所有被保护资源。在资源尚未见过新世代之前到达的旧写入,仍可能通过高水位检查。如果契约要求取得新所有权的一刻就立即撤销旧权限,需要在允许后续工作前,通过合适的协调步骤把新世代建立到资源侧。普通围栏不能描述成对全部在途请求的即时取消机制,测试应保留这个先后顺序边界。 计数器回退可能让新所有者拿到比旧持有者更小的编号。资源丢失持久化高水位,也可能重新接受旧世代。恢复流程必须同时保全发号序列与资源状态;只把一端恢复到旧备份,却不协调所有权,可能破坏原来的假设,即使两个组件都能正常启动。组件存活与协议仍安全,是不同判断。 缺失或格式错误的令牌不能悄悄绕过检查。检查每一条变更入口,包括维护任务和兼容客户端。不能执行令牌比较的文件服务或外部 API,需要另一种受支持的并发控制机制;仅在请求头中放一个数字,并不会保护实际副作用。比较必须位于真正拥有写入权的资源边界,而不是只存在于调用方日志中。 在资源侧执行围栏检查。依据原文绘制机制;模型值与原文报告的观察不代表本次运行测试。 快速验证 把下面标准库 Python 示例保存为 fencing.py 后运行。它是顺序执行的内存模型,不是持久锁服务,也不是并发存储实现;没有外部服务器、系统时钟或真实凭据参与,所有状态都只存在于当前程序中。 class StaleFenceError(RuntimeError): pass class FencingResource: """Sequential model: equal tokens permit more writes in the same epoch.""" def __init__(self): self.last_seen = 0 self.data = None def write(self, token, value): if type(token) is not int or token <= 0: raise StaleFenceError("missing or invalid fence token") if token < self.last_seen: raise StaleFenceError(f"stale token {token} < {self.last_seen}") self.last_seen = token self.data = value return True class Issuer: """Test counter; a real issuer must bind durable epochs to ownership.""" def __init__(self): self.counter = 0 def acquire(self): self.counter += 1 return self.counter def refused(resource, token): before = (resource.last_seen, resource.data) try: resource.write(token, "must-not-land") except StaleFenceError as error: assert (resource.last_seen, resource.data) == before print("rejected:", error) else: raise AssertionError("invalid or stale write was accepted") issuer = Issuer() old_leader, new_leader = issuer.acquire(), issuer.acquire() resource = FencingResource() # Issuance alone has not informed this resource about epoch 2. assert resource.write(old_leader, "arrived-before-new-epoch") assert resource.write(new_leader, "from-new-leader") refused(resource, old_leader) assert resource.write(new_leader, "second-write-in-same-epoch") assert resource.last_seen == 2 refused(resource, None) refused(resource, "2") refused(resource, True) next_token = issuer.acquire() assert resource.write(next_token, "newest") refused(resource, new_leader) assert (resource.last_seen, resource.data) == (3, "newest") print("PASS: observed newer epochs fence older writes; equal epoch remains usable") 首次旧令牌写入成功,因为资源还没有观察到第二世代。第二世代写入后,第一世代被拒绝,已存数据和高水位都不改变。同一个第二世代随后还能再写,说明一位所有者可以合法进行多次操作。缺失值、字符串和布尔值被拒绝;第三世代到来后,第二世代也成为旧值,不能继续修改资源。 ...