可搜索快照与冻结层:把冷数据放进对象存储还能查

系统讲解 Elasticsearch 的可搜索快照与冻结层:快照目录直读原理、frozen tier 的共享缓存与部分索引机制、partial searchable snapshot 的用法,以及对象存储成本与查询延迟之间的权衡方法。

日志、审计记录、历史订单——这些数据有明确的「热三天、温一月、冷一年」的访问规律,却往往占着最贵的 SSD 和最多数的数据节点。把它们删掉舍不得,留着又养不起。可搜索快照(searchable snapshots)给出了第三条路:数据只存在于对象存储的快照里,节点按需从快照读取并缓存,查询依然可用。冻结层则在此基础上把本地缓存压缩到极小的比例,用共享缓存服务多个节点,把每 GB 成本压到热层的几分之一。代价是首次查询的延迟明显上升,理解这个代价的边界是设计分层架构的关键。

1. 存储分层与冷数据困境

一句话总结: 冷数据占容量的大头却极少被访问,把它留在热层是最典型的成本浪费。

1.1 数据的访问规律

典型的日志或时序数据呈现强烈的长尾分布:最近 7 天的数据承载 90% 以上的查询,1 个月前的数据查询量降到个位数百分比,1 年前的几乎只在合规审计时被翻出来。而容量分布恰好相反——时间越久的数据累积越多。

1.2 传统分层的极限

用 ILM 把数据从热节点滚到温节点、冷节点,能缓解问题,但冷节点仍然是「数据节点」:它要持有完整的分片副本、占用本地磁盘、参与分片分配与恢复。数据量增长时,冷节点集群也要跟着扩,成本只是从 SSD 换成了 HDD,量级并没有质变。

1.3 可搜索快照的定位

可搜索快照让「冷分片」不再占用数据节点的本地存储:分片数据存放在快照仓库(通常是 S3 或兼容对象存储)里,节点只保存一份元数据与少量本地缓存。查询时按需从对象存储拉取数据块。它的成本结构从「按容量买节点」变成「按请求付对象存储费用」。

1.4 三层架构

层数据位置本地缓存典型延迟成本
hot本地 SSD全量毫秒级最高
warm本地 HDD全量毫秒级中
frozen对象存储极小比例秒级最低

2. 可搜索快照的原理

一句话总结: 可搜索快照把快照索引挂载成普通索引,读取时从对象存储按块拉取并缓存到本地,Lucene 层感知不到差别。

2.1 从快照直接读取

Elasticsearch 的段文件是不可变的,这个特性是可搜索快照的基础。快照仓库里保存的就是这些不可变的段文件。挂载一个可搜索快照索引时,节点不会把数据下载到本地,而是把快照目录「挂」进 Lucene 的目录抽象层,读取时按需从对象存储取块。

2.2 缓存机制

节点上有一个专用的共享缓存目录(path.shared_data),用于存放从对象存储拉取的数据块。缓存是块粒度的:一次查询只拉取涉及的块,后续命中同一块则直接从本地读取。缓存采用 LRU 淘汰,大小通过节点配置控制。

2.3 挂载的两种模式

  • full_copy:挂载时把快照数据完整复制到本地,之后与普通索引无异。适合数据量小、查询频繁的场景,本质上还是一次「恢复」。
  • shared_cache:共享缓存模式,数据留在对象存储,本地只放缓存。这是可搜索快照的核心模式,也是成本优势的来源。

2.4 只读与不可变

共享缓存模式挂载的索引是只读的:不能写入、不能更新、不能改映射。这符合冷数据的特性,也简化了实现。如果需要对冷数据做修正,只能重新生成快照并重新挂载。

3. 配置与使用

一句话总结: 使用可搜索快照需要先注册支持它的快照仓库,再通过挂载 API 把快照索引变成可查询索引。

3.1 注册仓库

PUT /_snapshot/cold_store
{
  "type": "s3",
  "settings": {
    "bucket": "my-es-cold",
    "base_path": "snapshots",
    "max_restore_bytes_per_sec": "500mb"
  }
}

只有支持 searchable_snapshots 能力的仓库类型才能挂载可搜索快照,S3、GCS、Azure Blob 等对象存储都支持。

3.2 创建快照

PUT /_snapshot/cold_store/snap-logs-2026.09?wait_for_completion=true
{
  "indices": "logs-2026.09",
  "include_global_state": false
}

快照完成后即可挂载。

3.3 挂载可搜索快照

POST /_snapshot/cold_store/snap-logs-2026.09/_mount?wait_for_completion=true
{
  "index": "logs-2026.09",
  "renamed_index": "logs-2026.09-cold",
  "index_settings": {
    "index.number_of_replicas": 0
  },
  "storage": "shared_cache"
}

挂载后得到一个名为 logs-2026.09-cold 的只读索引,可以像普通索引一样查询。renamed_index 是必要的——原索引名可能仍被占用。

3.4 解除挂载

DELETE /logs-2026.09-cold

删除索引即解除挂载,快照本身不受影响,需要时还可以重新挂载。这正是可搜索快照的灵活性所在:挂载与卸载的成本都很低。

3.5 节点角色与配置

可搜索快照节点需要配置共享缓存目录:

node.roles: [ data_frozen ]
path.shared_data: /mnt/shared_cache

data_frozen 是专用于冻结层的角色。共享缓存目录应挂在大容量的本地盘上,它是性能的关键。

4. frozen tier 与部分索引

一句话总结: 冻结层用极小本地缓存加共享缓存服务多个节点,并通过部分索引把不参与查询的段完全留在对象存储。

4.1 冻结层的定义

冻结层是可搜索快照的一种特定部署形态:节点角色为 data_frozen,本地缓存与数据量的比例极低(官方建议 1% 到 5% 量级),多个节点共享同一份缓存目录(通过网络文件系统挂载)。查询时数据从对象存储流入共享缓存,所有节点都能受益。

4.2 部分索引

共享缓存模式有两种行为:

  • 完全挂载:所有段都参与查询,未缓存的段从对象存储拉取。
  • 部分挂载:只有带 partial 标记的索引设置才生效——查询时只搜索已缓存的部分,未缓存的段不参与。这牺牲了完整性换取速度。

4.3 部分搜索的语义

部分搜索意味着结果可能不完整,但能极快返回。它的典型用法是「先看个大概」——用户在仪表盘上快速扫一眼历史趋势,不需要精确到每一条。是否启用部分搜索由查询参数控制,而不是索引设置,这样同一份数据可以按场景选择精确或快速。

4.4 与 ILM 的集成

ILM 的 frozen 阶段可以直接执行 searchable_snapshot 动作:

{
  "frozen": {
    "min_age": "90d",
    "actions": {
      "searchable_snapshot": {
        "snapshot_repository": "cold_store"
      }
    }
  }
}

数据到 90 天后自动转为可搜索快照,无需人工干预。这是把分层策略落地的标准做法。

4.5 冻结层与副本

冻结层的索引通常设置 number_of_replicas: 0:数据在对象存储里本身就是多副本的(对象存储自带冗余),再在本地存副本没有意义,只是浪费缓存空间。可用性由对象存储的持久性保证。

5. 缓存策略与性能

一句话总结: 可搜索快照的性能取决于缓存命中率,首次查询受对象存储延迟支配,重复查询接近本地读。

5.1 首次与后续查询

首次查询一个未缓存的段时,节点要从对象存储拉取数据块,延迟由网络与对象存储的响应时间决定,通常是几十到几百毫秒的额外开销,极端情况到秒级。缓存命中后,后续查询的延迟与本地磁盘读接近。

5.2 缓存预热

对已知会被频繁查询的索引,可以主动预热:

POST /logs-2026.09-cold/_search?request_cache=false
{
  "size": 0,
  "query": { "match_all": {} },
  "aggs": { "all": { "terms": { "field": "service", "size": 1 } } }
}

跑一次覆盖主要字段的聚合,能把相关段拉进缓存。预热通常在挂载后立即执行,或由定时任务在低峰期执行。

5.3 缓存大小与命中率

缓存大小决定命中率。缓存过小会导致频繁的拉取与淘汰,性能剧烈波动;缓存过大则成本优势被削弱。经验做法是让缓存能覆盖「最近一个查询周期内被访问的数据」,再根据监控的缓存命中率微调。

5.4 查询模式的影响

  • 时间范围窄的查询:只涉及少数段,缓存效率高。
  • 全量扫描或大范围聚合:会拉取几乎所有段,首次执行极慢。
  • 按字段排序或聚合:需要 doc values,同样要拉取数据块。

对冻结层的数据,应尽量避免「无边界的大范围聚合」,把查询范围收窄到具体时间段。

5.5 监控指标

关注 searchable_snapshots 相关的统计:缓存命中率、从对象存储读取的字节数、读取延迟。如果读取字节数持续高企而命中率低,说明缓存配置不合理或查询模式不适合冻结层。

6. 成本与延迟权衡

一句话总结: 可搜索快照把存储成本降低一个量级,代价是首次查询延迟上升,是否划算取决于数据的访问频率。

6.1 成本结构对比

维度本地节点存储可搜索快照
存储介质SSD 或 HDD对象存储
计费方式按节点容量按存储量加请求数
副本成本成倍增加对象存储自带冗余
冷启动延迟无首次查询明显
扩展方式加节点无需扩容

6.2 划算的临界点

判断标准是「查询频率」。如果某份数据每周被查几次,留在本地节点上是浪费;如果每天被查上千次,冻结层的延迟会累积成明显的体验问题,此时应该用温层。经验阈值是每天访问几次以下的数据,适合放进冻结层。

6.3 请求费用

对象存储按请求计费,大量小范围查询会产生可观的请求费用。把查询范围收窄、减少重复的小查询、利用请求缓存,都能降低这部分成本。这是从「买硬件」转向「付流量」后必须重新建立的成本意识。

6.4 延迟预算

如果业务对冷数据的查询有明确延迟要求(比如报表必须在 5 秒内返回),冻结层不一定合适。可以做的折中是:把最近 3 个月的数据放温层,更早的放冻结层,让延迟敏感的部分留在本地。

6.5 与 CCR 和搜索副本的关系

可搜索快照的索引只读且副本数为 0,不参与跨集群复制,也不提供搜索副本带来的查询吞吐扩展。它的定位是「降低存储成本」,不是「提升查询能力」。需要查询吞吐时,仍然要靠本地索引加副本。

7. 生产实践与常见坑

一句话总结: 可搜索快照的实施要点是「先用 ILM 自动流转、再监控缓存命中率、最后按访问频率验证分层是否合理」。

7.1 上线顺序

建议的实施顺序:

  1. 确认快照仓库可用且已完成首次全量快照。
  2. 挑选一个访问量低的索引做试点挂载,观察查询延迟。
  3. 用 ILM 的 frozen 阶段把流转自动化。
  4. 建立缓存命中率与对象存储请求量的监控。
  5. 根据实测数据调整分层边界(多少天进冻结层)。

7.2 常见坑

  • 快照仓库与数据盘同盘:对象存储应独立,不要用本地文件系统仓库冒充。
  • 缓存目录空间不足:path.shared_data 写满会导致缓存失效、性能崩塌。
  • 首次查询超时:客户端超时设置过短,冻结层首次查询被中断;应对冷数据查询放宽超时。
  • 大范围聚合打到冻结层:一次全量聚合可能拉取整个索引,耗时数分钟。
  • 忘记副本设为 0:白白占用缓存空间。
  • 仓库删除策略冲突:SLM 的保留策略可能删掉仍被挂载的快照,导致索引不可读。

7.3 与其他分层手段的配合

可搜索快照解决的是「冷数据存储成本」,与之配合的手段还有:用 ILM 强制合并把段数降到最少(段越少,对象存储拉取越高效)、用 index.codec: best_compression 压缩、以及把不需要查询的字段从映射里去掉。这些措施叠加起来,能进一步压低成本。

7.4 何时不该用

如果数据量本身不大(几百 GB)、或者查询频率很高、或者对延迟极度敏感,可搜索快照带来的复杂度与延迟代价可能超过收益。分层架构的价值随数据规模增长而增长,小规模集群直接加盘往往更简单。

8. 总结

环节要点
核心原理段不可变,快照目录直读并按块缓存
挂载模式full_copy 复制到本地,shared_cache 留对象存储
冻结层data_frozen 角色加极小本地缓存与共享缓存
部分搜索只查已缓存段,快但结果可能不完整
性能特征首次查询慢,缓存命中后接近本地读
成本结构从按容量买节点转为按存储与请求付费
自动化ILM 的 searchable_snapshot 动作完成流转
适用边界低频访问的冷数据,高频与低延迟场景不适用

可搜索快照是 Elasticsearch 分层架构里最漂亮的一块拼图:它把「不可变的段」这个底层设计发挥到了极致,让冷数据可以完全脱离数据节点而仍然可查。它的代价也很明确——首次查询的延迟。把这份延迟放进业务可接受的范围内,就是容量规划与分层策略要回答的问题。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 分页与深度分页:from/size、search_after、PIT 与 scroll
  2. 嵌套与父子关联查询:nested、join 字段与性能取舍
  3. 磁盘水位与容量规划:三档水位、分片规划与扩容决策