- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
导读
本文解读 WuKongIM 在 v2 存量数据向 v3 存储迁移过程中一次关键的边界修订:当 v2 历史数据与 v3 原生存储契约发生冲突时,项目选择"为 v3 保留完整契约,而不是为 v2 数据修改 v3 存储逻辑"。文章以docs/superpowers/reports/2026-09-06-v2-migration-existing-v3-contract.md为骨架,梳理了撤回与保留的实现范围、消息时间与身份映射规则、真实源诊断数据、验证矩阵与集成测试结果,并结合仓库源码说明迁移工具(磁盘工作空间、只读 v2 扫描器、迁移计划)的底层实现。读完本文,你将理解 WuKongIM 离线迁移的"契约优先"策略,掌握在message_id唯一性、空频道副本恢复等冲突场景下项目如何选择"保持失败状态而非改写 v3 恢复逻辑",以及wkcli migrate各阶段命令(diagnose / authority / prepare / export / import / verify)的边界语义。
背景:为什么需要一次"迁移边界修订"
本次修订的直接动因是用户提出的明确要求:不要为了 v2 数据强行改变 v3 存储逻辑。截至本记录形成时,尚未完成生产迁移,也未导入或切换现有服务器——也就是说,整个修订是在"迁移尚未落地"的前提下,对迁移工具与 v3 存储之间的边界做一次收敛。
记录中给出了两个关键提交作为参照:
- 开发开始前的 v3 基准:
ab1ced72450658ba85399b03293ac37c20278721,一切比较以此为准; - 迁移分支 HEAD:
f886aab91ea9308bb854a84b6cb9ff02c1fac867,本次修订尚未提交到该分支。
这条"先划边界、再谈迁移"的路径,与仓库中迁移模块的总体设计一致:internal/infra/migrationv2的包注释明确写着"读取固定的原始 v2 存储格式,绝不起动旧服务器、也绝不向其数据目录暴露写路径"(见 reader.go),迁移全程被约束为只读源 + 离线生成目标。
撤回与保留:本次修订划定的边界
已撤回的实现
为了让 v2 数据"看起来能直接放进 v3",迁移分支曾引入一批面向兼容的改动,本次修订全部撤回:
- 空 ID / 类型 0 ChannelInfo 的特殊原生行及查询例外:不再为"空身份"频道在 v3 存储中制造特殊写入路径和查询豁免;
- 完整协议字段 proposal version 2 与保留前缀 version 3:撤回对应的消息读写、跨节点 codec、摘要(digest)、恢复和修复分支;
- 迁移期间加入的相关运行时修复:随上述实现一并退出本次变更。
撤回后,以下模块被恢复为 v3 基准提交(ab1ced7...)的原始形态:
pkg/channelpkg/cluster/channels- 既有
pkg/db/message的读写/恢复文件 pkg/quorumlog/proposal.go
恢复之后,v3 的消息主键、唯一索引、连续序号、摘要版本和复制规则保持原样——这正是"不动 v3 存储逻辑"的直接落地。
保留的工具能力
需要强调的是,撤回并不等于砍掉迁移工具本身。以下与迁移配套的能力被明确保留:
- 工具需要的原有消息 codec 封装;
- 只读完整行 / 摘要链检查;
- 磁盘工作空间(disk workspace);
- 原有事件表的原子导入与只读分页;
- 初始化快照写入、启动完成标记核对与已有消息 ID 分配器下限;
- 事件同步接口。
记录特别说明:这些保留项"不替代 v3 原有消息存储与恢复机制",因此不宣称整个服务代码没有任何改动——改动被严格限定在迁移工具自身,而不是 v3 的存储与恢复路径。
从源码看,磁盘工作空间正是 pkg/db/transfer/spool.go 中的Spool:它用带IDENTITY标记文件(immutable migration identity)的目录承载离线迁移的排序/连接中间数据,OpenSpool只恢复与身份完全一致的工作区,绝不接纳未标识的非空目录,同时以engine.Open打开底层引擎,maxBytes限定每批行键值总字节上限(见 spool.go)。
消息写入与时间映射规则
在工具侧,v2 消息以原有 version 1 格式写入迁移输出,核心映射规则如下:
- 源秒级时间精确映射到
ServerTimestampMS:即ServerTimestampMS = Timestamp × 1000。这一点在源码 internal/infra/migrationv2/message.go 的DecodeMessage中有直接实现:msg.ServerTimestampMS = int64(msg.Timestamp) * 1000; - 冗余旧时间列按现有 v3 副本重建形式置零;
- 原 ID、序号、正文不改写:
DecodeMessage会校验MessageId、MessageSeq与主键后 8 字节一致,并校验频道哈希(channelHash(id + "-" + type))与row.Owner、键中段是否匹配,任何身份不一致都会直接报错(见 message.go)。
必须阻止转换的字段
由于 v3 现有存储路径无法完整保留下列字段语义,工具对包含这些字段的消息必须阻止转换,而不是用新格式补齐:
- 非零的
red_dot、sync_once、expire; - 非空
StreamNo、topic等字段; - 非正时间戳;
- 非 1 起始的历史(序号不从 1 开始的频道历史)。
这一点同样体现在源码中:Reader.InspectDedupeMessage先做DecodeMessage,再通过migration.UnsupportedMessageFields(m)收集不可转换的遗留标志(见 dedupe.go),diagnose阶段会对这些字段逐类计数。
业务范围边界:Stream/StreamMeta 是唯一获准排除项
本次修订明确了迁移的业务取舍规则:
- 旧 Stream / StreamMeta 是唯一获准排除的业务范围——它们在计划中通过
exclusions.legacy_stream_storage: true显式授权后,仅保留在带校验和的源归档中(见 cmd/wkcli/internal/migrate/README.md); - 主消息仍参加选择与验证:不允许通过"清空
StreamNo"或"丢弃主消息"来让检查通过——排除的是 Stream 附属表,不是主消息本身; - 原始数据和诊断证据必须保留:不得把不兼容的业务记录当作"管理归档已完成"来处理,从而跳过真实的迁移验证。
真实源诊断:在停机副本上做只读体检
诊断环境
真实源位于主机175.27.245.108:
- 源数据目录:
/root/wukongim_v2_bak,节点1001、1002、1003均保持停机; - 现有目标部署:
/root/wukongim(现有 v3); - 本轮只使用隔离演练目录,未操作现有 v3 数据或服务。
四次关键诊断运行
| 运行 | 说明 | 关键数据 |
|---|---|---|
empty-channel-09 | 曾临时允许空身份做全量预检 | 262.276 秒后在源索引检查报migration spool batch key conflict;峰值 RSS 224,817,152 字节,最低剩余空间 11,534,733,312 字节;无 PREPARED 成功,无目标输出;该尝试使用的存储兼容方案已被撤回,结果仅作为历史诊断证据 |
index-collision-10/11 | 索引冲突扫描 | 因诊断器自身的 50 万项上限退出,不作为数据库损坏结论 |
index-collision-12 | 8,192 项窗口扫描 | 21.976 秒完成;三节点分别扫描 1,031,141 / 1,035,215 / 1,035,221 个预期索引项,窗口内观察到 64 / 58 / 61 次冲突;注意这不是全库去重总数,也不是权威副本选择证明 |
duplicate-detail-13 | 定点读取 | 8.437 秒完成;三个副本均存在同一频道的 seq1093 / 1094,共享 message_id1861220579151880192,除 MessageSeq 外已存字段相同,原 message-ID 索引指向 1094;两条正文 SHA-256 均为6a09506e4e85a673c063c8b51e053370c3ba1f020c3f935c0e053be7d028fbea |
同时核对的一对会话记录,仅CreatedAt/UpdatedAt不同,原频道索引指向较大物理 ID。记录明确指出:不同原版读取接口的去重规则不能自动视作同一业务语义——该会话记录尚未批准合并或舍弃。
诊断的工程约束
诊断过程本身是克制的:
- 远程执行记录确认601 个源文件的大小 / mtime / inode / mode 均未变化,三个现有 v3 容器保持健康;
- 诊断只输出必要数值、字段差异和摘要,不打印原账号、凭据或正文。
唯一性冲突:保持失败,不代替用户决定
duplicate-detail-13揭示的矛盾本质是:
- 不兼容消息必须保留;
- 但现有 v3要求
message_id唯一。
因此无法同时承诺"两个序号共用原 ID 全部导入"与"保持现有唯一性"。迁移工具的选择是:
- 当前保持失败状态,不代替用户决定修改或丢弃哪条记录;
- 空身份、插件运行 / 配置映射等遗留问题也尚未解决,一并留在边界之外。
这符合迁移计划的整体哲学:internal/usecase/migration中的Plan把"排除项"设计成显式、精确、可审计的结构——Exclusions记录操作者授权的省略、Quarantine只授权"精确匹配的畸形主行"(QuarantineRow绑定 NodeID / Shard / Key / SHA256 / Reason,绝不允许通配或尽力跳过,见 quarantine.go)、Metadata/History/PluginNodes等策略全部要求显式声明(见 workflow.go)。任何未经授权的省略都会在准备阶段失败,发布不了 PREPARED 检查点。
验证矩阵:通过、未通过与未执行
通过的验证
- 迁移用例、v2/v3 适配器、CLI / 组合根相关单元测试:v2 侧 25.336 秒、v3 侧 7.829 秒;
- 存储与协议层单元测试:
pkg/db/...、pkg/channel/...、pkg/quorumlog/...、pkg/cluster/channels/...、cluster 适配器、CMD 用例和 HTTP API 单元测试;另通过pkg/cluster、Controller Raft store 和 Slot raftlog 单元测试; - 原版 fixture 的 CLI 进程拒绝矩阵:1→1、1→3、3→1、3→3、3→5 五种规模组合,以及原版空群的完整导入、正常启动、原凭据和第一条消息验收,共 48.702 秒——注意拒绝矩阵本身不代表历史消息迁移成功;
- 兼容合成消息的正向验证:原生 version 1 安装、有界 proposal 传输 / 恢复、单节点集群启动和重启;正向元数据 / 导入测试使用明确标注的合成输入,未修改原版归档,也不作为原版完整协议兼容的证据;
- 文档契约:
flow-doc-contracts79 个 FLOW 全部合规,git diff --check通过。
未通过的验证(关键项)
TestCompatibleMigrationRepairsAnEmptyChannelReplicaInNativeThreeNodeCluster:
- 三节点初始数据读取正常;
- 清空一个节点的
messages后重启,25 秒内读不到应有历史,最后返回空消息且无错误; - 整个集成命令失败。
该用例的定位在源码中非常清晰:测试先在三节点集群中停掉 Leader、删除其messages目录、重启该节点,然后期望它通过不变的 v3 恢复逻辑自行补回被导入的历史(见 target_cluster_integration_test.go)。结果是空副本恢复没有生效,但项目保留用例、不修改 v3 恢复逻辑来使它通过——再次贯彻"不为 v2 数据改变 v3 存储逻辑"的边界原则。
未执行的验证
以下项在本轮明确未执行,也是"尚未完成生产迁移"的直接证据:
- 真实源成功准备;
- 真实导入;
- 全量目标校验;
- 生产切换;
- 100 GiB / 4 小时性能验收。
记录特别提醒:历史报告中的"完整协议、历史保留前缀和故障恢复通过"结论针对的是已撤回的实现,不能用于当前切换——这是对证据有效性的重要时间边界。
证据位置与复现线索
本机工作树:
tmp/server-rehearsal-20260906/包含empty-channel-09、index-collision-10/11/12、duplicate-detail-13和existing-v3-contract-14。
远程演练根目录:
/root/wkmigrate-rehearsal-f886aab91-20260906-01原版样本、正式源库、现有 v3 数据目录均未被替换;修订 patch、逐文件摘要、工具构建摘要和测试日志保存在existing-v3-contract-14;诊断 12/13 的调用程序、执行记录和结果另有 SHA-256 清单。
想深入复现迁移工具链的读者,仓库中有三条路径:
- 迁移命令入口:
cmd/wkcli提供migrate子命令,阶段为diagnose(源级全量体检)、authority(频道配置权威审计)、prepare、export、import、verify,完整用法见 cmd/wkcli/internal/migrate/README.md; - 离线迁移工作流与计划 schema:见 internal/usecase/migration/workflow.go;
- 操作 runbook:见 docs/superpowers/runbooks/v2-to-v3-migration.md。
结论:契约优先的迁移边界
existing-v3-contract这份记录的价值在于它示范了一种以目标系统契约为优先的迁移工程方法:
- 先立基准:所有改动都以开发前的 v3 提交为比较基准,避免"为兼容而改造"悄无声息地污染存储语义;
- 可撤回、可保留:明确区分"迁移工具能力"与"v3 存储逻辑"——工具能力可以增强,存储契约必须原样;
- 诊断只读、结论克制:索引冲突、重复 seq 等结论都标注了适用边界(如"50 万项上限""不是全库去重总数"),不夸大证据;
- 冲突留白:当 v3 的
message_id唯一性与 v2 双 seq 共用 ID 冲突时,保持失败状态,把取舍决定留给用户,而不是用新格式"补齐"不可完整保留的业务字段。
对正在规划 v2 → v3 存量迁移的团队而言,这份记录的实操启示是:迁移验证的"通过"必须绑定到未改动的目标存储契约上,任何为存量数据特设的存储例外,最终都可能以丢失 v3 原生恢复能力为代价。
- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
相关推荐
WuKongIM v2 到 v3 迁移后的浏览器端 SDK 验收:用 wukongimjssdk 1.3.5 原包验证真实迁移集群
WuKongIM v2 到 v3 迁移后的浏览器端 SDK 验收:用 wukongimjssdk 1.3.5 原包验证真实迁移集群 本文围绕 WuKongIM
即时通讯后端uni-app x / UTS 内置对象 TextDecoder 详解:跨端字节流解码与实战应用
uni app x / UTS 内置对象 TextDecoder 详解:跨端字节流解码与实战应用 TextDecoder 是 UTS 内置对象中用于将二进制字节
即时通讯后端WuKongIM 受限权限 Cohort 验收诊断:尾延迟归因、原生引导修复与契约验收的实证边界
WuKongIM 受限权限 Cohort 验收诊断:尾延迟归因、原生引导修复与契约验收的实证边界 导读 :本文围绕 WuKongIM 仓库中 Issue 977
即时通讯后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考