ES系列8、正排索Doc Values和Field Data

1.Doc Values

聚合使用一个叫Doc Values的数据结构。Doc Values使聚合更快、更高效且内存友好。
Doc Values的存在是由于倒排索引只对某些操做是高效的。倒排索引的优点在于查找包含某个项的文档,而反过来肯定哪些项在单个文档里并不高效。
 
结构相似以下:
Doc      Terms
-----------------------------------------------------------------
Doc_1 | brown, dog, fox, jumped, lazy, over, quick, the
Doc_2 | brown, dogs, foxes, in, lazy, leap, over, quick, summer
Doc_3 | dog, dogs, fox, jumped, over, quick, the

 


     Doc values在索引的时候生成,伴随倒排索引的建立。像倒排索引同样基于per-segment,且是不可变,被序列化存储到磁盘。经过序列化持久化数据结构到磁盘,能够以来操做系统的文件缓存来代替JVM heap。可是当工做空间须要的内存很大时,Doc Values会被置换出内存,这样会致使访问速度下降,可是若是放在JVM heap,将直接致使内存溢出错误。
     Doc Values默认对除了分词的全部字段起做用。由于分此字段产生太多tokens且Doc Values对其并非颇有效。
     因为Doc Values默认开启,若是你不会执行基于一个肯定的子段 聚合、排序或执行脚本(Script ),你能够选择关闭Doc Values,这能够为你节省磁盘空间,提升索引数据的速度。
PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": false
        }
      }
    }
  }
}

 

 
设置doc_values: false,这个字段将再也不支持据聚合、排序和脚本执行(Script);
同时也能够对倒排索引作相似的配置:
PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "customer_token": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": true,
          "index": "no"
        }
      }
    }
  }
}

 

这个能够支持聚合,但不支持查询,由于不会对这个字段生成倒排索引。
 

2.聚合与分析

分析对聚合有两方面的影响,

2.1.分析影响聚合中使用的 tokens

          例如字符串 "New York" 被分析/分析成 ["new", "york"] 。这些单独的 tokens ,都被用来填充聚合计数,因此咱们最终看到 new 的数量而不是 New York。
     能够经过加multifield来修正,以下:聚合时指定为未分词的raw字段。
PUT /agg_analysis
{
  "mappings": {
    "data": {
      "properties": {
        "state" : {
          "type": "string",
          "fields": {
            "raw" : {
              "type": "string",
              "index": "not_analyzed"
            }
          }
        }
      }
    }
  }
}

2.1.Doc values 不支持 analyzed

Doc values 不支持 analyzed 字符串字段,由于它们不能颇有效的表示多值字符串。 Doc values 最有效的是,当每一个文档都有一个或几个 tokens 时, 但不是无               数的,分词字符串(想象一个 PDF ,可能有几兆字节并有数以千计的独特 tokens)。node

          出于这个缘由,doc values 不生成分词的字符串,然而,这些字段仍然可使用聚合,那怎么可能呢?
          答案是一种被称为 fielddata 的数据结构。与 doc values 不一样, fielddata 构建和管理 100% 在内存中,常驻于 JVM 内存堆。这意味着它本质上是不可扩展的,有不少边缘状况下要提防。 
  本章的其他部分是解决在分词字符串上下文中 fielddata 的挑战。
          从历史上看,fielddata 是 全部字段的默认设置。可是 Elasticsearch 已迁移到 doc values 以减小 OOM 的概率。分词字符串是仍然使用 fielddata 的最后一块阵地。 最终目标是创建一个序列化的数据结构相似于 doc values ,能够处理高维度的分词字符串,逐步淘汰 fielddata。
          避免分词字段的另一个缘由就是:高基数字段在加载到 fielddata 时会消耗大量内存。 分词的过程会常常(尽管不老是这样)生成大量的 token,这些 token 大多都是惟一的。 这会增长字段的总体基数而且带来更大的内存压力。
          有些类型的分词对于内存来讲 极度 不友好,想一想 n-gram 的分析过程, New York 会被 n-gram 分析成如下 token:
              ne、ew、w 、 y、yo、or、rk
          能够想象 n-gram 的过程是如何生成大量惟一 token 的,特别是在对成段文本分词的时候。当这些数据加载到内存中,会垂手可得的将咱们堆空间消耗殆尽。
          在聚合字符串字段以前,请评估状况:
               a.这是一个 not_analyzed 字段吗?若是是,能够经过 doc values 节省内存 。
               b.不然,这是一个 analyzed 字段,它将使用 fielddata 并加载到内存中。这个字段由于 n-grams 有一个很是大的基数?若是是,这对于内存来讲极度不友好。

 2.2.text类型默认禁用fielddate,排序、聚合须要手动开启

POST book1/_mapping/english/?pretty
{
    "english":{
        "properties":{
            "addr":{
                "type":"text",
                "fielddata":true
             }
         }
    }
}

 

Fielddata可能会消耗大量的堆空间,尤为是在加载高基数text字段时。一旦fielddata已加载到堆中,它将在该段的生命周期内保留。此外,加载fielddata是一个昂贵的过程,可能会致使用户遇到延迟命中。这就是默认状况下禁用fielddata的缘由。缓存

若是您尝试对text 字段上的脚本进行排序,聚合或访问,您将看到如下异常:session

默认状况下,在文本字段上禁用Fielddata。设置fielddata=true为[ your_field_name]以经过同相反向索引在内存中加载fielddata。请注意,这可能会占用大量内存。数据结构

3.Fielddata

     一旦分词字符串被加载到 fielddata ,他们会一直在那里,直到被驱逐(或者节点崩溃)。因为这个缘由,留意内存的使用状况,了解它是如何以及什么时候加载的,怎样限制对集群的影响是很重要的。
      Fielddata 是 延迟 加载。若是你历来没有聚合一个分析字符串,就不会加载 fielddata 到内存中。此外,fielddata 是基于字段加载的, 这意味着只有很活跃地使用字段才会增长 fielddata 的负担。
     然而,这里有一个使人惊讶的地方。假设你的查询是高度选择性和只返回命中的 100 个结果。大多数人认为 fielddata 只加载 100 个文档。
实际状况是,fielddata 会加载索引中(针对该特定字段的) 全部的文档,而无论查询的特异性。逻辑是这样:若是查询会访问文档 X、Y 和 Z,那颇有可能会在下一个查询中访问其余文档。
     与 doc values 不一样,fielddata 结构不会在索引时建立。相反,它是在查询运行时,动态填充。这多是一个比较复杂的操做,可能须要一些时间。 将全部的信息一次加载,再将其维持在内存中的方式要比反复只加载一个 fielddata 的部分代价要低。
JVM 堆 是有限资源的,应该被合理利用。 限制 fielddata 对堆使用的影响有多套机制,这些限制方式很是重要,由于堆栈的乱用会致使节点不稳定(感谢缓慢的垃圾回收机制),甚至致使节点宕机(一般伴随 OutOfMemory 异常)。
 
     在设置 Elasticsearch 堆大小时须要经过 $ES_HEAP_SIZE 环境变量应用两个规则:

3.1.不要超过可用 RAM 的 50%

          Lucene 能很好利用文件系统的缓存,它是经过系统内核管理的。若是没有足够的文件系统缓存空间,性能会收到影响。 此外,专用于堆的内存越多意味着其余全部使用 doc values 的字段内存越少。

3.2.不要超过 32 GB

若是堆大小小于 32 GB,JVM 能够利用指针压缩,这能够大大下降内存的使用:每一个指针 4 字节而不是 8 字节。
 

4.Fielddata的大小

     indices.fielddata.cache.size 控制为 fielddata 分配的堆空间大小。 当你发起一个查询,分析字符串的聚合将会被加载到 fielddata,若是这些字符串以前没有被加载过。若是结果中 fielddata 大小超过了指定大小,其余的值将会被回收从而得到空间。 
     默认状况下,这个设置是禁用的,Elasticsearch 永远都不会从 fielddata 中回收数据。
     这个默认设置是刻意选择的:fielddata 不是临时缓存。它是驻留内存里的数据结构,必须能够快速执行访问,并且构建它的代价十分高昂。若是每一个请求都重载数据,性能会十分糟糕。
     一个有界的大小会强制数据结构回收数据。
     设想咱们正在对日志进行索引,天天使用一个新的索引。一般咱们只对过去一两天的数据感兴趣,尽管咱们会保留老的索引,但咱们不多须要查询它们。不过若是采用默认设置,旧索引的 fielddata 永远不会从缓存中回收! fieldata 会保持增加直到 fielddata 发生断熔,这样咱们就没法载入更多的 fielddata。
     这个时候,咱们被困在了死胡同。但咱们仍然能够访问旧索引中的 fielddata,也没法加载任何新的值。相反,咱们应该回收旧的数据,并为新值得到更多空间。
为了防止发生这样的事情,能够经过在 config/elasticsearch.yml 文件中增长配置为 fielddata 设置一个上限:
      indices.fielddata.cache.size:  20% : 有了这个设置,最久未使用(LRU)的 fielddata 会被回收为新数据腾出空间。
      

4.1.监控fileddata

     Fielddata 的使用能够被监控:
          1).按索引使用 indices-stats API :GET /_stats/fielddata?fields=*
          2).按节点使用 nodes-stats API :  GET /_nodes/stats/indices/fielddata?fields=*
          3).按索引节点:GET /_nodes/stats/indices/fielddata?level=indices&fields=*
 

4.2.断路器(Circuit Breakers)

     fielddata 大小是在数据加载 以后 检查的。 若是一个查询试图加载比可用内存更多的信息到 fielddata 中会发生什么?答案很丑陋:咱们会碰到 OutOfMemoryException 。
     Elasticsearch 包括一个 fielddata 断熔器 ,这个设计就是为了处理上述状况。 断熔器经过内部检查(字段的类型、基数、大小等等)来估算一个查询须要的内存。它而后检查要求加载的 fielddata 是否会致使 fielddata 的总量超过堆的配置比例。
     若是估算查询的大小超出限制,就会 触发 断路器,查询会被停止并返回异常。这都发生在数据加载 以前 ,也就意味着不会引发 OutOfMemoryException 。
    
     Elasticsearch 有一系列的断路器,它们都能保证内存不会超出限制:
          1).indices.breaker.fielddata.limit
               fielddata 断路器默认设置堆的 60% 做为 fielddata 大小的上限。
          2).indices.breaker.request.limit
               request 断路器估算须要完成其余请求部分的结构大小,例如建立一个聚合桶,默认限制是堆内存的 40%。
          3).indices.breaker.total.limit
               total 揉合 request 和 fielddata 断路器保证二者组合起来不会使用超过堆内存的 70%。
 
     断路器的限制能够在文件 config/elasticsearch.yml 中指定,能够动态更新一个正在运行的集群: 
PUT /_cluster/settings
{
     "persistent" : {
     "indices.breaker.fielddata.limit" : "40%"
     }
 }

 

     关于给 fielddata 的大小加一个限制,从而确保旧的无用 fielddata 被回收的方法。 indices.fielddata.cache.size 和 indices.breaker.fielddata.limit 之间的关系很是重要。 若是断路器的限制低于缓存大小,没有数据会被回收。为了能正常工做,断路器的限制 必须 要比缓存大小要高。
 

4.3.fielddata过滤

PUT /music/_mapping/song
{
  "properties": {
    "tag": {
      "type": "string",
      "fielddata": {
        "filter": {
          "frequency": {
            "min":              0.01,
            "min_segment_size": 500 
          }
        }
      }
    }
  }
}

 

     1).只加载那些至少在本段文档中出现 1% 的项。     
     2).忽略任何文档个数小于 500 的段。
     有了这个映射,只有那些至少在 本段 文档中出现超过 1% 的项才会被加载到内存中。咱们也能够指定一个 最大 词频,它能够被用来排除 经常使用 项,好比 停用词 。
     这种状况下,词频是按照段来计算的。这是实现的一个限制:fielddata 是按段来加载的,因此可见的词频只是该段内的频率。可是,这个限制也有些有趣的特性:它可让受欢迎的新项迅速提高到顶部。
     min_segment_size 参数要求 Elasticsearch 忽略某个大小如下的段。 若是一个段内只有少许文档,它的词频会很是粗略没有任何意义。 小的分段会很快被合并到更大的分段中,某一刻超过这个限制,将会被归入计算。
 

5.预加载fielddata

     Elasticsearch 加载内存 fielddata 的默认行为是 延迟 加载 。 当 Elasticsearch 第一次查询某个字段时,它将会完整加载这个字段全部 Segment 中的倒排索引到内存中,以便于之后的查询可以获取更好的性能。
     对于小索引段来讲,这个过程的须要的时间能够忽略。但若是咱们有一些 5 GB 的索引段,并但愿加载 10 GB 的 fielddata 到内存中,这个过程可能会要数十秒。 已经习惯亚秒响应的用户很难会接受停顿数秒卡着没反应的网站。
     有三种方式能够解决这个延时高峰:
          1).预加载 fielddata
          2).预加载全局序号
          3).缓存预热
     全部的变化都基于同一律念:预加载 fielddata ,这样在用户进行搜索时就不会碰到延迟高峰。

5.1.预加载

     第一个工具称为 预加载 (与默认的 延迟加载相对)。随着新分段的建立(经过刷新、写入或合并等方式), 启动字段预加载可使那些对搜索不可见的分段里的 fielddata 提早 加载。
     这就意味着首次命中分段的查询不须要促发 fielddata 的加载,由于 fielddata 已经被载入到内存。避免了用户遇到搜索卡顿的情形。
预加载是按字段启用的,因此咱们能够控制具体哪一个字段能够预先加载:
PUT /music/_mapping/_song
{
  "tags": {
    "type": "string",
    "fielddata": {
      "loading" : "eager"
    }
  }
}

 

     Fielddata 的载入可使用 update-mapping API 对已有字段设置 lazy 或 eager 两种模式。
    

5.2全局序号

     有种能够用来下降字符串 fielddata 内存使用的技术叫作 序号 。
     设想咱们有十亿文档,每一个文档都有本身的 status 状态字段,状态总共有三种: status_pending 、 status_published 、 status_deleted 。若是咱们为每一个文档都保留其状态的完整字符串形式,那么每一个文档就须要使用 14 到 16 字节,或总共 15 GB。
     取而代之的是咱们能够指定三个不一样的字符串,对其排序、编号:0,1,2。
Ordinal | Term
-------------------
0       | status_deleted
1       | status_pending
2       | status_published
     序号字符串在序号列表中只存储一次,每一个文档只要使用数值编号的序号来替代它原始的值。
Doc     | Ordinal
-------------------------
0       | 1  # pending
1       | 1  # pending
2       | 2  # published
3       | 0  # deleted

     这样能够将内存使用从 15 GB 降到 1 GB 如下!app

     但这里有个问题,记得 fielddata 是按分 段 来缓存的。若是一个分段只包含两个状态( status_deleted 和 status_published )。那么结果中的序号(0 和 1)就会与包含全部三个状态的分段不同。
     若是咱们尝试对 status 字段运行 terms 聚合,咱们须要对实际字符串的值进行聚合,也就是说咱们须要识别全部分段中相同的值。一个简单粗暴的方式就是对每一个分段执行聚合操做,返回每一个分段的字符串值,再将它们概括得出完整的结果。 尽管这样作可行,但会很慢并且大量消耗 CPU。
取而代之的是使用一个被称为 全局序号 的结构。 全局序号是一个构建在 fielddata 之上的数据结构,它只占用少许内存。惟一值是 跨全部分段 识别的,而后将它们存入一个序号列表中,正如咱们描述过的那样。
     如今, terms 聚合能够对全局序号进行聚合操做,将序号转换成真实字符串值的过程只会在聚合结束时发生一次。这会将聚合(和排序)的性能提升三到四倍。
 
构建全局序号(Building global ordinals)
     固然,天下没有免费的晚餐。 全局序号分布在索引的全部段中,因此若是新增或删除一个分段时,须要对全局序号进行重建。 重建须要读取每一个分段的每一个惟一项,基数越高(即存在更多的惟一项)这个过程会越长。
     全局序号是构建在内存 fielddata 和 doc values 之上的。实际上,它们正是 doc values 性能表现不错的一个主要缘由。
     和 fielddata 加载同样,全局序号默认也是延迟构建的。首个须要访问索引内 fielddata 的请求会促发全局序号的构建。因为字段的基数不一样,这会致使给用户带来显著延迟这一糟糕结果。一旦全局序号发生重建,仍会使用旧的全局序号,直到索引中的分段产生变化:在刷新、写入或合并以后。
 
预构建全局序号(Eager global ordinals)    
     单个字符串字段 能够经过配置预先构建全局序号:
PUT /music/_mapping/_song
{
  "song_title": {
    "type": "string",
    "fielddata": {
      "loading" : "eager_global_ordinals"
    }
  }
}

正如 fielddata 的预加载同样,预构建全局序号发生在新分段对于搜索可见以前。electron

 
     序号的构建只被应用于字符串。数值信息(integers(整数)、geopoints(地理经纬度)、dates(日期)等等)不须要使用序号映射,由于这些值本身本质上就是序号映射。 所以,咱们只能为字符串字段预构建其全局序号
     也能够对 Doc values 进行全局序号预构建:
PUT /music/_mapping/_song
{
  "song_title": {
    "type":       "string",
    "doc_values": true,
    "fielddata": {
      "loading" : "eager_global_ordinals"
    }
  }
}

这种状况下,fielddata 没有载入到内存中,而是 doc values 被载入到文件系统缓存中。elasticsearch

 
     与 fielddata 预加载不同,预建全局序号会对数据的 实时性 产生影响,构建一个高基数的全局序号会使一个刷新延时数秒。 选择在因而每次刷新时付出代价,仍是在刷新后的第一次查询时。若是常常索引而查询较少,那么在查询时付出代价要比每次刷新时要好。若是写大于读,那么在选择在查询时重建全局序号将会是一个更好的选择。
 

5.3.索引预热器(index warmers)

     最后咱们谈谈 索引预热器 。预热器早于 fielddata 预加载和全局序号预加载以前出现,它们仍然尤为存在的理由。一个索引预热器容许咱们指定一个查询和聚合需要在新分片对于搜索可见以前执行。 这个想法是经过预先填充或 预热缓存 让用户永远没法遇到延迟的波峰。
     原来,预热器最重要的用法是确保 fielddata 被预先加载,由于这一般是最耗时的一步。如今能够经过前面讨论的那些技术来更好的控制它,可是预热器仍是能够用来预建过滤器缓存,固然咱们也仍是能选择用它来预加载 fielddata。
     让咱们注册一个预热器而后解释发生了什么:
PUT /music/_warmer/warmer_1
{
  "query" : {
    "bool" : {
      "filter" : {
        "bool": {
          "should": [
            { "term": { "tag": "rock"        }},
            { "term": { "tag": "hiphop"      }},
            { "term": { "tag": "electronics" }}
          ]
        }
      }
    }
  },
  "aggs" : {
    "price" : {
      "histogram" : {
        "field" : "price",
        "interval" : 10
      }
    }
  }
}

     1).预热器被关联到索引( music )上,使用接入口 _warmer 以及 ID ( warmer_1 )。工具

     2).为三种最受欢迎的曲风预建过滤器缓存。
     3).字段 price 的 fielddata 和全局序号会被预加载。  
 
     预热器是根据具体索引注册的, 每一个预热器都有惟一的 ID ,由于每一个索引可能有多个预热器。
而后咱们能够指定查询,任何查询。它能够包括查询、过滤器、聚合、排序值、脚本,任何有效的查询表达式都绝不夸张。 这里的目的是想注册那些能够表明用户产生流量压力的查询,从而将合适的内容载入缓存。
当新建一个分段时,Elasticsearch 将会执行注册在预热器中的查询。执行这些查询会强制加载缓存,只有在全部预热器执行完,这个分段才会对搜索可见。
相关文章
相关标签/搜索