数据库基础体系 · 第 42/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Elasticsearch 分片与集群:路由、副本、恢复、容量和故障诊断

Elasticsearch 的索引不是一个不可再分的文件,而是由多个 主分片(primary shard) 组成;每个主分片还可以有一个或多个 副本分片(replica shard)。多个节点通过集群协调、分片分配和请求路由共同提供数据写入、查询、故障转移与扩展能力。

理解这些机制,需要先区分四个层次:

  1. 索引:面向用户的逻辑数据集合。
  2. 分片:索引数据的物理分区,也是搜索和恢复的基本单位。
  3. 节点:运行 Elasticsearch 进程并承载分片、协调请求或维护集群状态的实例。
  4. 集群:由节点共同维护的集群状态、索引元数据和分片布局。

本文讨论 Elasticsearch 官方稳定版本中通用的语义。具体默认值、节点角色和配置项可能随版本变化,生产环境应以对应版本的官方文档为准。


一、为什么需要分片和集群

假设一个索引有 1 TB 数据,而单台节点的磁盘、内存或 CPU 无法承载全部数据。可以将索引拆成多个主分片:

logs
├── primary shard 0
├── primary shard 1
├── primary shard 2
└── primary shard 3

这些分片可以分布到不同节点:

node-a: shard 0, shard 2
node-b: shard 1
node-c: shard 3

这样做带来三类能力:

  • 容量扩展:数据可以分布到多个节点和磁盘。
  • 并行查询:不同分片可以并行执行搜索和聚合。
  • 故障隔离与恢复:主分片损坏或节点离线时,可以从副本分片恢复服务。

但分片不是免费的。一个搜索请求可能需要访问多个分片;分片越多,协调、文件句柄、内存、集群状态和恢复任务越多。因此“分片越多越好”是错误的,分片数量应由数据容量、写入并发、查询模式和恢复要求共同决定。


二、集群中的节点、集群状态与主节点

2.1 节点角色

当前 Elasticsearch 通常通过节点角色决定节点承担的职责。常见角色包括:

  • master:参与选举并维护集群状态。
  • data_contentdata_hotdata_warmdata_colddata_frozen:承载不同数据生命周期阶段的分片。
  • ingest:执行 Ingest Pipeline。
  • remote_cluster_client:参与跨集群连接。
  • mltransform 等:承担相应功能。

节点也可以承担多个角色。没有专门角色的节点通常可作为协调节点,接收请求、路由到数据节点并合并结果。

在中大型集群中,常见部署方式是:

3 个专用 master 节点
多个 data 节点
可选的 ingest 或协调节点

专用 master 节点不一定承载数据,但它们必须能够稳定保存集群状态并相互通信。

2.2 集群状态

集群状态包含但不限于:

  • 节点列表;
  • 索引和数据流元数据;
  • 分片数量和分配结果;
  • 索引设置与映射;
  • 路由表;
  • 集群级配置。

主节点负责发布集群状态。其他节点收到状态后,才能知道某个索引有哪些分片、分片当前在哪个节点以及哪些节点可以处理请求。

因此,主节点故障不等于数据立即不可用。只要剩余节点能选出新的主节点,数据节点仍可能继续提供服务;但如果集群无法形成法定多数,集群状态通常无法继续更新,索引创建、分片重新分配等操作会受到影响。

2.3 为什么通常需要三个 master-eligible 节点

集群选主依赖多数派。假设有三个可参与选主的节点:

节点 A、B、C
法定多数 = 2

A 离线后,B 和 C 仍可形成多数。

如果只有两个节点:

节点 A、B
法定多数 = 2

任意一个节点离线,剩下一个节点都不能安全地自动确认自己拥有集群控制权。这是为了避免网络分区时出现两个集群同时接受写入,即“脑裂”。

cluster.initial_master_nodes 只用于新集群的首次引导。集群已经形成后,不应把它当成每次重启都需要保留的普通配置;误用或把不同集群的节点混在一起,可能导致错误的集群引导。


三、主分片、副本分片和分片状态

3.1 主分片与副本分片

创建索引时可以指定主分片和副本数量:

curl -X PUT 'http://localhost:9200/orders' \
  -H 'Content-Type: application/json' \
  -d '{
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }'

该索引的逻辑分片数为:

主分片数 = 3
每个主分片的副本数 = 1
物理分片总数 = 3 × (1 + 1) = 6

副本不是“整个索引的一份备份”,而是每个主分片的副本。例如:

shard 0:
  primary  -> node-a
  replica  -> node-b

shard 1:
  primary  -> node-b
  replica  -> node-c

shard 2:
  primary  -> node-c
  replica  -> node-a

同一个分片的主分片和副本分片通常不会分配到同一个节点。否则节点故障时,主副本会同时丢失。但这不是把副本放在不同机房的自动保证;若需要机架或可用区隔离,应使用节点属性和 allocation awareness 或 forced awareness。

3.2 主分片数与副本数的可变性

副本数可以在线调整:

curl -X PUT 'http://localhost:9200/orders/_settings' \
  -H 'Content-Type: application/json' \
  -d '{
    "index": {
      "number_of_replicas": 2
    }
  }'

这会要求集群创建或删除副本分片,并触发分配和恢复。

主分片数量通常不能像副本数一样直接修改。原因是文档的默认路由依赖主分片数量:

目标分片 = hash(routing_value) mod number_of_primary_shards

如果主分片数突然从 3 变成 6,同一个 routing value 的目标分片可能改变,原有数据路由表就不再一致。

需要改变主分片数量时,常见方式包括:

  • 新建目标索引并使用 _reindex
  • 使用 shrink 将索引缩小;
  • 使用 split 将索引拆分;
  • 通过 ILM 的 rollover 创建后续索引。

这些操作有不同的前置条件和数据移动成本,不能把它们等价为“在线修改一个整数”。

3.3 分片状态

使用 _cat/shards 可以查看分片状态:

curl 'http://localhost:9200/_cat/shards/orders?v'

典型结果类似:

index  shard prirep state   docs store ip        node
orders 0     p      STARTED  ...  ...   10.0.0.1 node-a
orders 0     r      STARTED  ...  ...   10.0.0.2 node-b
orders 1     p      STARTED  ...  ...   10.0.0.2 node-b
orders 1     r      STARTED  ...  ...   10.0.0.3 node-c

常见状态包括:

  • STARTED:分片已分配并可服务请求。
  • INITIALIZING:正在创建或恢复。
  • RELOCATING:正在从一个节点迁移到另一个节点。
  • UNASSIGNED:没有分配到节点。

UNASSIGNED 不一定立即表示数据损坏,也可能是:

  • 没有满足副本的节点;
  • 磁盘水位限制阻止分配;
  • allocation rule 不允许分配;
  • 节点刚重启,恢复尚未完成;
  • 主分片丢失,且没有可用副本。

四、路由:文档写入究竟进入哪个分片

4.1 默认文档路由

写入文档时,Elasticsearch 需要决定它属于哪个主分片。默认使用文档 _id 作为 routing value:

routing_value = _id
target_shard = hash(routing_value) mod primary_shard_count

例如,假设索引有 4 个主分片:

_id = "user-100"
hash("user-100") mod 4 = 2

该文档会写入主分片 2,而不是由客户端随机选择。

这里的关键点是:客户端不应该自行实现 Elasticsearch 的哈希算法来推测分片。算法细节和兼容行为属于 Elasticsearch 的实现和 API 语义,应用应通过 API 指定 routing 或让 Elasticsearch 完成路由。

4.2 自定义 routing

对于具有明显租户或用户边界的数据,可以指定 routing:

curl -X PUT 'http://localhost:9200/orders/_doc/order-001?routing=tenant-a' \
  -H 'Content-Type: application/json' \
  -d '{
    "tenant_id": "tenant-a",
    "amount": 120
  }'

此时决定分片的是:

hash("tenant-a") mod primary_shard_count

而不是 _id

查询时必须使用相同的 routing,才能将请求限制到正确的分片:

curl -X GET 'http://localhost:9200/orders/_search?routing=tenant-a' \
  -H 'Content-Type: application/json' \
  -d '{
    "query": {
      "term": {
        "tenant_id": "tenant-a"
      }
    }
  }'

如果写入时使用了自定义 routing,而查询时不提供 routing,查询可能无法找到文档,因为它默认只按普通方式向所有相关分片发起搜索;对于某些按 routing 定位的读写场景,错误或缺失 routing 会直接导致找不到文档或访问错误分片。

routing 的收益

如果一个租户的所有文档都使用租户 ID 路由,那么只查询该租户时,可以减少搜索分片数:

没有 routing:访问 0、1、2、3 共 4 个分片
使用 routing:只访问 hash(tenant-a) 对应的 1 个分片

routing 的风险

自定义 routing 可能产生热点:

tenant-a -> shard 0
tenant-b -> shard 0
tenant-c -> shard 0

如果某个大租户远大于其他租户,单个分片会积累更多数据和请求,即使整个集群看起来还有大量空闲容量。

因此 routing 的本质是一个取舍:

  • 更少的查询 fan-out;
  • 更强的数据局部性;
  • 更高的热点风险;
  • 应用必须在每次相关读写中正确传递 routing。

4.3 写入请求的主副本路径

一个普通写入的逻辑过程如下:

客户端
  |
  v
协调节点计算目标主分片
  |
  v
目标节点上的 primary
  |
  +--> 写入本地 Lucene 索引和 translog
  |
  +--> 转发给 replica shard
  |
  v
根据 wait_for_active_shards 返回成功或失败

写入首先由主分片处理。主分片将操作复制到副本分片,副本成功响应的数量会影响请求何时返回。

wait_for_active_shards 可以限制写入需要等待多少个活动分片。例如:

curl -X PUT 'http://localhost:9200/orders/_doc/order-002?wait_for_active_shards=2' \
  -H 'Content-Type: application/json' \
  -d '{
    "tenant_id": "tenant-a",
    "amount": 80
  }'

这里的 2 表示主分片和至少一个副本都处于活动状态时才满足条件。它不是事务提交,也不代表数据已经跨机房持久化。

Elasticsearch 的 Bulk 请求也不是数据库事务:Bulk 中某一条失败,不会自动回滚其他已成功的条目。应用必须逐条检查响应中的 items


五、查询路由与分布式搜索

5.1 Scatter-Gather

一次查询通常经过两个阶段:

Scatter:
协调节点将查询发送到每个目标分片

Gather:
各分片返回局部结果,协调节点合并、排序、截取

例如一个索引有 5 个主分片、每个主分片 1 个副本。一次普通搜索不会同时查询 10 个物理分片,而是为每个逻辑分片选择主分片或副本中的一个副本:

shard 0 -> primary
shard 1 -> replica
shard 2 -> primary
shard 3 -> replica
shard 4 -> primary

具体选择会受到节点负载、请求 preference 和集群状态影响。

如果没有 routing,查询通常需要访问索引中的所有相关逻辑分片。分片数量越多,协调阶段越复杂,尤其是:

  • from + size 较大的分页;
  • 高基数聚合;
  • 多索引搜索;
  • 大量并发查询。

size 为 100 时,每个分片可能需要返回候选结果,再由协调节点合并;这不是“只从整个集群取 100 条”的单节点操作。

5.2 深分页为什么昂贵

对于按相关性或排序字段排序的搜索,协调节点要从每个分片获取候选结果,再做全局排序。

假设有 20 个分片,请求:

{
  "from": 10000,
  "size": 100
}

每个分片可能需要准备至少约 from + size = 10100 个候选位置,协调节点再合并它们。实际执行还会受到排序字段、查询类型和优化策略影响,但其成本随分片数和分页深度增长。

连续遍历大量结果时,应优先考虑:

  • search_after
  • Point in Time(PIT)配合 search_after
  • 适合离线处理的 scroll

这些属于查询层面的机制,不能用增加副本或增加节点解决。


六、副本的作用、限制和一致性边界

6.1 副本提供什么

副本主要用于:

  1. 故障转移:主分片所在节点故障时,副本可被提升为新的主分片。
  2. 查询并行:多个副本可以承接查询。
  3. 读吞吐扩展:在查询负载合适时,增加副本可以增加可用的查询执行位置。

6.2 副本不提供什么

副本不是以下机制的替代品:

  • 不是长期备份;
  • 不是跨集群灾难恢复;
  • 不是防止误删除的历史版本;
  • 不是事务提交日志;
  • 不能保证主副本一定位于不同机房。

如果应用误执行删除,副本通常也会复制这个删除。要恢复误删数据,需要快照或其他独立备份。

快照存储的是索引数据和元数据的备份视图,适合灾难恢复、迁移和长期保留;副本则主要用于在线可用性。二者解决的问题不同。

6.3 副本数量与可用性

假设:

3 个主分片
1 个副本

正常情况下每个逻辑分片有两个物理副本。一个节点故障后,只要每个主分片至少还有一个可用副本,集群可以通过提升副本继续提供完整数据服务。

但如果副本数为 0:

  • 主分片所在节点故障;
  • 没有可提升的副本;
  • 对应主分片会变成 UNASSIGNED
  • 集群健康可能变为 red
  • 该分片上的数据和请求不可用,除非从快照或其他来源恢复。

七、故障转移与分片恢复

7.1 节点故障时发生什么

shard 0 为例:

故障前:
node-a: shard 0 primary
node-b: shard 0 replica

当 node-a 离线后,集群会经历大致过程:

1. 主节点检测到 node-a 不再属于当前集群
2. shard 0 primary 被标记为不可用
3. node-b 上的 replica 被提升为新的 primary
4. 如果还有其他副本,集群尝试补建新的 replica
5. 集群状态从 red 或 pending 状态恢复

检测速度受节点间探活和故障检测配置影响,不能简单理解为“网络断开后立即完成切换”。

如果原节点恢复上线,Elasticsearch 不一定直接把它当作新的主分片继续使用,而是根据当前集群状态决定是否进行 peer recovery、重建副本或重新分配。

7.2 恢复类型

常见恢复来源包括:

  • Peer recovery:从同一集群中的其他分片复制数据。
  • Replica promotion:将现有副本提升为主分片。
  • Gateway recovery:节点重启或集群重建时,从本地已存在的数据恢复。
  • Snapshot recovery:从快照仓库恢复索引。

恢复过程通常涉及:

  1. 分配目标节点;
  2. 检查分配决策;
  3. 创建分片目录;
  4. 复制 Lucene segment;
  5. 同步 translog 或执行必要的操作重放;
  6. 校验并切换为可服务状态;
  7. 重新建立副本或继续迁移。

恢复期间,分片可能处于 INITIALIZINGRELOCATING。大量恢复会同时消耗:

  • 磁盘读写;
  • 网络带宽;
  • CPU;
  • 堆内存和文件句柄;
  • 正常查询与写入的资源。

7.3 sequence number 与恢复的一致性

Elasticsearch 使用 sequence number、checkpoint 等机制跟踪分片操作进度。副本并不是简单地“定时复制一个完整目录”,而是由主分片协调操作复制和确认。

这使得恢复时可以根据已有数据判断哪些操作已经存在、哪些操作需要补齐。但这不应被理解为任意情况下都能无限保留增量;如果副本落后时间过长、历史操作不可用或数据不满足安全条件,可能需要更大范围的文件复制,甚至从快照恢复。

写入的持久化还受 translog durability 配置影响:

  • request:每次请求按持久化语义处理后再确认,通常更安全但写入成本更高。
  • async:按周期异步刷写 translog,节点在周期内突然崩溃时可能丢失尚未持久化的最近操作。

这仍然不是跨节点事务。副本确认、磁盘持久化和快照恢复分别对应不同层次的可靠性。


八、如何诊断分片没有分配

8.1 先看集群健康

curl 'http://localhost:9200/_cluster/health?pretty'

典型结果:

{
  "cluster_name": "prod-es",
  "status": "yellow",
  "number_of_nodes": 3,
  "active_primary_shards": 12,
  "active_shards": 20,
  "unassigned_shards": 4
}

状态含义:

  • green:主分片和副本都已分配。
  • yellow:主分片已分配,但至少有副本未分配。
  • red:至少有一个主分片未分配。

yellow 不等于数据已经丢失,但表示副本保护不完整。red 需要优先确认是否影响具体索引和请求。

查看未分配分片:

curl 'http://localhost:9200/_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node'

8.2 使用 allocation explain

不要只根据 _cat/shards 猜原因。对具体分片执行:

curl -X GET 'http://localhost:9200/_cluster/allocation/explain' \
  -H 'Content-Type: application/json' \
  -d '{
    "index": "orders",
    "shard": 0,
    "primary": false
  }'

返回结果会说明:

  • 当前分片为何未分配;
  • 候选节点;
  • 每个节点的 allocation decider;
  • 哪个决策是 NO
  • 是否需要等待;
  • 是否受到磁盘、过滤规则、同分片限制或节点角色影响。

常见决策原因包括:

磁盘水位

Elasticsearch 使用磁盘水位避免把分片分配到磁盘即将耗尽的节点。具体默认阈值和版本行为应以当前版本为准。达到 flood-stage 时,相关索引可能被设置为:

index.blocks.read_only_allow_delete = true

这会表现为写入失败。修复磁盘空间后,还要检查并按需解除索引块:

curl -X PUT 'http://localhost:9200/orders/_settings' \
  -H 'Content-Type: application/json' \
  -d '{
    "index.blocks.read_only_allow_delete": null
  }'

解除之前必须确认磁盘水位问题已经解决,否则很快会再次进入保护状态。

分配过滤和 awareness

例如配置了:

index.routing.allocation.require.zone = zone-a

但集群中没有符合条件的节点,分片就不会分配。

又或者副本和主分片被要求分布到不同可用区,但当前节点数量不足,也会导致副本 UNASSIGNED

同分片约束

同一个逻辑分片的 primary 和 replica 不应放到同一节点。若只有一个符合条件的数据节点,而副本数大于 0,副本会保持未分配。

节点角色不匹配

索引要求某类 data 节点,但当前集群没有满足角色条件的节点,分配也会被拒绝。

8.3 诊断节点和分片分布

curl 'http://localhost:9200/_cat/nodes?v&h=name,ip,roles,heap.percent,ram.percent,cpu,load_1m,disk.used_percent'
curl 'http://localhost:9200/_cat/allocation?v'

这些命令可以帮助判断:

  • 某个节点是否承载了明显更多分片;
  • 磁盘是否接近水位;
  • JVM heap 是否长期过高;
  • 节点是否实际具备所需角色;
  • 分片是否正在大量迁移。

不要只看平均值。分片和数据通常并不均匀,单个热点节点可能已经达到瓶颈,而整个集群的平均 CPU 仍然很低。


九、容量规划:不能只按磁盘总量计算

9.1 物理容量公式

最基础的物理分片数量是:

物理分片数 = 索引数 × 主分片数 × (1 + 副本数)

例如:

100 个日索引
每个索引 5 个主分片
每个主分片 1 个副本

则物理分片数为:

100 × 5 × 2 = 1000 个

如果每个主分片平均 20 GB,数据本体大致为:

100 × 5 × 20 GB = 10 TB

加上一个副本后,分片数据本体约为:

10 TB × 2 = 20 TB

但生产容量不能只按这个数字购买。还要考虑:

  • Lucene segment 和合并临时空间;
  • translog;
  • 索引元数据;
  • doc values、倒排索引和 stored fields 的实际膨胀;
  • 快照或导出空间;
  • 节点故障后的重分配空间;
  • 磁盘水位保留空间;
  • 未来增长;
  • 热数据、温数据和冷数据的不同存储策略。

更接近实际的估算可以写成:

所需原始容量
≈ 数据源容量
  × 索引结构膨胀系数
  × (1 + 副本数)
  + 恢复和合并余量
  + 未来增长容量

其中“索引结构膨胀系数”没有通用固定值,必须用目标映射、字段类型、压缩方式和真实数据压测得到。

9.2 分片大小没有万能阈值

分片过小会产生:

  • 更多分片元数据;
  • 更多搜索 fan-out;
  • 更多 segment;
  • 更多恢复任务;
  • 更多文件句柄和调度开销。

分片过大则会产生:

  • 恢复耗时长;
  • 节点故障后数据迁移慢;
  • 单个分片成为查询或写入热点;
  • shrink、迁移和快照恢复粒度过粗。

因此不能脱离版本、硬件、查询、写入和恢复目标,给出一个适用于所有集群的固定“最佳分片大小”。更可靠的方法是:

  1. 用真实或接近真实的数据创建目标索引;
  2. 观察单分片的写入吞吐和查询延迟;
  3. 模拟节点故障;
  4. 测量分片恢复速度;
  5. 检查在正常业务负载下磁盘和堆内存余量;
  6. 再确定模板和 rollover 策略。

9.3 分片数量和 JVM 堆

分片会占用节点资源,尤其是:

  • 分片元数据;
  • Lucene segment 相关结构;
  • 文件句柄;
  • 查询缓存和请求上下文;
  • translog 和恢复状态。

“每 GB 堆支持固定数量分片”的经验法则只能作为粗略预警,不能代替压测和监控。实际问题往往不是分片数量本身,而是大量小分片叠加了高并发查询、聚合、segment 数量或不合理的索引生命周期。


十、分片分配、迁移与运维风险

10.1 临时关闭分配

在节点维护时,可以暂时关闭分片分配,避免节点短暂重启触发大量迁移:

curl -X PUT 'http://localhost:9200/_cluster/settings' \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "cluster.routing.allocation.enable": "primaries"
    }
  }'

这个配置允许主分片分配,但禁止普通副本分配。维护完成后必须恢复:

curl -X PUT 'http://localhost:9200/_cluster/settings' \
  -H 'Content-Type: application/json' \
  -d '{
    "persistent": {
      "cluster.routing.allocation.enable": null
    }
  }'

如果忘记恢复,集群可能长期处于 yellow,新副本也不会建立。

配置分配过滤、节点属性和 awareness 时,应优先使用集群设置或索引设置,并在变更后通过 _cluster/allocation/explain 验证实际决策,而不是只检查配置是否被接受。

10.2 手工强制分配的危险

当主分片永久丢失且没有副本时,可能看到需要手工分配的建议。某些强制分配操作需要显式接受 accept_data_loss

这类操作的含义不是“修复 Elasticsearch”,而是:

告诉 Elasticsearch:即使无法证明已有完整数据,也允许把一个本地分片目录作为主分片使用。

可能后果包括:

  • 丢失尚未存在于该目录的数据;
  • 产生数据缺口;
  • 后续副本复制错误数据;
  • 让应用误以为数据完整。

执行前应优先尝试:

  1. 确认节点只是暂时离线;
  2. 查找可用副本;
  3. 查看最近快照;
  4. 检查磁盘和数据目录;
  5. 评估是否可以接受数据丢失;
  6. 恢复后通过业务校验检查数据完整性。

10.3 滚动重启的边界

滚动重启的目标是始终保留足够的 master 节点和数据副本,使集群继续工作。一般流程是:

1. 确认集群有健康余量
2. 暂停或限制不必要的分片迁移
3. 一次只维护一个节点
4. 等待节点重新加入集群
5. 等待必要的分片恢复完成
6. 检查 master、数据和副本状态
7. 再处理下一个节点

如果副本数为 0,维护一个承载主分片的节点可能直接导致 red。如果有副本但所有副本都集中在同一故障域,机房级故障仍可能造成数据不可用。


十一、恢复速度与可用性的取舍

恢复涉及两个不同目标:

  • 尽快恢复服务:优先提升已有副本,减少等待。
  • 尽快恢复冗余:尽快重建缺失副本,避免再次故障时没有保护。

集群会受到恢复并发、网络和磁盘吞吐限制。可以查看恢复状态:

curl 'http://localhost:9200/_recovery?active_only=true&pretty'

也可以查看节点和分片级统计:

curl 'http://localhost:9200/_cat/recovery/orders?v'

输出中的常见信息包括:

  • stage:恢复阶段;
  • files:文件总数、已恢复数量;
  • bytes:字节总量、已恢复数量;
  • time:恢复耗时;
  • source_hosttarget_host:恢复来源与目标。

恢复期间,如果同时把并发调得过高,可能造成:

  • 正常查询延迟上升;
  • 写入延迟增加;
  • 磁盘队列过长;
  • 网络拥塞;
  • 节点因资源压力再次离线。

因此恢复设置不是越大越好。应根据磁盘类型、网络带宽、业务低峰期和故障演练结果调整,并观察业务指标而不是只追求恢复百分比增长。


十二、常见故障现象与诊断路径

12.1 集群为 yellow

可能原因:

  • 副本数大于可用数据节点数;
  • 主副本被要求分布到不同 zone,但 zone 不足;
  • 某节点离线;
  • 磁盘水位阻止副本分配;
  • 索引 allocation filter 不满足。

诊断顺序:

curl 'http://localhost:9200/_cluster/health?level=indices&pretty'
curl 'http://localhost:9200/_cat/shards?v'
curl -X GET 'http://localhost:9200/_cluster/allocation/explain' \
  -H 'Content-Type: application/json' \
  -d '{
    "index": "orders",
    "shard": 0,
    "primary": false
  }'

不要简单地把副本数改成 0 来“修复” yellow。这样只是删除保护层,只有在明确接受风险或临时降低资源压力时才应使用。

12.2 集群为 red

重点确认是哪些主分片未分配:

curl 'http://localhost:9200/_cat/shards?v&h=index,shard,prirep,state,unassigned.reason,node'

然后对主分片解释:

curl -X GET 'http://localhost:9200/_cluster/allocation/explain' \
  -H 'Content-Type: application/json' \
  -d '{
    "index": "orders",
    "shard": 0,
    "primary": true
  }'

不同原因的处理完全不同:

  • 节点暂时离线:恢复节点或等待故障转移;
  • 磁盘满:释放空间并确认数据目录完整;
  • 分配规则冲突:修正规则;
  • 主副本永久丢失:从其他副本或快照恢复;
  • 集群状态未更新:先诊断 master 选举和节点通信。

在没有确认数据来源之前,不要直接执行带数据丢失风险的强制分配。

12.3 写入失败但集群仍是 green

green 只表示分片分配完整,不代表所有写入都成功。还应检查:

  • 索引是否被 read_only_allow_delete 阻塞;
  • 请求是否使用了错误的 routing;
  • 映射冲突;
  • Bulk 中是否只有部分条目失败;
  • wait_for_active_shards 是否无法满足;
  • 磁盘是否刚刚越过 flood-stage;
  • 应用是否遇到版本冲突。

检查索引设置:

curl 'http://localhost:9200/orders/_settings?flat_settings=true&pretty'

检查 Bulk 响应时,不能只看 HTTP 状态码。必须读取每个 items 的结果:

{
  "errors": true,
  "items": [
    {
      "index": {
        "_id": "order-001",
        "status": 201
      }
    },
    {
      "index": {
        "_id": "order-002",
        "status": 400,
        "error": {
          "type": "mapper_parsing_exception"
        }
      }
    }
  ]
}

这里第一条已经成功,第二条失败;重试时必须避免无条件重复写入。

12.4 查询延迟突然升高

应区分是协调节点、数据节点、特定分片还是查询本身的问题:

curl 'http://localhost:9200/_nodes/stats/jvm,indices,thread_pool,fs?pretty'
curl 'http://localhost:9200/_cat/thread_pool?v'
curl 'http://localhost:9200/_cat/shards?v'

重点观察:

  • search 线程池是否排队;
  • 某个节点 CPU、堆或磁盘是否异常;
  • 是否正在进行大量 recovery;
  • 是否只有某个分片数据量异常;
  • 查询是否无意中 fan-out 到大量索引和分片;
  • 聚合是否产生大量桶;
  • 深分页是否导致协调节点内存压力。

副本可以帮助分担部分查询,但如果根因是查询访问了数千个小分片、聚合结果过大或单个 routing key 形成热点,盲目增加副本通常只能掩盖问题。


十三、容量、故障域和数据生命周期的组合设计

一个可持续的集群设计通常把以下因素一起考虑:

13.1 索引生命周期

时间序列数据不应无限写入一个索引。可以通过 rollover 和 ILM 让索引按大小、时间或文档量切换:

logs-000001 -> logs-000002 -> logs-000003

这样可以控制单个分片大小,并将旧数据迁移到 warm 或 cold 节点。

但 rollover 也可能制造大量小索引。若每天只有很少数据,却固定创建很多主分片,长期会积累大量空闲分片。因此 rollover 条件和主分片数必须结合真实写入量。

13.2 多可用区分布

如果集群跨多个可用区,应让节点带有 zone 属性,并通过 allocation awareness 使同一逻辑分片的主副本尽量分散。

目标不是让每个副本“看起来在不同节点”,而是让它们位于不同的故障域:

zone-a: primary
zone-b: replica

否则单个机架、交换机或可用区故障可能同时带走主副本。

13.3 副本和写入吞吐

增加副本会增加:

  • 磁盘占用;
  • 写入网络流量;
  • 副本写入成本;
  • 恢复和迁移工作量。

它可能提高查询并行度,但不能保证写入吞吐提升。对写入密集型索引,副本数需要和持久化、跨区网络以及可接受的数据安全目标一起评估。


十四、一个完整的最小实验

下面通过三个节点模拟分片、副本和故障观察。假设节点已使用相同的 cluster.name 加入同一个集群,并且节点具备数据角色。

创建索引:

curl -X PUT 'http://localhost:9200/demo' \
  -H 'Content-Type: application/json' \
  -d '{
    "settings": {
      "number_of_shards": 2,
      "number_of_replicas": 1
    }
  }'

写入几条文档:

curl -X PUT 'http://localhost:9200/demo/_doc/1' \
  -H 'Content-Type: application/json' \
  -d '{"user":"alice","amount":10}'

curl -X PUT 'http://localhost:9200/demo/_doc/2' \
  -H 'Content-Type: application/json' \
  -d '{"user":"bob","amount":20}'

查看分片:

curl 'http://localhost:9200/_cat/shards/demo?v'

预期能看到两个主分片和两个副本:

demo 0 p STARTED ...
demo 0 r STARTED ...
demo 1 p STARTED ...
demo 1 r STARTED ...

查看集群健康:

curl 'http://localhost:9200/_cluster/health/demo?pretty'

正常情况下应为:

{
  "status": "green",
  "active_primary_shards": 2,
  "active_shards": 4,
  "unassigned_shards": 0
}

停止一个承载分片的节点后,再执行:

curl 'http://localhost:9200/_cluster/health/demo?pretty'
curl 'http://localhost:9200/_cat/shards/demo?v'

如果对应副本位于其他节点,预期过程是:

原 primary 节点离线
  ->
副本提升为 primary
  ->
短时间内可能出现 INITIALIZING 或重新分配
  ->
相关主分片恢复可用
  ->
集群尝试补建新的 replica

此时可能短暂出现 yellow,因为副本尚未补齐;如果提升副本成功且所有分片最终都有副本,状态会回到 green

这个实验不能证明生产集群一定能在任意故障下无损恢复,因为真实结果还取决于:

  • 节点故障时副本是否已同步;
  • 故障域是否独立;
  • 是否存在未分配的其他分片;
  • 磁盘和网络是否有恢复余量;
  • 业务是否允许短暂不可用;
  • 是否有快照和恢复演练。

十五、需要牢牢记住的边界

  1. 分片数决定初始数据分布,副本数决定物理复制数量。
  2. 副本能提高可用性,但不是备份。
  3. 自定义 routing 能减少查询 fan-out,也可能制造数据和请求热点。
  4. green 只说明分片分配完整,不说明查询、写入、磁盘和业务数据都正常。
  5. yellow 通常表示副本未分配,不能简单视为“没有问题”。
  6. red 说明至少一个主分片不可用,应优先定位具体索引和分片。
  7. 主分片数量通常不能直接在线修改,调整容量需要 reindex、split、shrink 或生命周期设计。
  8. 恢复是磁盘、网络、CPU 和业务请求之间的竞争过程。
  9. 快照负责独立恢复能力,副本负责在线故障转移;两者不能互相替代。
  10. 分片、节点和集群的诊断必须结合实际 allocation decision、磁盘水位、节点角色、线程池和恢复状态,而不是只看一个健康颜色。

给主人留下些什么吧


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。