MENU

SQLite + zstd:定时任务日志压缩优化实践

August 4, 2025 • Read: 142 • 编码,算法,Go

背景

我们公司有大量的 PHP 和 Shell 脚本需要定时执行。起初使用 Linux 的 crontab 管理任务,但随着业务的增长,定时任务数量一度超过 50 个,管理成本迅速上升。

为了方便线上操作,当时在 GitHub 上找到了 lisijie/webcron 这个项目。它功能简单,能满足基本的定时任务管理需求,我们便直接投入使用。

遇到的问题

随着时间推移,我们开始遇到以下两个关键问题:

  1. 日志查询影响主业务性能
    Webcron 默认使用 MySQL 存储任务和日志,与业务共用数据库。当日志量激增后,任务列表页面需要联表查询日志,页面加载时间常超过 30 秒,严重影响了主业务性能。
  2. 日志数据膨胀,存储压力大

    • 部分任务执行频率高(如每 10 秒一次);
    • 部分任务每次输出大量日志(3MB+);
    • 日志需保留 6 个月。

例如:

项目月记录数MySQL .ibd 文件体积
A 项目120 万条1.2 GB
B 项目480 万条8.9 GB

如果按照此增长速度保留半年,B 项目的日志将占用约 50GB 的磁盘空间。

解决方案

我们决定重构整个定时任务管理系统,重点优化三方面:

1. 用 SQLite 替代 MySQL 存储日志

MySQL 查询开销大、影响业务,我们将日志存储迁移到 SQLite。它无需额外服务,具备完整 SQL 支持,适合做独立日志存储方案,查询性能也更可控。

2. 使用 zstd 压缩日志内容

观察日志内容后发现,大量输出存在重复格式,非常适合压缩。我们评估了几种压缩算法:

  • zstd(默认级别 / L7级别)
  • zstd + 自训练字典
  • gzip(Go 标准库实现)

压缩对比如下:

压缩方案A 项目(120 万)B 项目(480 万)
MariaDB .ibd 文件1.2 GB8.9 GB
SQLite 明文存储856 MB665 MB
gzip(默认)212 MB405 MB
zstd(无字典)198 MB335 MB
zstd + B 项目字典149 MB192 MB
zstd L7 + B 项目字典139 MB186 MB
zstd + A 项目字典95.1 MB224 MB

注:

  • 字典通过 zstd --train 命令生成,使用项目历史日志训练;
  • 字典在不同项目中交叉使用效果不一;
  • 字典需维护更新,综合考虑后未在最终方案中使用。

最终策略是:当单条日志长度 > 150 字节时启用 zstd 压缩,否则直接存储明文。读取时自动判断并解压。

3. 分月存储日志文件

为了避免单文件过大并简化清理流程,我们将日志按月分别存储,每月 1 号新建一个 SQLite 日志库文件(如 2025-08.db)。

这样带来两个好处:

  • 查询范围控制在单月文件,性能更稳定;
  • 清理历史数据只需删除 .db 文件即可,维护成本极低。

实际效果

经过重构后,系统在实际运行中表现稳定,存储与性能均有显著改善:

  • 压缩后磁盘占用:每 10 万条日志占用约 9~10 MB;
  • 查询性能:500 万条日志文件中,查询指定任务最近 20 条记录耗时 < 20ms;
  • 支持字段:支持根据任务 ID、执行状态、执行时间进行高效查询。

技术实现细节

为什么不用列式压缩?

日志是按行追加写入的,若采用列式压缩,需要额外拆分压缩、整合再写入逻辑,开发成本较高,不适合高频日志场景。因此我们采用了“逐行压缩”策略,开发上更轻量。

灵感来源与参考

本项目思路部分参考了 V2EX 上的帖子:使用 SQLite 存日志 + zstd 压缩,但实际测试中压缩比与原贴略有差异。总体来说,该方案在中小型系统中表现优异,值得借鉴。

后续优化方向

  • 探索 DuckDB 替代 SQLite:支持列式存储,可能带来更高压缩比与查询性能;
  • 动态字典优化:尝试用上月日志训练下月字典,实现自动演进,提高长期压缩效率;
  • 分布式合并查询支持:支持跨月日志合并检索,便于追溯问题全链路。

小结

这次定时任务系统重构,核心目标是“日志系统轻量化、可维护、低资源占用”。通过 SQLite + zstd 压缩 + 分月归档的组合,我们在不依赖额外服务的前提下,成功将海量日志压缩到可控范围,并提升了查询体验。

如果你的系统也面临日志存储冗余、查询卡顿等问题,或许可以尝试这种“小数据库 + 压缩”的路线。


文章内容由两双筷子原创,使用 ChatGPT 进行排版优化。