冷热分离存储架构设计

0 阅读42分钟

概述

系列定位说明

本文是**“分布式数据架构与存储选型”系列的第五篇。在前四篇文章中,我们深入拆解了分库分表**(水平扩展的骨架)、读写分离(读写流量的分流器)、多租户数据隔离(租户边界的守护者)以及全文搜索与数据分析引擎选型(检索与分析的引擎矩阵)。现在,我们将视角从“如何扩展”转移到“如何降本”——冷热分离是数据架构成本控制的核心手段,也是数据全生命周期管理的工程落地。当数据量从 GB 级增长到 TB/PB 级,存储成本从“可忽略”变为“关键决策因素”,冷热分离不再是一种锦上添花的优化,而是大规模系统在成本约束下的必选项。

总结性引言

在一个日增 100GB 日志的监控平台中,如果所有数据都存储在 SSD 上,每月的存储成本将高达数万元。但统计显示,90% 的查询只访问最近 3 天的数据,30 天前的数据几乎无人问津。冷热分离将热数据(近 7 天)保留在 SSD 高性能存储上,冷数据(>30 天)迁移到 HDD 或对象存储(如 S3/MinIO),在几乎不影响查询体验的前提下,存储成本降低 90% 以上。

MySQL 的 pt-archiver 通过分批游标式迁移,在不锁表的前提下将冷数据从热库归档到冷库;Elasticsearch 的 ILM 通过 Hot-Warm-Cold-Delete 四阶段自动流转,将冷索引从本地 SSD 迁移到 S3 可搜索快照,存储成本降低 80%;Redis 的 allkeys-lru 淘汰策略配合 EXPIRE 过期时间,让冷数据自动从内存中消失,热数据始终常驻;ClickHouse 的 TTL 机制将 30 天前的分区自动搬迁到冷存储卷,零外部工具依赖。当 pt-archiver--max-lag=5 检测到从库延迟超过 5 秒自动暂停归档,当 ES ILM 的 searchable_snapshot 将冷索引挂载到 S3 上、首次搜索延迟从 10ms 升到 500ms 但成本降低 80%,当 Spring Batch 的迁移 Job 在凌晨 2 点自动执行并在失败后从断点继续——这些机制的背后,是对**“数据温度如何度量、迁移如何零停机、成本如何量化”**的深度设计。

本文将从冷热的定义与策略出发,到五种工具的完整迁移机制,再到可量化的存储成本对比,最终以系统设计实战和面试题收尾,完整拆解冷热分离存储架构的工程内核。

核心要点

  • 冷热定义:热数据(SSD,高频访问,近 7 天)→ 温数据(SSD/HDD,低频访问,7-30 天)→ 冷数据(HDD/对象存储,极少访问,>30 天)→ 归档数据(对象存储/磁带,合规保留,>90 天)。
  • 成本驱动:SSD 1GB/月 ≈ 0.5-1 元,HDD 1GB/月 ≈ 0.1-0.3 元,S3 1GB/月 ≈ 0.02-0.05 元。冷热分离后存储成本可降低 80-95%。
  • 三种策略时间驱动(简单,适合时效性数据)、访问频率驱动(精细,适合无明确时效的热点数据)、混合驱动(时间 + 频率双重判断)。
  • MySQLpt-archiver 在线归档(不锁表 + --max-lag 安全保护)+ 表分区 + 冷表索引精简。
  • Elasticsearch:ILM 四阶段(Hot-Warm-Cold-Delete)+ shrink + forcemerge + searchable_snapshot 可搜索快照。
  • Redismaxmemory-policy 内存淘汰(allkeys-lru/allkeys-lfu)+ EXPIRE 过期 + 多级缓存(Caffeine L1 + Redis L2 + MySQL L3)。
  • ClickHouse:TTL 自动分区搬迁(TO VOLUME 'cold')+ 多存储卷。
  • Spring Batch:迁移流水线(ItemReader/ItemWriter/Tasklet)+ 断点续传 + 定时调度。
  • 跨系列关联:与分库分表叠加(第1篇)、多租户冷热策略(第3篇)、ES Segment 合并(ES系列第3篇)、Redis 淘汰算法(Redis系列第6篇)。

文章组织架构图

flowchart TD
    1[1. 冷热分离核心概念] --> 2[2. 三种冷热分离策略]
    2 --> 3[3. MySQL 冷热分离: pt-archiver + 表分区]
    2 --> 4[4. Elasticsearch 冷热分离: ILM]
    2 --> 5[5. Redis 冷热分离: maxmemory-policy + EXPIRE]
    2 --> 6[6. ClickHouse 冷热分离: TTL + 多存储卷]
    3 --> 7[7. Spring Batch 数据迁移流水线]
    4 --> 7
    5 --> 7
    6 --> 7
    7 --> 8[8. 存储成本量化对比]
    8 --> 9[9. 面试高频专题]

架构图说明

总览说明:全文 9 个模块从冷热分离的基础概念与策略出发,逐步深入到五种工具的迁移机制、Spring Batch 编排、成本量化对比,最后以面试题收尾。模块 1-2 建立前置知识,模块 3-6 是四大存储引擎的冷热分离实现,模块 7 是统一的迁移流水线,模块 8 是成本量化决策层,模块 9 面试巩固。

关键结论:冷热分离的核心是“存储成本 vs 查询性能”的权衡。MySQL 的 pt-archiver 不锁表分批归档适合在线业务,ES ILM 的 searchable_snapshot 适合日志与搜索场景的冷数据存储,Redis 的 maxmemory-policy 适合内存冷热自动淘汰,ClickHouse TTL 适合大规模 OLAP 数据的自动生命周期管理。Spring Batch 提供了统一的迁移编排与断点续传能力。存储成本从 SSD 到 S3 可降低 95%,是大数据量场景下必须考虑的架构手段。


1. 冷热分离核心概念:冷热定义 + 成本驱动 + 架构模式 + 与分库分表叠加

1.1 冷热数据的四个温度等级

数据温度是数据访问活跃度的度量,通常以“最后一次访问时间”和“访问频率”为指标。我们将数据分为四个等级:

  • 热数据 (Hot Data):近 7 天内创建或被频繁读写的业务数据。典型如最近几天的订单、实时交易流水、当前热门文章。存储于 SSD内存,访问延迟 < 1ms (内存) / < 10ms (SSD),成本最高 (0.5~1 元/GB/月)。对应的存储系统:Redis、MySQL 热分区、ES Hot 节点。
  • 温数据 (Warm Data):7~30 天内的数据,查询频率明显降低,但仍有分析或偶尔回溯需求。可存储于 SSD 或 HDD,访问延迟 10~50ms,成本中等 (0.1~0.3 元/GB/月)。对应的存储系统:MySQL 温分区、ES Warm 节点。
  • 冷数据 (Cold Data):30 天以上的历史数据,极少被访问,但必须保留以备审计或合规查询。存储于 HDD 或对象存储,访问延迟 100ms~秒级,成本低廉 (0.02~0.05 元/GB/月)。对应的存储系统:MySQL 冷分区/归档表、ES Cold/Frozen 节点、S3/MinIO。
  • 归档数据 (Archive Data):超过 90 天或 1 年,仅用于法律合规或数据备份,几乎从不查询。存储于 对象存储或磁带,访问延迟分钟级甚至小时级,成本极低 (0.01 元/GB/月以下)。通常为只读不可变。
flowchart LR
    A[热数据<br/>SSD/内存<br/><10ms<br/>0.5-1元/GB/月] -->|7天后| B[温数据<br/>SSD/HDD<br/>10-50ms<br/>0.1-0.3元/GB/月]
    B -->|30天后| C[冷数据<br/>HDD/对象存储<br/>100ms-秒级<br/>0.02-0.05元/GB/月]
    C -->|90天/1年后| D[归档数据<br/>对象存储/磁带<br/>分钟-小时<br/><0.01元/GB/月]

图1-冷热数据生命周期图

图表结构说明:本图横向展示了数据从产生到归档的完整生命周期,从左至右温度逐步降低,分别标注了各阶段的典型存储介质、访问延迟和单位存储成本。

核心流程解析:数据根据时间或访问频率自动在四个等级间流转。阈值可根据业务自定义(如热->温 7天,温->冷 30天)。流转动作由 pt-archiver、ILM、TTL 等机制自动触发。

关键技术细节:热数据层通常需要高并发写入和查询优化(如 MySQL 索引、ES 高频 refresh);温数据层可适度降低索引密度;冷数据层主要保证读吞吐,可接受较高延迟;归档数据层则要求不可变性和极低存储成本。

性能与成本分析:若 1TB 数据全部按热数据存储,月成本约 500~1000 元。若按 10% 热、20% 温、70% 冷存储,加权月成本可降至约 50~100 元,节省 90%。这种分层正是冷热分离的核心价值。

1.2 核心驱动力——存储成本 vs 查询性能

冷热分离的本质是在存储成本查询性能之间寻求帕累托最优。绝对性能(全 SSD)意味着极高成本,绝对低成本(全 S3)意味着不可接受的查询延迟。我们需要根据业务查询模式,将合适的数据放在合适的介质上。

以 1TB 数据保留 90 天为例,假设热数据 100GB,温数据 200GB,冷数据 700GB:

  • 全热存储 (SSD):1000GB × 0.8 元/GB/月 × 3 个月 = 2400 元(累计)。
  • 冷热分离 (热SSD + 温HDD + 冷S3):100GB×0.8 + 200GB×0.2 + 700GB×0.03 = 80 + 40 + 21 = 141 元/月,3 个月约 423 元,节省 82%

更关键的是,数据量随时间线性增长,全热方案的成本会持续飙升,而冷热分离方案的成本增长几乎只与冷存储有关,斜率极低。

1.3 两种架构模式

  • 单集群内冷热分层:同一 MySQL/ES 集群内,通过表分区或索引生命周期管理,将热数据和冷数据分布到不同存储节点或表空间上。优点是管理统一,查询可以透明跨分区;缺点是集群内节点角色复杂,冷数据占用在线集群资源(如 MySQL 冷分区仍占用实例内存)。
  • 跨存储介质冷热分离:热数据在 MySQL/ES,冷数据导出到 S3/HDFS 等外部存储。查询时需应用层路由(根据时间条件判断查热库还是冷库)或联邦查询引擎(如 Presto/Trino 统一查询入口)。优点是热集群轻量化,冷存储成本极低;缺点是跨系统联合查询复杂,需处理异构数据源,且冷数据查询延迟高。

应用层路由示例伪代码:

public List<Order> queryOrders(Date start, Date end) {
    if (start.after(coldThreshold)) {
        return hotDB.query(start, end);
    } else if (end.before(coldThreshold)) {
        return coldDB.query(start, end);
    } else {
        List<Order> hot = hotDB.query(start, coldThreshold);
        List<Order> cold = coldDB.query(coldThreshold, end);
        return merge(hot, cold);
    }
}

联邦查询则利用 Presto 的 Catalog 连接多个数据源,一条 SQL 自动分发并合并结果。

1.4 冷热分离与分库分表叠加

冷热分离可与分库分表正交叠加(参考本系列第1篇)。假设已经按 user_id 分 4 个库,每个库再按 create_time 分为热表 (orders_hot) 和冷表 (orders_cold)。路由规则为:

  1. 分片键 user_id % 4 定位物理库。
  2. 时间范围路由到具体表:若查询范围完全在热区间,只查热表;完全在冷区间,只查冷表;跨区间则合并。

通过 ShardingSphere 的 Hint 路由复合分片算法,可以将 user_id 和时间同时作为分片条件。热分片实例使用高配 SSD 机器,冷分片使用低配 HDD 机器,实现精细的成本控制。


2. 三种冷热分离策略:时间驱动 / 访问频率驱动 / 混合驱动

2.1 时间驱动

原理:以数据创建时间或更新时间作为唯一判断标准。例如 created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)

优点:规则简单,执行高效,不依赖外部监控,易于自动化。

缺点:无法区分“旧但仍频繁访问”的数据(如热门老文章),可能误迁热数据。一旦数据被迁移,再次访问需穿透到冷存储,可能引起性能抖动。

适用场景:时效性强的数据(订单、日志、监控指标、账单流水)。

实现示例:MySQL 分区 PARTITION BY RANGE (TO_DAYS(created_at)) 自动将数据写入对应分区;定时任务每日修改分区定义或直接 TRUNCATE 旧分区。

2.2 访问频率驱动

原理:追踪数据的访问次数(access_count)或最后访问时间(last_access_time)。例如 last_access_time < DATE_SUB(NOW(), INTERVAL 7 DAY) AND access_count < 5

优点:精细,能准确识别真正的冷数据,避免热数据误迁。可适应突然的热点复活(如旧文章被重新推荐),通过重新加热机制恢复性能。

缺点:需要额外的访问计数器或监控。频繁更新 access_count 可能成为写入热点,需要使用 Redis 计数器 + 定期落库,或使用 HyperLogLog 等近似算法。迁移逻辑复杂,需考虑计数器重置和衰减。

适用场景:无明确时间边界的热点数据(文章、帖子、用户资料、商品详情)。

访问计数器设计:每个数据项在 Redis 中维护 access:<id> 计数器,每次读取时 INCR。每天定时将计数器刷入 MySQL 并重置。迁移任务根据 MySQL 中的 weekly_accesslast_access_time 判断。

2.3 混合驱动

原理:先按时间初筛(如 created_at > 90 days),再按访问频率确认(access_count < 3),双重判断后才迁移。可有效避免将“古老但突然爆火”的数据误迁。

优点:兼顾安全与精细,最大限度平衡性能与成本。

缺点:逻辑最复杂,需同时维护时间与频率两个维度的元数据,并设计合理的衰减模型。

适用场景:超大流量平台(社交 Feed 流、电商商品)、成本敏感且访问模式多变。

策略判决伪代码

def should_migrate(item):
    days_since_creation = (now() - item.created_at).days
    if days_since_creation < 30:
        return False  # 热数据
    if days_since_creation > 90 and item.access_count < 2:
        return True   # 长期冷数据
    if item.last_access_time and (now() - item.last_access_time).days > 30:
        return True
    return False

2.4 选型决策表

策略复杂度准确性延迟敏感额外开销典型场景
时间驱动不敏感日志、IoT 时序
频率驱动较敏感计数器维护内容平台、社交网络
混合驱动最高敏感较高电商、大规模用户平台

3. MySQL 冷热分离:pt-archiver 在线归档 + 表分区 + 冷表索引精简

3.1 pt-archiver 深度原理

pt-archiver 是 Percona Toolkit 中的在线归档利器,其核心是基于索引的游标分批处理。它首先获取一批行的主键值,然后在同一个事务中执行目标库 INSERT 和源库 DELETE,提交后继续下一批。

详细流程

  1. 从源表按索引(通常是主键)升序 SELECT id, col1, col2 ... WHERE ... ORDER BY id LIMIT 1000,记录本批最大 id 作为游标位置。
  2. 对这 1000 行,在目标表执行 INSERT INTO dest (...) VALUES (...) (使用 --replace 可转为 REPLACE INTO)。
  3. 在同一事务中,执行 DELETE FROM source WHERE id IN (id1, id2, ...)DELETE FROM source WHERE id >= batch_start AND id <= batch_end--bulk-delete)。
  4. 提交事务,根据 --sleep 休眠,再重复步骤 1,从上一批最大 id 的下一个开始。

安全性保证

  • 不锁表:普通 SELECTDELETE 均在主键索引上操作,不会升级为表锁。InnoDB 的行锁和间隙锁仅影响正在处理的行,并发 DML 几乎无影响。
  • 从库保护--max-lag 每批后检查 SHOW SLAVE STATUSSeconds_Behind_Master,若大于阈值则休眠等待,防止归档产生的 binlog 冲击从库。
  • 断点续传:由于按主键递增处理,即使进程崩溃,重启后可从上次最大 id 继续(需加 --resume 或记下进度)。

3.2 完整生产级命令与参数解读

pt-archiver \
  --source h=192.168.1.10,P=3306,D=order_db,t=orders,u=archive_user,p=xxx \
  --dest   h=192.168.2.20,P=3306,D=cold_db,t=orders_archive,u=cold_user,p=yyy \
  --where "created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)" \
  --limit 2000 \
  --txn-size 2000 \
  --bulk-delete \
  --sleep 0.05 \
  --max-lag 3 \
  --check-slave-lag h=192.168.1.11,P=3306 \
  --progress 50000 \
  --statistics \
  --dry-run   # 正式运行时去掉

参数深度解析

  • --bulk-delete:使用 DELETE FROM source WHERE id >= ? AND id <= ? 范围删除,比 IN 列表效率高,减少事务日志。
  • --txn-size 应与 --limit 一致,确保一个批次为一个事务。
  • --sleep:控制批次间隔,0.05 秒可在吞吐和负载间取得平衡。若主库写入压力大,可调至 0.2。
  • --max-lag 3:此处 3 秒为推荐值,可根据从库承载能力调整。
  • --check-slave-lag:明确指定检查哪个从库,支持多个从库。
  • --dry-run:仅输出 SQL 而不执行,用于验证。

冷表索引精简:冷数据查询场景通常只有按主键或时间范围。归档后可删除二级索引:

ALTER TABLE orders_archive DROP INDEX idx_user_status;
ALTER TABLE orders_archive DROP INDEX idx_order_date;
-- 只保留主键和可能需要的时间索引

索引空间可减少 50%~70%。

3.3 表分区方案

若选择单集群内冷热分层,可使用 MySQL 分区表,将不同分区部署到不同表空间(通过 DATA DIRECTORY 或存储策略):

CREATE TABLE orders (
    id BIGINT NOT NULL,
    user_id BIGINT,
    created_at DATETIME NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (TO_DAYS(created_at)) (
    PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
    PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
    -- 每月增加分区
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

pt-archiver 也可用来定期 TRUNCATEDROP 最老的分区,释放空间。

3.4 MySQL pt-archiver 在线归档流程图

sequenceDiagram
    participant App as 应用服务
    participant Master as 主库(热库)
    participant Slave as 从库
    participant Archiver as pt-archiver
    participant Cold as 冷库

    Archiver->>Master: SELECT id, ... FROM orders<br/>WHERE created_at < NOW()-INTERVAL 30 DAY<br/>ORDER BY id LIMIT 2000
    Master-->>Archiver: 返回2000行,记录最大id=52000
    Archiver->>Cold: INSERT INTO orders_archive (...) VALUES (...)
    Cold-->>Archiver: OK
    Archiver->>Master: DELETE FROM orders WHERE id BETWEEN 50001 AND 52000
    Master-->>Archiver: OK
    Archiver->>Master: COMMIT
    Archiver->>Slave: SHOW SLAVE STATUS
    Slave-->>Archiver: Seconds_Behind_Master = 2 (小于3,继续)
    Archiver->>Archiver: SLEEP 0.05s
    Archiver->>Master: SELECT ... WHERE id > 52000 ORDER BY id LIMIT 2000
    Note over Archiver,Slave: 循环直到无数据返回
    Archiver-->>App: 归档完成

图2-MySQL pt-archiver 在线归档流程图

图表结构说明:时序图展示了 pt-archiver 与主库、冷库、从库的交互过程,体现了分批游标、事务保护、延迟检查等核心机制。

核心流程解析:每一批都是独立事务,包含“读源库、写目标库、删源库”,提交后检查延迟并休眠。游标基于主键升序,确保每次处理的数据不重叠,且能断点续传。

关键技术细节--bulk-delete 使用范围删除比 IN 列表更高效,尤其是当批大小较大时。--sleep--max-lag 协同工作,既保障主库负载可控,又防止从库延迟过高。

性能与成本分析:在 SSD 实例上,2000 行/批、0.05 秒间隔,吞吐量约为 20,000 行/秒,对主库 CPU 占用约 5%~10%,从库延迟可控制在 1 秒内。归档完成后可通过 OPTIMIZE TABLE 回收表空间,但需在维护窗口执行。


4. Elasticsearch 冷热分离:ILM 四阶段 + shrink + forcemerge + searchable_snapshot

4.1 ILM 策略设计与节点角色

ES 的 Index Lifecycle Management (ILM) 通过自动化策略将索引在四个阶段间转移,每个阶段对应特定的节点角色和存储介质。

节点角色规划(以 10 节点集群为例):

  • 3 个 Hot 节点 (data_hot):NVMe SSD,负责写入和近期搜索,硬件要求高 IOPS。
  • 4 个 Warm 节点 (data_warm):SATA SSD 或 HDD,存储只读索引。
  • 2 个 Cold 节点 (data_cold):HDD,挂载可搜索快照,本地缓存部分数据。
  • 1 个 Frozen 节点(可选):仅存元数据,搜索需全量从快照拉取。

配置 elasticsearch.yml

node.roles: [ data_hot ]
path.data: /data/ssd

4.2 完整 ILM 策略示例

PUT _ilm/policy/logs_policy
{
  "phases": {
    "hot": {
      "min_age": "0ms",
      "actions": {
        "rollover": {
          "max_primary_shard_size": "50gb",
          "max_age": "1d"
        },
        "set_priority": {
          "priority": 100
        }
      }
    },
    "warm": {
      "min_age": "3d",
      "actions": {
        "allocate": {
          "number_of_replicas": 1,
          "require": { "data": "warm" }
        },
        "shrink": {
          "number_of_shards": 1
        },
        "forcemerge": {
          "max_num_segments": 1
        },
        "set_priority": {
          "priority": 50
        }
      }
    },
    "cold": {
      "min_age": "7d",
      "actions": {
        "allocate": {
          "number_of_replicas": 0,
          "require": { "data": "cold" }
        },
        "searchable_snapshot": {
          "snapshot_repository": "s3_backup",
          "force_merge_index": true
        },
        "set_priority": {
          "priority": 0
        }
      }
    },
    "delete": {
      "min_age": "90d",
      "actions": {
        "delete": {}
      }
    }
  }
}

关键动作解读

  • Warm 阶段allocate 将分片转移到 warm 节点,shrink 将分片数缩减为 1(假设原为 4),减少内存占用;forcemerge 强制合并为 1 个段,加速搜索并清除删除标记。
  • Cold 阶段allocate 转移到 cold 节点,副本数设为 0(由快照保证可靠性),searchable_snapshot 将数据上传到 S3,本地只保留部分缓存。

4.3 shrink 与 forcemerge 原理

  • shrink:通过硬链接方式创建新的索引,将源分片合并。要求源索引只读,且所有分片必须在同一节点。ILM 会自动完成迁移和只读设置。
  • forcemerge:执行 Lucene 段合并,消耗大量 IO 和 CPU,因此仅在 Warm 阶段(低峰)执行,合并后搜索只需打开一个段,延迟显著降低。

4.4 可搜索快照深度解析

可搜索快照(searchable snapshot)将索引数据物化为快照存储于对象存储(S3、MinIO 等),本地节点上仅保留索引元数据和部分热点数据缓存。搜索时:

  • 如果所需数据块在本地缓存,直接返回(毫秒级)。
  • 如果不在,从 S3 拉取,首次请求延迟增加数百毫秒,但后续缓存命中后恢复正常。

存储成本:快照数据成本仅为 S3 存储费 + 少量 GET 请求费,本地可只保留 10%~20% 的缓存磁盘,大幅降低冷数据存储成本。

4.5 ILM 四阶段流转状态图

stateDiagram-v2
    [*] --> Hot: 新建索引
    Hot --> Warm: min_age=3d
    Warm --> Cold: min_age=7d
    Cold --> Delete: min_age=90d
    Delete --> [*]

    state Hot {
        [*] --> Active: 写入活跃
        Active --> Rollover: max_size=50GB/max_age=1d
        Rollover --> [*]
    }
    state Warm {
        [*] --> Shrink: 分片合并
        Shrink --> Forcemerge: 段合并
        Forcemerge --> [*]
    }
    state Cold {
        [*] --> Snapshot: 创建快照
        Snapshot --> SearchableSnapshot: 挂载为可搜索快照
        SearchableSnapshot --> [*]
    }

图3-ES ILM 四阶段流转状态图

图表结构说明:状态图展示了 ILM 策略下索引从创建到删除的生命周期,并分解了各阶段内部的核心动作。

核心流程解析:索引创建后处于 Hot 阶段处理写入,达到 rollover 条件后滚动新索引;旧索引进入 Warm 只读并执行优化;之后转入 Cold 阶段通过快照存放到 S3;达到 90 天删除。

关键技术细节shrinkforcemerge 必须在索引只读状态下执行,ILM 会自动先设置索引为只读。Cold 阶段的 searchable_snapshot 可配置 shared_cache 模式,允许指定缓存大小。

性能与成本分析:1TB 原始数据(经压缩后约 300GB)全 SSD 存储月成本约 240 元,使用 ILM 后 Cold 阶段 70% 数据存储在 S3,月成本仅约 30 元,整体降至约 100 元,节省 58%。


5. Redis 冷热分离:maxmemory-policy 淘汰 + EXPIRE + 多级缓存

Redis 的冷热分离是基于内存容量的自动淘汰,热数据常驻内存,冷数据被逐出或过期。核心机制包括:

5.1 maxmemory-policy 策略详解

策略适用场景注意事项
noeviction不允许淘汰,内存满时报错不适用冷热分离
allkeys-lru热点数据集中在部分 key,最近未访问的优先淘汰对偶发扫描敏感,可能挤出真正的热 key
allkeys-lfu访问频率差异大,低频 key 优先淘汰,抗扫描能力强需要计算频率衰减,内存和 CPU 开销稍高
volatile-lru仅淘汰设置了 TTL 的 key,永久 key 不受影响适合混合永久和临时数据场景
volatile-lfu仅淘汰带 TTL 的 key 中访问频率最低的同上

近似算法实现:Redis 使用采样近似,默认随机采样 5 个 key,淘汰其中最符合策略的。可通过 maxmemory-samples 10 提高精度,但增加 CPU 开销。

5.2 EXPIRE 主动淘汰

对于时效性强的数据,设置过期时间:

SETEX session:token:abc123 7200 "user_id:1001"   # 2小时过期

Redis 惰性删除(访问时检查)+ 定期扫描(每秒10次,随机抽取带过期 key 删除),保证冷数据及时清除。

5.3 多级缓存冷热架构

在应用层,缓存通常分为三级(详见本系列第6篇):

  1. L1 本地缓存(Caffeine):极热数据,微秒级,容量 100MB~1GB。
  2. L2 分布式缓存(Redis):热数据,毫秒级,容量数十 GB。
  3. L3 持久化存储(MySQL/ES):温/冷数据,磁盘级。

当数据被 Redis 淘汰后,下次请求 miss,从 L3 加载并重新放入 Redis(加热)。这种机制天然实现了“按访问频率自动冷热分离”。

5.4 Redis 淘汰机制流程图

flowchart TD
    A[请求 Key] --> B{内存中命中?}
    B -->|Y| C[返回数据]
    B -->|N| D[查询后端 DB]
    D --> E[数据写入 Redis]
    E --> C
    F[内存达到 maxmemory] --> G{淘汰策略}
    G -->|allkeys-lru| H[采样N个Key, 淘汰 idle 最长的]
    G -->|allkeys-lfu| I[采样N个Key, 淘汰频率最低的]
    G -->|volatile-*| J[仅在带 TTL 的 Key 中采样淘汰]
    H --> K[Key 被逐出]
    I --> K
    J --> K

图4-Redis maxmemory-policy 淘汰机制图

图表结构说明:流程图展示了 Redis 处理缓存请求和内存淘汰的两条路径。

核心流程解析:当内存满时,Redis 根据策略从样本中选出要淘汰的 key 并删除。被淘汰的 key 如果后续被请求,会再次从数据库加载,重新成为热数据。

关键技术细节:LRU 和 LFU 均基于对象的 lru 字段(24 bits),记录了访问时间或频率计数。LFU 使用对数计数器并随时间衰减,防止历史高频键永不淘汰。

性能与成本分析:通过淘汰策略,Redis 可以用 8GB 内存支撑 100GB+ 数据集的访问热度,只要大部分请求命中热数据。相比全量存储于内存,成本降低 90% 以上。


6. ClickHouse 冷热分离:TTL 自动搬迁 + 多存储卷

6.1 TTL 机制与存储策略配置

ClickHouse 的 TTL(Time To Live)可作用于表或列,配合多磁盘存储策略自动搬迁数据。首先定义存储策略:

<storage_configuration>
    <disks>
        <hot_disk>
            <path>/data/hot/</path>
        </hot_disk>
        <cold_disk>
            <path>/data/cold/</path>
        </cold_disk>
    </disks>
    <policies>
        <hot_cold_policy>
            <volumes>
                <hot_volume>
                    <disk>hot_disk</disk>
                </hot_volume>
                <cold_volume>
                    <disk>cold_disk</disk>
                </cold_volume>
            </volumes>
        </hot_cold_policy>
    </policies>
</storage_configuration>

创建表时指定:

CREATE TABLE logs (
    event_time DateTime,
    level String,
    message String
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (event_time, level)
TTL event_time + INTERVAL 30 DAY TO VOLUME 'cold_volume',
    event_time + INTERVAL 90 DAY DELETE
SETTINGS storage_policy = 'hot_cold_policy';

6.2 TTL 搬迁原理

后台 Merge 线程会定期检查每个 Part 中的最小和最大时间,如果 Part 内所有行都满足 TTL 搬迁条件,则整个 Part 被移动到冷卷;如果只有部分满足,则会发生内部拆分和移动。DELETE TTL 若整个 Part 过期,直接删除文件。

性能影响:搬迁是文件系统级别的移动(rename),速度极快,不阻塞读写。写入只往热卷写,查询可自动跨卷读取,对用户透明。

6.3 与 pt-archiver 对比

特性ClickHouse TTLMySQL pt-archiver
实现机制内置引擎异步自动外部工具 + 定时任务
搬迁粒度Part 级(文件移动)行级(SELECT + DELETE)
对写入的影响无阻塞轻微负载,可通过 sleep 控制
查询透明度完全透明,自动合并多卷数据需应用路由或视图
支持删除Part 级快速删除,无 Binlog 压力逐行删除,产生大量 Binlog
适用场景时序数据、日志分析在线业务订单、事务数据

6.4 ClickHouse TTL 搬迁流程图

flowchart TD
    A["写入"] --> B["Hot Volume<br/>SSD"]
    B --> C{"后台 Merge 检查 TTL"}
    C -- "Part 内全过期" --> D["直接删除 Part"]
    C -- "满足搬迁条件" --> E["文件移动至 Cold Volume<br/>HDD"]
    C -- "未触发" --> B
    E --> F["数据仍可查询,延迟稍高"]
    D --> G["数据消失"]

    classDef decision fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
    classDef process fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#1e293b
    class C decision
    class A,B,D,E,F,G process

图5-ClickHouse TTL 多存储卷搬迁图

图表结构说明:流程图展示了 ClickHouse 数据写入热卷,以及后台 Merge 依据 TTL 进行搬迁或删除的过程。

核心流程解析:数据分区被 TTL 规则驱动,自动在热卷和冷卷间流转。查询时,ClickHouse 会并行读取热卷和冷卷的 Parts,合并结果返回。

关键技术细节:可以通过 ALTER TABLE ... MODIFY TTL 在线调整策略。使用 system.parts 表可监控 Part 所在磁盘和 TTL 信息。

性能与成本分析:冷卷使用 HDD 存储,成本为 SSD 的 1/5。对于 90% 以上查询只覆盖热数据的情况,性能几乎不受影响。大规模日志分析场景的存储成本可节省 70% 以上。


7. Spring Batch 数据迁移流水线:Job/Step/Tasklet + 断点续传 + 定时调度

当跨存储介质迁移(如 MySQL 到 Hive,或 MySQL 到 S3)需要复杂转换时,Spring Batch 提供了强大的批处理框架。

7.1 迁移 Job 设计架构

一个典型的 MySQL 热库到冷库(或其他存储)的迁移 Job 分为三个 Step:

  • Step1 (数据读取与写入)JdbcPagingItemReader 从热库分页读取满足条件的记录,JdbcBatchItemWriter 批量写入冷库。
  • Step2 (数据清理)Tasklet 执行 DELETE 操作,清理热库已迁移的数据(可多次执行,如每次删 10 万行)。
  • 可选 Step3 (验证)Tasklet 进行计数校验,确保数据不丢失。

断点续传机制:Spring Batch 的 JobRepository 将每个 Step 的 commit_countlast_commit_position 等状态持久化到 BATCH_STEP_EXECUTION 表。当 Job 失败后重启,框架会自动从上次成功的位置继续读取(前提是 Reader 实现了 ItemStream 且数据库支持游标)。

7.2 完整代码示例(Spring Batch 5.x, JDK 17)

@Configuration
@EnableBatchProcessing
public class ColdMigrationJobConfig {

    @Autowired private JobRepository jobRepository;
    @Autowired private PlatformTransactionManager transactionManager;
    @Autowired private DataSource hotDataSource;
    @Autowired private DataSource coldDataSource;

    @Bean
    public Job coldMigrationJob() {
        return new JobBuilder("coldMigrationJob", jobRepository)
                .start(readWriteStep())
                .next(cleanupStep())
                .next(verifyStep())
                .build();
    }

    // Step1: 读取热数据并写入冷库,使用 Chunk 模式
    @Bean
    public Step readWriteStep() {
        return new StepBuilder("readWriteStep", jobRepository)
                .<Order, Order>chunk(1000, transactionManager)
                .reader(reader(null))
                .writer(writer())
                .faultTolerant()
                .retryLimit(3)
                .retry(DataAccessException.class)
                .build();
    }

    @Bean
    @StepScope
    public JdbcPagingItemReader<Order> reader(
            @Value("#{jobParameters['cutoffDate']}") String cutoffDate) {
        return new JdbcPagingItemReaderBuilder<Order>()
                .name("hotOrderReader")
                .dataSource(hotDataSource)
                .selectClause("SELECT id, user_id, amount, created_at")
                .fromClause("FROM orders")
                .whereClause("WHERE created_at < :cutoffDate ORDER BY id ASC")
                .parameterValues(Map.of("cutoffDate", cutoffDate))
                .sortKey("id")
                .pageSize(1000)
                .rowMapper(new OrderRowMapper())
                .saveState(true)  // 开启状态保存,支持断点续传
                .build();
    }

    @Bean
    public JdbcBatchItemWriter<Order> writer() {
        return new JdbcBatchItemWriterBuilder<Order>()
                .dataSource(coldDataSource)
                .sql("INSERT INTO orders_archive (id, user_id, amount, created_at) " +
                     "VALUES (:id, :userId, :amount, :createdAt)")
                .beanMapped()
                .build();
    }

    // Step2: 清理热库数据,采用 Tasklet 循环删除,避免大事务
    @Bean
    public Step cleanupStep() {
        return new StepBuilder("cleanupStep", jobRepository)
                .tasklet(cleanupTasklet(null), transactionManager)
                .build();
    }

    @Bean
    @StepScope
    public Tasklet cleanupTasklet(
            @Value("#{jobParameters['cutoffDate']}") String cutoffDate) {
        return (contribution, chunkContext) -> {
            JdbcTemplate jdbc = new JdbcTemplate(hotDataSource);
            int deletedTotal = 0;
            int batchSize = 10000;
            int deleted;
            do {
                // 使用 LIMIT 分批删除,避免长事务
                deleted = jdbc.update(
                    "DELETE FROM orders WHERE created_at < ? LIMIT ?",
                    cutoffDate, batchSize);
                deletedTotal += deleted;
                // 可适当休眠
                Thread.sleep(100);
            } while (deleted >= batchSize);
            System.out.println("清理完成,共删除:" + deletedTotal + " 条");
            return RepeatStatus.FINISHED;
        };
    }

    // Step3: 数据校验 Tasklet
    @Bean
    public Step verifyStep() {
        return new StepBuilder("verifyStep", jobRepository)
                .tasklet(verifyTasklet(), transactionManager)
                .build();
    }

    @Bean
    public Tasklet verifyTasklet() {
        return (contribution, chunkContext) -> {
            // 简单计数对比逻辑
            return RepeatStatus.FINISHED;
        };
    }
}

断点续传细节JdbcPagingItemReader 在每次 chunk 提交时,会将当前 id 的最大值作为读取位置保存在 ExecutionContext 中。重启时,Reader 会从该位置继续分页,从而避免重复读取或遗漏。

定时调度

@Scheduled(cron = "0 0 2 * * ?")
public void performMigration() throws Exception {
    JobParameters params = new JobParametersBuilder()
            .addString("cutoffDate", LocalDate.now().minusDays(30).toString())
            .addLong("time", System.currentTimeMillis())
            .toJobParameters();
    jobLauncher.run(coldMigrationJob(), params);
}

监控与告警:通过 Actuator 端点 /actuator/batch 可获取 Job 执行状态、失败异常等。可结合 Prometheus 导出指标,配置报警规则。


8. 存储成本量化对比:SSD vs HDD vs S3 的成本计算模型与实例

8.1 存储介质单位成本(云环境参考)

存储类型典型单价 (元/GB/月)IOPS / 吞吐能力适用数据温度
高性能 SSD0.8 ~ 1.2高 IOPS,低延迟热数据
普通 SSD0.5 ~ 0.8中高 IOPS热/温数据
HDD0.1 ~ 0.3低 IOPS,顺序读好温/冷数据
对象存储S30.02 ~ 0.05高吞吐,延迟高冷/归档数据
归档存储0.005 ~ 0.01极低,分钟级取回归档数据

8.2 实际案例成本计算

假设:日志平台日增 200GB 原始数据,压缩率 0.3(实际存储 60GB/天)。数据保留 90 天。热数据定义 3 天内,占总查询 90%。

全热存储方案:90 天总数据量 = 60GB/天 × 90 = 5400GB。全部用高性能 SSD,月成本 5400 × 0.8 = 4320 元/月

冷热分离方案(ES ILM):

  • 热数据(3天):60GB × 3 = 180GB,SSD 成本 180 × 0.8 = 144 元/月。
  • 冷数据(87天):60GB × 87 = 5220GB。使用 S3 存储,成本 5220 × 0.03 = 156.6 元/月。
  • 冷节点本地缓存:预留 10% 缓存 522GB × 0.3 ≈ 157 元(HDD)。
  • 总成本约 144 + 157 + 156.6 ≈ 457.6 元/月节省 89.4%

8.3 存储成本对比图

barChart
    title 存储成本对比 (日增200GB日志, 90天保留)
    "全SSD" : 4320
    "热SSD+冷S3 (ILM)" : 458
    "热SSD+冷HDD" : 1200
    "全S3" : 162

图6-存储成本对比图

图表结构说明:柱状图直观对比了四种存储方案下的月成本,突出冷热分离的成本优势。

核心流程解析:随着冷数据从 SSD 向 S3 迁移,成本呈指数级下降。全 S3 虽然成本最低,但查询性能无法满足热数据要求。

关键技术细节:实际还需考虑数据传出费用(S3 GET)、API 调用费、跨区域传输等,但通常不超过存储费的 15%。

性能与成本分析:冷热分离方案在几乎不牺牲热数据查询性能的前提下,使整体存储成本降低 80%~95%,是海量数据系统的必然选择。


9. 面试高频专题

Q1: 什么是冷热分离?热、温、冷、归档数据分别如何定义和存储? 一句话回答:冷热分离是根据数据访问频率和时效性将数据分层存储的策略,热数据存 SSD/内存,温数据存 HDD/SSD,冷数据存 HDD/对象存储,归档数据存对象存储/磁带。 详细解释:数据温度通过时间(创建/修改)和访问频率衡量。热数据(近7天高频)对性能敏感,使用 SSD 或 Redis;温数据(7-30天低频)对成本略敏感,可迁至 HDD;冷数据(>30天极少访问)追求极低成本,放至 S3/MinIO;归档数据(>90天合规保留)几乎不查询,使用冷存储或磁带。分层后存储成本逐级降低一个数量级。 多角度追问

  • 为什么不用全 SSD?海量数据下成本不可承受,且大部分数据很少访问,全 SSD 浪费。
  • 冷热分离对应用透明吗?取决于实现:ES ILM/ClickHouse TTL 透明;MySQL 归档表或 S3 需应用路由或联邦查询。
  • 如何保证归档数据安全?使用对象存储的多版本、WORM 锁定、跨区域复制。 加分回答:可以结合数据价值衰减曲线动态调整阈值,实现更精细的分层。

Q2: 冷热分离有哪三种策略?时间驱动和访问频率驱动各适合什么场景? 一句话回答:时间驱动、访问频率驱动、混合驱动。时间驱动适合日志/订单等时效性数据,频率驱动适合文章/帖子等无明确时间边界的热点数据。 详细解释:时间驱动简单可靠,按 created_at 迁移,但不能区分旧热点。频率驱动按访问次数或最后访问时间判断,更精准,但需额外监控(如 Redis 计数器)和衰减模型。混合驱动先时间初筛再频率确认,兼顾安全与精准,适用于大流量平台。 多角度追问

  • 如何实现频率驱动?在应用层或缓存层统计,定期写入数据库,迁移任务读取频率字段。
  • 如果旧数据突然变热,频率驱动如何响应?可设计“回温”机制:当冷数据被访问时,从冷存储加载并更新访问计数,下次迁移任务可重新识别。
  • 混合驱动中频率阈值如何确定?分析 P99 访问分布,结合业务容忍度设定。 加分回答:引入时间衰减因子,使频率计数随时间指数下降,更能反映近期热度。

Q3: MySQL 的 pt-archiver 如何实现在线归档?--max-lag 参数起什么作用? 一句话回答pt-archiver 通过主键游标分批迁移、每批独立事务实现不锁表归档;--max-lag 监控从库延迟并自动暂停归档,保护主从同步。 详细解释pt-archiver 循环执行 SELECT ... LIMITINSERT 目标DELETE 源表(同一事务)。--max-lag=5 在每批后检查 Seconds_Behind_Master,若超过 5 秒则 sleep 直至恢复,防止归档产生的 binlog 冲击从库读服务。此外 --sleep 控制批间间隔,降低主库负载。 多角度追问

  • 无主键表如何归档?指定唯一键作为游标,或使用 --no-delete 归档后手动清理。
  • 归档后源表空间不释放怎么办?需在低峰期执行 OPTIMIZE TABLE,会锁表,应谨慎。
  • 如何校验归档完整性?pt-table-checksum 或对比源表与目标表行数。 加分回答:使用 --bulk-delete--limit 2000,事务大小可控且高效。

Q4: ES 的 ILM 四阶段各自做什么?searchable_snapshot 原理是什么? 一句话回答:Hot 写入和滚动,Warm 合并分片和段,Cold 迁至可搜索快照,Delete 删除。searchable_snapshot 将索引存为 S3 快照并按需本地缓存。 详细解释:Hot 阶段用 SSD,通过 rollover 控制索引大小;Warm 阶段索引只读,执行 shrink(减少分片)和 forcemerge(合并段)优化资源;Cold 阶段执行 searchable_snapshot,将数据上传到对象存储,本地只保留元数据和热点缓存,查询时从 S3 拉取缺失块,首次延迟高但成本降低 80%;Delete 阶段到期删除。 多角度追问

  • forcemerge 为何在 Warm 做?因为该阶段索引已无写入,且只读后可安全合并,减少后续搜索开销。
  • 可搜索快照对内存有要求吗?需要本地缓存磁盘,一般建议 10% 的索引大小,使用 SSD 或 HDD。
  • Cold 阶段搜索延迟能否接受?对于日志分析偶尔查询可接受秒级延迟,频繁查询场景不适合。 加分回答:ES 7.12+ 的 frozen tier 更进一步,本地几乎不缓存数据,查询时全量从快照下载,成本更低。

Q5: Redis maxmemory-policy 有哪几种?allkeys-lruallkeys-lfu 区别? 一句话回答allkeys-lru 淘汰最久未使用的键,适合热点集中场景;allkeys-lfu 淘汰使用频率最低的键,适合访问频率差异明显、需抗扫描的场景。 详细解释allkeys-lru 近似 LRU,只关心最近访问时间,实现简单,但在“批量冷数据扫描”时可能误淘汰热数据。allkeys-lfu 近似 LFU,通过对数计数器记录频率并随时间衰减,更能保留真正的高频键,但消耗更多内存和 CPU。volatile-lru/lfu 仅淘汰带 TTL 的键。 多角度追问

  • 什么是 LFU 衰减?防止历史高频键永不淘汰,系统定期减少计数,使最近不访问的键逐渐降温。
  • volatile-lru 何时使用?当有永久常驻的配置 key 时,只淘汰临时数据。
  • 如何监控淘汰情况?INFO statsevicted_keysINFO memoryused_memory加分回答:可调整 maxmemory-samples 平衡精度和 CPU 开销,LFU 模式下建议增加到 10。

Q6: ClickHouse TTL 如何实现冷热分离?TO VOLUME 语法如何工作? 一句话回答TTL event_time + INTERVAL 30 DAY TO VOLUME 'cold' 将过期分区从热卷移至冷卷,由后台 Merge 线程异步执行。 详细解释:ClickHouse MergeTree 表定义 TTL,后台 Merge 检查 Part 内所有行是否满足 TTL 条件,满足则整个 Part 移动到另一存储卷(文件 rename)。若部分满足,则拆分并移动。DELETE TTL 满足则直接删除 Part,效率极高。多存储卷需在存储策略中定义磁盘和卷。 多角度追问

  • TTL 搬迁是原子性的吗?文件移动是原子性操作,不会出现中间态。
  • 搬迁对查询的影响?查询会自动合并热卷和冷卷数据,用户透明。
  • 如何监控 TTL 进度?查询 system.parts 表,看 disk_namettl_info加分回答:列级 TTL 可自动删除或移动特定列,节省更多空间。

Q7: Spring Batch 如何编排冷热分离迁移任务?断点续传如何实现? 一句话回答:通过 Job/Step/Tasklet 定义迁移流水线,JdbcPagingItemReader 分页读取,JdbcBatchItemWriter 批量写入;断点续传依赖 JobRepository 保存读取位置(分页游标),重启后从上次处继续。 详细解释:Step 使用 Chunk 模式,每处理一个 chunk 提交事务并记录 commit_count。Reader 基于主键排序分页,每页读取后保存当前最大 id 到 ExecutionContext。失败重启时,Reader 从 ExecutionContext 恢复上次位置继续分页,确保不重不漏。 多角度追问

  • 如果清理 Step 失败但数据已写冷库怎么办?保证冷库写入幂等(如使用 REPLACE INTO),清理重试。
  • 如何并行加速迁移?使用 Partitioner 按 id 范围分区,启动多线程并发执行 Step。
  • 如何监控迁移任务?Actuator 端点或自定义监听器记录日志和指标。 加分回答:可结合 Spring Cloud Task 实现分布式执行,记录每个 Job 实例的状态。

Q8: 冷热分离后冷数据如何查询?联邦查询引擎解决什么问题? 一句话回答:可通过应用层路由(代码判断时间查热/冷库)或联邦查询引擎(Presto/Trino)统一 SQL 查询多个异构源。 详细解释:应用层路由简单,但耦合度高;联邦查询引擎提供统一接口,连接 MySQL 热库和 Hive/S3 冷数据,一条 SQL 自动下推查询并合并,对应用完全透明。代价是查询延迟可能较高,尤其跨源 JOIN 时需拉取大量数据。 多角度追问

  • Trino 如何优化查询?利用分区裁剪、谓词下推,只扫描需要的数据。
  • 跨库事务怎么办?冷数据通常只读,无需事务;若需强一致,只能应用层保证。
  • S3 上的数据如何组织?通常以列存格式(Parquet)存储,按日期分区。 加分回答:可构建冷热分离视图,使用 ShardingSphere 虚拟库隐藏后端异构源。

Q9: 如何量化冷热分离的成本节省?1TB 数据全热 vs 冷热分离年化差异? 一句话回答:1TB 全 SSD 热存月成本约 800 元,冷热分离(热SSD+冷S3)月成本约 107 元,年化节省约 8300 元,降幅 86%。 详细解释:成本=数据量×月单价。假设热数据 100GB,冷数据 900GB。全 SSD 成本 1000×0.8=800 元/月。冷热分离:100×0.8 + 900×0.03 = 107 元/月。年节省 (800-107)×12=8316 元。数据量越大,节省越显著。 多角度追问

  • 对象存储 API 费用计入吗?GET 请求费用约占存储费 5%~10%,通常可忽略。
  • 压缩率如何影响?成本基于压缩后大小,ILM 快照和 ClickHouse 都有压缩,可减少存储量。
  • 是否考虑冷节点运营成本?冷节点通常用低配机器,分摊后成本仍极低。 加分回答:使用云平台的成本计算器,可精确模拟包括数据传输在内的全部费用。

Q10: ES ILM 的 forcemergeshrink 分别起什么作用?为什么只在 Warm 阶段做? 一句话回答shrink 减少主分片数以节省内存,forcemerge 合并段加速搜索;两者均要求索引只读,故在 Warm 阶段执行。 详细解释:Hot 阶段索引持续写入,无法只读。Warm 阶段数据不再变化,可安全 shrink(将多个分片硬链接合并为一个)和 forcemerge(合并段为 1 个)。shrink 降低集群状态和堆内存占用;forcemerge 减少搜索需打开的段数,并物理清除删除文档,提升查询效率。 多角度追问

  • forcemerge 有何代价?高 IO/CPU,须在低峰期执行,ILM 会自动控制。
  • 若索引很小是否仍需?小于 5GB 可跳过,但合并仍有益处。
  • 是否可以在 Cold 阶段 shrink?应在 Warm 阶段完成,Cold 阶段数据已作为快照,shrink 无意义。 加分回答:合并为 1 个段后,可设置 index.blocks.write: true 防止后续 merge 破坏单段结构。

Q11: 冷热分离如何与分库分表叠加?热分片和冷分片实例配置有何不同? 一句话回答:在分库分表基础上引入时间维度,热分片使用高配 SSD 实例,冷分片使用低配 HDD 实例,通过复合分片算法按 user_id 和时间路由。 详细解释:分库分表键保持 user_id,同时增加 create_time 作为二级分片条件。例如 user_id % 4 定位物理库,再根据时间判断表后缀 _hot_cold。热分片(8C16G SSD)负责近 30 天数据,冷分片(4C8G HDD)负责更早数据。数据迁移通过定时任务按用户维度归档到冷分片。 多角度追问

  • 跨时间查询如何合并?应用层分别查询热冷表并排序合并,或使用联邦引擎。
  • 冷分片是否需要备份?需要,但备份频率可降低,恢复时间更长。
  • 如何迁移特定用户数据?以 user_id 分组,按批次迁移每个用户的冷数据,便于后续分片查询。 加分回答:可通过滚动分片实现无限扩容,每月新建热分片,历史分片自动变为温、冷分片。

Q12 (系统设计题): 日志监控平台冷热分离架构设计 题目:一个日志监控平台日增 200GB 日志,90% 的查询访问近 3 天数据,日志需保留 90 天(合规)。请设计: (1) 冷热分离策略设计(冷热时间边界、存储介质选型) (2) ES ILM 策略完整定义(Hot/Warm/Cold/Delete 各阶段配置与阈值) (3) 冷热分离后的查询路由方案(应用层路由 vs Presto 联邦查询) (4) 存储成本计算(全热存储 vs 冷热分离的年化成本对比) (5) 迁移失败的监控与告警方案。

答案详细展开

(1) 冷热分离策略设计

  • 时间边界:热数据 0-3 天,温数据 3-7 天,冷数据 7-90 天,超过 90 天删除。
  • 存储介质:热节点使用本地 NVMe SSD,温节点使用 SATA SSD 或 HDD,冷节点使用 HDD 并挂载 S3 可搜索快照。
  • 索引策略:按天创建索引 logs-2026.05.18,ILM 自动滚动,保证活跃索引保持较小的体积。

(2) ES ILM 策略完整定义

(与 4.2 节类似,调整阈值):

PUT _ilm/policy/logs_policy
{
  "phases": {
    "hot": {
      "actions": {
        "rollover": { "max_primary_shard_size": "50gb", "max_age": "1d" },
        "set_priority": { "priority": 100 }
      }
    },
    "warm": {
      "min_age": "3d",
      "actions": {
        "allocate": { "number_of_replicas": 1, "require": { "data": "warm" } },
        "shrink": { "number_of_shards": 1 },
        "forcemerge": { "max_num_segments": 1 },
        "set_priority": { "priority": 50 }
      }
    },
    "cold": {
      "min_age": "7d",
      "actions": {
        "allocate": { "number_of_replicas": 0, "require": { "data": "cold" } },
        "searchable_snapshot": { "snapshot_repository": "s3_backup" },
        "set_priority": { "priority": 0 }
      }
    },
    "delete": {
      "min_age": "90d",
      "actions": { "delete": {} }
    }
  }
}

索引模板关联:

PUT _index_template/logs_template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 4,
      "number_of_replicas": 1,
      "index.lifecycle.name": "logs_policy",
      "index.lifecycle.rollover_alias": "logs-write"
    }
  }
}

(3) 查询路由方案

  • 方案A(应用层路由):在 Kibana 或自研查询界面,用户选择时间范围,系统根据时间范围自动生成查询。若范围超出热数据,查询透明跨热和冷(因为索引命名统一,ES 自动跨索引搜索)。
  • 方案B(联邦查询):如果冷数据导出到 S3 并以 Parquet 格式存储,可使用 Trino 连接 ES 热集群和 S3 冷数据,通过统一 SQL 查询,适合复杂分析。
  • 推荐:利用 ES 的可搜索快照,冷数据仍在 ES 集群内(虽然数据在 S3),查询无需联邦,ES 本身支持跨热冷搜索,实现完全透明。只需确保冷节点缓存可覆盖常用冷数据即可。

(4) 存储成本计算

  • 日增 200GB 日志,经压缩(LZ4)后约为 60GB。90 天需 5.4TB。
  • 全热存储(高性能 SSD):5.4TB × 0.8 元/GB = 4320 元/月。年 5.18 万元。
  • ILM 冷热分离
    • 热数据 3 天:0.18TB × 0.8 = 144 元。
    • 温数据 4 天:0.24TB × 0.3 = 72 元。
    • 冷数据 83 天:4.98TB × 0.03 (S3) ≈ 149.4 元。
    • 冷节点本地缓存(20% of 冷数据):0.996TB × 0.3 ≈ 298.8 元。
    • 总计约 664.2 元/月。年约 7970 元。节省 84.6%

(5) 迁移失败的监控与告警

  • ES ILM 监控:使用 GET _ilm/status 查看操作模式,指标 ilm.operation_mode 应为 RUNNING。监控索引停留在某阶段超时(如超过 4 天未进入 Warm),通过 Watcher 报警。
  • 快照失败:监控 _snapshot/s3_backup/_status,检查 state 不为 SUCCESS 则报警。
  • 冷节点磁盘水位:冷节点缓存磁盘使用率>80% 告警。
  • 搜索延迟:监控 Cold 阶段索引的查询 P99 延迟,若>2s 需检查缓存命中率或扩容冷节点。

系统架构图(日志平台冷热分离)

flowchart TD
    subgraph 数据输入
        A[日志生产者]
    end
    subgraph 热层
        B[Load Balancer] --> C[ES Hot 节点 x3]
        C -->|rollover| D[索引模板: logs-*]
    end
    subgraph 温层
        E[ES Warm 节点 x2]
    end
    subgraph 冷层
        F[ES Cold 节点 x2]
        G[对象存储 S3]
    end
    subgraph 查询层
        H[Kibana / Grafana]
        I[Trino 联邦查询]
    end
    A --> B
    C -- ILM 3d --> E
    E -- ILM 7d --> F
    F -- 可搜索快照 --> G
    H --> C
    H --> F
    I --> C
    I --> G

图7-日志监控平台冷热分离架构图

架构图说明:日志通过负载均衡写入 Hot 节点,ILM 自动将索引滚动到 Warm 节点(HDD)然后到 Cold 节点(快照存 S3)。Kibana 直接查询 ES,可跨热冷;Trino 可选用于联邦查询。

业务流程图(迁移与查询)

sequenceDiagram
    participant Producer as 日志生产
    participant ES_Hot as ES Hot (写入)
    participant ILM as ILM 引擎
    participant ES_Warm as ES Warm
    participant S3 as S3
    participant ES_Cold as ES Cold (缓存)
    participant User as 用户查询

    Producer->>ES_Hot: 写入索引 logs-2026.05.18
    ES_Hot->>ILM: 索引满足 rollover 条件
    ILM->>ES_Hot: 创建新索引 logs-2026.05.19
    ILM->>ES_Warm: 3 天后迁移 logs-2026.05.18 到 warm,shrink & forcemerge
    ILM->>S3: 7 天后将索引作为快照上传
    ILM->>ES_Cold: 挂载为可搜索快照
    User->>ES_Hot: 查询近2天日志
    ES_Hot-->>User: 快速返回
    User->>ES_Cold: 查询10天前日志
    ES_Cold->>S3: 缓存未命中,拉取数据块
    S3-->>ES_Cold: 返回数据
    ES_Cold-->>User: 返回结果 (延迟稍高)

图8-日志查询与时序图

时序图说明:展示了日志写入、ILM 自动流转,以及查询热数据和冷数据的完整交互过程。冷数据首次查询需从 S3 拉取,延迟上升。

通过上述设计,日志监控平台能够在满足合规要求的前提下,以极低的存储成本运行,同时保证近期数据的查询性能。


冷热分离存储架构速查表

工具/中间件冷热策略核心机制关键配置/命令成本优化点适用场景
MySQL时间驱动(pt-archiver / 分区)分批游标迁移 + 事务保护 + 从库延迟检查pt-archiver --limit 2000 --max-lag 5 --bulk-delete冷表索引精简;冷库使用 HDD/低配实例订单、账单归档
Elasticsearch时间驱动(ILM)Hot→Warm→Cold→Delete 四阶段自动流转;shrink/forcemerge/可搜索快照ILM policy JSON;node.roles: data_hot/warm/coldCold 阶段快照存 S3,降低 80% 存储成本;Warm 阶段合并分片段日志、安全事件、Metrics
Redis频率驱动(内存淘汰)maxmemory 策略(LRU/LFU)+ TTL;热数据常驻,冷数据淘汰maxmemory-policy allkeys-lruEXPIRE合理设定 maxmemory,避免内存浪费;配合本地缓存减少回源热点缓存、Session、计数器
ClickHouse时间驱动(TTL + 多卷)表/列 TTL 表达式,后台 Merge 按 Part 搬迁或删除TTL event_time + INTERVAL 30 DAY TO VOLUME 'cold'冷数据放 HDD,热数据 SSD;过期自动删除释放空间OLAP 分析、时序数据库
Spring Batch混合/自定义Job/Step/Tasklet 编排;分页读取、批量写入、清理;断点续传JdbcPagingItemReader + JdbcBatchItemWriter + Tasklet自动化迁移降低运维成本;支持跨存储介质复杂迁移跨系统数据同步、归档任务

延伸阅读

  • Percona Toolkit 官方文档:pt-archiver 详解
  • Elasticsearch 官方 ILM 与 searchable snapshot 指南
  • ClickHouse 官方 TTL 与 Storage Policies
  • Spring Batch 参考指南
  • 《The Data Warehouse Toolkit》第5章:数据生命周期管理

本文通过从策略到工具、从机制到成本的完整闭环,深入揭示了冷热分离作为数据架构成本控制核心手段的工程价值。在下一篇文章《缓存策略》中,我们将继续探讨如何用多级缓存进一步榨取性能。