avatar


4.性能优化

《ElasticSearch实战入门(6.X)》新增一篇文章《4.性能优化》,讨论"ElasticSearch中的性能优化"。

写入优化

写入时先将副本分片数置为0,完成写入后再将其复原

写入时先将副本分片数置为0,完成写入后再将其复原,设置命令如下所示:

1
2
3
4
5
6
PUT test
{
"settings":{
"number_of_replicas":0
}
}

优先使用系统自动生成ID的方式

文档ID的生成有两种方式:系统自动生成ID和外部控制自增ID。

如果使用外部控制自增ID,Elasticsearch会先尝试读取原来文档的版本号,以判断是否需要更新。也就是说,使用外部控制自增ID比系统自动生成ID要多进行一次读取磁盘操作。

所以,非特殊场景建议使用系统自动生成ID的方式。

合理调整刷新频率

背景

为了提高索引性能,Elasticsearch在写入数据的时候,采用延迟写入的策略。
先将数据写到内存中,当超过默认1秒(index.refresh_interval)会进行一次写入操作,将内存中segment数据刷新到磁盘中。
此时我们才能将数据搜索出来,所以这就是为什么Elasticsearch提供的是近实时搜索功能,而不是实时搜索功能。

关闭自动刷新

refresh_interval设置为-1,即禁用索引的自动刷新,这样索引将不会按照固定的时间间隔进行自动刷新,而是只有在显式调用_refreshAPI或者执行某些特定的操作(如批量索引完成后)时才会触发刷新。

示例代码:

1
2
3
4
5
6
PUT test
{
"settings":{
"refresh_interval":-1
}
}

注意:

  1. 在完成写入操作后,应立即调用_refreshAPI来手动刷新索引。
  2. 在完成写入操作后,应将refresh_interval恢复。

延长refresh时间间隔

结合业务需求,我们可以考虑延长refresh时间间隔,这样可以有效地减少segment合并压力,提高索引速度。

示例代码:

1
2
3
4
5
6
PUT test
{
"settings":{
"refresh_interval":"30s"
}
}

合理调整堆内存中的索引缓冲区大小

调整方法

在堆内存中,索引缓冲区用于存储新索引的文档,填满后,缓冲区中的文档将最终写入磁盘上的某个段。
index_bufer_size的默认值为堆内存的10%。

1
indices.memory.index_buffer_size: 10%

例如:给JVM提供31GB的内存,它将为索引缓冲区提供3.1GB的内存,一般情况下足以容纳大量数据的写入操作;如果数据量确实非常大,则建议调大该默认值,例如调整为堆内存的20%,但是必须在集群中的每个数据节点上进行配置。缓冲区越大,意味着能缓存的数据量越大,相同时间内,写入磁盘的频次更低、磁盘IO次数更少,从而提升写入性能。

给堆外的内存留够空间

官方建议内存分配时设置堆内存为机器内存大小的一半但不要超过32GB,堆内存之外的内存留给Lucene使用。

一般设置建议如下:

  • 如果内存大小 \geq 64GB,则堆内存设置为31GB。
  • 如果内存大小 << 64GB,则堆内存设置为内存大小的一半。

批量写入和多线程并发写入

在高写入负载的场景下,为了提高写入效率,Elasticsearch支持批量写入。

即,我们在《2.基本操作》讨论的"_bulk"。

批量写入允许将多个文档合并成一个请求发送给Elasticsearch,这样可以显著减少网络往返次数和每个请求的开销,从而提高写入性能,而且批量写入意味着相同时间产生的段更大,段的总个数更少。

但批量值的设置一般需要慎重,不能盲目地将其设置得太大。一般建议进行递增步长测试,直到资源使用上限。例如,第一次将批量值设置为100,第二次为200,第三次为400,等。

如果批量值设置完成,但集群尚有富余资源、资源利用没有饱和,怎么办?这时候可以考虑采用多线程,通过并发提升写入性能。

Elasticsearch内部使用了多种线程池来处理不同的任务,包括索引、搜索、批量写入等。对于批量写入,Elasticsearch使用了一个特殊的线程池(通常称为bulk线程池),这个线程池负责从批量队列中取出请求并处理。批量队列的大小是有限的,这是为了防止过多的未处理请求堆积,占用过多的系统资源,导致集群不稳定甚至崩溃。

当批量队列已满,Elasticsearch会报错"Bulk Rejections",新的批量写入请求无法被加入队列时。这通常是由于写入速率超过了集群的处理能力,或者线程池的线程数不足以快速处理队列中的请求,导致队列溢出。为了避免这种情况,可以考虑以下策略:

  • 增加线程池大小
    通过调整thread_pool.bulk.queue_sizethread_pool.bulk.size配置项来增加批量队列的大小和线程池的线程数,但这需要谨慎操作,以避免过度消耗系统资源。
  • 优化批量写入参数
    合理设置bulk.sizeconcurrent_requests参数,以平衡批量写入的大小和并发度,找到最佳的写入性能和稳定性之间的平衡点。

设置合理的映射

在实际业务中,不推荐使用默认的动态映射,一定要手动设置映射。

举例:

  1. 若默认字符串类型是text和keyword的组合类型,不一定适用于所有业务场景,需要结合业务场景进行设置,正文文本内容一般不需要设置keyword类型(因为不需要排序和聚合操作)。
  2. 在有些采集数据并存储的场景中,正文文本内容需要进行全文检索,但HTML样式的文本一般会留给前端展示用,不需要索引。因此,映射设置时需要将index设置为false。

合理使用分词器

分词器决定分词的粒度,对于中文常用的IK分词,可细分为粗粒度分词ik_smart和细粒度分词ik_max_word

从存储角度来看,基于ik_max_word分词的索引会比基于ik_smart分词的索引占据空间大,而更细粒度的自定义分词Ngram会占用大量资源,并且可能减慢索引速度并显著增加索引大小 。

所以,要结合检索指标(召回率和精准率)以及写入场景进行选型。

写入过程监控

Kibana的监控功能提供了很多指标,索引率(index rate)和查询速率(search rate)。

  • 索引率
    索引率是指每秒写入Elastiseach的文档数。
  • 查询速率
    查询速率表示每秒的查询次数,反映了Elastisearch集群的读取性能。

对于这两个指标,我们需要关注的主要是其稳定性和持续性。如果任何一个指标突然变化,都可能意味着系统中存在问题。这可能是因为硬件资源不足,也可能是因为查询或索引操作过于复杂。

除此之外,我们还需要关注其他一些指标,例如CPU使用率、内存使用率、磁盘IO等。

检索优化

不要返回全量或近全量数据

对于返回全量数据的需求,要评估其合理性。
如果必须返回全量文档,建议使用scroll API,这个可以更快地返回检索结果,并具有更好的性能。

避免使用大文档

不建议使用大文档。

大文档给网络、内存使用和磁盘使用带来了压力,在实现全文检索和高亮请求时,响应时间会随着原始文档的大小而增加。

比如说,我们有一本书需要被搜索。这时候,不一定要将整本书作为一个文档,更好的做法是将每一章(节)甚至每一段作为一个文档,然后在这些文档中添加一个属性,标识它们属于哪一本书的哪一章(节)。这不仅避免了大文档带来的问题,还可以获得更好的搜索体验。

检索方法层面的优化

尽可能减少检索字段数目

采用"显式指定需要返回的字段"的方法来减少检索字段数目。
具体是,在查询请求中使用_source参数指定需要返回的字段,避免返回所有字段。

合理设置size值

若将检索请求的size值设得很大,则会导致命中数据量大。一方面,会增加网络传输的负载,导致网络延时和性能下降;另一方面,需要占用更多的内存来缓存结果集,导致内存消耗过大,甚至造成OOM。

建议根据具体的业务场景和数据规模来合理设置分页size值,以有效减少资源消耗和保护系统的稳定性。
如果数据量确实很大,则可以考虑通过scroll或者search_after方式来实现。

尽量避免使用脚本

脚本可以用来执行一些高级的检索和聚合操作,但是在实际应用中,建议尽可能避免使用脚本,因为它们可能会影响性能,并且不够安全。
因为:

  1. 使用脚本会需要额外的计算资源和时间,这可能对性能产生负面影响。
  2. 如果允许用户自定义脚本,那么恶意用户可能会编写脚本来执行一些不安全的操作,例如删除索引或访问敏感信息。

所以,在生产环境中,建议禁止用户自定义脚本或仅支持受信任的用户组使用脚本。

有效使用filter缓存

为了提高性能,Elasticsearch引入了filter缓存机制。
filter缓存是指在执行过滤操作时,将结果缓存到内存中,以便在后续的查询中能够快速访问。
这样可以避免对同一个过滤器进行重复计算,从而提高查询性能。
尤其是在对相同的过滤器进行多次查询时,这种缓存方式可以大幅提高查询效率。

避免使用wildcard检索

避免使用wildcard通配符检索,尤其是前缀通配符查询。

原因在于:当使用通配符查询时,倒排索引针对keyword类型并不能发挥优势,Elastcsarch必须遍历所有符合通配符表达式的文档,这可能会导致性能下降和查询响应慢。

面对类似需求,推荐在前期使用预处理Ngram分词,以空间换时间来解决问题。

尽量避免使用正则匹配检索

不建议频繁使用正则匹配检索的方式,因为:

  1. 性能问题
    正则匹配检索通常比其他检索方法要慢得多,特别是当使用复杂的正则表达式时。
    正则表达式需要对每个文档的每个字段进行匹配,这对于大型数据集来说可能会导致性能问题。
  2. 精度问题
    正则匹配检索是基于模式实现的,因此它可能会返回一些用户不需要的结果。
    例如,你正在寻找包含单词car的文档,但是你的正则表达式模式匹配了包含单词"catch"和"category"的文档,这将导致精度问题。
  3. 无法利用倒排索引机制
    使用正则表达式进行检索时,将扫描整个文档集合,无法使用索引加快搜索。

正则匹配检索会有响应慢及性能问题,因此要谨慎使用,建议尽量避免。
如果必须使用正则表达式,则需尽可能简化它们,并使用其他过滤条件来缩小返回的结果集。

谨慎使用全量聚合和多重嵌套聚合

聚合本身是不精准的,主要是因为主、副本分片数据的不一致性。
对于实时性业务数据的场景,每分、每秒都有数据写入,就要考虑到数据在变化,聚合结果也会随之变化。

全量聚合会在所有匹配的文档上执行聚合操作,如果匹配的文档数非常大,就可能会占用大量的内存,甚至导致OOM。
同时,全量聚合需要扫描所有匹配的文档,会对查询性能产生影响。

多重嵌套聚合指的是在一个聚合操作内嵌套另一个聚合操作,形成多层嵌套。这种方式会增加查询复杂度和降低查询性能。

因此,在使用Elasticsearch进行聚合分析时,应该谨慎使用全量聚合和多重嵌套聚合,可以考虑将聚合操作拆分成多个步骤来执行,从而降低每个聚合操作的复杂度、减小检索范围。

慢查询日志

可以通过如下的命令,开启慢查询日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PUT  /_template/{TEMPLATE_NAME}
{

"template":"{INDEX_PATTERN}",
"settings" : {
"index.indexing.slowlog.level": "INFO",
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000",
"index.search.slowlog.level": "INFO",
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
},
"version" : 1
}

PUT {INDEX_PAATERN}/_settings
{
"index.indexing.slowlog.level": "INFO",
"index.indexing.slowlog.threshold.index.warn": "10s",
"index.indexing.slowlog.threshold.index.info": "5s",
"index.indexing.slowlog.threshold.index.debug": "2s",
"index.indexing.slowlog.threshold.index.trace": "500ms",
"index.indexing.slowlog.source": "1000",
"index.search.slowlog.level": "INFO",
"index.search.slowlog.threshold.query.warn": "10s",
"index.search.slowlog.threshold.query.info": "5s",
"index.search.slowlog.threshold.query.debug": "2s",
"index.search.slowlog.threshold.query.trace": "500ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
}

解释说明:

  • PUT /_template/{TEMPLATE_NAME},使用模板(Template),未来符合该模板的索引也会被影响。
  • PUT {INDEX_PATTERN}/_settings,针对一个或一组具体的索引(通过{INDEX_PATTERN}指定)进行设置,这种修改只会影响指定的索引,不会影响其他未提及的索引,也不会对未来创建的索引产生作用。

设置完成后,在日志目录下的慢查询日志就会有输出记录必要的信息了:

  • {CLUSTER_NAME}_index_indexing_slowlog.log
  • {CLUSTER_NAME}_index_search_slowlog.log

数据模型优化

文档结构务必规范、一致

  1. 避免将结构完全不同的文档放入同一索引,将这些文档放入不同的索引通常会更好。
  2. 考虑为较小的索引提供较少的分片,因为总体上包含的文档较少。
  3. 避免具有相同功能的字段命名不一致的问题。
    例如,如果索引中的文档包含时间戳字段,但有些文档将其命名为"timestamp",有些文档则将其命名为"ts",这种情况是要避免的。
    建议在设计建模阶段就进行一致性处理,以方便后续的检索操作。

设置合理的分片数和副本数

主分片的设置需要结合集群数据节点规模、全部数据量和日增数据量等维度综合考量才给出值, 一般建议设置为数据节点的1~3倍。
分片不宜过小,有很多小分片可能会导致大量的网络调用和线程开销,这会严重影响搜索性能。

在许多情况下,拥有更多副本有助于提高搜索性能,但是不代表副本越多越好。
增加副本之前要考虑磁盘存储空间的容量上限和磁盘警戒水位线,本质还是以空间换时间。
对于一般的非高可用场景,认为一个副本足够。

多使用写入前预处理操作

假设存在一个舆情系统,其中设计的情感值包括3个区间,负面、正面、中性。如果通过范围检索方式进行区间检索会慢。

可以考虑在建立数据模型的时候将数据在写入阶段转成-101的keyword类型值,以此将范围检索变成基于倒排索引的精准查找的过程。

合理使用边写入边排序机制

考虑以下配置代码段。此配置展示了在创建索引时如何设定排序字段,并且排序操作并不在数据写入后进行,而是预先定义好,即在索引创建时设置。
这个过程的主要目标是通过牺牲一些写入性能来显著提升检索速度。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT my-index
{
"settings":{
"index":{
"sort.field":"date",
"sort.order":"desc"
}
},
"mappings":{
"properties":{
"date":{
"type":"date"
}
}
}
}

尽量使用keyword字段类型

如果一个字段既可以设置为number类型,也可以设置为keyword类型。
那么可以参考如下方式:

  1. 如果涉及范围检索,推荐使用数值类型的字段。
  2. 如果仅需精准匹配term级别的检索,推荐keyword类型。
  3. 如果两种都有需求,则建议设置keyword和number双类型,可借助fields组合类型实现。
    示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    PUT test
    {
    "mappings":{
    "properties":{
    "age":{
    "type":"integer",
    "fields":{
    "keyword":{
    "type":"keyword"
    }
    }
    }
    }
    }
    }

硬件优化

内存

Elasticsearch是一个内存密集型的应用程序,足够的内存可以提高性能和响应速度。

  1. Elasticsearch使用缓存来加速搜索操作,缓存是存储在内存中的数据结构,可以减少对硬盘的读取次数,内存越大,可以存储的缓存也就越多,从而可以提高搜索性能。
  2. Elasticsearch使用倒排索引来加速搜索操作,倒排索引和字段数据也需要存储在内存中,以加快搜索和聚合操作。
  3. Java程序的内存管理是通过GC来实现的,内存越大,垃圾回收的效率也就越高,从而可以缩短系统的停顿时间。

我们需要给Elasticsearch分配足够的内存以提高搜索性能、减少停顿时间和提高系统的稳定性,同时我们需要根据具体的应用场景和数据规模来合理配置内存大小。

磁盘

SSD

建议必要时使用SSD;如果SSD资源紧张,建议结合业务场景设置冷热集群架构,将SSD优先分配给热节点。

警戒水位线

属性名 属性值 含义
cluster.routing.allocation.disk.watermark.low 85%85\% 低警戒水位线
cluster.routing.allocation.disk.watermark.high 90%90\% 高警戒水位线
cluster.routing.allocation.disk.watermark.flood_stage 95%95\% 洪泛警戒水位线

CPU

Elasicsarch是一个高并发的应用;在选择CPU时,应尽量选择多核的CPU。

在并发写入或查询量变大之后,可能会出现CPU满了的情况,建议根据CPU核数合理调节线程池和队列的大小。

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/11204
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板