ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Elasticsearch分片数量与副本数量上限及容量规划

Elasticsearch分片数量与副本数量上限及容量规划

分片数量和副本数量这两个参数,几乎是每个接触 Elasticsearch 的人在建第一个正式索引时都会被要求填的两个数字,也是最容易被当成填空题随手带过去的两个数字。我见过太多索引在创建时number_of_shards写成 5、number_of_replicas写成 1,然后数据量涨上来、节点挂掉一台、查询延迟开始抖动,才回过头来问“分片数上限到底是多少”“副本到底该配几个”。这时候往往已经晚了——主分片数在索引创建之后就是不可改的,副本数虽然能改,但改完要触发一次全集群的数据搬迁,赶上业务高峰就是一次事故。

这篇文章不讲入门的增删改查,专门把这两个参数掰开:分片数量在不同层面上的“上限”分别是什么、这些上限是怎么被算出来的、如何从数据量和节点数反推一个合理的分片数、副本数量在可用性和写入延迟之间怎么权衡,以及已经配错了之后有哪些补救路径。适合正在做索引规划的人、被集群变黄折腾过的运维,以及想知道“为什么我的集群明明没多少数据但状态更新很慢”的开发。文中的数值和命令我都在实际集群里跑过,会标注哪些是官方默认值、哪些是经验值,你可以直接拿去对照自己的环境。

1. 分片不是越多越好的旋钮:先看清它在消耗什么

1.1 一个分片背后是一个完整的索引引擎实例

很多人对分片的理解停留在“数据太大,切开存”这一层,觉得分片就像给文件分了几个文件夹,多分几个只是多几个目录名。实际不是。每一个分片在底层都是一个独立的索引引擎实例,它有自己的段文件集合、自己的事务日志、自己的段合并调度、自己的查询缓存和写入缓冲。也就是说,一个索引被切成 10 个分片,就等于在节点上跑了 10 套相对独立的存储与检索结构,只不过它们共用同一份进程堆内存和同一个线程池。

这个认知非常关键,因为它决定了分片数量带来的开销是线性叠加的,而不是像很多人想的那样“加个名字而已”。你把一个 5 分片的索引拆成 50 分片,硬件不变的前提下,每个分片分到的写入缓冲变小、段合并更零碎、查询时需要跨更多分片做扇出再归并,单次查询的协调开销显著上升。在数据量很小的时候这种开销看不出来,因为每个分片里就几万个文档,几分钟就能合并完;一旦数据量上来,问题会集中爆发。

1.2 分片数量真正在吃掉的三种资源

第一是堆内存。每个分片都有一批常驻的对象,包括段的元数据、字段信息结构、分片级别的请求缓存等。这些开销和分片里存了多少数据关系不大,是一个接近固定的底噪。业内比较通行的经验值是每 GB 堆内存承载不超过 20 个分片,这个数字在很多公开的容量规划材料里都能看到。按这个算,一台给 16GB 堆的节点,理论上不该承载超过 300 个分片,而这还只是“不炸”的线,不是“跑得好”的线。

第二是文件句柄。每个分片在磁盘上是一组段文件,段合并过程中新旧文件会同时存在一段时间,句柄占用会短暂翻倍。分片数越多,打开的文件越多,系统级的文件描述符上限如果没调,很容易在某个合并高峰撞到 “Too many open files”,然后节点失联。这个坑我在早期项目里踩过一次,现象是节点每隔几小时就从集群里掉出去一次,查了半天才发现是分片数乘上每分片段数超过了默认上限。

第三是集群状态。每个索引、每个分片的路由信息、映射信息都要被主节点维护并广播给所有节点。分片数越多,集群状态对象越大,主节点处理一次状态变更(比如某个节点下线触发的重平衡)就越慢,状态在节点间传播的延迟也越长。当分片数到几万的量级,你会明显感觉到创建索引变慢、集群健康状态刷新滞后,本质就是主节点在序列化和广播一份巨大的状态对象。

1.3 副本不是备份盘,它是另一份可查询的活索引

副本最常见的误解是“相当于备份”。备份是离线的、滞后的、不参与读写的;副本是实时的、在线的、参与写入确认和查询分摊的。写入一条文档时,请求先到主分片,主分片处理完立刻并行转发给所有副本,等副本都写成功(具体等几个由wait_for_active_shards控制)才向客户端返回成功。这意味着副本数直接乘在了写入链路上,副本越多,一次写入需要等待的确认越多,尾部延迟越容易被最慢的那个副本拖住。

但副本带来的收益也是实打实的:查询可以在主分片和副本之间轮询分摊,读吞吐近似随副本数线性增长;某个节点故障时,它上面的副本失效但主分片或其他副本还在,集群能自动把副本提升或者重新分配,服务不中断。所以副本数的本质是在“写放大”和“读并发加可用性”之间做交换,不是简单的一句“配 1 就行”。

2. 分片数量的上限到底在哪:三个层面各有各的天花板

2.1 单索引层面:主分片数创建即定死,默认上限 1024

先说最硬的那条。一个索引的主分片数量由index.number_of_shards决定,这个参数在索引创建完成后不可修改,任何“我想把 5 分片改成 10 分片”的想法,在不重建索引的前提下都无法实现。从 7.x 版本开始,这个参数的默认值是 1,而更早的版本默认是 5,很多老项目里遗留的 5 分片模板就是从那个年代带过来的,如果直接沿用,在新版本上会显得分片偏多。

单索引的分片数还有一个默认的最大值限制,通常在 1024 这个量级。这条限制存在的理由不是磁盘不够,而是防止有人用一个索引就把整个集群状态撑爆。想突破它需要改系统级配置,但我的建议是根本不要往这个方向想——单索引分片数超过几百,这个索引的查询扇出和状态开销已经很难接受了,正确做法是拆索引或者用滚动索引,而不是把一个索引的分片数往四位数堆。

2.2 集群层面:每个节点能挂多少分片有个软上限

除了单索引限制,集群级别还有一个“防呆”参数,控制每个数据节点平均能承载多少个分片,默认值是 1000。它的判定逻辑大致是:集群里所有索引的主分片加副本总数,除以数据节点数,如果超过这个阈值,新建索引或者分配分片就会被拒绝,报错信息里会明确告诉你超出了每个节点的分片上限。

这个参数的初衷是好的,防止一个配置失误的模板批量创建出几十万个分片把集群拖死。但它是个平均值,不看实际分布,所以在节点数很少的集群上特别容易误伤。比如一个三节点的小集群,按默认 1000 算,总容量是 3000 个分片,听起来很多,但如果你的业务是每天生成一个 5 分片、1 副本的索引,一天就是 10 个分片,一年就是 3650 个,不到一年就会撞线,而且这时候每个索引里可能只有几百 MB 数据。

调整它的方式很直接:

PUT _cluster/settings { "persistent": { "cluster.max_shards_per_node": 2000 } }

提示:调高这个值只是解除“拒绝创建”的保护,并不会让集群跑得更快。调之前先确认堆内存和文件句柄真的撑得住,否则只是把明确的报错换成了隐性的性能崩塌。

2.3 物理层面:堆、句柄、状态大小构成真正的天花板

真正决定你能挂多少分片的,是上一节讲的那三项资源的组合,而不是任何一个可以配置的数字。可以用一张表把三个层面的限制放在一起对照,方便判断自己的集群到底卡在哪一层:

限制层面典型默认值触发后的表现是否建议调高
单索引主分片数1024 左右创建索引直接失败不建议,应改索引设计
每节点平均分片数1000新建索引或分片分配被拒绝可小幅度调,需评估资源
堆内存承载约 20 分片/GB 堆查询变慢、GC 频繁、节点假死不可调,只能加分片或加节点
文件句柄系统 ulimit 决定节点掉线,日志报 too many open files可调,但治标不治本
集群状态规模无固定值主节点 CPU 高、状态更新滞后不可调,只能减少分片

看清这张表之后,很多“上限是多少”的问题就能自问自答了:如果你的集群有 6 个数据节点、每节点 32GB 堆,按 20 分片/GB 算是 3840 个分片的堆容量上限,而按每节点 1000 的软上限算是 6000,那么真正的瓶颈在堆,软上限反而不是最先撞到的。反过来,如果节点堆很小但数量很多,先撞到的往往是软上限。

3. 从数据规模和节点数反推:分片数该怎么算

3.1 单个分片多大的区间是合理的

在讨论分片数之前,得先有一个“单分片目标大小”的锚点。公开的容量规划建议里,比较通行的区间是单分片 10GB 到 50GB。这个区间的下限来自固定开销的摊薄:分片太小,前面说的那批常驻对象的成本占比就高,硬盘上全是元数据,真正存数据的比例低;上限来自恢复和合并的成本:分片太大,一次段合并的 IO 峰值更高,节点故障后重建这个分片的耗时也更长。

不同场景的取值还要再收窄一些。日志和时序类数据,写入量大、查询多是按时间范围扫描、很少更新,可以往 30GB 到 50GB 靠,分片少一些能减少扇出;面向用户的搜索类索引,查询模式复杂、经常需要跨字段打分排序,建议往 10GB 到 30GB 靠,单分片小一点合并更轻、缓存命中更好;更新极其频繁的索引(比如库存、状态类),单分片太大反而会让每次合并的代价变高,也要往小的一侧压。

3.2 一套可以直接照做的估算流程

我把平时用的流程整理成五步,你按顺序走一遍基本不会出大错。

第一步,估数据总量。不是当前量,是索引一个完整生命周期结束时的量。比如按天滚动的索引保留 30 天,日增原始数据 8GB,主分片不压缩的情况下大约 8GB 乘 30 天,再考虑副本和段合并的临时膨胀,磁盘要多留 20% 到 30%。

第二步,定单分片目标大小。按上面说的场景选一个数,日志类取 40GB 为例。

第三步,算主分片数。总量除以目标大小,向上取整。假设是 240GB 除以 40GB,得到 6 个主分片。这里有个细节:如果算出来是 1.2,不要取 2,宁可取 1 然后靠滚动索引控制单索引大小。小索引上的多分片是纯粹的浪费。

第四步,算分片总数。主分片数乘上(1 加副本数)。6 个主分片配 1 副本,就是 12 个分片。

第五步,校验。分片总数除以数据节点数,看是否远低于每节点 1000 的软上限;分片总数除以堆总容量(GB),看是否远低于 20;再除以节点数,看单节点分片数是否落在合理范围。三个校验都通过,这个方案才站得住。

举个完整例子:6 个数据节点、每节点 16GB 堆、日增 8GB、保留 30 天、单分片目标 40GB。主分片 6,副本 1,总分片 12,每节点平均 2 个分片,每 GB 堆 0.125 个分片。这个配置非常宽松,实际上意味着你完全可以用更少的节点,或者把单分片目标调小以提高并行度。反过来说,如果你算出来每节点平均分片数已经接近 800,那不是在扩容边缘,是该重新设计索引结构了。

3.3 分片数为什么几乎是不可逆的:三条补救路径的代价

主分片数创建即固定,这是设计约束,不是 bug。真要改,只有三条路,每条都有明确代价。

第一条是重建索引。新建一个目标分片数的索引,然后_reindex把数据搬过去,再切别名。这条最通用,能改分片数、能改映射、能改分析器,代价是搬运期间要双倍磁盘和大量 IO,数据量大时会跑很久,而且如果源索引还在写入,需要处理增量同步。

POST _reindex?wait_for_completion=false { "source": { "index": "orders_v1" }, "dest": { "index": "orders_v2" } }

第二条是拆分。_split能把一个索引的分片数按整数倍扩大,比如 2 拆 4、4 拆 8。它比 reindex 快得多,因为底层是硬链接段文件而不是重写数据。但有前提:源索引必须设为只读,目标分片数必须是源分片数的整数倍,且不能超过创建时设置的 routing 分片数。也就是说,如果你建索引时没考虑过以后要拆,这条路可能走不通。

第三条是收缩。_shrink把分片数缩到原来的因数分之一,比如 8 缩 4。它要求所有主分片先被迁移到同一个节点上,这个过程本身就要搬数据,而且目标索引在收缩期间同样只读。

方式能否改分片数相对耗时前置条件适用场景
_reindex任意改最慢需要额外磁盘通用,还要改映射时首选
_split只能整数倍扩大快索引只读、routing 分片数够分片偏少,想扩容并发
_shrink只能按因数缩小中等分片集中到同一节点、只读分片过多,小索引合并

我的实际经验是,这三种方式里最常用的其实是 reindex,因为业务变化往往不只是分片数的问题。但不管用哪种,都要提前准备好别名切换方案,让上层应用感知不到索引名的变化。

4. 副本数量怎么定:可用性、写延迟和读吞吐的三角权衡

4.1 副本数等于 N 时,写入链路发生了什么

写一条文档,请求先路由到主分片所在的节点。主分片完成本地写入后,把请求并行发给所有活跃副本,等它们都返回成功,才向客户端返回。默认情况下wait_for_active_shards的值是 1,也就是只要求主分片本身活跃,但这只是“等待确认”的下限,实际写入仍然会同步到所有在同步集合里的副本。

所以副本数从 1 加到 2,写入需要等的确认从 1 个变成 2 个,每一次写入的延迟取决于最慢的那个副本节点。如果三个节点里有一个正在做段合并或者 GC,它的响应会变慢,整个写入的 P99 就跟着变差。这是很多人在扩容副本之后发现“写变慢了”的直接原因,不是索引变大了,是尾部延迟被拉长了。

副本的另一个隐性成本是磁盘和恢复时间。副本数翻倍,存储成本翻倍,这还好算;真正麻烦的是节点重启后的分片恢复,副本越多,需要从主分片同步的数据量越大,恢复窗口越长。一个大索引配 2 副本,一个节点重启可能要跑十几分钟才恢复绿色,这期间的查询能力是打折的。

4.2 副本能不能放得下,取决于节点数和分布策略

副本不会被分配到和主分片相同的节点上,这是基本规则。所以副本数的上限实际受节点数约束:3 个数据节点的集群,配 1 副本刚好每个节点上一主一副的合理分布;配 2 副本意味着每个节点要扛三份数据(一个主两个副的组合在三个节点间轮转),磁盘压力直接翻三倍,而且任何一个节点故障都会影响更大比例的分片。

只有一个数据节点的集群配 1 副本,索引会一直处于黄色状态,因为副本无处分配。这不是错误,是正常现象,但很多人第一次看到黄色健康状态会慌。测试环境里正确的做法是把副本设为 0,生产环境则至少保证三个数据节点。

如果节点分布在不同的机架或可用区,还要注意分片分配策略,避免主分片和它的副本落在同一个故障域里。相关参数控制的是同一分片的主副本是否允许共置于同一主机,默认是禁止的,这个默认值不要动。

4.3 生产上几种常见的副本配置组合

场景节点数推荐副本数理由
本地开发、单机测试10副本无处分配,避免索引常黄
小规模生产、读写均衡31允许挂一台,读吞吐翻倍,写放大可控
读多写少的搜索服务5 以上2查询分摊明显,写入量小可承受写放大
写多读少、日志采集3 到 51写入吞吐优先,副本多了写延迟明显
归档、冷数据任意0 或 1查询少,优先省磁盘

还有一种常见做法是热温分层:热节点上的索引配 1 到 2 副本保证服务能力,滚动到温节点后把副本降到 0 或 1,用磁盘换成本。这需要配合索引生命周期策略自动调整副本数,而不是手工改,手工改在大批量索引上几乎不可能维护。

5. 已经配错了怎么办:从症状到补救的完整链路

5.1 分片过多的现场长什么样,怎么一步步查出来

分片过多的症状很分散,容易被误判成别的问题。典型表现是:创建索引偶尔超时、主节点 CPU 长期偏高但数据节点不高、集群健康状态刷新有延迟、节点重启后恢复时间以十分钟计、查询延迟在数据量没怎么涨的情况下缓慢上升。这几个现象同时出现两三个,就该怀疑分片数了。

排查的第一步是看总量和分布:

GET _cat/shards?v&h=index,shard,prirep,state,docs,store,node GET _cat/allocation?v GET _cluster/health?pretty

第一条能看到每个索引的每个分片落在哪个节点、多大、多少文档。重点看两类:一是文档数很少但占用分片名额的索引(比如几十万文档却占了 5 个分片),二是单分片大小明显偏小的索引。第二条看每个节点的分片数和磁盘占用,能立刻发现某个节点扛了远多于平均值的分片。

第二步是看集群状态的压力:

GET _nodes/stats/indices/segments,indices/fielddata,jvm?pretty GET _cluster/stats?pretty

重点看主节点的堆使用和 GC 频率、各节点的段数量、字段数据占用。段数量异常高通常意味着分片太多或者合并跟不上。

第三步是算账。把索引数乘上每索引分片数,除以数据节点数,除以堆总容量(GB),得到每 GB 堆承载的分片数。超过 20 就要警惕,超过 40 基本可以确认问题就在这里。

5.2 分片过少的补救:split 和 reindex 的选择顺序

分片过少的表现和过多相反:单个分片体积膨胀到上百 GB、写入集中在少数分片上导致 CPU 不均、查询并行度不够、段合并耗时越来越长。这时候第一反应是加节点,但如果分片数不变,加节点并不能提升这个索引的写入并行度,因为一个分片只能落在一个节点上。

补救的顺序我建议这样:如果这个索引当初创建时设过 routing 分片数并且还有余量,优先用_split,速度快、风险低。操作前先把索引设为只读并等待所有分片健康:

PUT /orders_v1/_settings { "settings": { "index.blocks.write": true } } POST /orders_v1/_split/orders_v2 { "settings": { "index.number_of_shards": 8, "index.number_of_replicas": 1 } }

如果 routing 分片数不够或者映射也要一起改,那就走 reindex,配合别名切换。切换顺序是:建新索引并写数据校验、跑 reindex、比对文档数、把别名从旧索引原子切到新索引、观察一段时间后再删旧索引。千万不要先删旧索引再建新的,那是最常见的自找麻烦。

5.3 索引模板里最容易埋的两个默认值

第一个坑是模板里写死分片数。很多团队在建模板时按当时的业务量定了 5 分片,之后所有按天滚动的索引都继承这个值,结果每天的索引只有 2GB 数据,也照样占 5 个分片,一年下来几千个几乎空的分片。正确做法是把模板里的分片数设成 1,让滚动策略根据单分片大小来触发,而不是根据时间。

第二个坑是滚动条件写的是时间而不是大小。按天滚动看起来很整齐,但业务量波动大的时候,某天数据暴涨就会产生一个巨大的分片,某天数据很少就产生一堆碎片。用单分片大小或者单索引大小作为滚动条件,分片大小才是可控的。

PUT _ilm/policy/orders_policy { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_primary_shard_size": "40gb", "max_age": "7d" } } }, "delete": { "min_age": "30d", "actions": { "delete": {} } } } } }

max_primary_shard_size这个条件比按天数滚动稳得多,分片大小一旦到了阈值就滚,不受业务波动影响。同时保留一个max_age作为兜底,防止低流量时期索引无限期不滚动。

6. 我自己的几组配置习惯和每周要看的几个数

说几个我在实际项目里形成的固定做法。建索引之前一定先算一遍分片数,哪怕是最简单的临时索引,也会先问一句“这个索引一个月后大概多大”,因为事后补救的成本远高于事前多想两分钟。默认值我一般按单分片 30GB 作为起点,日志类放宽到 40GB,搜索类收紧到 15GB 左右,这个起点在绝大多数中小规模项目上都够用。

副本数我默认给 1,除非节点数少于 3。节点数只有两个的时候配 1 副本意义不大,因为一个节点故障,剩下那个节点要同时扛主副两份数据,服务能力断崖式下降,还不如把资源用在扩容节点上。读多写少的搜索服务可以给到 2,但要盯着写入延迟的 P99,一旦发现尾部延迟变大,先怀疑副本同步而不是索引设计。

每周的巡检我固定看四个数:集群总分片数除以数据节点数、每 GB 堆承载的分片数、单分片体积的最大值和最小值、集群状态的更新耗时。前两个判断容量是否健康,第三个判断分片设计有没有失衡,第四个判断主节点是不是快要扛不住了。这四个数里任何一个明显跑偏,都比等到报警再去查要省事得多。

还有一个容易被忽略的细节:调整副本数是个在线操作,改完会立刻触发分片重新分配和大量数据拷贝,直接影响磁盘 IO 和网络。这种操作不要放在业务高峰做,也不要一次给几百个索引同时改,分批来,改完等集群恢复绿色再做下一批。我自己就吃过一次亏,为了赶一个“降本”的节点,一次性把几百个索引的副本从 2 降到 1,结果集群花了几个小时才稳定下来,期间查询延迟翻了三倍,比省下的那点磁盘钱贵多了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进