• 发文
  • 评论
  • 微博
  • 空间
  • 微信

Elasticsearch性能优化指南

java达人 2020-09-06 11:49 发文

先了解相关读写原理

es 写数据过程

客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node (协调节点)。

coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)。

实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node 。

coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。


es 读数据过程

可以通过 doc id 来查询,会根据 doc id 进行 hash,判断出来当时把 doc id 分配到了哪个 shard 上面去,从那个 shard 去查询。

客户端发送请求到任意一个 node,成为 coordinate node 。

coordinate node 对 doc id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。

接收请求的 node 返回 document 给 coordinate node 。

coordinate node 返回 document 给客户端。

es 搜索数据过程

es 最强大的是做全文检索,就是比如你有三条数据:

java真好玩儿啊

java好难学啊

j2ee特别牛

你根据 java 关键词来搜索,将包含 java 的 document 给搜索出来。es 就会给你返回:java真好玩儿啊,java好难学啊。


客户端发送请求到一个 coordinate node 。

协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard ,都可以。

query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。

fetch phase:接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。

写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。

写数据底层原理


先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。


如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh 到一个新的 segment file 中,但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 os cache 。这个过程就是 refresh 。


每隔 1 秒钟,es 将 buffer 中的数据写入一个新的 segment file ,每秒钟会产生一个新的磁盘文件 segment file ,这个 segment file 中就存储最近 1 秒内 buffer 中写入的数据。


但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。


操作系统里面,磁盘文件其实都有一个东西,叫做 os cache ,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 os cache ,先进入操作系统级别的一个内存缓存中去。只要 buffer 中的数据被 refresh 操作刷入 os cache 中,这个数据就可以被搜索到了。


为什么叫 es 是准实时的? NRT ,全称 near real-time 。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 restful api 或者 java api ,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache 中,让数据立马就可以被搜索到。只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。


重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。


commit 操作发生第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。然后,将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file ,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。


这个 commit 操作叫做 flush 。默认 30 分钟自动执行一次 flush ,但如果 translog 过大,也会触发 flush 。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。


translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。


translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。


es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。


总结一下,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。


数据写入 segment file 之后,同时就建立好了倒排索引。

一个segment是一个完备的lucene倒排索引,而倒排索引是通过词典 (Term Dictionary)到文档列表(Postings List)的映射关系,快速做查询的。由于词典的size会很大,全部装载到heap里不现实,因此Lucene为词典做了一层前缀索引(Term Index),这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。这种数据结构占用空间很小,Lucene打开索引的时候将其全量装载到内存中,加快磁盘上词典查询速度的同时减少随机磁盘访问次数。

每个segment都有会一些索引数据驻留在heap里。因此segment越多,瓜分掉的heap也越多,并且这部分heap是无法被GC掉的

删除/更新数据底层原理

如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。

如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。

buffer 每 refresh 一次,就会产生一 个 segment file ,所以默认情况下是 1 秒钟一个 segment file ,这样下来 segment file 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point ,标识所有新的 segment file ,然后打开 segment file 供搜索使用,同时删除旧的 segment file 。




通用建议


不要返回大结果集

Elasticsearch被设计为搜索引擎,使其非常适合检索与查询匹配的最前面的文档。但是,对于属于数据库域的工作负载(如检索匹配特定查询的所有文档),效果不佳。如果需要执行此操作,请确保使用Scroll API。


避免大文档

假设默认的http.max_content_length设置为100MB,Elasticsearch将拒绝索引任何大于此值的文档。您可能决定增加该特定设置,但是Lucene仍然有大约2GB的限制。


即使不考虑硬性限制,大型文档通常也不实用。大型文档给网络,内存使用和磁盘带来了更大的压力,即使对于不要求_source的搜索请求也是如此,因为Elasticsearch在所有情况下都需要获取文档的_id,并且由于 文件系统缓存的工作方式,大型文档获取此字段的成本更高。索引此文档使用的内存量是该文档原始大小的数倍。近似搜索(例如phrase查询)和高亮显示开销也变得更加昂贵,因为它们的成本直接取决于原始文档的大小。


重新考虑信息单位有时是有用的。例如,您想使书籍可搜索,并不一定意味着文档应包含整本书。最好将章节或段落用作文档,这些文档中具有一个属性,以标识它们属于哪本书。这不仅避免了大文档的问题,还使搜索体验更好。例如,如果用户搜索foo和bar这两个词,则匹配的结果跨不同章节可能体验非常差,而匹配结果落入同一段落中就可能很不错。


秘密诀窍


混合精确搜索和提取词干

在构建搜索应用程序时,通常必须使用词干,比如对于“skiing”的查询需要匹配包含“ ski”或“ skis”的文档。但是,如果用户想专门搜索“skiing”怎么办?执行此操作的典型方法是使用 multi-field,以便以两种不同的方式对相同的内容建立索引:


curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{  "settings": {    "analysis": {      "analyzer": {        "english_exact": {          "tokenizer": "standard",          "filter": [            "lowercase"          ]        }      }    }  },  "mappings": {    "properties": {      "body": {        "type": "text",        "analyzer": "english",        "fields": {          "exact": {            "type": "text",            "analyzer": "english_exact"          }        }      }    }  }}'curl -X PUT "localhost:9200/index/_doc/1?pretty" -H 'Content-Type: application/json' -d'{  "body": "Ski resort"}'curl -X PUT "localhost:9200/index/_doc/2?pretty" -H 'Content-Type: application/json' -d'{  "body": "A pair of skis"}'curl -X POST "localhost:9200/index/_refresh?pretty"


两个记录都返回的搜索:


curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{  "query": {    "simple_query_string": {      "fields": [ "body" ],      "query": "ski"    }  }}'


返回第一个记录的搜索:


curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"query": {"simple_query_string": {"fields": [ "body.exact" ],"query": "ski"    }  }}'



获得一致的评分

当要获得良好的评分功能时,Elasticsearch使用分片和副本进行操作会增加挑战。


分数不可复制

假设同一位用户连续两次执行相同的请求,并且文档两次都没有以相同的顺序返回,这是非常糟糕的体验,不是吗?不幸的是,如果您有副本(index.number_of_replicas大于0),则可能会发生这种情况。原因是Elasticsearch以循环方式选择查询应访问的分片,因此,如果您连续运行两次相同的查询,很有可能会访问同一分片的不同副本。


现在为什么会出现问题?索引统计是分数的重要组成部分。而且由于删除的文档,同一分片的副本之间的索引统计可能会有所不同。您可能知道删除或更新文档时,不会立即将旧文档从索引中删除,而是将其标记为已删除,并且仅在下次合并该旧文档所属的segment时才从磁盘中删除它。但是,出于实际原因,这些已删除的文档将用于索引统计。因此,假设主分片刚刚完成了一个大型合并,删除了许多已删除的文档,那么它的索引统计信息可能与副本(仍然有大量已删除文档)完全不同,因此得分也有所不同。


解决此问题的推荐方法是,使用一个标识所登入的用户的字符串(例如,用户ID或会话ID)作为首选项。这样可以确保给定用户的所有查询始终会打到相同的分片,因此各查询的得分更加一致。


解决此问题的另一个好处是:当两个文档的分数相同时,默认情况下将按其内部Lucene文档ID(与_id无关)对它们进行排序。但是,这些doc ID在同一分片的副本之间可能会有所不同。因此,通过始终访问相同的分片,得分相同的文档更获得一致的排序。


相关性看起来不对

如果您发现具有相同内容的两个文档获得不同的分数,或者完全匹配的内容没有排在第一位,则该问题可能与分片有关。默认情况下,Elasticsearch使每个分片负责产生自己的分数。但是,由于索引统计信息是得分的重要贡献者,因此只有在分片具有相似的索引统计信息时,此方法才有效。假设是由于默认情况下文档均匀地路由到分片,因此索引统计信息应该非常相似,并且评分将按预期进行。但是,如果您:


在写入索引时路由,

查询多个索引,

或索引中的数据太少

那么很有可能所有与搜索请求有关的分片都没有相似的索引统计信息,并且相关性可能很差。


如果数据集较小,则解决此问题的最简单方法是将所有内容编入具有单个分片(index.number_of_shards:1)的索引,这是默认设置。然后,所有文档的索引统计信息都将相同,并且得分也将保持一致。


否则,解决此问题的推荐方法是使用dfs_query_then_fetch搜索类型。这将使Elasticsearch对所有涉及的分片执行初始往返,要求他们提供与查询有关的索引统计信息,然后协调节点将合并这些统计信息,并在请求分片执行查询阶段时将合并的统计信息与请求一起发送,这样分片就可以使用这些全局统计信息而不是它们自己的统计信息来进行评分。


在大多数情况下,这种额外的往返开销应该很少。但是,如果您的查询包含大量字段/term或模糊查询,请注意,仅收集统计信息可能并不便利,因为必须在term词典中查找所有term才能查找到统计信息。


将静态相关性信号纳入评分

许多域具有已知的与相关性相关的静态信号。例如,PageRank和URL长度是Web搜索的两个常用功能,以便独立于查询来调整网页的分数。


有两个主要查询,可以将静态分数贡献与文本相关性结合起来,例如。用BM25计算得出: - script_score query - rank_feature query、

例如,假设您有一个希望与BM25得分结合使用的pagerank字段,以使最终得分等于score = bm25_score + pagerank /(10 + pagerank)。


使用script_score查询,查询将如下所示:


curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"query": {"script_score": {"query": {"match": { "body": "elasticsearch" }      },"script": {"source": "_score * saturation(doc[u0027pageranku0027].value, 10)"      }    }  }}'


尽管这两个选项都将返回相似的分数,但需要权衡取舍:script_score提供了很大的灵活性,使您可以根据需要将文本相关性分数与静态信号结合起来。另一方面,rank_feature查询仅提供了几种将静态信号混合到评分中的方法。但是,它依赖于rank_feature和rank_features字段,它们以一种特殊的方式索引值,从而使rank_feature查询可以跳过非竞争性文档并更快地获得查询的顶部匹配项。


写入优化


加大translog flush间隔,目的是降低iops、writeblock。

从ES 2.x开始,在默认设置下,translog的持久化策略为:每个请求都“flush”。对应配置项如下:index.translog.durability: request

这是影响 ES 写入速度的最大因素。但是只有这样,写操作才有可能是可靠的。如果系统可以接受一定概率的数据丢失(例如,数据写入主分片成功,尚未复制到副分片时,主机断电。由于数据既没有刷到Lucene,translog也没有刷盘,恢复时translog中没有这个数据,数据丢失),则调整translog持久化策略为周期性和一定大小的时候“flush”,例如:index.translog.durability: async

设置为async表示translog的刷盘策略按sync_interval配置指定的时间周期进行。

index.translog.sync_interval: 120s


加大index refresh间隔,除了降低I/O,更重要的是降低了segment merge频率。

每次索引的refresh会产生一个新的Lucene段,这会导致频繁的segment merge行为使更改对搜索可见的操作(称为刷新)非常昂贵,并且在正在进行索引活动的情况下经常进行调用会损害索引速度。


默认情况下,Elasticsearch会定期每秒刷新一次索引,但仅在最近30秒内已收到一个或多个搜索请求的索引上刷新。


如果您没有或只有很少的搜索流量(例如,每5分钟少于一个搜索请求)并且想要优化索引速度,则这是最佳配置。此行为旨在在不执行搜索时在默认情况下自动优化批量索引。为了选择退出此行为,请显式设置刷新间隔。


另一方面,如果您的索引遇到常规搜索请求,则此默认行为表示Elasticsearch将每1秒刷新一次索引。如果您有能力增加从索引到文档可见之间的时间,则可以将index.refresh_interval增加到更大的值,例如30s,可能有助于提高索引速度。


调整bulk请求。

批量请求将比单文档索引请求产生更好的性能。为了知道批量请求的最佳大小,您应该在具有单个分片的单节点上运行基准测试。首先尝试一次索引100个文档,然后索引200个,再索引400个,依此类推。在每次基准测试运行中,批量请求中的文档数量加倍。当索引速度开始趋于平稳时,您便知道已达到批量请求数据的最佳大小。如果得分相同,宁可少也不要多。当大量请求同时发送时,请注意太大的批量请求可能会使集群处于内存压力下,因此,建议即使每个请求看起来执行得更好,也要避免每个请求超过几十兆字节。


如果 CPU 没有压满,则应该提高写入端的并发数量。但是要注意 bulk线程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端请求被拒绝,此时客户端会收到429错误(TOO_MANY_REQUESTS),客户端对此的处理策略应该是延迟重试。不可忽略这个异常,否则写入系统的数据会少于预期。即使客户端正确处理了429错误,我们仍然应该尽量避免产生reject。因此,在评估极限的写入能力时,客户端的极限写入并发量应该控制在不产生reject前提下的最大值为宜。


bulk线程池和队列

建立索引的过程属于计算密集型任务,应该使用固定大小的线程池配置,来不及处理的任务放入队列。线程池最大线程数量应配置为CPU核心数+1,这也是bulk线程池的默认设置,可以避免过多的上下文切换。队列大小可以适当增加,但一定要严格控制大小,过大的队列导致较高的GC压力,并可能导致FGC频繁发生。



升级硬件


如果索引是受I / O约束的,则应研究为文件系统高速缓存提供更多内存(请参见上文)或购买速度更快的驱动器。特别是,已知SSD驱动器的性能要比旋转磁盘好。始终使用本地存储,应避免使用NFS或SMB等远程文件系统。还请注意虚拟存储,例如Amazon的Elastic Block Storage。虚拟存储在Elasticsearch上可以很好地工作,并且很有吸引力,因为它安装起来如此之快且简单,但是与专用本地存储相比,它在本质上在持续运行方面还很慢。如果在EBS上建立索引,请确保使用预配置的IOPS,否则操作可能会很快受到限制。


通过配置RAID 0阵列,跨多个SSD划分索引。请记住,由于任何一个SSD的故障都会破坏索引,因此会增加故障的风险。但是,通常这是一个正确的权衡:优化单个分片以实现最佳性能,然后在不同节点之间添加副本,以便为任何节点故障提供冗余。您还可以使用快照和还原来备份索引以提供进一步保障。


优化磁盘间的任务均匀情况,将shard尽量均匀分布到物理主机的各个磁盘。

如果部署方案是为path.data配置多个路径来使用多块磁盘,则ES在分配shard时,落到各磁盘上的 shard 可能并不均匀,这种不均匀可能会导致某些磁盘繁忙,利用率在较长时间内持续达到100%。这种不均匀达到一定程度会对写入性能产生负面影响。ES在处理多路径时,优先将shard分配到可用空间百分比最多的磁盘上,因此短时间内创建的shard可能被集中分配到这个磁盘上,即使可用空间是99%和98%的差别。后来ES在2.x版本中开始解决这个问题:预估一下shard 会使用的空间,从磁盘可用空间中减去这部分,直到现在6.x版也是这种处理方式。但是实现也存在一些问题:


从可用空间减去预估大小这种机制只存在于一次索引创建的过程中,下一次的索引创建,磁盘可用空间并不是上次做完减法以后的结果。这也可以理解,毕竟预估是不准的,一直减下去空间很快就减没了。但是最终的效果是,这种机制并没有从根本上解决问题,即使没有完美的解决方案,这种机制的效果也不够好。如果单一的机制不能解决所有的场景,那么至少应该为不同场景准备多种选择。为此,我们为ES增加了两种策略。·

   简单轮询:在系统初始阶段,简单轮询的效果是最均匀的。·

   基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询。


您应该通过禁用交换来确保操作系统不会交换出Java进程。

大多数操作系统尝试为文件系统高速缓存使用尽可能多的内存,并急切换出未使用的应用程序内存。这可能会导致部分JVM堆甚至其可执行页面换出到磁盘上。

交换对性能,节点稳定性非常不利,应不惜一切代价避免交换。它可能导致垃圾收集持续数分钟而不是毫秒,并且可能导致节点响应缓慢甚至断开与集群的连接。在弹性分布式系统中,让操作系统杀死该节点更为有效。

文件系统缓存将用于缓冲I / O操作。您应确保至少将运行Elasticsearch的计算机的一半内存分配给文件系统缓存。

自动生成id

为具有显式ID的文档建立索引时,Elasticsearch需要检查具有相同ID的文档是否已存在于同一分片中,这是一项昂贵的操作,并且随着索引的增长而变得更加昂贵。通过使用自动生成的ID,Elasticsearch可以跳过此检查,这使索引编制更快。

优化节点间的任务分布,将任务尽量均匀地发到各节点。

为了节点间的任务尽量均衡,数据写入客户端应该把bulk请求轮询发送到各个节点。当使用Java API或REST API的bulk接口发送数据时,客户端将会轮询发送到集群节点,节点列表取决于:· 使用Java API时,当设置client.transport.sniff为true(默认为false)时,列表为所有数据节点,否则节点列表为构建客户端对象时传入的节点列表。· 使用REST API时,列表为构建对象时添加进去的节点。Java API的TransportClient和REST API的RestClient都是线程安全的,如果写入程序自己创建线程池控制并发,则应该使用同一个Client对象。在此建议使用REST API,Java API会在未来的版本中废弃,REST API有良好的版本兼容性好。理论上,Java API在序列化上有性能优势,但是只有在吞吐量非常大时才值得考虑序列化的开销带来的影响,通常搜索并不是高吞吐量的业务。

要观察bulk请求在不同节点间的均衡性,可以通过cat接口观察bulk线程池和队列情况:_cat/thread_pool

优化Lucene层建立索引的过程,目的是降低CPU占用率及I/O,例如,禁用_all字段。

初始加载禁用副本

如果您有大量数据想要一次全部加载到Elasticsearch中,则将index.number_of_replicas设置为0可能有益于扩大索引编制。没有副本意味着丢失单个节点可能会导致数据丢失,因此将数据保存在其他位置很重要,以便在出现问题时可以重试此初始负载。初始加载完成后,可以将index.number_of_replicas设置回其原始值。


如果在索引设置中配置了index.refresh_interval,则在初始加载期间将其取消设置,并在初始加载完成后将其设置回其原始值可能会有所帮助。


多线程请求

发送批量请求的单个线程不太可能最大化使用Elasticsearch集群的索引容量。为了使用集群的所有资源,您应该从多个线程或进程发送数据。除了更好地利用集群的资源之外,这还有助于降低每个fsync的成本。


确保注意TOO_MANY_REQUESTS(429)响应代码(Java客户端使用EsRejectedExecutionException),这是Elasticsearch告诉您它无法跟上当前索引速率的方式。发生这种情况时,您应该暂停索引,然后再重试,最好使用随机指数避免。


与确定批量请求的大小类似,只有测试才能确定最佳数。可以通过逐渐增加工作程序数量直到集群上的I / O或CPU达到饱和来测试这一点。


索引buffer大小调整

如果您的节点仅执行大量的索引任务,请确保index.memory.index_buffer_size足够大,以使每个分片在进行大量的索引时最多提供512 MB索引缓冲区(此后加大,索引性能通常不会提高)。Elasticsearch接受该设置(占Java堆的百分比或绝对字节大小),并将其用作所有活动分片上的共享缓冲区。非常活跃的分片自然会比执行轻量级索引的分片更多地使用此缓冲区。


默认值为10%,这通常足够了:例如,如果给JVM 10GB的内存,它将为索引缓冲区提供1GB的空间,这足以容纳两个进行大量索引的分片。


indexing buffer在为doc建立索引时使用,当缓冲满时会刷入磁盘,生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成新segment的机会。每个shard有自己的indexing buffer,下面的这个buffer大小的配置需要除以这个节点上所有shard的数量:indices.memory.index_buffer_size默认为整个堆空间的10%。indices.memory.min_index_buffer_size默认为48MB。indices.memory.max_index_buffer_size默认为无限制。在执行大量的索引操作时,indices.memory.index_buffer_size的默认设置可能不够,这和可用堆内存、单节点上的shard数量相关,可以考虑适当增大该值。


使用跨集群复制来防止搜索窃取索引写入操作时的资源

在单个集群中,索引和搜索可竞争资源。通过设置两个集群,配置跨集群复制以将数据从一个集群复制到另一个集群,并将所有搜索路由到具有 follower 索引的集群,搜索活动将不再从托管该集群的 leader 索引中窃取资源。


 磁盘优化,见后续


搜索优化


 为文件系统cache预留足够内存

Elasticsearch严重依赖于文件系统缓存,以加快搜索速度。通常,您应确保至少有一半的可用内存分配给文件系统缓存,以便Elasticsearch可以将索引的热点数据保留在物理内存中。

使用更快的硬件

写入性能对CPU的性能更敏感,而搜索性能在一般情况下更多的是在于I/O能力,使用SSD会比旋转类存储介质好得多。始终使用本地存储,应避免使用NFS或SMB等远程文件系统。还请注意虚拟存储,例如Amazon的Elastic Block Storage。虚拟存储在Elasticsearch上可以很好地工作,并且很有吸引力,因为它安装起来如此之快且简单,但是与专用本地存储相比,它在本质上在持续运行方面还很慢。如果在EBS上建立索引,请确保使用预配置的IOPS,否则操作可能会很快受到限制。如果搜索类型属于计算比较多,则可以考虑使用更快的CPU。

文档模型

为了让搜索时的成本更低,文档应该合理建模。特别是应该避免join操作,嵌套(nested)会使查询慢几倍,父子(parent-child)关系可能使查询慢数百倍,因此,如果可以通过非规范化(denormalizing)文档来回答相同的问题,则可以显著地提高搜索速度。

预索引数据

还可以针对某些查询的模式来优化数据的索引方式。例如,如果所有文档都有一个 price字段,并且大多数查询在一个固定的范围上运行range聚合,那么可以通过将范围“pre-indexing”到索引中并使用terms聚合来加快聚合速度。


curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{"mappings": {"properties": {"price_range": {"type": "keyword"      }    }  }}'curl -X PUT "localhost:9200/index/_doc/1?pretty" -H 'Content-Type: application/json' -d'{"designation": "spoon","price": 13,"price_range": "10-100"}'

curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"aggs": {"price_ranges": {"terms": {"field": "price_range"      }    }  }}'



字段映射

有些字段的内容是数值,但并不意味着其总是应该被映射为数值类型,例如,一些标识符,将它们映射为keyword可能会比integer或long更好。

优化日期搜索

在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配到的范围一直在变化。但是,从用户体验的角度来看,切换到一个完整的日期通常是可以接受的,这样可以更好地利用查询缓存。


curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"query": {"constant_score": {"filter": {"range": {"my_date": {"gte": "now-1h/m","lte": "now/m"          }        }      }    }  }}'



代替


curl -X PUT "localhost:9200/index/_doc/1?pretty" -H 'Content-Type: application/json' -d'{"my_date": "2016-05-11T16:30:55.328Z"}'curl -X GET "localhost:9200/index/_search?pretty" -H 'Content-Type: application/json' -d'{"query": {"constant_score": {"filter": {"range": {"my_date": {"gte": "now-1h","lte": "now"          }        }      }    }  }}'



为只读索引执行force-merge

为不再更新的只读索引执行force merge,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并,将只读索引强制合并为一个Lucene分段不仅可以优化搜索过程,对索引恢复速度也有好处。基于日期进行轮询的索引的旧数据一般都不会再更新。此前的章节中说过,应该避免持续地写一个固定的索引,直到它巨大无比,而应该按一定的策略,例如,每天生成一个新的索引,然后用别名关联,或者使用索引通配符。这样,可以每天选一个时间点对昨天的索引执行force-merge、Shrink等操作。


只读的索引可能会从合并到单个段中受益。对于基于时间的索引,通常是这种情况:只有当前时间范围的索引才能获取新文档,而较旧的索引则为只读。强制合并到单个段中的分片可以使用更简单,更有效的数据结构来执行搜索。


不要强行合并仍在写入的索引,或者将来会再次写入的索引。而是,根据需要依靠自动后台合并过程执行合并,以保持索引平稳运行。如果继续写入强制合并的索引,则其性能可能会变得更差。

预热全局序号 

全局序号是一种数据结构,用于在keyword字段上运行terms聚合。它用一个数值来代表字段中的字符串值,然后为每一数值分配一个 bucket。这需要一个对 global ordinals 和 bucket的构建过程。默认情况下,它们被延迟构建,因为ES不知道哪些字段将用于 terms聚合,哪些字段不会。可以通过配置映射在刷新(refresh)时告诉ES预先加载全局序数:



curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{"mappings": {"_doc": {"properties": {"foo": {"type": "keyword","eager_global_ordinals": true        }      }    }  }}'



execution hint 

terms聚合有两种不同的机制:

     1 通过直接使用字段值来聚合每个桶的数据(map)。

      2 通过使用字段的全局序号并为每个全局序号分配一个bucket(global_ordinals)。

ES 使用 global_ordinals 作为 keyword 字段的默认选项,它使用全局序号动态地分配bucket,因此内存使用与聚合结果中的字段数量是线性关系。在大部分情况下,这种方式的速度很快。当查询只会匹配少量文档时,可以考虑使用 map。默认情况下,map 只在脚本上运行聚合时使用,因为它们没有序数。

转换查询表达式

在组合查询中可以通过bool过滤器进行and、or和not的多个逻辑组合检索,这种组合查询中的表达式在下面的情况下可以做等价转换:(A|B) & (C|D) ==> (A&C) | (A&D) | (B&C) | (B&D )


调节搜索请求中的bached_reduce_size 

该字段是搜索请求中的一个参数。默认情况下,聚合操作在协调节点需要等所有的分片都取回结果后才执行,使用 batched_reduce_size 参数可以不等待全部分片返回结果,而是在指定数量的分片返回结果之后就可以先处理一部分(reduce)。这样可以避免协调节点在等待全部结果的过程中占用大量内存,避免极端情况下可能导致的OOM。该字段的默认值为512,从ES 5.4开始支持。


使用近似聚合

近似聚合以牺牲少量的精确度为代价,大幅提高了执行效率,降低了内存使用。近似聚合的使用方式可以参考官方手册


深度优化还是广度优先

ES有两种不同的聚合方式:深度优先和广度优先。深度优先是默认设置,先构建完整的树,然后修剪无用节点。大多数情况下深度聚合都能正常工作,但是有些特殊的场景更适合广度优先,先执行第一层聚合,再继续下一层聚合之前会先做修剪,官方例子:https://www.elastic.co/guide/cn/elasticsearch/guide/current/_preventing_combinatorial_explosions.html。


限制搜索请求分片数

一个搜索请求涉及的分片数量越多,协调节点的CPU和内存压力就越大。默认情况下,ES会拒绝超过1000个分片的搜索请求。我们应该更好地组织数据,让搜索请求的分片数更少。如果想调节这个值,则可以通过action.search.shard_count配置项进行修改。虽然限制搜索的分片数并不能直接提升单个搜索请求的速度,但协调节点的压力会间接影响搜索速度,例如,占用更多内存会产生更多的GC压力,可能导致更多的stop-the-world时间等,因此间接影响了协调节点的性能


利用自适应副本选择ARS提升ES响应速度

为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力和健康水平。

ES希望这个过程足够智能,能够将请求路由到其他数据副本,直到该节点恢复到足以处理更多搜索请求的程度。在ES中,此过程称为“自适应副本选择”。


搜索尽可能少的字段

query_string或multi_match查询目标的字段越多,它的速度就越慢。提高多个字段搜索速度的常用技术是在索引时间将其值复制到单个字段中,然后在搜索时使用此字段。可以使用映射的“copy_to”指令来自动执行此操作,而不必更改文档的来源。



PUT movies{"mappings": {"properties": {"name_and_plot": {"type": "text"      },"name": {"type": "text","copy_to": "name_and_plot"      },"plot": {"type": "text","copy_to": "name_and_plot"      }    }  }}




避免脚本


如果可能,请避免在搜索中使用脚本或脚本字段。由于脚本无法利用索引结构,因此在搜索查询中使用脚本会导致搜索速度降低。


如果您经常使用脚本来转换索引数据,则可以通过在摄取期间进行这些更改来加快搜索速度。但是,这通常意味着较慢的索引速度。


如果一定要用,则应该优先考虑painless和expressions。


预热文件系统缓存

如果重新启动运行Elasticsearch的计算机,则文件系统缓存将为空,因此,操作系统需要一些时间才能将索引的热区加载到内存中,以便快速进行搜索操作。您可以使用index.store.preload设置明确告诉操作系统哪些文件应该预先地加载到内存中,具体取决于文件扩展名。


如果文件系统高速缓存的大小不足以容纳所有数据,则将索引过多或文件过多的数据预加载到文件系统高速缓存中,会使搜索速度变慢。请谨慎使用。


使用索引排序以加快连接速度

索引排序可能有用,以便使连接更快,但以稍微慢一些的索引写入为代价。索引排序文档 index sorting documentation.


使用preference优化缓存利用率

有多个可以帮助提高搜索性能的缓存,例如文件系统缓存,请求缓存或查询缓存。但是所有这些缓存都在节点级别维护,这意味着如果您连续两次运行相同的请求,具有一个或多个副本,并使用默认路由算法“轮询”,则这两个请求将转到不同的分片副本 ,节点级缓存不起作用。


由于搜索应用程序的用户通常会依次运行类似的请求(例如为了分析索引的较窄子集),因此使用标识当前用户或会话的preference值可以帮助优化缓存的使用。


数据预热

假如说,哪怕是你就按照上述的方案去做了,es 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G 数据留在了磁盘上。

其实可以做数据预热。

举个例子,拿微博来说,你可以把一些大V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。

对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。

冷热分离

es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

你看,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 filesystem cache 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。


副本可能有助于提高吞吐量,但并非始终如此

除了提高弹性之外,副本还可以帮助提高吞吐量。例如,如果您有一个单分片索引和三个节点,则需要将副本数设置为2,以便总共拥有3个分片副本,利用所有节点。


现在,假设您有一个2分片索引和两个节点。在一种情况下,副本数为0,这意味着每个节点都拥有一个分片。在第二种情况下,副本数为1,这意味着每个节点都有两个分片。哪种设置在搜索效果方面效果最好?通常,每个节点的分片总数较少的设置会更好地执行。这样做的原因是,它为每个分片提供了更多的可用文件系统缓存份额,并且文件系统缓存可能是Elasticsearch的第一性能因素。同时,请注意,在单节点故障的情况下,没有副本的部署更容易失败,因此在吞吐量和可用性之间要进行权衡。


那么正确的副本数是多少?如果您的集群总共有num_nodes个节点,总共有num_primaries个主分片,并且如果您希望最多一次处理max_failures个节点故障,那么适合您的副本数是max(max_failures,ceil(num_nodes / num_primaries)-1)。


使用Profile API调整查询

您还可以使用Profile API分析查询和聚合每个部分的成本。这可能使您可以调整查询花费更少成本,从而获得正面的性能结果并减少负载。另请注意,可以在Search Profiler中轻松查看Profile API负载以提高可读性,Search Profiler是所有X-Pack许可证(包括免费的X-Pack Basic许可证)中均可使用的Kibana开发工具UI。


对Profile API的一些警告是:


Profile API作为调试工具增加了搜索执行的大量开销,并且还可能产生非常冗长的输出

给定增加的开销,得出的花费时间指标可能不是实际花费时间,但是可以在子句之间进行比较以获取相对时间差异

Profile API最适合用于研究查询中花费最多开销的子句后面的可能原因,但并非旨在准确衡量每个子句的时间


使用index_phrases进行更快的短语查询

文本字段具有index_phrases选项,该选项索引2个,并且查询解析器会自动利用该选项来运行不带斜率的词组查询。如果您的用例涉及运行大 量短语查询,则可以大大加快查询速度。



使用index_prefixes进行更快的前缀查询

文本字段具有index_prefixes选项,该选项为所有term的前缀编制索引,并由查询解析器自动利用以运行前缀查询。如果您的用例涉及运行大量前缀查询,则可以大大加快查询速度。


使用constant_keyword加快过滤


调整磁盘使用率

禁用不需要的功能  


默认情况下,Elasticsearch为大多数字段编入索引并将doc值添加到大多数字段,以便可以立即搜索和聚合汇总它们。例如,如果您有一个名为foo的数字字段,需要在其上运行直方图,但您永远不需要对其进行过滤,则可以安全地在 mappings中禁用对此字段的索引:


text字段在索引中存储归一化因子,以便能够对文档进行评分。如果您只需要 text字段上的匹配功能但不关心产生的分数,则可以将Elasticsearch配置为不将 norms 写入索引:


curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{"mappings": {"properties": {"foo": {"type": "text","norms": false      }    }  }}'


默认情况下,文本字段还将频率和位置存储在索引中。频率用于计算分数,位置用于运行 phrase 查询。如果您不需要运行 phrase 查询,可以告诉Elasticsearch不要索引位置信息:


curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{"mappings": {"properties": {"foo": {"type": "text","index_options": "freqs"      }    }  }}'


此外,如果您既不计分,也可以将Elasticsearch配置为仅对每个 term的匹配文档编制索引。您仍然可以在此字段上进行搜索,但是词组查询将引发错误,并且评分将假定 term在每个文档中仅出现一次。


curl -X PUT "localhost:9200/index?pretty" -H 'Content-Type: application/json' -d'{"mappings": {"properties": {"foo": {"type": "text","norms": false,"index_options": "freqs"      }    }  }}'


不要使用默认的动态字符串映射

默认的动态字符串mappings会将字符串字段索引为文本和关键字。如果您只需要其中之一,则很浪费。通常,id字段仅需要索引为 keyword,而body字段仅需要索引为 text 字段。


可以通过在字符串字段上配置显式映射或设置将字符串字段映射为text 或keyword的动态模板来禁用此功能。



观测分片大小

较大的分片将在存储数据方面更加有效。要增加分片的大小,可以通过创建具有较少主分片的索引,创建较少的索引(例如通过利用Rollover API)或使用Shrink API修改现有索引来减少索引中的主分片数量。


请记住,较大的分片大小会带来缺点,例如较长的完整恢复时间。


禁用_source

_source字段存储文档的原始JSON body 。如果您不需要访问它,可以将其禁用。但是,需要访问_source的API(例如 update 和 reindex )将无法使用。


使用best_compression

_source和storage字段很容易占用不可忽略的磁盘空间。通过使用best_compression编解码器,可以更大程度地压缩。


强制合并

Elasticsearch中的索引存储在一个或多个分片中。每个分片都是一个Lucene索引,由一个或多个 segment(磁盘上的实际文件)组成。较大的 segment更有效地存储数据。


_forcemerge API可用于减少每个分片的段数。在许多情况下,通过设置max_num_segments = 1,可以将每个分片的 segment数减少到一个。


收缩索引

使用Shrink API,您可以减少索引中的分片数量。与上面的Force Merge API一起使用,可以大大减少索引的分片和分段的数量。


使用足够的最小数字类型

您为数字数据选择的类型可能会对磁盘使用产生重大影响。特别是,整数应使用整数类型( byte, short, integer o或 long)存储,并且浮点数应在适当的情况下存储在scaled_float中,或存储在适合以下用例的最小类型中:相对于double优先使用float或相对于float优先使用half_float将有助于节省存储空间。


使用索引排序来定位相似的文档

当Elasticsearch存储_source时,它会一次压缩多个文档,以提高整体压缩率。例如,文档共享相同的字段名称是很常见的,而文档共享某些字段值是很常见的,尤其是在基数较低或zipfian分布的字段上。


默认情况下,文档按添加到索引的顺序压缩在一起。如果启用了索引排序,则它们将按排序顺序进行压缩。将具有相似结构,字段和值的文档分类在一起可以提高压缩率。


将字段按相同顺序放在文档中

由于多个文档被压缩成块,因此如果字段总是以相同的顺序出现,则更有可能在这些_source文档中找到更长的重复字符串。



避免分片过多

在某些情况下,减少集群中的分片数量,同时保持相同数量的数据,可以更有效地利用系统资源(CPU,RAM,IO)。在这些情况下,我们认为集群分片过多了。


出现此拐点的分片数量取决于多种因素,包括:


可用的硬件

分片负载

数据量

针对集群执行的查询的类型

这些查询的发出率

正在查询的数据量


在生产环境硬件上,针对生产数据进行测试是校准最佳分片大小的唯一方法。通常使用数十GB的分片大小,这可能是进行实验的有用起点。当评估不同分片大小的影响时,Kibana的Elasticsearch监视功能可提供有用的历史集群性能视图。


为什么分片效率低下

每个segment都有需要保留在堆内存中的元数据。这些包括字段列表,文档数和term词典。随着分片的大小增加,其segment的大小通常也会增加,因为较小的segment会合并为较少的较大segment。通常,这可以减少给定数据量的分片的segment元数据所需的堆数量。分片至少应大于1GB,才能最有效地利用内存。


但是,即使一开始分片在1GB左右的内存效率上有所提高,但充满1GB分片的集群的性能仍可能会很差。这是因为拥有许多小分片也会对搜索和索引操作产生负面影响。每个查询或索引操作在要查询或建立索引的索引分片的单个线程中执行。接收到来自客户端的请求的节点将负责将该请求分配给适当的分片,并将这些单独分片的结果归并为单个响应。即使假设集群有足够的 search threadpool threads 可立即针对请求所需的所有分片进行请求的操作,但与向持有这些分片的节点发出网络请求以及必须合并许多小型结果相关的开销会导致分片延迟增加。这继而可能导致线程池耗尽,并因此导致吞吐量降低。


如何减少分片数量并增加分片大小

尝试使用这些方法来减少过度分片。


减少新索引的分片数量

您可以为使用create index API创建的新索引,指定index.number_of_shards设置,也可以为由索引生命周期管理(ILM)自动创建的索引,将它作为索引模板的一部分。


使用 rollover index API. rollover索引时,可以覆盖index.number_of_shards。


通过增加过渡阈值来创建更大的分片

您可以使用  rollover index API或通过在ILM策略中指定  rollover 操作来翻转索引。如果使用ILM策略,请增加rollover条件阈值(max_age,max_docs,max_size),以允许索引在过渡之前增长到更大的大小,这会创建更大的分片。


请特别注意任何空索引。由于满足max_age阈值,这些可以由滚动索引的ILM策略管理。在这种情况下,您可能需要调整策略以利用max_docs或max_size属性来防止创建这些空索引。一个可能发生这种情况的示例是一个或多个Beats停止发送数据。如果将这些Beats的ILM管理的索引配置为每天滚动,则每天将生成新的空索引。可以使用 cat count API.识别空索引。


通过使用跨越较长时间段的索引模式来创建更大的分片

创建涵盖更长时间段的索引可减少索引和分片数,同时增加索引大小。例如,您可以创建每月甚至每年的索引,而不是每日索引。


如果使用Logstash创建索引,则可以将Elasticsearch输出的index属性修改为涵盖较长时间段的日期数学表达式。例如,使用logstash-%{+ YYYY.MM}而非logstash-%{+ YYYY.MM.dd}创建月度索引,而不是每日索引。Beats还允许您更改在Elasticsearch输出的index属性中定义的日期数学表达式,例如Filebeat。


将现有索引缩小到更少的分片

您可以使用缩小索引API将现有索引缩小到更少的分片。


索引生命周期管理还可以在warm阶段对索引执行收缩操作。


重新索引现有索引以减少分片

您可以使用reindex API将现有索引重新分配到具有更少分片的新索引。重新索引数据后,可以删除过多分片的索引。


将索引从较短时期重新索引为较长时期

您可以使用reindex API将涵盖较短时间段的多个小索引重新索引为涵盖较长时间段的较大索引。例如,十月起的每日索引以及诸如foo-2019.10.11之类的命名模式可以合并为月度foo-2019.10索引,如下所示:


curl -X POST "localhost:9200/_reindex?pretty" -H 'Content-Type: application/json' -d'{"source": {"index": "foo-2019.10.*"  },"dest": {"index": "foo-2019.10"  }}'






声明:本文为OFweek维科号作者发布,不代表OFweek维科号立场。如有侵权或其他问题,请及时联系我们举报。
2
评论

评论

    相关阅读

    暂无数据

    java达人

    我们的使命是为程序员赋能,请搜索...

    举报文章问题

    ×
    • 营销广告
    • 重复、旧闻
    • 格式问题
    • 低俗
    • 标题夸张
    • 与事实不符
    • 疑似抄袭
    • 我有话要说
    确定 取消

    举报评论问题

    ×
    • 淫秽色情
    • 营销广告
    • 恶意攻击谩骂
    • 我要吐槽
    确定 取消

    用户登录×

    请输入用户名/手机/邮箱

    请输入密码