ES内存使用总结

前言 Elasticsearch 是 Java 语言开发,底层的存储引擎是基于Lucene实现,Lucene的倒排索引(Inverted Index)是先在内存里生成,然后定期以段文件(segment file)的形式刷到磁盘的。
每个段文件实际就是一个完整的倒排索引,并且一旦写到磁盘上就不会做修改。API层面的文档更新和删除实际上是增量写入的一种特殊文档,会保存在新的段里。不变的段文件易于被操作系统cache,热数据几乎等效于内存访问。
内存结构 ElasticSearch的内存从大的结构可以分堆内存(On Heap)和堆外内存(Off Heap)。Off Heap部分由Lucene进行管理。On Heap部分存在可GC部分和不可GC部分,可GC部分通过GC回收垃圾对象,从而释放内存。不可GC部分不能通过GC回收垃圾对象,这部分会通过LRU算法进行对象清除并释放内存。更加具体的内存占用与分配如下图:
ES内存使用总结
文章图片

On Heap内存占用 这部分内存占用的模块包括:Indexing Buffer、Node Query Cache、Shard Request Cache、Field Data Cache以及Segments Cache。
Indexing Buffer
默认分配的内存大小是10% heap size,当缓存满了或者refresh/flush interval到了,就会以segment file的形式写入到磁盘。
Indexing Buffer的存在可以提高文档写入请求的响应速度,获得更高的吞吐量,减少磁盘IO的访问频率,节省了CUP资源。这部分空间是可以通过GC释放被反复利用的。
缓存时机:新文档数据写入的时候
失效或者回收:当空间满了的时候会触发GC清楚缓存对象,释放空间
Node Query Cache (Filter Cache)
节点级别的缓存,节点上的所有分片共享此缓存,是Lucene层面的实现。缓存的是某个filter子查询语句在一个segment上的查询结果。如果一个segment缓存了某个filter子查询的结果,下次可以直接从缓存获取结果,无需再在segment内进行过滤查询。
每个segment有自己的缓存,缓存的key为filter子查询(query clause ),缓存内容为查询结果,这些查询结果是匹配到的document numbers,保存在位图FixedBitSet中。
【ES内存使用总结】缓存的构建过程是:对segment执行filter子查询,先获取查询结果中最大的 document number: maxDoc(document number是lucene为每个doc分配的数值编号,fetch的时候也是根据这个编号获取文档内容)。然后创建一个大小为 maxDoc的位图:FixedBitSet,遍历查询命中的doc,将FixedBitSet中对应的bit设置为1。
例如:查询结果的maxDoc是8,那么创建出的FixedBitSet就是:[0,0,0,0,0,0,0,0],可以理解为是一个长度为8的二值数组,初始值都是0,假设filter查询结果的doc列表是:[1,4,8],那么FixedBigSet就设置为:
[1,0,0,1,0,0,0,1],当查询有多个filter子查询时,对位图做交并集位运算即可。
用一个例子来说明Node Query Cache结构。如下图查询语句包含两个子查询,分别是对date和age字段的range查询,Lucene在查询过程中遍历每个 segment,检查其各自的LRUQueryCache能否命中filter子查询,segment 8命中了对age和date两个字段的缓存,将会直接返回结果。segment 2只命中了对age字段的缓存,没有命中date字段缓存,将继续执行查询过程。
ES内存使用总结
文章图片

缓存时机:
1.访问频率大于等于特定阈值之后,query结果才会被缓存
2.segment的 doc 数量需要大于10000,并且占整个分片的3%以上
失效或回收:segment合并会导致缓存失效。内存的管理使用LRU算法。
Shard Request Cache
Shard Request Cache简称Request Cache,他是分片级别的查询缓存,每个分片有自己的缓存,属于ES层面的实现。ES默认情况下最多使用堆内存的1%用作 Request Cache,这是一个节点级别的配置。内存的管理使用LRU算法。
缓存的实现在IndicesRequestCache类中,缓存的key是一个复合结构,主要包括shard,indexreader,以及客户端请求。缓存的value是将查询结果序列化之后的二进制数据。

final Key key =new Key(cacheEntity, reader.getReaderCacheHelper().getKey(), cacheKey);

cacheEntity:主要是shard信息,代表该缓存是哪个shard上的查询结果。
readerCacheKey:主要用于区分不同的IndexReader。
cacheKey:主要是整个客户端请求的请求体(source)和请求参数(preference、indexRoutings、requestCache等)。
Request Cache的主要作用是对聚合的缓存,聚合过程是实时计算,通常会消耗很多资源,缓存对聚合来说意义重大。
由于客户端请求信息直接序列化为二进制作为缓存key的一部分,所以客户端请求的json顺序,聚合名称等变化都会导致cache无法命中。
缓存时机:简单的可以理解成只有客户端查询请求中size=0的情况下才会被缓存
失效或回收:
1.新的segment写入到分片后,缓存会失效,因为之前的缓存结果已经无法代表整个分片的查询结果。
2.分片refresh的时候,缓存失效
Field Data Cache
在有大量排序、数据聚合的应用场景,需要将倒排索引里的数据进行解析,按列构造成 docid->value 的形式才能够做后续快速计算。对于数据量很大的索引,这个构造过程会非常耗费时间,因此ES 2.0以前的版本会将构造好的数据缓存起来,提升性能。由于heap空间有限,当遇到用户对海量数据做计算的时候,就很容易导致heap吃紧,集群频繁GC,根本无法完成计算过程。内存的管理使用LRU算法。
Segment Cache(Segment FST Cache)
一个segment是一个完备的lucene倒排索引,倒排索引是通过词典 (Term Dictionary)到文档列表(Postings List)的映射关系实现快速查询的。由于词典和文档的数据量比较大,全部装载到heap里不现实,所以存储在硬盘上的。
为了快速定位一个词语在词典中的位置。Lucene为词典(Term Dictionary)做了一层词典索引(Term Index)。这个词典索引采用的数据结构是FST (Finite State Transducer)。Lucene在打开索引的时候将词典索引(Term Index)全量装载到内存中,即:Segment FST Cache,这部分数据永驻堆内内存,且无法设置大小,长期占用50% ~ 70%的堆内存。内存管理使用LRU算法。
FST(详细参考这里) 。可以参考TRIE树进行理解。FST在时间复杂度和空间复杂度上都做了最大程度的优化,使得Lucene能够将Term Dictionary(词典)完全加载到内存,快速的定位Term找到响应的output(posting倒排列表)。
内存回收与释放:
1.删除不用的索引
2.关闭索引(文件仍然存在于磁盘,只是释放掉内存),需要的时候可重新打开。
3.定期对不再更新的索引做force merge。实质是对segment file强制做合并,segment数量的减少可以节省大量的Segment Cache的内存占用。
Off Heap内存占用 Lucene中的倒排索引以段文件(segment file)的形式存储在磁盘上,为了提高倒排索引的加载与检索速度,避免磁盘IO访问导致的性能损耗,Lucene会把倒排索引数据加载到磁盘缓存(操作系统一般会用系统内存来实现磁盘缓存),所以在进行内存分配的时候,需要考虑到这部分内存,一般建议是把50%的内存给Elasticsearch,剩下的50%留给Lucene。

    推荐阅读