elasticsearch除了提供检索功能之外,还能做更多的事情,比如近实时的数据分析。对此elasticsearch提供了聚合aggregation模块,这比之前的facet模块更加强大。具体提供的聚合类型本文不再介绍,而是重点关注aggr模块中关于内存控制的一些事情。
1:Fielddata
aggr大量使用fielddata,fielddata也是es中的内存大户。众所周知,倒排索引建立了term到doc之间的映射,因此适合快速查询匹配term的doc。而aggr的应用场景恰恰是相反的,因此需要建立doc到term之间的映射关系,这就是fielddata。
(1) analyzed vs not_analyzed:
如果是分词字段,则该字段的值被拆解成为多个term,如果是针对term的聚合操作就会出现意向不到的结果,所以要根据应用场景来决定是对分词字段进行聚合还是对不分词字段进行聚合操作,当然这种错误是比较容易发现的,在multifield的前提下可以及时作出调整。
对于基数很高的string类型的分词字段,如果进行聚合操作,fielddata会非常大,加载到内存中会有隐患。分词进程通常会产生大量term,并且很多term都是唯一的,这就增加了这个field的总体基数,也就增加了更多的内存压力。考虑n-gram分词的场景,比如new york将会产生ne,ew,w_,_y,yo,or,rk多个,这无疑增加了内存的消耗。
(2) 控制fielddata的内存使用量
为了aggr操作的速度考虑,fielddata需要load到内存中以提高访问效率。但这并不是没有代价的。过多的fielddata会造成jvm的垃圾回收变慢,甚至会造成oom。jvm的内存空间也会有限资源,如果fielddata的内存使用控制不好,会对节点稳定性造成影响。es进程可以使用的内存大小可以通过ES_HEAP_SIZE来进行设置。注意:可以调高这个参数来获取更多的内存使用量,但是建议不超过总体内存的50%,但是不要超过32G.(这是jvm的机制决定的,在小于32G的情况下会开启指针压缩,8字节变4字节,无疑将会节约大量内存,大于32G的情况下,则不启用指针压缩,空间虽然大了,但是垃圾回收将会消耗更大,导致不稳定性)。下边来看一下es对fielddata内存使用量的控制机制:
indices.fielddata.cache.size:控制fielddata可以使用多大内存。如果超过这个设置,则lru(这会导致大量的磁盘IO,在内存中也会出现大量的垃圾)而es对这个设置的默认取值是无限制的(鉴于几个考量:1:fielddata并不是只用一次的cache 2:fielddata建立过程是昂贵的操作,如果每次请求再去加载,显然是无法接受的。),因此在默认设置下,是不会有数据淘汰的。
这里有一个关键点:对于size的衡量是否超过限制,是先加载再衡量的,因此就会有一个问题:如果需要加载的fielddata超过已经超过了目前的限制,会怎么样呢?很遗憾,会导致oom。鉴于这个考量,有了另外一个机制:circuit breaker。该机制会预估将要加载的fielddata的数量来决定是否加载,如果容量充足则加载,如果容量不足则会放弃这次请求,抛出一个异常(如果抛出了这个异常,应用就需要考虑下为什么你的请求需要那么多的fielddata呢?是否要优化一下。)。因为是预估,所以是在加载之前,因此也就不会造成oom的问题。针对fielddata的配置项为:indices.breaker.fielddata.limit。默认60%。另外还有indices.breaker.request.limit,默认40% indices.break.total.limit,默认70%。
建议将circuit breaker配置为一个相对保守的数字,默认60%是相对合理的。我们应该注意到,jvm的heap是共享的,也就是说fielddata.limit, request.limit,以及index buffer,filter cache等等都是共享heap的。因此如果过度设置circuit breaker会引起潜在oom,可能让node挂掉。
注意:indices.fielddata.cache.size < indices.breaker.fielddata.limit.limit的值一定要大于size值。否则将永远不会有lru发生,也就失去了设置的意义。
其实circuit breaker机制也不是没有缺陷的。首先,是一种预估,并不是准确的,因此会存在偏差。其次,比较的对象是total heap而不是free heap,因此强烈建议配置的值要是相对保守的。
另外fielddata支持filter,可以决定加载哪些不加载哪些,比如tf小于0.01的不加载等等。根据具体的业务场景来进行分析。
2: doc values
理想情况下我们希望fielddata是全部常驻内存的,但是内存毕竟是有限的,因此需要增加节点来尽量多的保存fielddata数据。但是这样也造成了资源的浪费,在内存被充分利用的情况下,cpu等其他资源则可能是闲置的。如果采用了上述的配置,内存是限制住了,但是缺陷也是显而易见的。有没有一种访问速度快还不占用内存的fielddata的方案呢?有,doc values。
目前doc values的访问速度还是略慢于fielddata,大约10%-25%左右的样子。但是优势也是显而易见的:首先,磁盘存储而不是内存,这就不再多说了。其次,doc value是在index的时候创建的,fielddata需要在查询的时候加载(反转倒排索引),而doc value由于是index的时候创建完成了,因此在初始化过程要明显快于fielddata。
当然没有什么事情是完美的,doc values的优势自然也带来了不足,比如索引会变大,访问速度略慢于fielddata。但是我们真的会在乎访问速度的细微差别么?不尽然。doc values已经足够的高效,所以应用也许并不关注这点细微的差距。况且由此来会带来较快的垃圾回收等优势。
doc values目前还不能应用与analyzed string field。
doc value的启用也非常简单,只需要在mapping中对应的doc_values属性设置为true即可。
在可以遇见的将来doc value format将会成为es默认设置。
3: aggr的collect_mode
默认是depth_first,大多情况下都能很好的工作,但在一些特殊场景下breadth_first则更适合。比如
我们有一份电影的数据,记录了每一部电影的参演演员信息。我们想得到参演最多的10个演员,并且得到与这10个演员合作做多的演员的前5个助演。
显然,只需要对actors进行terms aggr,size=10,并且内嵌一个同样类型的terms aggr,size=5。
但是,我们只是想得到top10 和 top5,总计50个对象,而默认的执行过程是depth_first,假设一部电影平均n个演员,那这个数量级显然是平方级别的,其实大部分对我们并没有用处。在这种应用场景下,我们只需要第一层聚合的10个对象,然后再进一步拓展,因此需要把第一层其余bucket去除,也就是breadth_first要做的事情。根据名称也可以知道,类似于多叉树的深度优先和广度优先。