Multi-shard:日志引擎怎样把写入真正分散到各个 shard

围绕 sharded AsyncWriter、路由选择和按 shard 分发的批量写入路径,分析 seastar-log-engine 的 multi-shard 语义到底意味着什么。

multi-shard 不是"多开几个 writer"这么简单

很多系统一提到 multi-shard,很容易先想到并行度提升。但在 seastar-log-engine 里,multi-shard 的意义不只是把写入任务分散出去,而是把"路由、提交、刷盘、恢复"都放回各自 shard 的局部上下文里。

项目当前的基础形态是:

seastar::sharded<AsyncWriter> _writers;

这行代码背后真正表达的是:每个 shard 都有独立 writer、独立 pending 队列、独立 active log 生命周期。也正因为如此,multi-shard 不只是吞吐模型,同时也是整个系统状态组织方式。

启动和停止都围绕每个 shard 的本地状态展开

系统启动时,需要先把 _writers 启起来,再在每个 shard 上完成 writer 自身初始化;停止时,则要反过来让所有 shard 上的 writer 各自完成收尾,再统一停掉容器本身。

Rendering diagram...

这种流程看起来比"只有一个全局 writer"更繁琐,但它换来的是非常清楚的边界:

  • 哪个 shard 负责哪部分 active log
  • 哪个 shard 维护自己的 flush / rotate 节奏
  • 哪个 shard 在恢复时只处理自己的日志状态

如果这些状态一开始就混在一起,后面无论是性能分析还是故障恢复,都会变得更模糊。

单条写入的核心,是先选 shard 再本地提交

对于 append(LogMessage) 这类单条写入,请求不会盲目广播到所有 shard,而是先经过 ShardRouter 选出目标位置,然后只在目标 shard 上执行提交。

这一步的重要性在于,它让 route key 真正变成写入模型的一部分,而不是一个附带字段。只要 key 稳定,消息就能稳定落到同一个 shard,本地 writer 也才能持续利用自己的批量和缓存优势。

从这一点出发再看 multi-shard,会更容易理解它的价值:系统不是把所有日志简单"摊开",而是在 shard 层面维持可重复、可推断的数据归属关系。

批量写入真正复杂的地方,是"先路由再分组"

append_batch() 比单条写入更有代表性,因为它会遇到一个现实问题:一批日志并不一定都属于同一个 shard。

当前实现会根据路由结果,把消息按 shard 分组,再把每一组交给对应 writer。这里的重点不是 fanout 本身,而是分发时尽量保留局部性,避免无意义的跨 shard 抖动。

在一些特定场景下,这条路径还会更快:

  • consistent hashing 下,整批消息的 route key 全部相同
  • empty_route_policy=local 且整批都是空 key
  • empty_route_policy=round_robin 且整批都是空 key

这些分支的共同目标,是尽量在已经明显同向的输入上少做一次额外拆分。它们不是新的语义,只是围绕当前路由模型做的顺势优化。

路由策略决定了 multi-shard 的稳定性

multi-shard 是否真正可控,最终还是取决于路由实现。

当前系统支持的核心要素包括:

  • hash_modulo
  • consistent_hashing
  • empty_route_policy
  • routing_virtual_nodes

而且它并不是随手用 std::hash<std::string> 做分发。当前实现使用稳定哈希;在 consistent hashing 路径里,ring 也是排序后的 vector<pair<token, shard>>,而不是 std::map

这类实现细节会直接影响 multi-shard 行为能否稳定复现。对日志系统来说,这一点非常关键,因为只要路由分布漂移,同一个 route key 的数据就可能在不同 shard 之间来回跳动,后面的写入局部性和恢复推断都会一起变差。

空 route key 的处理同样属于架构语义

很多时候大家会更关注"有 key 怎么路由",但 empty route key 的处理其实也会显著影响 multi-shard 行为。

当前系统通过 empty_route_policy 明确处理这类输入,常见取值包括:

  • local
  • round_robin

这意味着"没有 route key"并不是未定义行为,而是显式进入另一套分配规则。这样做的收益,是批量写入、状态接口和性能分析都可以围绕同一套语义展开,而不是把空 key 留成一块灰色地带。

恢复阶段为什么还要强调 multi-shard consistency

如果系统只关心写进去,multi-shard 讨论到这里就差不多了。但日志引擎还要面对恢复。

项目里专门强调 multi-shard recovery consistency,原因很现实:每个 shard 都有自己的 active log,也有自己的恢复边界。异常退出之后,真正需要确认的是"每个 shard 是否都恢复到了各自可信的连续边界上",而不是只看整个进程能否启动成功。

这一步一旦不稳,问题往往不会马上表现成崩溃,而是数据归属慢慢漂移、查询行为开始不一致,最后才在更靠后的地方暴露出来。

所以 multi-shard 在这里不是单纯的并行写入技巧,它同时决定了恢复时系统如何重新建立秩序。

这篇文章真正想说明什么

seastar-log-engine 的 multi-shard 设计,本质上是在用 Seastar 最擅长的方式组织日志写入:让每个 shard 维护自己的 writer、本地状态和文件生命周期,再通过稳定路由把消息送到正确的位置。

从技术博客的视角看,最值得关注的并不是某个 invoke_on(...) 调用,而是这条设计主线:

  • 先路由,再写入
  • 能在本地完成的动作,不升级成跨 shard 协调
  • batch、flush、rotate、recovery 都围绕 shard 局部性展开

只有这样,multi-shard 才不只是"并发更多",而是真正成为系统结构的一部分。