Elasticsearch性能优化汇总——写入&搜索
概述
本文沉淀Elasticsearch性能优化方式,包含写入、搜索优化。
推荐结合《Elasticsearch必备原理理解》一起食用~
一、写入速度优化
在Elasticsearch的默认设置下,是综合考虑数据可靠性、搜索实时性、写入速度等因素的。当离开默认设置、追求极致的写入速度时,很多是以牺牲可靠性和搜索实时性为代价的。有时候,业务上对数据可靠性和搜索实时性要求并不高,反而对写入速度要求很高,此时可以调整一些策略,最大化写入速度。
接下来的优化基于正常运行的前提下,如果是集群首次批量导入数据,则可以将副本数设置为0,导入完毕再将副本数调整回去,这样副本分片只需要复制,节省了构建索引过程。
综合来说,提升写入速度从以下几个方面入手:
- 加大translog flush间隔,目的是降低iops、writeblock;
- 加大index refresh间隔,除了降低I/O,更重要的是降低了segment merge频率;
- 调整bulk请求;
- 优化磁盘间的任务均匀情况,将shard尽量均匀分布到物理主机的各个磁盘;
- 优化节点间的任务分布,将任务尽量均匀地发到各节点;
- 优化Lucene层建立索引的过程,目的是降低CPU占用率及I/O,例如,禁用_all字段。
1.1 translog flush间隔调整
从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
加大translog刷盘间隔时间。默认为5s,不可低于100ms
index.translog.flush_threshold_size:1024mb
超过这个大小会导致refresh操作,产生新的Lucene分段。默认值为512MB。
1.2 索引刷新间隔refresh_interval
默认情况下索引的refresh_interval为1秒,这意味着数据写1秒后就可以被搜索到,每次索引的refresh会产生一个新的Lucene段,这会导致频繁的segment merge行为,如果不需要这么高的搜索实时性,应该降低索引refresh周期,例如:
index.refresh_interval: 30s
1.3 indexing buffer
indexing buffer在为doc建立索引时使用,当缓冲满时会刷入磁盘,生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成新segment的机会。每个shard有自己的indexing buffer,下面的这个buffer大小的配置需要除以这个节点上所有shard的数量:
indices.memory.index_buffer_size
默认为整个堆空间的10%
iindices.memory.min_index_buffer_size
默认为48MB
indices.memory.max_index_buffer_size
默认为无限制
在执行大量的索引操作时,indices.memory.index_buffer_size的默认设置可能不够,这和可用堆内存、单节点上的shard数量相关,可以考虑适当增大该值。
1.4 使用bulk请求
批量写比一个索引请求只写单个文档的效率高得多,但是要注意bulk请求的整体字节数不要太大,太大的请求可能会给集群带来内存压力,因此每个请求最好避免超过几十兆字节,即使较大的请求看上去执行得更好。
1.5 索引过程调整和优化
1.5.1 自动生成doc ID
通过ES写入流程可以看出,写入doc时如果外部指定了id,则ES会先尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可以避免这个环节。
1.5.2 调整字段Mappings
- 减少字段数量,对于不需要建立索引的字段,不写入ES;
- 将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义;
- 减少字段内容长度,如果原始数据的大段内容无须全部建立索引,则可以尽量减少不必要的内容;
- 使用不同的分词器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异。
1.5.3 调整_source字段
_source字段用于存储doc原始数据,对于部分不需要存储的字段,可以通过includes excludes过滤,或者将_source禁用,一般用于索引和数据分离。
这样可以降低I/O的压力,不过实际场景中大多不会禁用_source,而即使过滤某些字段,对于写入速度的提升作用也不大,满负荷写入情况下,基本是CPU先跑满了,瓶颈在于CPU。
1.5.4 禁用_all字段
从ES 6.0开始,_all字段默认认为不启用,而在此前的版本中,_all字段默认是开启的。_all字段中包含所有字段分词后的关键词,作用是可以在搜索的时候不指定特定字段,从所有字段中检索。
1.5.5 对Analyzed的字段禁用Norms
Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:
"title " : { "type" : "string" , "norms" : { "enabled" : false}}
1.5.6 index_options设置
index_options用于控制在建立倒排索引过程中,哪些内容会被添加到倒排索引,例如,doc数量、词频、positions、offsets等信息,优化这些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。
不过在实际场景中,通常很难确定业务将来会不会用到这些信息,除非一开始方案就明确是这样设计的。
二、搜索速度的优化
2.1 为文件系统cache预留足够的内存
在一般情况下,应用程序的读写都会被操作系统“cache”(除了direct方式),cache保存在系统物理内存中(线上应该禁用swap),命中cache可以降低对磁盘的直接访问率。搜索很依赖对系统cache的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延迟。应该至少为系统cache预留一半的可用物理内存,更大的内存有更高的cache命中率。
2.2 使用更快的硬件
写入性能对CPU的性能更敏感,而搜索性能在一般情况下更多的是在于I/O能力,使用SSD会比旋转类存储介质好得多。尽量避免使用NFS等远程文件系统,如果NFS比本地存储慢3倍,则在搜索场景下响应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访问。
如果搜索类型属于计算比较多,则可以考虑使用更快的CPU。
2.3 文档模型
为了让搜索时的成本更低,文档应该合理建模。特别是应该避免join操作,嵌套(nested)会使查询慢几倍,父子(parent-child)关系可能使查询慢数百倍,因此,如果可以通过非规范化文档来回答相同的问题,则可以显著地提高搜索速度。
2.4 字段映射
有些字段的内容是数值,但并不意味着其总是应该被映射为数值类型,例如,一些标识符,将它们映射为keyword可能会比integer或long更好。
2.5 避免使用脚本
一般来说,应该避免使用脚本。如果一定要用,则应该优先考虑painless和expressions。
2.6 为只读索引执行force-merge
可以对旧数据,定时强制执行强制段合并,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引不存在多个分段时,每个分段会单独执行搜索再将结果合并,将只读索引强制合并为Lucene分段不仅可以优化搜索过程,对索引恢复速度也有好处。
2.7 尽量使用FilterContext
尽量使用Filter Context,利用缓存机制,减少不必要的算分
可以结合Profile,Explain API分析慢查询问题,持续优化数据建模。【严禁使用*开头通配符的Terms查询,会对集群造成比较大的损害】
2.8 优化分片
- 避免Over Sharing
- 一个查询需要访问每一个分片,分片过多,会导致不必要的查询开销
- 结合应用场景,控制单个分片的尺寸
- 搜索场景:20GB以内
- 日志场景:40GB以内
- Force-merge Read-only索引
- 使用基于时间序列的索引,将只读的索引force merge,减少segment数量
参考资料:
《Elasticsearch源码解析与优化实战》