Elasticsearch之深入了解Doc Values 和 Fielddata
DocValues什么是DocValues简单说明DocValues就是一个种列式的数据存储结构(docid、termvalues)。倒排索引的优势在于查找包含某个项的文档,即通过Term查找对应的docid。Field 的倒排FieldDoc_1Doc_2Doc_3brownXXdogXField2 的倒排Field2...
基本概念
这两个概念比较像,所以大部分时候会放在一起说。
这两个概念源于Elasticsearch(后面简称ES)除了强大的搜索功能外,还可以支持排序,聚合之类的操作。搜索需要用到倒排索引,而排序和聚合则需要使用 “正排索引”。说白了就是一句话,倒排索引的优势在于查找包含某个项的文档,而反过来确定哪些项在单个文档里并不高效。
doc_values和fielddata就是用来给文档建立正排索引的。他俩一个很显著的区别是,前者的工作地盘主要在磁盘,而后者的工作地盘在内存。
索引速度稍低这个是相对于fielddata方案的,其实仔细想想也可以理解。那排序举例,相对于一个在磁盘排序,一个在内存排序。谁的速度快自然不用多说。
在ES 1.x版本的官方说法是,
Doc values are now only about 10–25% slower than in-memory fielddata
虽然速度稍慢,doc_values的优势还是非常明显的。一个很显著的点就是他不会随着文档的增多引起OOM问题。正如前面说的,doc_values在磁盘创建排序和聚合所需的正排索引。这样我们就避免了在生产环境给ES设置一个很大的HEAP_SIZE,也使得JVM的GC更加高效,这个又为其它的操作带来了间接的好处。
而且,随着ES版本的升级,对于doc_values的优化越来越好,索引的速度已经很接近fielddata了,而且我们知道硬盘的访问速度也是越来越快(比如SSD)。所以 doc_values 现在可以满足大部分场景,也是ES官方重点维护的对象。
所以我想说的是,doc values相比field data还是有很多优势的。所以 ES2.x 之后,支持聚合的字段属性默认都使用doc_values,而不是fielddata。
1、正排索引(doc values )VS倒排索引:
(1)概念:从广义来说,doc values 本质上是一个序列化的 列式存储 。列式存储 适用于聚合、排序、脚本等操作,所有的数字、地理坐标、日期、IP 和不分析( not_analyzed )字符类型都会默认开启。
(2)特点:倒排索引的优势 在于查找包含某个项的文档,相反,如果用它确定哪些项是否存在单个文档里。
(3)优化:es官方是建议,es大量是基于os cache来进行缓存和提升性能的,不建议用jvm内存来进行缓存,那样会导致一定的gc开销和oom问题,给jvm更少的内存,给os cache更大的内存。比如64g服务器,给jvm最多416g(1/161/4),os cache可以提升doc value和倒排索引的缓存和查询效率
DocValues
什么是DocValues
简单说明DocValues就是一个种列式的数据存储结构(docid、termvalues)。
倒排索引的优势在于查找包含某个项的文档,即通过Term查找对应的docid。
- Field 的倒排
Field | Doc_1 | Doc_2 | Doc_3 |
---|---|---|---|
brown | X | X | |
dog | X |
- Field2 的倒排
Field2 | Doc_1 | Doc_2 | Doc_3 |
---|---|---|---|
brown2 | X | X | |
dog2 | X |
如此能够快速定位包含brown的文档为doc1和doc2。
但是对于从另外一个方向的相反操作并不高效,即根据docid找到该文档的指定字段(Field2)的值是什么。但是聚合、排序和明细查询等时候需要这种的访问模式。
声明遍历索引是不可取。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。为了能够解决上述问题,我们使用了Doc values,通过转置两者间的关系来解决这个问题。
(注意 doc_value string不能为text型只能为 keyword)
- Field 的docvalues
Doc | Field |
---|---|
Doc_1 | brown |
Doc_2 | brown |
Doc_3 | dog |
- Field2 的docvalues
Doc | Field2 |
---|---|
Doc_1 | brown2 |
Doc_2 | brown2 |
Doc_3 | dog2 |
举例说明:
select Field2 ,count(1) from table where Field =‘brown’ group by Field2
如要完成类似上述sql的聚合查询,则:
- 定位数据范围。检索到Field ='brown’的docid(doc1,doc2),此时使用倒排索引
- 进行聚合计算。根据doc1,doc2定位到Field2 的字段均为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),计算时候则会得到
Field | count |
---|---|
the | 1 |
first | 1 |
而不是
Field | count |
---|---|
the first | 1 |
那想要怎么达到我们想要的结果呢?fielddata。
Fielddata
doc values 不生成分析的字符串,然而,这些字段仍然可以使用聚合,是因为使用了fielddata 的数据结构。与 doc values 不同,fielddata 构建和管理 100% 在内存中,常驻于 JVM 内存堆。fielddata 是 所有 字段的默认设置。
注意内存使用
一些特性:
-
- fielddata 是延迟加载的。如果你从来没有聚合一个分析字符串,就不会加载 fielddata 到内存中,是在查询时候构建的。
-
- fielddata 是基于字段加载的, 只有很活跃地使用字段才会增加fielddata 的负担。
-
- fielddata 会加载索引中(针对该特定字段的) 所有的文档,而不管查询是否命中。逻辑是这样:如果查询会访问文档 X、Y 和 Z,那很有可能会在下一个查询中访问其他文档。
-
- 如果空间不足,使用最久未使用(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.缓存预热。已经弃用。
总结
对比 | DocValues | FieldData |
---|---|---|
创建时机 | 索引是创建,与倒排索引创建时机一致 | 搜索时及时创建,懒加载 |
创建位置 | 磁盘。与倒排一样,保存在lucene文件中(序列化到磁盘)。 lucene文件操作依赖于操作系统的缓存来管理 | JVM Heap |
优点 | 不会占有Heap内存 | 不会占用额外的磁盘资源 |
缺点 | 减慢索引的速度,占用额外的磁盘资源 | 文档过多时,即时创建会花过多时间,占用过多Heap |
支持数据类型 | 满足非analyed字段 | analyed字段(分词)的字符串 |
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)