elasticsearch的Doc Values 和 Fielddata

DocValues

什么是DocValues

简单说明DocValues就是一个种列式的数据存储结构(docid、termvalues)。 
倒排索引的优势在于查找包含某个项的文档,即通过Term查找对应的docid。

term的倒排

TermDoc_1Doc_2Doc_3
brown X X  
dog     X

term2的倒排

Term2Doc_1Doc_2Doc_3
brown2 X X  
dog2     X

如此能够快速定位包含brown的文档为doc1和doc2。

但是对于从另外一个方向的相反操作并不高效,即根据docid找到该文档的指定字段(Term2)的值是什么。但是聚合、排序和明细查询等时候需要这种的访问模式。 
声明遍历索引是不可取。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。为了能够解决上述问题,我们使用了Doc values,通过转置两者间的关系来解决这个问题。 
term的docvalues

DocTerm
Doc_1 brown
Doc_2 brown
Doc_3 dog

term2的docvalues

DocTerm2
Doc_1 brown2
Doc_2 brown2
Doc_3 dog2

举例说明:

select term2,count(1) from table where Term=’brown’ group by term2

如要完成类似上述sql的聚合查询,则: 
1. 定位数据范围。检索到Term=’brown’的docid(doc1,doc2),此时使用倒排索引 
2. 进行聚合计算。根据doc1,doc2定位到term2的字段均为brown2,进行聚合累加得到计算结果。browm2的count(1)=2。

搜索和聚合是相互紧密关联的。搜索使用倒排索引查找文档,聚合操作收集和聚合 doc values 里的数据。

DocValues是如何工作的

DocValues的官方文档介绍特点为fast, efficient and memory-friendly。 
1、DocValues是在索引时与倒排索引同时生成的,并且是不可变的。与倒排一样,保存在lucene文件中(序列化到磁盘)。 
lucene文件操作依赖于操作系统的缓存来管理,而不是在 JVM 堆栈里驻留数据。 这个特点决定了在使用es时候要分配足够内存给os,保证文件处理性能,详细设置可以参照 生产环境elasticsearch的配置建议

2、DocValues使用列式压缩 
现代 CPU 的处理速度要比磁盘快几个数量级, 这意味着减少必须从磁盘读取的数据量总是有益的,尽管需要额外的 CPU 运算来进行解压。

DocValues使用了很多压缩技巧。它会按依次检测以下压缩模式: 
- 如果所有的数值各不相同(或缺失),设置一个标记并记录这些值 
- 如果这些值小于 256,将使用一个简单的编码表 
- 如果这些值大于256,检测是否存在一个最大公约数 
- 如果没有存在最大公约数,从最小的数值开始,统一计算偏移量进行编码 
这些压缩模式不是传统的通用的压缩方式,比如 DEFLATE 或是 LZ4。 因为列式存储的结构是严格且良好定义的,我们可以通过使用专门的模式来达到比通用压缩算法(如 LZ4 )更高的压缩效果。

,字符类型通过借助顺序表(ordinal table)进行类似编码的。字符类型是去重之后存放到顺序表的,通过分配一个 ID,然后这些 ID 和数值类型的文档值一样使用。 也就是说,字符类型和数值类型一样拥有相同的压缩特性。 
顺序表本身也有很多压缩技巧,比如固定长度、变长或是前缀字符编码等等。

3、DocValues支持禁用 
此值默认是启动状态,如果没有必要使用可以设置 doc_values: false来禁用。

DocValues不支持的

根据上面介绍,Doc values 是不支持 analyzed 字符串字段的,想象一下,如果一个字段是analyzed,如the first,则在分析阶段则会docvalues则会存储为两条docvalue(the和first),计算时候则会得到

termcount
the 1
first 1

而不是

termcount
the first 1

那想要怎么达到我们想要的结果呢?fielddata。

Fielddata

doc values 不生成分析的字符串,然而,这些字段仍然可以使用聚合,是因为使用了fielddata 的数据结构。与 doc values 不同,fielddata 构建和管理 100% 在内存中,常驻于 JVM 内存堆。fielddata 是 所有 字段的默认设置。

注意内存使用

一些特性: 
1. Fielddata 是延迟加载的。如果你从来没有聚合一个分析字符串,就不会加载 fielddata 到内存中,是在查询时候构建的。 
2. fielddata 是基于字段加载的, 只有很活跃地使用字段才会增加fielddata 的负担。 
3. fielddata 会加载索引中(针对该特定字段的) 所有的文档,而不管查询是否命中。逻辑是这样:如果查询会访问文档 X、Y 和 Z,那很有可能会在下一个查询中访问其他文档。 
4. 如果空间不足,使用最久未使用(LRU)算法移除fielddata。 
所以,fielddata应该在JVM中合理利用,否则会影响es性能。 
我们可以使用indices.fielddata.cache.size限制fielddata内存使用,可以是具体大小(如2G),也可以是占用内存的百分比(如20%)。 
也可以使用如下命令进行监控。

GET /_stats/fielddata

最后,如果一次性加载字段直接超过内存值会发生什么?挂掉?所以es为了防止这种情况,采用了circuit breaker(熔断机制)。 
它通过内部检查(字段的类型、基数、大小等等)来估算一个查询需要的内存。它然后检查要求加载的 fielddata 是否会导致 fielddata 的总量超过堆的配置比例。如果估算查询大小超出限制,就会触发熔断,查询会被中止并返回异常。

indices.breaker.fielddata.limit fielddata级别限制,默认为堆的60% 
indices.breaker.request.limit request级别请求限制,默认为堆的40% 
indices.breaker.total.limit 保证上面两者组合起来的限制,默认堆的70%

Fielddata过滤

通过设置可以只加载部分fielddata来节省内存。

“frequency”: { 
“min”: 0.01, 
“min_segment_size”: 500 

只加载那些至少在本段文档中出现 1% 的项。 
忽略任何文档个数小于 500 的段。 
详细参照官网。

Fielddata预加载

加载fielddata默认是延迟加载 。 当 Elasticsearch 第一次查询某个字段时,它将会完整加载这个字段所有 Segment中的倒排索引到内存中,以便于以后的查询能够获取更好的性能。 
对于小索引段来说,这个过程的需要的时间可以忽略。但如果索引很大几个GB,这个过程可能会要数秒。对于 已经习惯亚秒响应的用户很难会接受停顿数秒卡顿。 
有三种方式可以解决这个延时高峰:

    1. 预加载 fielddata,设置提前加载。
    2. 预加载全局序号。一种减少内存占用的加载优化方式,类似于一种全局字典(存储string字段和其对应的全局唯一int值),这样只加载int值,然后查找字典中的对应的string字段。
    3. 缓存预热。已经弃用。
posted @ 2018-04-19 10:06  程序猿小硕  阅读(553)  评论(0编辑  收藏  举报