- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
本篇以 WuKongIM 仓库中的验证报告 docs/reports/mqtt-compound-originals/README.md 为主体,完整还原“compound originals”复合读取路径的设计意图与证据边界:它如何把一次恢复计划(recovered plan)加锚定原始页(anchored original page)的元数据读取从 5 次压缩为 2 次,在不引入任何 authority 缓存、不增加 worker、不加通用重试的前提下保持全部既有安全契约;并基于同一报告目录内的产物 JSON、RED 测试日志与 E2E 测试源码,给出可逐条对照、可复现的验证细节。
一、验证目标:一次复合读取,两次新鲜元数据读取
在 MQTT 会话的恢复与回放路径中,消费者需要证明一段“有界原始页”(bounded original range)与其对应的恢复计划(plan)在一致的新鲜 serving authority 下成立。改造前,这条路径被拆成多个独立 RPC(计划读取、内容读取等),整体链路要做5 次全新的元数据读取(fresh metadata reads),每一次都意味着一次完整的集群元数据一致性校验开销。
本次验证针对候选修订838222fcfc4f4a94ed957056c6802dcf9b0ef749,基线为437f7297c0d18bb2083f41b6e75d4bc838afb89f(以下记作437f7297c)。报告的核心结论是:
- 改造后的 routed Service 链路,对“一次恢复计划 + 一个锚定原始页”只做2 次fresh 元数据读取:规划前一次、replica refresh 与内容读取之后再校验一次;
- 改造前的真实 Service 回归测试会因 5 次读取而失败(RED 证据见下文),即该测试在旧实现上必然失败、新实现上必然通过;
- 外层 placement 校验、最终 Owner/receive 权限检查、native committed propagation、既有的四读者准入(four-reader admission)全部继续生效;
- 显式“三不”约束:不新增 authority 缓存、不增加 worker、不引入通用重试。
这个约束值得展开:读取次数的压缩如果靠缓存元数据来完成,就会破坏“每次交付决策都基于最新 authority”的 fail-closed 语义;报告把这两者明确对立,并在源码层面验证(见第二节)。
二、实现层面:readMQTTOriginals的两次新鲜读取
候选实现的核心入口在 pkg/cluster/channels/mqtt_originals.go。函数readMQTTOriginals的调用序列是:
- 对整个读取应用 5 秒超时(
context.WithTimeout(ctx, 5*time.Second)),并校验请求有效性; - 第一次新鲜读取:
s.mqttRepairAuthority(ctx, q.Request)解析当前 placement,并校验 leader 路由(非 leader 直接返回ErrNotLeader); - 若本机即 leader:执行
planner.PlanMQTTReplay生成计划,校验计划的 source 覆盖(StartAfter/CommittedThrough与消费者边界的关系)、锚点存在性与锚点Through >= AccountedThrough;随后请求一次replicaCommitRefresh(有界的 native committed 提示,不是证明); - 通过四读者信号量
mqttConsumerReads做准入,拿不到令牌则返回ErrBackpressured——这就是“既有四读者准入仍然生效”的落点; - 用存储 reader 的
ReadMQTTReplayAnchor读取锚定页;非 leader 则走forward.ForwardMQTTOriginals转发; - 第二次新鲜读取:
s.recheckMQTTRepairAuthority(ctx, q.Request, m)再校验一次 authority,并与读取前的解析结果比对; - 最后
result.ValidFor(q)联合校验计划与页面的关联性,才放行返回。
测试 pkg/cluster/channels/mqtt_originals_test.go 中的TestMQTTOriginalsShareFreshAuthorityAcrossPlanAndContent把这条预算固化为断言:
got, e := readOriginalsForBudget(context.Background(), s, q) require.NoError(t, e) require.Equal(t, want, got) require.Equal(t, 2, m.calls, "one fresh read before planning and one after refresh/content") require.Equal(t, 1, refreshes) require.Equal(t, 1, st.calls)其中m.calls计数元数据读取、refreshes计数 replica refresh、st.calls计数存储页读取。该测试的readOriginalsForBudget辅助函数还会先探测 Service 是否实现了ch.MQTTReplayOriginalReader;旧实现没有该接口,会退化为“先PlanMQTTReplay再ReadMQTTReplay”的两段式真实链路,从而在 5 次读取上失败——这正是 RED 日志 docs/reports/mqtt-compound-originals/read-budget-red.txt 记录的现象:
--- FAIL: TestMQTTOriginalsShareFreshAuthorityAcrossPlanAndContent (0.00s) Error: Not equal: expected: 2 actual : 5 Messages: one fresh read before planning and one after refresh/content请求/结果的边界绑定
请求结构定义在 pkg/channel/mqtt_originals.go。MQTTReplayOriginalRequest除了普通的Request(通道 ID、epoch、路由代次、页范围)外,额外绑定两个边界:
StartAfter:消费者的固定边界,注释明确“never a metadata cache hint”(绝不能当作元数据缓存提示);AccountedThrough:完整的已记账前沿,必须被捕获的 committed 锚点覆盖。
Valid()强制StartAfter < Range.From且Range.Through <= AccountedThrough;结果的ValidFor则把计划、页、锚点三方关联起来逐字段核对(计划 source 覆盖、锚点 Through、页前后边界与计划 source/锚点的一致性)。注释同时声明:该结果“grants no consumer mutation, permission or source-release authority”——读取本身不授予任何准入、网络发送或 source 退役权限,外层检查依旧独立执行。
RPC 104:RPCChannelMQTTOriginals
跨节点转发走新增的 RPC 服务号104。从 pkg/cluster/net/ids.go 的 iota 定义顺序看,RPCChannelMQTTOriginals紧随RPCChannelMQTTConsumerRead等既有服务之后;pkg/cluster/net/ids_test.go 把它固化为常量断言:
if RPCChannelMQTTOriginals != 104 || transportServiceAlias(RPCChannelMQTTOriginals) != "channel mqtt originals" || !isForegroundChannelMutationService(RPCChannelMQTTOriginals) { ... } if !s.serviceOptions(RPCChannelMQTTOriginals).CancelRunning { ... }即 104 是前台(foreground)Channel 服务,且必须支持运行中取消。其编解码在 pkg/cluster/channels/mqtt_originals_rpc.go:
- 请求 magic
WMOQ\x01、应答 magicWMOR\x01,请求头固定 21 字节(magic + 两个大端 uint64,分别是StartAfter与AccountedThrough),请求体复用既有 plan 编解码并做Leader绑定——mqttOriginalsForwardRequest.Leader的注释说明其用途是“防止转发请求跟随新的 serving node 漂移”; - 应答采用“外层精确回显”(exact echo):回显完整请求并嵌套 plan 与页两段负载,外层 echo 同时绑定消费者边界与已记账前沿;
- 读取侧
readMQTTOriginalsPart只返回有界 frame 视图,页消息字节由内层 consumer decoder 所有,避免对整页再拷贝一次;应答长度超过mqttReplayRPCMaxReplyBytes直接判非法。
fail-closed 的全阶段测试
pkg/cluster/channels/mqtt_originals_test.go 的TestMQTTOriginalsFailClosedAcrossEveryStage以 22 个故障模式逐一注入(fence 稳定/续期/清除、路由/leader/ISR/状态/写栅栏在读取前后变化、元数据错误、各阶段取消与错误、坏计划、缺锚、短锚、坏页、越界 StartAfter/前缀、存储错误、背压等),断言每一处都按错误拒绝而不是“带病通过”。对应的 RED 日志 docs/reports/mqtt-compound-originals/storage-error-red.txt 记录了实现前store_error子测试的失败:
--- FAIL: TestMQTTOriginalsFailClosedAcrossEveryStage/store_error (0.00s) Error: An error is expected but got nil.上层调用点:窗口准入不变
会话层的调用入口在 internal/usecase/mqttsession/originals.go 的readAnchoredOriginals:它先用metadata.ResolveChannelMetaFresh做一次 placement 校验(outer placement check),若 channels 实现了ReadChannelMQTTOriginals就走复合路径,否则退化为 plan + consumer read 两段式;读页之后再ResolveChannelMetaFresh一次,并比对 placement、MQTTReplayCopyAuthority与WriteFence是否变化,任何不一致返回ErrEvidence。函数注释明确:“Callers bracket it with their owner and receive-permission checks. This read grants no admission, network-send or source-retirement authority.”
TestWindowAdmissionCompoundOriginalsRetainOuterAuthorityAndOwnerChecks(internal/usecase/mqttsession/originals_compound_test.go)验证了报告所称的“外层检查仍被强制”:测试桩在复合路径下若发生任何独立 plan 调用或独立内容调用都会直接t.Fatal,并逐一注入 success / invalid_result / placement / permission / owner / canceled / dependency_error 七种情形,断言外层 authority 与 Owner 检查的独立性(RED 日志 docs/reports/mqtt-compound-originals/read-budget-red.txt 也保留了该测试在实现前compound path must not make a separate plan call的失败记录)。
三、规模验收闸门:TestGroupScaleDeliveryAndChurnRetirement如何工作
普通复现用的验收测试位于 test/e2e/mqtt/scale/scale_test.go,同目录的 test/e2e/mqtt/scale/AGENTS.md 与规格 docs/specs/mqtt-scale-acceptance.md 描述其行为契约。测试在单节点集群上运行,配置覆盖固定为:
| 配置项 | 值 | 来源 |
|---|---|---|
WK_CLUSTER_HASH_SLOT_COUNT | 256 hash Slots | 源码 overrides |
WK_CLUSTER_INITIAL_SLOT_COUNT | 12 个初始 Slot 组 | 源码 overrides |
WK_GATEWAY_TOKEN_AUTH_ON/WK_MQTT_ENABLE | true | 源码 overrides |
| 成员数 / 在线连接 / 初始发布 / churner / 轮数 | 由环境变量控制 | 下表 |
规模环境变量与默认值(envInt读取,缺省即报告所用值):
WK_E2E_MQTT_SCALE_MEMBERS:默认 100000(成员,member 0 为发送者,要求 conns < members);WK_E2E_MQTT_SCALE_CONNECTIONS:默认 500(持久 Paho 订阅者);WK_E2E_MQTT_SCALE_MESSAGES:默认 20(初始发布数);WK_E2E_MQTT_SCALE_CHURN:默认 200(churn 订阅者,要求不超过 conns);WK_E2E_MQTT_SCALE_ROUNDS:默认 3(UNSUBSCRIBE/SUBSCRIBE 往返轮数);WK_E2E_MQTT_REPORT_DIR:产物输出目录;WK_E2E_MQTT_SCALE_PROFILE:置 1 才开启 debug profiling,验收运行必须不设置(报告中的运行均profiling_enabled: false)。
测试分阶段推进并逐阶段计时:provision_members(经公开 HTTP 以 5000 为一批创建/添加群成员)→provision_tokens→connect_subscribe(并发 32 建连并订阅 QoS 1 主题wk/v1/groups/<base64url(group)>/messages)→fanout(WKProto 认证发送者逐条SendPacket并等待 ACK,每条等全部订阅者收齐)→idle_measurement→churn(每轮 200 个客户端先 UNSUBSCRIBE 再 SUBSCRIBE,均校验 ACK reason)→post_churn_delivery(再发 1 条并等齐 21 条)→retirement(等待wukongim_mqtt_consumer_events_total{event="retired"}增量不少于churners*rounds = 600)→verify。
几个关键的防放水设计(对应报告中“assertions unchanged”):
awaitAll注释直言“never accepts fewer”:require.Eventually直到每个订阅者都持有want条回执才返回;若某持久订阅者中途被服务端关闭(Client.Done()),直接判定失败,不允许靠“客户端已断开”蒙混过关;- 发送者每 15 秒发真实 PING/PONG 心跳(Gateway 在 3 分钟无入站流量时会断开连接),心跳错误即失败,计数记入产物(
wkproto_heartbeat_count);绝不重连、绝不更换消息身份; - 最终
verify对每个订阅者逐条核对:不重复、不缺失、不乱序(seq严格递增)、身份与 SEND ACK 的MessageID一致(wrongID)、无意外消息(extra),且消息总数恰为messages+1; - 空闲代价闸门:
idle_measurement阶段在一个完整 10 秒刷新窗口内读取wukongim_slot_read_barrier_duration_seconds_count{result="ok"},断言每订阅者每秒的 Slot 读取屏障速率严格小于 10/client/s——这就是报告中“quiet barriers are 4.149/client/s, below the unchanged ten/client/s ceiling”的出处(scale_test.go 中require.Less(t, idleRate/float64(conns), float64(10), ...)); - 失败时落盘两个产物:
mqtt-scale-failure.json(含failed_phase、confirmed_members、phase_ms)与mqtt-scale-delivery-failure.json(有界回执直方图 + 独立的 Paho closed/incomplete 计数),成功后落盘mqtt-scale.json。
四、三次运行的结果:一张对照表与背后的产物数据
报告给出的对照表(全部数据均可在同目录产物中复核):
| 运行 | 成员 / 客户端 / 初始发布 | 初始 fanout | 最终判定 |
|---|---|---|---|
基线437f7297c | 2,000 / 500 / 20 | 三分钟回执等待失败;延迟快照有 9,862 条回执且全部客户端仍在线 | churn 之前 FAIL |
| 候选(小规模) | 2,000 / 500 / 20 | 151.380s;全部客户端收齐 20 条 | 在 0 基轮次的第 2 轮(第三轮)unsubscribe 处 FAIL;一次 conflict closure |
| 候选(全规模) | 100,000 / 500 / 20 | 151.982s;最终身份/顺序断言全过 | PASS,含 post-churn 交付与 600 次退役 |
基线为何“FAIL before churn”
docs/reports/mqtt-compound-originals/baseline-2000-failure.json 显示failed_phase: "fanout",阶段耗时只记到provision_members/provision_tokens/connect_subscribe,fanout 本身从未完成(180 秒的awaitAll窗口耗尽)。其回执快照 docs/reports/mqtt-compound-originals/baseline-2000-receipts.json 是一个延迟失败快照(deadline 之后采集,不是精确时间戳样本):
"closed_clients": 0, "incomplete_clients": 98, "receipt_histogram": { "16": 1, "17": 9, "18": 19, "19": 69, "20": 402 }即 500 个客户端中 402 个收满 20 条、98 个缺 1~4 条,累计 9,862 条回执,且没有客户端被服务端关闭——排除“客户端掉线”这一解释,指向服务端 fanout 吞吐问题。
候选小规模失败:一次未定位的瞬时冲突
docs/reports/mqtt-compound-originals/candidate-2000-failure.json 中failed_phase: "churn",fanout 已完成(151,380 ms,全部客户端 20 条回执,见 candidate-2000-receipts.json 的"20": 500直方图;另有 1 个客户端处于 closed 状态但回执完整)。provenance.json 记录了唯一一次 closure 观测:
"fixed_closure_observation": { "operation": "unsubscribe", "reason": "conflict", "count": 1, "round_zero_based": 2 }报告对此的定性非常克制:这是一个未定位的(unlocalized)瞬时 unsubscribe conflict。由于失败发生在 churn,post-churn 交付、最终身份/顺序断言与退役断言都“未被触及”。现有 stage 计数器无法区分三种候选成因——订阅 CAS 耗尽、child 变化、或 drain/projection 冲突;因此下一步必须先定位终止阶段,才允许修改重试或准入行为。报告的结论段明确警告:全规模一次通过并不证明该竞态已修复,也不为更宽的 MQTT 失败/恢复行为提供资格证明(qualification)。
候选全规模通过:完整阶段数据
docs/reports/mqtt-compound-originals/candidate-full.json 是 PASS 运行的完整产物,阶段与断言数据(对照 candidate-full-phases.txt 的逐阶段日志):
| 阶段 | 耗时 |
|---|---|
provision_members | 119,825 ms |
provision_tokens | 1,185 ms |
connect_subscribe | 17,983 ms |
fanout(20 条 × 10 万成员的首发) | 151,982 ms |
idle_measurement | 10,031 ms |
churn(200 × 3 轮) | 27,005 ms |
post_churn_delivery | 30,611 ms |
retirement | 25 ms |
| 测试总时长 | 370.84 s |
交付质量断言全部为零:duplicates: 0, missing: 0, reordered: 0, wrong_identity: 0, unexpected: 0,retired_delta: 600(恰等于200 × 3的下限);每订阅者空闲屏障速率4.149390.../client/s,低于 10/client/s 上限;投递延迟 p50 = 101,389 ms、p99 = 151,589 ms、max = 151,710 ms。测试日志同时打印idle Slot barriers/s=2074.7 per subscriber/s=4.15,与产物一致。
五、证据边界:这份报告“不算”什么
报告对自身局限性有明确段落,写作引用时必须连同继承,不能只挑 PASS 的一面:
- 这是一次本地顺序比较(one local sequential comparison),没有受控的主机噪声估计;provenance.json 的
shared_host_limit记录了依赖准备期间观察到的其他 Go 构建/语言服务器活动未被停止; - 基线从未完成 fanout,因此不存在基线完成延迟,也就不存在任何百分比加速——报告拒绝给出这种数字;
- 带插桩的历史测量与这些“ordinary runs”是相互独立的;
- 失败回执直方图是延迟快照,不是带精确时间戳的 deadline 采样;
- 早期一次未成功的启动没有保留终端 stderr,被排除在证据之外(
unarchived_launch_limit); - 两个二进制的内嵌 VCS 元数据都报告主 checkout 的修订
1982a56b...,因此VCS 元数据不被用作源码证明;源码证据改为显式的 worktree 构建命令、overlay 文件哈希(候选变更了 8 个生产文件,基线 overlay 恢复 6 个文件并排除 5 个新增文件)与“候选独有符号存在于候选、缺席于基线”的差分。
六、配套验证与完整复现
复现命令
报告给出的普通复现步骤(在候选修订的干净 worktree 上):
GOWORK=off go build -o /tmp/wukongim-mqtt-compound ./cmd/wukongim WK_E2E_BINARY=/tmp/wukongim-mqtt-compound \ WK_E2E_MQTT_SCALE_MEMBERS=100000 WK_E2E_MQTT_SCALE_CONNECTIONS=500 \ WK_E2E_MQTT_SCALE_MESSAGES=20 WK_E2E_MQTT_SCALE_CHURN=200 \ WK_E2E_MQTT_SCALE_ROUNDS=3 WK_E2E_MQTT_REPORT_DIR=/tmp/mqtt-compound-reproduction \ GOWORK=off go test -p 1 -tags=e2e ./test/e2e/mqtt/scale \ -run '^TestGroupScaleDeliveryAndChurnRetirement$' -count=1 -timeout=15m -v规则补充(来自 README 与 provenance.json 中runs数组记录的原始命令):
- 保持
WK_E2E_MQTT_SCALE_PROFILE未设置; - 基线在
437f7297c的独立干净 worktree 中运行,顺序对(pair)使用 2,000 成员; - 两个负载不得并发运行;每次尝试必须保留自己的 verdict/artifact,通过尝试不能替代失败尝试;
- 基线构建使用
go build -overlay=...外部 overlay 仅用于恢复/排除候选 Go 文件、精确重建其产品源码;仓库依赖与配置文件未被改动(候选构建环境见candidate_harness_environment与dependency_preparation:三个固定依赖以原始代理档案形式拷入任务私有缓存,并经go mod download对go.sum校验)。
其余验证日志
README 的证据清单在同目录下全部落盘,provenance.json 的validation与artifact_sha256给出各自的命令、结果与 SHA-256:
- related-race.txt:对
pkg/channel、pkg/cluster/channels、pkg/cluster/net、internal/usecase/mqttsession的相关竞态测试go test -race,全绿(mqttsession 包耗时约 40 s); - final-race.txt:针对
Test(MQTTOriginals|MQTTReplayNodeForegroundGates|MQTTAnchorNodeForegroundGates|WindowAdmissionCompound)的最终-race运行,全绿; - app-integration.txt:
internal/app三个集成测试(三节点 Group source preparation 恢复、Paho 单节点连接交付、退订后收件箱保留 exchange)通过; - flow-contracts.txt:
go run ./scripts/flowcheck --mode check,87 个 FLOW 文件 87 compliant、0 invalid、11 条既有软长度警告。
七、小结
这份验证的价值不在“跑过一次 10 万成员”本身,而在它把一次读取预算的压缩(5 → 2 次 fresh 元数据读取)与一整套可机器核对的证据链绑在一起:RED 日志证明旧链路确实在 5 次读取上失败,TestMQTTOriginalsFailClosedAcrossEveryStage的 22 个故障模式保证压缩没有换掉任何 fail-closed 行为,RPC 104 的请求/回显框架把消费者边界与已记账前沿一起跨节点传递,规模 E2E 的“从不接受更少回执”的等待逻辑与 10/client/s 空闲闸门封死了放水路径。同时它诚实保留了两个负面结论:2,000 成员规模下仍有未定位的瞬时 unsubscribe conflict 竞态,以及单次本地顺序比较不构成容量资格或加速比证明。对后续维护者的直接指引是明确的——先定位该竞态的终止阶段,再谈任何重试或准入改动。
- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
相关推荐
Suricata 告警(Alert)解读实战:从规则元数据到 Eve JSON 取证
Suricata 告警(Alert)解读实战:从规则元数据到 Eve JSON 取证 当 Suricata 发出告警时,判断它意味着什么——是真实攻击、无关注入
网络安全go-openapi/validate 基准测试解读:验证 Kubernetes Swagger API 时如何将内存分配从 6000 万次压降到 1700 万次
go openapi/validate 基准测试解读:验证 Kubernetes Swagger API 时如何将内存分配从 6000 万次压降到 1700 万
云原生容器运行时虚拟化容器编排OpenMontage 前端优化实战:用 Index Map 取代重复 find(),把 1M 次查询压缩到 2K 次
OpenMontage 前端优化实战:用 Index Map 取代重复 find ,把 1M 次查询压缩到 2K 次 当一段代码需要反复按同一个键去数组中查找目
人工智能AI Agent音视频媒体生成工作流自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考