2. 索引分片与副本策略设计

分片与副本是 Elasticsearch 索引的物理骨架。分片决定数据如何被切分、并行处理能力有多强;副本决定容错能力和读吞吐上限。两者一旦在索引创建时确定,主分片数量便无法再改(只能重建索引),因此这一节的目标是:在写入前就把分片数、副本数、路由方式和分配约束想清楚,避免后期被迫 reindex。

一、分片数量估算:从数据量和堆内存反推

分片不是越多越好。每个分片都是一个独立的 Lucene 实例,占用文件句柄、堆内存和集群状态。经验上,单个分片大小控制在 10GB~50GB 之间较为健康,搜索类场景偏小、日志类场景偏大。估算公式可以写成:

主分片数 ≈ 索引总数据量 ÷ 单分片目标大小
再向上取整到节点数的整数倍,便于均匀分布

例如一个日志索引,按每天 200GB、保留 7 天计算,总量约 1.4TB。若单分片目标 30GB,则 1400 ÷ 30 ≈ 47,取 48 个主分片。若集群有 8 个数据节点,48 正好是 8 的 6 倍,每个节点承载 6 个主分片,分布均匀。反过来,如果只有 3 个节点却设了 100 个分片,每个节点要扛 33 个分片,堆内存和段合并压力会显著上升。

另一个约束来自堆内存。粗略经验是每 GB 堆内存对应 20~30 个分片。一个 30GB 堆的节点,分片总数(含副本)建议不超过 600~900。这个上限比数据量估算更硬,因为它直接关系到集群能否稳定运行。

二、副本配置权衡:可用性、成本与写入放大

副本数(number_of_replicas)的取值需要在三个维度上权衡:

  • 可用性:副本为 0 时,任一节点故障都会导致分片不可用;副本为 1 可容忍单节点故障;副本为 2 可容忍双节点同时故障。
  • 读吞吐:搜索请求会轮询主分片和副本分片,副本越多,可并行服务的分片副本越多,读 QPS 上限越高。
  • 写入成本:每次写入都要同步到所有副本,副本数增加会放大写入网络和磁盘开销,同时占用更多存储。

生产环境的常见选择是副本数为 1:既保证单节点故障不丢数据,又不至于让写入放大过于严重。对读多写少、SLA 要求高的索引,可以设为 2;对可重建的日志类索引,副本 0 或 1 都可接受。副本数可以随时通过 API 调整,这一点比主分片灵活得多:

PUT /logs-2024.06/_settings
{
  "index": { "number_of_replicas": 1 }
}

三、路由机制:文档如何落到某个分片

默认情况下,Elasticsearch 用文档 _id 做哈希路由:

shard = hash(_id) % number_of_primary_shards

这意味着相同 _id 的文档总是落到同一分片,保证写入和查询的一致性。但默认路由有两个问题:一是无法控制数据分布,二是无法把相关数据聚到一起。可以通过 routing 参数自定义路由值,例如让同一用户的订单都落到同一分片:

PUT /orders/_doc/1001?routing=user_42
{ "user": "user_42", "amount": 199 }

查询时也要带上相同的 routing,否则会广播到所有分片。自定义路由的代价是可能造成分片倾斜——如果某个 routing 值对应的文档特别多,那个分片就会成为热点。因此 routing 键要选择基数高、分布均匀的字段。

四、分片分配过滤:把分片放到该去的地方

分配过滤通过 index.routing.allocation 系列参数控制分片落在哪些节点上,常用于冷热分层、机架感知和硬件隔离。典型配置:

PUT /logs-2024.06/_settings
{
  "index.routing.allocation.require.box_type": "hot",
  "index.routing.allocation.exclude._name": "node-old-01"
}

require 是硬约束,不满足则分片无法分配;include 是软偏好;exclude 用于排除节点。冷热架构中,热节点用 SSD 承载写入,冷节点用大容量 HDD 承载历史数据,通过 require.box_type 实现自动迁移。需要注意的是,硬约束过严可能导致分片无法分配(unassigned),此时集群健康会变黄甚至变红,应配合 _cluster/allocation/explain 排查。

五、热点分片与脑裂的规避

热点分片通常来自三种原因:分片数过少导致单分片过大、routing 键分布不均、以及写入集中在少数分片。应对手段包括:增加主分片数、改用更均匀的 routing 键、使用 index.routing_partition_size 让路由在多个分片间再散列,以及通过 ILM 滚动索引把新数据写入新索引。

脑裂是集群中出现多个 master 各自认为自己是主节点,导致元数据不一致。根本原因是 master 选举时无法确认自己是否仍拥有多数派。规避方法是设置 discovery.zen.minimum_master_nodes(7.x 之前)或使用 7.x 之后的自动 quorum 机制,并保证 master 候选节点数为奇数。生产环境建议至少 3 个专用 master 节点,避免数据节点兼任 master 带来的选举抖动。

分片与副本策略设计全景 分片数量估算 总数据量 1.4TB 单分片目标 30GB 1400 ÷ 30 ≈ 47 → 48 节点数 8,48 = 8 × 6 堆内存约束:≤ 30 分片/GB 主分片 = 48,副本 = 1 路由机制 默认:hash(_id) % 主分片数 自定义:?routing=user_42 查询需带相同 routing routing 键基数要高 否则分片倾斜 → 热点 routing_partition_size 再散列 副本配置权衡 副本 0:无容错,读吞吐低 副本 1:容忍单节点故障 副本 2:容忍双节点故障 写入放大 = 副本数 + 1 副本数可动态调整 生产推荐:副本 = 1 分片分配过滤 require.box_type = hot(硬约束) include / exclude 软偏好与排除 冷热分层:SSD 热节点 + HDD 冷节点 硬约束过严 → unassigned 分片 排查:_cluster/allocation/explain ILM 滚动索引 + 自动迁移 热点分片与脑裂规避 热点成因:分片少 / routing 偏 / 写入集中 对策:增分片、均匀 routing、滚动索引 脑裂:多 master 各自为政 规避:奇数 master 候选 + quorum 选举 生产:≥ 3 个专用 master 节点 集群健康:green / yellow / red

把上述四件事串起来看:先用数据量和堆内存定下主分片数,再用可用性与成本定下副本数,然后用 routing 控制数据落点,最后用分配过滤把分片约束到合适的节点上。热点和脑裂不是独立问题,而是这四个决策没做对时的症状。索引创建前多花十分钟估算,往往能省下后期数小时的 reindex 和故障排查。