- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
本文基于 WuKongIM 仓库内的迁移诊断报告《消息协议字段:排除 CMD、去重后的影响》整理并扩充。该报告记录了 2026-09-07 在
codex/v2-v3-migration工作树中对三个真实停机源节点镜像执行的只读扫描、协议字段影响统计与原生行为复现实验。文中所有结论均以仓库内报告、wkcli migrate实现源码与原生存储/复制代码为依据,可作为理解 v2→v3 离线迁移中消息协议兼容边界、规划dedupe-plan报告与 RedDot 保留修复的参考资料。
背景:为什么需要评估"排除 CMD、去重后的消息协议字段影响"
WuKongIM 从 v2 集群离线迁移到原生 v3 集群时,迁移工具wkcli migrate(见 migrate 命令实现)只读取停机状态下的源数据库,不修改 v3 存储逻辑,也不新增字段丢弃规则。迁移前的关键问题不是"有多少条消息",而是:当按已批准的策略排除了 CMD 消息、流消息并按规则去重之后,剩余保留消息中还有哪些字段是原生 v3 读取/复制路径无法承载的。
原文档明确界定了本轮的调查范围:
- 暂缓插件问题,本轮只继续检查消息;
- 不修改 v3 存储逻辑,不新增字段丢弃规则;
- 用户此前已批准排除旧流分片/元数据、CMD,以及按既定规则去重、映射序号。
在此基础上,报告回答了三个问题:dedupe-plan报告如何统计字段影响?真实停机备份中保留消息还剩哪些协议字段阻断?原生存储路径对 RedDot / StreamNo 的真实行为是什么?
dedupe-plan报告升级:按节点输出protocol_impact
dedupe-plan是wkcli migrate的只读规划命令,它在独立工作空间中生成 SHA-256 绑定的 JSONL 清单,只做影响规划,不生成 PREPARED、转换完成标记或目标数据。本轮将报告升级到 v3,为每个节点新增protocol_impact统计。
统计口径(严格继承自原文档)
- 在已批准的 CMD 排除、重复消息候选选择之后,分别统计保留消息和排除消息的字段;
- 同一条消息只在"不兼容消息数量"中计一次,它的各字段分别计数(即一消息多字段时消息数不重复累计);
- 每个字段最多保留三条不含正文/凭据的样本;计数不依赖样本截断;
- 节点汇总进入 JSONL 和 SHA-256,重跑具有确定性;
- 使用新的
:dedupe-v3工作空间身份,旧工作空间不能原地复用; - 插件不参与本命令的业务放行;退出码 0 只表示影响规划完整且无未决去重冲突,不代表字段已经兼容。
源码中的统计结构
protocol_impact的数据结构定义在 dedupe_types.go,其核心字段与报告口径一一对应:
// DedupeProtocolImpact separates retained-field blockers from rows omitted by // the requested policy. Counts are physical rows per node, not unique replicas. type DedupeProtocolImpact struct { Retained uint64 `json:"candidate_retained"` RetainedUnsupported uint64 `json:"candidate_retained_with_unsupported_fields"` RetainedFields map[string]uint64 `json:"retained_fields"` OmittedUnsupported uint64 `json:"candidate_omitted_with_unsupported_fields"` OmittedFields map[string]uint64 `json:"omitted_fields"` // Samples keeps at most three payload-free retained examples per field. Samples map[string][]DedupeMessage `json:"retained_samples"` }从实现上看:
RetainedFields/OmittedFields按字段名分别累计,字段计数不依赖样本条数;Samples每个字段最多保留三条不含正文/凭据的样例,用于人工核对而不泄露消息内容;- 报告结构
DedupeReport(同文件 L91-L107)以CommandExitCode()决定命令退出码:只要ScanComplete为假或存在Unresolved未决冲突,退出码即为 1;退出码 0 仅表示规划扫描完整。
执行流程与工作空间身份
dedupe-plan的实际执行入口在 internal/app/migration/app.go,工作空间身份在计划摘要上追加:dedupe-v5(原文档记录时点该身份为 v3,后续因 RedDot 修复与报告演进升级到 v4、v5)。核心扫描逻辑在 internal/usecase/migration/dedupe.go,逐节点读取停机源的 Message 主行,先封存文件清单摘要,再对id与client两类去重键分组(L126-L130),随后统计每频道的候选删除与保留及序号影响。
关键的一处实现细节在 dedupe.go 的 L298-L330:被 CMD 或流策略排除的消息计入Omitted*统计,而未被排除的候选保留消息计入Retained*统计,两者分开计数,确保"排除策略本身"不会污染"剩余保留字段阻断"的判断。
真实停机备份扫描结果
报告使用本机已经核对过的三个源节点镜像(1001 / 1002 / 1003),读取时仍进行停机文件清单和一致性检查,所有节点数据摘要均与先前完整捕获相同。扫描结果如下:
| 源节点 | 原消息物理行 | 排除 CMD | 排除重复项 | 候选保留 | 保留且有 RedDot | 保留且有 StreamNo |
|---|---|---|---|---|---|---|
| 1001 | 335660 | 1641 | 247 | 333772 | 333112 | 15 |
| 1002 | 337020 | 1660 | 248 | 335112 | 334452 | 15 |
| 1003 | 337021 | 1660 | 248 | 335113 | 334453 | 15 |
报告对这张表给出了三条必须遵守的解读约束:
- 这些是每节点物理候选,不能把三份副本相加当成最终业务消息数。本步骤没有重新完成权威副本选择,不同节点的数量差异不在这里被擅自合并;
- 未决去重冲突为 0,15 条流主消息在每个节点均未被去重或 CMD 策略排除;
- 保留消息当前仅发现
red_dot、stream_no两类协议字段阻断;sync_once仅存在于已排除的候选中;每节点同时有两种字段的主消息为一条,字段数量不能直接相加当成不兼容消息数量。
扫描的资源与完整性证据
- 运行耗时 106.787 秒,峰值 RSS 212992000 字节(203.125 MiB),运行参数
GOMAXPROCS=2、GOMEMLIMIT=768MiB; - 原文档明确注明:这只是约 1.7 GB 源镜像的诊断扫描,不能作为 100 GiB / 四小时完整迁移性能验收;
- 明细 SHA-256:
494ca7d75572ce93e2b362065442ace22650d280508c76b7aac2d0fc6b0badda; - 私有证据位于
tmp/server-rehearsal-20260907/messages-34/。
15 条流主消息的实际内容
扫描显示每个节点恰好有 15 条保留消息带 StreamNo,且全部为流主消息。报告从已封存的原始行重新取出45 个物理副本,逐行对照本轮摘要,结论如下:
- 共 15 个不同 MessageID,同一消息的三个副本字段摘要相同;
- 全部
Setting=2,即旧流标记开启(对应源码中的SettingStream = 1 << 1,见 pkg/protocol/frame/setting.go); - 10 条原始主消息正文为零字节;4 条为 39 字节;1 条为 108 字节;
- 保留这 15 条主消息,不会把已经获准排除的流分片内容重新合并进正文——本轮没有读取或恢复分片正文,也没有把空主消息替换成编造的内容。
由此得出一个重要结论:"清空 StreamNo"本身并不等于转换成普通消息,流标记仍然存在。若以后选择普通消息形式,还需明确批准该标记的转换及其业务影响。
原生行为复现:ReadLog → ApplyFollower 会丢失旧消息字段
为了证明"底层列存在、写入成功"不能替代完整读取/复制兼容性验证,报告新增了隔离测试TestNativeFollowerRoundTripLosesLegacyMessageFields,分别使用合成的 RedDot 消息与带 Setting=2/StreamNo 的消息。复现步骤严格按五个阶段进行:
- 迁移工具按现有规则拒绝该消息;
- 实验绕过迁移准入,仅向临时数据库直接写原有格式的完整消息行;
- 原生数据库读取确实能返回 RedDot/StreamNo,写入和原有 version-1 proposal 均成功——但报告强调这不是迁移通过的证据;
- 经原生 Channel
ReadLog→ApplyFollower写入另一临时数据库后,MessageID、序号、正文和 Setting 保留,但RedDot 变为 false,StreamNo 为空; - 原生 Channel Message/Record DTO 也不承载这两个字段;历史 API 读取适配器只从其中设置 SyncOnce,没有补回历史 RedDot。
测试通过,证明不能以"底层列存在、写入成功"替代完整读取/复制兼容性验证。报告同时注明:这是存储适配器实验,不是一次新的真实集群故障恢复验收,也没有修改该路径;原生历史冷启动恢复问题继续作为独立未解决事项。
源码侧的印证
上述丢失行为可以从原生消息结构中得到印证。ReadLog/ApplyFollower对应的通道日志与复制路径使用 pkg/channel/types.go 中的Record结构承载复制数据,其字段仅包含ID/Index/Epoch/Setting/Expire/FromUID/ClientMsgNo/ServerTimestampMS/SyncOnce/RedDot/Payload等,没有 StreamNo 字段;存储适配器 pkg/channel/store/channel_adapter.go 用encodeDBCompatibleFramerFlags把协议标志压缩进一个掩码字节(RedDot 对应flags&2),历史上 RedDot 位在部分路径上并未完整贯通。这正是"底层列存在、写入成功,但完整读取后字段丢失"的机制来源。
RedDot 原值保留与 v3 通用修复
原文档开头即附有后续更正:用户已决定直接保留 RedDot,此前"RedDot 仅留档"的建议已撤回。完整的修复过程记录在配套报告 RedDot 验证报告 中,摘要如下:
- 通用 v3 修复在独立
codex/red-dot-preservation工作树开发(基线223d1eba2f7228806cfd2842d86dd60f9c7583f4),同一补丁接入迁移工作树; - 补齐 SendCommand → durable Message → Channel Message/Record → 已有
FramerFlags掩码 0x02 的传递链,以及持久化读取、复制、恢复、普通历史 API 返回路径; - 复用已有存储位,消息键、列、行编码、兼容消息格式、proposal/entry v1 摘要均未改变,未新增 v2 专用存储路径,未调整 v3 序号/追加校验规则;
- Channel RPC 升级以携带 RedDot,并对会丢失 true 值的旧版本编码明确报错;Quorum Exchange 同步升级且不回退;
- 迁移工具不再把 RedDot 视为不兼容字段;其他未支持字段仍阻止完成;
dedupe-plan报告/工作空间身份随之升级,避免复用旧判断结果; - 目标集群各节点必须统一使用修复版本,已有 v3 的升级需要停流量并一起升级;源 v2 不需要修改、启动或升级。
需要特别强调的是:现有 v1 proposal 摘要不包含 RedDot,本次也没有改变其历史定义,因此摘要相等不能单独证明该位相等;迁移验收仍须独立比较完整消息字段。该修复不改变 v3 未读计数算法,也不处理 StreamNo 或 CMD sync 的其他字段。
验证与边界
自动化测试
- 迁移用例全包通过(5.850 秒),v2 适配器全包通过(111.909 秒);组合根、CLI 与命令包编译通过;
- 新测试最初使用错误的存储接收者方法导致 v3 测试包编译失败;已改为该原生类型现有的
GetMessageBySeq,并让带 Setting=2 的测试 proposal 使用对应的 Setting 值;最终 v3 适配器全包通过(6.145 秒),包含流标记实验; - 定向测试覆盖:CMD/重复项与保留字段分开计数、两字段消息不重复计数、多副本隔离、样本上限、确定性重跑、CLI 接线和工作空间隔离;
- 275 个既有原生文件继续与基线
ab1ced72450658ba85399b03293ac37c20278721逐字节相同; - 后续 RedDot 修复的验证覆盖 API、节点 RPC、发送契约与运行时、集群适配器、Channel 全子树、Channel RPC、DB 全子树相关单元测试,以及单节点/三节点集群从各入口发送 false/true 后从各入口读取历史的组合测试。
未做的事项(严格准入边界)
本轮没有运行现场插件、改动原始源库、导入现场 v3 或进行业务切换。报告同时把下列事项明确列为未完成验收:
- 三节点原生冷重启历史尾部可见性(已由后续独立通用修复关闭,见配套报告);
- 15 条主消息的 StreamNo/流语义兼容——没有获得清空字段或额外排除的授权;
- 暂缓中的插件完整兼容与其余全量 prepare 准入项;
- 100 GiB / 四小时性能验收、SDK 缓存/游标切换及最终现场全量迁移验收。
待明确的数据策略:保留而不是清除
原文档对数据策略给出了明确的收尾结论,这也是迁移操作中必须遵守的边界:
- 此前用户只批准排除旧流分片/元数据和 CMD,以及按既定规则去重、映射序号,并要求保留 15 条流主消息;该授权不包含自动删除历史 RedDot、StreamNo 或 Setting 中的流标记;
- 此前提出的"RedDot、StreamNo 与流标记仅留档"方案没有获批,现已撤回;
- RedDot 按用户要求直接保留;15 条流主消息继续保留,StreamNo/流标记不得擅自清除;
- 原生历史冷启动问题和插件完整兼容分别保留准入边界,任何迁移决策不得以"底层能写入"作为放行依据。
与整体迁移流程的关系
dedupe-plan只是wkcli migrate完整流程(diagnose→authority→dedupe-plan→prepare→export→import→verify)中的影响规划环节,命令清单见 migrate 命令实现。其输出属于"诊断/规划"而非"验收":
prepare阶段会在源检查开始后可能输出status: "blocked"预检并部分证据,任何插件兼容阻断都会使选择摘要为空并阻止转换/导出;- 实际的 CMD 排除、流消息识别与序号压紧由计划中的
messages策略控制(keep_latest_duplicates/exclude_cmd/exclude_streams/compact_sequences,见 message_transform.go),其中流消息按Setting & (1<<1) != 0 || StreamNo != ""识别,不根据正文猜测业务类型; - 只有
dedupe-plan的字段影响扫描与 prepare/import 的显式策略配合,才能判断哪些字段会成为真正的迁移阻断,哪些已被排除策略消化。
操作者在使用相关命令时,可参考 wkcli migrate 命令文档 中关于工作空间隔离、计划摘要与归档重建的要求:修改任何消息策略都会改变计划身份,必须使用新工作空间和新归档,旧工作空间不能原地复用。
小结
WuKongIM v2→v3 迁移中的消息协议字段影响评估,核心方法论可以概括为四句话:先按已批准策略排除,再统计剩余保留消息的字段阻断;物理副本不合并、字段计数不冒充消息计数;"底层列存在、写入成功"不等于读取/复制兼容;未获授权的字段(StreamNo/流标记)一律保留,不做擅自清除。dedupe-plan的protocol_impact报告把"哪些消息被排除"与"哪些保留消息还带不兼容字段"严格分开,配合 RedDot 通用修复与配套验证报告,为最终的迁移验收提供了可复核、可重跑的字段级证据链。
- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
相关推荐
WuKongIM v2→v3 迁移:消息去重规划(dedupe-plan)与消息序号影响分析
WuKongIM v2→v3 迁移:消息去重规划(dedupe plan)与消息序号影响分析 导读 :本文围绕 WuKongIM v2→v3 离线迁移中的消息去
即时通讯后端WuKongIM v2→v3 离线迁移消息策略:CMD 消息排除、最新重复保留与序号压紧实战解析
WuKongIM v2→v3 离线迁移消息策略:CMD 消息排除、最新重复保留与序号压紧实战解析 本篇技术指南聚焦 WuKongIM 从 v2 到 v3 离线迁
即时通讯后端WuKongIM v2→v3 离线迁移:流消息排除与频道连续序号保序方案
WuKongIM v2→v3 离线迁移:流消息排除与频道连续序号保序方案 导读 本文介绍 WuKongIM v2→v3 离线迁移中的一项关键业务策略: 流消息(
即时通讯后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考