1.起因:(超时分析)
1.用户往ES里面导入数据,导入数据的过程中CPU很高,所以查询请求响应变慢,这个是很容易理解的;
2.当导入数据完成后,CPU慢慢降下来了,但是发现查询请求响应还有一小段时间是很慢的,此时CPU很低、GC也很少
3.很容易想到是不是有哪些任务在执行,导致请求变慢,或者是请求本身变化(经过沟通,排除请求本身变化因素)
4.其他消耗性能的任务,最容易想到的就是merge,force merge,flush,查看监控,发现是merge很多
5.merge多影响查询,有两种可能
一是merge本身消耗性能;(可以自行翻看源码,这里暂时不拓展,merge不消耗ES线程池,对ES性能消耗比较小)
二是merge会导致缓存失效;(很显然是缓存失效导致查询响应时间变长)
2.缓存分析:
缓存类型:1.Query Cache 2.Request Cache 3.Field data Cache 4.pagecache
这里只考虑1.Query Cache 2.Request Cache
缓存类型 | 缓存对象 | 失效 | 缓存条件 | 缓存 Key | 缓存 Value |
---|---|---|---|---|---|
Query Cache | segment | merge | filter请求,与频率有关,昂贵的请求2次,非昂贵请求5次 | filter 子查询 | FixedBitSet Map<Query, Map<Segment, DocIdSet>> |
Request Cache | shard | refresh | 聚合请求(size=0),与频率无关 | 整个客户端请求 | 查询结果序列化 |
2.2.Request Cache:
Shard Request Cache 简称 Request Cache,是分片级别的查询缓存,每个分片有自己的缓存。
该缓存采用 LRU 机制,缓存的 key 是整个客户端请求,缓存内容为单个分片的查询结果。
缓存的实现在 IndicesRequestCache 类中,缓存的 key 是一个复合结构,主要包括shard,indexreader,以及客户端请求三个信息:
1 2 3 |
final Key key = new Key(cacheEntity, reader.getReaderCacheHelper().getKey(), cacheKey);
|
cacheEntity, 主要是shard信息,代表该缓存是哪个shard上的查询结果。readerCacheKey,主要用于区分不同的 IndexReader。cacheKey, 主要是整个客户端请求的请求体(source)和请求参数(preference、indexRoutings、requestCache等)。由于客户端请求信息直接序列化为二进制作为缓存 key 的一部分,所以客户端请求的 json 顺序,聚合名称等变化都会导致 cache 无法命中。
缓存的 value比较简单,就是将查询结果序列化之后的二进制数据。
Request Cache 的主要作用是对聚合的缓存,聚合过程是实时计算,通常会消耗很多资源,缓存对聚合来说意义重大。
Request Cache是分片级别的缓存,segment不变的特性使得当segment没有新增和merge时,整个分片的数据都是不变的,缓存不会“过期”,当有新的 segment 写入到分片后,缓存会失效,因为之前的缓存结果已经无法代表整个分片的查询结果。所以分片每次 refresh之后,缓存会被清除;
2.3.Filter Cache
Node Query Cache (Filter Cache) 缓存的是某个 filter 子查询语句,在一个 segment 上的查询结果。如果一个 segment 缓存了某个子查询的结果,下次可以直接从缓存获取,无需对 segment 进行查询。Request Cache 只对 filter 查询进行缓存,因此又称为 Filter Cache。
Query Cache是 Lucene 层面实现的,封装在 LRUQueryCache 类中,默认开启。
ES 层面会进行一些策略控制和信息统计。每个 segment 有自己的缓存,缓存的 key 为 filter 子查询(query clause),缓存内容为查询结果,这些查询结果就是匹配到的 document numbers,保存在位图 FixedBitSet中。
QueryCache策略决定某个查询要不要缓存。ES使用 UsageTrackingQueryCachingPolicy作为默认的缓存策略,在这个策略中,判断某个查询要不要缓存,主要关注两点:
- 某些类型的查询,永远不会缓存,目前 shouldNeverCache 方法中定义了以下类型:TermQuery、MatchAllDocsQuery、MatchNoDocsQuery、以及子查询为空的BooleanQuery、DisjunctionMaxQuery。
- 某条 query 的 访问频率大于等于特定阈值之后,该 query结果才会被缓存。对于访问频率,主要分为2类,一类是访问2次就会被缓存,包括: MultiTermQuery、MultiTermQueryConstantScoreWrapper、TermInSetQuery、PointQuery 在 isCostly方法中定义。其余类型的查询访问5次会被缓存。
最近使用次数如何统计的?lucene 里维护一个大小为256的环形缓冲,最近使用过的 query 会做一下 hash 保存到这个缓冲里,当缓冲满的时候直接覆盖最后一个,被访问一次也不会调整他在缓冲里的顺序。因此“最近访问频率”可以理解成:在最近的256个历史记录里,query 被访问的次数。
由于缓存是为每个 segment 建立的,当 segment 合并的时候,被删除的 segment 其关联 cache 会失效。其次,对于体积较小的 segment 不会建立Query cache,因为他们很快会被合并。因此一个 segment的 doc 数量需要大于10000,并且占整个分片的3%以上才会走 cache 策略。
缓存知识点分析参考:https://www.easyice.cn/archives/367
3.总结:
1.ES的两类关键的缓存都是存在JVM的堆中,docvalues是存在堆外内存
2.Query Cache缓存是lucene控制,主要是建立filter子查询在segment维度的缓存,Request Cache缓存由ES控制,主要是建立聚合查询在shard维度的缓存
3.缓存主要是通过segment的变化来触发失效
4.缓存大小一般由两个逻辑控制,一个是size参数(indices.queries.cache.size)控制,例如堆内存百分之10,另一个是count参数(indices.queries.cache.count)控制,例如最多缓存 10000个子查询的结果(LRU 的大小);