第三章:索引分片设计——主分片与副本分片原理、分片大小与数量估算公式、Routing路由机制

分片设计,说白了就是ES集群的“地基工程”。

我见过太多团队,上来就建索引,分片数随便填个5或者10。结果数据量一上来,集群直接崩了。嗯,今天咱们就把这块彻底讲透。

3.1 主分片与副本分片:ES的“主从复制”机制

先搞清楚一个概念:分片(Shard)是ES数据存储的最小单元。每个分片本质上就是一个Lucene索引。

主分片(Primary Shard):数据写入的第一站。你往ES里写一条文档,它先落到主分片上。

副本分片(Replica Shard):主分片的“影子”。主分片写完了,数据同步到副本分片。副本分片可以分担读请求。

核心原则:

  • 主分片数量在索引创建时固定,后续不可修改
  • 副本分片数量可以动态调整
  • 主分片和它的副本不会落在同一台节点上

为什么会这样设计?

你想想看,如果主分片和副本在同一台机器上,那台机器挂了,数据不就全丢了吗?ES的容错机制就是靠“分散”来实现的。

我记得有一次线上事故,某团队把副本数设为0,说是“为了省磁盘”。结果一台节点宕机,索引直接变红,数据恢复花了整整两天。嗯,这就是典型的“省小钱亏大钱”。

3.2 分片大小与数量:到底设多少才合适?

这是面试高频题,也是实战中最容易踩坑的地方。

3.2.1 分片大小的黄金法则

我个人习惯遵循一个经验值:每个分片20GB-50GB

为什么是这个范围?

  • 分片太小(<10GB):分片数量过多,集群管理开销大,查询时需要合并太多结果
  • 分片太大(>50GB):Lucene的段合并压力大,故障恢复慢

我的实战经验:

日志类场景,我一般控制在30GB左右。搜索类场景,可以放宽到40-50GB。如果数据有TTL(定期删除),建议偏小,20GB左右,方便滚动索引。

3.2.2 分片数量估算公式

直接上公式,别整那些虚的:

主分片数 = 预期数据总量 / 目标分片大小

举例:
预期数据总量 = 500GB
目标分片大小 = 30GB
主分片数 = 500 / 30 ≈ 17

建议向上取整,留点余量 → 20个主分片

副本分片数怎么定?

  • 高可用要求高:副本数=2(即总共3份数据)
  • 一般场景:副本数=1(总共2份数据)
  • 读多写少场景:副本数可以到3-5,提升查询吞吐

注意:

总分片数 = 主分片数 × (1 + 副本数)

这个总数不要超过集群节点数的20倍。比如你有10个节点,总分片数最好控制在200以内。否则集群的元数据管理会成为瓶颈。

我曾经接手过一个集群,单索引分了100个主分片,副本数2,总分片300个。但集群只有5个节点。结果每次集群重启,元数据恢复要半小时。嗯,这就是典型的“分片过多综合征”。

3.3 Routing路由机制:数据到底落在哪个分片上?

ES默认的路由规则很简单:

shard_num = hash(_routing) % num_primary_shards

默认情况下,_routing就是文档的_id。所以同一个_id的文档,永远落在同一个分片上。

但问题来了——如果你做批量查询,比如查询某个用户的所有订单,默认路由会导致请求广播到所有分片,然后再聚合。效率很低。

3.3.1 自定义路由:把相关数据“粘”在一起

我们可以指定一个字段作为路由键:

PUT my_index/_doc/1?routing=user_123
{
  "user_id": "user_123",
  "order_id": "order_001",
  "amount": 99.9
}

PUT my_index/_doc/2?routing=user_123
{
  "user_id": "user_123",
  "order_id": "order_002",
  "amount": 199.9
}

这样,user_123的所有订单都落在同一个分片上。查询时指定路由:

GET my_index/_search?routing=user_123
{
  "query": {
    "term": { "user_id": "user_123" }
  }
}

查询只访问一个分片,速度能快10倍以上。

避坑指南:

我曾经犯过一个错——用了自定义路由,但忘了在查询时也带上routing参数。结果查询走了广播,数据没查到(因为路由不一致)。

解决方案:建索引时设置 routing_partition_size,或者干脆在mapping里强制要求所有请求必须带routing。

3.3.2 路由的副作用:数据倾斜

自定义路由有个坑——如果某个路由键的数据量特别大(比如大V用户有100万条订单),那个分片就会变成“热分片”。

怎么解决?

  • 使用复合路由键:比如 user_id + date,把数据打散到多个分片
  • 或者用routing_partition_size:让一个路由键的数据分布到多个分片上
PUT my_index
{
  "settings": {
    "index.number_of_routing_shards": 6,
    "index.routing_partition_size": 2
  }
}

这样,同一个user_id的数据会落在2个分片上,既保留了路由的局部性,又避免了单分片过热。

3.4 实战总结:分片设计检查清单

决策项 建议值 说明
单分片大小 20-50GB 日志类偏小,搜索类偏大
主分片数 数据总量 / 目标分片大小 向上取整,留余量
副本数 1-2(生产环境至少1) 读多写多可适当增加
总分片数 ≤ 节点数 × 20 避免元数据膨胀
是否使用自定义路由 按业务场景决定 注意数据倾斜和查询一致性

最后说一句:分片设计没有“银弹”。我每次建新索引前,都会先估算半年到一年的数据量,再反推分片数。宁可多留点余量,也别让分片数太少导致后期无法扩展。

嗯,这一章就到这里。下一章咱们聊聊索引生命周期管理——数据怎么自动滚、自动删、自动冷热分离。