- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
导读
本文以 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)
规范文档在实现前整理了一份九大类故障清单,每一项都对应后续实现中的一条硬性约束,理解它们才能看懂代码为什么这样写:
- 待定预留(pending reservation)抢在已提交激活之前执行:不同 UID、ClientID、生命周期、owner 代际、进程或连接可能作用于错误的 owner。ClientID 共存不得继承 WK 主设备冲突规则。
- 过期/重复的激活或续期延长执行:过期 owner 可能因 expiry worker 迟到而重启。因此每个操作都同步、独立于清扫调度地检查本地期限。
- Quiesce 在请求 socket 关闭后就返回成功,而已准入的操作仍可能继续执行。成功要求 close 回调完成、且每个已准入操作都释放其 scope;仅取消不构成证明。
- close 错误、回调 panic 或调用方取消不得释放容量或重新打开准入;并发 close 调用不得触发互相竞争的回调。失败的 close 保持 fenced 并被保留,后续调用可重试同一个精确 owner。
- 缺失行被当作已关闭:连接 ID 必须由本注册表分配与发布,绝不由调用方提供、复用或包装。只有本进程内已退役的已签发 ID才能证明缺席;外部 boot / 未来 ID 一律 fail-closed。
- 限额遗漏 pending/closing owner 或在途 scope;续期不得让 expiry 队列无限增长;关闭不得为每个连接创建 goroutine 或每 tick 扫描全部 Session。应每个被保留 owner 维护一个索引化期限,限制 reservation/operation/sweep 页面,并在锁外执行回调。
- 过期与续期竞争:过期的堆弹出版本不得关闭已续期的 owner;调用方取消或重复释放 scope 不得泄漏计数。快照计数不含身份或凭据。重复清扫只能由 App 驱动。
- Gateway close 适配不得在未封住报文写入、未关闭物理连接的情况下报告 quiescence;运行时回调不得重入 quiescence 造成死锁。回调只负责关闭传输,从不等待业务清理。
- 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):
fenceLocked:永久关闭准入、取消操作 context(不等待,不提供隔离证明);- 通过注入的 close 回调关闭传输;只有物理关闭与 scope 排空都完成才算成功;
- 重复请求合并(coalesce)到同一个
ownerCloseAttempt;回调失败保持 fenced、保留容量,后续调用可重试同一精确 owner; - 成功回执只针对这个精确 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)
相关推荐
WuKongIM 禁言插件入口与在途验证:从"静默拒绝"到显式 reason=25 的错误契约
WuKongIM 禁言插件入口与在途验证:从"静默拒绝"到显式 reason=25 的错误契约 本文基于 WuKongIM 仓库中 2026 09 24 的验证
即时通讯后端WuKongIM 发送准入诊断:权限指标证据补齐契约与三节点重跑验证解析
WuKongIM 发送准入诊断:权限指标证据补齐契约与三节点重跑验证解析 本文基于 WuKongIM 仓库中的压力诊断记录 docs/reports/2026
即时通讯后端WuKongIM MQTT Channel Leader 故障转移:写栅栏下的 Replay 恢复与准入重建
WuKongIM MQTT Channel Leader 故障转移:写栅栏下的 Replay 恢复与准入重建 导读 本文基于 WuKongIM 仓库的 docs
即时通讯后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考