第 7
篇 把 Lucene Index 讲成「段 +
IndexWriter + NRT Reader」的单机模型;第 10
篇 说明排序与聚合走 DocValues
列式侧面。到了 Elasticsearch,运维界面里出现的
Index 并不是 Lucene Index
的同义词复刻——它是 逻辑命名空间 + 分片路由表 +
副本策略 的分布式封装。第一次给索引设
number_of_shards: 50
时若只想到「并行度」,却忽略 每个主分片都是独立
Lucene 索引、都会在 cluster state
里占一条路由元数据,几个月后 master 的 Cluster
State
Update(CSU)变慢,往往与分片数而非文档条数更相关。
本文锚定 Elasticsearch 8.x Reference(Index modules、Cluster、Shard allocation),回答四个会直接决定容量规划的问题:
- ES Index / Primary Shard / Replica 与 Lucene Index、Segment 各在哪一层?
- 文档如何经 主分片哈希路由
落盘?
_routing与自定义路由键的代价是什么? - 副本 分担读负载时,协调节点如何在主/副本间挑选执行分片?
- cluster state 里装的是什么、更新代价为何随分片数超线性恶化?与 搜索引擎架构 的分片膨胀叙事如何衔接?
本文是「全文检索引擎」系列第 11 篇(共 18 篇)。→ 系列目录
版本锚定:Elasticsearch 8.x Reference;底层 Lucene 9.x(与 ES 8.15 发行版一致)。本篇不写千节点吞吐自测;机制结论来自官方文档与 Lucene 段不可变模型。
一、三层名词:ES Index、Shard、Lucene Segment
| 层级 | 对象 | 可变性 | 本篇关注点 |
|---|---|---|---|
| 集群 | Index(逻辑) | 映射、设置、别名可变 | 路由表、副本因子 |
| 节点 | Shard(物理 Lucene Index) | 段内不可变;通过 refresh/merge 演进 | 主/副本、分配 |
| 段 | Segment | 写入后不可变 | 见 第 7–8 篇 |
一个 Primary Shard 在数据节点上就是一个完整的 Lucene 索引目录:拥有自己的 FST 词典、postings、DocValues 文件集合(第 4–5、10 篇)。Replica 不是「只读缓存文件」,而是 与主分片同构的 Lucene 索引副本,通过复制索引操作保持段集合一致(复制细节与 translog 在 第 12 篇)。
flowchart TB
IDX["ES Index<br/>settings + mapping + alias"]
P0["Primary shard 0<br/>Lucene Index on node A"]
P1["Primary shard 1<br/>Lucene Index on node B"]
R0["Replica shard 0<br/>Lucene Index on node C"]
R1["Replica shard 1<br/>Lucene Index on node D"]
IDX --> P0
IDX --> P1
P0 -. replication .-> R0
P1 -. replication .-> R1
P0 --> S0["Segments"]
P1 --> S1["Segments"]
创建索引时
number_of_shards(主分片数)在 ES 8.x 中
之后不可改(要改变分片数需 reindex 或
shrink/split 等迁移手段,见官方 Index
modules)。number_of_replicas
可随时调整——它只改变
每个主分片挂多少副本,不改变路由空间大小。
二、主分片路由:哈希取模与 scatter-gather
2.1 默认路由公式
对单条文档的索引、GET、DELETE,协调节点(或接收写入的数据节点)先算 目标主分片:
\[ \text{shard\_id} = \text{hash}(\text{routing}) \bmod N_{\text{primary}} \]
routing默认等于文档_id(或由客户端显式传入的routing参数)。hash在 ES 8.x 中对 routing 字符串做 MurmurHash3 后取非负整数(实现细节以org.elasticsearch.cluster.routing.OperationRouting为准;对外语义以 Reference Document APIs — Routing 为准)。- \(N_{\text{primary}}\)
即创建索引时的
index.number_of_shards。
因此 同一路由键的文档共址 在同一主分片——这与 Redis Cluster 的 hash tag 共槽动机类似(见 Redis Cluster),但 ES 没有 跨分片多文档原子事务;共址只为 定向查询 与 关联文档局部性。
PUT /orders/_doc/1001?routing=user_42
{
"user_id": "user_42",
"amount": 128
}查询时带上相同 routing,协调节点可把请求
扇入单个主分片(及其被选中的副本),避免全索引
scatter-gather。若省略 routing,按 _id 哈希,则
按 _id 点查 仍只需一片,但
无法 保证「同一 user_id
的全部订单」落在一片——除非写入时统一用 user_id
作 routing。
2.2 搜索为何常要查所有分片
匹配查询(如
match、query_string)在不知道词项落在哪些文档的前提下,倒排相交必须在
每个主分片 上各做一遍,再在协调节点
归并(第 13
篇)。这就是分布式倒排的经典
scatter-gather:与 Zobel & Moffat
综述里「分区倒排 +
结果合并」模型一致——并行度来自分片数,协调开销来自
跨分片归并 Top-K 与
全局统计(可选 DFS,见第 13 篇)。
| 访问模式 | 是否必须全分片 | 典型 API |
|---|---|---|
按 _id / 已知 routing |
否 | GET /idx/_doc/id?routing=... |
| 全文 / 过滤 / 聚合 | 是(除非 routing 裁剪) | _search、_msearch |
| 按时间滚动索引 | 可用别名裁剪索引集 | index template + ILM |
自定义 routing 的风险 在 搜索引擎架构 第三节已用「大卖家订单倾斜」说明:哈希均匀 ≠ 业务均匀。本篇只强调机制——热点主分片 无法靠加副本根治,因为 写入仍打在主分片。
三、副本:读扩展、容错与分配约束
每个主分片可有 index.number_of_replicas
个副本(默认 1)。副本分片
不参与写入路由;写入路径总是 主分片
→ 复制到副本(复制与 refresh 顺序见第 12 篇)。
读路径上,协调节点对 每个主分片副本组 挑选一个 活跃分片 执行查询——默认在组内 轮询 以均衡负载(Reference Search — Preference 可覆盖)。因此增加副本可线性扩展 只读 吞吐,但 不改变 单文档写入的主分片瓶颈。
sequenceDiagram
participant C as Coordinating node
participant P as Primary shard
participant R as Replica shard
C->>P: index / bulk (write)
P-->>R: replicate indexing ops
C->>R: search phase (read may hit replica)
R-->>C: local top hits + aggs
分片分配器(Shard Allocator)在 master 上运行,约束包括(Reference Cluster — Shard allocation):
- 主分片与副本 不同节点(否则节点宕机同时丢主备)。
- 磁盘水位线:超过
cluster.routing.allocation.disk.watermark.high不再迁入新分片。 - 可选 awareness attributes(机架/可用区)强制主备跨域。
与 architecture/42 对照:该文用运维故事说明 分片数膨胀 → CSU 变慢;本篇钉机制——副本数翻倍 = 活跃分片数近似翻倍,同样放大 cluster state 与分配心跳,而非「免费高可用」。
四、cluster state:元数据体积与更新代价
Cluster state 是 master-eligible 节点维护的 集群级一致快照,包含但不限于(Reference Cluster — Cluster state):
- 索引级:settings、mapping、aliases、lifecycle 策略绑定。
- 分片级:每个 主/副本分片 的
state(STARTED/INITIALIZING/RELOCATING…)、分配节点、恢复进度。 - 节点级:角色、属性、版本。
任何创建索引、改 mapping、分片开始/完成恢复、节点上下线,都会触发 新的 cluster state 版本 并广播给所有节点。数据节点收到后更新本地路由表,搜索与写入才能找到正确分片。
4.1 为何分片数是「元数据维度」
每个 Lucene 分片在 JVM 堆内持有段元数据、缓存词典句柄、文件描述符;在 cluster state 里则持有 路由条目 + 恢复状态机。分片数 \(S\) 增大时:
- CSU 频率:批量建索引、滚动日志按天建索引、多租户「一租户一索引」都会让 \(S\) 与索引数 \(I\) 同步膨胀。
- 广播与 diff 成本:状态体随 \(S\) 增长;大状态下 master 与数据节点 GC、Full GC 压力上升——architecture/42 的「8000 分片」案例是 症状叙事,机制核心是 \(O(S)\) 乃至超线性的协调面开销,而非某个魔法阈值。
官方 sizing 指导(Reference Size your shards,B 级工程建议)强调:单分片目标体积(常见建议 10–50 GB 量级,随硬件与查询类型变化)与 每 GB 堆内存可分片数上限——目的是让 \(S\) 与数据量匹配,而不是「分片越小越安全」。
4.2 运维可观测:从指标反推 \(S\)
不写集群 QPS,只列 与分片数直接相关
的只读诊断(ES 8.x _cat API):
# 活跃分片总数(primary + replica)
curl -s 'http://localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node' | head
# 单节点承载分片数是否倾斜
curl -s 'http://localhost:9200/_cat/allocation?v'
# 集群状态体量间接信号:pending tasks 堆积
curl -s 'http://localhost:9200/_cluster/pending_tasks?pretty'pending cluster state tasks 长期非空且与
批量建索引 / 分片恢复 同期出现,应优先怀疑
\(S\) 或索引数
\(I\)
过大,而不是先加数据节点 CPU。完整症状树见 第 17
篇。
4.3 与 Lucene 单索引多段的对比
第 8 篇 讨论 段过多 拖慢查询,解法是 merge。分片过多 是分布式层的同类问题,但 不能 用 Lucene merge 合并——只能 shrink、reindex、合并索引模板,或从设计上减少 \(I \times N_{\text{primary}}\)。这是 工程间隙:论文里的分布式倒排分区常假设分片数固定且远小于生产误配下的指数增长。
五、学术谱系、争论与开放问题
谱系:倒排索引分区 + 查询广播(Zobel
& Moffat, Inverted Files for Text Search, 2006
survey)→ 各系统固定分区数(ES 创建时锁定
number_of_shards)→ 协调层 scatter-gather +
可选全局统计(DFS)→ 云原生日志栈按
时间索引 控制 \(I\)(与 observability/08
日志选型 衔接)。
争论:
- 固定分片数 vs 动态重分片:ES 选择创建时固定以降低运行时迁移复杂度;Cassandra/Dynamo 风格动态环在 成员变更 上更灵活,但倒排 段复制成本 远高于键值行迁移。二者优化目标不同,不宜用「环更优雅」否定固定分区。
- 副本读扩展 vs 写路径主分片:增加副本对 写入延迟 可能略增(复制 fan-out),对 热点写 无帮助——这与 architecture/42 强调的规划失误(分片过碎)是同一运维面。
开放问题:
- 跨分片 join 与 全局排序 在 ES 中仍非关系代数意义下的 join;ES|QL 等路径在分片边界上的代价模型,生产侧更多靠 索引设计规避,而非引擎自动最优。
- Serverless / 托管 形态下的分片自动伸缩对 cluster state 的隐藏——自管集群读者仍需理解本篇路由与 \(S\) 代价,才能读懂厂商「自动分片」背后的假设。
- 主分片迁移(shrink/split/reindex)期间 CSU 与恢复流量对搜索尾延迟的影响,缺少与业务写入速率解耦的通用公式;规划时应按 维护窗口 而非「在线无感」假设排期。
六、常见误解
| 误解 | 事实 |
|---|---|
| 「ES 的 index = Lucene 的 index」 | ES index 是 逻辑层;每个 primary shard 才对应一个 Lucene index |
| 「副本越多写入越快」 | 写入只走 主分片;副本增加复制开销,只扩展 读 |
「number_of_shards 可以随时改大」 |
主分片数创建后 不可改;只能迁移数据或建新索引 |
| 「哈希路由保证负载均衡」 | 保证的是 键空间均匀;业务热点(大 routing)仍会造成 单分片热点 |
| 「分片越小越安全」 | 过碎分片使 \(S\) 暴涨,cluster state 与段固定开销 主导故障模式(见 architecture/42) |
| 「协调节点不参与计算」 | 协调节点做 路由、归并、fetch 二次扇出;高扇出时它也会成为尾延迟点 |
七、小结
三句话小结:
- Elasticsearch 把 每个主分片
实现为节点上的 独立 Lucene
索引;
hash(routing) mod N决定写入落点,默认 routing 为_id。 - 副本 分担读负载与容错,但不改变写路径;协调节点在每个副本组内挑选执行分片,分配器强制主备异节点。
- cluster state 随分片数与索引数增长而膨胀,CSU 与广播成本是分布式层的硬约束;分片规划应对标数据体积与查询扇出,而非盲目追求「更多并行」。
下一篇拆解 refresh、flush、translog
三者的保证边界,以及 refresh_interval 与
wait_for 如何塑造近实时语义:refresh
· flush · translog。
参考资料
规范与文档(A 级)
- Elasticsearch 8.x Reference, Index modules —
Index settings、
number_of_shards/number_of_replicas。 - Elasticsearch 8.x Reference, Document APIs — Routing。
- Elasticsearch 8.x Reference, Cluster — Cluster state、Shard allocation。
- Elasticsearch 8.x Reference, Size your shards(分片体积与堆内存指导)。
- Apache Lucene 9.x, IndexWriter / Segment 不可变模型(与 第 7 篇 一致)。
论文与综述(A 级)
- Zobel, J. & Moffat, A., Inverted Files for Text Search, 2006(分布式倒排分区与合并语义)。
站内对读(B 级)
返回 系列目录 | 上一篇:DocValues 与 stored fields | 下一篇:refresh · flush · translog
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】查询路径:协调节点上的 query + fetch
拆解 Elasticsearch 8.x 搜索的两阶段 scatter-gather:协调节点扇出 query phase 收集 doc id 与排序键,再 fetch phase 取 stored fields,reduce 全局 Top-K;并说明 DFS 如何修正跨分片 IDF。
【全文检索引擎】refresh · flush · translog:近实时与持久化边界
用保证矩阵拆解 Elasticsearch 8.x 的 refresh、flush、translog 各自承诺什么:可搜窗口、磁盘持久化、崩溃恢复;对照 refresh_interval 与 wait_for,并在单节点 8.15 上实测写入可见性。
【列存引擎内核】ReplicatedMergeTree
ReplicatedMergeTree 副本协调:Log entry、ClickHouse Keeper/ZooKeeper 路径、副本同步与 recovery。双节点实验步骤(本环境未部署)。24.x LTS 默认推荐 Keeper。
【列存引擎内核】Distributed 引擎与分布式查询路由
ClickHouse Distributed 表的分片键、写入路由与 SELECT 下推;GLOBAL IN/JOIN 的代价与替代方案;与 ReplicatedMergeTree 副本层的关系;对照 PG Citus 的边界。