当涉及管理和优化千亿级数据时,性能优化是至关重要的。在这篇文章中,我们将探讨一些关键的性能优化方向,结合我积累的上百个优化参数的经验,为大规模数据管理提供有价值的见解。
千亿级数据的管理和优化是一项复杂而挑战性的任务。然而,通过有效的性能优化策略,结合数据分区、压缩、索引、缓存、并行处理、资源管理以及数据清洗等方法,我们可以实现卓越的性能,提高数据处理效率,同时确保数据的安全性和合规性。这些优化方向的结合将为大规模数据管理提供可行的解决方案,为数据驱动的决策和应用提供可靠的支持。
希望这篇文章为您提供了有关性能优化的重要见解,帮助您更好地管理千亿级数据。无论您是处理金融数据、大数据分析、人工智能、或其他领域,性能优化都将对您的项目和业务产生积极的影响。
在本文中,我给大家提供近百个实用的优化参数,涵盖集群级别、索引级别、客户端级别,三个层面。这些都是两年的几千亿集群优化经验,希望能帮到大家。
参数限制 | 作用范围 | 参数 | 效果 | 参数取值说明 | 相关链接 | 备注 |
集群参数 | ||||||
禁止使用 wildcard | cluster | PUT _cluster/settings { "transient": { "search.allow_expensive_queries": false } } |
禁止使用通配符条件匹配。这通常是一个非常耗费资源的操作。可以考虑使用match来实现同样的效果。 | |||
禁止使用 通配符匹配索引名称 | cluster | PUT /_cluster/settings { "transient": { "action.destructive_requires_name": true } } |
开启此参数的目的是:防止应用端通配符匹配索引名称,导致单次检索中命中超多个索引,超多个分片。开启后,不能在通过类似于:twitter_tweet_202302*这样来对索引执行操作。 | 删除索引。不允许适用通配符来删除。应该设置参数为false | Index management settings | Elasticsearch Guide [8.10] | Elastic | |
集群是否允许自动创建索引 | cluster | PUT /_cluster/settings { "transient": { "action.auto_create_index": false } } |
不允许自动创建索引 | 取值false,为不允许自动创建索引。通常来说,自动创建的索引,字段类型会有冗余。 | Index management settings | Elasticsearch Guide [8.10] | Elastic | |
限制同一个索引的分片,不分配到同一台机器上 | cluster | cluster.routing.allocation.same_shard.host: true | 控制分片分配到不同机器上。解决一台物理机部署多个es节点,分片被分配到同一台机器上多个分片,导致数据倾斜的问题 | 默认为false。单台物理机部署一个节点不需要设置。 | ||
限制同一个节点上,单个索引最大可分配分片数 | cluster | index.routing.allocation.total_shards_per_node:2 | 控制单个索引,在每个节点上分配的最大分片数。从而保证数据分布足够的均匀 | 如果将将参数设置成1,则可以保证数据分布绝对的平衡。但是在在节点数小于单个索引的总的分片数的时候,会有分片不能被分配。导致集群为red或者 yellow状态。在索引在集群节点数远大于单个索引总的分片数的时候,适合设置为1。 | ||
设置集群在什么时候开始平衡 | cluster | cluster.routing.allocation.allow_rebalance | 此参数,解决在集群长期处于yellow状态下,导致集群分片分配不平衡的问题。可以设置成indices_primaries_active,只要主分片都在,就可以去做平衡。 | always - 一直允许重新平衡。 indices_primaries_active - 只有当集群中的所有的 primaries 被分配。 indices_all_active - ( 默认 )只有当集群中的所有分片 ( primaries and replicas ) 被分配。 |
||
cluster | cluster.routing.allocation.node_concurrent_recoveries | 限制每个节点可以同时执行的恢复操作数 | 该参数用于限制每个节点并发恢复的分片数。默认值为2,可以通过修改该参数来限制每个节点并发恢复的分片数,以避免节点过载 | |||
cluster | indices.recovery.max_bytes_per_sec | 限制每个节点的恢复操作的速度,以避免恢复过程过多占用网络带宽 | 该参数用于限制每个节点恢复数据时的最大网络吞吐量。默认值为40mb,可以通过修改该参数来限制每个节点恢复数据时的最大网络吞吐量,以避免节点过载。 | |||
cluster | cluster.routing.allocation.cluster_concurrent_rebalance | 限制集群重新平衡操作的并发数 | ||||
cluster | indices.store.throttle.max_bytes_per_sec | 控制写入磁盘的速度。默认情况下,此限制是关闭的,可能会导致磁盘写入速度过快,占用大量的磁盘带宽 | ||||
cluster | cluster.routing.allocation.balance.shard | 控制分片的负载均衡策略。默认情况下,这个限制是开启的,Elasticsearch会自动将分片分配到负载较低的节点上。可以将这个限制关闭,以避免负载均衡带来的网络带宽占用和资源消耗 | ||||
cluster | http.enabled:false | 在数据节点上添加此配置,可以保证 data 节点服务器只需处理创建/更新/删除/查询索引数据等操作 | ||||
每天做forceMerge | cluster | POST mblog_info_202212-000079/_forcemerge?max_num_segments=5&flush=true | 先合并成少量段,来释放堆内存空间。提升检索性能。此过程可以有效的降低检索过程中的随机IO的次数,会对检索有非常大的提升! | 确定数据更新的频率。确定多长时间以前的数据不会发生修改和新增,合并成一个段。对于一周前的数据强制合并成5个段。 max_num_segments=1 会非常耗时,3天-5天。且过程中需要花费多一倍的磁盘空间。需要注意,数据的变更,如果做了forceMerge的优化,再对数据进行修改,特别是大量的修改,会降低性能 | ||
设置每个节点可以使用的核心数 | cluster | node.processors: 20 | 对于单台物理机部署多个ES节点实例的情况。特别需要注意此参数。默认情况下,es会查看服务器的实际核心数,但是不会考虑多个节点的情况。所以在集群启动,初始化线程池的时候,对开辟出来过多的线程数。 | 假如一台服务器逻辑核心数是96,假如部署两个ES节点实例,则每个节点配置 48 | ||
节点分片延迟分配 | index | PUT _all/_settings { "settings": { "index.unassigned.node_left.delayed_timeout": "30m" } } |
在节点离线时,延迟重新分配分片。集群有可能因为短暂压力,或者长时间的GC,导致节点无法被通信到,导致某个节点假离线。假离线,该节点上的分片,会重新在其它节点上分配。此过程会占用较多的网络带宽和磁盘的IO。且在假离线恢复以后,重新加入进来以后,会将在其它节点上恢复过的分片,标记删除,且因为分片数变少,而重新rebalance,此过程又会占用网络带宽和磁盘IO。所以不妨修改为更长的时间。 | 根据集群的实际情况,我们的节点通常会在半个小时以内完成恢复。建议设置为30分钟 | ||
节省堆空间 将FST文件放在堆外 | index | index.store.type:"niofs" | 去掉此参数,会节省大量的堆内存。会将FST文件,从JVM堆中移到堆外。节省堆空间。 | 将索引中该参数去掉。ES7.X 默认会FST放在堆外。此参数非常重要,我们当前集群面临最大的问题就是JVM堆空间不足的问题 | ||
索引操作 | ||||||
将索引改为read only | index | PUT /my-index/_settings { "index": { "blocks": { "write": true } } } |
对数据做一个forceMerge,然后再改成read only, 则不会再产生新的段 | 只适合对不再发生改变的数 ,做此操作! | ||
节省磁盘空间角度优化 | ||||||
字段不用于检索(节省磁盘空间) | index | PUT my_index { "mappings": { "properties": { "price": { "type": "integer", "index": false } } } } |
字段如果不需要检索就设置 "index":false | |||
字段不做评分(节省磁盘空间) | index | PUT my_index { "mappings": { "properties": { "content": { "type": "text", "norms": false } } } } |
如果不关注分数,则可以关闭全局分数相关的存储 "norms": false | |||
字段不做phrase匹配(节省磁盘空间) | index | PUT my_index { "mappings": { "properties": { "content": { "type": "text", "index_options": "freqs" } } } } |
不存储词句相关的内容 | |||
关闭 doc_value(节省磁盘空间) | index | PUT my_index { "mappings": { "properties": { "name": { "type": "keyword", "doc_values": false } } } } |
所有支持doc value的字段都默认启用了doc value。如果确定不需要对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用doc value以节省磁盘空间: 禁用doc values,空间占用量下降10%左右 |
|||
缓存相关 | ||||||
在请求的时候,优先对近三个月的数据进行缓存 | client | request_cache | 提高缓存利用率。 | 设置是否缓存查询结果。类似于数据推送,则没有必要缓存结果。因为缓存的可用空间是有限的。考虑只缓存 近三个月的查询的数据。 | ||
缓存过期时间 | cluster | indices.queries.cache.expire | 设置查询缓存中的项的过期时间,以避免过多的内存消耗。 | 在集群的每个数据节点设置 | ||
缓存过期时间 | cluster | indices.fielddata.cache.expire | 设置字段数据缓存中的项的过期时间,以避免过多的内存消耗 | 在集群的每个数据节点设置 | ||
QueryCache | cluster | indices.queries.cache.size | 控制查询缓存的大小。默认情况下,查询缓存大小是10%的堆大小 | |||
FieldDataCache | cluster | indices.fielddata.cache.size:10% | 在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。根据es监控观察,可以看出来我们集群中的fieldDataCache经常被换出!适当调大有没有效果。该参数默认是 30% | 调研,将fieldDataCache 单独存储,放在redis中的可能性(可以考虑自研)。这部分可能大大提升聚合的性能! 闻海2.0集群因为堆空间不足,给到了10%,问题是此部分的换出非常的严重,缓存的利用率非常低。 |
||
ShardRequestCache | indices.requests.cache.size: 1% | |||||
有效利用filter缓存 | client | { "query": { "bool": { "filter": [ { "term": { "status": "published" } }, { "range": { "published_at": { "gte": "2022-01-01" } } } ] } } } |
在bool组合查询中,应该将有可能会缓存的内容放在filter中(一定要这样做,要保证放在filter里边的内容足够的干净)。用来命中缓存,并且提高缓存的利用率。如果滥用缓存,实际上只会带来更差的检索效果。以为此部分是使用LRU的置换策略,滥用的查询结果会冲刷有效的缓存数据。应该用监控去监控此部分的置换情况。 | 在上面的例子中,bool 查询使用 filter 子句将两个 Filter 对象组合在一起,每个 Filter 对象都是一个单独的 JSON 对象,可以使用相应的 Filter 构建器构造出来。也就是说它们是分来缓存的。 { "term": { "status": "published" } }的结果是一个filter对象,{ "range": { "published_at": { "gte": "2022-01-01" } }也是一个缓存对象。 | ||
预热数据 | index | index.store.preload: ["nvd","dvd"] | 预热文件到osCache中 | 需要调研,哪些是最重要的,最浪费时间的,然后将它们预热到osCache中。 如果文件系统缓存不够大,则无法保存所有数据,那么为太多文件预加载数据到文件系统缓存中会使搜索速度变慢。 | ||
JVM相关 | ||||||
切换垃圾回收器 | cluster | -XX:+UseG1GC | 使用G1垃圾回收起,G1更适合做大堆的垃圾回,并且擅长的是低延迟垃圾回收。减少STW的时间。 | |||
垃圾回收最大停顿时间 | cluster | -XX:MaxGCPauseMillis=50 | 设置GC过程中的最大GC时间 | |||
设置最小堆空间 | cluster | -Xms32727m | 此值是对象指针压缩失效的极限值。最大可以给堆设置这么大的堆空间。相比较之前每个节点会多出1G的堆空间。 | |||
设置最大堆空间 | cluster | -Xmx32727m | 此值是对象指针压缩失效的极限值。最大可以给堆设置这么大的堆空间。相比较之前每个节点会多出1G的堆空间。 | |||
索引写入方面 | ||||||
索引刷新间隔 | index | index.refresh_interval | 通过延长 refresh 时间间隔,可以有效地减少 segment 合并压力,提高索引速度 | 120s ,增加了数据的延时性。 | ||
调小写入堆内存缓冲池 | cluster | indices.memory.index_buffer_size: 10% | 控制内存索引缓冲区的大小。默认情况下,索引缓冲区的大小是10%的堆大小。集群中通常为31G的堆空间,百分之十是3G的空间。集群中实时集群有40台机器,80个节点。索引按天维护,每天的索引分片应该只有30 - 40个 分片。控制每天的分片绝对的均匀分布,实际上每台机器就一个分片承担写入。3G的堆空间会有盈余。此部分可以测试一下,看看降到多少更合理。 | 建议调小。调小多少,根据测试写入效果来确定。在不影响正常写入的情况下,尽可能的压缩此值,留更多的堆空间用于检索。当将Index Buffer的大小调小时,可能会产生以下影响: 写入性能下降:由于Index Buffer是用于缓存写入的文档,如果将其大小调小,那么文档的缓存空间就会减小,从而导致写入性能下降。因为写入文档需要频繁地将文档写入磁盘,如果缓存的文档比较少,那么就需要频繁地将文档从磁盘中读取到内存中,导致写入性能下降。 索引性能下降:当Index Buffer的大小减小时,如果索引过程中需要使用Index Buffer,那么就会因为Index Buffer不足而需要从磁盘中读取文档,导致索引性能下降。 内存占用减小:由于Index Buffer占用了一定的内存空间,如果将其大小减小,那么就可以减少ES节点的内存占用,从而避免出现内存不足的情况。 |
||
多个字段提升检索性能 | index | 多个字段开启 copy_to | 检索的字段越多,检索性能越差,以为要IO的次数越多。可以考虑使用copy_to来解决此问题。 | 代价是花费额外的磁盘空间。特别是将内容很长的字段A和字段B合并成一个字段。则需要额外花费 A+B的存储资源。写入性能降低 | ||
预热数据,提升聚合和排序的性能 | index | PUT zmc_index/_mapping { "properties": { "field": { "type": "keyword", "eager_global_ordinals": true } } } |
预热全局序数。全局序数有助于聚合分析。问题是集群在面对大规模的数据的时候,堆空间不足,不足以支撑预热全部需要聚合的数据 | 暂时我们的集群不适用。 | ||
写translog异步 | index | PUT /my_index { "settings": { "index": { "translog": { "durability": "async" } } } } |
设置translog日志刷新为异步。默认情况下为 request,默认情况下,每次请求,都会将数据刷到磁盘上。可以通过指定为异步,可以很大程度上提升写入能力。代价就是会冒一定的数据丢失风险。假如集群足够的稳定,节点不会掉线,则可以改成异步。 | 在创建索引时使用 index.translog.durability 参数来设置刷新策略。可以设置为 "request" 或 "async"。 | ||
index | PUT /my_index { "settings": { "index": { "translog": { "sync_interval": "120s" } } } } |
日志刷新间隔,可以提升写入性能 | 默认为 5s。修改为 120s。 | |||
index | PUT /my_index { "settings": { "index": { "translog": { "flush_threshold_size" : "512mb" } } } } |
日志刷新间隔。此参数用来做数据担保。是最大的阈值,单个分片的日志量到达这么大,会触发flush。如果给的过大,会花费过多的linux 内存。如果写入压力过大,则考虑使用的默认的参数。 | 线上使用的2048m,新架构建议使用默认的参数 512m,观察写入的性能。 | |||
index | "index.merge.policy.segments_per_tier":"24" | 减少段合并的频率。 | 属性指定了每层分段的数量,取值越小则最终segment越少,因此需要merge的操作更多,可以考虑适当增加此值。默认为10 | |||
检索层面 | ||||||
修改最大并发数 | client | maxConcurrentShardRequests | 搜索一个月的时候,可以改为 25 | 参数默认为5,大于则请求会在node上排序,分批次执行 | ||
修改协调节点处理单个请求的最大分片的并发度 | client | batchedReduceSize | 控制协调节点批量发送分片的最大个数,主要是控制协调节点内存的消耗而提供的一种保护机制 | 待验证。理论上来说,增大此值,可以增加聚合的速度,代价是增加协调节点内存的消耗。 | Change the default batched_reduce_size of search requests · Issue #51857 · elastic/elasticsearch · GitHub | |
聚合请求应该限制返回的桶数 | client | { "query": {}, "aggs": { "my_agg": { "terms": { "field": "", "size": 10 } } } } |
对于聚合是一个非常耗费内存的操作,应该限制聚合返回的数据量。通常情况下不应该大于50。达到保护堆内存的效果。如果需要获取全部的聚合结果,应该使用 compositeAggregation | 限制在50以内,特别是在大规模的聚合中查询中 | ||
优化求总量的API | client | GET article_info-000002/_count { "query": { } } |
对于求总量的接口,应该使用count API,而不是agg + cardinality | count API成本代价是极低的,仅仅用到query 阶段就可以将数据返回。而 agg + cardinality 是用来去重操作的, 它是极其耗费性能的,尤其是在涉及命中非常多的分片的时候。它需要将结果在协调节点合并。它不仅仅需要需要query阶段,还需要将数据取回来做计算。 | ||
检索不再使用 script 脚本排序 | cluster | 集群参数 script.engine: false PUT /_cluster/settings { "transient": { "script.engine": false } } |
需要注意的是,动态修改 script.engine 属性可能会影响集群的性能和稳定性,因此需要谨慎评估修改的影响,并确保在生产环境中使用合适的安全措施来保护集群的安全性和可靠性。 | |||
跳过打分且不缓存数据 | client | GET my_index/_search { "query": { "constant_score": { "filter": {}, "query": { "bool": { "must": [ {} ], "must_not": [ {} ] } }, "boost": 0 } } } |
GET sight_video_202303/_search { "query": { "bool": { "must": [ { "constant_score": { "filter": { "query_string": { "query": "北京", "fields": [ "content^1.0" ], "type": "phrase", "default_operator": "and", "max_determinized_states": 10000, "enable_position_increments": true, "fuzziness": "AUTO", "fuzzy_prefix_length": 0, "fuzzy_max_expansions": 50, "phrase_slop": 0, "escape": false, "auto_generate_synonyms_phrase_query": true, "fuzzy_transpositions": false, "boost": 1 } } } } ] } } } |
|||
查询条件时间近似到小时 | client | |||||
提前结束请求,降低请求计算的深度 | client | { "from": 0, "size": 10, "query": { "bool": { "must": [ { "term": { "field_1": {"value": "a" }}}, { "terms": {"field_2": [ "a", "b", "c","f", "e", "f", "g", "h", "i" ]} }, { "range": { "time": {"from": 1, "to": 10}}} ] } }, "sort": [{ "index_sort_field": { "order": "asc"}} ], "track_total_hits": false } |
此操作,可以在检索过程中提前结束请求。降低请求计算的深度,从而达到降低时间附加度和空间复杂度的效果。代价是,不能返回总的数据量。需要业务接受。或者使用异步请求,再将总的数据量返回。 | 需要和业务确认。 | ||
多使用rank字段排序 | client | { "from": 0, "size": 10, "query": { "bool": { "must": [ { "term": { "field_1": {"value": "a" }}}, { "terms": {"field_2": [ "a", "b", "c","f", "e", "f", "g", "h", "i" ]} }, { "range": { "time": {"from": 1, "to": 10}}} ] } }, "sort": [{ "rank": { "order": "desc"}} ], "track_total_hits": false } |
我们在写入数据的时候,做了数据的预处理。根据rank(综合排序值)做根据分数的倒序排序。搭配上,会让请求提前终止。此操作,可以将检索原本检索的时间复杂度从O(n) 降到 O(n/10000) | 需要和业务确认。 | ||
优化查询条件 | client | (A|B) & (C|D) ==> (A&C) | (A&D) | (B&C) | (B&D ) | 做查询条件的等价转换,查询过程中,会对每个条件构造一个bigset来存放结果集,然后再做逻辑运算。通过此类型的转换,可以大大降低堆内存的使用 | |||
terms 优化 | client | 将terms 用 should + term + minShouldMatch=1 替换 | 可优化倒排链合并。 | 此项优化在特定场景有效,当terms里边的内容命中缓存率高的情况下,拆成should 会很有效。场景案例可以参考后边的链接 | Elasticsearch的TermsQuery慢查询分析和优化-阿里云开发者社区 | |
去重聚合(cardinality)近似聚合 | client | GET twitter_user/_search { "size": 0, "query": { "match_all": {} }, "aggs": { "NAME": { "cardinality": { "field": "user_name", "precision_threshold": 500 } } } } |
通过调整精准度,来降低内存的使用,以及提升去重聚合的速度 | 此参数默认为3000,最大值为40000。取值越大,精准度就越大,内存花费就越大。其中取值10000的时候,偏差率就小于百分之一了。这里我测了500和10000的值是相近的。比较靠谱一点。 | Cardinality aggregation | Elasticsearch Guide [8.10] | Elastic |