日志文件达阈值前需预判切换:写入前用file_size()检查大小并预留缓冲,原子关闭旧文件、异步压缩、流式gzwrite逐块处理,文件名用“应用名_日期_时间_秒内序号”格式,失败时保留.pending并降级轮转。

日志文件达到大小阈值后,如何触发 gzip 压缩归档
核心是「在写满当前日志前,提前判断并切换文件」,而不是等 write() 失败再处理。常见错误是等到 ofstream 写入失败或 stat() 发现文件已超限才行动,此时最后一段日志可能丢失或截断。
- 每次写入前检查当前日志文件大小:用
std::filesystem::file_size()(C++17)或stat()(POSIX)获取实时尺寸 - 预设阈值(如 10MB),但预留 1–2KB 缓冲,避免刚好卡在边界导致单条日志跨文件
- 切换逻辑必须原子:先
close()当前句柄,再调用压缩函数,最后用新文件名打开下一个日志 - 压缩过程应异步或后台线程执行,否则阻塞主线程日志输出——尤其在高频打点场景下,
gzopen()+gzwrite()耗时不可忽略
用 zlib 实现无临时文件的流式 gzip 压缩
直接读取原日志文件、压缩后写入 .log.gz,看似简单,但容易因内存或 I/O 阻塞出问题。关键是绕过磁盘中转,用内存 buffer + zlib 的流接口完成「读—压—写」流水线。
- 不要用
system("gzip xxx.log"):无法控制压缩级别、无错误反馈、路径含空格时易崩,且依赖系统命令 - 正确做法:用
gzopen()打开目标.log.gz,然后用普通std::ifstream逐块(建议 8KB–64KB)读原日志,每块调用gzwrite() - 注意
gzwrite()返回值:它可能只写入部分数据,需循环直到全部写完,否则压缩文件损坏 - 压缩完成后必须调用
gzclose(),否则尾部 CRC 和 ISIZE 字段不完整,gunzip会报unexpected end of file
归档文件名怎么生成才不易冲突又方便检索
用时间戳是最稳妥的,但直接用 std::chrono::system_clock::now() 得到的毫秒值,在高并发写日志时仍可能重复;而单纯用序号又难定位时间范围。
- 推荐组合格式:
appname_20240520_142305_001.log.gz—— 日期 + 精确到秒的时间 + 序号 - 序号不是全局计数器,而是「该秒内第几个归档」,靠
std::atomic_int每秒重置,避免多线程竞争 - 避免使用
localtime():线程不安全;改用localtime_r()(Linux)或localtime_s()(Windows) - 文件名中禁用冒号
:、星号*等非法字符,Windows 下尤其要注意
压缩失败时,日志不能丢,但也不能卡死主流程
zlib 函数失败(比如 gzopen() 返回 Z_NULL)通常意味着磁盘满、权限不足或路径不存在。这时候如果直接 abort 或 throw,业务日志就断了。
- 压缩失败后,把原日志文件保留,改名加
.pending后缀(如app_20240520.log.pending),下次启动时尝试续压 - 记录一条「压缩失败」的明文日志到当前活动文件,内容包含错误码(
errno或 zlib 的zError())、文件路径、时间 - 不要重试超过 3 次:防止反复失败占满磁盘或 CPU,第 3 次失败后改用降级策略——跳过压缩,直接轮转为
.log.bak - 所有 zlib 调用前后,检查
errno和返回值,gzerror()比strerror(errno)更准,因为 zlib 自己管理部分错误状态
最麻烦的其实是信号安全:如果程序收到 SIGTERM 时正在压缩,gzclose() 可能没执行完,残留的 .gz 文件就是坏的。所以关键操作得加锁,或者用 RAII 封装 gzFile 生命周期。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











