
日志归档用哪个压缩器,答案多半来自习惯,而不是数据。为了把习惯换成数字,生成两份固定种子的数据:12 MiB 的半结构化访问日志,4 MiB 的 JSON 记录,然后让五个压缩器的 14 个档位各跑一遍,统一从 stdin 读、往 stdout 写。脚本存档 tools/verify-compression-bench.py,下面所有数字都是它的输出,两份数据的解压结果都做了 sha256 逐字节校验。
压缩器版本:
Apple gzip 475
bzip2, a block-sorting file compressor. Version 1.0.8, 13-Jul-2019.
xz (XZ Utils) 5.8.3
*** Zstandard CLI (64-bit) v1.5.7, by Yann Collet ***
brotli 1.2.0
数据集 A:12 MiB 访问日志
样例前两行:
2026-09-02 21:24:10.891 INFO [req-387480] /static/app.js 989ms 56974 okhttp/4.12
2026-09-15 21:36:38.512 WARN [req-260645] /api/order 1785ms 17818 okhttp/4.12
按压缩后体积排序
命令 压缩后MiB 压缩率 压缩s 解压s
bzip2 -9 2.07 17.2% 0.52 0.18
xz -6 2.36 19.7% 3.47 0.09
xz -6 -T0 2.36 19.7% 3.44 0.09
brotli -q 11 2.37 19.7% 11.71 0.02
zstd -19 2.40 20.0% 3.04 0.01
zstd -19 -T0 2.40 20.0% 3.03 0.01
zstd -9 2.80 23.3% 0.14 0.01
brotli -q 6 2.94 24.5% 0.22 0.02
xz -1 3.01 25.1% 0.16 0.03
gzip -9 3.06 25.5% 0.37 0.01
gzip -6 3.12 26.0% 0.20 0.01
zstd -1 3.32 27.7% 0.02 0.01
brotli -q 1 3.47 29.0% 0.04 0.03
gzip -1 3.83 31.9% 0.05 0.01
全部 14 个档位的解压结果与原文件逐字节一致: True
数据集 B:4 MiB JSON 记录
样例前两行:
{"ts":1757694675,"event":"page_view","uid":357840,"session":"a4c4c25e","props":{"page":"/p/379","ref":"google","ms":888},"ok":true}
{"ts":1757459211,"event":"search","uid":316195,"session":"88efc1ba","props":{"page":"/p/255","ref":"google","ms":447},"ok":true}
按压缩后体积排序
命令 压缩后MiB 压缩率 压缩s 解压s
bzip2 -9 0.50 12.4% 0.20 0.05
brotli -q 11 0.54 13.6% 3.47 0.01
xz -6 0.55 13.7% 0.81 0.03
xz -6 -T0 0.55 13.7% 0.81 0.03
zstd -19 0.56 14.0% 0.73 0.00
zstd -19 -T0 0.56 14.0% 0.74 0.00
zstd -9 0.65 16.2% 0.03 0.00
brotli -q 6 0.68 16.9% 0.04 0.01
gzip -9 0.69 17.2% 0.13 0.00
gzip -6 0.70 17.6% 0.03 0.00
xz -1 0.70 17.6% 0.06 0.02
zstd -1 0.73 18.1% 0.01 0.00
brotli -q 1 0.82 20.5% 0.01 0.01
gzip -1 0.93 23.3% 0.02 0.00
全部 14 个档位的解压结果与原文件逐字节一致: True
体积冠军换了场景,但没换人
bzip2 -9 在两份数据上都排第一:日志压到 17.2%,JSON 压到 12.4%。第二名是 xz -6 和 brotli -q 11 轮流坐,差距很小,日志上差 2.5 个百分点,JSON 上只差 1.2 个点。bzip2 的块排序在重复度高的文本上一直表现很好,这一点和平时听到的「xz 压缩率最高」不太一致,原因大概是那些对比多跑在二进制或者已经压过的数据上。这份样本里它赢了,换成本机安装包之类的输入未必还是这个结果。
三件事的兑换率
这三列数字不可能同时最优,具体摆在一起更清楚:
从 zstd -9 到 zstd -19,日志体积从 23.3% 降到 20.0%,压缩耗时从 0.14 s 涨到 3.04 s,21 倍的时间换 3.3 个百分点的体积。JSON 上同样的跨度是 16.2% 到 14.0%,耗时 0.03 s 涨到 0.73 s,约 24 倍换 2.2 个点。高压缩档位的边际收益很低,除非磁盘真的紧张。
brotli -q 11 值得单独说。它在日志上花 11.71 s 换到 19.7%,和 xz -6 的 3.47 s 拿到同样的体积,慢 3.4 倍;但它解压只要 0.02 s,是全场最快的一组。压一次、解很多次的场景(静态资源预压缩最典型)这笔账是划算的,按天归档每次都要解压的场景反过来。到了 JSON 数据上它的排名还升了两位,反超 xz -6。
解压这一列,bzip2 是唯一的短板:日志上 0.18 s,是 zstd 全部档位(0.01 s)的 18 倍;JSON 上 0.05 s 对 0.00 s。归档数据保留得久、读回次数多的话,省下的那 2 个点体积会被解压时间吃掉大半。
xz -1 在两个数据集上的落差最大,JSON 上掉了两名:17.6% 和 gzip -6 完全打平,耗时却是后者的两倍(0.06 s 对 0.03 s)。LZMA 的快速档在短记录这种形态上没有优势,日志那种长行重复的模式才是它的主场。
多线程在这两个体积上没收益
xz -6 加 -T0 是 3.44 s,不加是 3.47 s;zstd -19 加 -T0 是 3.03 s,不加是 3.04 s。JSON 那边更干脆,0.81 s 对 0.81 s,0.74 s 对 0.73 s,差别在噪声里。12 MiB 和 4 MiB 都太小,多线程的启动和分块开销抵消了并行收益。这条只覆盖这个体积,几百 MiB 以上的文件没有测,别直接外推。
定档
日志按天归档,选 zstd -9:0.14 s 压完,体积 23.3%,解压 0.01 s,三项都很平均。磁盘压力大又不在乎解压慢,选 xz -6 或者 bzip2 -9。临时传输、管道中间环节,选 zstd -1,0.02 s 压完,体积 27.7%。静态资源预压缩选 brotli -q 11,一次性付出 11.71 s,之后每次请求都省。只剩 gzip 能选的场合(对方只认 .gz),那就 gzip -6,别用 gzip -9,0.37 s 只比 0.20 s 多换来 0.06 MiB。
综合下来我的默认档是 zstd -9,理由只能算半条:体积不是最小,但速度和体积都不难看,解压也快,同一份文件要换档位只要改一个数字。
这份数据的边界
两份文件都是脚本生成的合成数据,真实日志的熵比它高,压缩率会更差,但各档位之间的相对顺序通常不会翻盘。所有耗时都是本机单次测量,解压那一列尤其受系统缓存影响,同一份文件连压两次的结果也不会完全一样。内存占用和压缩格式的兼容性没测,这两项在选型里同样重要。