深入理解 doc valuesedit

在上节中一开始我们就说 doc values"快速、高效并且内存友好" 。 这个口号听不起来不错,不过话说回来 doc values 到底是如何工作的呢?

doc values 是在索引时与 倒排索引 同时生成的。也就是说 doc values倒排索引 一样,基于 Segement 生成并且是不可变的。同时 doc values倒排索引 一样序列化到磁盘,这样对性能和扩展性有很大帮助。

doc values 通过序列化把数据结构持久化到磁盘,我们可以充分利用操作系统的内存,而不是 JVMHeap 。 当 working set 远小于系统的可用内存,系统会自动将 doc values 驻留在内存中,使得其读写十分快速;不过,当其远大于可用内存时,系统会根据需要从磁盘读取 doc values,然后选择性放到分页缓存中。很显然,这样性能会比在内存中差很多,但是它的大小就不再局限于服务器的内存了。如果是使用 JVMHeap 来实现那么只能是因为 OutOfMemory 导致程序崩溃了。  

因为 doc values 不是由 JVM 来管理,所以 Elasticsearch 实例可以配置一个很小的 JVM Heap,这样给系统留出来更多的内存。同时更小的 Heap 可以让 JVM 更加快速和高效的回收。

之前,我们会建议分配机器内存的 50% 来给 JVM Heap。但是对于 doc values,这样可能不是最合适的方案了。 以 64GB 内存的机器为例,可能给 Heap 分配 4-16GB 的内存更合适,而不是 32GB。

有关更详细的讨论,查看 堆内存:大小和交换.

列式存储(column-store)的压缩edit

从广义来说,doc values 本质上是一个序列化的 列式存储 。 正如我们上一节所讨论的,列式存储 适用于聚合、排序、脚本等操作。

而且,这种存储方式也非常便于压缩,特别是数字类型。这样可以减少磁盘空间并且提高访问速度。现代 CPU 的处理速度要比磁盘快几个数量级(尽管即将到来的 NVMe 驱动器正在迅速缩小差距)。所以我们必须减少直接存磁盘读取数据的大小,尽管需要额外消耗 CPU 运算用来进行解压。

要了解它如何压缩数据的,来看一组数字类型的 doc values

Doc      Terms
-----------------------------------------------------------------
Doc_1 | 100
Doc_2 | 1000
Doc_3 | 1500
Doc_4 | 1200
Doc_5 | 300
Doc_6 | 1900
Doc_7 | 4200
-----------------------------------------------------------------

按列布局意味着我们有一个连续的数据块: [100,1000,1500,1200,300,1900,4200] 。因为我们已经知道他们都是数字(而不是像文档或行中看到的异构集合),所以我们可以使用统一的偏移来将他们紧紧排列。

而且,针对这样的数字有很多种压缩技巧。你会注意到这里每个数字都是 100 的倍数,doc values 会检测一个段里面的所有数值,并使用一个 最大公约数 ,方便做进一步的数据压缩。

如果我们保存 100 作为此段的除数,我们可以对每个数字都除以 100,然后得到: [1,10,15,12,3,19,42] 。现在这些数字变小了,只需要很少的位就可以存储下,也减少了磁盘存放的大小。

doc values 在压缩过程中使用如下技巧。它会按依次检测以下压缩模式:

  1. 如果所有的数值各不相同(或缺失),设置一个标记并记录这些值
  2. 如果这些值小于 256,将使用一个简单的编码表
  3. 如果这些值大于 256,检测是否存在一个最大公约数
  4. 如果没有存在最大公约数,从最小的数值开始,统一计算偏移量进行编码

你会发现这些压缩模式不是传统的通用的压缩方式,比如 DEFLATE 或者 LZ4。 因为列式存储的结构是严格且良好定义的,我们可以通过使用特殊的方案来达到比通用压缩算法(如 LZ4 )更高的压缩效果。

你也许会想 "好吧,貌似对数字很好,不知道字符串怎么样?" 通过借助顺序表(ordinal table),String 类型也是类似进行编码的。String 类型是去重之后存放到顺序表的,通过分配一个 ID,然后通过数字类型的 ID 构建 doc values。这样 String 类型和数值类型可以达到同样的压缩效果。

顺序表本身也有很多压缩技巧,比如固定长度、变长或是前缀字符编码等等。

禁用 doc valuesedit

doc values 默认对所有字段启用,除了 analyzed strings。也就是说所有的数字、地理坐标、日期、IP 和不分析(not_analyzed)的字符串类型都会默认开启。

analyzed strings 暂时还不能使用 doc values。文本经过分析流程生成很多 token,使得 doc values 不能高效运行。我们将在 聚合与分析 讨论如何使用分析字符类型来做聚合。

因为 doc values 默认启用,你可以选择对数据集里面的大多数字段进行聚合和排序操作。但是你怎么知道 你永远也不会对某些字段进行聚合、排序或是使用脚本操作呢?

尽管这并不常见,但是你可以通过禁用特定字段的 doc values 。这样不仅节省磁盘空间,也许会提升索引的速度。

要禁用 doc values ,在字段的映射(mapping)中设置 doc_values: false 即可。例如,这里我们创建了一个新的索引,字段 "session_id" 禁用了 doc values

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": false 
        }
      }
    }
  }
}

通过设置 doc_values: false ,这个字段将不能被用于聚合、排序以及脚本操作

反过来也是可以进行配置的:让一个字段可以被聚合,通过禁用倒排索引,使它不能被正常搜索,例如:

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "customer_token": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": true, 
          "index": "no" 
        }
      }
    }
  }
}

doc values 被启用以允许聚合

(倒排)索引被禁用了,这让该字段不能被查询/搜索

通过设置 doc_values: trueindex: no ,我们得到一个只能被用于聚合/排序/脚本的字段。无可否认,这是一个非常少见的情况,但有时很有用。