4.2 快照生成:定时与里程碑

持久化里最容易被「日志」掩盖的一块:快照。日志保正确、快照保快。快照是「某一时刻整板」的定格,用来快速恢复、做封面、当回放锚点。本篇讲快照怎么定时打、怎么按里程碑打、打完怎么用。

快照是什么、为什么必要

用途 没有快照 有快照
恢复房间 从 seq=0 重放几万笔 取最近快照 + 少量增量
做封面 没法秒出 直接用最近一张
分享 要重算 直接发图
回放定位 只能从头 从锚点起

快照把「重放代价」从 O(总笔数) 降到 O(间隔笔数),是性能与体验的支点。

两种打点策略

策略 触发 适用
定时 每 N 分钟 / 每 N 笔 持续协作
里程碑 关键事件(章节结束、导出、关房前) 语义点

两者叠加:定时保底、里程碑保语义,回放既有「均匀锚点」又有「有意义的起点」。

快照怎么生成

步骤 动作 说明
1 取当前全量 / 最近快照 + 增量 确定要定的内容
2 渲染成图片 服务端离屏渲染
3 记锚点 seq 快照对应到第几笔
4 上传 COS 冷存
5 更新元信息 指向最新快照

关键是「快照必带锚点 seq」——没有它,恢复时不知道从哪接增量。

快照与日志怎么配

数据 保留 说明
快照 每间隔留一张,旧的降级 控数量
日志 快照之间的增量可裁剪 锚点之间
锚点 随快照 恢复入口

「快照 + 增量」成对,恢复永远「最近快照 + 到现在的增量」,两条数据量都受控。

踩坑提示

最坑是「快照不带锚点 seq」:恢复时拉了最新快照,却不知道该接哪段增量,要么重头放、要么漏笔。再一个坑:打快照阻塞广播——渲染整板是重活,若在广播线程同步做,千人房间会卡住所有人。快照必须异步生成,不阻塞在线事件流。

小结

快照 = 定时的整板定格,带锚点 seq、存 COS、异步生成。它把「从头重放」变「快照加增量」,是恢复、封面、分享、回放的共同底座。

定时器怎么配

定时快照不是随便拍:间隔太密浪费存储和渲染力,太疏则恢复时增量太长。按房间活跃度分档是常见做法: | 房间活跃度 | 定时间隔 | 笔数触发 | |---|---|---| | 高频(百人以上) | 5 分钟 | 每 2000 笔强制一张 | | 中频 | 10 分钟 | 每 1000 笔 | | 低频 | 30 分钟 | 可关 |

服务端用独立的快照调度器,避免和广播线程抢资源:

// 每 10 分钟扫一次活跃房间,快照进异步队列
setInterval(() => {
  for (const roomId of activeRooms()) {
    snapQueue.push({ roomId, reason: 'timer' });
  }
}, 10 * 60 * 1000);

// 消费端:渲染 → 上传 → 记锚点
snapQueue.process(async ({ roomId, reason }) => {
  const seq = await redis.get(`room:seq:${roomId}`);
  const image = await renderBoard(roomId);
  const key = `snap/${roomId}/${seq}.png`;
  await cos.putObject({ Key: key, Body: image });
  await db.snapshot.create({ roomId, seq, key, reason });
});

reason 字段一定要记:定时和里程碑的快照混在一起,日后排查「为什么这张快照出现在这个时间点」全靠它。

里程碑触发有哪些时机

里程碑快照的价值在「语义」:它标出「值得再回来」的时刻: | 里程碑 | 为什么值得定格 | |---|---| | 章节结束 | 讨论告一段落,回放从段头起 | | 导出前 | 保证导出的图和增量一致 | | 关房前 | 最后一张,作长期恢复基线 | | 大清理前 | 裁剪日志前先存快照,防误删 |

定时保底、里程碑保语义,两者叠加之后回放既有均匀锚点、又有有意义的起点。

快照存哪、叫什么

路径命名直接决定读取和清理好不好做: | 路径段 | 内容 | 例 | |---|---|---| | 前缀 | 固定 | snap/ | | 房间 | roomId | board-42/ | | 锚点 | seq | 1287/ | | 文件 | 版本号 | v1.png |

清理只对「旧锚点目录」动手,最新一个永远保留,关键时刻绝不翻车。

快照数量怎么控

打了一个月快照不清理,COS 账单会教你做人。降级规则按时间衰减: | 年龄 | 保留密度 | |---|---| | 当天 | 全留 | | 一周内 | 每小时一张 | | 一月内 | 每天一张 | | 更早 | 仅里程碑 |

再配合 COS 生命周期规则把旧快照自动沉降到低频存储,成本能压到很低。项目刚起步的话,可以先在 腾讯云云产品合集特惠 领 COS 资源包再上线。

恢复时怎么用快照

实测链路是五步:客户端进房 → 服务端查数据库里该房最新快照 → 从 COS 拉图 → 从锚点 seq 之后回放增量日志 → 拼成当前画面。链路里最慢的一步是 COS 拉图,所以给快照开 CDN 预热也很有必要。顺序上有一个铁律:先上传快照成功,再更新数据库锚点,反了就会出现「锚点指向不存在的快照」。

渲染整板用什么思路

服务端渲染白板,常见两条路:无头浏览器跑页面截图,或用 Canvas 库按事件流重绘。前者实现快但吃内存,后者轻但画布逻辑要在服务端重写一遍。选型参考: | 方案 | 优点 | 缺点 | |---|---|---| | 无头浏览器截图 | 前端代码直接复用 | 内存高、并发差 | | 服务端 Canvas 重绘 | 轻量、可分片 | 绘制逻辑维护两份 | | 客户端渲染上报 | 不占服务端 | 依赖在线成员,不可靠 |

中小团队建议从无头浏览器起步,量上来再迁服务端重绘;不论哪种,渲染都必须在独立进程里跑,别和广播抢事件循环。

快照失败怎么兜底

快照任务失败是常态:渲染超时、COS 限流、机器重启都会撞上。兜底就三条: | 故障 | 兜底动作 | |---|---| | 渲染失败 | 重试 2 次,仍失败告警,不阻塞在线 | | 上传失败 | 本地暂存,补偿任务续传 | | 锚点没更新 | 下一轮定时自然覆盖,绝不写半成品 |

记住「快照可以迟,但不能错」:一张错锚点的快照比没有快照更难查。

快照和恢复的联动验证

快照做完不验证等于没做。最简单的验证脚本:随机挑房间 → 关掉 Redis 热数据 → 走恢复链路重建 → 对比重建后画面与重建前是否一致。每天跑一次,挂了就报警。这套自检和我们在 K8s 流水线里跑的冒烟测试是一个思路,参见 K8s 实战:手搭一个多环境部署流水线。

快照策略跟着房间规模走

小房间和大房间不能用同一套快照参数。十人小会更在意「别频繁打扰」,千人直播更在意「恢复别太久」: | 房间规模 | 定时间隔 | 锚点密度 | 恢复目标 | |---|---|---|---| | 十人以下 | 30 分钟 | 稀 | 10 秒内 | | 百人级 | 10 分钟 | 中 | 5 秒内 | | 千人级 | 5 分钟 + 笔数触发 | 密 | 3 秒内 |

之所以分档,是因为「快照成本」和「恢复成本」此消彼长:打得勤快,渲染开销大;打得稀疏,恢复时增量回放又久。分档就是找每个规模下的平衡点,上线后按监控数据微调,别指望一次配终身。

快照不是越快越好

新人容易把快照频率调得很高来换体验,结果渲染进程先撑不住、COS 账单也跟着翻倍。快照的节奏感像交通信号灯:该停停、该走走,强行加速只会堵死整条恢复链路。合理的频率永远来自压测数据,而不是直觉。

延伸阅读