作为一个近实时的搜索引擎,它的缓存机制还有很有必要了解一下的,本文介绍Elasticsearch2.x的三种缓存,其它高版本的与此相差不是很大,可能会有一些参数名称的调整等,具体情况具体对待即可。
1. Query Cache
Query Cache也称为Filter Cache,顾名思义它的作用就是对一个查询中包含的过滤器执行结果进行缓存。
比如常用的term,terms,range过滤器都会在满足某种条件后被缓存,注意,这里的bool过滤器是不会被缓存的,但bool过滤器包含的子query clause会被缓存,我们可以用下面的命令来查询Query Cache的情况:
1 | curl -XGET '127.0.0.1:9200/_nodes/stats/query_cache?pretty&human' |
查询结果如下:
1 | { |
结果中包含所有ES节点的query_cache情况。
举例说明:
1 | { |
如上的DSL查询,有两个过滤器,一个是term过滤器,用来过滤productID为 “JODL-X-1937-#pV7” 的产品,一个range过滤器用来过滤价格在20以上的产品,在这个例子中这两个过滤器执行的结果会分别作为一个BitSet(位图)缓存,返回的查询结果则是这两个位图交集。
注意:上面描述的Query Cache只有在满足某种条件下才会被缓存,具体是什么条件由于本人没做细致调研,感兴趣的小伙伴可以研究一下子。
2. Request Cache
2.1 说明
当一个查询发送到ES集群的某个节点上时,这个节点会把该查询扩散到其他节点并在相应分片上执行,我们姑且把在分片上执行的结果叫“本地结果集“,这些本地结果集最终会汇集到最初请求到达的那个协调节点,这些“分片级”的结果集会合并成“全局”结果集返回给调用端。Request Cache模块就是为了缓存这些“分片级”的本地结果集,但是目前只会缓存查询中参数size=0的请求,所以就不会缓存hits 而是缓存 hits.total,aggregations和suggestions。
查询request_cache命令:
1 | curl -XGET '127.0.0.1:9200/_nodes/stats/request_cache?pretty&human' |
返回的结果形式和query_cache一致,只是名称有改变,此处不再赘述。
2.2 缓存失效
Request Cache缓存失效是自动的,当索引refresh时就会失效,也就是说在默认情况下Request Cache是每1秒钟失效一次(注意:分片在这段时间内确实有改变才会失效)。也就是说当一个文档被索引到该文档变成Searchable之前的这段时间内,不管是否有请求命中缓存该文档都不会被返回,正是是因为如此ES才能保证在使用Request Cache的情况下执行的搜索和在非缓存近实时搜索的结果一致。如果我们把索引刷新时间设置得越长那么缓存失效的时间越长,如果缓存被写满将采用LRU策略清除。当然我们也可以手动设置参数indices.request.cache.expire指定失效时间,但是基本上没必要去这样做,因为缓存在每次索引refresh时都会自动失效。
也可以手动执行如下命令清除缓存:
清除全部缓存
1 | curl -XPOST '127.0.0.1:9200/_cache/clear?pretty' |
清除单个索引缓存
1 | curl -XPOST '127.0.0.1:9200/index_name/_cache/clear?pretty' |
清除多索引缓存
1 | curl -XPOST '127.0.0.1:9200/index_name1,index_name2/_cache/clear?pretty' |
2.3 缓存使用
在默认情况下Request Cache是关闭的,我们需要手动开启:
1 | curl -XPUT '127.0.0.1:9200/index/_settings' -d'{ "index.requests.cache.enable": true }' |
开启缓存后,我们需要在那些需要缓存的搜索请求上加上request_cache=true这个参数然后将size设置为0才能使我们的查询请求被缓存。例如:
1 | curl 'localhost:9200/my_index/_search?request_cache=true' -d '{ |
注意两点:
1 上面的参数size:0非常重要必须强制指定才能被缓存,否则请求是不会缓存的
2 在使用script执行查询时一定要指定request_cache=false,因为脚本的执行结果是不确定的(比如使用random函数或使用了当前时间作为参数),这种情况下应该避免使用缓存
2.4 缓存的Key
对于Request Cache来说,它的Cache Key就是整个查询的DSL语句,所以如果要命中缓存查询生成的DSL一定要一样,这里的一样是指DSL这个字符串一样。只要有一个字符或者子查询的顺序变化都不会命中缓存。
通过下面的参数我们可以设置缓存的大小,默认情况下是JVM堆的1%大小,当然我们也可以手动设置在elasticsearch.yml文件里:
1 | indices.requests.cache.size: 1% |
2.5 总结
- Request Cache是一个“分片级”的缓存
- Request Cache是一个面向请求的缓存,缓存的key是查询DSL字符串
- Request Cache默认没有开启,需要手动开启,且要缓存生效需要在请求参数上加上request_cache=true并把size设置为0
- 缓存是自动失效的,失效时间就是索引的refresh时间(index.refresh_interval),在分片有改变的情况下默认是1秒失效一次
- 缓存的默认大小是JVM堆内存的1%,可以通过参数indices.request.cache.expire 手动设置
3. Fielddata
3.1 说明
Fielddata是专门针对分词的字段在query-time(查询期间)的数据结构的缓存。当我们第一次在一个分词的字段上执行聚合、排序或通过脚本访问的时候就会触发该字段Fielddata Cache的加载,这种缓存是segment级别的,当有新的segment打开时旧的缓存不会重新加载,而是直接把新的segement对应的Fielddata Cache加载到内存。
加载Fielddata Cache是一个非常昂贵的操作,一旦Fielddata被加载到内存,那么在该Fielddata Cache对应的Segement生命周期范围内都会驻留在内存中。也就是说当段合并时会触发合并后更大段的Fielddata Cache加载。
Fielddata会消耗大部分的JVM堆内存,特别是当加载“高基数”的分词字段时(那些分词后存在大量不同词的字段),针对这种字段的聚合排序其实是非常没有意义的,我们更多的要去考虑是否能用not_analyzed代替(这样就可以使用doc_values实现聚合、排序)。
3.2 缓存使用
默认情况下Fielddate Cache是默认开启的,我们可以通过下面的设置来关闭,关闭后就无法对分词字段执行聚合、排序操作了:(Kibana命令)
1 | PUT my_index |
在ES1.X里面除了string类型其他类型也是使用Fieldata的,在ES2.X中除了分词的String类型字段,其他类型都使用doc_values。
Fielddata的加载方式有3种:
1.lazy: 懒加载是默认的加载方式,当第一次使用时加载
2.eager:预加载模式是当一个新的索引段变成Searchable之前会被加载
3.eager_global_ordinals:全局序数预加载模式,这种方式能生成一份全局序数表,可降低内存使用。
如下设置为eager_global_ordinals:
1 | PUT my_index |
Fielddata也可指定满足某些条件的term才被加载进内存,比如通过词频加载:
1 | PUT my_index |
上面的设置表示词频在0.001到0.1之间的且段持有的文档数在500以上的term会被加载到内存。
或者通过正则表达式加载:
1 | PUT my_index |
上面的设置表示只有满足pattern的词才会被加载到内存。
3.3 缓存配置
1.indices.fielddata.cache.size:此参数设置缓存大小(默认是不限制)。可设置百分数如30%,或者数字12GB。
2.indices.breaker.fielddata.limit:此参数设置Fielddata断路器限制大小(公式:预计算内存 + 现有内存 <= 断路器设置内存限制),默认是60%JVM堆内存,当查询尝试加载更多数据到内存时会抛异常(以此来阻止JVM OOM发生)。
3.indices.breaker.fielddata.overhead:一个常数表示内存预估值系数,默认1.03,比如预计算加载100M数据,那么100*1.03=103M会用103M作为参数计算是否超过断路器设置的最大值。