Rust LogCleaner 源码解剖:三阶段流水线设计与永不误删的工程智慧
🦀 Rust LogCleaner 源码深度解析:三阶段流水线(Scanner → Selection → Action)+ gzip 压缩 + 活跃文件排除机制全剖析
RustFS(高性能 S3 兼容对象存储)中的日志生命周期管理核心就是 LogCleaner。它不是简单的 rm -rf,而是一个生产级、配置驱动、可测试的后台清理引擎,与我们之前剖析的 RollingAppender(时间+大小双轮转)完美联动。
本文基于最新主干代码(2026-03-15,crates/obs/src/cleaner/),逐文件、逐函数、逐行拆解 mod.rs、types.rs、scanner.rs、compress.rs、core.rs 的完整实现。结合 Issue #2130(活跃文件忽略导致日志暴涨)最新修复思路,由浅入深带你看懂“为什么永不爆盘、永不误删”。
浅层:模块结构与公共入口(mod.rs)
// crates/obs/src/cleaner/mod.rsmod compress;mod core;mod scanner;pubmod types;pubuse core::LogCleaner;
-
唯一对外暴露: LogCleaner(来自 core.rs) -
README 示例(直接可拷贝)展示了完整 builder + cleanup 调用 -
集成测试:5 个单元测试覆盖 keep_files、max_total_size、干跑、忽略无关文件等场景
中层:共享类型(types.rs)—— FileMatchMode + FileInfo
// crates/obs/src/cleaner/types.rs#[derive(Debug, Clone, Copy, PartialEq, Eq)]pubenumFileMatchMode { Prefix, // "rustfs.log." 开头匹配归档 Suffix, // ".rustfs.log" 结尾匹配(默认)}#[derive(Debug, Clone)]pub(super) structFileInfo {pub path: PathBuf,pub size: u64,pub modified: SystemTime,}
-
FileMatchMode与RollingAppender完全一致(Suffix/Prefix),保证扫描与轮转命名策略对齐。 -
FileInfo只存必要元数据(路径+大小+修改时间),避免反复metadata()调用,性能极高。
深层一:扫描器(scanner.rs)—— Discovery 阶段
核心函数 scan_log_directory 返回 LogScanResult:
pub(super) structLogScanResult {pub logs: Vec<FileInfo>, // 待清理普通日志pub compressed: Vec<FileInfo>, // .gz 文件(单独保留策略)// ... 其他统计字段}
关键过滤逻辑(对应 Issue #2130):
-
read_dir非递归扫描(极轻量) -
跳过活跃文件: if file_name == active_filename { continue; } -
年龄门控: min_file_age_seconds(默认 3600s = 1h),保护刚轮转的文件 -
排除模式:glob 匹配 exclude_patterns -
空文件清理:若 delete_empty_files=true,直接fs::remove_file(在扫描阶段就删,不计入保留) -
.gz 单独收集:用于后续按 compressed_file_retention_days删除
注意:活跃文件因年龄 < min_age 被跳过 → 旧版 size 限制失效(Issue #2130)。当前版本已在 RollingAppender 里内置实时 size 轮转,双保险已解决。
深层二:压缩引擎(compress.rs)—— Action 阶段压缩
pub(super) fncompress_file(path: &Path, level: u32, dry_run: bool) -> Result<(), std::io::Error> {let gz_path = path.with_extension("gz"); // 使用 DEFAULT_OBS_LOG_GZIP_COMPRESSION_EXTENSIONif gz_path.exists() { returnOk(()); }if dry_run { /* 只 log */returnOk(()); }let input = File::open(path)?;letmut encoder = GzEncoder::new(Vec::new(), Compression::new(level.clamp(1,9))); std::io::copy(&mut reader, &mut encoder)?;// 写入 .gz + flush}
-
使用 flate2(零依赖) -
幂等:已存在 .gz 直接跳过 -
dry_run 友好:生产验证神器 -
原文件不在这里删除(交给 core 统一处理)
深层三:核心编排(core.rs)—— Selection + Action 全流程
pubstructLogCleaner { log_dir: PathBuf, file_pattern: String, active_filename: String, match_mode: FileMatchMode, keep_files: usize, // 默认 DEFAULT_LOG_KEEP_FILES max_total_size_bytes: u64, max_single_file_size_bytes: u64, compress_old_files: bool, gzip_compression_level: u32, compressed_file_retention_days: u64, exclude_patterns: Vec<String>, delete_empty_files: bool, min_file_age_seconds: u64, dry_run: bool,// ... builder 私有字段}
Builder 模式(链式调用,与 local.rs 配置 1:1 映射):
impl LogCleaner {pubfnbuilder(log_dir: PathBuf, file_pattern: String, active_filename: String) -> LogCleanerBuilder { ... }// .match_mode() .keep_files() .max_total_size_bytes() ... .build()}
cleanup() 主流程(单次清理原子操作):
pubfncleanup(&self) -> Result<(usize, u64), Error> { // deleted, freed_bytes// 1. Scannerlet scan = scan_log_directory(...) ?;// 2. Selection(core::select_files_to_delete)let to_delete = self.select_files_to_delete(&scan.logs, &scan.compressed);// 3. Actionletmut deleted = 0;letmut freed = 0;for file in to_delete {ifself.compress_old_files && !file.path.extension().map_or(false, |e| e == "gz") { compress_file(&file.path, self.gzip_compression_level, self.dry_run)?; }if !self.dry_run { fs::remove_file(&file.path)?; } deleted += 1; freed += file.size; }// 额外:删除超期 .gz 文件(按 compressed_file_retention_days 计算)Ok((deleted, freed))}
Selection 策略优先级(最聪明部分):
-
按 modified时间倒序排序 -
强制保留最近 keep_files个 -
若总大小超 max_total_size_bytes→ 从最老开始删 -
单文件超 max_single_file_size_bytes→ 立即删除 -
.gz文件单独按天数清理
生产级特性全解析
-
永不误删:活跃文件 + exclude_patterns + match_mode 三重防护 -
永不爆盘:max_total + max_single + 压缩 + 空文件清理 -
零阻塞: tokio::task::spawn_blocking调用(local.rs 已集成) -
可观测: tracing全程 debug/info + 返回 (deleted, freed) 可打指标 -
跨平台:Windows 文件锁问题已在 RollingAppender 重试,LogCleaner 只读+删除 -
干跑验证: dry_run=true只报告不操作(Issue 验证必备)
与 RollingAppender 联动闭环(完整日志生命周期)
-
RollingAppender:微秒+原子计数器归档 + 实时 size 轮转(已修复 Issue #2130) -
LogCleaner:每 5 分钟(默认)扫描 → 压缩 → 删除 -
结果:活跃文件始终 < max_single,历史文件压缩后保留 30~90 天,磁盘使用率稳定 < 20%
实战配置模板(直接环境变量)
RUSTFS_OBS_LOG_DIRECTORY=/var/log/rustfsRUSTFS_OBS_LOG_FILENAME=rustfs.logRUSTFS_OBS_LOG_MATCH_MODE=suffixRUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES=1073741824 # 1GBRUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES=10485760 # 10MBRUSTFS_OBS_LOG_COMPRESS_OLD_FILES=trueRUSTFS_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS=90RUSTFS_OBS_LOG_MIN_FILE_AGE_SECONDS=0 # 强烈建议设 0(已支持活跃文件 size 轮转)RUSTFS_OBS_LOG_DRY_RUN=false
参考资料(官方最新)
-
完整源码目录:https://github.com/rustfs/rustfs/tree/main/crates/obs/src/cleaner -
README(架构金矿):https://github.com/rustfs/rustfs/blob/main/crates/obs/src/cleaner/README.md -
Issue #2130(活跃文件修复历史):https://github.com/rustfs/rustfs/issues/2130 -
flate2 压缩:https://docs.rs/flate2
掌握 LogCleaner 源码,你就拥有了 Rust 生态中最硬核的日志全生命周期引擎。无论单机还是分布式集群(多节点侧车 + Loki),都能做到“日志永存、磁盘永控、查询永快”。
写在最后:日志清理不是“删文件”,而是系统稳定性的最后一公里。用好这套三阶段流水线 + gzip + 双保险,你的 RustFS(或任意 Rust 服务)将真正生产就绪。欢迎 Star RustFS 并 PR 你的压缩优化!🦀


无论身在何处
有我不再孤单孤单
长按识别二维码关注我们

夜雨聆风