Elasticsearch性能优化指南

java达人
关注

先了解相关读写原理

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的计算机的一半内存分配给文件系统缓存。

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存