前言:ES作为nosql 的数据存储,为什么它在承载PB级别的数据的同时,又可以对外提高近实时的高效搜索,它又是通过什么算法完成对文档的相关性分析;又是怎么保证聚合的高效性;
1 ES 分布式文档存储:
1.1 文档存储:
所谓分布式文档存储,就是我们在想ES存入数据时,ES在进行一次序列化为JSON字符串后,可以按照一定的路由规则将文档数据存储到不同的服务器中。
ES中的文档存储与分片中,所以只要使得分片分布于不同的服务器中就可以实现分布式存储的功能;ES在我们创建索引时如果不指定分片和副本的数量,ES会默认分配5个分片,一个副本;对于ES来说每个分片是都是一个最小的工作单元,并保存一个索引的一部分数据;分片的主要作用就是对外提供文档的存储,更新,删除和查询;副本的主要目的就是做数据备份同时对外提高查询功能,并在分片不可用时,从副本晋升为分片承担起分片的职责;
创建索引的同时设置索引分片和副本:
PUT /my_index0
{
"settings": {
"number_of_shards": 3,// 主分片数量
"number_of_replicas": 1// 每个主分片有几个副本
}
}
1.2 文档的路由规则:
当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
在建立索引时定义好主分片的数量,然后在索引文档时通过对文档id 的hash 和主分片取模得到,文档的分片位置;
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
你可能觉得由于 Elasticsearch 主分片数量是固定的会使索引难以进行扩容,实际上ES可以通过修改副本的数量来成倍提升性能,并且所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。
ES 天然支持分布式,它的分片和副本特点,使得数据可以均衡的分布在不同节点的分片上;
2 ES搜索的高效性:
2.1 倒排索引的构建:
搜索大批量的数据,如果进行数据的逐条比较显然不能满足对于我们时间上的要求,ES中通过对文档中的字段,维护倒排索引的方式来加速文档的搜素;
对于某个倒排索引来说,到索引或者更新文档时,都会将改字段的不同值进行罗列平铺,这样就当查询这个字段时,可以清楚的知道包含相应查找值的文档是哪些;
例如,假设我们有两个文档,每个文档的 content 字段包含如下内容:
文档1: The quick brown fox jumped over the lazy dog
文档2: Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
显然如果索引中的字段不存在倒排索引,意味着我们无法通过查询改字段完成检索;
2.2 倒排索引的维护:
2.2.1 倒排索引的不变性和分段:
当我们把数据存入到ES中后,后续的检索完全依赖于倒排索引的构建,所以就很有必要保证倒排索引的准确和完整性;
为了字段可被搜索,改字段需要维护一个倒排索引,早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到,这样每次进行文档的索引,更新和删除,都要去维护这个索引,而且随着数据的增大倒排索引的维护难度也随着增加。
既然只有一个的倒排索引维护难度很大,那么是否可以在对已经生成的倒排索引不变的情况下,进行扩展;
不变性,倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。 不变性有重要的价值:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。 - 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
那么对于文档索引,更新和删除,又改怎么去维护这个倒排索引;既然以前的索引不能被修改,只能去增加新的倒排索引,对文档的每次修改都进行记录,并且将原有的文档进行删除记录;这样每一个倒排索引都会被轮流查询到—从最早的开始—查询完后再对结果进行合并,就可以得到想要的结果。
对于新增加的倒排索引,我们必须按照一定条件进行分隔,这样保证每个倒排索引的大小可控,每个被分隔开的倒排索引就成为每个段;’
过程:
- 新文档首先被收集到内存索引缓存,然后通过不是的提交将内存索引缓存追加或者新建端来维护倒排索引;
- 一定的段又会被一个提交点所管理,改提交点列出了所有已知段的文件;
- 提交点被写入磁盘,完成持久化;
2.2.2 文档删除和更新时倒排索引的操作:
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。
当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
2.2.3 倒排索引的数据安全性:
倒排索引在提交点中的段中时就可以被搜索到,那么当ES宕机,提交点中每个段的数据怎么保证不丢失;
在保证ES 实时搜索时,放弃 了fsync 把数据从文件系统缓存刷(flush)到硬盘,这就意味着ES在宕机或者程序退出后,提交点段中没有被写入到磁盘的倒排索引会丢失,从而造成文档无法被搜索到,显然这种情况是不被允许的,我们需要确保变化的数据都被写入到磁盘中;
所以ES需要将每个索引的提交点进行持久化的记录,Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片,从而完成数据恢复。
第一次ES将缓存中的倒排索引提交到每个段中,然后在将提交点中的段刷新到磁盘中,那么对于还在缓存中,没有被及时提交到段中的倒排索引数据,也需要进行保存以确保数据不会丢失;Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。
translog 过程:
(1) 一个文档被索引,被放入到缓存的倒排索引中,同时也被 追加到了 translog 日志文件中:
(2)在分片进行每秒刷新缓存到提交点中的段时,缓存中的倒排索引会被清空,但是translog日志文件不会被清空,更多的操作可以被放入到translog日志文件中:
- 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操作。
- 这个段被打开,使其可被搜索。
- 内存缓冲区被清空。
(3)当索引被刷新(flush)时,一个新的 translog 被创建,并且一个全量提交被执:
- 所有在内存缓冲区的文档都被写入一个新的段。
- 缓冲区被清空。
- 一个提交点被写入硬盘。
- 文件系统缓存通过 fsync 被刷新(flush)。
- 老的 translog 被删除(因为数据已经被放入段中,所以不在需要从translog 恢复数据)。
translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
(4)flush API
这个执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush 。 分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。请查看 translog 文档 来设置,它可以用来 控制这些阈值:flush API 可以被用来执行一个手工的刷新(flush):
-- 刷新(flush) blogs 索引。
POST /blogs/_flush
-- 刷新(flush)所有的索引并且并且等待所有刷新在返回前完成。
POST /_flush?wait_for_ongoing
你很少需要自己手动执行 flush 操作;通常情况下,自动刷新就足够了。
这就是说,在重启节点或关闭索引之前执行 flush 有益于你的索引。当 Elasticsearch 尝试恢复或重新打开一个索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快。
(5)Translog 有多安全?:
translog 的目的是保证操作不会丢失。这引出了这个问题: Translog 有多安全?
在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被 fsync 到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。
在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。
但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 fsync 。
这个行为可以通过设置 durability 参数为 async 来启用:
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
这个选项可以针对索引单独设置,并且可以动态进行修改。如果你决定使用异步 translog 的话,你需要 保证 在发生崩溃时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。
如果你不确定这个行为的后果,最好是使用默认的参数( “index.translog.durability”: “request” )来避免数据丢失。
ES 通过对提交点分段倒排索引和translog 日志文件的维护来保证倒排索引数据的安全性;
(6)倒排索引段合并:
ES随着每次refresh时都会产生新的段,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中:
启动段合并不需要你做任何事, 进行索引和搜索时会自动进行,一旦段合并完成旧的段会被删除:
1、 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
2、 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。
(7)段个数的设置:
optimize API大可看做是 强制合并 API。它会将一个分片强制合并到 max_num_segments 参数指定大小的段数目。 这样做的意图是减少段的数量(通常减少到一个),来提升搜索性能。
注意 :optimize API 不应该 被用在一个活跃的索引————一个正积极更新的索引。后台合并流程已经可以很好地完成工作。 optimizing 会阻碍这个进程。不要干扰它!
ES 分布式查询的阶段:
在特定情况下,使用 optimize API 颇有益处。例如在日志这种用例下,每天、每周、每月的日志被存储在一个索引中。 老的索引实质上是只读的;它们也并不太可能会发生变化。
在这种情况下,使用optimize优化老的索引,将每一个分片合并为一个单独的段就很有用了;这样既可以节省资源,也可以使搜索更加快速:
合并索引中的每个分片为一个单独的段
POST /logstash-2014-10/_optimize?max_num_segments=1
请注意,使用 optimize API 触发段合并的操作不会受到任何资源上的限制。这可能会消耗掉你节点上全部的I/O资源, 使其没有余裕来处理搜索请求,从而有可能使集群失去响应。 如果你想要对索引执行 optimize,你需要先使用分片分配(查看 迁移旧索引)把索引移到一个安全的节点,再执行。
2.3 ES怎么做到近实时搜索:
为了保证ES数据不丢失,提交点每个段中的倒排索引需要被存入到磁盘中进行持久化,新的段到磁盘需要一个 fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。然而持久化到磁盘这种IO通常是耗时的, fsync 操作代价很大, 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。如果我们不及时的讲缓存索引写入磁盘,文档数据就不会被搜索到,如果实时的去写入索引,又会造成很大的性能问题,这似乎是个矛盾点;
我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync 要从整个过程中被移,我么需要将缓存索引在被高效放入到某个地方后,就能被搜索到,同时有需要兼顾到缓存索引写入磁盘的工作:
首先解决实时搜索问题:
- 新文档首先被收集到内存索引缓存,然后每个分片会定时的,将索引缓存追加到已有的段,或者开辟新的段盛放索引缓存,此时新的段先被写入到文件系统缓存,这一步代价较低,稍后再被刷新到磁盘中这一步代价较高,只要文件已经在文件系统缓存中了就可以像其它文件一样被打开和读取了;
在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh,每个分片默认1s 执行一次,将缓存索引,追加到已有的段,或者开辟新的段,进入段中,文档就可以被搜索到,这就是为什么我们说 Elasticsearch 是 近 实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
这些行为可能会对新用户造成困惑: 他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh API 执行一次手动刷新:
POST /_refresh
POST /blogs/_refresh
尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。 相反,你的应用需要意识到 Elasticsearch 的近实时的性质,并接受它的不足。
并不是所有的情况都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索, 可以通过设置 refresh_interval , 降低每个索引的刷新频率:
设置每30秒刷新 my_logs 索引。
PUT /my_logs
{
"settings": {
"refresh_interval": "30s"
}
}
refresh_interval 可以在既存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:
-- 关闭自动刷新。
PUT /my_logs/_settings
{ "refresh_interval": -1 }
-- 开启每秒自动刷新。
PUT /my_logs/_settings
{ "refresh_interval": "1s" }
refresh_interval 需要一个 持续时间 值, 例如 1s (1 秒) 或 2m (2 分钟)。 一个绝对值 1 表示的是 1毫秒 --无疑会使你的集群陷入瘫痪。
3 ES 全文搜索和相关性得分:
3.1 分析器:
ES的全文搜索和相关性得分的评判,依赖于倒排索引中对每个字段数据的维护,当我们把字段content 内容为:
“Set the shape to semi-transparent by calling set_trans(5)”
进行存储时,实际上ES对此字符串内容进行了分析,通过切割,转换后从而得到n多个词条,当检索时通过通过对检索词的切割和转换,进行词条的匹配,匹配的越多显然其相关性也就越强;
其中对字符的分析依赖于分析器:
将文本按照一定规则进行处理后在进行存入,分析器 实际上是将三个功能封装到了一个包里:
- 字符过滤器:首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and。
- 分词器:其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
- Token 过滤器:最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
ES 中已经内置了一些分析器供我们使用:
我们看看每个分析器会从下面的字符串得到哪些词条:
“Set the shape to semi-transparent by calling set_trans(5)”
- 标准分析器:
标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生
set, the, shape, to, semi, transparent, by, calling, set_trans, 5 - 简单分析器:
简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生
set, the, shape, to, semi, transparent, by, calling, set, trans - 空格分析器:
空格分析器在空格的地方划分文本。它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5) - 语言分析器
特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。
英语 分词器会产生下面的词条:
set, shape, semi, transpar, call, set_tran, 5
注意看 transparent、 calling 和 set_trans 已经变为词根格式。
3.2 测试分析器:
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触Elasticsearch。为了理解发生了什么,你可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:
GET /_analyze
{
"analyzer": "standard",// 使用标准分析器
"text": "Text to analyze"// 被分析的字段
}
使用标准分析器解析出的词条:
{
"tokens": [
{
"token": "text",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "to",
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "analyze",
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。
3.3 指定分析器:
当Elasticsearch在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。
你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域—不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。
要做到这一点,我们必须手动指定这些域的映射。
4 聚合正排索引:
4.1 Doc values :
ES 对文档的查询使用了倒排索引,那么当进行聚合的时候是否也可以直接使用倒排索引,对于聚合ES需要找到对应文档中某些需要聚合的字段进行收集处理,如果使用到索引,ES需要遍历对应文档中某个特定字段的索引,通过迭代每个词项来收集到对文档中改字段集合,并最终进行合并,特别是遇到该字段被分隔成为N多个词项的时候,收集的效率随着数据的增加变得低效,显然ES是不允许的;
ES 中聚合使用一个叫 doc values 的数据结构(在 Doc Values 介绍 里简单介绍)。 Doc values 可以使聚合更快、更高效并且内存友好,所以理解它的工作方式十分有益。
Doc values 通过转置两者间的关系来解决这个问题。倒排索引将词项映射到包含它们的文档,doc values 将文档映射到它们包含的词项:
如果文档中的字符串是被分析的 意味着Doc values 索引会将拆分后的词项和文档进行一一映射,此时如果对改字段进行term 桶聚合,可能得到对应词项的文档数,这可能不是我们想要的;被分析的字符串因为会被分词,所以在查询是,加载这些字段到内存会占用很大的空间:
一旦分析字符串被加载到 fielddata ,他们会一直在那里,直到被驱逐(或者节点崩溃)。由于这个原因,留意内存的使用情况,了解它是如何以及何时加载的,怎样限制对集群的影响是很重要的;
indices.fielddata.cache.size 控制为 fielddata 分配的堆空间大小。 当你发起一个查询,分析字符串的聚合将会被加载到 fielddata,如果这些字符串之前没有被加载过。如果结果中 fielddata 大小超过了指定 大小 ,其他的值将会被回收从而获得空间。
indices.fielddata.cache.size: 20%
当数据被转置之后,想要收集到 Doc_1 和 Doc_2 的唯一 token 会非常容易。获得每个文档行,获取所有的词项,然后求两个集合的并集。
4.2 Doc Values 索引的存储:
Doc Values 是在索引时与 倒排索引 同时生成。也就是说 Doc Values 和 倒排索引 一样,基于 Segement 生成并且是不可变的。同时 Doc Values 和 倒排索引 一样序列化到磁盘,这样对性能和扩展性有很大帮助。
Doc Values 通过序列化把数据结构持久化到磁盘,我们可以充分利用操作系统的内存,而不是 JVM 的 Heap 。 当 working set 远小于系统的可用内存,系统会自动将 Doc Values 驻留在内存中,使得其读写十分快速;不过,当其远大于可用内存时,系统会根据需要从磁盘读取 Doc Values,然后选择性放到分页缓存中。很显然,这样性能会比在内存中差很多,但是它的大小就不再局限于服务器的内存了。如果是使用 JVM 的 Heap 来实现那么只能是因为 OutOfMemory 导致程序崩溃了。
4.3 禁用 Doc Values
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: true 和 index: no ,我们得到一个只能被用于聚合/排序/脚本的字段。无可否认,这是一个非常少见的情况,但有时很有用;
5 ES–JVM:
ES 是使用java 写的,所以他也依赖于JVM:JVM 设置:
选择堆大小(Choosing a Heap Size)
在设置 Elasticsearch 堆大小时需要通过 $ES_HEAP_SIZE 环境变量应用两个规则:
- 不要超过可用 RAM 的 50%
Lucene 能很好利用文件系统的缓存,它是通过系统内核管理的。如果没有足够的文件系统缓存空间,性能会受到影响。 此外,专用于堆的内存越多意味着其他所有使用 doc values 的字段内存越少。 - 不要超过 32 GB
如果堆大小小于 32 GB,JVM 可以利用指针压缩,这可以大大降低内存的使用:每个指针 4 字节而不是 8 字节。
ES 中存在断路器,当我们的查询超过jvm 中的剩余内存,改查询会被中断,并且返回异常
机敏的读者可能已经发现 fielddata 大小设置的一个问题。fielddata 大小是在数据加载 之后 检查的。 如果一个查询试图加载比可用内存更多的信息到 fielddata 中会发生什么?答案很丑陋:我们会碰到 OutOfMemoryException 。
Elasticsearch 包括一个 fielddata 断熔器 ,这个设计就是为了处理上述情况。 断熔器通过内部检查(字段的类型、基数、大小等等)来估算一个查询需要的内存。它然后检查要求加载的 fielddata 是否会导致 fielddata 的总量超过堆的配置比例。
如果估算查询的大小超出限制,就会 触发 断路器,查询会被中止并返回异常。这都发生在数据加载 之前 ,也就意味着不会引起 OutOfMemoryException 。
可用的断路器(Available Circuit Breakers)
Elasticsearch 有一系列的断路器,它们都能保证内存不会超出限制:
fielddata 断路器默认设置堆的 60% 作为 fielddata 大小的上限
indices.breaker.fielddata.limit
request 断路器估算需要完成其他请求部分的结构大小,例如创建一个聚合桶,默认限制是堆内存的 40%。
indices.breaker.request.limit
total 揉合 request 和 fielddata 断路器保证两者组合起来不会使用超过堆内存的 70%。
断路器的限制可以在文件 config/elasticsearch.yml 中指定,可以动态更新一个正在运行的集群: 这个限制是按对内存的百分比设置的
indices.breaker.total.limit
PUT /_cluster/settings
{
"persistent" : {
"indices.breaker.fielddata.limit" : "40%"
}
}
最好为断路器设置一个相对保守点的值。 记住 fielddata 需要与 request 断路器共享堆内存、索引缓冲内存和过滤器缓存。Lucene 的数据被用来构造索引,以及各种其他临时的数据结构。 正因如此,它默认值非常保守,只有 60% 。过于乐观的设置可能会引起潜在的堆栈溢出(OOM)异常,这会使整个节点宕掉。
另一方面,过度保守的值只会返回查询异常,应用程序可以对异常做相应处理。异常比服务器崩溃要好。这些异常应该也能促进我们对查询进行重新评估
在 Fielddata的大小 中,我们提过关于给 fielddata 的大小加一个限制,从而确保旧的无用 fielddata 被回收的方法。 indices.fielddata.cache.size 和 indices.breaker.fielddata.limit 之间的关系非常重要。 如果断路器的限制低于缓存大小,没有数据会被回收。为了能正常工作,断路器的限制 必须 要比缓存大小 要高。
值得注意的是:断路器是根据总堆内存大小估算查询大小的,而 非 根据实际堆内存的使用情况。 这是由于各种技术原因造成的(例如,堆可能看上去是满的但实际上可能只是在等待垃圾回收,这使我们难以进行合理的估算)。但作为终端用户,这意味着设置需要保守,因为它是根据总堆内存必要的,而 不是 可用堆内存。
参考:
ES权威指南