ES 缓存体系

22 May 2026 – wusfe · 5 min read

ES 缓存体系

ES 的查询性能很大程度上取决于缓存命中率。理解 ES 的三层缓存体系——Query Cache、Fielddata Cache、Request Cache——是排查"为什么同样的查询有时快有时慢"的关键。本文逐一拆解每层缓存的原理、适用场景和监控方法。

三层缓存全景图

缓存 缓存粒度 缓存内容 适用场景 失效条件
Node Query Cache Segment 级,节点级 filter 子句的文档 ID 集合(bitset) 频繁执行的 term/range filter Segment 合并(merge)、索引 refresh
Shard Request Cache 分片级 整个请求的查询结果(JSON) size=0 的聚合查询、不翻页的搜索 分片 refresh
Fielddata Cache 字段级,分片级 text 字段的全局序数(ordinals)和文档值 text 字段上的聚合/排序 分片 close、circuit breaker 触发驱逐

1. Node Query Cache(最重要)

Node Query Cache 缓存的是 filter 上下文中匹配条件的文档 ID 集合。比如:

GET /products/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "category": "手机" } },
        { "range": { "price": { "gte": 1000, "lte": 3000 } } }
      ]
    }
  }
}

Lucene 执行 filter 时,会生成一个 FixedBitSet(位图),其中每一位代表一个文档 ID:1 表示匹配,0 表示不匹配。这个 bitset 会被缓存下来。下次同样的 filter 子句执行时,直接返回缓存的 bitset 拼接,全程不访问磁盘

缓存 Key 的计算公式

cache_key = hash(index_name + field_name + query_type + query_value)

两个表面上不同的 filter 语句,如果分解后 Lucene Query 对象相同(.equals() 返回 true),缓存就能命中。

缓存大小配置

# elasticsearch.yml
indices.queries.cache.size: 10%   # 堆内存的 10%,默认值

也可以按索引单独设置:

PUT /my_index/_settings
{
  "index.queries.cache.enabled": true
}

查看缓存命中率

GET /_nodes/stats/indices/query_cache

关键指标:

  • query_cache.memory_size_in_bytes:当前缓存占用的内存
  • query_cache.total_count:累计查询次数
  • query_cache.hit_count:累计命中次数
  • query_cache.miss_count:累计未命中次数
  • query_cache.evictions:驱逐次数(越大说明缓存空间不足)

2. Fielddata Cache(慎用!)

Fielddata Cache 用于 text 字段的聚合、排序和 script 访问。ES 默认全局禁用 text 字段的 fielddata(因为这个缓存非常危险):

PUT /my_index/_mapping
{
  "properties": {
    "description": {
      "type": "text",
      "fielddata": true   // 小心!
    }
  }
}

为什么危险? text 字段存储的是分词后的 term 列表。要为 text 字段构建 Fielddata,ES 需要把所有文档的所有 term 加载到 JVM 堆内存中,按文档→term 排列建立数据结构。假设 1 亿篇文档的 title 字段,平均每篇 5 个词,fielddata 可能占用数 GB 堆内存。

踩坑案例

某论坛系统对 content(text 类型,存储帖子正文)做 terms 聚合统计热门话题词。刚上线几秒,节点 OOM 崩溃——因为 fielddata 一次性加载了所有帖子的所有分词,堆内存瞬间吃光。

正确做法

// 不要对 text 聚合,用 keyword 子字段
PUT /posts/_mapping
{
  "properties": {
    "content": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    }
  }
}

// 用 keyword 子字段做聚合(使用 doc_values,磁盘上,不走堆)
GET /posts/_search
{
  "size": 0,
  "aggs": {
    "tags": { "terms": { "field": "content.keyword" } }
  }
}

fielddata 频率过滤(如果实在要用):

PUT /my_index/_settings
{
  "indices.breaker.fielddata.limit": "40%",
  "indices.breaker.fielddata.overhead": 1.03
}

indices.breaker.fielddata.limit 是熔断器(Circuit Breaker)阈值,当 fielddata 内存占用超过堆的 40%,后续请求直接抛异常而不让节点 OOM。

3. Shard Request Cache

Request Cache 缓存的是 整个查询请求在分片级别返回的结果 JSON。它与 Query Cache 的核心区别:

维度 Query Cache Request Cache
缓存内容 文档 ID 集合(bitset) 完整 JSON 响应
缓存级别 节点级(跨分片) 分片级(每个 shard 一份)
适用查询 仅 filter 上下文 任何查询
size=0 聚合 部分受益 最大受益者
翻页查询 不适用 不适用(每页不同)
内存占用 小(bitset 紧凑) 大(完整 JSON)

size=0 的聚合查询是 Request Cache 的最佳场景

GET /sales/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "range": { "date": { "gte": "2024-01-01", "lte": "2024-01-31" } } }
      ]
    }
  },
  "aggs": {
    "by_category": {
      "terms": { "field": "category", "size": 20 }
    }
  }
}

这个查询没有变化尺寸(size=0),在下次 refresh 之前的任何重复请求都能精确命中 Request Cache,连聚合计算都省了。

缓存失效条件

  • Refresh:默认每 1 秒 refresh 一次,refresh 后分片数据版本变化,Request Cache 全部失效。
  • Merge:Segment 合并会导致 Query Cache 中受影响 Segment 的缓存条目失效。

查看 Request Cache 指标

GET /_nodes/stats/indices/request_cache

关键指标:

  • request_cache.memory_size_in_bytes
  • request_cache.evictions
  • request_cache.hit_count
  • request_cache.miss_count

缓存失效的连锁反应

当索引发生 refresh 时:

  1. 新 Segment 生成 → 该分片的 Request Cache 全部清除
  2. 新 Segment 的 filter 结果不在 Query Cache 中 → 下个查询需重新计算。
  3. 如果请求量大,缓存骤然失效可能引发缓存雪崩——大量查询同时穿透缓存,直击磁盘。

缓解策略

  • 对读多写少的索引,适当拉大 index.refresh_interval(如 30s 甚至 -1 禁用)。
  • 控制写入批量的大小和频率,避免频繁 refresh。

监控最佳实践

建议搭建以下监控面板:

指标 告警条件 原因
Query Cache 命中率 < 90% filter 查询可能未充分复用
Query Cache 驱逐率 evictions 持续增长 缓存空间不足,考虑调大
Request Cache 命中率 < 50%(对聚合场景) 可能 refresh 过频
Fielddata 内存占用 持续 > 5% 堆 有 text 聚合在跑,危险
Circuit Breaker 触发次数 > 0 有查询请求被熔断,需立即排查

小结

ES 三层缓存的用武之地各不相同:Query Cache 管 filter 的位图、Request Cache 管重复聚合结果、Fielddata Cache 能关就关。默认配置下多数场景无需手动调参,但理解它们的失效条件,才能解释"为什么重启后查询变慢"这类问题——答案是缓存预热需要时间。