初见程序员

搜索引擎ElasticSearch之(8)、性能优化

2020-05-13  本文已影响0人  桥头放牛娃

1、写入速度优化

在 ES 的默认设置下,是综合考虑数据可靠性、搜索实时性、写入速度等因素的。当离开默认设置、追求极致的写入速度时,很多是以牺牲可靠性和搜索实时性为代价的。

1.1、TranslogFulsh间隔调整

默认情况下,translog持久化策略为每个请求都flush,其保证了写入操作的可靠性。
对应配置为:

index.translog.durability: request

若系统接受一定概率的数据丢失,如系统日志数据等,则可以调整translog持久化策略为周期性和一定大小的时候flush。

//设置translog刷盘策略按sync_interval指定的时间周期进行
index.translog.durability: async
//加长translog的刷盘周期,默认为5s
index.translog.sync_interval: 120s
//超过这个大小会导致refresh操作,产生新的Lucene分段。默认值为512MB。
index.translog.flush_threshold_size: 1024mb

1.2、索引刷新间隔

默认索引的refresh_interval为1秒,即数据写入后最长1s就可被搜索到,每次索引的refresh会产生一个新的Lucene段,这会导致频繁的segment merge行为,如果不需要这么高的搜索实时性,应该降低索引refresh周期。
对应配置:

index.refresh_interval: 120s

1.3、段合并优化

段合并scheduler:

//concurrent或serial
index.merge.scheduler.type

serial为串行合并策略,此策略下只用一个线程来执行段合并任务。
concurrent为并行合并策略,会创建多个线程来执行段合并任务。
默认线程数为:
Math.max(1, math.min(4,Runtime.getRuntime().availableProcessors() / 2))
可通过设置:index.merge.scheduler.max_thread_count,来改变最大线程数。

段合并策略:
可通过:index.merge.policy.type来设置合并策略;
主要配置如下:
tiered(分层合并策略):
ES默认策略,其将大小相似的段放在一起合并,依据每层允许的段数量的最大值,可以区分出一次合并中段的数量。在索引过程中,此策略会计算索引中允许存在的段数量,即为预算,若索引中段数量大于预算值,此策略先按段大小降序排序,找出开销最小的合并方案,合并的开销会考虑比较小的段及删除文档较多的段。如果合并产生的段的大小大于index.merge.policy.max_merged_segment配置,则此合并会减少段合并的数量,以保持合并后新的段在预算之内。

log_byte_size(字节大小对数合并策略):
该策略将创建大小处于对数运算后大小在指定范围内的段组成的索引。当一个新段尝试并其与其他段不在一个数量级时,所有处于该数量级的段就会合并。索引中段的数量与经对数运算后新段字节的大小成比例。此策略通常能在段合并开销较小的情况下将索引中的段保持在一个较低的水平。

log_doc(文档对数合并策略):
与log_byte_size策略类似,只不过以文段数量的计算方式来合并段。

1.4、indexing buffer

indexing buffer在为doc建立索引时使用,当缓冲区满时会刷入磁盘,生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成新segment的地方。

//节点索引缓存的大小,默认为整个堆空间的10%
indices.memory.index_buffer_size
//节点索引缓存的最小值,默认为48MB
indices.memory.min_index_buffer_size
//节点索引缓存的最大值,默认为无限制
indices.memory.max_index_buffer_size

1.5、使用bulk请求

批量写比一个索引请求只写单个文档的效率高得多,但是要注意bulk请求的整体字节数不要太大,太大的请求可能会给集群带来内存压力,因此每个请求最好避免超过几十兆字节,即使较大的请求看上去执行得更好。

1.6、bulk线程池和队列

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

1.7、并发执行bulk请求

bulk写请求是个长任务,为了给系统增加足够的写入压力,写入过程应该多个客户端、多线程地并行执行,如果要验证系统的极限写入能力,那么目标就是把CPU压满。磁盘util、内存等一般都不是瓶颈。如果 CPU 没有压满,则应该提高写入端的并发数量。但是要注意 bulk线程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端请求被拒绝,此时客户端会收到429错误(TOO_MANY_REQUESTS),客户端对此的处理策略应该是延迟重试。不可忽略这个异常,否则写入系统的数据会少于预期。即使客户端正确处理了429错误,我们仍然应该尽量避免产生reject。

1.8、自动生成docId

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

1.9、调整字段Mappings

1.10、对Analyzed的字段禁用Norms

Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:

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

1.11、index_options 设置

index_options用于控制在建立倒排索引过程中,哪些内容会被添加到倒排索引,例如,doc数量、词频、positions、offsets等信息,优化这些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。不过在实际场景中,通常很难确定业务将来会不会用到这些信息,除非一开始方案就明确是这样设计的。

2、搜索速度优化

2.1、为文件系统cache保留足够内存

应用程序的读写都会被操作系统“cache”(除了direct方式),cache保存在系统物理内存中(线上应该禁用swap),命中cache可以降低对磁盘的直接访问频率。搜索很依赖对系统 cache 的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延迟。应该至少为系统cache预留一半的可用物理内存,更大的内存有更高的cache命中率。

2.2、使用更快的硬件

使用SSD会比旋转类存储介质好得多。尽量避免使用NFS 等远程文件系统,如果 NFS 比本地存储慢3倍,则在搜索场景下响应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访问。如果搜索类型属于计算比较多,则可以考虑使用更快的CPU。

2.3、预索引数据

可以针对某些查询的模式来优化数据的索引方式。例如,如果所有文档都有一个 price字段,并且大多数查询在一个固定的范围上运行range聚合,那么可以通过将范围“pre-indexing”到索引中并使用terms聚合来加快聚合速度。

PUT index/type/1
{
    "designation": "spoon",
    "price": 13
}

PUT index/type/1
{
    "designation": "spoon",
    "price": 13,
    "price_range":"10-100"
}

2.4、字段映射

有些字段的内容是数值,但并不意味着其总是应该被映射为数值类型,例如,一些标识符,将它们映射为keyword可能会比integer或long更好。

2.5、优化日期搜索

在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配到的范围一直在变化。但是,从用户体验的角度来看,切换到一个完整的日期通常是可以接受的,这样可以更好地利用查询缓存。

2.6、为只读索引执行force-merge

为不再更新的只读索引执行force merge,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并,将只读索引强制合并为一个Lucene分段不仅可以优化搜索过程,对索引恢复速度也有好处。

2.7、预热文件系统cache

如果ES主机重启,则文件系统缓存将为空,此时搜索会比较慢。可以使用index.store.preload设置,通过指定文件扩展名,显式地告诉操作系统应该将哪些文件加载到内存中。

PUT /my_index
{
    "settings": {
        "index.store.preload": ["nvd", "dvd"]
    }
}

2.8、利用自适应副本选择(ARS)

为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力和健康水平。
ES的ARS实现基于这样一个公式:对每个搜索请求,将分片的每个副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮询方式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求路由到那里。ES7.0开始默认开启。

PUT /_cluster/settings
{
    "transient": {
        "cluster.routing.use_adaptive_replica_selection": true
    }
}

3、磁盘使用量优化

3.1、索引映射参数

索引创建时可以设置很多映射参数:

3.2、禁用不需要的特性

禁用norms:
text 类型的字段会在索引中存储归一因子(normalizationfactors),以便对文档进行评分,如果只需要在文本字段上进行匹配,而不关心生成的得分,则可以配置 ES 不将 norms 写入索引。

PUT index
{
    "mappings":{
        "type":{
            "properties":{
                "foo":{
                    "type":"text",
                    "norms":false
                }
            }
        }
    }
}

修改index_options参数:
text类型的字段默认情况下也在索引中存储频率和位置。频率用于计算得分,位置用于执行短语(phrase)查询。如果不需要运行短语查询,则可以告诉ES不索引位置。
index_options参数:

PUT index
{
    "mappings":{
        "type":{
            "properties":{
                "foo":{
                    "type":"text",
                    "index_options":"freqs"
                }
            }
        }
    }
}

3.3、禁用doc values

所有支持doc value的字段都默认启用了doc value。如果确定不需要对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用doc value以节省磁盘空间:

PUT index
{
    "mappings":{
        "type":{
            "properties":{
                "foo":{
                    "type":"text",
                    "doc_values":false
                }
            }
        }
    }
}

3.4、使用best_compression

_source和设置为"store": true的字段占用磁盘空间都比较多。默认情况下,它们都是被压缩存储的。默认的压缩算法为LZ4,可以通过使用best_compression来执行压缩比更高的算法:EFLATE。但这会占用更多的CPU资源。

PUT index
{
    "settings": {
        "index": {
            "codec": "best_compression"
        }
    }
}

3.5、Fource Merge

一个ES索引由若干分片组成,一个分片有若干Lucene分段,较大的Lucene分段可以更有效地存储数据。使用_forcemerge API来对分段执行合并操作,通常,我们将分段合并为一个单个的分段:max_num_segments=1。

4、综合优化

4.1、集群层优化

4.1.1、规划集群规模

集群规模和数据总量控制:

4.1.2、多节点部署

ES不建议为JVM配置超过32GB的内存,超过32GB时,Java内存指针压缩失效,浪费一些内存,降低了CPU性能,GC压力也较大,因此推荐设置为31GB。确保堆内存最小值(Xms)与最大值(Xmx)大小相同,防止程序在运行时动态改变堆内存大小,这是很耗系统资源的过程。
当物理内存超过64GB时部署方案:

4.2、节点层

4.2.1、控制线程池的队列大小

不要为bulk和search分配过大的队列,队列并非越大越好,队列缓存的数据越多,GC压力越大,默认的队列大小基本够用了,即使在压力测试的场景中,默认队列大小也足以支持。除非在一些特别的情况下,例如,每个请求的数据量都非常小,可能需要增加队列大小。但是我们推荐写数据时组合较大的bulk请求。

4.2.2、为系统cache保留一半物理内存

搜索操作很依赖对系统cache的命中,标准的建议是把50%的可用内存作为ES的堆内存,为Lucene保留剩下的50%,用作系统cache。

4.3、系统层

4.3.1、关闭swap

在服务器系统上,无论物理内存多么小,哪怕只有1GB,都应该关闭交换分区。当服务程序在交换分区上缓慢运行时,往往会产生更多不可预期的错误,因此当一个申请内存的操作如
果真的遇到物理内存不足时,宁可让它直接失败。一般在安装操作系统的时候直接关闭交换分区,或者通过swapoff命令来关闭。

4.3.2、配置系统的OOM Killer

在Linux下,进程申请的内存并不会立刻为进程分配真实大小的内存,因为进程申请的内存不一定全部使用,内核在利用这些空闲内存时采取过度分配的策略,假如物理内存为1GB,则两个进程都可以申请1GB的内存,这超过了系统的实际内存大小。当应用程序实际消耗完内存的时候,怎么办?系统需要“杀掉”一些进程来保障系统正常运行。这就触发了OOM Killer,通过一些策略给每个进程打分,根据分值高低决定“杀掉”哪些进程。默认情况下,占用内存最多的进程被“杀掉”。
如果ES与其他服务混合部署,当系统产生OOM的时候,ES有可能会无辜被“杀”。为了避免这种情况,我们可以在用户态调节一些进程参数来让某些进程不容易被OOM Killer“杀掉”。

4.4、索引层

4.4.1、Force Merge

对冷索引执行Force Merge会有许多好处,可以选择在系统的空闲时间段对不再更新的只读索引执行ForceMerge。

上一篇 下一篇

猜你喜欢

热点阅读