☰
Elasticsearch分片数量与副本数设置:上限、代价与容量规划
2026/11/2 2:41:05 网站建设 项目流程

1. 埋点前的准备:把“分片”和“副本”这两个词拆开看

在一个集群上折腾分片数栽过跟头的人,通常都有一个共同点:一开始压根没算过账。我印象最深的一次是接手一个索引,200 多个主分片、副本设了 2,看着挺热闹,结果集群健康度长期飘黄,一堆分片在队列里排队分配,查询延迟从 80ms 涨到 1.2s。后来把主分片压到 32、副本砍到 1,延迟直接掉回百毫秒以内。所以 Elasticsearch 的分片(Shards)数量上限和副本数量这两个问题,不是背几个数字就能应付过去的,得先把它们各自管什么、代价在哪里搞清楚。

这篇内容想解决的就是这件事:当你拿到一个索引,该怎么判断主分片该开几个、副本该设几份、什么时候会撞到集群的分片上限、撞上了又该怎么拆。读完你应该能自己算出一个靠谱的分片方案,而不是抄别人的“默认 5 分片 1 副本”。

1.1 一个索引被切成几块,为什么非切不可

Elasticsearch 里的索引并不是一整块连续存放的数据,它在创建的那一刻就被定死了要切成多少个主分片(primary shard)。每个主分片本质上是一个独立的 Lucene 索引实例,拥有自己的段文件、自己的倒排表、自己的写入缓冲。之所以要切,核心原因是单机容量和单机并行度的天花板:一台机器的磁盘装不下几百 GB 甚至几 TB 的数据,一个 Lucene 实例也扛不住几十万 QPS 的并发写入。

切分片带来三个直接好处。第一,数据能横向摊到多台机器上,单节点只负责自己那几个分片,磁盘不会爆。第二,写入和查询都能并行,一个查询会同时打到索引的所有分片(每个分片算一部分结果,再在协调节点归并),分片越多单次查询的理论并行度越高。第三,扩容时可以把分片迁移到新节点,负载是颗粒度更细的搬运。

代价同样明显。每个分片都是一个 Lucene 实例,都要占文件句柄、内存里的 segment 元数据、线程池队列里的任务槽位。分片过多时,集群花在“维护分片”上的开销会盖过真正干活的开销,这就是大家常说的“分片开销”。所以分片数量的本质是一场取舍:用更多的分片换并行度,但要为每个分片付出固定的资源税。

1.2 副本不是备份,它是另一份能干活的数据

很多人把副本(replica)当成数据库的主从备份,这个理解只对了一半。副本确实提供冗余,主分片所在的节点挂了,副本可以顶上来,但这只是它的一半职责。另一半职责是分担读请求:一个索引有 1 份副本时,这个索引的总分片数是主分片数的两倍,读请求可以落在主分片或副本分片上,读吞吐理论上翻倍。

写入就不一样了。每一条文档写入都要在主分片上完成后再同步到所有副本分片,副本数越多,写放大的倍数越高。副本数为 1 时,一份数据实际写两遍;副本数为 2 时写三遍。这也是为什么日志类索引通常只给 1 份副本,而对读要求高的业务索引才会考虑 2 份副本。

还有一个容易忽略的点:副本分片和主分片不会被分配到同一个节点上。这条规则是硬约束,它直接决定了副本数的上限——副本数最多只能设到“数据节点数减一”,多出来的副本会一直处于未分配状态,白白占着分片配额,却带不来任何好处。

1.3 三个数量概念,混在一起就容易算错

实际工作中出问题,多半是这三个数量没分清:

概念含义能否事后修改典型取值
主分片数number_of_shards索引被切成几块,数据真正分布的单位不能直接改,只能重建、shrink 或 split1 到几十
副本数number_of_replicas每个主分片复制几份随时可改,动态生效0 到 2
索引总分片数主分片数 ×(1 + 副本数)随上面两个变化与节点数匹配

举个具体例子,一个索引 8 个主分片、1 份副本,它的总分片数是 16。这 16 个分片会计入集群的总分片配额,也会消耗 16 份内存和文件句柄资源。你在看监控的时候如果只盯着主分片数,很容易低估真实开销——集群里那个 5000 分片的数字,通常是主分片 2000 加副本 3000 凑出来的。

2. 分片数量到底有没有上限?三层限制要分开看

“分片数有没有上限”这个问题,答案分三层:单索引层有一个明确的天花板,集群层有一个可配置的配额,内存层有一条不成文的红线。三层里任何一层撞上,表现出来都是同一句报错,但解法完全不同。

2.1 单索引主分片数的天花板:1024

从 7.x 版本开始,一个索引的主分片数上限被明确限制在 1024。这个数字不是随便定的,它跟分片路由的计算方式有关——文档路由到分片靠的是 hash 取模,分片数太大时,路由表本身的开销和再平衡的颗粒度都会变得不合理。实际上你几乎不该接近这个值,正常情况下超过 100 就该重新审视设计。

需要强调的是,这个 1024 指的是主分片数,不是总分片数。理论上一个索引可以设 1024 个主分片、10 份副本,总分片数上万,但那样做除了把集群拖垮没有任何意义。

还有一个容易被忽略的参数index.number_of_routing_shards,它决定了这个索引未来能被 split 成多少份。默认值等于主分片数,也就是说默认情况下一个索引只能 shrink 不能 split。如果你的业务预期数据会大幅增长,创建索引时把这个值设成主分片数的整数倍(比如主分片 4、routing shards 设 16),后续才有 split 的空间。

2.2 单节点分片配额:cluster.max_shards_per_node

这是最常遇到的那道墙。7.x 及以后版本引入了一个动态参数cluster.max_shards_per_node,默认值是 1000。集群的总分片上限等于这个值乘以非冻结数据节点数。比如 5 个数据节点的集群,默认最多容纳 5000 个分片(主分片加副本全算)。

超过配额时的报错长这样:

validation_exception: Validation Failed: 1: this action would add [20] total shards, but this cluster currently has [4990]/[5000] maximum shards open

看到这个报错,第一反应不该是把这个参数调大。调大只是把问题往后推,集群的真实承载能力并没有变。正确的动作是先盘点:这 4990 个分片里,有多少是历史索引留下的、有多少副本是可以砍的、有多少是每天一个索引但实际没数据量的空壳。

从 8.x 开始还多了冻结节点的独立配额cluster.max_shards_per_node.frozen,默认 3000,专门给可搜索快照那类冷数据用。这个设计思路很明确:热数据的分片要精打细算,冷数据的分片可以放宽,但两者不能互相挤占配额。

查看当前用量的方式很直接:

GET _cluster/health?pretty GET _cat/allocation?v

_cat/allocation输出的shards列就是每个节点当前承载的分片数,加总起来就是集群已用配额。想临时放宽(不推荐长期这么做):

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

改完之后记得把这笔账记下来,因为配额放宽了,下一个撞墙的可能就是内存。

2.3 内存这条隐形红线:每 GB 堆内存 20 个分片

官方给出的经验值是:每个数据节点的分片数控制在“每 1GB 堆内存对应 20 个分片”以内。这个数字来自长期的实践统计,不是理论推导。一个 16GB 堆的节点,建议承载不超过 320 个分片;8GB 堆的节点,建议不超过 160 个。

为什么是 20 这个量级?因为每个分片在内存里都有一份常驻开销:段元数据、分片级缓存、集群状态里的一条路由记录。分片数上去以后,集群状态(cluster state)本身会膨胀,每次集群状态变更都要广播到所有节点并重新计算路由,这个过程会消耗 CPU 和网络。分片数几千的集群,光是集群状态同步就可能占掉可观的一截 CPU。

这条红线比cluster.max_shards_per_node更值得关注,因为它是软性的、不会报错,只会让你的集群慢慢变慢。你在监控里看到的症状通常是:写入吞吐莫名下降、查询 P99 抖动、节点 CPU 不高但 GC 频繁、_cluster/health里number_of_pending_tasks长期不为 0。

2.4 真正卡住你的其实是查询延迟

前面三层都是“能不能建”的问题,还有一层是“建了跑不跑得动”。一个查询默认会打到索引的每一个分片,每个分片独立执行、返回自己的 Top N,最后在协调节点归并。分片数越多,这个“分片级固定开销”被放大的倍数越高。

实测下来,单个分片的查询固定开销在几十微秒到几百微秒之间,看着很小,但乘以分片数就不一样了。50 个分片时固定开销可能只占几毫秒,500 个分片时就可能吃掉几十毫秒甚至更多。对于延迟敏感的业务,这笔开销是实打实的。

另外分片多会让from + size深分页的成本急剧上升,因为每个分片都要取出from + size条再归并。分页场景下控制分片数,比调大index.max_result_window有用得多。如果查询能走 routing,也就是明确知道文档落在哪个分片上,可以只打一个分片,这时候分片数的影响就小很多;但绝大多数业务查询做不到这一点。

3. 副本数量能设多大?先把代价算清楚再动手

副本数的官方上限是不存在的——index.number_of_replicas你写 100 也能创建成功。但能不能分配出去、分配出去以后集群扛不扛得住,是另外两个问题。实际有效的上限由节点数决定,实际合理的取值由读写比例和成本决定。

3.1 硬约束:副本数不会超过“节点数减一”

同一分片的主副本不能共存于同一节点,这条规则决定了:3 个数据节点的集群,副本数最大设 2;5 个节点的集群最大设 4。超过这个值的副本会一直处于unassigned状态。

这里有个坑要提醒。设了 3 份副本但只有 3 个节点时,第三个副本永远分配不出去,集群健康度会一直显示黄色。很多人会去翻日志、查分片分配,最后发现只是副本数设多了。查询GET _cat/shards/index_name?v看UNASSIGNED那一列对应的分片号,再用GET _cluster/allocation/explain让它自己解释原因,通常会直接告诉你“no node available to allocate the replica”。

如果你是跨机房或跨可用区部署,情况会再复杂一层。分片分配感知(shard allocation awareness)会让主副本尽量分散到不同区域,此时副本数的有效上限可能比节点数减一更小。这个特性有它的用途,但要意识到它会让“副本数 = 节点数 - 1”这个简单公式失效。

3.2 写放大与磁盘成本,是副本最实在的代价

副本对写入的放大是线性叠加的。一条文档写入主分片后,要同步到所有副本分片,每个副本都要走一遍完整的 index 流程:写 translog、进内存缓冲、生成 segment、刷盘。副本数为 1 时单条写入的工作量翻倍,副本数为 2 时翻三倍。

磁盘成本也是线性的。原始数据 100GB,1 份副本实际占 200GB,2 份副本占 300GB。这里的 200GB 和 300GB 不是简单相乘的结果,因为主副本在同一时间点的段结构可能有细微差异,但量级上就是这么算的。规划磁盘时按“原始数据 ×(1 + 副本数)× 1.1~1.3”来估,那个额外系数留给段合并期间的临时空间和 translog。

还有一个隐性成本是恢复时间。节点重启后,它上面的分片要从其他节点复制回来。副本数越多,需要恢复的总数据量越大,恢复期也越长。一个 500GB 的索引,1 份副本时节点故障要恢复 500GB,2 份副本时理论上不增加单次恢复量,但整体的网络和磁盘压力会更大。恢复速度受indices.recovery.max_bytes_per_sec控制,默认是 40MB/s,大规模恢复前评估一下这个值是否需要调整。

3.3 副本数怎么定:按场景给几档参考

场景推荐副本数理由
单节点开发/测试0没有第二个节点可放副本,设了也是黄色
日志、指标类(写多读少)1保证高可用即可,写放大控制到最小
常规业务检索1性价比最高,读吞吐已有翻倍效果
读密集型、缓存前置2读吞吐再提升,前提是节点数至少 3
冷数据、归档索引0 或 1冷数据读少,0 副本可省一半空间,配合快照兜底

有个细节值得说:副本数是可以动态调整的,改完立刻生效,不需要重建索引。所以不需要一次定死,大促前临时把核心索引副本从 1 调到 2,活动结束再调回来,是完全可行的操作。调整命令就一行:

PUT /orders_2024/_settings { "index": { "number_of_replicas": 2 } }

注意:调大副本后集群需要时间做分片复制,这段时间里磁盘和网络会有明显抬升,别在流量高峰的瞬间做这个操作。调小副本会触发分片删除,数据本身还在主分片上,但如果恰好此时有节点故障,可用的冗余就少了,尽量挑低峰期。

4. 分片规划实操:从数据量倒推数量

前面讲了限制和代价,这一节讲怎么把数字算出来。我给一套自己常用的五步法,不保证最优,但能覆盖绝大多数场景,至少不会算出离谱的结果。

4.1 容量估算五步法

第一步,估总量。取“日均新增原始数据量 × 保留天数”,得到原始总量。如果有明确的采样数据,用实际值比拍脑袋可靠得多。

第二步,估存储。ES 里的实际占用不等于原始数据量。原始文本进 ES 后会建倒排索引、存 doc_values、存_source,通常膨胀 1.1 到 1.5 倍;如果字段映射设计得差,比如该用keyword的用了text、该关的_source没关,膨胀 2 倍以上也正常。稳妥起见按 1.3 倍估。

第三步,乘副本系数。存储总量 = 原始总量 × 膨胀系数 ×(1 + 副本数)。

第四步,定单分片大小。官方建议单个分片落在 10GB 到 50GB 之间。小于 10GB 说明分片太碎,分片开销占比过高;大于 50GB 时单个分片的段合并和恢复会变慢。我自己的偏好是 20GB 到 30GB,兼顾了后续增长的空间。

第五步,算分片数。主分片数 = 存储总量 ÷ 单分片目标大小,然后向上取整到方便的数(比如 2 的倍数或整数十)。最后校验一遍:总主分片数 ÷ 数据节点数,得到每个节点承载的分片数,对照“每 GB 堆 20 分片”的红线检查是否超标。

4.2 一个完整的算例

假设一个日志场景:每天新增 150GB 原始日志,保留 30 天,1 份副本,节点是 3 台、每台 16GB 堆。

存储总量 = 150 × 30 × 1.3 × 2 = 11700GB,约 11.4TB。

按单分片 30GB 算:11700 ÷ 30 = 390 个分片。这个数字意味着每个节点承载 130 个分片,3 台节点共 390 个分片,对照 16GB 堆 × 20 = 320 的上限,390 略超。

两个调整方向:一是把单分片目标提到 40GB,得到 292 个分片,落进安全区;二是压缩保留天数或降低副本。这里选前者,单分片 40GB 虽然偏大,但对日志这种写多读少、查询以时间范围过滤为主的场景是可以接受的。

再看这个集群为什么会撞 1000 的上限。如果按天创建索引,每天一个索引,那每个索引的主分片数应该是 390 ÷ 30 ≈ 13,取整到 12 或者 16。很多人是按“一天 150GB,单分片 30GB,所以每天 5 个主分片,1 副本 = 10 个分片”这样算的,30 天累计 300 个分片,看似没问题。但如果你把number_of_shards设成 12、副本 2,30 天就是 30 × 12 × 3 = 1080 个分片,直接越过 1000 的红线,这就是典型的“三个数字没分清”。

4.3 创建索引的推荐配置模板

基于上面的算例,一个日志索引的创建语句大概长这样:

PUT /logs-2024.06.01 { "settings": { "number_of_shards": 12, "number_of_replicas": 1, "refresh_interval": "30s", "index.number_of_routing_shards": 24, "index.translog.durability": "async", "index.translog.sync_interval": "30s", "index.codec": "best_compression" }, "mappings": { "dynamic": "strict", "properties": { "timestamp": { "type": "date" }, "level": { "type": "keyword" }, "service": { "type": "keyword" }, "message": { "type": "text" } } } }

几个参数的选择理由值得说一下。refresh_interval从默认的 1s 调到 30s,是为了减少段生成频率,日志场景对实时性要求没那么高,这个调整能明显降低写入压力。number_of_routing_shards设成主分片数的 2 倍,给未来 split 留了口子。translog改成异步,牺牲极小的可靠性换取写入吞吐,日志场景通常可以接受。codec用best_compression,日志文本压缩率提升明显,代价是查询时解压开销略高。

对业务检索类索引,配置思路反过来:refresh_interval保持 1s 甚至更短,translog保持默认的request保证不丢数据,分片数宁可少一些,number_of_replicas根据读压力决定。

4.4 主分片数改不了,但有三条路可以补救

索引创建后number_of_shards就锁死了,这是 ES 的硬规则。真算错了,有三条补救路径:

重建索引。用_reindex把数据搬到新索引,最稳妥但最慢。适合数据量不大、或者本来就在做结构升级的场景。写操作要在应用层双写或停写,切换时机得挑好。

shrink。把小索引合并成大分片。要求源索引只读(index.blocks.write设为true),且所有分片必须在同一节点上,目标分片数必须是源分片数的因数。比如 12 个主分片缩到 6 个或 4 个或 3 个,但不能缩到 5 个。很小的历史索引适合这条路。

split。反过来,把分片拆细。目标分片数必须是number_of_routing_shards的因数。适合数据量比预期增长快、单分片过大的索引。split 的代价是磁盘临时占用翻倍,source 索引用只读模式挂在那里,等 split 完成后再切换。

shrink 的关键命令长这样:

PUT /logs-2024.06.01/_settings { "settings": { "index.blocks.write": true } } POST /logs-2024.06.01/_shrink/logs-2024.06.01-shrunk { "settings": { "index.number_of_replicas": 1, "index.codec": "best_compression" } }

注意:shrink 前必须确保源索引的所有分片都落在同一个节点上,否则会报 “all shards must be allocated on the same node”。做法是先通过分片分配过滤把索引“赶”到一个节点,或者直接对一个新建的、还没扩散的索引做 shrink。这一步在文档里写得很轻描淡写,实际第一次操作基本都会卡在这里。

5. 排查实录:分片分配不上、集群变黄变红怎么办

分片配额、副本数、节点数这三件事一旦对不上,集群健康度就会掉下来。这一节整理几个我实际遇到过的问题和排查顺序。

5.1 unassigned 分片的排查顺序

看到unassigned_shards大于 0,先别急着重启。按下面这个顺序查,基本能在三五分钟内定位:

第一步,GET _cluster/health?pretty,看是 yellow 还是 red。yellow 说明主分片都在、副本缺;red 说明有主分片没分配,数据可能不可用。这个区分很重要,处理优先级完全不同。

第二步,GET _cat/shards?v&s=state,找出UNASSIGNED的那些分片,注意看它们是p(主)还是r(副本)。

第三步,挑一个具体的 unassigned 分片,让集群自己解释:

GET _cluster/allocation/explain { "index": "logs-2024.06.01", "shard": 3, "primary": false }

输出里的allocate_explanation会直接告诉你原因,常见的有这几种:

报错原因含义处理方式
no node available to allocate the replica副本数超过节点数减一调小副本数或加节点
the node is above the high watermark磁盘使用率超过 85% 水位清数据、扩容或调整水位
shard allocation is disabled分配开关被关掉检查cluster.routing.allocation.enable
same shard exists on the same node主副本被规则约束到同一节点检查分配过滤和感知配置
waiting for node left delayed timeout节点刚离开,等待期未满等待默认 1 分钟,或调小index.unassigned.node_left.delayed_timeout

high watermark这一条最容易被忽视。磁盘到 85% 触发low watermark(默认值在 7.x 是 85%,早期版本是 85%、90%、95% 三档阈值),ES 会停止往该节点分配新分片;到 90% 会开始主动迁移分片到其他节点;到 95% 会直接对索引设为只读。这三个阈值我都见过设错的情况,有的集群磁盘只剩 5% 空间还在写,最后索引被锁成只读,只能手动作业解锁。

5.2 分片太多导致的典型症状

分片过多不会直接报错,它表现为一串“慢性病”。如果你在集群上看到下面这些现象同时出现两三个,就该怀疑分片数失控了:

  • 集群状态同步频繁,number_of_pending_tasks长期不为 0
  • 节点 CPU 不高,但 GC 次数多、堆使用率高,尤其老年代增长快
  • 写入吞吐随索引数量增加而线性下降,加机器也没明显改善
  • 查询 P99 抖动大,慢日志里大量请求耗时集中在“分片级”阶段
  • 监控上看每个节点的分片数都在几百以上,且总量接近max_shards_per_node × 节点数

处理思路是按索引盘一遍:把长期没有写入的历史索引做 shrink 或者直接关闭(closed index 不占分片配额,但查询要走 reopen);把可搜索快照类的冷数据挪到冻结层;把按天创建的索引改成 rollover 配合 ILM,控制活跃索引数量。

_cat/indices?v&s=pri.store.size:desc是很好用的入口,先按存储大小排序,看头部几个大索引的分片数是否合理;再看尾部一堆小索引,那些几百 MB 却开了 5 主分片的索引就是典型的浪费。

5.3 恢复慢与 force merge 的时机

节点故障恢复时,如果发现分片恢复速度慢得离谱,先看两个地方。一个是indices.recovery.max_bytes_per_sec,默认 40MB/s,在万兆网络和 SSD 环境下这个值明显偏保守,可以按网络带宽的 60% 左右往上调。另一个是并发度cluster.routing.allocation.node_concurrent_recoveries,默认 2,适度提高到 4 到 6 能加速恢复,但别一次拉太高,否则会把磁盘 IO 打满,影响在线查询。

force merge 是另一个常见误区。它对只读的历史索引有用,能把多个段合并成少量大段,减少段元数据开销、提升查询效率。但如果在还在写入的索引上执行_forcemerge,代价极高:大量磁盘 IO、CPU 占用,而且合并完新写入又会生成新段,纯粹白干。

我的做法是只对已经 rollover 并把index.blocks.write设为true的历史索引做 force merge,且限定max_num_segments=1,在业务低峰期跑。一个 30GB 的分片做一次全合并,在普通 SSD 上大概十几分钟,这个时间窗口要提前留出来。

6. 我在这件事上踩出来的几条经验

分片数这件事,我现在的习惯是:任何新索引上线前,先在纸上算一遍存储总量和分片数,算完对着“单分片 20 到 30GB、每 GB 堆 20 分片、副本不超过节点数减一”这三条过一遍,三条都过才动手建。这个流程花不了五分钟,但能省掉后面好几天的重建和迁移。

副本数我现在基本不做静态判断,而是先给 1,上线后看实际读压力再决定要不要加到 2。绝大部分场景 1 份副本就够,多出来的那份副本带来的读吞吐提升,往往不如把这台机器资源用在别处。真正需要 2 份副本的是读 QPS 高、且查询无法被缓存或降级的场景,这种场景不多。

还有一个我反复提醒团队的点:不要靠调大cluster.max_shards_per_node来“解决”分片超限。这个参数是配额检查用的,调大只是让检查通过,底层节点该扛不住还是扛不住。真正要做的永远是减少分片数,或者增加节点数——前者靠重建、shrink、rollover、关闭历史索引,后者才是把配额和内存红线一起抬高的唯一办法。

最后分享一个小技巧:给自己的集群建一个定时任务,每周拉一次_cat/allocation?v和_cat/indices?v&s=pri.store.size:desc存下来,看趋势而不是看单点。分片数从 800 涨到 950 这件事,只有在趋势图里才刺眼,单个时间点看永远觉得“还早”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询