整体架构与核心模块: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 为中心的统一实现。
1. LogEngine:统一入口
LogEngine 是对外暴露的门面层,负责:
- 保存和校验
EngineConfig - 启动/停止
seastar::sharded<AsyncWriter> - 基于
ShardRouter决定消息写到哪个 shard - 提供
append与append_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;
};
启动过程也比较直接:
validate()校验配置start()启动所有 shard 上的AsyncWriter- 用
routing_strategy + routing_virtual_nodes + smp::count配置ShardRouter
对应实现可见 src/log_engine.cc。
2. AsyncWriter:Per-shard 写入核心
AsyncWriter 负责单个 shard 的写入、背压、flush、rotate、checkpoint 恢复。
它内部保存的不是原始 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;
};
这意味着当前模型是:
submit()/submit_many()先把LogMessage编码成temporary_buffer<char>- 编码后的记录进入
_pending - 达到
batch_size或命中 flush 定时器时触发刷写 AppendWriter负责真正的 DMA 对齐写入
背压模型
当前背压是基于待写字节数的,而不是简单忙等:
max_pending_bytes = 0表示关闭背压- 超过阈值后,提交方等待
condition_variable - 恢复阈值优先取
pending_bytes_low_watermark - 若未设置 low watermark,则回落到
max_pending_bytes / 2
Ack 语义
当前只有两档确认语义:
write_ack写入 batch/tail 后返回,但不额外显式 flushsync_ack写入后再执行 flush,再向调用方确认
这一点和项目文档保持一致。
3. ShardRouter:路由决策
项目里的真实类型名是 ShardRouter,不是额外抽象出来的 RoutingEngine。
它支持两种策略:
hash_moduloconsistent_hashing
还支持空 route_key 的两种策略:
localround_robin
路由实现要点
-
非空
route_key- 先做稳定哈希
hash_modulo直接% shard_countconsistent_hashing在排序后的虚拟节点 ring 上找第一个>= hash的 token
-
空
route_keylocal:落到当前 shardround_robin:按全局递增计数轮转
这套架构的实现边界
这里有两个容易被误写的点:
- 当前实现没有直接使用
std::hash<std::string>作为路由基础,而是用了稳定的 FNV 风格哈希,避免不同环境下行为漂移。 - 一致性哈希 ring 在实现里是
std::vector<pair<token, shard>>排序后做lower_bound,不是std::map。
4. AppendWriter:统一 DMA 对齐写路径
AppendWriter 的职责不是“每条记录单独补 padding 再写”,而是维护一条tail-buffer + aligned flush 路径。
简化后可以理解为:
- 新 batch 先和历史 tail 拼起来看总字节数
- 能凑满对齐块的前缀立即写入
- 剩余尾巴先留在
_tail_chunks - 在
force_flush()或关闭文件时再补零并刷出 - 最终用
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_bytesrotate_interval_seconds > 0 && 打开时长超过阈值
触发后流程大致是:
- flush 当前 tail
- 关闭 active file
- 递增
rotation_index - 通过
LogManager旋转 active file - 重置
AppendWriter - 重新打开新的 active segment
- 如启用 checkpoint,则持久化 checkpoint
恢复语义
项目当前恢复策略已经比早期版本更保守:
- 启动恢复时会结合 checkpoint 和 active log verified scan
- 残缺 checkpoint 会被忽略
- 过旧 checkpoint 也会被忽略
- 回退时会保守采用 scan 结果,而不是盲目信任 sidecar
这部分是当前实现里的一个重点,不应简化成“直接从 checkpoint 恢复”。
6. append_batch 的真实优化点
append_batch() 不是简单“按 route_key 分组”这么一句话能概括完。
当前代码里有几类快路径:
- 一致性哈希下,如果整批消息
route_key完全相同,直接整批发往同一个 shard empty_route_policy=local且整批都是空 key,直接走本地 shardempty_route_policy=round_robin且整批都是空 key,先按 shard 均摊再并行提交- 若整批最终都落到同一个 shard,则直接一次
submit_many() - 否则才按 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
GetStatusRouteQueryRecords
- Prometheus
- 通过单独
metrics-port暴露
- 通过单独
Writer 指标和 Reader 指标当前都已经接入 Prometheus:
log_engine_writer_*log_engine_reader_*log_engine_health_*
总结
如果只用一句话概括当前实现,可以写成:
Seastar Log Engine 当前是一个基于
seastar::sharded<AsyncWriter>的单机异步顺序写日志引擎,围绕统一 DMA 对齐写路径、可恢复 active log、可观测查询链路和分片路由策略展开。
这版实现最值得关注的,不是“概念层面的分层是否优雅”,而是几个非常工程化的细节:
append_batch()的多快路径分发AsyncWriter的字节级背压和 flush 协调AppendWriter的 tail-buffer 对齐写路径- checkpoint / verified scan 的保守恢复语义
下一篇:《异步批量写入模型:深入 Per-shard Writer 的设计》