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 存更老的合规数据,主表只留热数据。落地要点是「分批操作加双重校验加产品入口适配」。归档不是把数据当垃圾扔掉,而是给数据分级——不同时效的数据配不同成本与延迟的存储,这是数据治理的基本功。

延伸阅读