1. 为什么使用fielddata
对于不分词的field,在进行POST/PUT的时候会创建doc->value正排索引,但是对于分词的field,在进行POST/PUT的时候,是不会自动生成doc->value正排索引的,因为这会消耗大量的空间。 对于分词的field进行聚合操作,会发现报错,报错信息如下:
{ "error": { "root_cause": [ { "type": "illegal_argument_exception", "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory." } ], "type": "search_phase_execution_exception", "reason": "all shards failed", "phase": "query", "grouped": true, "failed_shards": [ { "shard": 0, "index": "test_index", "node": "4onsTYVZTjGvIj9_spWz2w", "reason": { "type": "illegal_argument_exception", "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory." } } ], "caused_by": { "type": "illegal_argument_exception", "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory." } }, "status": 400 }
根据报错信息 Fielddata is disabled on text fields by default. Set fielddata=true on [test_field] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory.
, 你必须要打开fielddata,然后将正排索引数据加载到内存中,才可以对分词的field执行聚合操作,而且会消耗很大的内存.
2.分词field+fielddata的工作原理
如果需要进行聚合操作的field不分词,那么在index-time就会自动生成doc->value正排索引,正对这些不分词的field执行聚合操作的时候,就会直接使用doc -> value来进行聚合操作。
对于分词的field,在index-time的时候是不会建立doc->value正排索引的,因为分词之后。占用的空间过大,所有默认不支持对分词的field进行聚合操作,如果直接对其进行聚合操作,会直接报错。
对于分词的field,要进行聚合操作,必须要打开和使用fielddata,即将field的fielddata设置为true。
在将fielddata设置为true之后,es就会在执行聚合操作的时候,现场将field对应的数据,建立一份fielddata正排索引,其结构和doc -> value正排索引类型,但是只会讲fielddata正排索引加载到内存中,然后基于内存中的fielddata正排索引执行分词field的聚合操作。
使用fielddata之后,生成的fielddata正排索引会加载到内存中,这会耗费内存空间;之所以将其放在内存之中,是因为基于磁盘和os cache,那么性能会很差。
3. fielddata核心原理
fielddata加载到内存中的过程是懒加载的,在对一个分词field执行聚合操作的时候,才会加载。 加载时field级别的加载,而不是匹配查询条件的文档,一个index的一个field,所有的doc都会被加载,而不是少数的doc被加载。 不是在index-time的时候创建,而是在quer-time的时候创建。
4.fielddata内存限制
indices.fielddata.cache.size 控制为fielddata分配的堆空间大小。 当你发起一个查询,分析字符串的聚合将会被加载到fielddata,如果这些字符串之前没有被加载过。
如果结果中fielddata大小超过了指定大小,其他的值将会被回收从而获得空间(使用LRU算法执行回收)。 默认无限制,限制内存使用,但是会导致频繁evict和reload,大量IO性能损耗,以及内存碎片和gc,这个参数是一个安全卫士,必须要设置: indices.fielddata.cache.size: 20%
5. 监控fielddata内存使用
Elasticsearch提供了监控监控fielddata内存使用的命令,我们在上面可以看到内存使用和替换的次数,过高的evictions值(回收替换次数)预示着内存不够用的问题和性能不佳的原因:
# 按照索引使用 GET /_stats/fielddata?fields=* # 按照节点使用 GET /_nodes/stats/indices/fielddata?fields=* # 按照节点索引使用 GET /_nodes/stats/indices/fielddata?level=indices&fields=*
fields=*表示所有的字段,也可以指定具体的字段名称。
6.circuit breaker
indices.fielddata.cache.size的作用范围是当前查询完成后,发现内存不够用了才执行回收过程,如果当前查询的数据比内存设置的fielddata 的总量还大,如果没有做控制,可能就直接OOM了。
熔断器的功能就是阻止OOM的现象发生,在执行查询时,会预算内存要求,如果超过限制,直接掐断请求,返回查询失败,这样保护Elasticsearch不出现OOM错误。
常用的配置如下:
- indices.breaker.fielddata.limit:fielddata的内存限制,默认60%
- indices.breaker.request.limit:执行聚合的内存限制,默认40%
- indices.breaker.total.limit:综合上面两个,限制在70%以内
最好为熔断器设置一个相对保守点的值。fielddata需要与request断路器共享堆内存、索引缓冲内存和过滤器缓存,并且熔断器是根据总堆内存大小估算查询大小的,而不是实际堆内存的使用情况,如果堆内有太多等待回收的fielddata,也有可能会导致OOM发生。
7. fielddata filter的细粒度内存加载控制
滤的主要目的是去掉长尾数据,我们可以加一些限制条件,如下请求:
POST /test_index/_mapping/my_type { "properties": { "my_field": { "type": "text", "fielddata": { "filter": { "frequency": { "min": 0.01, "max": 0.1, "min_segment_size": 500 } } } } } }
min: 仅仅加载至少在1%的doc中出现过得term对应的fielddata。 max: 仅仅加载最多在10%的doc中出现过得term对应的fielddata。 min_segment_size: 少于500个doc的segment不加载fielddata。
比如说某个值,hello,总共有1000个doc,hello必须在10个doc中出现,最多在100个doc中出现,那么这个field对应的fielddata才会加载到内存中去。
fidelddata是按段来加载的,某个segment里面的doc数量少于500个,那么这个segment的fielddata就不加载,所以出现频率是基于某个段计算得来的,如果一个段内只有少量文档,统计词频意义不大,等段合并到大的段当中,超过500个文档这个限制,就会纳入计算。
8. fielddata预加载
如果真的要对分词的field执行聚合,那么每次都在query-time现场生产fielddata并加载到内存中来,速度可能会比较慢。
POST /test_index/_mapping/test_type { "properties": { "test_field": { "type": "string", "fielddata": { "loading" : "eager" } } } }
query-time的fielddata生成和加载到内存,变为index-time,建立倒排索引的时候,会同步生成fielddata并且加载到内存中来,这样的话,对分词field的聚合性能当然会大幅度增强。
9. 序号标记预加载
假设我们的文档用来标记状态有几种字符串:
- SUCCESS
- FAILED
- PENDING
- WAIT_PAY
状态这类的字段,系统设计时肯定是可以穷举的,如果我们存储到Elasticsearch中也用的是字符串类型,需要的存储空间就会多一些,如果我们换成1,2,3,4这种Byte类型的,就可以节省很多空间。
"序号标记"做的就是这种优化,如果文档特别多(PB级别),那节省的空间就非常可观,我们可以对这类可以穷举的字段设置序号标记,如下请求:
POST /test_index/_mapping/test_type { "properties": { "test_field": { "type": "string", "fielddata": { "loading" : "eager_global_ordinals" } } } }
10. 深度优先VS广度优先
Elasticsearch的聚合查询时,如果数据量较多且涉及多个条件聚合,会产生大量的bucket,并且需要从这些bucket中挑出符合条件的,那该怎么对这些bucket进行挑选是一个值得考虑的问题,挑选方式好,事半功倍,效率非常高,挑选方式不好,可能OOM,我们拿深度优先和广度优先这两个方式来讲解。
我们举个电影与演员的例子,一部电影由多名演员参与,我们搜索的需求:出演电影最多的10名演员以及他们合作最多的5名演员。
如果是深度优先,这种查询方式需要构建完整的数据,会消耗大量的内存。假设我们每部电影有10位演员(1主9配),有10万部电影,那么第一层的数据就有10万条,第二层为9*10万=90万条,共100万条数据。
我们对这100万条数据进行排序后,取主角出演次数最多的10个,即10条数据,裁掉99加上与主角合作最多的5名演员,共50条数据。
构建了100万条数据,最终只取50条,内存是不是有点浪费?
如果是广度优先,这种查询方式先查询电影主角,取前面10条,第一层就只有10条数据,裁掉其他不要的,然后找出跟主角有关联的配角人员,与合作最多的5名,共50条数据。
聚合查询默认是深度优先,设置广度优先只需要设置collect_mode参数为breadth_first,示例:
GET /music/children/_search { "size": 0, "aggs": { "lang": { "terms": { "field": "language", "collect_mode" : "breadth_first" }, "aggs": { "length_avg": { "avg": { "field": "length" } } } } } }
注意
使用深度优先还是广度优先,要考虑实际的情况,广度优先仅适用于每个组的聚合数量远远小于当前总组数的情况,比如上面的例子,我只取10位主角,但每部电影都有一位主角,聚合的10位主角组数远远小于总组数,所以是适用的。
另外一组按月统计的柱状图数据,总组数固定只有12个月,但每个月下的数据量特别大,广度优先就不适合了。 所以说,使用哪种方式要看具体的需求。