二、ES 核心缓存机制深度剖析:节点级、分片级与查询级缓存
Elasticsearch 的缓存体系是保障搜索性能与实时性平衡的关键。理解不同缓存的作用域、生命周期与失效机制,是设计缓存一致性闭环接口的前提。本章从节点级、分片级到查询级,逐层拆解每种缓存的工作原理与生命周期。
2.1 缓存体系总览
ES 缓存按作用域分为三个层级:节点级(Node Level)、分片级(Shard Level)和查询级(Query Level)。每个层级服务于不同的数据访问模式,生命周期与失效策略也各不相同。
| 缓存层级 | 缓存名称 | 作用域 | 存储内容 | 生命周期 |
|---|---|---|---|---|
| 节点级 | Filter Cache | 节点内所有分片共享 | 过滤条件匹配的文档位集(Bitset) | 节点重启 / 索引设置变更 / LRU 淘汰 |
| 节点级 | Field Data Cache | 节点内所有分片共享 | 字段排序 / 聚合的倒排索引结构 | 节点重启 / 字段数据变化 / LRU 淘汰 |
| 分片级 | Request Cache | 单个分片 | 查询结果(聚合、命中数、文档 ID) | 分片 Refresh / 索引写入 / 显式清除 |
| 查询级 | Query Cache(Shard Query Cache) | 单个分片 | 查询子句的中间结果(DocIdSet) | 分片 Refresh / 查询条件变化 |
关键认知: 节点级缓存跨分片共享,适合静态或低频变化的过滤条件;分片级缓存与索引写入频率强相关,写入越频繁,缓存命中率越低。
2.2 节点级缓存:Filter Cache
2.2.1 工作原理
Filter Cache 缓存的是 Bitset(位集),即每个文档是否匹配某个过滤条件的位图。当查询使用 filter 上下文时,ES 会为每个 segment 构建一个 Bitset,并缓存到节点内存中。后续相同过滤条件可直接复用 Bitset,跳过文档遍历。
- 构建时机: 首次使用 filter 查询时,按 segment 粒度构建。
- 存储结构: 每个 segment 一个 Bitset,位长度等于 segment 内文档总数。
- 共享范围: 节点内所有分片共享,相同 filter 语句在不同索引间也可复用。
- 淘汰策略: LRU(最近最少使用),受
indices.queries.cache.size控制(默认 10% 堆内存)。
2.2.2 生命周期
- 创建: 首次 filter 查询触发,按 segment 构建 Bitset。
- 存活: 驻留在节点内存中,被后续相同 filter 查询复用。
- 失效: 节点重启、索引设置变更(如
index.number_of_replicas)、segment merge 后原 Bitset 失效。 - 淘汰: 内存不足时,LRU 算法淘汰最久未使用的 Bitset。
最佳实践: 将高频且值基数低的过滤条件(如状态码、分类标签)放入 filter 上下文,充分利用 Filter Cache。避免对高基数字段(如时间戳、用户 ID)使用 filter,否则 Bitset 占用过大且复用率低。
2.3 节点级缓存:Field Data Cache
2.3.1 工作原理
Field Data Cache 用于 排序 和 聚合 操作。当对 text 类型字段或未开启 doc_values 的字段进行排序/聚合时,ES 会将字段的所有值加载到内存中,构建倒排结构(fielddata)。
- 构建时机: 首次对字段进行排序或聚合时触发。
- 存储结构: 每个字段一个 fielddata 结构,包含所有文档的字段值。
- 共享范围: 节点内所有分片共享。
- 淘汰策略: LRU,受
indices.fielddata.cache.size控制(默认无上限,但受堆内存限制)。
2.3.2 生命周期
- 创建: 首次排序/聚合查询触发,加载字段全部值。
- 存活: 持续驻留,直到被淘汰或显式清除。
- 失效: 节点重启、字段 mapping 变更、索引关闭后重新打开。
- 淘汰: 堆内存压力大时,LRU 淘汰最久未使用的 fielddata。
风险提示: Field Data Cache 是 O(N) 内存消耗(N 为文档数),对高基数 text 字段做聚合极易触发 OOM。生产环境应优先使用
doc_values(列式存储),避免使用 fielddata。
2.4 分片级缓存:Request Cache
2.4.1 工作原理
Request Cache 缓存的是 完整查询结果,包括聚合结果、命中总数、文档 ID 列表等。它工作在分片级别,每个分片独立缓存自己的查询结果。
- 构建时机: 查询请求到达分片,且查询条件可缓存(非
now、非scroll、非profile)。 - 存储结构: 键为查询 DSL 的 JSON 序列化哈希,值为查询结果。
- 共享范围: 仅限单个分片,不同分片缓存独立。
- 淘汰策略: 分片 Refresh 后自动失效,或显式调用
_cache/clear。
2.4.2 生命周期
- 创建: 分片执行可缓存查询后,将结果存入缓存。
- 存活: 直到分片发生 Refresh(默认 1 秒)或索引写入操作。
- 失效: 分片 Refresh 后,所有缓存条目被清空;索引写入(index/update/delete)也会触发 Refresh。
- 显式清除:
POST /index/_cache/clear?request=true。
性能影响: Request Cache 的命中率与索引写入频率成反比。写入密集型场景(如日志索引)几乎无法利用 Request Cache;适合低频写入、高频查询的报表类索引。
2.5 查询级缓存:Query Cache(Shard Query Cache)
2.5.1 工作原理
Query Cache 缓存的是 查询子句的中间结果(DocIdSet),即每个查询子句匹配的文档 ID 集合。它位于 Lucene 层面,每个分片独立维护。
- 构建时机: 查询子句首次执行时,Lucene 将匹配的文档 ID 集合缓存。
- 存储结构: 键为查询子句的
Query对象,值为DocIdSet。 - 共享范围: 仅限单个分片内的 Lucene IndexReader。
- 淘汰策略: IndexReader 重新打开(Refresh)后失效。
2.5.2 生命周期
- 创建: 查询子句首次执行,Lucene 缓存 DocIdSet。
- 存活: 直到 IndexReader 被重新打开(Refresh 或 segment merge)。
- 失效: 分片 Refresh 后,所有 Query Cache 条目失效。
- 与 Request Cache 区别: Query Cache 缓存的是子句级中间结果,Request Cache 缓存的是完整查询结果。
调优建议: 可通过
index.queries.cache.enabled(默认 true)控制是否启用 Query Cache。对于频繁 Refresh 的索引,建议关闭 Query Cache 以减少缓存构建开销。
2.6 缓存生命周期对比与失效链路
下图展示了四种缓存在一次查询请求中的协作关系,以及 Refresh 事件如何触发缓存失效。
2.7 缓存一致性闭环设计要点
在构建缓存一致性闭环接口时,需要针对不同缓存的生命周期特点,设计对应的失效通知与重建策略:
| 缓存类型 | 失效触发条件 | 闭环接口设计策略 |
|---|---|---|
| Filter Cache | 节点重启 / segment merge / 索引设置变更 | 监听索引设置变更事件,主动清除相关 filter 缓存;segment merge 后自动失效,无需干预 |
| Field Data Cache | 节点重启 / LRU 淘汰 / 显式清除 | 避免使用 fielddata,改用 doc_values;若必须使用,在字段 mapping 变更后调用 _cache/clear?fielddata=true |
| Request Cache | 分片 Refresh / 索引写入 / 显式清除 | 写入操作后自动失效;对于准实时要求高的场景,可设置 index.refresh_interval 为 -1 禁用自动 Refresh,手动控制缓存生命周期 |
| Query Cache | 分片 Refresh / IndexReader 重新打开 | 与 Request Cache 联动失效;高频写入索引建议关闭 Query Cache(index.queries.cache.enabled: false) |
闭环核心原则: 缓存一致性闭环接口需要做到“写后失效、读时重建”。写入操作完成后,通过监听 Refresh 事件或显式调用清除 API,确保后续读取能获取最新数据。对于 Filter Cache 这类跨分片共享的缓存,还需考虑节点级别的广播失效。
2.8 本章小结
- Filter Cache:节点级,缓存 Bitset,适合低基数、高频 filter 字段,LRU 淘汰。
- Field Data Cache:节点级,缓存排序/聚合数据,O(N) 内存消耗,应优先使用 doc_values。
- Request Cache:分片级,缓存完整查询结果,Refresh 后立即失效,适合低频写入场景。
- Query Cache:分片级,缓存子句中间结果,与 Refresh 强相关,高频写入场景建议关闭。
- 一致性闭环:理解每种缓存的失效触发条件,设计对应的监听、清除与重建机制,是保证缓存与索引数据一致性的关键。