4.2 冷数据归档:转 COS 与冷表
分库之后主表行数的增长并没有停。想在三年里把分片稳定在 5000 万行内,唯一的办法是把老数据持续搬走。这一节讲我们的归档流水线:每月一次把 1 年前的订单搬到 COS 冷存储加冷表,主表永远只留热数据。
场景:归档是唯一能让主表「倒着长」的手段
冷热分离的价值可以用一组数字说明:全站订单里 1 年前的占 78%,而访问量只占 2%。把这 78% 搬走后,主表行数从 2 亿降到 4400 万,每个分片从 1250 万降到 275 万,buffer pool 命中率从 82% 升到 99%——这一步的效果比加内存大得多。
| 对比项 | 归档前 | 归档后 | 变化 |
|---|---|---|---|
| 主表行数 | 2 亿 | 4400 万 | 减 78% |
| 单分片行数 | 1250 万 | 275 万 | 减 78% |
| buffer pool 命中率 | 82% | 99% | 大幅改善 |
| 慢查询条数 | 340 条/分 | 12 条/分 | 减 96% |
| 冷查询耗时 | 不需要 | 2 到 5 秒 | 新成本 |
冷查询耗时 2 到 5 秒是新成本:用户翻一年前的老订单时,要走另一条冷链路。
从体验上做了补偿——列表页默认只展示近 3 个月,老订单要点「查看历史订单」走异步导出。客服系统则有专门的冷查询入口,等 2 到 5 秒比用户自己等更合理(客服对延迟容忍度高,且有等待提示)。这是归档方案必须接受的产品设计调整:冷热分离的本质是给数据分级,也给查询分级。
方案:三层存储结构
归档后的数据分三处落位,各服务不同查询场景:
| 存储层 | 内容 | 查询方式 | 延迟 |
|---|---|---|---|
| 主表(分片) | 近 1 年订单 | 正常分片路由 | 毫秒级 |
| 冷表(MySQL) | 1 到 3 年订单 | 按月分区的单实例 | 100 毫秒级 |
| COS 归档 | 3 年以上订单 | 按月打包的对象存储 | 分钟级取回 |
冷表放在独立的大容量 MySQL 实例上,按月份分区,查老订单先按时间定位分区再查。COS 存的是按月打包的 JSONL 文件,配合一份「订单号到月」的索引小表:查订单详情时先查索引小表拿到月份,再从 COS 拉对应文件解析。COS 归档还有个隐形好处:合规审计要的原始数据导出可以直接用归档文件,几天的工作量变成几分钟。
归档对象还包括索引表(2.1 节的 t_user_order_idx):1 年前订单的用户维度索引同样归档,冷查询时按「用户 ID 加月份」查冷索引。索引表归档与主表归档必须同步,否则老订单的热链路也会找不到。
归档的 ROI 算账
归档方案要长期推行,必须算清经济账。每月一次归档任务约 3 小时人力加机器时间,换来的是主表稳定在 4400 万行与 99% 的 buffer pool 命中率。如果选择不归档、靠加内存续命,每月要多花一倍实例费且仍然挡不住行数增长。账算清了,归档就不会被「太忙了下次再说」拖死。
| 方案 | 月度成本 | 主表行数 | 长期可持续性 |
|---|---|---|---|
| 不归档加内存 | 2 倍实例费 | 持续增长 | 半年一加 |
| 归档到冷表 | 0.3 倍实例费加 1 次任务 | 稳定 4400 万 | 可持续 |
| 归档到 COS | 0.1 倍存储费加 1 次任务 | 同上 | 可持续 |
| 直接删老数据 | 0 | 最低 | 合规不允许 |
订单属于金融相关数据,按合规要求至少保留 5 年,所以「直接删」从来不在选项里。COS 的存储成本约为标准 MySQL 磁盘的十分之一,是长期沉淀老数据的唯一经济解。对成本敏感的团队可以按这张表直接套算自己的归档 ROI。
冷查询链路的设计
冷链路不能只是「能查到」,还要「查得不那么难受」。我们对冷查询的容许延迟是 5 秒以内,设计上用「索引小表定位加 COS 按月取回」两步:
| 查询类型 | 路径 | 延迟 |
|---|---|---|
| 近 1 年订单 | 分片主表 | 毫秒级 |
| 1 到 3 年订单 | 冷表按月分区 | 100 毫秒级 |
| 3 年以上详情 | 索引小表加 COS 拉取 | 2 到 5 秒 |
| 批量历史导出 | 异步任务加 COS 下载 | 分钟级,下载链接 |
批量导出是冷链路的「舒适区」:用户点导出后异步生成,完成后发通知,直接下载 COS 文件。把低频的冷查询整体引向异步导出,是体验与成本双赢的设计——同步等待 5 秒和异步等 2 分钟,用户体感差距其实不大,但系统压力差一个数量级。
配置:月度归档流水线
归档流水线每月 1 日凌晨执行,分五步走。核心 SQL 是「按时间窗口分批删除主表」,分批是关键,一次删几千万行会产生巨大 redo 日志:
-- 步骤 2:分批导出(每批 2 万行,主键游标翻页)
SELECT order_no, ... FROM t_order
WHERE create_time < '2023-10-01'
AND order_no > ? ORDER BY order_no LIMIT 20000;
-- 步骤 4:分批删除主表,每批 commit 一次
DELETE FROM t_order WHERE create_time < '2023-10-01' LIMIT 20000;
每批操作之间 sleep 100 毫秒,给主从同步留出余量。整条流水线用 5.2 节的影子库机制在独立只读实例上预演一遍,确认数据量与耗时后再在主库执行。一次月度归档(约 2400 万行)的总耗时约 3 小时,全程对线上无感知。
归档任务自身的可靠性也重要。流水线设计成可重入的:每一步落库前先写检查点,任务中断后从检查点续跑而不是从头来。检查点表记录「当前归档月份、当前批次游标、当前步骤」,整个月度任务宕机重启也能在一个小时内恢复。流水线脚本的编写思路参考了 k8s 实战:发布流水线 的阶段化与可重入设计。
归档还有个配套动作是「归档后 ANALYZE」。归档删掉大量数据后表的数据分布大变,统计信息立刻过时,优化器可能选错索引。4.4 节有一次线上事故正是这个原因造成的,所以归档流水线的第 5 步(复核)里固定加了 ANALYZE 高变更表,作为收尾动作。
| 步骤 | 动作 | 耗时占比 |
|---|---|---|
| 1 校验 | 对账确认该月数据一致 | 5% |
| 2 导出 | 分批导出到冷表与 COS | 55% |
| 3 验证 | 冷链路回放抽检 | 10% |
| 4 删除 | 分批删除主表 | 25% |
| 5 复核 | 对账再跑一轮 | 5% |
踩坑:归档删数据的恐惧
第一个坎是心理关:从生产库 DELETE 几千万行,DBA 手抖。第一次执行归档时我们做了三重保险:冷表备份验证通过、归档文件 MD5 校验、主表全量备份完成后才执行删除。第三个月后流程跑顺,保险简化为「冷链路抽样 100 条能查到」。
第二个坑是 DELETE 的实际开销被低估。DELETE ... LIMIT 20000 删 2 万行看似不多,但如果这些行带索引,每个索引都要同步维护。实测每批 2 万行的删除耗时约 3 秒,2400 万行要 6 小时——后来把窗口拆细到「按分片并行归档」,16 个分片同时跑,总耗时压到 40 分钟。
第三个坑是归档后冷查询的产品适配。有用户反馈「老订单怎么查不到了」,其实是要点「历史订单」入口。产品上把入口做明显、给预估等待时间提示,是归档方案落地的必要一环。
第四个坑是归档边界条件的精确性。归档窗口按 create_time < '2023-10-01' 切分,而订单的 create_time 有时区与毫秒精度问题。一次归档脚本时区配错(写成 UTC),导致归档多删了一批当天订单,靠对账与冷表回补修复。此后归档窗口统一用「订单完成时间而非创建时间」做边界,且所有时间参数在流水线日志里打印明文便于核对。
小结
冷热归档是唯一能让主表倒着长的手段:冷表承接 1 到 3 年查询需求,COS 存更老的合规数据,主表只留热数据。落地要点是「分批操作加双重校验加产品入口适配」。归档不是把数据当垃圾扔掉,而是给数据分级——不同时效的数据配不同成本与延迟的存储,这是数据治理的基本功。
延伸阅读
- rust 实战:日志管道:海量数据归档移动的管道工程实践
- node 实战:白板系统:协作类应用的历史数据分层设计
- MySQL 实战:分库分表订单系统:回到本书,4.3 节讲慢查询定位
