腾讯云服务器 Elasticsearch 落地:堆内存、分片与云硬盘的性能账

腾讯云服务器 Elasticsearch 落地:堆内存、分片与云硬盘的性能账

在腾讯云服务器Elasticsearch 落地这件事上,我经手过的集群里,真正被复杂查询压垮的其实很少,绝大多数是上线第一天就把堆内存、分片数量和云硬盘 IOPS 三笔账同时算错了。有人把 64GB 内存的机器堆直接给到 48GB,有人给每天几十万条日志的索引建了 5 个主分片还天天滚动新索引,也有人把热数据放在最低档的云硬盘上,然后反过来问我为什么查询慢。这篇不讲倒排索引原理,只算工程账,讲清楚哪些参数是可以查的、哪些坑是必须提前避开的。

堆内存:一半物理内存与 31GB 这条线

Elasticsearch 的内存账其实可以压缩成一句话:JVM 堆不要超过物理内存的一半,剩下的留给 Lucene 段文件和操作系统的页缓存。 这句话被说了无数遍,但真正照做的人不多,因为堆给大一点,JVM 里看起来更宽裕,启动也不报错,短期没有任何症状。问题在于 Elasticsearch 的查询性能很大一部分依赖文件系统缓存,Lucene 的段文件由操作系统缓存,堆只管对象、聚合桶、协调开销这些。你把 64GB 内存的机器堆给到 48GB,页缓存只剩十几 GB,查询一上量就是磁盘随机读,延迟直接起飞。

31GB 这条线值得单独拎出来说。JVM 在堆小于约 32GB 时可以使用压缩普通对象指针(Compressed Oops),对象引用按 4 字节寻址,空间利用率高;一旦堆越过这个阈值,引用变回 8 字节,同样的对象要占更多内存,你多给的那点堆会被指针膨胀吃掉。设置 `-Xms32g -Xmx32g` 经常还不如 `-Xms31g -Xmx31g` 划算,这正是很多人想不明白的地方。所以 64GB 的云服务器 CVM,堆落在 30g 到 31g 是常见做法;128GB 的机器,如果一定要给大堆,就要接受关闭压缩指针的事实,按 64g 以上去算,并配合更多内存的规格。

还有三件小事别漏:-Xms 和 -Xmx 必须设成相等,避免运行期动态扩缩;swap 尽量关掉,或者在 elasticsearch.yml 里开 bootstrap.memory_lock: true 并在系统层执行 swapoff,否则一次内存交换就能让节点假死几十秒;堆别设成 50% 以下太少,堆太小会导致频繁 GC,尤其是聚合和大量小索引的场景,堆给 8g 在这种负载下是撑不住的。

分片规划:默认分片太多是原罪

分片是 Elasticsearch 的并行单元,也是最容易被滥用的东西。老版本默认每个索引 5 个主分片,很多人的日志按天滚动,一天一个索引,一年就是三千多个分片,再加上副本翻倍,集群状态内存被撑爆,主节点光维护集群状态就忙不过来。每个分片本身有固定开销,分片不是越多越快,而是越多越贵。 经验做法是给每个分片定一个目标大小,搜索场景单分片 10GB 到 50GB 之间,日志场景偏 30GB 到 50GB,然后按数据总量反推主分片数,而不是照着默认值抄。

反推的公式很朴素:预估索引总量除以目标分片大小,向上取整,再取一个不太难看的数。比如一天 6GB 的数据,目标单分片 30GB,那就是 1 个主分片足够;一天 80GB,目标 40GB,那大概 2 到 3 个主分片。别为了"看起来能并行"就切成十几片,每片几 GB,段合并和查询协调的开销反而更重。另外一个老生常谈是分片数一旦确定就不能通过改配置调小,只能 reindex 或者用分片路由重建,所以前期宁可少而大,后期再拆。

分片规划还要配合索引生命周期管理(ILM),热节点放最近的索引,温节点放几周前的,冷节点放归档数据,到点直接删除。腾讯云服务器上跑这套,ILM 的 rollover 条件要同时看大小、文档数和时间三个维度,只按时间滚动最容易出大分片。

refresh、translog 与写入吞吐的三方拉扯

很多人以为写入慢是磁盘不行,其实是 refresh 太频繁。Elasticsearch 默认 refresh_interval 是 1s,意思是每秒把内存里的新文档刷成一个新的 Lucene 段,让文档可以被搜到。每刷一次就多一个小段,段越多,后续合并压力越大。批量导入数据时,正确做法是把 refresh_interval 临时设为 -1,导入完成后改回来再手动 _refresh;索引的 number_of_replicas 也可以在导入期间设为 0,导完再设回 1,避免边写边复制,写入吞吐能明显好转。

translog 是另一条安全绳,它保证没落盘的文档在节点崩溃后还能恢复。默认 index.translog.durability 是 request,每条请求都 fsync 一次,最安全也最慢;批量导入场景可以临时改成 async 并调大 index.translog.sync_interval,用一点数据风险换吞吐。但要记住 async 意味着宕机可能丢最近一小段数据,这个开关只能用在可重导的批处理上,绝不能用在订单、支付这类不能丢的写入上。

写入吞吐还受合并策略限制。段合并是后台进行的,会持续吃磁盘 IO 和 CPU,段越大合并越重,会出现周期性的 IO 毛刺。机械盘或者普通云硬盘上,建议把 index.merge.scheduler.max_thread_count 设成 1,避免多个合并线程把磁盘打满;SSD 上可以放宽到 CPU 核数的一半左右。

mapping 与字段膨胀:看不见的内存杀手

动态映射是个双刃剑。默认情况下,一个从没见过的字段进来,Elasticsearch 会自动猜类型,字符串常常同时生成 text 和 keyword 两个子字段,字段一多,映射就膨胀,集群状态占用的堆越来越大,最后表现为莫名其妙的 GC 和主节点压力。日志类场景字段尤其容易失控:不同业务往同一个索引塞各自的字段,几百个字段很快就有了。

比较稳的做法是提前写索引模板,把 dynamic 设成 strict 或者 false。strict 会让写错字段的文档直接报错,适合字段固定的业务;false 会忽略未知字段但仍写入文档,适合日志这类字段会变的场景。真正不需要检索的字段,可以考虑 index: false 或者 doc_values: false 省空间;纯过滤用的字符串字段用 keyword 就够了,别让它再生成一个没用的分词字段。字符串字段还应该配合 ignore_above,把超长的内容排除在索引之外,防止个别超长字段把段撑大。

字段膨胀的危害往往在出事前完全看不出来,等索引了几个月的日志再想改 mapping,就只能靠 reindex 重建。所以 mapping 这件事一定要在上线前定型,越早越便宜。

云硬盘 IOPS 与段合并毛刺

云硬盘 CBS 的性能账经常被忽略。不同档位的云硬盘规格在随机读写 IOPS 和吞吐上有明确上限,而且这个上限往往还跟挂载的实例规格、是否开启额外性能挂钩。Elasticsearch 是典型的随机读写重负载,尤其是段合并阶段,会同时产生大量顺序写和随机读。你给一个 4 核 8G 的小规格 CVM 配一块入门级云硬盘,然后期望它能扛住每秒几千次的写入,这不现实。

选盘的思路是看两个指标:目标写入的文档数换算成每秒的段写入量,再对照云硬盘的随机写 IOPS 上限留出余量。热数据放在增强型 SSD 上,温冷数据可以退到性能略低的盘或者对象存储。千万别只看容量不看性能档位,同样是一块云硬盘,性能和价格可以差出一个数量级,选错比容量不够更难受。

段合并的毛刺要提前在监控里能看见:定期出现写入延迟尖峰、磁盘 util 拉满、查询 P99 抖动,这三个信号同时出现通常就是合并在吃 IO。应对办法是控制索引的写入节奏,避免大量小段堆积;也可以给合并限速,牺牲一点合并速度换写入稳定性。

磁盘水位:集群变只读的真相

集群突然所有写入报只读错误,八成是磁盘水位触发了。Elasticsearch 有几个水位线:低水位默认 85%,到线后不再往这个节点分配新分片;高水位默认 90%,会尝试把分片挪走;到了泛洪水位默认 95%,索引直接变成只读,写入报 index read-only 类错误。这个机制本意是保护磁盘不被打爆,但在磁盘吃紧时会让业务瞬间停写。

处理顺序是先把磁盘腾出来,再解除只读。腾空间可以删过期索引、把老的段搬运到冷层,或者临时扩容。解除只读要显式清掉索引的只读块设置,否则它不会自动恢复。更根本的做法是把水位线当作运维指标去盯,别等到 95% 才反应。下面这张表是按业务写入量级给的匹配参考,实际规划时还要结合副本数和字段膨胀率往上留余量。

日增数据量级 目标单分片大小 主分片数思路 云硬盘选型方向

1GB 以内 10GB 到 20GB 单索引 1 到 2 个主分片,按周滚动 入门高性能云硬盘,关注容量

1GB 到 10GB 20GB 到 30GB 按天索引,每索引 1 到 3 个主分片 SSD 云硬盘,关注随机 IOPS

10GB 到 50GB 30GB 到 40GB 按天或半天索引,按数据量反推 增强型 SSD,留意吞吐上限

50GB 到 200GB 30GB 到 50GB 按小时索引,配合 ILM 冷热分层 增强型 SSD,热节点优先高 IO

200GB 到 1TB 40GB 到 50GB 冷热分离,热节点用高 IO 盘 多节点分散,避免单盘打满

1TB 以上 40GB 到 50GB 冷层归档,热层只保留近数据 热盘加对象存储备份组合

角色分离、快照备份与深分页

集群角色分离是从小规模走向稳定的一道坎。生产集群建议至少有 3 个专用主节点,只做选主和集群状态管理,不承担数据读写;数据节点专心吃磁盘和内存;查询量大的场景再加协调节点,专门做查询的汇总和分发。混布在开发环境没问题,但一旦上了生产,一个被写入压满的数据节点同时又是主节点,整个集群可能因为选主超时而抖动。

备份一定要做快照,而且要存到对象存储 COS 上,不要只存在本地云硬盘。快照是增量的,第一次全量之后,后续只传变化的段,配合快照生命周期策略定期执行,既省空间又能快速恢复。没有快照的集群,本质上是在赌硬盘不会坏。

深分页是查询侧最典型的坑。from + size 的分页在 index.max_result_window 默认一万条处就封顶了,翻到后面会直接报错;真要做全量导出用 scroll,做在线深翻页用 search_after 配合 PIT。聚合的坑也类似,高基数的 cardinality 聚合很吃内存和 CPU,多层嵌套聚合会让协调节点吃不消,需要分页聚合的场景用 composite 聚合来做游标。这些不是配置问题,是查询写法问题,改一处能省下整台机器的资源。

FAQ

Q:单台云服务器能跑 Elasticsearch 生产集群吗?

A:技术上能,但风险很高。单节点没有副本落点,索引会一直是黄色状态,节点一挂数据就不可用。真要单机,至少要接受不能容灾,并且把快照备份做扎实,同时在同地域预留一台可快速接管的实例。有条件还是建议三节点起步,用不同可用区分散。

Q:堆内存到底给多大最合适?

A:先守住两条硬线,一是不超过物理内存的一半,二是不越过 31GB 的压缩指针阈值。64GB 内存给 30g 到 31g,128GB 内存可以给到 31g,再往上就要接受失去压缩指针的代价。然后根据实际 GC 日志微调,重点看老年代回收频率和停顿时间,而不是凭感觉。

Q:为什么我的集群老是变红或者写入被拒?

A:按顺序查四件事:磁盘水位是不是过了泛洪线导致索引只读;主分片是不是有未分配的分片,通常是节点数少于副本需求;堆是不是被字段膨胀或大聚合撑爆出现 GC 停顿;写入线程池队列是不是满了,那是写入速度超过了节点处理能力。分清是资源问题还是配置问题,处置方式完全不同。

Q:分片建多了能改少吗?

A:不能直接改。主分片数在索引创建后就固定了,只能通过 rei

3 .0