优化磁盘使用量与建立索引时的映射参数和索引元数据字段密切相
关,在介绍具体的优化措施之前,我们先介绍这两方面的基础知识。
元数据字段
- _source:原始的JSON文档数据。
- _all:索引所有其他字段值的一种通用字段,这个字段中包含了所有其他字段的值。
允许在搜索的时候不指定特定的字段名,意味着“从全部字段中搜索”,例如:
http://localhost:9200/website/_search?q=keyword
_all字段是一个全文字段,有自己的分析器。从ES 6.0开始该字段被禁用。之前的版本默认启用,但字段的store属性为false,因此它不能被查询后取回显示。
索引mapping参数
index:控制字段值是否被索引。它可以设置为true或false,默认为true。未被索引的字段不会被查询到,但是可以聚合。除非禁用doc_values。
doc values:默认情况下,大多数字段都被索引,这使得它们可以搜索。倒排索引根据term找到文档列表,然后获取文档原始内容。但是排序和聚合,以及从脚本中访问某个字段值,需要不同的数据访问模式,它们不仅需要根据term找到文档,还要获取文档中字段的值。这些值需要单独存储。doc_values 就是用来存储这些字段值的。
它是一种存储在磁盘上的列式存储,在文档索引时构建,这使得上述数据访问模式成为可能。它们以面向列的方式存储与_source相同的值,这使得排序和聚合效率更高。几乎所有字段类型都支持doc_values,但被分析(analyzed)的字符串字段除外(即text类型字符串)。doc_values默认启用。
store:默认情况下,字段值会被索引使它们能搜索,但它们不会被存储(stored)。意味着可以通过这个字段查询,但不能取回它的原始值。但这没有关系。因为字段值已经是_source字段的一部分,它是被默认存储的。如果只想取回一个字段或少部分字段的值,而不是整个
_source,则可以通过source filtering达到目的。
在某些情况下,存储字段是有意义的。例如,如果有一个包含标题、日期和非常多的内容字段的文档,则可能希望只检索标题和日期,而不需要从大型_source字段中提取这些字段。
还有一种情况可能用到存储字段,就是不在_source中出现的字段(例如,copy_to字段)。
doc_values和存储字段("stored":ture)都属于正排内容,两者的设计初衷不同。
stored fields被设计为优化存储,doc_values被设计为快速访问字段值。搜索可能会访问很多doc values中的字段,所以必须能够快速访问,我们将doc_values用于聚合、排序,以及脚本中。现在,ES中的许多特性都会自动使用doc_values。
另一方面,stored fields仅用于返回前几个最匹配文档的字段值,默认情况下 ES 只将其用于这种情况,解压存储字段,将其发送给客户端。为少量文档获取存储字段还好。它不能在查询的时候使用,否则会让查询变得非常慢。脚本中可以访问存储字段,但最好不要那么做。
禁用对你来说不需要的特性
默认情况下,ES为大多数的字段建立索引,并添加到doc_values,以便使之可以被搜索和聚合。但是有时候不需要通过某些字段过滤,例如,有一个名为 foo 的数值类型字段,需要运行直方图,但不需要在这个字段上过滤,那么可以不索引这个字段:
Text(动态的string类型应该也可以配置,keyword类型默认关闭norms)类型的字段会在索引中存储归一因子(normalizationactors),以便对文档进行评分,如果只需要在文本字段上进行匹配,而不关心生成的得分,则可以配置 ES 不将 norms 写入索引:
text类型的字段默认情况下也在索引中存储频率和位置。频率用于计算得分,位置用于执行短语(phrase)查询。如果不需要运行短语查询,则可以告诉ES不索引位置,及index_options仅配置频率:
在text类型的字段上,index_options的默认值为positions。
index_options参数用于控制添加到倒排索引中的信息。
freqs 文档编号和词频被索引,词频用于为搜索评分,重复出现的词条比只出现一次的词条评分更高。
positions 文档编号、词频和位置被索引。positions被用于邻近查询(proximity queries)和短语查询(phrase queries)。
此外,如果也不关心评分,则可以将ES配置为只为每个term索引匹配的文档。仍然可以在这个字段上搜索,但是短语查询会出现错误,评分将假定在每个文档中只出现一次词汇。
禁用doc values
所有支持doc value的字段都默认启用了doc value。如果确定不需要对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用doc value以节省磁盘空间:
谨慎使用默认的dynamic字符串映射
默认的动态字符串映射会把字符串类型的字段同时索引为 text 和keyword。如果只需要其中之一,则显然是一种浪费。通常,id字段只需作为 keyword类型进行索引,而content字段只需作为text类型进行索引。
要禁用默认的动态字符串映射,则可以显式地指定字段类型,或者在动态模板中指定将字符串映射为text或keyword。下例将字符串字段映射为keyword:
管理shard大小
较大的分片可以更有效地存储数据。为了增加分片大小,可以在创建索引的时候设置较少的主分片数量(规则参考index的shard规划原则章节),或者使用shrink API来修改现有索引的主分片数量。
但是较大的分片也有缺点,例如,较长的索引恢复时间。
科学使用_source
_source 字段存储文档的原始内容。对于部分不需要存储的字段,可以通过 includes excludes过滤,减少_source字段的存储量。如果不需要访问它,则可以将其禁用。但是,需要访问_source的API将无法使用,至少包括下列情况:
- update、update_by_query、reindex;
- 高亮搜索;
- 重建索引(包括更新mapping、分词器,或者集群跨大版本升级可能会用到)
- 调试聚合查询功能,需要对比原始数据
使用best_compression
_source和设置为"store": true的字段占用磁盘空间都比较多。默认情况下,它们都是被压缩存储的。默认的压缩算法为LZ4,可以通过使用best_compression来执行压缩比更高的算法:DEFLATE。但这会占用更多的CPU资源。
PUT index
{
"settings": {
"index": {
"codec": "best_compression"
}
}
}
两种压缩特点比较:
| 压缩比 | 压缩速度 | 解压速度 | 压缩内存占用 | 解压内存占用 |
DEFLATE | 高 | 慢 | 慢 | 少 | 少 |
LZ4 | 低 | 快 | 快 | 多 | 多 |
两种压缩的压缩比测试:
Test | String fields | _all | index size /w LZ4 | index size /w DEFLATE | expansion ratio /w LZ4 | expansion ratio /w DEFLATE | Impact of DEFLATE |
Structured data file. Original file size: 67644119 |
|
|
|
|
|
|
|
1 | analyzed and not_analyzed | enabled | 63047579 | 53131592 | 0.932 | 0.785 | -0.157 |
2 | analyzed and not_analyzed | disabled | 48271433 | 38327106 | 0.713 | 0.566 | -0.206 |
3 | not_analyzed | disabled | 38920800 | 29014796 | 0.575 | 0.428 | -0.254 |
3b | not_analyzed, except for 'message' field which is retained and analyzed | disabled | 65382872 | 49532858 | 0.966 | 0.732 | -0.242 |
4 | not_analyzed, except for 'agent' field which is analyzed | disabled | 43083702 | 32063602 | 0.636 | 0.474 | -0.255 |
Semi-structured data file. |
|
|
|
|
|
|
|
1 | analyzed and not_analyzed | enabled | 100478376 | 82132782 | 1.339 | 1.094 | -0.182 |
2 | analyzed and not_analyzed | disabled | 75238480 | 56911638 | 1.002 | 0.758 | -0.243 |
3 | not_analyzed | disabled | 71866672 | 53553561 | 0.957 | 0.713 | -0.254 |
3b | not_analyzed, except for 'message' field which is retained and analyzed | disabled | 104638750 | 83824398 | 1.394 | 1.117 | -0.198 |
4 | not_analyzed, except for 'agent' field which is analyzed | disabled | 72925624 | 54603882 | 0.971 | 0.727 | -0.251 |
根据结果比较,使用best_compression之后压缩比会提升15%-25%
为只读索引执行force-merge
为不再更新的只读索引执行force merge,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并;另外执行force_merge后也会释放无法被GC的segmentCache(另一种是close掉索引)。所以将只读索引强制合并为一个Lucene分段不仅可以优化搜索过程,减少内存占用,对索引恢复速度也有好处。
基于日期使用rollover创建的索引的旧数据一般都不会再更新。此前的章节中说过,应该避免持续地写一个固定的索引,直到它巨大无比,而应该按一定的策略,例如,每天生成一个新的索引,然后用别名关联,或者使用索引通配符。这样,可以每天凌晨对昨天的索引执行force-merge、Shrink等操作。
Shrink Index
Shrink API允许减少索引的分片数量,结合上面的Force Merge API,可以显著减少索引的分片和Lucene分段数量。
合理选择数值类型长度
为数值类型选择的字段类型也可能会对磁盘使用空间产生较大影响,整型可以选择byte、short、integer或long,浮点型可以选择scaled_float、float、double、half_float,每个数据类型的字节长度是不同的,为业务选择够用的最小数据类型,可以节省磁盘空间。
使用索引排序来排列类似的文档
当 ES 存储_source 时,它同时压缩多个文档以提高整体压缩比。例如,文档共享相同的字段名,或者它们共享一些字段值,特别是在具有低基数或 zipfian 分布(参考https://en.wikipedia.org/wiki/Zipf%27s_law)的字段上。
默认情况下,文档按照添加到索引中的顺序压缩在一起。如果启用了索引排序,那么它们将按排序顺序压缩。对具有相似结构、字段和值的文档进行排序可以提高压缩比。
关于索引排序的详细内容请参考官方手册:
https://www.elastic.co/guide/en/elasticsearch/reference/master/index-modules-index-sorting.html。
测试结果总结
笔者没有自己测试,找了一些公开的测试结果,效果总结如下:
-
- 禁用_source,空间占用量下降30%左右;
- 禁用doc values,空间占用量下降10%左右;
- 压缩算法将LZ4改为Deflate,空间占用量可以下降15%~25%。
本文的测试结果可作一定参考,但是实际业务最好使用自己的样本数据进行压力测试以获取准确的结果。
终极版优化方案详见:Elasticsearch配置优化方案最终完整版