第 15 / 23 篇

第 14 章:并发领取与系统容量是两道题

启动两个 worker 很容易,证明它们不会同时执行同一项任务更难。即使成功避免了重复领取,也仍可能因为连接池、工具并发或恢复扫描耗尽资源。正确性讨论需要先确定每一行归谁,容量讨论再解释一整批任务怎样共享有限资源。

14.1 领取必须在数据库内竞争

本书的领取使用 PostgreSQL 行锁和 FOR UPDATE SKIP LOCKED。候选任务先按到期时间和创建时间排序,事务锁定其中一条,再写入 running、owner、新 fence、尝试次数和租约。竞争者遇到已锁定的行会跳过,不会靠先读后写的应用判断争抢同一条记录。

这个顺序把“看见可执行任务”和“成为当前拥有者”连接在同一个事务里。若只先查询 ready,再由两个进程各自更新 owner,两者都可能在更新前相信自己有权调用。即使最终只留下一个 owner,也不能撤销另一个进程已经发出的 HTTP 请求。

行锁只覆盖领取事务,代码不会持锁跨越工具调用。把网络等待放进事务会延长锁寿命,占用连接并扩大数据库故障的影响。事务提交后靠租约和 fence 管理执行权,靠工具合同处理副作用身份,这些机制负责不同边界。

14.2 两个真实进程怎样同时出发

TestTwoClaimersCannotOwnOneWorkflow 验证同一进程内两个并发调用;TestTwoWorkerProcessesClaimExactlyOne 则启动两个独立的 worker 可执行文件。二者不是同一项证据的重复命名,后者同时覆盖进程环境、数据库重连和真实命令入口。

子进程先各自输出 BOOK_READY,父测试再分别向它们的标准输入写入换行,释放启动门。胜出的 worker 在 after_claim 暂停,另一位执行者完成一次空领取并退出。测试读取数据库中的 owner,释放真正的胜者,等待工具调用和终态提交。

真实运行得到:

{"os_processes":2,"claims":1,"effects":1,"state":"succeeded"}

这里同时检查了事件表里的领取次数、HTTP POST 次数和工具账本行数。如果只检查最终状态为 succeeded,两位 worker 各做了一次副作用也可能得到同样的显示结果。并发实验的观察对象必须跨过界面状态,抵达副作用账本。

14.3 fence 保护写入,幂等键保护效果身份

胜出之后也不是永久拥有。租约可能过期,恢复程序会提高 fence,后续执行者获得新的 fence。旧 worker 即使迟到地拿到成功回执,也不能用旧 owner、旧 fence 或过期租约更新当前工作流。

TestExpiredOwnerCannotCommitAnOldResult 显式用 SQL 使租约过期,再经过 unknown 和权威空查询取得新领取。实验观察到旧 fence 为 1、新 fence 为 4,旧写入遭拒;同一业务键重复请求仍只留下一个工具效果。这里的租约过期是精确边界注入,不能写成等待自然到期的测量。

这些数字不要求 fence 每次只加一次。领取、回收、核查和完成都可能增加代数。它的重要性质是单调变化,并且写入者必须携带自己领取时获得的值。生产工具若能接受 fencing token,还可以进一步约束外部资源;本实验的 HTTP 工具使用稳定幂等键,不宣称实现了提供者侧 fence。

14.4 连接池不是可以无限放大的水管

单个 Store 将数据库最大打开连接数限制为 12。这只是教学进程的保守上限,不是依据压测求出的推荐配置。两个进程各有自己的连接池,工具服务还会访问数据库;把 worker 数量增加十倍,会同时改变连接总量、心跳频率和工具请求压力。

评估容量时先列出资源预算:数据库可给本服务的连接、每个任务需要的连接阶段、下游允许的并发和速率、平均与长尾调用时长,以及 unknown 核查的额外请求。计算得到的吞吐上限还要通过压测确认,不能直接由 CPU 核数推导。

SKIP LOCKED 降低了等待锁的竞争,却不保证严格公平。长期被锁住、尚未到期或反复失败的记录可能被后来的任务越过。生产调度需要监控队列年龄,并考虑租户配额、优先级和老任务检查。实验只验证单条竞争的安全性,没有验证大量租户的公平性。

14.5 恢复流量也消耗容量

一次进程重启可能使许多租约接近到期。如果回收之后全部立刻查询工具,再一起重排,恢复本身会成为尖峰。更危险的是主调用和核查共享下游配额,核查把正常任务挤出,而新的超时又制造更多 unknown。

实际系统可以给恢复扫描设置批次、给查询设置独立并发预算,并对重新派发增加适当分散。实现这些策略时仍要保留原始键和持久尝试次数。把排队等待伪装成新的业务操作、借此绕过预算,会破坏前面建立的身份与边界。

本实验的 RecoverExpired 会在一个事务里锁定并回收符合条件的记录,适合数量受限的教学数据。生产规模需要分析事务大小、索引、扫描开销和多恢复者竞争,再决定分批方法。通过二进程实验不能推导任意数量执行者都能线性扩展。

练习。 现在有 40 个 worker,每个连接池上限为 12,数据库只允许这项服务使用 120 个连接。请提出一种不改变领取正确性的容量调整方案,并说明要观测哪些队列、连接和工具指标。再设计一个实验区分“领取冲突导致慢”与“工具长尾导致租约风险”。