ES原理和优化

发布时间 2023-04-03 11:12:54作者: edclol

如果希望把 ES 作为大型数据库使用,建议注意两个方面的问题:

1、使用场景方面

我们把 ES 定位为分布式的搜索分析型数据库,并通过实际业务验证,可以非常好的支持下列场景:

  • 全文搜索ES 最初的目标场景,非常多的大型互联网公司都在基于 ES 搭建自己的核心搜索,包含 Git 的代码搜索、美团的电商搜索、头条的视频搜索等。ES 在 DB-Engines 的 Search Engine 领域也是常年排名第一。
  • 日志实时分析:这是推动 ES 快速发展的场景,从官方统计数字、云上运营经验看,占据了 ES 使用场景的 70%+。Elastic Stack 提供的完整日志解决方案,已经助力 ES 成为日志实时分析的开源首先方案。
  • 时序数据处理我们从 2017 年中开始探索使用 ES 作为时序数据解决方案,逐步完善 ES 的时序处理能力,相比开源的时序数据库,在写入吞吐、查询性能等方面已经追齐、甚至超越竞品,并且很好的支持了腾讯云监控单集群每秒千万级的业务吞吐。受腾讯在时序场景的启发,Elastic 官方也在探索支持 Metrics、APM 等时序场景。
  • 其他:社区和公司内也有其他团队把 ES 应用于 OLAP 分析、文档数据库等场景,有需要的团队可结合场景需求评估测试。

由于不支持事务,以及在建立索引方面的计算 / 存储成本,ES 并不适合于 OLTP 及 离线日志数据处理等场景。

2、大规模使用方面

开源 ES 的易用性和完整的日志解决方案,推动 ES 非常快速的发展。但不少用户在大规模应用 ES 时遇到了不少问题,例如稳定性方面有集群易雪崩、压力不均等,成本方面硬盘、内存开销高等。TencentES 在兼容开源 API 的同时,在内核方面做了非常多的优化,用于支撑腾讯云 ES 服务、公司内部大量核心业务,包含日志实时分析、全文搜索、时序数据处理等众多场景。具体内核优化内容简单介绍如下:

可用性

  • 自保护机制:提供权限认证、漏斗限流、内存限流、[多租户隔离]等多层级、全链路的自我保护
  • 均衡策略优化:支持节点间、多盘间的分片均衡,避免容量、读写压力不均
  • 集群扩展性:单集群达到千级节点,支持 50w 分片,索引创建速度控制在秒级,相比开源提升 30+ 倍
  • Bug Fix:修复 Master 阻塞、[分布式死锁]等已知 Bug,提升集群稳定性

成本

  • 堆内存优化:通过 LRU+Off Heap 技术降低堆内存使用 80%,单节点支撑容量由 3T 提升至 20T
  • [冷热分离]:集群内支持多层级存储设备混部,降低用户成本
  • Rollup:支持高性能 Rollup 计算,降低存储成本,数量级级别提升分析性能

性能

  • Merge重构:支持时间序 Merge、冷索引自动 merge 等,搜索等场景查询性能提升 50%+
  • [查询执行计划]优化:基于 CBO 模型优化 Range 查询,长时间范围 Range 查询性能提升 10+ 倍
  • 写入性能优化:降低主键唯一性检查开销,支持存储裁剪,性能提升 50%+

DB与ES本质上是属于不同应用领域的数据库产品,混合应用在一起主要面临2个问题 :

1、同步实时性,数据在DB更新之后,需要多久才能更新到 Elasticsearch,多久的时间是应用系统可以接受的范围,一般需要控制在1s以内,如果是分钟以上,那这就属于离线同步。

2、数据一致性,数据频繁在DB变更修改,更新到Elasticsearch之后如何保证数据与DB一致,在容许的时间范围内应用系统查询的数据有效的,可接受的,如果变更出现覆盖等,那数据是无效的,应用系统是不可接受的。

在数据同步方面,主要有3种同步模式

1、推送模式,数据源将变更数据推送到目标源,如RabbitMQ产品,服务端会主动MQ发送到客户端。

2、拉取模式,目标源定时去数据源拉取变更数据,如Mysql数据库的数据主从同步机制,Slave会去Master拉取变更数据。

3、推拉结合,数据源与目标源之间,既有推送方式,也有拉取方式,此种模式一般会借助于中间媒介实现,如基于Kafka产品的日志应用,数据源(采集端)会将日志数据发送到Kafka集群,目标源会定期的从Kafka拉取数据更新。

从技术层面看,DB同步到ES有好多种方式

1、同步双写,更新DB时同步更新ES。此技术方案最简单,附带问题最多,数据冲突,数据覆盖,数据丢失,处处是坑,谨慎选择。

2、异步双写,更新DB之后,记录一条MQ,MQ通知消费端,消费端反向查询DB数据,最后更新到ES。此技术方案与业务系统耦合严重,需要更加业务需求独立编写,且每个业务都需要专门编写相关程序,非常不利于快速响应需求。

3、CDC,全称Change Data Capture,变更数据捕捉,从数据库内部捕捉变更数据,将变更数据推送到中间程序中,中间程序逻辑实现同步推送到ES。CDC机制速度极快,数据精准,且与应用程序耦合少,可抽象脱离业务系统,适合大规模使用。

1.写入优化

日志属于写多读少的业务场景,对写入速度要求很高。拿我们其中一个集群来说,单集群日志量达到百TB,每秒钟日志写入量达到10W条。

数据写入,主要有三个动作:flush、refresh和merge。通过调整它们的行为,即可在性能和数据可靠性之间进行权衡。

1.1 translog异步化

首先,ES需要写一份translog,它类似于MySQL中的redolog,为的是避免在断电的时候数据丢失。ES默认每次请求都进行一次flush,但对于日志来说,这没有必要,可以将这个过程改为异步的,刷盘间隔为60秒。参数如下:

curl-H"Content-Type: application/json"-XPUT'http://localhost:9200/_all/_settings?preserve_existing=true'-d'{
  "index.translog.durability" : "async",
  "index.translog.flush_threshold_size" : "512mb",
  "index.translog.sync_interval" : "60s"
}'

这可以说是最重要的一步优化了,对性能的影响最大,但在极端情况下会有丢失部分数据的可能。对于日志系统来说,是可以忍受的。

1.2 增加refresh间隔

除了写translog,ES还会将数据写入到一个缓冲区中。但是注意了!此时,缓冲区的内容是无法被搜索到的,它还需要写入到segment里面才可以,也就是刷新到lucence索引里面。这就是refresh动作,默认1秒。也就是你写入的数据,大概率1秒之后才会被搜索到。

这也是为什么ES不是实时搜索系统的原因,它从数据写入到数据读出,一般是有一个合并过程的,有一定的时间差。

通过index.refresh_interval可以修改这个刷新间隔。

对于日志系统来说,当然要把它调大一点啦。xjjdog这里调整到了120s,减少了这些落到segment的频率,I/O的压力自然会小,写入速度自然会快。

curl-H"Content-Type: application/json"-XPUT'http://localhost:9200/_all/_settings?preserve_existing=true'-d'{
  "index.refresh_interval" : "120s"
}'

1.3 merge

merge其实是lucene的机制,它主要是合并小的segment块,生成更大的segment,来提高检索的速度。

原因就是refresh过程会生成一大堆小segment文件,数据删除也会产生空间碎片。所以merge,通俗来讲就像是碎片整理进程。像postgresql等,也有vaccum进程在干同样的事。

显而易见,这种整理操作,既浪费I/O,又浪费CPU。

如果你的系统merge非常频繁,那么调整merge的块大小和频率,是一个比较好的方法。

2.读取优化

2.1 指定路由

如果你向ES里写数据,那么它会为你设置一个离散的隐藏ID,落到哪个分片,是不一定的。如果你根据一个查询条件查询数据,你设置了6个shards的话,它要查询6次才行。如果能够在路由的时候就知道数据在哪个分片上,查询速度自然会上升,这就要求我们在构造数据的时候,人工指定路由规则。它的实际运行规则如下:

shard = hash(routing) % number_of_primary_shards

比如,一个查询会变成这样。

GET my-index-000001/_search
{
  "query": {
    "terms": {
      "_routing": [ "user1" ] 
    }
  }
}

当然,如果你的查询维度较多,又对数据的查询速度有非常高的有求,根据routing存放多份数据是一个比较好的选择。

2.2 rollover冷热分离

rollover根据索引大小,文档数或使用期限自动过渡到新索引。当rollover触发后,将创建新索引,写别名将更新为指向新索引,所有后续更新都将写入新索引,比如indexname-000001.这种模式。

从rollover这个名字可以看出来,它和Java的log日志有一定的相似之处,比如Log4j的RollingFileAppender。

当索引变的非常大,通常是几十GB,那它的查询效率将变的非常的低,索引重建的成本也较大。实际上,很多索引的数据在时间维度上有较为明显的规律,一些冷数据将很少被用到。这个时候,建立滚动索引将是一个比较好的办法。

滚动索引一般可以与索引模板结合使用,实现按一定条件自动创建索引,ES的官方文档有具体的_rollover建立方法。

2.3 使用BoolQuery替代TermQuery

Bool查询现在包括四种子句,must、filter、should和must_not。Bool查询是true、false对比,而TermQuery是精确的字符串比对,所以如果需求相似,BoolQuery自然会快于TermQuery。

2.4 将大查询拆成分段查询

有些业务的查询比较复杂,我们不得不拼接一张非常大的宽表放在ES中,这有两个比较明显的问题。

  1. 宽表的数据往往需要从其他数据源中回查拼接而成,数据更新时对源库或者ES本身都有较大的压力
  2. 业务的查询JSON需要书写的非常复杂,查询效率未知,一次查询锁定的内存过高,无法进行深入优化

其实,宽表不论在RDBMS中还是ES中,都会与复杂的查询语句有关,其锁定时间都较长,业务也不够灵活。

应对这种场景的策略,通常将复杂的数据查询,转移到业务代码的拼接上来。比如,将一段非常冗长的单条查询,拆分成循环遍历的100条小查询。所有的数据库都对较小的查询请求有较好的响应,其整体性能整体上将优于复杂的单条查询。

这对我们的ES索引建模能力和编码能力提出了挑战。毕竟,在ES层面,互不相关的几个索引,将作为整体为其他服务提供所谓的数据中台接口。

2.5 增加第一次索引的速度

很多业务的索引数据往往来自于MySQL等传统数据库,第一次索引往往是全量索引,后面才是增量索引。必要的时候,也会进行索引的重建,大量的数据灌入造成了ES的索引速度建立缓慢。

为了缓解这种情况,建议在创建索引的时候,把副本数量设置成1,即没有从副本。等所有数据索引完毕,再将副本数量增加到正常水平。

这样,数据能够快速索引,副本会在后台慢慢复制。

3.通用优化

当然,我们还可以针对ES做一些通用的优化。比如,使用监控接口或者trace工具,发现线程池有明显的瓶颈,则需要调整线程池的大小。

具体的优化项如下。

3.1 线程池优化

新版本对线程池的配置进行了优化,不需要配置复杂的search、bulk、index线程池。有需要配置下面几个就行了:thread_pool.get.size, thread_pool.write.size, thread_pool.listener.size, thread_pool.analyze.size。具体可观测_cat/thread_pool接口暴露的数据进行调整。

3.2 物理冷热分离

上面的rollover接口,我们可以实现索引滚动。但是如何将冷数据存放在比较慢但是便宜的节点上?如何将某些索引移动过去?

ES支持给节点打标签,具体方式是在elasticsearch.yml文件中增加一些属性。比如:

//热节点
node.attr.temperature: hot 
//冷节点
node.attr.temperature: cold 

节点有了冷热属性后,接下来就是指定数据的冷热属性,来设置和调整数据分布。ES提供了index shard filtering功能来实现索引的迁移。

首先,可以对索引也设置冷热属性。

PUT hot_data_index/_settings
{
    "index.routing.allocation.require.temperature": "hot"
}

这些索引将自动转移到冷设备上。我们可以写一些定时任务,通过_cat接口的数据,自动的完成这个转移过程。

3.2 多磁盘分散I/O

其实,可以通过配置多块磁盘的方式,来分散I/O的压力,但容易会造成数据热点集中在单块磁盘上。

ES支持在一台机器上配置多块磁盘,所以在存储规模上有更大的伸缩性。在配置文件中,配置path.data属性,即可挂载多块磁盘。

path.data : /data1, /data2, /data3

值得注意的是,如果你是在扩容,那么就需要配合reroute接口进行索引的重新分配。

3.3 减少单条记录的大小

Lucene的索引建立过程,非常耗费CPU,可以减少倒排索引的数量来减少CPU的损耗。第一个优化就是减少字段的数量;第二个优化就是减少索引字段的数量。具体的操作,是将不需要搜索的字段,index属性设置为not_analyzed或者no。至于_source和_all,在实际调试中效果不大,不再赘述。

End

ES的使用越来越广泛,从ELKB到APM,从NoSQL到搜索引擎,ES在企业中的地位也越来越重要。本文通过分析ES写入和读取场景的优化,力求从原理到实践层面,助你为ES加速。希望你在使用ES时能够更加得心应手。

通常,一个ES集群对配置的要求是较高的,尤其是APM等场景,甚至会占到PaaS平台的1/3资源甚至更多。ES提供了较多的配置选项,我们可以根据应用场景,调整ES的表现,使其更好的为我们服务。

关于Easticsearch写入原理,是一个面试的重灾区,同时也是生产环境非常有用的知识点。

在这个问题上,可以延伸出很多相关的面试题,比如:

  • 你了解文档的写入、删除过程吗
  • ES 如何保证数据的写入一致性
  • 文档写入超时可能是哪些原因导致的
  • 如何保证文档的写入实时性
  • 海量数据的下,如何保证数据的吞吐性能
  • 如何保证 ES 的高并发写入能力
  • 如何提高 ES 的检索性能

是的,检索性能是可以通过写入调优来提升的。这是典型的 以 A 换 B 的调优手段。下面具体来说一下 ES 的写入原理和常见调优手段

1、ES 的写入过程

1.1 ES支持四种对文档的数据写操作

  • create:如果在PUT数据的时候当前数据已经存在,则数据会被覆盖,如果在PUT的时候加上操作类型create,此时如果数据已存在则会返回失败,因为已经强制指定了操作类型为create,ES就不会再去执行update操作。比如:PUT /pruduct/_create/1/ ( 老版本的语法为 PUT /pruduct/_doc/1/_create )指的就是在索引product中强制创建id为1的数据,如果id为1的数据已存在,则返回失败。
  • delete:删除文档,ES对文档的删除是懒删除机制,即标记删除。
  • index:在ES中,写入操作被称为Index,这里Index为动词,即索引数据为将数据创建在ES中的索引,后面章节中均称之为“索引数据”。
  • update:执行partial update(全量替换,部分替换)

1.2 写流程

ES中的数据写入均发生在Primary Shard,当数据在Primary写入完成之后会同步到相应的Replica Shard。下图演示了单条数据写入ES的流程:

图片写入流程

以下为数据写入的步骤:

  1. 客户端发起写入请求至node 4
  2. node 4 通过文档 id 在路由表中的映射信息确定当前数据的位置为分片0,分片0的主分片位于node 5,并将数据转发至node 5。
  3. 数据在node 5写入,写入成功之后将数据的同步请求转发至其副本所在的node 4和node 6上面,等待所有副本(具体等待多少副本取决于wait_for_active_shards的配置值)数据写入成功之后node 5 将结果报告node 4,并由 node 4 将结果返回给客户端,报告数据写入成功。

在这个过程中,接收用户请求的节点是不固定的,上述例子中,node 4 发挥了协调节点和客户端节点的作用,将数据转发至对应节点和接收以及返回用户请求。

数据在由 node4 转发至 node5的时候,是通过以下公式来计算,指定的文档具体在那个分片的

shard_num = hash(_routing) % num_primary_shards

其中,_routing 的默认值是文档的 id。

1.3 写一致性策略

ES 5.x 之后,一致性策略由 wait_for_active_shards 参数控制:

即确定客户端返回数据之前必须处于active 的分片分片数(包括主分片和副本),默认为 wait_for_active_shards = 1,即只需要主分片写入成功,设置为 all或任何正整数,最大值为索引中的分片总数 ( number_of_replicas + 1 )。如果当前 active 状态的副本没有达到设定阈值,写操作必须等待并且重试,默认等待时间30秒,直到 active 状态的副本数量超过设定的阈值或者超时返回失败为止。

执行索引操作时,分配给执行索引操作的主分片可能不可用。造成这种情况的原因可能是主分片当前正在从网关恢复或正在进行重定位。默认情况下,索引操作将在主分片上等待最多 1 分钟,然后才会失败并返回错误。

2、ES 的写入原理

2.1 图解文档的写入原理

图片

ES 文档写入原理

2.2 Translog

对索引的修改操作在会 Lucene 执行 commit 之后真正持久化到磁盘,这个过程是非常消耗资源的,因此不可能在每次索引操作或删除操作后执行。Lucene 提交的成本太高,无法对每个单独的更改执行,因此每个分片副本还将操作写入其 事务日志,也就是 translog 。所有索引和删除操作在被内部 Lucene 索引处理之后但在它们被确认之前写入到 translog。如果发生崩溃,当分片恢复时,已确认但尚未包含在最后一次 Lucene 提交中的最近操作将从 translog 中恢复。

Elasticsearch Flush 是Lucene 执行 commit 并开始写入新的 translog 的过程。刷新是在后台自动执行的,以确保 translog 不会变得太大,这将导致在恢复期间重放其操作需要相当长的时间。手动执行刷新的能力也通过 API 公开,但是一般并不需要。

translog 中的数据仅在 translog 被执行 fsync 和 commit 时才会持久化到磁盘。如果发生硬件故障或操作系统崩溃或 JVM 崩溃或分片故障,自上次 translog 提交以来写入的任何数据都将丢失。

默认情况下,index.translog.durability设置为意味着 Elasticsearch 仅在 translog在主分片和每个副本上 request 成功编辑并提交后,才会向客户端报告索引、删除、更新或批量请求的成功。fsync 如果 index.translog.durability 设置为 async then Elasticsearch fsync并仅提交 translog index.translog.sync_interval,这意味着当节点恢复时,在崩溃之前执行的任何操作都可能丢失。

以下可动态更新的每个索引设置控制 translog 的行为:

  • index.translog.sync_interval

无论写入操作如何,translog 默认每隔 5sfsync 写入磁盘并 commit 一次,不允许设置小于 100ms 的提交间隔。

  • index.translog.durability

是否 fsync在每次索引、删除、更新或批量请求后提交事务日志。此设置接受以下参数:

      • request(默认):fsync并在每次请求后提交。如果发生硬件故障,所有确认的写入都已经提交到磁盘。
      • async:fsync 并在后台提交每个sync_interval`. 如果发生故障,自上次自动提交以来所有确认的写入都将被丢弃
  • index.translog.flush_threshold_size

``translog 存储所有尚未安全保存在 Lucene 中的操作(即,不是 Lucene 提交点的一部分)。尽管这些操作可用于读取,但如果分片停止并必须恢复,则需要重播它们。此设置控制这些操作的最大总大小,以防止恢复时间过长。一旦达到最大大小,就会发生刷新,生成一个新的 Lucene 提交点。默认为 512mb.

2.3 Refresh

2.3.1 概念和原理

内存索引缓冲区(图 1)中的文档被写入新段(图 2)。新段首先写入文件系统缓存(这个过程性能消耗很低),然后才刷新到磁盘(这个过程则代价很低)。但是,在文件进入缓存后,它可以像任何其他文件一样打开和读取。

图片

Lucene 允许写入和打开新的段,使它们包含的文档对搜索可见,而无需执行完整的提交。这是一个比提交到磁盘更轻松的过程,并且可以经常执行而不会降低性能。

图片

在 Elasticsearch 中,这个写入和打开新段的过程称为刷新。刷新使自上次刷新以来对索引执行的所有操作都可用于搜索。

2.3.2 设置刷新间隔

index.refresh_interval:多久执行一次刷新操作,这使得对索引的最近更改对搜索可见。默认为 1s. 可以设置 -1 为禁用刷新。

并不是所有的情况都需要每秒刷新。比如 Elasticsearch 索引大量的日志文件,此时并不需要太高的写入实时性, 可以通过设置 refresh_interval ,增大刷新间隔来降低每个索引的刷新频率,从而降低因为实时性而带来的性能开销。进而提升检索效率。

POST <index_name>{    "settings": {    "refresh_interval": "30s"  }}

2.3.3 强制对索引刷新

POST <target>/_refresh
GET <target>/_refresh
POST /_refresh
GET /_refresh

2.4 Flush

刷新数据流或索引是确保当前仅存储在 Traslog 中的任何数据也永久存储在 Lucene 索引中的过程。重新启动时,Elasticsearch 会将所有未刷新的操作从事务日志重播到 Lucene 索引,以使其恢复到重新启动前的状态。Elasticsearch 会根据需要自动触发刷新,使用启发式算法来权衡未刷新事务日志的大小与执行每次刷新的成本。

一旦每个操作被刷新,它就会永久存储在 Lucene 索引中。这可能意味着不需要在事务日志中维护它的额外副本。事务日志由多个文件组成,称为 generation

,一旦不再需要,Elasticsearch 将删除任何生成文件,从而释放磁盘空间。

也可以使用刷新 API 触发一个或多个索引的刷新,尽管用户很少需要直接调用此 API。如果您在索引某些文档后调用刷新 API,则成功响应表明 Elasticsearch 已刷新在调用刷新 API 之前索引的所有文档。

2.5 Merge

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

图片

Elasticsearch 中的一个 shard 是一个 Lucene 索引,一个 Lucene 索引被分解成段。段是存储索引数据的索引中的内部存储元素,并且是不可变的。较小的段会定期合并到较大的段中,并删除较小的段

图片

合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。

3、写入性能调优

3.1 基本原则

写性能调优是建立在对 Elasticsearch 的写入原理之上。ES 数据写入具有一定的延时性,这是为了减少频繁的索引文件产生。默认情况下 ES 每秒生成一个 segment 文件,当达到一定阈值的时候 会执行merge,merge 过程发生在 JVM中,频繁的生成 Segmen 文件可能会导致频繁的触发 FGC,导致 OOM。为了避免避免这种情况,通常采取的手段是降低 segment 文件的生成频率,手段有两个,一个是 增加时间阈值,另一个是增大 Buffer的空间阈值,因为缓冲区写满也会生成 Segment 文件。

生产经常面临的写入可以分为两种情况:

高频低量:高频的创建或更新索引或文档一般发生在 处理 C 端业务的场景下。

低频高量:一般情况为定期重建索引或批量更新文档数据。

在搜索引擎的业务场景下,用户一般并不需要那么高的写入实时性。比如你在网站发布一条征婚信息,或者二手交易平台发布一个商品信息。其他人并不是马上能搜索到的,这其实也是正常的处理逻辑。这个延时的过程需要处理很多事情,业务层面比如:你的信息需要后台审核。你发布的内容在搜索服务中需要建立索引,而且你的数据可能并不会马上被写入索引,而是等待要写入的数据达到一定数量之后,批量写入。这种操作优点类似于我们快递物流的场景,只有当快递数量达到一定量级的时候,比如能装满整个车的时候,快递车才会发车。因为反正是要跑一趟,装的越多,平均成本越低。这和我们数据写入到磁盘的过程是非常相似的,我们可以把一条文档数据看做是一个快递,而快递车每次发车就是向磁盘写入数据的一个过程。这个过程不宜太多,太多只会降低性能,就是体现在运输成本上面。而对于我们数据写入而言就是体现在我们硬件性能损耗上面。

3.2 优化手段

以下为常见 数据写入的调优手段,写入调优均以提升写入吞吐量和并发能力为目标,而非提升写入实时性。

3.2.1 增加 flush 时间间隔

目的是减小数据写入磁盘的频率,减小磁盘IO频率。

3.2.2 增加refresh_interval的参数值

目的是减少segment文件的创建,减少segment的merge次数,merge是发生在jvm中的,有可能导致full GC,增加refresh会降低搜索的实时性。

ES的 refresh 行为非常昂贵,并且在正在进行的索引活动时经常调用,会降低索引速度,这一点在索引写入原理中介绍过,了解索引的写入原理,可以关注我的博客Elastic开源社区。

默认情况下,Elasticsearch 每秒定期刷新索引,但仅在最近 30 秒内收到一个或多个搜索请求的索引上。

如果没有搜索流量或搜索流量很少(例如每 5 分钟不到一个搜索请求)并且想要优化索引速度,这是最佳配置。此行为旨在在不执行搜索的默认情况下自动优化批量索引。建议显式配置此配置项,如 30秒。

3.2.3 增加Buffer大小

本质也是减小refresh的时间间隔,因为导致segment文件创建的原因不仅有时间阈值,还有buffer空间大小,写满了也会创建。默认最小值 48MB< 默认值 JVM 空间的10% < 默认最大无限制

3.2.4 关闭副本

当需要单次写入大量数据的时候,建议关闭副本,暂停搜索服务,或选择在检索请求量谷值区间时间段来完成。

第一,是减小读写之间的资源抢占,读写分离 第二,当检索请求数量很少的时候,可以减少甚至完全删除副本分片,关闭segment的自动创建以达到高效利用内存的目的,因为副本的存在会导致主从之间频繁的进行数据同步,大大增加服务器的资源占用。具体可通过则设置index.number_of_replicas 为0以加快索引速度。没有副本意味着丢失单个节点可能会导致数据丢失,因此数据保存在其他地方很重要,以便在出现问题时可以重试初始加载。初始加载完成后,可以设置index.number_of_replicas改回其原始值。

3.2.5 禁用swap

大多数操作系统尝试将尽可能多的内存用于文件系统缓存,并急切地换掉未使用的应用程序内存。这可能导致部分 JVM 堆甚至其可执行页面被换出到磁盘。

交换对性能和节点稳定性非常不利,应该不惜一切代价避免。它可能导致垃圾收集持续几分钟而不是几毫秒,并且可能导致节点响应缓慢甚至与集群断开连接。在Elastic分布式系统中,让操作系统杀死节点更有效。

3.2.6 使用多个工作线程

发送批量请求的单个线程不太可能最大化 Elasticsearch 集群的索引容量。为了使用集群的所有资源,应该从多个线程或进程发送数据。除了更好地利用集群的资源外,还有助于降低每个 fsync 的成本。

确保注意 TOO_MANY_REQUESTS (429)响应代码(EsRejectedExecutionException使用 Java 客户端),这是 Elasticsearch 告诉我们它无法跟上当前索引速度的方式。发生这种情况时,应该在重试之前暂停索引,最好使用随机指数退避。

与调整批量请求的大小类似,只有测试才能确定最佳工作线程数量是多少。这可以通过逐渐增加线程数量来测试,直到集群上的 I/O 或 CPU 饱和。

3.2.7 避免使用稀疏数据

3.2.8 max_result_window参数

max_result_window是分页返回的最大数值,默认值为10000。max_result_window本身是对JVM的一种保护机制,通过设定一个合理的阈值,避免初学者分页查询时由于单页数据过大而导致OOM。

在很多业务场景中经常需要查询10000条以后的数据,当遇到不能查询10000条以后的数据的问题之后,网上的很多答案会告诉你可以通过放开这个参数的限制,将其配置为100万,甚至1000万就行。但是如果仅仅放开这个参数就行,那么这个参数限制的意义有何在呢?如果你不知道这个参数的意义,很可能导致的后果就是频繁的发生OOM而且很难找到原因,设置一个合理的大小是需要通过你的各项指标参数来衡量确定的,比如你用户量、数据量、物理内存的大小、分片的数量等等。通过监控数据和分析各项指标从而确定一个最佳值,并非越大越好

4、查询调优

4.1 读写性能不可兼得

首先要明确一点:鱼和熊掌不可兼得。读写性能调优在很多场景下是只能二选一的。牺牲 A 换 B 的行为非常常见。索引本质上也是通过空间换取时间。写生写入实时性就是为了提高检索的性能。

当你在二手平台或者某垂直信息网站发布信息之后,是允许有信息写入的延时性的。但是检索不行,甚至 1 秒的等待时间对用户来说都是无法接受的。满足用户的要求甚至必须做到10 ms以内。

4.2 优化手段

4.2.1 避免单次召回大量数据

搜索引擎最擅长的事情是从海量数据中查询少量相关文档,而非单次检索大量文档。非常不建议动辄查询上万数据。如果有这样的需求,建议使用滚动查询

4.2.2 避免单个文档过大

鉴于默认http.max_content_length设置为 100MB,Elasticsearch 将拒绝索引任何大于该值的文档。您可能决定增加该特定设置,但 Lucene 仍然有大约 2GB 的限制。

即使不考虑硬性限制,大型文档通常也不实用。大型文档对网络、内存使用和磁盘造成了更大的压力,即使对于不请求的搜索请求也是如此,_source因为 Elasticsearch_id在所有情况下都需要获取文档的文件系统缓存有效。对该文档进行索引可能会占用文档原始大小的倍数的内存量。Proximity Search(例如短语查询)和高亮查询也变得更加昂贵,因为它们的成本直接取决于原始文档的大小。

有时重新考虑信息单元应该是什么是有用的。例如,您想让书籍可搜索的事实并不一定意味着文档应该包含整本书。使用章节甚至段落作为文档可能是一个更好的主意,然后在这些文档中拥有一个属性来标识它们属于哪本书。这不仅避免了大文档的问题,还使搜索体验更好。例如,如果用户搜索两个单词fooand bar,则不同章节之间的匹配可能很差,而同一段落中的匹配可能很好。

4.2.3 单次查询10条文档 好于 10次查询每次一条

批量请求将产生比单文档索引请求更好的性能。但是每次查询多少文档最佳,不同的集群最佳值可能不同,为了获得批量请求的最佳阈值,建议在具有单个分片的单个节点上运行基准测试。首先尝试一次索引 100 个文档,然后是 200 个,然后是 400 个等。在每次基准测试运行中,批量请求中的文档数量翻倍。当索引速度开始趋于平稳时,就可以获得已达到数据批量请求的最佳大小。在相同性能的情况下,当大量请求同时发送时,太大的批量请求可能会使集群承受内存压力,因此建议避免每个请求超过几十兆字节。

4.2.4 数据建模

很多人会忽略对 Elasticsearch 数据建模的重要性。

nested属于object类型的一种,是Elasticsearch中用于复杂类型对象数组的索引操作。Elasticsearch没有内部对象的概念,因此,ES在存储复杂类型的时候会把对象的复杂层次结果扁平化为一个键值对列表。

特别是,应避免连接。Nested 可以使查询慢几倍,Join 会使查询慢数百倍。两种类型的使用场景应该是:Nested针对字段值为非基本数据类型的时候,而Join则用于 当子文档数量级非常大的时候。

关于数据建模,在我的博客中有详细的讲解,此处不再赘述

4.2.5 给系统留足够的内存

Lucene的数据的fsync是发生在OS cache的,要给OS cache预留足够的内从大小,详见JVM调优。

4.2.6 预索引 ** 利用查询中的模式来优化数据的索引方式。例如,如果所有文档都有一个price字段,并且大多数查询 range 在固定的范围列表上运行聚合,可以通过将范围预先索引到索引中并使用聚合来加快聚合速度。

4.2.7 使用 filter 代替 query

query和filter的主要区别在:filter是结果导向的而query是过程导向。query倾向于“当前文档和查询的语句的相关度”而filter倾向于“当前文档和查询的条件是不是相符”。即在查询过程中,query是要对查询的每个结果计算相关性得分的,而filter不会。另外filter有相应的缓存机制,可以提高查询效率。

4.2.8 避免深度分页

避免单页数据过大,可以参考百度或者淘宝的做法。es提供两种解决方案 scroll search 和 search after。关于深度分页的详细原理,推荐阅读:详解Elasticsearch深度分页问题

4.2.9 使用 Keyword 类型

并非所有数值数据都应映射为数值字段数据类型。Elasticsearch为 查询优化数字字段,例如integeror long。如果不需要范围查找,对于 term查询而言,keyword 比 integer 性能更好。

4.2.10 避免使用脚本

Scripting是Elasticsearch支持的一种专门用于复杂场景下支持自定义编程的强大的脚本功能。相对于 DSL 而言,脚本的性能更差,DSL能解决 80% 以上的查询需求,如非必须,尽量避免使用 Script

1、线上实战问题

问题 1:想要请问一下,我这边需求是每分钟利用 sparksteaming 插入按天的索引150万条数据。一般情况下还好,索引7个分片,1副本,但是偶尔会出现延迟很高的情况。比如:一般情况下1分钟插入150万能正常插入,可能突然就出现了需要5分钟才能插入成功,然后又正常了。很头疼。

请问这种情况我需要怎么去查看一下是否正常。我已经把副本设置成了0,还把批量插入的参数从 5000 设置成 2 万。我节点是 12 个 16g 的,但是好像还是没有改观。

问题 2:由于使用了多个分词器的原因造成数据写入慢,请问有什么优化的方法吗?

问题 3:求问:现在日志 收集链路 kafka-logstash-es,压力测试 logstash输出70M/s,而 Elasticsearch 索引写入一半不到。这边的性能损失可能是什么原因呢?需要怎么调优呢?

类似问题还有很多、很多......

2、问题分析

以上三个问题各有各自特点,但基本都是基于不同的数据源向 Elasticsearch 写入过程中遇到的问题。

可以简单归结为:Elasticsearch 写入问题或者写入优化问题。Elasticsearch 写入问题涉及写入流程、写入原理及优化策略。

本文针对如上几点展开讨论。

3、基于单文档/批量文档写入流程谈写入优化

单个文档写入对应 Index 请求,批量写入对应 Bulk 请求,Index 和 Bulk 是相同的处理逻辑,请求统一封装到 BulkRequest中。

图片

流程拆解如下:

第一:客户端向 Node 1 发送写数据请求。

注意,此时Node1 便充当协调节点(cooridiniate)的角色。

第二:节点Node1 使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。

使用文档的 _id 确定文档所属分片的方法是:路由算法。

路由算法计算公式:

shard = hash(routing) % number_of_primary_shards
  • routing:文档 _id。
  • number_of_primary_shards: 主分片个数。
  • shard: 文档 _id 所属分片号。

第三:Node 3 在主分片上面执行写入操作。如果写入成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告写入成功,协调节点向客户端报告写入成功。

如上流程拆解后的注意点:

  • 写操作必须在主分片执行成功后,才能复制到相关的副本分片。
  • 主分片写入失败,则整个请求被认为是写失败。
  • 如果有部分副本写失败(前提:主分片写入成功),则整个请求被认为是写成功。

如果设置了副本,数据会先写入主分片,主分片再同步到副本分片,同步操作会加重磁盘 IO,间接影响写入性能。

基于以上分析,既然主分片的写入起到写入成败的决定性作用。那么写入前将:副本分片写入前置为0,待写入完成后复原副本,是不是就能提升写入性能了呢?

是的!

写入优化一:副本分片写入前置为0,等完成写入后复原副本

PUT test-0001
{
  "settings": {
    "number_of_replicas": 0
  }
}

写入优化二:优先使用系统自动生成 id

文档的_id 的生成有两种方式,

  • 第一:系统自动生成id。
  • 第二:外部控制自增id。

但,如果使用外部 id,Elasticsearch 会先尝试读取原来文档的版本号,以判断是否需要更新。

也就是说,使用外部控制 id 比系统自动生成id要多一次读取磁盘操作。

所以,非特殊场景推荐使用系统自动生成的 id。

4、基于 Elasticsearch 写入原理谈写入优化

Elasticsearch 中的 1 个索引由一个或多个分片组成,每个分片包含多个segment(段),每一个段都是一个倒排索引。如下图所示:

图片

在 lucene 中,为了实现高索引速度,使用了segment 分段架构存储。一批写入数据保存在一个段中,其中每个段最终落地为磁盘中的单个文件。

将文档插入 Elasticsearch 时,它们会被写入缓冲区中,然后在刷新时定期从该缓冲区刷新到段中。刷新频率由 refresh_interval 参数控制,默认每1秒刷新一次。

也就是说,新插入的文档在刷新到段(内存中)之前,是不能被搜索到的。如下图所示:

图片

刷新的本质是:写入数据由内存 buffer 写入到内存段中,以保证搜索可见。

来看个例子,加深对 refresh_inteval 的理解,注释部分就是解读。

PUT test_0001/_doc/1
{
  "title":"just testing"
}

# 默认一秒的刷新频率,秒级可见(用户无感知)

GET test_0001/_search

如下设置后,写入后 60s 后才可见。

DELETE test_0001
# 设置了60s的刷新频率
PUT test_0001
{
  "settings": {
    "index":{
      "refresh_interval":"60s"
    }
  }
}
 
PUT test_0001/_doc/1
{
  "title":"just testing"
}
# 60s后才可以被搜索到
GET test_0001/_search

关于是否需要实时刷新:

  • 如果新插入的数据需要近乎实时的搜索功能,则需要频繁刷新。
  • 如果对最新数据的检索响应没有实时性要求,则应增加刷新间隔,以提高数据写入的效率。

所以,自然我们想到的优化是:调整刷新频率。

写入优化三:合理调整刷新频率

调整方法如下:

方法1:写入前刷新频率设置为 -1,写入后设置为业务实际需要值(比如:30s)。

PUT test-008
{
  "settings": {
    "refresh_interval": -1
  }
}

方法2:直接设置为业务实际需要值(比如:30s)

PUT test-008
{
  "settings": {
    "refresh_interval": "30s"
  }
}

写入优化四:合理调整堆内存中索引缓冲区(index_buffer)大小

堆内存中 index_buffer 用于存储新索引的文档。

填满后,缓冲区中的文档将最终写入磁盘上的某个段。

index_buffer_size 默认值如下所示,为堆内存的 10%。

indices.memory.index_buffer_size: 10%

例如,如果给 JVM 31GB的内存,它将为索引缓冲区提供 3.1 GB的内存,一般情况下足以容纳大量数据的写入操作。

但,如果着实数据量非常大,建议调大该默认值。比如:调整为堆内存的 20%。

调整建议:必须在集群中的每个数据节点上进行配置。

缓存区越大,意味着能缓存数据量越大,相同时间段内,写盘频次低、磁盘 IO 小,间接提升写入性能。

写入优化五:给堆外内存也留够空间(常规要求)

这其实算不上写入优化建议,而是通用集群配置的常规配置。

内存分配设置堆内存比例官方建议:机器内存大小一半,但不超过 32 GB。

一般设置建议:

  • 如果内存大小 >= 64 GB,堆内存设置:31 GB。
  • 如果内存大小 < 64 GB,堆内存设置:内存大小一半。

堆内存之外的内存留给:Lucene 使用。

推荐阅读:干货 | 吃透Elasticsearch 堆内存

写入优化六:bulk 批量写入而非单个文档写入

批量写入自然会比单个写入性能要好(批量写入意味着相同时间产生的段会大,段的总个数自然会少),但批量值的设置一般需要慎重,不要盲目一下搞的很大。

一般建议:递增步长测试,直到达到资源使用上限。

比如:第一次批量值设置:100,第二次:200,第三次:400,以此类推......

批量值 bulk 已经 ok 了,但集群尚有富余资源,资源利用并没有饱和怎么办?

上多线程,通过并发提升写入性能。

写入优化七:多线程并发写入

这点,在 logstash 同步数据到 Elasticsearch,基于spark、kafka、Flink 批量写入 Elasticsearch时,经常会出现:Bulk Rejections 的报错。

当批量请求到达集群中的某个节点时,整个请求将被放入批量队列中,并由批量线程池中的线程进行处理。批量线程池处理来自队列的请求,并将文档转发到副本分片,作为此处理的一部分。子请求完成后,将响应发送到协调节点。

Elasticsearch 具有有限大小的请求队列的原因是:为了防止集群过载,从而增加了稳定性和可靠性。

如果没有任何限制,客户端可以很容易地通过恶意攻击行为将整个集群搞宕机。

这里就引申出下面的优化点。

写入优化八:合理设置线程池和队列大小

关于线程池和队列,参考:Elasticsearch 线程池和队列问题,请先看这一篇

核心建议就是:结合 CPU 核数和 esrally 的测试结果谨慎的调整 write 线程池和队列大小。

为什么要谨慎设置?

针对批量写入拒绝(reject)的场景,官方建议:

增加队列的大小不太可能改善集群的索引性能或吞吐量。相反,这只会使集群在内存中排队更多数据,这很可能导致批量请求需要更长的时间才能完成。

队列中的批量请求越多,将消耗更多的宝贵堆空间。如果堆上的压力太大,则可能导致许多其他性能问题,甚至导致集群不稳定。

推荐阅读:

https://www.elastic.co/cn/blog/why-am-i-seeing-bulk-rejections-in-my-elasticsearch-cluster

5、其他写入优化建议

写入优化九:设置合理的Mapping

实战业务场景中不推荐使用默认 dynamic Mapping,一定要手动设置 Mapping。

  • 举例1:默认字符串类型是:text 和 keyword 的组合类型,就不见得适用所有业务场景。要结合自己业务场景设置,正文 cont 文本内容一般不需要设置 keyword 类型(因为:不需要排序和聚合操作)。
  • 举例2:互联网采集数据存储场景,正文需要全文检索,但包含 html 样式的文本一般留给前端展示用,不需要索引。这时候Mapping 设置阶段要果断将 index 设置为 false。

写入优化十:合理的使用分词器

分词器决定分词的粒度,常见的中文分词 IK 可细分为:

  • 粗粒度分词:ik_smart。
  • 细粒度分词:ik_max_word。

从存储角度基于 ik_max_word 分词的索引会比基于 ik_smart 分词的索引占据空间大。

而更细粒度自定义分词 ngram 会占用大量资源,并且可能减慢索引速度并显着增加索引大小。

所以要结合检索指标(召回率和精准率)、结合写入场景斟酌选型。

写入优化十一:必要时,使用 SSD 磁盘

SSD 很贵,但很香。

尤其针对写入密集型场景,如果其他优化点都考虑了,这可能是你最后一根“救命稻草“。

写入优化十二:合理设置集群节点角色

这也是经常被问到的问题,集群规模小的时候,一般节点会混合多种角色,如:主节点 + 数据节点、数据节点 + 协调节点混合部署。

但,集群规模大了之后,硬件资源相对丰富后,强烈建立:独立主节点、独立协调节点。

让各个角色的节点各尽其责,对写入、检索性能都会有帮助。

写入优化十三:推荐使用官方客户端

推荐使用官方 Elasticsearch API,因为官方在连接池和保持连接状态方面有优化。

高版本 JAVA API 推荐:官方的High-level-Rest API。

其他写入优化

待补充......

6、写入过程中做好监控

如下是 kibana 监控截图,其中:index Rate 就是写入速率。

图片

  • index rate: 每秒写入的文档数。
  • search rate:每秒的查询次数(分片级别,非请求级别),也就意味着一次查询请求命中的分片数越多,值越大。

7、小结

图片

Elasticsearch 写入优化没有普适的最优优化建议,只有适合自己业务场景的反复试验、调优,形成属于自己业务场景的最佳实践。

你的业务场景做过哪些写入优化?欢迎留言讨论交流。