自动生成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)。