Elasticsearch优化——写入优化
Elasticsearch优化——写入优化
在ES的默认设置下,是综合考虑数据可靠性、搜索实时性、写入速度等因素的。当离开默认设置、追求极致的写入速度时,很多是以牺牲可靠性和实时搜索性为代价的。有时候,业务上对数据可靠性和搜索实时性要求并不高,反而对写入熟读要求很高,此时可以调整一些策略,最大化写入速度。
接下来的优化基于集群正常运行的前提下,如果集群首次批量导入数据,则可以将副本数设置为0,导入完毕再将副本数调整回去,这样副本需要复制,节省索引过程。
综合来说,提升写入速度从以下几方面入手:
- 加大translog flush间隔,目的是降低iops、writeblock。
- 加大index refresh间隔,除了降低I/O,更重要的是降低segment merge频率。
- 调整bulk请求。
- 优化磁盘间的任务均匀情况,将shard尽量均匀分布到物理主机的各个磁盘。
- 优化节点间的任务分布,将任务尽量均匀的分发到各个节点。
- 优化Lucene层建立索引的过程,目的是降低CPU占用率及I/O,例如,禁用 _all字段。
1. translog flush间隔调整
从ES 2.x开始,在默认设置下,translog的持久化策略为:每个请求都“flush”。对应配置项如下:
index.translog.durability: request
这是影响ES写入速度的最大因素。但是只有这样,写操作才有可能是可靠的。如果系统可以接受一定概率的数据丢失(例如,数据写入主分片成功,尚未复制到副本分片,主机断电。由于数据既没有刷到Lucene,translog也没有刷盘,恢复是translog中没有这个数据,数据丢失),则调整translog持久化策略为周期性和一定大小的时候"flush",例如:
#设置为async表示translog的刷盘策略按sync_interval配置指定的时间周期进行
index.translog.durability: async
#加大translog刷盘间隔时间。默认5s,不可低于100ms。
index.translog.sync_interval: 120s
#超过这个大小会导致refresh操作,产生新的Lucene分段。默认值512MB
index.translog.flush_threshold_size: 1024mb
2. 索引刷新间隔refresh_interval
默认情况下索引的refresh_interval为1秒,这意味着数据写入1秒后就可以被搜索到,每次索引的refresh会产生一个新的Lucene段,这会导致频繁的segment merge行为,如果不需要这么高的搜索实时性应该降低索引refresh周期,例如:
index.refresh_interval: 120s
或者
PUT /my_index
{
"settings":{
"refresh_interval": "120s"
}
}
3. 段合并优化
segment merge操作对系统I/O和内存占用都比较高,从ES 2.0开始,merge行为不再由ES控制,而是Lucene控制。因此以下配置被删除:
indices.store.throttle.type
indices.store.throttle.max_bytes_per_sec
index.store.throttle.type
index.store.throttle.max_bytes_per_sec
改为以下调整开关:
index.merge.scheduler.max_thread_count: 4
index.merge.policy.*
最大线程数max_thread_count
的默认值如下:
max_thread_count = Math.max(1, Math.min(4, Runtime.getRuntime().avaliableProcessors() / 2))
以上是一个比较理想的值,如果只有一个硬盘并且并非SSD,则应该把它设置为1,因为在旋转存储介质上并发写,由于寻址的原因,只会降低写入速度。
merge策略index.merge.policy有三种:
- tiered(默认策略);
- log_byte_size;
- log_doc。
每个策略的具体描述可以参考:
目前我们使用默认策略,但是对策略的参数进行了调整。
索引创建时合并策略就已经确定,不能更改,但是可以动态更新策略参数。如果堆栈经常有很多merge,则可以尝试调整以下策略配置:
-
index.merge.policy.segments_per_tier
该属性指定了每层分段的数量,取值越小则segment越少,因此需要merge的操作更多,可以考虑适当增加此值。默认为10,其应该大于等于
index.merge.poliycy.max_merge_at_once
。 -
index.merge.policy.max_merged_segment
指定了单个segment的最大容量,默认为5GB,可以考虑适当降低此值。
4. indexing buffer
ndex buffer
在为doc建立索引时使用,当缓冲满时会刷入磁盘,生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成新segment的机会。每个shard有自己的ndexing 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数量相关,可以考虑适当增大该值。
5. 使用bulk请求
批量写比一个索引请求只写单个文档的效率高得多,但是要注意bulk请求的整体字节数不要太大,太大的请求可能会给集群带来内存压力,因此每个请求最好避免超过几十兆字节,即使较大的请求看上去执行的更好。
5.1 bulk线程池和队列
建立索引的过程属于计算密集型任务,应该使用固定大小的线程池,来不及处理的任务放入队列。线程池最大线程数量应配置为CPU核心数+1,这也是bulk线程池的默认配置,可以避免过多的上下文切换。队列大小可以适当增加,但一定要严格控制大小,过大的队列导致较高的GC压力,并可能导致FGC频繁发生。
5.2 并发执行bulk请求
bulk写请求是个长任务,为了给系统增加足够的写入压力,写入过程应该多个客户端、多线程的并行执行,。如果要验证系统的极限写入能力,那么目标就是把CPU压满。磁盘util、内存等一般都不是瓶颈。如果CPU没有压满,则应该提高写入端的并发数量。但是要注意bulk线程池队列的reject情况,出现regect代表ES的bulk队列满了,客户端请求被拒绝,此时客户端收到429错误(TOO_MANY_REQUESTS
),客户端对此的处理策略应该是延时重试。不可忽略这个异常,否则写入系统的数据会少于预期。即使客户端正确处理了429错误,我们仍然应该尽量避免产生reject。因此,在评估极限的写入能力时,客户端的极限写入并发量应该控制在不产生reject前提下的最大值为宜。
6. 磁盘间的任务均衡
如果不熟方案是为path.data配置多个路径来使用多块磁盘,ES通过下面2种策略均衡的写入不同的磁盘:
- 简单轮询:在系统初始化阶段,简单轮询的效果是最均匀的。
- 基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询。
7. 节点间的任务均衡
为了节点间的任务尽量均衡,数据写入客户端应该把bulk请求轮询发送到各个节点,当使用REST API的bulk接口发送数据时,客户端将会轮询发送到集群节点,在创建客户端对象时添加节点。
8. 索引过程调整和优化
8.1 自动生成doc ID
通过ES写入流程可以看出,写入doc时如果外部指定了id,则es会尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘操作,通过自动生成doc ID可以避免这个环节。
8.2 调整字段mappings
- 减少字段数量,对于不需要建立索引的字段不写入ES。
- 将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有意义。
- 减少字段内容长度,如果原始数据的大段内容无需建立索引,则尽量减少不必要的内容。
- 使用不同的分析器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异。
8.3 调整_source字段
_source字段用于存储doc原始数据,对于不需要存储的字段,可以通过includes excludes过滤,或者将 _source禁用,一般用于索引和数据分离。
这样可以降低I/O的压力,不过实际场景中大多不会禁用 _source,即使过滤掉某些字段,对于写入速度提升作用也不大,满负荷写入情况下,基本是CPU先跑满,瓶颈在于CPU。
8.4 禁用_all字段
从 ES 6.0开始, _all字段默认不启用,而在此前的版本中, _all字段默认是开启的。 _all字段中包含所有字段分词后的关键词,作用是可以在搜索的时候不指定特定字段,从所有字段中检索。ES 6.0默认禁用 _all的主要原因有以下几点:
- 由于需要从其他的全部字段复制所有字段值,导致 _all字段占用非常大的空间。
- _all字段有自己的分析器,在进行某些查询时(例如,同义词),结果不符合预期,因为没有匹配同一个分析器。
- 由于数据重复引起的额外建立索引的开销。
- 想要调试时,其内容不容易检查。
- 有些用户甚至不知道存在这个字段,导致了查询混乱。
- 有更改的替代方案。
在ES 6.0之前的版本中,可以在mapping中将enabled设置为false来禁用 _all字段:
PUT /my_index
{
"mappings":{
"my_type":{
"_all":{
"enabled":false
}
}
}
}
禁用 _all字段可以明显降低对CPU和I/O的压力。
8.5 对Analyzed的字段禁用Noms
Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:
PUT my_index/_mapping/my_type
{
"properties":{
"title":{
"type":"keyword",
"norms":{
"enabled":false
}
}
}
}
8.6 index_options设置
index_options
用于控制在建立倒排索引的过程中,哪些内容会被添加到倒排索引,例如,doc数量、词频、options、offset等信息,优化这些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。
不过在实际场景中,通常很难确定业务将来会不会用到这些信息,除非一开始方案就明确是这样设计的。
9. 参考配置
索引级的设想需要写在模板中,或者在创建索引是指定。
{
"template":"*",
"order":0,
"settings":{
//单个分段的最大容量
"index.merge.policy.max_merged_segment":"2gb",
//每层分段的数量,值越小,合并操作越多,可以适当增加,默认10
//不要小于max_merge_at_once
"index.merge.policy.segments_per_tier":"24",
//索引刷入操作系统缓存时间,默认1秒
"index.refresh_interval":"120s",
//索引刷入操作系统缓存策略,默认request,改为定时刷新
"index.translog.durability":"async",
//索引刷入磁盘的translog的阈值,默认512mb,提交commit point
"index.translog.flush_threshold_size":"512mb",
//日志刷新磁盘的时间,默认5s
"index.translog.sync_interval":"120s",
//索引分配延迟时间,默认1分钟
"index.unassigned.node_left.delayed_timeout":"5d"
}
}
elasticsearch.yml中的配置:
#索引buffer大小,默认可使用堆的10%
indices.memory.index_buffer_size: 30%