ES 缓存体系
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_bytesrequest_cache.evictionsrequest_cache.hit_countrequest_cache.miss_count
缓存失效的连锁反应
当索引发生 refresh 时:
- 新 Segment 生成 → 该分片的 Request Cache 全部清除。
- 新 Segment 的 filter 结果不在 Query Cache 中 → 下个查询需重新计算。
- 如果请求量大,缓存骤然失效可能引发缓存雪崩——大量查询同时穿透缓存,直击磁盘。
缓解策略:
- 对读多写少的索引,适当拉大
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 能关就关。默认配置下多数场景无需手动调参,但理解它们的失效条件,才能解释"为什么重启后查询变慢"这类问题——答案是缓存预热需要时间。