Elasticsearch 的路由机制是如何工作的?
路由机制的核心原理
Elasticsearch 是分布式搜索引擎,每个索引由多个分片(shard)组成,每个分片是一个独立的 Lucene 索引。当写入或查询一条文档时,系统必须确定这条文档属于哪个分片——这就是路由机制要解决的问题。
路由算法的公式:
shellshard_num = hash(routing_value) % number_of_primary_shards
默认情况下,routing_value 就是文档的 _id。Elasticsearch 使用的哈希函数是 Murmur3Hash(不是 SHA-256),它计算速度快且分布均匀。这意味着相同的 _id 永远路由到同一个分片,保证读写的确定性。
为什么分片数创建后不能改? 因为一旦 number_of_primary_shards 变化,已有文档的路由结果会改变,导致数据"丢失"(实际还在,但按新公式找不到)。所以分片数只能在创建索引时指定。
写请求的路由流程
- 客户端向任意节点发送写入请求,该节点成为协调节点(coordinating node)
- 协调节点根据
hash(_id) % primary_shards计算目标分片 - 请求被转发到目标主分片所在节点,写入 Memory Buffer,最终持久化
- 主分片写入成功后,并行复制到所有副本分片
- 协调节点收集所有副本的响应后,返回客户端成功
读请求的路由流程
读请求的路由比写请求多一步选择:
- 协调节点同样根据哈希公式定位目标分片
- 在主分片及其所有副本中,使用 round-robin 轮询算法随机选一个执行查询——这就是读请求的负载均衡
- 选中的分片返回结果给协调节点,协调节点合并后返回客户端
这个机制意味着:副本越多,读吞吐量越高,因为读请求可以分散到多个副本上并行处理。
自定义路由(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" } } }
javaIndexRequest 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_0、user_123_1),人为分散到多个分片。
坑三:更新文档时 routing 必须一致。 如果更新时用了不同的 routing 值,旧文档不会被覆盖,而是作为新文档写入另一个分片,造成数据冗余。
required_routing 强制约束
从 Elasticsearch 7.x 开始,可以在 mapping 中配置 routing 为 required:
jsonPUT /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:
yamlcluster.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 决定文档存在哪个分片。