5月27日 23:48

Elasticsearch 的路由机制是如何工作的?

路由机制的核心原理

Elasticsearch 是分布式搜索引擎,每个索引由多个分片(shard)组成,每个分片是一个独立的 Lucene 索引。当写入或查询一条文档时,系统必须确定这条文档属于哪个分片——这就是路由机制要解决的问题。

路由算法的公式:

shell
shard_num = hash(routing_value) % number_of_primary_shards

默认情况下,routing_value 就是文档的 _id。Elasticsearch 使用的哈希函数是 Murmur3Hash(不是 SHA-256),它计算速度快且分布均匀。这意味着相同的 _id 永远路由到同一个分片,保证读写的确定性。

为什么分片数创建后不能改? 因为一旦 number_of_primary_shards 变化,已有文档的路由结果会改变,导致数据"丢失"(实际还在,但按新公式找不到)。所以分片数只能在创建索引时指定。

写请求的路由流程

  1. 客户端向任意节点发送写入请求,该节点成为协调节点(coordinating node)
  2. 协调节点根据 hash(_id) % primary_shards 计算目标分片
  3. 请求被转发到目标主分片所在节点,写入 Memory Buffer,最终持久化
  4. 主分片写入成功后,并行复制到所有副本分片
  5. 协调节点收集所有副本的响应后,返回客户端成功

读请求的路由流程

读请求的路由比写请求多一步选择:

  1. 协调节点同样根据哈希公式定位目标分片
  2. 在主分片及其所有副本中,使用 round-robin 轮询算法随机选一个执行查询——这就是读请求的负载均衡
  3. 选中的分片返回结果给协调节点,协调节点合并后返回客户端

这个机制意味着:副本越多,读吞吐量越高,因为读请求可以分散到多个副本上并行处理。

自定义路由(custom routing)

默认按 _id 路由在大多数场景下没问题,但某些业务需要更精细的控制。比如一个订单系统,希望同一用户的订单落在同一分片上,这样按用户查询时只需命中一个分片,避免 scatter-gather。

指定 routing 参数

bash
# 写入时指定 routing PUT /orders/_doc/1?routing=user_123 { "user_id": "user_123", "amount": 99.9 } # 查询时必须带上相同的 routing GET /orders/_search?routing=user_123 { "query": { "term": { "user_id": "user_123" } } }
java
IndexRequest request = new IndexRequest("orders"); request.id("1"); request.routing("user_123"); request.source("user_id", "user_123", "amount", 99.9); client.index(request, RequestOptions.DEFAULT);

自定义路由的三个坑

坑一:查询忘带 routing,触发全分片扫描。 写入时用了 routing,查询时没带,Elasticsearch 会在所有分片上执行搜索,性能急剧下降。

坑二:routing 值不均匀导致数据倾斜。 如果用 city 做 routing,北上广深的数据量远超其他城市,会造成某些分片过大。解决方案是对 routing 值再加一层哈希,或在 routing 后面拼序号(如 user_123_0user_123_1),人为分散到多个分片。

坑三:更新文档时 routing 必须一致。 如果更新时用了不同的 routing 值,旧文档不会被覆盖,而是作为新文档写入另一个分片,造成数据冗余。

required_routing 强制约束

从 Elasticsearch 7.x 开始,可以在 mapping 中配置 routingrequired

json
PUT /orders { "mappings": { "_routing": { "required": true } } }

设为 required 后,不带 routing 的写入和查询请求会被直接拒绝,从机制上避免坑一。

分片分配感知(Allocation Awareness)

除了文档级别的路由,Elasticsearch 还支持节点级别的分片分配策略,确保主分片和副本分布在不同物理机上:

yaml
# elasticsearch.yml node.attr.rack_id: rack_one cluster.routing.allocation.awareness.attributes: rack_id

配置后,Elasticsearch 尽量将同一分片的主副本分布在不同 rack 上。如果某个 rack 宕机,数据仍然可用。

还可以配置 forced awareness,防止集群在只有一个 rack 时将主副本分配到同一 rack:

yaml
cluster.routing.allocation.awareness.attributes: rack_id cluster.routing.allocation.awareness.force.rack_id.values: rack_one,rack_two

面试追问

Q:路由公式为什么用取模而不是一致性哈希? 取模保证分片数不变时结果确定,实现简单且均匀。一致性哈希在节点增减时只需迁移少量数据,但 Elasticsearch 的分片数固定不变(创建后不可改),取模已经够用。这也是分片数不能改的根本原因。

Q:如何监控路由是否均匀? GET _cat/shards?v 查看各分片的 docs 数和 store 大小。如果某分片明显偏大,说明 routing 值分布不均,需要调整 routing 策略或增加分片数。

Q:_id 和 routing 的关系是什么? _id 是文档唯一标识,routing 是路由计算依据。默认 routing = _id,但自定义 routing 后两者独立。_id 保证文档唯一性,routing 决定文档存在哪个分片。

标签:ElasticSearch