Rotate 与 Archive 设计:当前日志生命周期实现

基于当前仓库实现分析 Seastar Log Engine 的 rotate、archive、gzip 和清理逻辑,包括触发条件、文件布局、保留策略以及与恢复/查询链路的协作方式。

日志生命周期的目标

当前项目的写入文件不是无限长的单文件,而是围绕下面几类对象运转:

  1. active log
  2. archived log
  3. checkpoint sidecar
  4. 可选 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 表示关闭按大小 rotate
  • rotate_interval_seconds = 0 表示关闭按时间 rotate

只要任一条件成立,就会执行 rotate。

当前 rotate 流程

当前实现的顺序大致是:

  1. flush_tail(true) 确保当前 tail 落盘
  2. 关闭当前 active file
  3. rotation_index++
  4. LogManager::rotate_active_file(...)
  5. AppendWriter::reset_after_rotation()
  6. 打开新的 active segment
  7. 若启用了 checkpoint,则写一次 checkpoint

这里有两个需要注意的事实:

  1. rotation_index 是 writer 内部状态的一部分
  2. rotate 后 checkpoint 不是“可选优化建议”,而是当前恢复语义的一部分补充

active 与 archive 的文件布局

active log

当前 active file 名称由:

  • log_dir
  • shard_file_prefix
  • shard_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 段文件名包含:

  1. shard id
  2. timestamp_ms
  3. rotation_index
  4. .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_count
  • max_archive_age_seconds

这两个名字在当前仓库里并不存在。

它们的含义

compress_archives

  • true:archive 段可压缩为 .gz
  • false:保留普通 .log

archive_retention_seconds

  • 0:不按时间淘汰
  • > 0:超过该保留窗口的 archive 可被清理

max_archived_files_per_shard

  • 每个 shard 最多保留多少归档文件

因此当前清理策略是“按 shard 维度限量 + 可选按时间窗口保留”。

gzip 的当前语义

当前实现支持 gzip archive,但更重要的是它已经做了崩溃安全强化。

1. gzip 输出是临时文件原子落盘

不是直接把最终 .gz 文件一边写一边暴露出去,而是:

  1. 先写 .gz.tmp
  2. 完成后再原子 rename 成 .gz

这样做的目的很明确:

  • 防止中断时留下半成品 .gz

2. 查询链路会处理 .log / .log.gz 冲突

当前项目已经修过一个实际问题:

  • 如果压缩过程中中断,archive 目录里可能同时残留同一段的 .log.log.gz

现在 archive 枚举会按:

  • shard_id + timestamp_ms + rotation_index

去重,并优先选择未压缩 .log

这点很关键,因为 rotate/archive 设计不只是“怎么存”,还直接影响 query 结果是否重复。

cleanup 的当前理解方式

当前 cleanup 更准确的理解是:

  1. 先枚举 archive segments
  2. 按 shard / 时间 / rotation 排序
  3. 再按保留策略决定删除哪些文件

由于当前真实字段是:

  • archive_retention_seconds
  • max_archived_files_per_shard

所以文章里如果写“全局最多 100 个 archive 文件”就是不准确的,真实语义是每个 shard 单独限量

rotate / archive 与 query 链路的关系

这部分在当前实现里很重要。

查询链路会同时读取:

  1. active segments
  2. 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 机制最准确的描述是:

  1. rotate 判断在 AsyncWriter::maybe_rotate() 中完成
  2. active log 按大小或按时间触发滚动
  3. archive 支持 .log.log.gz
  4. gzip 输出使用 .gz.tmp 原子落盘
  5. archive 清理按 max_archived_files_per_shardarchive_retention_seconds 工作
  6. archive 枚举已经考虑 .log/.log.gz 冲突去重

如果压缩成一句话:

当前日志生命周期实现的重点,不只是“防止文件无限长”,而是“让 rotate、archive、gzip、query、recovery 这几条语义彼此兼容”。


系列 2 完结