深入理解 Doc Values

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

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

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

[NOTE]

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

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

有关更详细的讨论,查看 heap-sizing.


列式存储的压缩

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

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

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

  1. Doc Terms
  2. -----------------------------------------------------------------
  3. Doc_1 | 100
  4. Doc_2 | 1000
  5. Doc_3 | 1500
  6. Doc_4 | 1200
  7. Doc_5 | 300
  8. Doc_6 | 1900
  9. Doc_7 | 4200
  10. -----------------------------------------------------------------

按列布局意味着我们有一个连续的数据块: [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 )更高的压缩效果。

[NOTE]

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

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


禁用 Doc Values

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

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

因为 Doc Values 默认启用,你可以选择对你数据集里面的大多数字段进行聚合和排序操作。但是如果你知道你永远也不会对某些字段进行聚合、排序或是使用脚本操作?尽管这并不常见,但是你可以通过禁用特定字段的 Doc Values 。这样不仅节省磁盘空间,也许会提升索引的速度。

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

  1. PUT my_index
  2. {
  3. "mappings": {
  4. "my_type": {
  5. "properties": {
  6. "session_id": {
  7. "type": "string",
  8. "index": "not_analyzed",
  9. "doc_values": false (1)
  10. }
  11. }
  12. }
  13. }
  14. }

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

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

  1. PUT my_index
  2. {
  3. "mappings": {
  4. "my_type": {
  5. "properties": {
  6. "customer_token": {
  7. "type": "string",
  8. "index": "not_analyzed",
  9. "doc_values": true, <1>
  10. "index": "no" <2>
  11. }
  12. }
  13. }
  14. }
  15. }

<1> Doc Values 被启用来允许聚合

<2> 索引被禁用了,这让该字段不能被查询/搜索

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