上个月有个朋友半夜找我,说他们集群的_cluster/health一直在 yellow 和 red 之间反复横跳,日志里全是分片分配失败的告警。远程连上去一看,一台 16G 堆内存的节点上挂了 4200 多个分片,索引数量不到 300 个,但每个索引都是 12 主 2 副本起步——建索引的人当时想着“分片多一点查询快”,结果把一个好好的集群塞成了筛子。这类事我见得太多:Elasticsearch 的分片(Shards)数量到底有没有上限、副本(Replicas)到底该开几个,这两个数字看起来是建索引时随手填的两个参数,实际上是整个集群能不能稳住的地基,而且主分片数一旦定死,后面想改就得动数据搬迁。
这篇就把这两件事掰开讲透:分片的软上限和硬闸门分别是什么、怎么按数据量和堆内存倒推主分片数、副本数受哪些约束、超过或不足会有什么症状、以及分片定错了之后有哪些补救手段。适合已经能跑起集群、但还在“拍脑袋填参数”阶段的同学,也适合负责容量规划、需要给团队定标准的人。我尽量把每一步的换算过程写出来,你可以直接拿自己的数据套。
1. 先把概念拧清楚:分片和副本到底各管什么
很多人把分片和副本当成同一类“性能参数”,实际上它们在集群里承担的角色完全不同,一个是切分数据的单位,一个是复制数据的冗余。搞清楚这一点,后面所有的取舍才有依据。
1.1 主分片是水平切分的单位,不是性能开关
一个索引在物理上是若干个 Lucene 索引(segment 集合)拼起来的,主分片就是这个“拼”的最小单位。文档写进来时,ES 会对文档 ID 做一次哈希,落到hash(id) % 主分片数对应的那个分片上,也就是说一条文档只会存在于一个主分片里。这个机制决定了两件事:第一,主分片数在索引创建那一刻就固定了,因为它直接参与路由计算,改了路由就全乱了;第二,主分片是并行度的上限,一个查询打到索引上,协调节点会把请求广播给所有分片,再汇总结果,分片越多,理论上能并行的线程越多,但汇总开销也越大。
这就解释了为什么“分片多了查询就快”是个半对半错的直觉。在单节点、小数据量的场景里,把分片从 1 改成 5,你大概率感觉不到变快,甚至因为多线程上下文切换和结果归并而变慢。真正的收益出现在数据量大到单个分片处理不过来的时候——比如单个分片 100GB,一次聚合要扫几十亿条 doc values,这时候切成 5 个 20GB 的分片,让 5 个线程分摊,提速是实打实的。
所以判断标准不是“分片越多越好”,而是“单个分片的数据量是否落在合理区间”。经验值是这样的:搜索类业务,单分片控制在 10GB 到 30GB 之间比较舒服;日志、时序类写入为主的场景,单分片 30GB 到 50GB 也常见。低于 10GB 的分片基本属于浪费,因为每个分片本身有固定的内存开销(segment 元数据、fielddata、过滤缓存、分片级队列),空转成本不为零。
1.2 副本是容灾与读吞吐的双重角色
副本分片是主分片的完整拷贝,由 ES 自动分配,永远不会和它的主分片落在同一个节点上(这是硬规则,除非你手动改分配策略)。它带来两个收益:一是节点挂了,副本能被提升为新的主分片,数据不丢,服务不断;二是读请求可以在主分片和副本之间轮询分摊,读吞吐理论上能随副本数线性增长。
但副本不是免费的。写入一条文档,主分片处理完之后必须同步写到所有副本分片,等副本确认了才返回给客户端(可以调wait_for_active_shards,但默认行为就是等全部 in-sync 副本)。也就是说副本数从 1 调到 2,写入链路从 2 次落盘变成 3 次落盘,写入吞吐大约掉三分之一,写延迟也会上去。很多团队在压测时发现“加了副本反而写得更慢”,就是这个原因,不是集群出问题了,是物理规律。
另外副本占用存储是实打实的。1 主 1 副本意味着磁盘占用翻倍,2 副本是三倍。做容量规划的时候,别只算原始数据量,要乘以(1 + 副本数),再留 20% 到 30% 的余量给段合并、translog 和临时空间。我见过按原始数据量买盘的,上线三天磁盘水位就告急了。
1.3 分片数、副本数、节点数三者的三角关系
这三个数不是独立的。集群里所有索引的分片总数 = Σ(每个索引的主分片数 × (1 + 副本数)),这个总数要分摊到各个节点上,每个节点能承载的分片数又受堆内存限制。同时,副本能不能分配出去还受节点数限制:3 个节点的集群,某个索引设 2 个副本,一共 3 份数据正好一人一份,能分配;但如果集群只有 2 个节点,设 2 副本就一定有一个副本处于 unassigned 状态,集群状态永远是 yellow。
这里有个容易忽略的细节:节点数不是按物理机算,是按“能承载分片的节点”算,包括数据节点和带 data 角色的节点。专用的协调节点、master 节点不存数据,不参与这个计算。做规划时先用GET _cat/nodeattrs?v和GET _cat/nodes?v&h=name,node.role,heap.percent确认有多少个真正的数据节点。
2. 分片数量到底有没有上限:从几道硬闸门说起
严格说,ES 没有给一个“总共最多多少个分片”的绝对数字,但实际使用中会撞上三到四道闸门,任何一道撞上,集群就开始不听话了。理解这些闸门的位置,比记住某个数字重要得多。
2.1 集群级软上限:每 GB 堆内存 20 个分片的由来
这是最常被引用的一条经验法则:每 1GB 堆内存大约能承载 20 个分片。它不是硬编码的限制,而是一条容量参考线,背后的逻辑是每个分片都要常驻一部分内存——分片级的 search 线程队列、query cache、request cache、segment 的元数据(FST 索引、doc values 的内存映射结构)等等。
换算方式很直接:集群软上限 ≈ 数据节点总堆内存(GB) × 20。举个例子,3 台数据节点,每台 31GB 堆(这是官方建议的堆上限附近,再大压缩指针失效、GC 停顿变长),那么软上限就是 3 × 31 × 20 = 1860 个分片。注意这个数是“分片总数”,包含主分片和副本分片,不是主分片数。
为什么定在 20 而不是 40?因为这条线是“能扛住但不舒服”的位置,实际生产里我更建议按 10 到 15 来规划,留出余量给突发索引和滚动重建。压到 20 的集群,一旦开始做 reindex 或者大批量索引滚动,分片数翻倍,立刻越线,然后你就看到集群状态更新越来越慢、master 的 pending tasks 堆积。
还有一个隐藏成本:集群状态(cluster state)里记录了每个分片的分配信息。分片数上万之后,每次集群状态变更都要广播给所有节点,这个报文会变得很大,集群状态收敛变慢,主节点压力明显上升。我在一个 8000 分片的集群上观察到,一次简单的索引创建操作,集群状态推送能卡 3 到 5 秒。
2.2 索引级硬闸门:max_shards_per_node 与相关参数
从 7.x 开始,ES 引入了一个更硬的保护机制:cluster.max_shards_per_node,默认值 1000。它的作用是限制每个节点上能承载的分片总数(同样是主 + 副本),一旦某个操作会导致越线,创建索引或者分片分配会直接被拒绝,报错信息里会明确提示超过了 per-node 的分片上限。
这个设计的初衷是防止“一个误操作把集群打挂”——比如有人写脚本批量建索引,每个索引设 20 个分片,一个 for 循环下去几千个分片就出来了,没有这道闸门的话,集群会在几分钟内进入不可用状态。
调整它需要谨慎:
PUT _cluster/settings { "persistent": { "cluster.max_shards_per_node": 2000 } }改之前先问自己:为什么现有额度不够?是分片规划本身有问题(单分片太小),还是业务真的需要这么多索引?我见过最典型的情况是“每天建一个索引,每个索引 5 主 1 副本,保留一年”,算下来 365 × 10 = 3650 个分片,这还没算副本的扩容。这种场景正确的做法不是提高上限,而是缩短保留周期、加大单分片容量、或者用 ILM 把老数据降到更少的分片上(shrink)。
除了这个参数,还有几个相关配置值得知道:
| 参数 | 默认值 | 作用 | 什么时候要动 |
|---|---|---|---|
cluster.max_shards_per_node | 1000 | 单节点分片总数上限 | 确实需要更多索引时 |
index.routing.allocation.total_shards_per_node | 未设置 | 单个索引在单节点上的分片数上限 | 防止单个大索引把节点占满 |
cluster.routing.allocation.node_concurrent_recoveries | 2 | 单节点并行恢复的分片数 | 恢复慢时谨慎调大 |
index.number_of_shards | 1(7.x 起) | 主分片数 | 建索引时定,之后不可改 |
2.3 算一遍:从数据量倒推主分片数
光讲原则没用,走一遍完整的换算。假设一个订单查询类索引,预估一年数据量 600GB,留 25% 余量也就是按 750GB 规划,单分片目标 30GB,那么主分片数 = 750 / 30 = 25,向上取整到方便的数字,24 或 30 都可以。副本设 1,总存储 = 750 × 2 = 1.5TB,分摊到 3 个节点,每节点 500GB,磁盘单节点配 1TB 就够,留了一倍空间给合并和突发。
再看分片总数:这个索引 30 主 × 2 = 60 个分片。如果集群里有 20 个这样的索引,就是 1200 个分片。3 节点、每节点 31GB 堆,软上限 1860,1200 在 65% 左右,属于健康区间。但如果这个数是 40 个索引,就是 2400 个分片,越线了,必须调整。
如果是日志类场景,逻辑要换一下。日志通常是按天或按小时滚动建索引,这时候“总数据量除以单分片大小”这个算法要改成“单日数据量除以单分片大小”。日增 200GB、单分片 40GB,那么每日索引 5 个主分片,1 副本就是 10 个分片;保留 30 天就是 300 个分片,完全可控。但如果日增 2TB,每天 50 个主分片、100 个分片,30 天就是 3000 个分片,这时候要么缩短保留期,要么强制每天合并成更少的分片,要么上更粗的时间粒度(按周滚动)。
提示:算分片数的时候一定要算上副本。很多人规划时只盯着
number_of_shards,忘了实际占用是它的 (1 + 副本数) 倍,这是最常见的容量误判来源。
3. 副本数量的约束条件与实际取值判断
副本数比主分片数灵活得多,随时可以通过 API 改,改完 ES 会自动做分片搬迁。但灵活不等于随便填,它受三方面约束,每一条都是实打实的成本。
3.1 三重约束:节点数、故障域、写入放大
第一重是节点数。副本分片不能和主分片同节点,所以一个索引的副本数上限是“数据节点数 - 1”。2 个数据节点最多设 1 副本,设 2 就必然有一个 unassigned。判断当前配置是否合理,跑一句GET _cat/shards?v&s=index看有没有UNASSIGNED状态的副本就知道了。
第二重是故障域。如果你的 3 个数据节点跑在同一台物理机上(容器化部署时很常见),那设 2 副本毫无意义,机器一挂全没了。反过来说,如果节点分布在不同的机架或者不同的可用区,1 副本就足以扛住单点故障带来的可用性问题,2 副本则是为了扛住“同时挂两个”这种更极端的场景——但代价是存储和写入都增加 50%。
第三重是写入放大。前面提过,副本数 +1 意味着写入链路多一次同步落盘。对于写多读少的场景(日志、埋点、监控指标),我通常建议副本设 1 就够了,甚至在某些可以容忍短时数据丢失的分析类索引上设 0,把写入吞吐榨出来。对于读多写少的场景(商品搜索、内容检索),副本可以设 2,让读请求分摊到更多分片上。
3.2 从 1 副本改成 2 副本,成本到底是多少
改副本数是一条命令的事:
PUT /order_search/_settings { "index": { "number_of_replicas": 2 } }但背后的动作是:ES 需要为每个主分片新建一个副本,从主分片复制全量数据过去,这个过程叫分片恢复(recovery)。成本包括——网络传输全量数据(压缩后)、目标节点磁盘写入、以及恢复期间主分片的额外读压力。一个 500GB 的索引,从 1 副本加到 2 副本,大约要再复制 500GB 数据,在千兆网络下乐观估计也要小半天,期间集群的 IO 和 CPU 都有明显波动。
更要命的是恢复并发。默认cluster.routing.allocation.node_concurrent_recoveries是 2,也就是每个节点同时只恢复 2 个分片,这样能保护集群不被恢复流量打垮,但代价是恢复时间长。如果你在一个生产高峰做这个操作,业务侧可能感受到查询变慢。稳妥做法是放在低峰期,或者临时调低indices.recovery.max_bytes_per_sec(默认 40mb)来限速。
反方向,从 2 副本降到 1 副本,多出来的副本会被删除,磁盘空间释放是异步的,不会立刻看到水位下降,别以为没生效就反复执行命令。
3.3 常见配置组合的适用场景对照
| 主分片 | 副本 | 总份数 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| N | 0 | N | 可重建的日志、临时分析、批量导入中间态 | 节点故障会导致部分数据不可查,慎用于生产查询 |
| N | 1 | 2N | 绝大多数生产业务的主力配置 | 至少 2 个数据节点才能完全分配 |
| N | 2 | 3N | 读密集、对可用性要求高的检索服务 | 写入吞吐约降 30%,存储翻三倍,至少 3 个数据节点 |
| N | 3+ | 4N+ | 极少数超高读并发的场景 | 收益递减明显,多数情况下加协调节点更划算 |
这张表里的“N”不写具体数字,就是因为主分片数必须按数据量算,抄别人的数字是最容易踩的坑。我见过照搬“5 主 1 副本”模板建了几十个索引,结果每个索引只有不到 2GB 数据,白白浪费了一堆分片槽位。
4. 一次完整的索引规划推演
理论讲完,走一遍真实流程。假设要做一个内容检索服务,需要支持标题、正文、标签的全文检索,数据来源是历史存量加每日增量。
4.1 第一步:估数据量和增长曲线
存量数据 400GB(压缩后,ES 里的实际存储),日增 3GB,保留全部历史(不删)。一年后总量约 400 + 3 × 365 ≈ 1500GB。考虑段合并带来的临时膨胀,按 1.3 倍规划,也就是接近 2TB。这个数字决定了后面所有计算。
估算的时候有几个坑要避开:一是别用原始文本大小估算,ES 里的实际占用和字段类型强相关,text字段带索引和 doc values,占用可能是原始文本的 1.5 到 2 倍,而keyword字段如果开了doc_values,也比想象中大;二是_source默认是存的,等于原始 JSON 又存了一份;三是数值字段如果用scaled_float而不是double,能省不少空间。最靠谱的办法是先灌 10GB 真实数据进去,看GET _cat/indices?v&bytes=gb的实际数字,再线性外推。
4.2 第二步:定主分片数,兼顾未来扩容
按 2TB 总量、单分片 30GB 算,需要约 67 个主分片。但这不能一次建出来——因为这是一个持续增长的索引,不是按时间滚动的。有两种做法:
方案一是单索引 + 大分片数。直接建 60 个主分片,数据慢慢涨,分片不动。优点是管理简单,缺点是前期每个分片只有几 GB,长期处于“分片偏小”的状态,浪费槽位;而且 60 个分片意味着每次查询扇出到 60 个分片,对小结果集的查询不友好。
方案二是按时间滚动 + 别名。按月建索引,每月数据量约 90GB,切 3 个主分片,单分片 30GB,正好。12 个月就是 12 个索引、36 个主分片。查询走别名,覆盖所有月份。这个方案的优点是每个分片大小始终健康,缺点是查询要跨 12 个索引,需要靠时间范围过滤来裁剪。
我一般选方案二,但会加一个前置条件:查询必须带时间范围。如果业务上大量查询是“全库模糊搜标题”且不带时间条件,那滚动索引反而会增加扇出,这时候单索引大分片更合适。所以定方案之前先看查询模式,别只看数据量。
4.3 第三步:定副本数,把 SLA 和预算一起算进去
这个检索服务的可用性要求是“单节点故障不影响查询”,数据节点 3 台,分布在同一机房的不同机柜。1 副本就能满足单节点故障,因为副本和主分片在不同节点、不同机柜。设 2 副本的话,能扛住双节点同时故障,但概率极低,而成本是存储从 4TB(2TB × 2)涨到 6TB,写入延迟增加。
最终定 1 副本。存储按 4TB 规划,3 个节点每节点约 1.4TB,磁盘配 2TB 单节点,水位控制在 70% 以下。这个余量是给段合并和未来的索引重建留的——reindex 的时候新旧索引会同时存在,瞬时占用可能翻倍。
4.4 第四步:落地命令和验证清单
按月滚动的索引,建模板比建索引更重要:
PUT _index_template/content_search_template { "index_patterns": ["content_search-*"], "template": { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "refresh_interval": "5s", "index.mapping.total_fields.limit": 2000 }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "standard" }, "content": { "type": "text" }, "tags": { "type": "keyword" }, "publish_time": { "type": "date" } } } }, "priority": 200 }建完之后一定要验证三件事,别建完就不管了:
# 1. 分片是否全部分配 GET _cat/shards/content_search-2024.05?v&h=index,shard,prirep,state,node # 2. 单个分片的实际大小 GET _cat/shards?v&s=store:desc&h=index,shard,prirep,store # 3. 集群整体分片水位 GET _cat/allocation?v_cat/allocation返回的shards列就是每个节点当前承载的分片数,拿它和堆内存 × 20对比,就知道还剩多少额度。这个命令我基本每周都会跑一次。
5. 分片过多或过少的典型症状与排查实录
出问题的时候,症状往往不直接指向分片数,需要一层层往下扒。下面这些是我实际遇到过的几类情况。
5.1 分片过多:三种典型表现
第一种是集群状态收敛慢。表现是GET _cluster/health的响应时间从几十毫秒变成几秒,GET _cluster/pending_tasks里堆积了大量create-index或者shard-started任务。原因是每次状态变更都要把完整的分片路由表推给所有节点,分片越多报文越大。
第二种是查询延迟抖。表现是 P99 突然拉高,但 CPU、磁盘都不饱和。看GET _nodes/stats/thread_pool会发现search线程池的rejected在涨,队列打满。这是因为查询要扇出到所有分片,每个分片都要建自己的 Top-K 堆,分片数多了之后,协调节点合并结果的成本、以及线程池的排队成本都上来了。
第三种是堆压力。表现是老年代 GC 频繁,GET _nodes/stats/jvm里old_gen_used_percent长期在 75% 以上。分片级别的缓存和元数据是常驻的,空索引也占地方。一个 5000 分片的集群,光分片本身的开销可能就吃掉好几 GB 堆。
排查的第一步永远是量化:
# 按节点看分片数 GET _cat/allocation?v&h=node,shards,disk.percent,heap.percent # 找分片数最多的前 10 个索引 GET _cat/indices?v&s=pri.store.size:desc&h=index,pri,rep,docs.count,pri.store.size把每个索引的pri(主分片数)和pri.store.size除一下,就知道单分片平均大小。低于 5GB 的索引基本都属于分片过多,尤其是那种“10 主分片但只有 8GB 数据”的,一个分片不到 1GB。
5.2 分片过少:单分片过大带来的连锁问题
反过来的情况也不好受,只是症状出现得更晚。单个分片 100GB 以上时,首先受不了的是恢复时间——节点重启后要从磁盘加载分片、重放 translog,一个 100GB 的分片恢复可能要十几分钟,这期间集群状态是 yellow,可用性打折。其次是查询的并行度不够,一个聚合查询只能在一个分片的一个线程里跑,CPU 用不起来。
还有一种隐蔽表现是段合并压力。单个分片数据量大,segment 数量多,后台合并的 IO 峰值会很高,容易和查询抢磁盘。我遇到过一次 P99 抖动,最后定位到是某个 150GB 的单分片索引在触发大合并,把 IO 打满了。
判断标准很直接:GET _cat/shards?v&s=store:desc排序,看最大的几个分片是多少 GB。超过 50GB 就要留意,超过 80GB 基本该考虑拆分了(用 split API)或者滚动重建。
5.3 副本分配失败的排查清单
UNASSIGNED的副本是最常见的问题,原因有好几类,按出现频率排:
| 报错关键词 | 原因 | 解决方式 |
|---|---|---|
not enough nodes | 数据节点数 ≤ 副本数 | 减少副本数或增加数据节点 |
disk watermark exceeded | 节点磁盘水位超过 85% | 清理数据、扩容,或临时调高cluster.routing.allocation.disk.watermark.high |
allocation deciders | 分配规则(如机架感知)导致无可用节点 | 检查index.routing.allocation.*设置 |
max_retries exceeded | 分配反复失败,通常是网络或磁盘异常 | 检查目标节点日志和磁盘健康 |
same node as primary | 副本和主分片在同一节点 | 增加节点数或调整分配策略 |
定位这些原因,最快的工具是_cluster/allocation/explain:
GET _cluster/allocation/explain { "index": "content_search-2024.05", "shard": 0, "primary": false }它会直接告诉你“这个副本为什么分不出去”,把每个节点被拒绝的原因逐条列出来。这个接口比翻日志快十倍,我基本是遇到分配问题第一个就跑它。
5.4 日常巡检的几个关键指标
想避免临场救火,日常盯住这几个数就够了:
# 每节点分片数与堆使用 GET _cat/allocation?v # 未分配分片 GET _cat/shards?v&h=index,shard,prirep,state&s=state # 集群级指标 GET _cluster/health?pretty GET _nodes/stats/indices,jvm,thread_pool?human判断标准:_cat/allocation里每个节点的shards数,除以该节点堆内存 GB 数,得到“每 GB 堆承载的分片数”,这个值超过 20 就该动手了,接近 30 就是危险区。另外_cluster/health里的unassigned_shards不为 0 且持续存在,必须查原因,不能放着不管——它可能只是黄色状态,但背后可能藏着磁盘快满这类隐患。
6. 分片定错了怎么办:三条补救路径
主分片数创建后不能直接改,这是很多人卡住的地方。但路是有的,只是都要搬数据,选哪条取决于当前分片是太多还是太少、以及能不能接受停机。
6.1 分片太多:用 shrink 把多个分片合成一个
shrink 是把源索引的所有主分片合并到目标索引的少数分片上,目标分片数必须是源分片数的因子。比如源 12 个主分片,可以缩到 6、4、3、2、1。
触发 shrink 有几个前置条件:源索引必须只读(index.blocks.write: true)、所有主分片必须在同一个节点上(先做分片分配收敛)、目标索引名不能和源索引冲突。
PUT /content_search-2024.01/_settings { "settings": { "index.blocks.write": true } } POST /content_search-2024.01/_shrink/content_search-2024.01-shrunk { "settings": { "index.number_of_shards": 3, "index.number_of_replicas": 1, "index.routing.allocation.require._name": null, "index.blocks.write": null } }执行完之后,用别名把旧索引切到新索引,确认查询正常再删旧索引。整个过程最耗时的其实是“把分片收敛到同一节点”这一步,对于几十 GB 的索引,可能要在节点间搬几百 GB 数据,务必挑低峰期做。
6.2 分片太少:用 split 把分片拆开
反过来,目标分片数必须是源分片数的倍数。1 个主分片可以拆成 2、4、8……但 3 个主分片不能拆成 4 个,只能拆成 6、9、12。这个约束经常被人忽略,事先要算好。
split 同样要求源索引只读,而且要满足index.number_of_routing_shards的约束——这个设置在建索引时决定了最多能拆成多少份,默认值通常是主分片数的倍数,但如果你建索引时显式设了一个很小的值,可能拆不动。这是最容易踩的坑:建索引时为了省事没设 routing_shards,后面想拆发现拆不了,只能 reindex。
6.3 最通用的办法:reindex 到新索引
不管是太多还是太少,reindex 都能解决,代价是要全量搬一次数据。流程是:建好目标索引(分片数设对)→ 执行 reindex → 校验文档数 → 切别名 → 删旧索引。
POST _reindex?wait_for_completion=false&slices=auto&requests_per_second=5000 { "source": { "index": "old_index" }, "dest": { "index": "new_index" } }几点实操经验:slices=auto让 ES 自动按分片数并发,能显著提速;requests_per_second要限速,否则 reindex 会吃满集群 IO 把线上查询拖垮,宁可慢一点;wait_for_completion=false让请求异步返回,你能拿到 task id 用GET _tasks/<id>看进度。
切换那一刻有个小技巧:用别名做原子切换,避免出现“旧索引删了、新索引还没好”的空窗:
POST _aliases { "actions": [ { "remove": { "index": "old_index", "alias": "content_search" } }, { "add": { "index": "new_index", "alias": "content_search" } } ] }这个操作在 ES 里是原子的,客户端不会感知到中断。
注意:reindex 期间新旧索引会同时存在,磁盘瞬时占用接近翻倍。执行前确认磁盘水位在 60% 以下,否则中途磁盘打满,整个集群会进入只读保护模式,写入全部失败。
7. 踩过的坑和我的默认参数起点
最后聊几个反直觉的现象,以及我自己现在建索引时的默认起点,你可以拿去当模板用。
7.1 三个容易踩的认知误区
第一个误区是“分片数要按节点数来设,节点多分片就多”。这是把分片当成了负载均衡的手段。实际上分片首先服务的是单分片数据量,节点数决定的是副本怎么分配。3 个节点的集群给一个 5GB 的小索引用 12 个主分片,分片确实均匀分布在 3 个节点上了,但每个分片才 400MB,完全是浪费。
第二个误区是“副本数设得越高越安全”。安全性取决于故障域,不取决于副本数。3 个节点全在同一台物理机上,设 2 副本和设 1 副本的安全性差不多,都扛不住整机故障。真正有用的是把节点分散到不同的机架,同时副本设 1;这时候再加副本,边际收益很低,成本却是线性的。
第三个误区是“主分片数以后可以改”。技术上能改,但那叫“重建索引”,代价是全量数据搬迁。所以建索引那一刻就要想清楚未来一到两年的数据量,宁可一开始分片稍微多一点(比如多留 50%),也好过三个月后重建。
7.2 我现在的默认起点
对于新建的业务索引,我的默认配置是:单分片目标 25GB 到 35GB,按预估两年数据量算主分片数,并且取偶数(方便后续 shrink 到更少分片或者 split);副本数按数据节点数决定,3 节点以上且分散部署就设 1,2 节点设 1,单节点设 0(仅限可以重建的索引)。
对于时间序列型索引,一律用 ILM 配滚动索引,滚动周期按单日或单月数据量定,保证每次滚动的单分片落在 30GB 左右;老索引超过保留期后先 shrink 到 1 个主分片,再等一段时间删除,这样能显著降低长期分片总数。这一招对控制分片水位特别有效——线上查询只关心最近几天的数据,历史数据虽然保留但没人查,没理由占着那么多分片槽位。
还有一条:建索引的脚本里,永远显式写number_of_shards和number_of_replicas。别依赖默认值,ES 7.x 默认是 1 主 1 副本,看起来合理,但你的场景未必就适合这个默认;更重要的是,显式写出来等于在代码里留了一份文档,后面接手的人能看懂当时的意图,也能顺着算出总容量。
7.3 一个压测的小建议
参数定完别急着上线,先做一次定向压测:按规划建好索引,灌入目标数据量的 30% 到 50%,然后跑真实查询抽样,同时观察_cat/allocation的分片数、_nodes/stats/jvm的堆曲线、_nodes/stats/thread_pool的队列。如果发现 search 线程池的队列长期有积压,说明分片偏少或者查询本身需要优化;如果堆的常驻占用随分片数线性上升,说明分片偏多。
压测数据量不必做满,做到一半就能看出趋势。我一般是把_cat/shards按store:desc排序,确认最大分片不超过 50GB、平均分片不低于 10GB,这两个边界守住了,后面基本不会出大问题。