一张订单表五年没清,99% 的查询只看近三个月,每次分页却要扫全表。直觉是挪走老数据,真问题不是能不能分,而是分了之后谁受影响:报表、对账、客服、审计、合规导出,全在剩下那 1% 里。
冷热分离是边界变更,不是清理:先列受影响名单,再算迁移、路由与一致性的账。
1. 先列受影响方,再谈技术方案
| 受影响方 | 关心什么 | 分层后的变化 |
|---|---|---|
| 在线接口 | P99、可用性 | 热路径变快,跨层变慢 |
| 报表与对账 | 完整性、可重跑 | 跨层聚合或改跑汇总 |
| 客服与后台 | 能查任意一单 | 老单跨层或降为摘要 |
| 审计与导出 | 可追溯、可导出 | 冷层仍需可查可导 |
| 运维 | 备份、监控、容量 | 运维面翻倍,故障域增加 |
列不出五个真实使用方,就说明还没准备好——没人说得清老数据谁在用。
判据:答不上"最老那批数据最近被谁读过"之前,不要动;答不上来,边界就是猜的。
2. 冷热判定:四条依据,两两冲突
| 依据 | 判定方式 | 与谁冲突 |
|---|---|---|
| 访问频率 | 按分区统计读次数 | 时效:低频但须秒回 |
| 访问模式 | 点查还是范围扫描 | 频率:低频全扫更贵 |
| 时效要求 | 业务允许多久查到 | 合规:长期可查 |
| 合规要求 | 保留期、可导出 | 成本:不能删不能降级 |
优先级是合规 > 时效 > 访问模式 > 访问频率:违法是法律成本,查不到是业务成本,扫得慢是性能成本。
第二条最常被忽略:每天扫一次的全表聚合,成本可能高于十万次点查。
SELECT table_name, rows_read, rows_returned,
rows_read / NULLIF(rows_returned, 0) AS read_amplification
FROM performance_schema.table_io_waits_summary_by_table
ORDER BY rows_read DESC;
放大倍数高说明在扫大量冷行,收益大;接近 1 说明是精准点查。
3. 四种实现与各自的迁移成本
| 实现 | 做法 | 迁移成本 | 主要风险 |
|---|---|---|---|
| 同库分表 | 冷热同实例,按时间后缀 | 低,改 SQL 与路由 | 表数膨胀,备份变重 |
| 分库 | 冷数据搬到独立实例 | 中,需同步与回滚 | 跨库查询与事务 |
| 分引擎 | 热走 OLTP,冷走列存 | 高,双写与类型映射 | 两套语义不一致 |
| 分介质 | 热在库,冷在对象存储 | 中高,需取回路径 | 取回延迟、格式演进 |
判据不是"哪层更省",而是查询模式能否拆成两套互不干扰的路径:点查与范围扫描能拆,实时聚合拆不动。
最易低估的是 DDL 与备份:按月分表后 1 张变 61 张,每次变更跑 61 次;在线 DDL 行为按版本核对。
4. 物理分离与逻辑分离
| 形式 | 消费者感知 | 代价 |
|---|---|---|
| 视图 | 无感,仍查同一视图 | UNION 拖慢所有查询 |
| 分区表 | 基本无感,需分区键 | 键选错即退化为全扫 |
| 路由层 | 需改调用方或 SDK | 复杂度上移,测试面变大 |
| 独立冷入口 | 感知"这是历史查询" | 需独立 UI 与权限 |
逻辑分离最省事,但复杂度留在数据库里:不带分区键就全扫,UNION ALL 让计划变差。路由层适合"必须挪走",代价是逻辑显式化:
route(query):
if time_range ⊆ hot_window: return hot_store
if time_range ⊆ cold_window: return cold_store
if has_aggregation or has_order_by: return merge_engine # 最贵
if is_paginated and crosses_boundary: return cursor_required
else: return error(needs_time_range)
最后一行最有价值:强制调用方给时间范围,成本是一次接口改造,收益是路由复杂度下降。
5. 查询路由变难的四件事
| 能力 | 单表时 | 分层后 |
|---|---|---|
| 跨层查询 | 一条 SQL | 两次查询 + 应用侧合并 |
| 聚合 | GROUP BY 直出 |
预聚合或分片再合并 |
| 排序 | ORDER BY 走索引 |
归并排序或限单层 |
| 分页 | LIMIT/OFFSET |
需游标,深分页不可行 |
成本不是线性的:只要一个接口不能降级,整套方案就得保留跨层能力,而它一旦成为默认路径,最终等于没分层还多一套存储。
所以要把接口显式分级,只有白名单保留合并逻辑:
query_policy:
tier_required: # 必须跨层,白名单,每季度复核
- report.monthly_settlement
- audit.export
tier_optional: # 允许跨层,但必须带时间范围
- order.detail
hot_only: # 超窗口直接拒绝,返回明确错误
- order.list
6. 写路径的分裂:历史还能不能改
| 策略 | 做法 | 适用 | 代价 |
|---|---|---|---|
| 冷层只读 | 归档后禁更新 | 流水、日志、已结算 | 更正走审批补偿 |
| 冷层可写 | 保留写路径与路由 | 需事后更正 | 双写一致性、延迟 |
| 可写但延迟 | 队列异步落冷层 | 可容忍分钟级 | 读自己刚写的会失败 |
只读最省事,也最易被拒:一旦有客服改单、财务冲正,只读就变成"把数据搬回来改"。
判据:统计近一年对老数据的写操作次数。一年个位数只读即可;每周都有则冷层必须可写,降成本收益会被双写成本吃掉大半。
7. 一致性与延迟的账
异步归档必然有窗口:数据已"过期"但未搬走。窗口内的行为要写死。
A 双读兜底:先热后冷,miss 再查冷 → 延迟增加,一致性最好
B 只读热层:窗口内老数据不可见 → 简单,但会有"数据消失"投诉
C 双写过渡:同时写热与冷 → 一致,成本最高,仅迁移期
A 的代价是延迟与放大:每次 miss 多一次跨层调用,冷层更慢。B 的代价是体感故障——分页到第 3 页突然空了,用户以为数据丢了。
还有一处常被忽略:边界的时钟。按月分表时"上个月"由谁定义?三处时区不一致时,某一天的数据会两边都没有。
8. 迁移的执行顺序
1 影子读:新路径只读,与旧路径按天抽样比对,记录差异率
2 双写:写旧路径同时写新路径,差异告警接值班
3 回填:按时间分片、限速、可中断续跑,记录分片位点
4 校验:行数、金额汇总、抽样明细三方比对
5 切读:按接口灰度,从 tier_optional 开始
6 停双写:确认无差异后关闭旧写路径,保留回滚窗口
两处顺序不能颠倒:影子读在双写之前,停双写在切读之后(否则没有回滚数据)。
判据:每步都能单独回退;回填必须幂等可续跑——中断后从头再跑是常见事故。
9. 回退:分层做错了怎么回去
| 回退层级 | 手段 | 代价 |
|---|---|---|
| 查询回退 | 路由切回单层(需全量副本) | 存储成本不降 |
| 数据回退 | 冷层反向回填热层 | 小时级,占磁盘与 IO |
| 逻辑回退 | 关路由层,恢复单表查询 | 表结构需兼容 |
| 不可回退 | 冷层已降为摘要、明细已删 | 只能靠备份,天级 |
回退能力取决于是否保留完整原始数据。把冷层做成"摘要保留",回退就不可逆,这个决定要写进方案。
判据:回退时间超过一次故障容忍窗口,等于没有回退。
10. 成本账:省下的存储与新增的复杂度
| 项 | 方向 | 说明 |
|---|---|---|
| 存储与备份 | 下降 | 冷介质单价低,但副本数不变 |
| 热路径延迟 | 下降 | 热表变小,缓存命中变好 |
| 开发成本 | 上升 | 路由、合并、双写、回填 |
| 故障成本 | 上升 | 多一套存储,多"查不到"故障 |
| 运维成本 | 上升 | 备份、监控、容量、权限各一套 |
账常被写成"存储省了 X",但对照项是开发与故障成本——不在账单上,只在排期与事故里。
判据:存储节省小于新增一套存储的年运维成本、且动机不是性能时,不要做。
11. 合规压力可能直接否决分层
| 合规要求 | 对分层的影响 |
|---|---|
| 保留 N 年可查 | 冷层必须支持查询,不能只是文件 |
| 可完整导出 | 导出链路要走通跨层 |
| 不可篡改 | 冷层倾向对象存储 + 校验 |
| 删除覆盖所有副本 | 冷层与备份也要进删除链路 |
最后一条是最易忽略的副作用:分层增加了"位置",也扩大了删除必须覆盖的范围;确认删除链路能延伸过去再动手。
12. 度量分层是否成立
热数据占比 = 热层行数 / 总行数 (小而热,不是越小越好)
跨层查询比例 = 触发合并的请求 / 总请求 (> 5% 说明路由没起作用)
热层 P99 = 分层前后对比,按接口分桶
冷层取回延迟 = 冷查询 P95,按介质区分
校验差异率 = 影子读差异,应趋近 0
跨层比例居高不下,通常是没强制时间范围或白名单太长,该改调用约定而不是存储。
13. 排错:分层之后常见的六种故障
| 现象 | 根因 | 处理 |
|---|---|---|
| 老数据"消失" | 窗口内只读热层 | 双读兜底或缩窗口 |
| 分页到后面空了 | 未用游标分页 | 改游标或限窗口 |
| 聚合结果对不上 | 合并漏层或重复计 | 统一口径,加对账 |
| 某天数据两边都没有 | 时区边界不一致 | 单一配置生成边界 |
| 回填反复重跑 | 非幂等、无位点 | 记位点,幂等写入 |
| 分层后更慢了 | 跨层查询成默认 | 收紧白名单 |
14. 边界:什么时候不要分层
| 场景 | 判断 | 替代做法 |
|---|---|---|
| 单表千万级以下 | 索引与分区够用 | 优化索引、做归档表 |
| 查询模式尚未稳定 | 边界会频繁改 | 先观察 3 个月分布 |
| 无运维两套存储能力 | 故障域翻倍 | 同库分表,先不动存储 |
| 数据几乎全被查询 | 冷热无区分 | 不拆,改查询方式 |
| 合规要求全量低延迟 | 冷层无法降级 | 只做索引与缓存优化 |
第二类最易被跳过:产品还在改、边界三个月一变,按今天分布切,半年后要重切。分层收益来自稳定的边界。
15. 自测
- 分层方案的第一份产出应该是什么,为什么不是技术方案?
- 四条判定依据冲突时,为什么合规排最前?
rows_read / rows_returned很高说明什么?- 分区表最容易被误用的地方在哪?
- 冷数据设为只读之前,要先统计什么?
- 跨层查询比例长期 20%,该改存储还是改调用约定?