整体架构与核心模块:Seastar Log Engine 当前实现解读

基于当前仓库代码梳理 Seastar Log Engine 的真实架构,包括 LogEngine、AsyncWriter、ShardRouter、AppendWriter 与查询链路的职责边界。

架构总览

当前版本的 Seastar Log Engine 已经收敛为一条统一写入链路:

submit -> route -> per-shard pending queue -> batch/flush -> DMA aligned append -> optional rotate/archive/checkpoint

从代码结构看,核心分层如下:

Application Layer
  ├─ compat_glog
  ├─ demo / bench / verify / read
  └─ query_server / query_client

LogEngine
  ├─ seastar::sharded<AsyncWriter>
  ├─ ShardRouter
  └─ EngineConfig

Per-shard Write Path
  ├─ AsyncWriter
  ├─ AppendWriter
  └─ LogManager

Storage / Query Path
  ├─ active log
  ├─ archived log (.log / .log.gz)
  ├─ checkpoint sidecar
  └─ LogReader + health/metrics

这里最重要的一点是:项目当前不是“多条写路径并存”的实验态,而是以 AsyncWriter 为中心的统一实现。

Rendering diagram...

1. LogEngine:统一入口

LogEngine 是对外暴露的门面层,负责:

  1. 保存和校验 EngineConfig
  2. 启动/停止 seastar::sharded<AsyncWriter>
  3. 基于 ShardRouter 决定消息写到哪个 shard
  4. 提供 appendappend_batch 两条入口

当前接口以仓库代码为准:

class LogEngine {
public:
    seastar::future<> start(EngineConfig config);
    seastar::future<> stop();
    seastar::future<> append(LogMessage message);
    seastar::future<> append_batch(std::vector<LogMessage> messages);

private:
    seastar::sharded<AsyncWriter> _writers;
    ShardRouter _router;
    EngineConfig _config;
    bool _started = false;
};

启动过程也比较直接:

  1. validate() 校验配置
  2. start() 启动所有 shard 上的 AsyncWriter
  3. routing_strategy + routing_virtual_nodes + smp::count 配置 ShardRouter

对应实现可见 src/log_engine.cc

2. AsyncWriter:Per-shard 写入核心

AsyncWriter 负责单个 shard 的写入、背压、flush、rotate、checkpoint 恢复。

Rendering diagram...

它内部保存的不是原始 LogMessage 队列,而是已经编码后的记录 buffer

class AsyncWriter {
private:
    EngineConfig _config;
    seastar::timer<seastar::lowres_clock> _flush_timer;
    seastar::gate _gate;
    seastar::condition_variable _backpressure;
    std::deque<seastar::temporary_buffer<char>> _pending;
    std::size_t _pending_bytes = 0;
    layout::SegmentDescriptor _active_segment;
    std::uint64_t _sequence = 0;
    std::uint64_t _rotation_index = 0;
    AppendWriter _append_writer;
    LogManager _log_manager;
    bool _started = false;
    bool _stopping = false;
    bool _flush_in_progress = false;
};

这意味着当前模型是:

  1. submit() / submit_many() 先把 LogMessage 编码成 temporary_buffer<char>
  2. 编码后的记录进入 _pending
  3. 达到 batch_size 或命中 flush 定时器时触发刷写
  4. AppendWriter 负责真正的 DMA 对齐写入

背压模型

当前背压是基于待写字节数的,而不是简单忙等:

  • max_pending_bytes = 0 表示关闭背压
  • 超过阈值后,提交方等待 condition_variable
  • 恢复阈值优先取 pending_bytes_low_watermark
  • 若未设置 low watermark,则回落到 max_pending_bytes / 2

Ack 语义

当前只有两档确认语义:

  • write_ack 写入 batch/tail 后返回,但不额外显式 flush
  • sync_ack 写入后再执行 flush,再向调用方确认

这一点和项目文档保持一致。

Rendering diagram...

3. ShardRouter:路由决策

项目里的真实类型名是 ShardRouter,不是额外抽象出来的 RoutingEngine

它支持两种策略:

  • hash_modulo
  • consistent_hashing

还支持空 route_key 的两种策略:

  • local
  • round_robin

路由实现要点

  1. 非空 route_key

    • 先做稳定哈希
    • hash_modulo 直接 % shard_count
    • consistent_hashing 在排序后的虚拟节点 ring 上找第一个 >= hash 的 token
  2. route_key

    • local:落到当前 shard
    • round_robin:按全局递增计数轮转

这套架构的实现边界

这里有两个容易被误写的点:

  1. 当前实现没有直接使用 std::hash<std::string> 作为路由基础,而是用了稳定的 FNV 风格哈希,避免不同环境下行为漂移。
  2. 一致性哈希 ring 在实现里是 std::vector<pair<token, shard>> 排序后做 lower_bound,不是 std::map

4. AppendWriter:统一 DMA 对齐写路径

AppendWriter 的职责不是“每条记录单独补 padding 再写”,而是维护一条tail-buffer + aligned flush 路径。

简化后可以理解为:

  1. 新 batch 先和历史 tail 拼起来看总字节数
  2. 能凑满对齐块的前缀立即写入
  3. 剩余尾巴先留在 _tail_chunks
  4. force_flush() 或关闭文件时再补零并刷出
  5. 最终用 truncate(_logical_size) 恢复逻辑长度

这个设计的好处是:

  • 避免频繁为小记录单独补 padding
  • 统一处理部分对齐尾巴
  • 保持“物理写入对齐、逻辑长度精确”

所以如果只看“DMA 对齐写入”这个标题,很容易误以为每条日志都会直接 dma_write;真实实现比这个更细。

5. LogManager:rotate / archive / checkpoint / recovery

LogManager 负责生命周期管理,但 rotate 判断是在 AsyncWriter::maybe_rotate() 里触发的:

  • rotate_size_bytes > 0 && logical_size >= rotate_size_bytes
  • rotate_interval_seconds > 0 && 打开时长超过阈值

触发后流程大致是:

  1. flush 当前 tail
  2. 关闭 active file
  3. 递增 rotation_index
  4. 通过 LogManager 旋转 active file
  5. 重置 AppendWriter
  6. 重新打开新的 active segment
  7. 如启用 checkpoint,则持久化 checkpoint

恢复语义

项目当前恢复策略已经比早期版本更保守:

  • 启动恢复时会结合 checkpoint 和 active log verified scan
  • 残缺 checkpoint 会被忽略
  • 过旧 checkpoint 也会被忽略
  • 回退时会保守采用 scan 结果,而不是盲目信任 sidecar

这部分是当前实现里的一个重点,不应简化成“直接从 checkpoint 恢复”。

6. append_batch 的真实优化点

append_batch() 不是简单“按 route_key 分组”这么一句话能概括完。

当前代码里有几类快路径:

  1. 一致性哈希下,如果整批消息 route_key 完全相同,直接整批发往同一个 shard
  2. empty_route_policy=local 且整批都是空 key,直接走本地 shard
  3. empty_route_policy=round_robin 且整批都是空 key,先按 shard 均摊再并行提交
  4. 若整批最终都落到同一个 shard,则直接一次 submit_many()
  5. 否则才按 shard 拆分后 when_all_succeed()

因此,当前批量优化的重点是:

  • 减少跨 shard 次数
  • 尽量保留整批提交
  • 对空 key round-robin 场景做批量分发优化

7. 配置模型

当前配置结构比“只列几个核心参数”更完整,几个关键默认值如下:

struct EngineConfig {
    std::string log_dir = "logs";
    std::string archive_dir = "archive";
    std::string shard_file_prefix = "shard";
    AckMode ack_mode = AckMode::write_ack;
    RoutingStrategy routing_strategy = RoutingStrategy::hash_modulo;
    EmptyRoutePolicy empty_route_policy = EmptyRoutePolicy::local;
    std::size_t routing_virtual_nodes = 128;
    std::size_t batch_size = 32;
    std::size_t flush_interval_ms = 0;
    std::size_t max_pending_bytes = 0;
    std::size_t pending_bytes_low_watermark = 0;
    std::uint64_t rotate_size_bytes = 0;
    std::uint64_t rotate_interval_seconds = 0;
    bool checkpoint_enabled = false;
    bool compress_archives = false;
    bool use_dsync = false;
    bool record_crc_enabled = false;
    CrcClass record_crc_class = CrcClass::full;
    bool record_timestamp_enabled = false;
    bool record_level_enabled = false;
    bool record_shard_id_enabled = false;
    bool record_sequence_enabled = false;
};

另一个容易写错的点是:当前仓库的配置文件格式是扁平 key=value,不是 TOML。

例如:

log-dir=./logs
archive-dir=./archive
routing-strategy=consistent_hashing
routing-virtual-nodes=256
batch-size=256
flush-ms=1
checkpoint-enabled=true

8. 查询与观测链路

查询服务当前是单独的 query_server 进程,提供:

  • HTTP
    • /healthz
    • /v1/status
    • /v1/route
    • /v1/records
  • gRPC
    • GetStatus
    • Route
    • QueryRecords
  • Prometheus
    • 通过单独 metrics-port 暴露

Writer 指标和 Reader 指标当前都已经接入 Prometheus:

  • log_engine_writer_*
  • log_engine_reader_*
  • log_engine_health_*

总结

如果只用一句话概括当前实现,可以写成:

Seastar Log Engine 当前是一个基于 seastar::sharded<AsyncWriter> 的单机异步顺序写日志引擎,围绕统一 DMA 对齐写路径、可恢复 active log、可观测查询链路和分片路由策略展开。

这版实现最值得关注的,不是“概念层面的分层是否优雅”,而是几个非常工程化的细节:

  1. append_batch() 的多快路径分发
  2. AsyncWriter 的字节级背压和 flush 协调
  3. AppendWriter 的 tail-buffer 对齐写路径
  4. checkpoint / verified scan 的保守恢复语义

下一篇:《异步批量写入模型:深入 Per-shard Writer 的设计》