☰
WuKongIM MQTT 精确 Owner 执行与静默(Quiescence)契约:从本地准入栅栏到跨节点接管证明
2026/10/15 11:27:54 网站建设 项目流程
  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载

导读

本文以 WuKongIM 仓库内 MQTT owner execution and quiescence 规范文档为主体,系统讲解 MQTT 模块中“连接 Owner(执行主体)的本地执行门禁”如何与“跨节点接管时的隔离证明”协作:从Owner身份模型、Reserve/Activate/Renew/Begin/Quiesce生命周期,到 Service 107 精确 Owner RPC 的二进制编解码,再到配套的自动清扫(sweep)调度与可观测指标。读完本文,你将掌握 WuKongIM 如何通过本地单调时钟 + 保守执行期限挡住过期执行,同时拒绝用本地过期代替分布式隔离证明,并能在源码与测试中逐一找到对应实现。

模块定位:owner 本地半边与权威协议的边界

该模块是“已批准接管契约(takeover contract)”的 owner 本地半边。它明确声明自己不会把“socket 关闭请求、context 取消或 deadline”当成“旧 owner 已停止”的分布式证明。运行时激活仍然要求:

  • 一个已提交(committed)的 Session owner;
  • 由权威 lease 协议提供的保守本地执行期限(conservative local execution deadline)。

在协议、恢复与投递被完整接线并验证之前,产品准入(product admission)保持关闭。换句话说,mqttsession运行时包只负责有界本地执行与静默,不负责认证用户、不获取持久所有权、不推导分布式 lease、不发布消息,也不解释 MQTT 报文——这一点在 internal/runtime/mqttsession/FLOW.md 的 Responsibility 中写得很明确。

实现前的故障清单(Failure Inventory)

规范文档在实现前整理了一份九大类故障清单,每一项都对应后续实现中的一条硬性约束,理解它们才能看懂代码为什么这样写:

  1. 待定预留(pending reservation)抢在已提交激活之前执行:不同 UID、ClientID、生命周期、owner 代际、进程或连接可能作用于错误的 owner。ClientID 共存不得继承 WK 主设备冲突规则。
  2. 过期/重复的激活或续期延长执行:过期 owner 可能因 expiry worker 迟到而重启。因此每个操作都同步、独立于清扫调度地检查本地期限。
  3. Quiesce 在请求 socket 关闭后就返回成功,而已准入的操作仍可能继续执行。成功要求 close 回调完成、且每个已准入操作都释放其 scope;仅取消不构成证明。
  4. close 错误、回调 panic 或调用方取消不得释放容量或重新打开准入;并发 close 调用不得触发互相竞争的回调。失败的 close 保持 fenced 并被保留,后续调用可重试同一个精确 owner。
  5. 缺失行被当作已关闭:连接 ID 必须由本注册表分配与发布,绝不由调用方提供、复用或包装。只有本进程内已退役的已签发 ID才能证明缺席;外部 boot / 未来 ID 一律 fail-closed。
  6. 限额遗漏 pending/closing owner 或在途 scope;续期不得让 expiry 队列无限增长;关闭不得为每个连接创建 goroutine 或每 tick 扫描全部 Session。应每个被保留 owner 维护一个索引化期限,限制 reservation/operation/sweep 页面,并在锁外执行回调。
  7. 过期与续期竞争:过期的堆弹出版本不得关闭已续期的 owner;调用方取消或重复释放 scope 不得泄漏计数。快照计数不含身份或凭据。重复清扫只能由 App 驱动。
  8. Gateway close 适配不得在未封住报文写入、未关闭物理连接的情况下报告 quiescence;运行时回调不得重入 quiescence 造成死锁。回调只负责关闭传输,从不等待业务清理。
  9. Owner RPC不得接受畸形/无界身份、不得把目标指向别的节点、不得把不支持/缺失/错误响应解读为关闭、不得接受针对另一个 owner 的回执。闭合版本化编解码必须回显完整身份;取消与瞬时错误必须和 quiesced 回执区分开。

Owner 身份模型与本地契约

Owner 的构成

Owner是不可变的执行化身标识,包含:

  • broker namespace 与 ClientID(合成Key);
  • Session 代际(SessionGeneration,持久订阅/游标生命周期);
  • owner 代际(OwnerGeneration,每次已提交的连接接管都变化);
  • node ID、boot ID(注册表重建时必须变化,包括 restore);
  • 注册表签发的连接 ID(ConnectionID,由 owner 注册表签发,客户端或之前的 boot 都不能提供)。

它不含凭据、报文或具体 socket。每个 reservation 只在本节点上保留一个 UID 与一个 close 回调。连接 ID 独立于 Gateway 的 Session ID。以上定义可见 internal/contracts/mqttsession/owner.go,其中ValidIdentity对 namespace/ClientID/BootID 的长度、UTF-8 合法性、空串/空白做了统一校验。

Reserve:待定且有界

Reserve在身份进入持久元数据之前发布一个待定身份。不同的 ClientID 与并发候选不会触发 WK 设备踢除。它受两个硬限制:len(m.entries) >= Capacity或issued == MaxUint64时返回ErrOwnerLimit。ConnectionID只在注册表锁内递增,永不回绕或复用(见 internal/runtime/mqttsession/owner.go)。

OwnerOptions的所有边界在 owner.go 中都有校验:

参数约束/含义
NodeID节点 ID,必须非零
BootID本进程与注册表生命周期内唯一,≤128 字节
Capacity包含 pending 与 closing owner,范围 [1, 1,000,000]
MaxOperations每 owner 准入 scope 上限,范围 [1, 1024],无等待队列
PendingTimeout激活前未完成本地预留的期限,范围 (0, 1 分钟]
MaxLease本地执行视野上限,范围 (0, 1 分钟]
CloseRetry失败/未完成清理的重试延迟,范围 (0, 1 分钟]
Now与所有提供的期限共用本进程单调时间基

NewOwners还会拒绝Now()返回now.Round(0)的假时钟(owner.go),防止非单调时钟混入。

Activate / Renew:递增持久修订 + 未来期限

  • Activate只在调用方证明了这个精确的已提交 owner并推导出保守期限后打开执行;
  • Renew只接受更新的已提交回执,且仅在现有 lease 存活时有效。延迟续期或精确重试都不能让已 fenced 的 owner 复活。

installLease的实现要点(owner.go):

  • 本地时钟已越过e.deadline或处于 closing 阶段 →fenceLocked并返回ErrOwnerFenced;
  • revision == 0、期限非单调、until不超过 now、或until.Sub(now) > MaxLease→ErrOwnerInvalid;
  • 精确回执重试(相同 revision + 相同 until)幂等返回 nil,但不能移动期限;
  • 续期要求revision > e.revision,否则 fenced。

Begin / Operation:一次准入一个 scope

Begin在执行任何业务工作前准入一个有界操作,其 scope 必须跨越所有关联效果与网络写入保持打开:

  • 同步检查:stopped、owner 非 active、本地期限已过(此时会顺带fenceLocked)、operations >= MaxOperations(返回ErrOwnerLimit);
  • Operation.Done是显式且幂等的(CompareAndSwap),忽略已取消的 scope 会保留 owner 并防止伪造 quiescence 回执;
  • scope不得逃逸到未跟踪的后台效果;元数据变更仍携带持久 owner 栅栏;
  • Check在不再占用第二个 operation 槽位的情况下重新校验存活 lease;即便 sweep 尚未运行,期限过期也会栅栏准入(owner.go)。

一个依赖项可能返回时,已接受的 Channel 工作仍在继续。此时必须在Done之前调用MarkUncertain:它将一个“隔离未证明”的屏障永久保留在这个有界注册表中。物理关闭与本地 scope 排空只会以失败唤醒等待者;时间、Sweep 与 Close 都不能擦除它,且当前没有恢复证明清除 API(owner.go)。

Quiesce:先永久关闭准入,再关传输

Quiesce的执行顺序(owner.go):

  1. fenceLocked:永久关闭准入、取消操作 context(不等待,不提供隔离证明);
  2. 通过注入的 close 回调关闭传输;只有物理关闭与 scope 排空都完成才算成功;
  3. 重复请求合并(coalesce)到同一个ownerCloseAttempt;回调失败保持 fenced、保留容量,后续调用可重试同一精确 owner;
  4. 成功回执只针对这个精确 owner,不是凭据、未来 owner 或发布授权。

callOwnerClose会把回调 panic 与一般错误统一为ErrOwnerClose,但保留context.Canceled/DeadlineExceeded语义(owner.go)。awaitQuiescence在等待结束后还会复查e.uncertain,一旦为 true 即返回ErrOwnerUnknown。

期限过期是准入栅栏,不是已准入工作已排空的证明。分布式接管路径不得用本地过期、presence TTL、boot 不匹配或 best-effort 踢除来替代所需的隔离证明。

Fence / Close / StopAdmission:关闭路径的三个层次

  • Fence:永久关闭准入并取消 scope,不调用传输、不等待已准入效果(owner.go);
  • StopAdmission:永久拒绝新 reservation/scope 并取消现有 scope,不等待传输或业务清理,也不提供证明(owner_deadlines.go);
  • Close:先StopAdmission,再按最多 256 个一页循环Quiesce所有保留 owner;超时后可重试,保留的工作绝不被擦除以伪造成功关闭(owner_deadlines.go)。

已实现的 Owner RPC 契约(Service 107)

线协议

规范文档给出的协议被 internal/access/node/mqtt_owner_codec.go 完整实现:

  • 服务 ID:107,经既有CallRPC传输向精确 owner 节点请求 quiescence;
  • 请求 magic:WKMQ,响应 magic:WKMq,两者都使用格式字节 1 后跟操作/状态字节;
  • 请求操作 1 = quiesce;响应状态 1=quiesced、2=isolation unproved、3=close failed、4=cancelled、5=deadline exceeded;未知格式/操作/状态一律失败;
  • 双方向按固定顺序携带:namespace、ClientID、SessionGeneration、OwnerGeneration、NodeID、BootID、ConnectionID;
  • 字符串用大端 uint16 字节长度(上限 1,024/1,024/128);整数为大端 uint64;
  • 帧上限 2,304 字节,拒绝截断与尾部多余字节;
  • 客户端只接受与其完整精确请求身份匹配的成功;不支持的服务或通信失败既不重路由也不推断隔离。

测试 internal/access/node/mqtt_owner_rpc_test.go 给出了一个黄金字节串断言:

574b4d51010100016e0001630000000000000001000000000000000200000000000000030001620000000000000004

逐字节对应:magicWKMQ、格式 1、操作 1、0001+"n"(namespace)、0001+"c"(ClientID)、1(SessionGeneration)、2(OwnerGeneration)、3(NodeID)、0001+"b"(BootID)、4(ConnectionID)。同一个测试还验证了:对每个前缀截断、尾部追加、4096 字节、JSON、错误操作/格式字节的请求,服务端都不调用底层 close 端口;对响应中篡改 ConnectionID、BootID 或返回空/单字节的回执,客户端一律报错。

与 Gateway 物理关闭的衔接

Quiescence 的“物理关闭”由 Gateway 的可选能力提供,详见 gateway-transport-close-proof.md:

  • transport.CloseWaiter.CloseAndWait(ctx):栅栏未来写入,仅当物理关闭回调成功后才返回 nil;取消只限制调用方等待,不影响底层关闭;后续调用合并到同一个回执(pkg/gateway/transport/transport.go);
  • session.OutboundFencer.FenceOutbound():永久关闭普通写入准入,不等待已进入的编码器;
  • types.Context.CloseTransportAndWait(ctx, reason):要求两个能力都存在,栅栏入站/出站准入、取消请求工作、请求物理关闭,不调用也不等待协议/业务清理。

实现位于 pkg/gateway/core/transport_close.go:先检查transport.CloseWaiter与session.OutboundFencer两个能力(缺失返回ErrCloseProofUnsupported),再FenceOutbound()、取消请求 context,最后closer.CloseAndWait(ctx)。文档特别强调:在 gnet v2.9.7 中OnClose先于残余写入与 close 系统调用,只有CloseWithCallback的完成才能提供这个边界。

自动清扫(Owner Sweep)调度

动机

即使 acquisition 从未注册连接,产品也必须驱动已有的 owner 期限堆。本地期限栅栏准入,但不证明远端效果完成,也不授权持久 Session 迁移。早期实现前,App 集成测试在 7 秒后失败:product never swept an expired unregistered reservation——只有已注册连接才受自动监督,导致 pending reservation 与未注册过期 owner 永远占用容量。

实现契约

  • OwnerSweeper拥有一个可选的受管任务mqtt/owner_sweeper;
  • 每轮调用Owners.Sweep,最多 256 次访问、250ms 取消期限,默认间隔 250ms;
  • 不新增队列、per-owner 任务、表、命令、RPC 或产品配置字段;既有的同步准入检查在 sweep 落后时仍会栅栏过期 lease。

参数边界在 owner_sweeper.go:Limit默认 256、上限 256;Interval默认 250ms、范围 [1ms, 1 分钟];TurnTimeout默认 250ms、范围 [1ms, 5s]。一个不配合的回调占用当前轮直到返回;Stop可以取消但不能丢弃它。超时的Stop保留 run 所有权并拒绝Start,直到后续Stop完成 join。Start的 context 不拥有 run(owner_sweeper.go)。

Sweep 的公平性与选型

Owners.Sweep(owner_deadlines.go)在注册表锁内完成选型、续期与栅栏:

  • 过期堆顶条目先fenceLocked,再把下次尝试移到now + CloseRetry——失败 close 不会垄断后续每一页的堆头;
  • 物理回调在锁外执行;一轮内至多访问limit个到期 owner;
  • 每个保留 owner 在ownerDeadlines堆中只有一个索引化期限;续期用heap.Fix修正既有堆项而非追加陈旧记录(owner_deadlines.go),因此 100 次续期后Snapshot().Deadlines仍为 2(见 owner_test.go)。

复杂度:每轮 O(k log n)(k 为访问条目数),聚合快照 O(1)。调度器每轮只加一个 goroutine/timer 与一个 context,绝无per-session goroutine、全注册表扫描或消息体存储。配置的访问上限不是吞吐度量,也不构成 10 万用户恢复 SLA——完整规模认证仍需进行(见 mqtt-owner-sweeping.md)。

App 组合中的启动/停止顺序

internal/app/mqtt_product.go 展示了完整组合:

  • wireMQTT在启用mqtt.enable后注册MQTTOwnerRPCServiceID,并向自有 Gateway 追加 "mqtt" TCP 监听器;
  • newMQTTGeneration每次全新构造:随机 boot(crypto/rand16 字节 hex)、NewOwners(Capacity=MaxConnections、MaxOperations=8、PendingTimeout=5s、MaxLease=1 分钟、CloseRetry=250ms)、sweeper、retirements、dispatches、sessions、connections、deliveries 等;
  • Start在Gateway 准入之前启动[sweeper, replay, deadlines, wills, consumers, connections, deliveries];
  • Stop先StopAdmission、停认证,再依次 join 各 worker,最后owners.Close(ctx),之后才持久化 graceful boot retirement 并释放数据目录锁——顺序保证“先栅栏 owner 准入,再在传输与集群依赖仍可用时 join 清扫”。

可观测性:两个封闭指标

  • wukongim_mqtt_owner_sweep_total只含turns、visited、failures三类事件;visits 统计尝试次数(含重试),不是唯一退役数;取消可计为失败轮次;
  • wukongim_mqtt_owner_work只含held、pending、active、closing、operations、deadlines、uncertain七种状态,零值全部物化。Gauge 是“最后一次采样的聚合”,不是原子性的抓取时点普查;Stop不会为保留工作伪造零值。

实现位于 pkg/metrics/mqtt.go 与 pkg/metrics/mqtt.go:ObserveOwnerSweep(visited, failed)与SetOwnerWork(state, value)都只接受封闭名称,绝不从 owner 身份、回调错误或凭据创建 label。App 侧接线见 internal/app/mqtt_owner_sweep.go。

集成测试 internal/app/mqtt_owner_sweep_integration_test.go 验证了完整产品路径:单节点 256 hash slot、启用 MQTT 与指标,手动owners.Reserve一个“被遗弃”的 pending 连接(未走连接注册),断言 sweeper 自动关闭它、Held归零、wukongim_mqtt_owner_sweep_total{event="visited"}≥ 1,且Stop后所有 MQTT 任务 join、Deadlines归零。

与周边模块的协作边界

  • 产品组合:mqtt-product-runtime.md 注册该适配器并将运行时回调接到 Gateway;CloseTransportAndWait只负责物理关闭完成,普通CloseSession仍只是关闭请求;foreground 组合与 graceful restart retirement 已接线,不可达/崩溃 owner 的恢复证明仍是完整接管验收的必需项;
  • Session acquisition:mqtt-session-acquisition.md 现在执行带提交结果与“proposal 前捕获期限”的认证 acquisition/renewal;它要求显式的 prior-owner isolation,隔离端口只有在精确 owner 执行与传输隔离后才返回 nil,绝不由 Session 行或时间推导该证明;租期默认上限 86,400 秒,本地 lease 与注入时钟共用 owner 本地单调时间基;
  • Owner sweeper:mqtt-owner-sweeping.md 补充了自动本地过期/重试清理,但不改变分布式证明要求;sweeper 永不修改 Session 行、不清除 uncertain 效果、不签发隔离回执。

测试验证速览

  • internal/runtime/mqttsession/owner_test.go 覆盖:未激活 begin 被 fenced、精确激活幂等、单 owner 操作数上限、身份任一字段篡改即失败、quiescence 等待物理关闭与 scope 排空(回执先于 drain 会失败)、失败 close 三种模式(error/panic/cancel)保持 fenced 且占用容量、续期不增长期限堆、取消不结束 operation、关闭后拒绝新 reservation;
  • internal/access/node/mqtt_owner_rpc_test.go 验证 RPC 等待运行时 quiescence:先等传输回调触发,断言回执未提前返回,op.Done()后成功,且篡改 BootID 返回ErrOwnerUnknown;
  • 默认层级全套 runtime/app/metrics/goroutine 测试以-race通过;聚焦 race 集成覆盖慢回调 join、保留操作、uncertain 屏障、有界/公平轮次、续期与产品启停。

结论与剩余范围

WuKongIM 的 MQTT owner 执行模块把“本地执行门禁”与“跨节点隔离证明”严格分离:本地一侧用单调时钟、保守期限、有界容量与精确身份保证过期即栅栏、scope 不逃逸、quiesce 不放水;远端一侧用 Service 107 的闭合版本化编解码保证只有精确 owner 的回执才算数。目前 foreground 组合与 graceful restart retirement 已接线,但崩溃隔离、uncertain 远端效果解析、pending-unsubscribe 与无游标恢复、持久记录回收、restore 再激活以及完整进程故障/规模矩阵仍属于开放工作(见 mqtt-owner-sweeping.md 的 Capacity and remaining scope)。

  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载
上一篇:全球时区速查清单:IANA 时区标识符与 UTC 偏移完整对照表(reference 项目)
下一篇:Plate 项目 Portable Text 测试索引(test-index)完全指南:从 200 个可运行测试行到源码锚点的检索体系

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询