路由策略与分片机制:当前 ShardRouter 的实现方式
基于当前仓库实现分析 Seastar Log Engine 的路由策略,包括 hash_modulo、consistent_hashing、空 route_key 策略以及 append_batch 的分发快路径。
路由问题的本质
项目里的每条日志消息最终都要落到某个 shard 的 AsyncWriter。
路由策略需要同时满足几件事:
- 相同
route_key尽量稳定落到同一个 shard - 空
route_key也要有明确策略 - 不同 shard 之间尽量均衡
- 给
append_batch()留出优化空间
当前项目里的真实实现类型名是 ShardRouter,不是额外抽象出来的 RoutingEngine。
当前支持的路由配置
路由策略
当前只支持两种:
hash_moduloconsistent_hashing
空 route_key 策略
当前支持两种:
localround_robin
这两组配置都在 EngineConfig 中:
RoutingStrategy routing_strategy = RoutingStrategy::hash_modulo;
EmptyRoutePolicy empty_route_policy = EmptyRoutePolicy::local;
std::size_t routing_virtual_nodes = 128;
真实的哈希实现
1. 非空 key 的稳定哈希
项目当前不是直接用 std::hash<std::string> 做路由基础,而是自己实现了一套稳定哈希。
原因很简单:
std::hash的行为不适合作为跨实现、跨环境的稳定分布基础- 项目希望路由结果由自己控制
当前实现使用的是 FNV 风格哈希:
std::uint64_t ShardRouter::stable_hash(std::string_view value) noexcept
因此这里更值得关注的是稳定哈希带来的可预期分布,以及它与 shard 路由语义的配合方式。
2. 一致性哈希 ring 的数据结构
当前 consistent hashing ring 也不是 std::map<size_t, size_t>,而是:
std::vector<std::pair<std::uint64_t, unsigned>> _ring;
构建方式是:
- 对每个 shard
- 为每个虚拟节点生成 token
- push 到
_ring - 最后排序
查询时通过 lower_bound() 找第一个 >= hash 的 token。
这个实现比 std::map 更接近“预构建排序数组 + 二分查找”的模式。
hash_modulo 的当前语义
非空 route_key
对非空 key:
stable_hash(route_key) % shard_count
空 route_key
对空 key,不直接取模,而是根据 EmptyRoutePolicy 走 fallback:
local落到当前 shardround_robin落到empty_route_index % shard_count
所以“空 key 时总是 this_shard_id()”也不是完整说法,要看空 key 策略。
consistent_hashing 的当前语义
ring 构建
构建流程:
virtual_nodes至少被修正为 1- 对每个 shard 和每个 vnode 生成 token
- token 由
hash_parts(shard_id, vnode_id)生成 - 所有 token 排序
因此 ring 大小满足:
ring_size = shard_count * virtual_nodes
这点在单测里也有直接验证。
路由
- 先计算
stable_hash(route_key) - 在
_ring中找第一个token >= hash - 若没找到,则回绕到
_ring.front()
返回结果不只是 shard,还包括:
shardhashtokenused_local_fallback
这也是为什么 /v1/route 和 gRPC RouteReply 能直接把这些字段返回出来。
空 route_key 的当前处理
这是当前实现里比很多简化描述更重要的一部分。
local
empty_route_policy=local 时:
- 空 key 直接落到当前 shard
used_local_fallback=true
优点:
- 避免跨 shard 通信
- 本地提交链路更短
代价:
- 如果上层大量使用空 key,可能形成局部倾斜
round_robin
empty_route_policy=round_robin 时:
- 空 key 不再固定写本地
- 使用递增计数轮转分配 shard
used_local_fallback=false
这对多 shard 空 key 场景更均衡,但会增加跨 shard 分发开销。
append_batch 的路由优化
如果只从 ShardRouter 看,会以为“每条消息都各算一次 shard”;但 LogEngine::append_batch() 其实在上层做了多种快路径优化。
1. consistent hashing 下的同 key 快路径
如果:
- 使用
consistent_hashing - 整批第一条消息的
route_key非空 - 整批所有消息的
route_key都和第一条相同
则整批直接发往同一个 shard。
2. local 策略下的全空 key 快路径
如果整批消息的 route_key 都为空,且策略是 local,则整批直接提交到当前 shard。
3. round_robin 策略下的全空 key 快路径
如果整批消息全是空 key,且策略是 round_robin,则先按 shard 均摊,再并发 fanout。
4. all_same_shard 快路径
即使 route key 不同,只要最终全部算出来都落到同一个 shard,也会直接一次 submit_many()。
5. 普通慢路径
只有在以上快路径都不命中时,才会:
- 预统计每个 shard 的消息数
- 构建
per_shardbuckets - 对每个 shard 发一次
submit_many() - 用
when_all_succeed()汇总
所以当前实现真正优化的重点,不只是“hash 算得快”,而是“尽量减少跨 shard fanout 次数”。
这套路由模型的实现边界
下面这些说法在当前项目里都应该避免:
- “RoutingEngine 使用
std::hash” - “一致性哈希 ring 用
std::map” - “空 key 永远回退本地 shard”
- “批量写入只会简单按 shard 分组”
这些都只描述了某种常见设计,不是当前仓库事实。
性能上应该怎么理解
当前仓库已有文档能支持的结论是:
hash_modulo路径更简单consistent_hashing能提供更稳定的映射和 ring 可观测性empty_route_policy=round_robin在空 key 密集场景下能提升吞吐,但会引入更多跨 shard 协调成本
但当前仓库并没有足够可靠的统一实测数据支持“某策略一定是 1.8M msg/s、另一策略一定是 1.6M msg/s”这种精确宣传。
因此文章更适合写“实现差异和工程权衡”,而不是虚构绝对性能表。
查询接口如何暴露路由信息
当前 query_server 和 query_client 都能直接查看路由结果:
HTTP
curl "http://127.0.0.1:18080/v1/route?key=route-a"
gRPC CLI
./build/log_engine_query_client --target 127.0.0.1:19090 --method route --route-key route-a
返回字段包括:
route_keyshardhashtokenused_local_fallback
这使得当前路由设计不仅是“写入内部逻辑”,也是一个可直接观测的运维能力。
当前默认值与使用建议
默认值
当前默认是:
routing_strategy = hash_modulo
empty_route_policy = local
routing_virtual_nodes = 128
什么时候用 hash_modulo
适合:
- shard 数稳定
- 更关注实现简单和路径短
- 不准备做 shard 数变动实验
什么时候用 consistent_hashing
适合:
- 想保留 ring token 级别可观测性
- 需要更稳定的 key 分布语义
- 希望未来做 shard 数变化实验时更自然
什么时候考虑 round_robin
适合:
- 上层经常不传
route_key - 空 key 流量占比很高
- 希望尽量让空 key 请求分散到多 shard
但代价也要明确:
- 会增加跨 shard 提交
- 并不总是比
local更好
总结
当前项目的路由设计可以概括为:
- 用
ShardRouter统一封装hash_modulo和consistent_hashing - 用稳定哈希而不是
std::hash - 用排序后的 vector ring 而不是 map ring
- 明确区分空 key 的
local与round_robin - 在
append_batch()上层做多种分发快路径
如果只记一句话:
当前路由优化的核心,不只是“选哪种 hash”,而是“让写入批次尽量少跨 shard、少 fanout、少不必要调度”。
下一篇:《数据一致性与恢复模型:Checkpoint 与 Crash Recovery 的实现》