DocValues
什么是DocValues
简单说明DocValues就是一个种列式的数据存储结构(docid、termvalues)。
倒排索引的优势在于查找包含某个项的文档,即通过Term查找对应的docid。
term的倒排
Term | Doc_1 | Doc_2 | Doc_3 |
---|---|---|---|
brown | X | X | |
dog | X |
term2的倒排
Term2 | Doc_1 | Doc_2 | Doc_3 |
---|---|---|---|
brown2 | X | X | |
dog2 | X |
如此能够快速定位包含brown的文档为doc1和doc2。
但是对于从另外一个方向的相反操作并不高效,即根据docid找到该文档的指定字段(Term2)的值是什么。但是聚合、排序和明细查询等时候需要这种的访问模式。
声明遍历索引是不可取。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。为了能够解决上述问题,我们使用了Doc values,通过转置两者间的关系来解决这个问题。
term的docvalues
Doc | Term |
---|---|
Doc_1 | brown |
Doc_2 | brown |
Doc_3 | dog |
term2的docvalues
Doc | Term2 |
---|---|
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),计算时候则会得到
term | count |
---|---|
the | 1 |
first | 1 |
而不是
term | count |
---|---|
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,这个过程可能会要数秒。对于 已经习惯亚秒响应的用户很难会接受停顿数秒卡顿。
有三种方式可以解决这个延时高峰:
- 预加载 fielddata,设置提前加载。
- 预加载全局序号。一种减少内存占用的加载优化方式,类似于一种全局字典(存储string字段和其对应的全局唯一int值),这样只加载int值,然后查找字典中的对应的string字段。
- 缓存预热。已经弃用。