- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
本文以 WuKongIM 仓库内《迁移后历史分页与 Chat Demo 验收》报告为骨架,完整还原三节点 v3 测试集群在迁移候选 HA78 上遇到的历史分页缺陷(limit=15却只返回 12 条可见消息、more=0提前截断)的根因、修复方案与验收证据,并结合 PageReader 源码、消息同步用例 与 Chat Demo 前端实现 深入讲解可见消息分页的 lookahead 语义、有界扫描预算、more契约、客户端“加载更早消息”与“重新同步”的正确做法,以及迁移候选的交付、校验与回退路径。读完本文,你将掌握在含隐藏控制记录的频道上正确翻页的 API 语义,并能够复现本次验收的完整步骤与检查点。
一、背景:v2→v3 离线迁移与本次验收边界
本次验收针对未经修改的 v2(v2.2.5-20260422,提交a888f89533d0e7d1b2030e06504ca97f1ad891d4)迁入全新 v3 集群的离线迁移流程,完整操作与检查条件见 v2→v3 离线迁移操作手册。迁移工具wkcli migrate与目标 v3 服务必须使用包含同一迁移实现的代码版本构建,执行链为:
停止业务写入 → 原版 v2 收敛并排空通知 → 正常停止全部 v2 节点 → 保存完整停机备份 → diagnose(列出兼容缺口)→ prepare → export → import → 停机 verify → 隔离启动 v3 并做业务验收 → 切换流量用户指定的三节点 v3 测试集群已部署最终候选HA78。原 v2 未启动、未升级、未修改,601 个源文件摘要与最初捕获完全一致。本次验收范围明确:
- 通过项:普通消息历史分页、真实 Chat Demo 清缓存重新同步、新消息递增验收。
- 未决项:100 GiB/四小时性能验收等待测试环境;插件验收按用户要求后置,本报告不解除插件业务兼容要求,也不声称生产切换全部通过。
此前的高可用修复和两轮真实暂停验收见 原生前缀恢复报告。本轮修改仅涉及普通历史读取与 Demo 前端,不改变 v3 数据格式、复制协议、成员权限或序号分配规则。
二、问题根因:恢复屏障等控制记录吞掉可见页预算
HA76 候选在limit=15的“最新历史”请求中返回了12 条普通消息且more=0,而实际频道共有 188 条可见消息。定位结论是:
恢复 barrier 等
SyncOnce控制记录参与了原始扫描的数量统计,过滤后计数不足,扫描被错误地提前结束。
即:扫描器按“原始记录条数”凑满页大小,过滤掉控制记录后,剩余可见消息少于请求页大小;又因为原始条数已达limit,被误判为“已到历史尽头”而返回more=0。独立的固定序号窗口核验确认数据仍在存储中,只是分页逻辑错误地截断了遍历。
三、修复方案:共享 PageReader 的可见消息 lookahead 与有界扫描
修复的核心是把“按可见消息收集”的语义下沉到共享的 PageReader 中。它同时服务普通消息同步与插件读取,无状态、不保留跨调用的请求或消息。关键设计如下:
- lookahead 收集:每页按可见消息收集
limit+1条(多取一条用于判定是否还有下一页),控制记录不消耗调用方的可见页大小。 - 原始游标推进:扫描位置按最后一条原始记录的序号推进(正向
FromSeq = last + 1,逆向FromSeq = last - 1),保留读取方向、成员可见下界及结束序号的排他语义。 - 整页隐藏继续扫描:若一整页都是控制记录,继续扫描而不结束;底层实现见 page_reader_continuation_test.go,测试分别覆盖“隐藏记录之后的可见历史仍应
HasMore=true”和“整页 130 条控制记录中只有一条可见消息”的正向/逆向场景。 - 有界预算(fail-closed):每个查询最多64 波扫描、每波最多1,024 条原始记录,请求总上下文最长5 秒(常量定义见 page_reader.go:
messagePageScanChunk=1024、messagePageScanWaves=64、messagePageScanTimeout=5s)。达到预算但仍无法证明历史结束时,返回明确错误ErrSyncPageScanBudget,绝不把不完整结果当成more=0;重复、逆向或越界的原始游标同样报错(ErrSyncPageScanInvalid,见 errors.go)。 - 批量对齐:批量请求中只继续尚未完成的批量项,已完成的项不受其他项失败影响。
底层波次执行逻辑在SyncMessagesBatch(page_reader.go):每波把待处理项的当前游标合并成一次对齐扫描,逐项consume,已完成的项从 pending 中移除;超过波次上限或总读取字节预算(持久化会话预读 8 MiB 上限)后,剩余项统一以ErrSyncPageScanBudget失败。consume(page_reader.go)还严格校验单调性:逆向扫描要求序号递减且不超过FromSeq,正向要求递增且不低于FromSeq,任何乱序都会触发ErrSyncPageScanInvalid,从而拒绝“不推进的扫描”。
3.1 分页计划:最新页语义与排他边界
planMessagePage(page_reader.go)负责把请求意图解析为一次有界扫描:
limit被钳制到maxSyncMessagesLimit(10000)以内,非法值回退为 1;latest := StartSeq == 0 && EndSeq == 0——最新页语义,此时逆向扫描(Reverse = PullModeDown || latest),FromSeq与MaxSeq均置为^uint64(0)(正向遍历起点为 1);PullModeDown时EndSeq作为排他下界(excludeThroughSeq),扫描上限为StartSeq;PullModeUp时EndSeq-1作为扫描上界,EndSeq本身被excludeFromSeq排除,保证过滤在存储读取之后仍然生效。
最终page()(page_reader.go)再次过滤SyncOnce与排他边界,hasMore = len(kept) > limit——只有多收集到的可见记录才能证明还有下一页,随后把逆向收集结果翻转为升序返回。
3.2 与成员可见性的组合:visibility floor
普通消息同步在进入 PageReader 前会独立计算成员可见下界MinSeq:取成员DeletedToSeq与JoinSeq-1中较大者作为可见下限(见 sync.go)。该下界与最新页意图组合解释,不会改写调用方的 start 序号来强制可见性。finishSyncChannelMessages把page.HasMore直接映射为响应中的More(sync.go)。组合测试 message_page_test.go 覆盖了最新页、正向区间、逆向区间、低于可见下界的起点、历史耗尽、反向排他结束、未知方向等 11 种组合,并断言每波 lookahead 为limit+1。
四、API 契约:pull_mode、start/end 与 more 的正确用法
历史兼容同步走 HTTPPOST /channel/messagesync,另有批量接口POST /channel/messagesyncbatch(路由注册见 message_send.go)。请求与响应结构见 channel_messagesync.go,实测请求/响应样例见 message_legacy_test.go:
// 请求 {"login_uid":"u1","channel_id":"u2","channel_type":1, "start_message_seq":2,"end_message_seq":5,"limit":10, "pull_mode":1,"include_event_meta":1,"event_summary_mode":"compat"}核心语义(sync.go 与 sync.go):
| 参数 | 语义 |
|---|---|
pull_mode | 0=PullModeDown,拉取start_message_seq及之前的更早消息;1=PullModeUp,拉取之后的更新消息 |
start_message_seq/end_message_seq | 起始/排他结束边界;start=0,end=0表示最新页(latest 语义) |
limit | 单页最大消息数,服务端钳制上限 10000 |
more | 是否在请求边界内还存在下一页,按可见消息判定 |
关键教训:start=0,end=0是最新页语义,即使指定pull_mode=1也不能把它当作正向全量遍历的起点。正确的向前翻页是“每页以前一页最早可见序号减一继续向前,直到more=0”;超长隐藏记录区间可能触发有界扫描错误,遇到错误应保留当前游标并报告失败,绝不能当作历史结束。初版验证脚本曾误用pull_mode=1 + start=0做全量遍历并已纠正,服务契约本身未改动。
五、Chat Demo 适配:15 条页大小、加载更早按钮与重新同步
5.1 SDK provider 使用调用方页大小
Demo 的 SDK provider(APIClient.ts)现在把调用方传入的limit原样透传给/channel/messagesync,而不是写死固定大小。视图层默认每页15 条:
- 最新页拉取(Chat.vue):
limit:15, startMessageSeq:0, endMessageSeq:0, pullMode:PullMode.Up;pulldownFinished = msgs.length < 15 || msgs[0]?.messageSeq === 1。 - 向前翻页(Chat.vue):
startMessageSeq: firstMsg.messageSeq - 1, endMessageSeq:0, pullMode:PullMode.Down,命中最早序号或不足一页即结束。
5.2 “加载更早消息”按钮:滚动失效场景的补救
真实浏览器验收发现:较高窗口能容纳全部首屏消息时没有滚动条,滚动事件无法触发历史加载。因此 Demo 在消息列表顶部增加“加载更早消息”按钮(Chat.vue),与滚动入口(scrollTop <= 100触发,见 Chat.vue)共用原有分页逻辑与单请求保护(pulldowning标志防止并发重复拉取)。按钮文案见 i18n.ts(加载更早消息/Load earlier messages),模板位于 Chat.vue。该入口不绕过 SDK,也不直接操作服务端数据,与滚动拉取行为完全一致。
5.3 重新同步:按迁移代次清空缓存、保留凭据
客户端迁移代次的正确顺序是:先停止发送并关闭旧连接,记录迁移代次;保留 UID、token、设备标识与业务登录状态;删除旧消息缓存、会话列表派生缓存、历史翻页位置和消息同步序号,再重新加载页面重建 SDK 单例,用原凭据连接 v3。Demo 的“重新同步”按钮(Chat.vue)断开连接后整页location.reload(),从而丢弃 Vue 与 SDK 内存中的消息与游标状态,但登录凭据保存在sessionStorage中保持不变。清理只作用于客户端消息副本与派生同步位置,不删除服务端历史或凭据,也不能只迁移服务端数据库便宣布客户端旧序号问题已处理。
六、验收证据:从回归失败到真实浏览器逐条核对
6.1 修复前后回归与组合测试
- 修复前回归确实失败:混合控制记录、整页控制记录、基于页大小结束导致客户端遗漏历史三种场景均可稳定复现。
- 修复后覆盖:双向读取、排他边界、批量对齐、重复游标、取消及固定预算。
go test -race ./internal/usecase/message ./internal/usecase/plugin ./internal/infra/cluster ./internal/access/api -count=1通过。go test -race ./internal/app -run TestMessagePage -count=1通过;组合测试保持权限可见范围。- Demo 13 项测试、
vue-tsc与 Vite 构建通过(现有大资源块提示不影响构建)。
6.2 完整隔离故障套件
完整隔离 Linux 真实进程故障套件通过,耗时 61.1 秒,覆盖:kill、副本不足、补副本、严格暂停、暂停历史安全。严格暂停用例专门增加limit=2的普通 API 翻页,依赖more和最早消息序号,不使用显式窗口绕过。该套件使用 HA77;HA78 仅增加 Demo 手动入口和翻译,Go 服务逻辑与通过套件的 HA77 完全相同。
6.3 实机 API 全量遍历与 Chrome 逐条核对
- API 层:每页 15 条,分别从最新向前和从序号 1 向后完整读取——群聊 188 条/13 页,重编号单聊 149 条/10 页,全部基线字段一致、ID 与序号无重复。
- 真实 Chrome Chat Demo:以原有 Web Token 登录,手动逐页加载群聊 188 条、单聊 149 条;逐一核对 DOM 消息
ClientMsgNo与顺序,无遗漏、重复,最后“加载更早消息”入口正常结束。 - 重新同步 + 新消息递增:Demo 发送一条消息后点击“重新同步”,保持原登录、清空消息和游标,再发送第二条;新消息序号为220、221(严格递增)。再次重新同步后从首屏逐页读回全部190条,DOM 标识及顺序与服务完整结果一致,两条消息均收到正常状态。
- 部署前最终核对:双向 API 遍历群聊 190 条/13 页、单聊 149 条/10 页,全部 337 条记录逐字段一致;原始迁移样本 100/147 条的 ID、
ClientMsgNo、发送者、正文、RedDot 一致。
6.4 环境与残留状态检查
三节点 ready、运行且未暂停,进程二进制摘要一致;原 v2 的 601 文件摘要不变;调试配置不存在、MCP 关闭、临时凭据为零、隔离实验进程为零。需要说明的是:可见消息序号因原生内部控制记录存在合法间隔,本次普通新写严格递增;控制记录不展示,也不通过重写存储来消除间隔。
七、最终候选交付与校验
最终 Linux amd64 服务 SHA-256:
effbc52eb8a1a32588dce3ceb0a99f65c2a5278c76da143ceb6d23140cae6c85配套wkmigrateSHA-256:
1f2c1cdbfa874aa7effdfdf771e45533cb09c3c6c0cc507142267c743a5102f7构建工具链为Go 1.25.0,使用GOWORK=off GOOS=linux GOARCH=amd64 CGO_ENABLED=0与-buildvcs=false -trimpath。交付包的manifest.json固定源码提交及两个二进制摘要,SHA256SUMS校验包内文件;包中不包含生产凭据、消息正文、私有计划或源数据。
部署路径为/root/wukongim/migration-ha-20260908-78/。部署前正常停止三个目标容器,保存data-before/及compose-before.yaml,再启动候选。需特别强调:该快照是当时 v3 数据的离线副本,不是新的 v2 迁移输出;原 v2 备份保持原样。
八、回退路径与数据处置
- 回退时先停止业务写入;仅回退程序可以用保存的 compose、当前 v3 数据及前一二进制恢复三个目标服务,再核对历史与就绪。本轮无存储格式升级。
- 不得把
data-before/自动覆盖当前数据,因为验收已追加两条消息(序号 220、221)。 - 若必须回到快照或 v2,先单独保留切换后写入并明确数据处置,再执行离线恢复;不能让两套环境同时承接写入。
- 完整迁移流程与检查条件见 v2→v3 迁移操作手册;程序回退的后续实测及合并审查见 回退演练报告。
九、边界与未决项
- 性能验收未执行:100 GiB 源业务数据、停写至完成校验 ≤4 小时是约定的性能验收目标,等待测试环境;本机小样本测试只证明对应功能,不构成吞吐或停机时长承诺。
- 插件验收后置:本报告不解除插件业务兼容要求;插件程序迁移、配置来源分配、
PluginUser绑定等详见程序迁移报告与操作手册。 - 原始证据保留:私有原始证据保留在忽略目录
tmp/server-rehearsal-20260908/pagination-77/、pagination-78/,公开报告不包含凭据或历史正文。
结论:含隐藏控制记录的频道上,历史分页必须按“可见消息 + lookahead”推进游标,
more只由可见消息判定;start=0,end=0仅表示最新页;超预算必须失败关闭而非伪装历史结束。HA78 以此契约通过三节点实机、真实浏览器与故障注入全链路验收,可作为后续迁移部署的分页正确性基线。
- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
相关推荐
WuKongIM v2→v3 迁移后内嵌 Chat Demo 验收实践:凭据、历史恢复与单节点故障的 readiness 边界
WuKongIM v2→v3 迁移后内嵌 Chat Demo 验收实践:凭据、历史恢复与单节点故障的 readiness 边界 2026 09 08,WuKon
即时通讯后端WuKongIM v2 全量迁移后空历史 API 验收:从 channel not found 到空页响应
WuKongIM v2 全量迁移后空历史 API 验收:从 channel not found 到空页响应 本篇文章围绕 WuKongIM 仓库中的 v2→v3
即时通讯后端transcribe.cpp的路径设计:UTF-8路径与跨平台文件操作实现
transcribe.cpp的路径设计:UTF 8路径与跨平台文件操作实现 transcribe.cpp 是一个基于 ggml 的 语音转文字(speech t
即时通讯后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考