本文要点
- 同一份 16MiB 的日志语料,四种工具的产物差了 4.5 倍:
gzip -9压到 12.6%、bzip2 -96.1%、zstd -194.8%、xz -62.8%——但耗时也从 gzip 的 0.85 秒一路涨到 zstd -19 的 37 秒、xz -6 的 15 秒 - ⚠️ 三个工具的「ratio」列口径完全不同,直接抄进表格会出错:
gzip -l是节省百分比(87.4% 表示省了 87.4%),xz -l是压缩后/压缩前(0.028),zstd -l是压缩前/压缩后(20.795)。同一份文件三个数字,说的是同一件事 - 压缩率是用内存换的:实测峰值内存
gzip -9仅 2.0 MiB、xz -682 MiB、zstd -19100 MiB;同一台机器上xz -9压同一份数据是 145 MiB(级别越高字典越大,内存随输入规模继续涨) - 核数不够时并行压缩完全是心理安慰:1 核机器上
pigz -p4与gzip耗时一样(都是 0.2 秒),xz -T0与-T1产物字节数完全相同(都是 1 stream / 1 block) - ⚠️ zstd 的档位不是越高越好:实测
-3压出 1,296,890 字节,而-9反而压出 1,490,926 字节——更高的档位得到更大的文件。别默认「档位越高压缩率越好」,要拿自己的数据量 - 解压速度的排序和压缩速度完全是两回事:
zstd -d0.02 秒 <gzip -d0.06 秒 <xz -d0.07 秒 <<bzip2 -d0.44 秒。压得最狠的 xz 解压并不慢,而 bzip2 两头都不占优 - 对已经压缩过的数据(内核镜像、jpg、mp4)压它纯属浪费:11MiB 的
vmlinuz四种工具都只能省下约 1.5%,耗时却从 0.38 秒到 6.36 秒不等 tar侧的两个实用细节:--zstd默认走zstd -3,想指定档位要用-I 'zstd -19'(同一个目录实测 2.59MB → 1.50MB);给 gzip 加--rsyncable贵 3.2% 体积,换来的是 rsync 增量备份不整包重传
Linux 压缩工具实测:gzip、bzip2、xz 与 zstd 的压缩率、耗时与内存
gzip 是 Linux 上最不需要动脑的选择:到处都有、压得也快。但只要你的场景是「备份要存很久」或者「日志要传很远」,就该知道旁边还站着三个同门师兄弟——bzip2、xz、zstd,它们的取舍差别大得离谱。
网上关于「谁压得更小」的结论满天飞,但几乎都不带机器信息,抄过来往往不成立。本文在一台 1 核、1.9G 内存、Ubuntu 22.04 的小机器上(gzip 1.10 / bzip2 1.0.8 / xz 5.2.5 / zstd 1.4.8 / pigz 2.6),用同一份语料把这四个工具的压缩率、耗时、内存、解压速度全量了一遍,下面所有数字都来自这次实测。
实验语料
语料是一份 16MiB 的仿 Nginx 访问日志,200000 行,字段重复度高(真实日志的典型特征):
cd /tmp/comp && nproc && stat -c '语料: %n = %s 字节' access.log1
语料: access.log = 16236276 字节nproc 输出 1,这一点很关键——后面所有「并行加速」的结论都建立在单核之上,换成 8 核机器数字会完全不同。后文还会补一份 11MiB 的二进制语料(内核镜像 vmlinuz)做对照。
压缩率与耗时
四个工具各用一档「有代表性的设置」压同一份文件,time -p 记录真实耗时:
cd /tmp/comp && nproc && stat -c '语料: %n = %s 字节' access.log
rm -f access.log.gz access.log.bz2 access.log.xz access.log.zst
{ time -p gzip -9 -k access.log ; } 2>&1
{ time -p bzip2 -9 -k access.log ; } 2>&1
{ time -p xz -6 -k access.log ; } 2>&1
{ time -p zstd -q -19 -k access.log ; } 2>&1
stat -c '%-16n %9s 字节' access.log access.log.gz access.log.bz2 access.log.xz access.log.zst
把结果整理成表:
| 工具 | 耗时(real) | 产物大小 | 占原始比例 |
|---|---|---|---|
gzip -9 | 0.85 s | 2,044,685 | 12.6% |
bzip2 -9 | 1.77 s | 994,046 | 6.1% |
zstd -q -19 | 37.39 s | 780,775 | 4.8% |
xz -6 | 14.88 s | 455,016 | 2.8% |
这张表里最值得盯的是最后两行:
xz -6用 gzip 17 倍的耗时,换来 4.5 分之一个体积。对「压一次、存三年」的归档场景,这笔交易通常划算;对「每次备份都压一遍」的场景,就是纯浪费 CPU;zstd -19在这份语料上同时输给了 xz:更慢(37 秒 vs 15 秒)还更大(780KB vs 455KB)。zstd 的真正优势区间在低档位——它的设计目标是「用 gzip 的速度做到接近 gzip 两三倍的压缩率」,把它拉到 -19 去和 xz 拼压缩率,是拿自己的短板打别人的长板。
几点补充说明:
time -p的 real 是墙钟时间,同一台 1 核机器上重复跑会有百分之几到几十的抖动(本次xz -6在 14.88 ~ 19.28 秒之间波动),看数量级即可,别当基准测试;-k是保留原文件(--keep),不加它压缩完原件就没了;-q是让 zstd 别往终端刷进度条,写脚本时必加;{ ... ; } 2>&1这个写法不是多余的:time的输出走 stderr,不重定向的话它会和 stderr 一起被甩到所有 stdout 后面,输出顺序就乱了。
ratio 三个口径,别抄混
想知道压缩效果,四个工具里三个都自带「看产物」的子命令,但它们的 ratio 列说的根本不是一回事:
cd /tmp/comp
gzip -l access.log.gz
xz -l access.log.xz
zstd -l access.log.zst
for f in access.log.gz access.log.bz2 access.log.xz access.log.zst; do s=$(stat -c %s "$f"); awk -v n="$f" -v s="$s" -v o=16236276 'BEGIN{printf "%-16s %9d 字节 %5.1f%%\n", n, s, s*100/o}'; done
对照着看这三行:
| 工具 | ratio 列的值 | 实际含义 |
|---|---|---|
gzip -l | 87.4% | 节省了 87.4%(= 1 − 12.6%) |
xz -l | 0.028 | 压缩后 ÷ 压缩前(越小越好) |
zstd -l | 20.795 | 压缩前 ÷ 压缩后(越大越好) |
同一份文件,三个数字(87.4 / 0.028 / 20.795),一个说「省了 87%」、一个说「只剩 2.8%」、一个说「20 倍」——它们描述的是同一件事。把不同工具的输出直接填进同一张对比表,就会得出「zstd 是 xz 的 700 倍」这种荒谬结论。
bzip2 没有列表模式,只能自己算。上面那段 for 循环用 stat 取字节数、用 awk 统一算成「占原始大小的百分比」,这样四个工具才终于在同一条口径上:
access.log.gz 2044685 字节 12.6%
access.log.bz2 994046 字节 6.1%
access.log.xz 455016 字节 2.8%
access.log.zst 780775 字节 4.8%输出里的0.028/20.795这类比值也不适合直接比较——xz -l的 Ratio 保留了三位小数,zstd -l是三位有效数字。要横向比,就统一换算成「占原始大小的百分比」。
内存代价与并行
压缩率不是白来的,第三种代价是内存。/usr/bin/time -f 的 %M 能直接给出进程的峰值物理内存(注意要用 /usr/bin/time 全路径,shell 内建的 time 没有这个能力,apt install time 才有):
cd /tmp/comp && nproc
{ /usr/bin/time -f 'gzip -9 峰值内存=%MkB 耗时=%es' gzip -9 -c access.log > /dev/null ; } 2>&1
{ /usr/bin/time -f 'xz -6 峰值内存=%MkB 耗时=%es' xz -6 -c access.log > /dev/null ; } 2>&1
{ /usr/bin/time -f 'zstd -19 峰值内存=%MkB 耗时=%es' zstd -q -19 -c access.log > /dev/null ; } 2>&1
{ time -p gzip -6 -c access.log > /dev/null ; } 2>&1
{ time -p pigz -6 -p4 -c access.log > /dev/null ; } 2>&1
gzip -9 峰值内存=2004kB 耗时=0.81s
xz -6 峰值内存=84132kB 耗时=15.63s
zstd -19 峰值内存=102064kB 耗时=36.59sgzip -9只吃 2 MiB 内存,这是它在嵌入式和小内存机器上不可替代的原因;xz -6吃到 82 MiB,zstd -19吃到 100 MiB——是 gzip 的 40~50 倍。在只有 1.9G 内存、且没有 swap的小机器上,这不是一个可以忽略的数字;- 换更高的档位内存还会涨:本次单独测了
xz -9压同一份 16MiB 数据,峰值内存 145 MiB(xz -6的 1.7 倍)。xz 的内存开销既随档位涨、也随输入规模涨,所以「xz 很吃内存」这句话到底吃多少,只能按你自己的数据量实测——网上流传的固定数字基本都是在大文件下测的。
至于并行:pigz 是 gzip 的多线程版,-p4 开四个线程。在这台 1 核机器上结果毫无悬念:
real 0.21 ← gzip -6
real 0.22 ← pigz -6 -p4一核机器上并行压缩不会变快,线程数超过核数只多了调度开销。同理,xz -T0(自动取核数)与 xz -T1 在这台机器上产出的文件字节数完全一致(455,016,xz -l 显示都是 1 stream / 1 block)——因为 -T0 探测到只有 1 个核,退化成了单线程。并行压缩的收益上限由核数决定,这是先决条件,不是调参能绕过去的。
zstd 的档位曲线不是单调的
顺手把 zstd 的四个档位都压了一遍,结果有点反直觉:
| 档位 | 耗时 | 产物大小 |
|---|---|---|
zstd -1 | 0.07 s | 1,488,686 |
zstd -3 | 0.07 s | 1,296,890 |
zstd -9 | 0.48 s | 1,490,926 |
zstd -19 | 40.66 s | 780,775 |
-9 的产物比 -3 还大(1,490,926 > 1,296,890),而且慢了 7 倍。这不是测量误差——同一个文件、同一个版本、重复跑结果一致。
原因是 zstd 的高档位会切换到不同的匹配搜索策略和更大的窗口,对这种高度重复的合成日志来说反而选错了发力方向。结论不是「zstd 有问题」,而是:档位与压缩率之间没有单调保证,任何「用 -19 就对了」的建议都必须拿你自己的数据验证。真要压得狠,先 zstd -3 压一遍看看,再决定要不要付 -19 的时间。
解压速度:排序完全反过来
压缩是一次性的,解压却是每次恢复、每次读取都要付的。把四个产物各解一遍到 /dev/null:
| 工具 | 解压耗时 | 相对 gzip |
|---|---|---|
zstd -d | 0.02 s | 0.3× |
gzip -d | 0.06 s | 1× |
xz -d | 0.07 s | 1.2× |
bzip2 -d | 0.44 s | 7.3× |
两个反直觉的点:
- 压缩最狠的 xz,解压并不慢——0.07 秒和 gzip 基本持平。xz 慢的是压缩端,解压端只做查表和拷贝,这也是它适合做长期归档的原因;
- bzip2 两头都不占优:压缩比不过 xz、速度比不过 gzip,解压还是最慢的(7 倍于 gzip)。它今天的价值基本只剩「兼容老数据」——看到
.bz2可以放心换成别家格式。
不可压缩的数据:白费力气
拿 11MiB 的内核镜像 vmlinuz 再压一遍(它内部已经是压缩过的数据,属于典型的不可压缩输入):
| 工具 | 耗时 | 产物大小 | 占原始比例 |
|---|---|---|---|
gzip -9 | 0.38 s | 10,903,684 | 98.5% |
xz -6 | 6.36 s | 10,887,216 | 98.4% |
zstd -19 | 4.65 s | 10,891,826 | 98.4% |
四种工具都只能省下约 1.5%,却要付出 0.38 秒到 6.36 秒不等的 CPU。jpg、mp4、已压缩的 zip/tar.gz、vmlinuz 都属于这一类。
判断一个文件值不值得压,先看它是什么:
file vmlinuz输出里出现 gzip compressed data 之类的字样,就别再压了。文件的真实类型识别可以看 Linux 二进制分析:从 file 类型识别到 ELF 结构与 objdump 反汇编。
和 tar 配合的两个实用细节
打包和压缩是两件事,tar 用 -z/-j/-J 分别调用 gzip/bzip2/xz,而 zstd 有两个入口:
tar -c --zstd -f t.tar.zst tardir
tar -c -I 'zstd -19 -T0' -f t19.tar.zst tardir同一个目录(32MiB 输入):
t.tar.gz 4444318
t.tar.xz 1009200
t.tar.zst 2594599
t19.tar.zst 1502197两点结论:
tar --zstd走的是zstd默认档位(-3),想指定档位必须用-I(--use-compress-program)把整条命令传进去。本次实测把--zstd的 2.59MB 换成-I 'zstd -19 -T0'后压到 1.50MB——同一份数据,同一个工具,只差一个参数;-I是通用逃生口:-I 'xz -9 -T0'、-I 'pigz -p4'、-I 'zstd --long=27'都能这么写,比记-z/-j/-J的固定组合灵活得多。
打包与增量备份本身的细节,见 Linux tar 打包与压缩:从归档原理到增量备份。
gzip 的 --rsyncable:用体积换增量效率
同一份日志,加不加 --rsyncable:
r-plain.gz 2222294 ← gzip -6
r-rsync.gz 2293130 ← gzip -6 --rsyncable体积大了 3.2%,换来的是:--rsyncable 会定期重置压缩字典,让文件内容的小改动只影响产物的局部,rsync 这类增量同步工具因此只需要传输变化的那一小段,而不是整个压缩包。每天备份同一份日志、又用 rsync 往异地传的场景,这 3.2% 换得非常值;一次性归档则没必要加。
增量同步那一侧的用法见 rsync 数据同步与备份:从增量原理到定时快照。
选型与避坑
| 场景 | 选择 | 理由 |
|---|---|---|
| 临时传个文件、兼容性优先 | gzip | 到处都能解,2MiB 内存,速度快 |
| 长期归档、压一次存很久 | xz -6(或 -9) | 压缩率最好,解压并不慢 |
| 日常备份、要求速度 | zstd -3 | 速度接近 gzip,压缩率明显更好 |
| 大文件 + 多核机器 | pigz -p N / xz -T0 / zstd -T0 | 核数是收益上限,单核别指望 |
| 已经压过的数据 | 不压 | 只能省 1.5%,纯耗 CPU |
| 压缩包要每天 rsync 同步 | gzip --rsyncable | 贵 3.2% 体积,省下整包重传 |
几条容易踩的坑:
-l的 ratio 口径不统一(本文第四节),跨工具比较前先统一换算成百分比;zstd -19不是「比 -3 更好」,它可能又慢又大,必须实测;zstd不是所有系统的默认组件——最小化安装的 Ubuntu 就没有,归档成.zst前先确认接收方能解压,否则会出现「备份文件在,但恢复不了」;- 压缩吃掉的内存要算进容量规划:小内存机器上
xz -9这种档位跑之前先看free -m; - 压缩过的日志不会再被 logrotate 的 gzip 压小,轮转策略本身见 Linux logrotate 日志轮转:从轮转策略到防止磁盘写满;
- 别拿压缩当省磁盘的手段——文件系统层面的稀疏文件和 zram 是另外两条路(Linux 稀疏文件:从 truncate 预分配到 du 与 ls 大小不一致、Linux zram:压缩内存当 swap 用),它们解决问题的层次完全不同。
小结
四句话记住这套工具:
- 压缩率与耗时是明码标价的交易:gzip 0.85 秒 → 12.6%,xz 15 秒 → 2.8%,中间没有免费午餐;
- 内存是第三种代价,gzip 2 MiB、xz/zstd 高档位 80~100 MiB 起步,小机器上先看
free -m; - 档位和并行都要按自己的场景实测:zstd
-9可能比-3还大,1 核机器上pigz -p4一点用没有; - 解压速度是另一条曲线,长期归档选 xz(解压不慢),图全面均衡选 zstd,bzip2 可以退役了。
最后,如果你要压缩的是一个大文件、并且磁盘快满了,压缩产物落盘前记得留够空间:先 df -h 看一眼,具体排查思路见 Linux 磁盘空间排查与清理:从 df 到 journald 的完整救火流程。
评论 (0)
暂无评论,快来抢沙发吧!