欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

elasticsearch写入速度优化

程序员文章站 2022-07-05 13:08:59
...

        追求极致的写入速度时,很多是以牺牲可靠性和搜索实时性为代价的。有时候,业务上对数据可靠性和搜索实时性要求并不高,反而对写入速度要求很高,此时可以调整一些策略,最大化写入速度

         如果是集群首次批量导入数据,则可以将副本数设置为0,导入完毕再将副本数调整回去,这样副分片只需要复制,节省了数据同步的过程。

如果是实时数据写入,综合来说,提升写入速度从以下几方面入手:

  • 加大translog flush间隔,目的是降低iops、writeblock。
  • 加大index refresh间隔,除了降低I/O,更重要的是降低了segment merge频率。
  • 调整bulk请求。
  • 优化磁盘间的任务均匀情况,将shard尽量均匀分布到物理主机的各个磁盘。
  • 优化节点间的任务分布,将任务尽量均匀地发到各节点。
  • 优化Lucene层建立索引的过程,目的是降低CPU占用率及I/O,例如,禁用_all字段(ES6.0已经默认禁用,不去额外处理即可)。

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。

refresh_interval索引刷新间隔优化

默认情况下索引的refresh_interval为1秒,这意味着数据写1秒后就可以被搜索到,进而满足Elasticsearch的近实时查询,但是每次索引的refresh会产生一个新的Lucene段,这会导致频繁的segment merge行为,造成大量的磁盘io和内存占用,影响效率,如果不需要这么高的实时性,应该降低索引refresh周期,例如:

index.refresh_interval: 30s

segment merge段合并优化

segment merge操作对系统I/O和内存占用都比较高,从ES 2.0开始, merge行为不再由ES控制,而是由Lucene控制,在6.X版本中由以下配置控制:

index.merge.scheduler.max_thread_count

index.merge.policy.*

最大线程数max_thread_count的默认值如下:

Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))

以上是一个比较理想的值,当节点配置的cpu核数较高时,merge占用的资源可能会偏高,影响集群的性能,如果只有一块硬盘并且非 SSD,则应该把它设置为1,因为在旋转存储介质上并发写,由于寻址的原因,只会降低写入速度;如果Elasticsearch写路径配置多个磁盘,可以在磁盘数的范围内结合集群的负载取一个合适的值,可以通过下面的命令调整某个index的merge过程的并发度:

index.merge.scheduler.max_thread_count:2

merge策略index.merge.policy有三种:

  • tiered(默认策略);
  • log_byete_size;
  • log_doc;

目前我们使用默认策略,但是对策略的参数进行了一些调整。

索引创建时合并策略就已确定,不能更改,但是可以动态更新策略参数,可以不做此项调整。如果堆栈经常有很多merge,则可以尝试调整以下策略配置:

index.merge.policy.segments_per_tier

该属性指定了每层分段的数量,默认为10,取值越小则最终segment越少,因此需要merge的操作更多,可以考虑适当增加此值。其应该大于等于index.merge.policy.max_merge_at_once(默认为10)。

index.merge.policy.max_merged_segment

指定了单个segment的最大容量,默认为5GB,大于这个大小的segment,不用参与归并。forcemerge 除外,为了减少参与merge的segment的数量,减少磁盘IO以及内存占用,可以考虑适当降低此值,此场景适用于按天或者时间段建index的场景,当index变为只读后使用forcemerge进行为强制归并,在提高检索和Reindex效率的同时,减少内存的占用。

indexing buffer索引缓存优化

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如果配置成百分比,则下面两个参数即min与max生效,用来规约indexing buffer占用的实际内存的最大值和最小值。

在执行大量的写入操作时,indices.memory.index_buffer_size的默认设置可能不够,这和可用堆内存、单节点上的shard数量相关,可以考虑适当增大该值。

例如:

indices.memory.index_buffer_size:15%

(该配置为集群配置,配置项写在conf/Elasticsearch.yml中)

这也说明了为什么需要控制一个节点上shard的数量,数量越多,每个shard分配到的indexing buffer的内存就会越少,进而引发频繁的refresh,生成大量的segment,进而引发频繁的segment merge,严重影响I/O以及内存占用……

在控制单节点的shard数量的同时,需要对只读索引进行force_merge,对warm以及code数据进行shrink操作进行shard裁剪,在当前优化后并且还增加了indices.memory.index_buffer_size的值以后还是无法解决写入性能以及积压的问题,就需要考虑扩展硬件资源了

大量的写入考虑使用bulk请求

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

线程池的大小不建议随意改变,保持默认就好;队列大小的修改如下:

thread_pool.bulk.queue_size:500

(该配置为集群配置,配置项写在conf/Elasticsearch.yml中)

每个bulk请求的doc数量设定区间推荐为1k~1w,具体可根据业务场景选取一个适当的数量。

另外需要注意 bulk线程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端请求被拒绝,此时客户端会收到429错误(TOO_MANY_REQUESTS),客户端对此的处理策略应该是延迟重试。不可忽略这个异常,否则写入系统的数据会少于预期。即使客户端正确处理了429错误,我们仍然应该尽量避免产生reject。因此,在评估极限的写入能力时,客户端的极限写入并发量应该控制在不产生reject前提下的最大值为宜。如果发现在增加了队列长度仍然无法避免reject的情况下,说明数据的吞吐量已经超过了当前ES集群的能力,在已经进行了其他优化的前提下就应该考虑扩展硬件资源了

单节点磁盘间的任务均衡

首先在配置文件conf/Elasticsearch.yml中为path.data配置多个路径来使用多块磁盘,多磁盘带来的并行写的优势可以增加吞吐量,这对提升Elasticsearch的写入是很友好的;但是多磁盘写入可能会带来任务不均衡的问题,Elasticsearch在分配shard时,落到各磁盘上的 shard 可能并不均匀,这种不均匀可能会导致某些磁盘繁忙,利用率在较长时间内持续达到100%,而某些磁盘可能使用率很低甚至为0,这种不均匀达到一定程度会对写入性能产生负面影响。

对于此种场景,有两种策略可以考虑:

  • 简单轮询:在系统初始阶段,简单轮询的效果是最均匀的。
  • 基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询。

节点间的任务均衡

为了节点间的任务尽量均衡,数据写入客户端应该把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请求来处理数据写入,需要观察bulk请求在不同节点间的均衡性,可以通过cat接口观察bulk线程池和队列情况:

_cat/thread_pool

Index相关的优化

自动生成doc ID:

通过ES写入流程可以看出,写入doc时如果外部指定了id,则ES会先尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可以避免这个环节。

调整index的Mappings:

(1)减少字段数量,对于不需要建立索引的字段,不写入ES。

(2)将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义。

(3)减少字段内容长度,如果原始数据的大段内容无须全部建立索引,则可以尽量减少不必要的内容。

(4)使用不同的分析器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异。

调整_source字段:

_source 字段用于存储 doc 原始数据,对于部分不需要存储的字段,可以通过 includes excludes过滤,减少_source字段的存储量,或者将_source禁用(此时Elasticsearch的update,update by query以及Reindex接口无法使用),一般用于索引和数据分离的实现方案(HBase+es,HBase存储数据,es存储索引,HBase与es同步采用HBase的协处理器coprocessor实现)。这样可以降低 I/O 的压力,不过实际场景中大多不会禁用_source,而即使过滤掉某些字段,对于写入速度的提升作用也不大(滤掉某些字段更多的是为了节省硬盘空间,降低存储成本),满负荷写入情况下,基本是 CPU 先跑满了,瓶颈在于CPU。

笔者建议以下设计方案:

Elasticsearch以及_source存储业务需要索引的关键字段以及对应HBase原始数据的rowkey,保证在大多数的业务场景下可以通过Elasticsearch的一次查询即可以返回业务需要的字段,满足业务需求;在个别场景下需要查询详细信息时,通过Elasticsearch和HBase关联的rowkey去HBase中get详细数据。_source字段的保留也为后续的Elasticsearch的扩展性(update以及Reindex)留下了接口。

禁用_all字段:

6.X版本已经默认禁用,不需要做任何配置,之前的版本使用可以在mapping中将enabled设置为false来禁用_all字段。禁用_all字段可以明显降低对CPU和I/O的压力。

对Analyzed的字段禁用Norms:

开启norms之后,每个document的每个field需要一个字节存储norms。对于keyword 类型的字段默认关闭 norms对于 text 类型的字段而言是默认开启norms的,因此对于不需要评分的 text 类型的字段,可以禁用norms:

"title": {"type": "text","norms": {"enabled": false}}

参考配置与分析

下面是笔者的线上环境使用的全局模板和配置文件的部分内容,省略掉了节点名称、节点列表等基础配置字段,仅列出与写入速度优化的相关内容,其他内容会在接下来的章节进行讲解。

Elasticsearch索引的建立方式建议使用rollover Api借助模板进行索引的建立,我们把各个索引通用的配置写到了模板中,作为通用的默认配置进行使用

{

       "template": "*",//模板匹配全部的索引

       "order": 0,// 具有最低的优先级,让用户定义的模板有更高的优先级,以覆盖这个模板中的配置

       "settings": {

              "index.merge.policy.max_merged_segment": "2gb",//大于2g的segment不参与merge

              "index.merge.policy.segments_per_tier": "24",//每层分段的数量为24,增加每个segment的大小,减少segment merge的发生的次数

              "index.number_of_replicas": "1",//数据1备份,容灾并提升数据查询效率

              "index.number_of_shards": "24",//每个index有24个shard,这个数字需要根据数据量进行评估,原则上是尽量的少,毕竟多一个shard对Elasticsearch的压力也会增加很多,shard数量设计原则可以参考index的shard规划原则

              "index.optimize_auto_generated_id": "true",//自动生成doc ID

              "index.refresh_interval": "30s",//refresh的自动刷新间隔,刷新后数据可以被检索到,根据业务的实时性需求来配置该值

              "index.translog.durability": "async",//异步刷新translog

              "index.translog.flush_threshold_size": "1024mb",//translog强制flush的大小阈值

              "index.translog.sync_interval": "120s",//translog定时刷新的间隔,可以根据需求调节该值

              "index.unassigned.node_left.delayed_timeout": "5d"//该配置可以避免某些Rebalancing操作,该操作会带来很大的开销,如果节点离开后马上又回来(如网络不好,重启等),则该开销完全没有必要,所以在集群相对稳定以及运维给力的前提下,尽量增大该值以避免不必要的资源开销

       }

}

elasticsearch.yml中的配置:

indices.memory.index_buffer_size: 15%//当写入压力过大时,可以适当增加该值,但是如果增加完该值并优化过后还无法解决写入积压的问题,则考虑增加硬件资源

thread_pool.bulk.queue_size:500//修改bulk队列大小,这个不要太大,否则会引发严重的FGC问题,建议不超过1000