Rotate 与 Archive 设计:当前日志生命周期实现
基于当前仓库实现分析 Seastar Log Engine 的 rotate、archive、gzip 和清理逻辑,包括触发条件、文件布局、保留策略以及与恢复/查询链路的协作方式。
日志生命周期的目标
当前项目的写入文件不是无限长的单文件,而是围绕下面几类对象运转:
- active log
- archived log
- checkpoint sidecar
- 可选 gzip archive
它们共同组成一条生命周期:
active append -> rotate trigger -> archive segment -> optional gzip -> retention cleanup
rotate 由谁触发
当前 rotate 判断不在一个单独的“后台生命周期线程”里,而是在 AsyncWriter::maybe_rotate() 中做。
每次 flush 成功后都会检查两类条件:
1. 按大小触发
_config.rotate_size_bytes > 0 &&
_append_writer.logical_size() >= _config.rotate_size_bytes
2. 按时间触发
_config.rotate_interval_seconds > 0 &&
opened_duration >= rotate_interval_seconds
也就是说:
rotate_size_bytes = 0表示关闭按大小 rotaterotate_interval_seconds = 0表示关闭按时间 rotate
只要任一条件成立,就会执行 rotate。
当前 rotate 流程
当前实现的顺序大致是:
flush_tail(true)确保当前 tail 落盘- 关闭当前 active file
rotation_index++LogManager::rotate_active_file(...)AppendWriter::reset_after_rotation()- 打开新的 active segment
- 若启用了 checkpoint,则写一次 checkpoint
这里有两个需要注意的事实:
rotation_index是 writer 内部状态的一部分- rotate 后 checkpoint 不是“可选优化建议”,而是当前恢复语义的一部分补充
active 与 archive 的文件布局
active log
当前 active file 名称由:
log_dirshard_file_prefixshard_id
共同决定。
默认前缀是:
shard_file_prefix = "shard"
所以默认 active 文件更接近:
logs/shard-0.log
logs/shard-1.log
而不是很多示意文章里写的 log_engine-0.log。
checkpoint
checkpoint 路径基于 active segment 生成,是一个 sidecar 文件。
archive log
archive 段文件名包含:
- shard id
- timestamp_ms
- rotation_index
.log或.log.gz
这也是为什么查询链路可以在 archive 目录中按 shard/timestamp/rotation 做排序和去重。
当前 archive 相关配置
这部分最容易被写错。
当前真实配置字段是:
std::uint64_t archive_retention_seconds = 0;
std::size_t max_archived_files_per_shard = 8;
bool compress_archives = false;
不是:
max_archive_countmax_archive_age_seconds
这两个名字在当前仓库里并不存在。
它们的含义
compress_archives
true:archive 段可压缩为.gzfalse:保留普通.log
archive_retention_seconds
0:不按时间淘汰> 0:超过该保留窗口的 archive 可被清理
max_archived_files_per_shard
- 每个 shard 最多保留多少归档文件
因此当前清理策略是“按 shard 维度限量 + 可选按时间窗口保留”。
gzip 的当前语义
当前实现支持 gzip archive,但更重要的是它已经做了崩溃安全强化。
1. gzip 输出是临时文件原子落盘
不是直接把最终 .gz 文件一边写一边暴露出去,而是:
- 先写
.gz.tmp - 完成后再原子 rename 成
.gz
这样做的目的很明确:
- 防止中断时留下半成品
.gz
2. 查询链路会处理 .log / .log.gz 冲突
当前项目已经修过一个实际问题:
- 如果压缩过程中中断,archive 目录里可能同时残留同一段的
.log和.log.gz
现在 archive 枚举会按:
shard_id + timestamp_ms + rotation_index
去重,并优先选择未压缩 .log。
这点很关键,因为 rotate/archive 设计不只是“怎么存”,还直接影响 query 结果是否重复。
cleanup 的当前理解方式
当前 cleanup 更准确的理解是:
- 先枚举 archive segments
- 按 shard / 时间 / rotation 排序
- 再按保留策略决定删除哪些文件
由于当前真实字段是:
archive_retention_secondsmax_archived_files_per_shard
所以文章里如果写“全局最多 100 个 archive 文件”就是不准确的,真实语义是每个 shard 单独限量。
rotate / archive 与 query 链路的关系
这部分在当前实现里很重要。
查询链路会同时读取:
- active segments
- archived segments
因此 rotate/archive 设计必须满足:
- 文件命名可解析
- segment 顺序可稳定排序
.log/.log.gz冲突能被去重- 坏 gzip 不会把整条查询链路拖死
从这个角度看,rotate/archive 不是一个单纯的“存储空间优化机制”,而是 query 语义的一部分。
rotate / archive 与 recovery 的关系
对恢复来说,active log 和 archive 的角色不同:
active log
- 启动恢复时的主要事实来源
archive log
- 用于后续查询
- 不直接参与 active writer 的恢复边界判断
因此当前 rotate 后的 checkpoint 和 active segment 状态,比 archive 本身对恢复更关键。
当前实现里没有的东西
为了更清楚地界定这套实现的边界,这里把当前未纳入的内容单独列出来:
- 没有
max_archive_count - 没有
max_archive_age_seconds - 没有一个通用“后台并发 archive worker 池”
- 没有单独的 archive 完整性索引数据库
- 没有复杂的冷热分层存储策略
当前实现已经足够工程化,但还没有到那种“完整生命周期存储平台”的级别。
当前更准确的配置理解
只做 rotate,不做压缩
rotate_size_bytes > 0
compress_archives = false
适合开发或调试阶段。
rotate + gzip + 限量保留
rotate_size_bytes > 0
compress_archives = true
max_archived_files_per_shard = 8
这是当前实现更典型的生产式用法。
rotate + 时间保留窗口
archive_retention_seconds > 0
用于让 archive 具备“超过窗口自动淘汰”的语义。
总结
当前 rotate/archive 机制最准确的描述是:
- rotate 判断在
AsyncWriter::maybe_rotate()中完成 - active log 按大小或按时间触发滚动
- archive 支持
.log与.log.gz - gzip 输出使用
.gz.tmp原子落盘 - archive 清理按
max_archived_files_per_shard和archive_retention_seconds工作 - archive 枚举已经考虑
.log/.log.gz冲突去重
如果压缩成一句话:
当前日志生命周期实现的重点,不只是“防止文件无限长”,而是“让 rotate、archive、gzip、query、recovery 这几条语义彼此兼容”。
系列 2 完结