Phorge 现代化改造实战(十五):用 Go 接管 Phorge 工作队列,如何避免新旧消费者互相抢任务

💡 原文中文,约9400字,阅读约需23分钟。
📝

内容提要

本文介绍Phorge工作队列迁移至Go服务的过程。通过gorge-taskqueue管理队列状态、gorge-worker处理消费循环,将任务调度与PHP业务实现分离。迁移需处理ID分配、租约机制、归档语义等兼容问题,并停用旧taskmaster确保所有权唯一。过渡期未迁移任务经Conduit回传PHP执行,最终实现队列运行时独立,无需一次性重写所有worker。

🔎

延伸解读

ID 分配与租约语义是迁移的隐形陷阱

Phorge 的 worker_activetask 不使用自增 ID,而是通过 lisk_counter 在应用层分配,Go 侧若另建自增序列会导致 ID 冲突。租约获取必须用带原条件的 UPDATE 确保所有权,SELECT 只提供候选。yield 通过哨兵字符串表示,归档表字段与活跃表不对称,这些细节都需严格兼容,否则会出现任务重复执行或数据错乱。

新旧消费者并存不等于任务重复执行

旧 taskmaster 与 gorge-worker 并存时,由于双方都遵循条件更新和租约规则,任务通常会被竞争瓜分,不会重复执行。但为了所有权唯一,仍需停用旧消费者。灰度开关 GORGE_TASKQUEUE_DISABLE_PHD_TASKMASTER 可用于过渡,但不宜长期使用,切换时应尽量缩短并存时间。

健康检查通过不代表队列已接管

taskqueue 的 /readyz 仅对 MySQL 执行 Ping,不验证表结构或协议。只有真实跑通 enqueue → lease → complete 闭环,才能证明 schema、凭据、命名空间和队列协议正确。首次启动时,若 Phorge 等待 taskqueue 健康而 taskqueue 又依赖未创建的数据库,会形成死锁,需注意编排顺序。

内部执行入口存在信任缺口

worker.execute 方法关闭了认证和防护写入,其安全前提是调用只能经过带 X-Service-Token 的 gorge-conduit,但该方法自身未验证。生产环境需阻止外部直接访问该入口,并建议后续实现独立凭据验证和 taskClass 白名单。allowed-uris 只解决 Host 匹配,不能替代身份认证。

Q&A

Phorge 工作队列迁移到 Go 服务时,如何避免新旧消费者互相抢任务?

迁移时通过设置 phd.taskmasters=0 停用旧的 PhabricatorTaskmasterDaemon,确保消费权唯一。同时,gorge-worker 和旧 taskmaster 都遵循相同的租约规则(条件更新),即使并存也不会导致同一任务执行两次,但为了所有权唯一,必须关闭旧消费者。

gorge-taskqueue 和 gorge-worker 在 Phorge 工作队列迁移中分别承担什么职责?

gorge-taskqueue 负责管理队列状态,提供入队、租约、完成等 HTTP 接口,并处理与 MySQL 或 Redis 的存储交互;gorge-worker 负责消费循环,从 taskqueue 领取任务并执行,或将未迁移的任务通过 Conduit 委派回 PHP。

Phorge 工作队列迁移中,任务 ID 分配有什么特殊之处?

Phorge 的 worker_activetask 表不使用 AUTO_INCREMENT,而是通过 lisk_counter 在应用层分配 ID。Go 侧必须复用 Phorge 的计数器逻辑,在同一个事务中插入 lisk_counter 并获取 ID,否则会导致 ID 冲突。

Phorge 工作队列迁移中,租约机制是如何实现的?

租约分两阶段:先选择从未租过的任务(leaseOwner IS NULL),再选择租约过期的任务(leaseExpires < 当前时间)。选出候选后,通过带原条件的 UPDATE 语句获取租约,确保只有一次更新成功,从而保证所有权。

Phorge 工作队列迁移中,归档表与活跃表有哪些字段差异?

归档表没有 failureTime 字段,该字段仅用于活跃任务的重试退避,归档时丢弃。归档行的结构为活跃表字段减去 failureTime,再加上 result、duration 和 archivedEpoch。

Phorge 工作队列迁移中,如何将未迁移的任务委派回 PHP 执行?

gorge-worker 配置 GORGE_WORKER_CONDUIT_URL 后,会安装一个 fallback,将未在本地实现的任务通过 Conduit 的 worker.execute 方法发送给 Phorge。Phorge 新增的 PhabricatorWorkerExecuteConduitAPIMethod 会构造临时任务对象并调用 executeTask() 执行,结果再返回给 Go。

Phorge 工作队列迁移中,为什么需要停用旧的 taskmaster?

虽然新旧消费者并存不会必然导致任务重复执行,但为了确保消费权唯一,必须停用旧 taskmaster。设置 phd.taskmasters=0 可以完成明确的消费权交接,避免混合消费带来的不确定性和管理复杂性。

Phorge 工作队列迁移中,MySQL 和 Redis 两种后端在回滚能力上有何差异?

MySQL 模式下,PHP 和 Go 共享同一份数据,回滚只需切换调度入口;Redis 模式涉及数据面迁移,回滚需要定义排空、迁移或回放方案,不能简单依赖代码 fallback。

Phorge 工作队列迁移中,如何验证迁移成功?

验证分三层:Go 单元与契约测试、仓库内的 e2e 测试(tests/e2e/taskqueue.sh)以及完整栈上用 bin/worker flood 注入真实任务。最有说服力的证据是状态转移与所有权:谁入队、谁领取、谁执行、谁归档,每一步都能对上。

Phorge 工作队列迁移中,worker.execute 接口存在哪些安全风险?

worker.execute 设置了 shouldRequireAuthentication() = false 和 shouldAllowUnguardedWrites() = true,安全前提是调用只能经过带 X-Service-Token 的 gorge-conduit,但该接口自身不验证调用者。生产环境需阻止外部直接访问,并建议后续实现独立的服务凭据验证和 taskClass 白名单。

🏷️

标签

➡️

继续阅读