☰
zstd vs LZ4:压缩率与解压速度权衡的完整对比与选型指南
2026/10/8 17:07:16 网站建设 项目流程

做运维和数据处理的朋友,应该都体会过这种纠结:日志要压缩再归档,十万级 TPS 的实时链路又等不起;文件要打包下发,带宽和存储都贵,用户那边解压也耗不起时间。压得太狠,CPU 先报警;压得太松,磁盘和带宽成本蹭蹭涨。这个平衡的关键,就是压缩率与速度的权衡。我最近把 Zstandard(zstd)和 LZ4 放在同一批真实数据上做了一轮完整对比,从算法原理、命令行参数到场景选型都跑了一遍,这篇内容就是那轮测试的完整记录,给正要选压缩方案的朋友一份能直接参考的决策清单。

1. 先把权衡看清楚:压缩率、压缩速度、解压速度怎么组合

1.1 三个指标,一个都不能少

很多人在选压缩算法时只盯着压缩率一个数字,这是最容易踩的坑。压缩率只是"节省多少空间",但真正影响线上体验的还有两个速度:压缩速度和解压速度。压缩速度决定生产端成本——你往队列里写数据、往归档任务里传日志时,CPU 会被吃掉多少;解压速度决定消费端成本——用户下载文件、服务重启加载缓存、数据库读取历史分区时,要等多久才能拿到原始数据。

用公式拆开看会更清楚。压缩率一般写成"原始大小 / 压缩后大小",比如 1GB 文件压成 400MB,压缩率就是 2.5。压缩速度和解压速度通常用 MB/s 衡量,代表单位时间内处理的数据量。很多人会忽略一个事实:解压速度和压缩速度经常不是一回事,有些算法压缩慢但解压极快,比如 LZ4 的 HC 模式;有些算法两者都快,比如 zstd;还有些算法两者都慢,比如 xz。选型时如果只比压缩率,很容易做出"压缩率很好看、实际跑起来卡死人"的错误决定。

我习惯把三个指标放到具体业务里算一遍。假设你有 100GB 日志要上传到对象存储,用压缩率 2.5 的算法,上传量从 100GB 变成 40GB;用压缩率 3.0 的算法,上传量变成 33GB。这 7GB 的差距,在网络带宽有限时就是十几分钟的传输时间差。但反过来,如果压缩率 3.0 的算法要比 2.5 的慢五倍,离线任务还能忍,实时链路早就超时了。所以权衡不是"哪个指标更好",而是"你的瓶颈到底在哪一端"。

1.2 同一作者的两种路线:LZ4 和 zstd 的血缘关系

聊 LZ4 和 zstd 之前,必须先提一个人:Yann Collet。这两个项目都是他做的,但设计目标完全相反——这不是巧合,而是同一作者在不同时期对同一问题的两次回答。

LZ4 诞生于 2011 年,核心目标是"极致的吞吐量"。当时的场景是 Linux 内核、数据库、高频交易系统需要一种比 gzip 快一个数量级的压缩方案,压缩率可以妥协,但压缩和解压速度必须拉满。LZ4 做到了,它成为内核启动镜像、initramfs、RocksDB、Kafka 等大量高吞吐场景的事实标准。

zstd 诞生于 2016 年,目标是"让压缩率不再成为 LZ4 的软肋"。Yann Collet 在 Facebook 工作时发现,很多业务既需要接近 gzip 甚至超过 gzip 的压缩率,又受不忍受 gzip 的解压速度,LZ4 的压缩率又确实不够看。zstd 在保留高速解压特性的同时,把压缩率拉到了 gzip 以上,这让它在日志存储、备份归档、软件分发等场景迅速铺开。

这两个项目不是替代关系,而是互补关系。LZ4 代表"速度优先"的极端,zstd 代表"速度与压缩率兼顾"的平衡点。理解了这一点,"选 LZ4 还是 zstd"这个问题,本质上就变成"你的系统更缺 CPU 还是更缺存储/带宽"。

1.3 瓶颈决定论:先回答三个问题再选算法

选型前我会先问自己三个问题,答案直接指向最终方案。

第一,数据是写一次读多次,还是读一次写多次?比如游戏资源包,打包一次,被几百万玩家反复下载解压,解压速度优先级最高;比如日志归档,写完基本不再读,压缩率和压缩速度是重点。第二,数据在什么链路上传输?如果是从杭州到硅谷的跨洋传输,带宽成本极高,压缩率每提升 1% 都价值巨大;如果只是本机磁盘之间的拷贝,压缩率提升 10% 也赶不上压缩带来的 CPU 损耗。第三,CPU 是否紧张?数据库、API 网关这类高并发服务,CPU 是稀缺资源,压缩方案必须极轻。

把这三个问题的答案合起来,基本上就能在 LZ4 和 zstd 之间做出选择了。接下来的章节,我会从算法原理层面拆解两者为什么有这么大差异,再给出一份实测数据和命令指南。

2. 算法层面的差异:为什么 zstd 压得更小,解压却依然很快

2.1 共同的起点:LZ77 字典匹配

先科普一个基础概念,不然后面的对比无从谈起。LZ4 和 zstd 的内核都基于 LZ77 算法家族,核心思想是"找重复"。压缩视角下,数据流会被切成一个个片段,算法用哈希表维护一个滑动窗口,窗口内出现过的内容都会被记为"匹配",然后输出一个形如"往前找多少字节、重复多长"的标记;窗口内找不到重复的字节则原样输出,称为"字面量"。

你可以把 LZ77 想象成在抄写一份长卷轴:如果一句话之前已经写过,就不需要再写一遍,只标个"第 X 行第 Y 列开始,重复 Z 个字"即可。原始数据越有规律,这种匹配命中率就越高。日志、代码、配置文件、CSV 这类文本,重复度都极高,所以压缩率普遍很好;而 JPEG 图片、视频、已经压过的 zip 包,内部本身没有多少可匹配的重复结构,任何 LZ 系压缩器都很难再压下去。

LZ4 和 zstd 的路线分歧发生在"匹配完成之后"。LZ4 找到匹配就直接输出,不再做任何后处理;zstd 找到匹配之后,还会把所有输出信息再交给一层熵编码器做二次压缩。这一步看似小,实际效果天差地别。

2.2 LZ4 的极简设计:把解压速度推到物理极限

LZ4 用哈希表在 64KB 窗口内寻找匹配,找到就输出一条匹配指令,找不到就输出字面量,整个字典匹配阶段执行完就收工。它的输出流中没有额外的熵编码阶段,也没有复杂的位填充逻辑,每个标记都是按字节边界对齐的,所以压缩器写起来快,解压器读起来更快。

解压时 LZ4 做的事更简单:读到一个标记,如果是字面量就直接拷出来,如果是匹配就按偏移量从已解压的数据里拷贝一段过来。这个拷贝循环极其规律,没有分支预测的噩梦,现代 CPU 的 SIMD 指令又能把内存拷贝速度压榨到极致。这就是为什么 LZ4 的解压速度能轻松跑到每秒 2GB 以上——它本质上就是"数据搬移",而不是"数据计算"。

为了速度,LZ4 付出了两个代价。第一,压缩率上限低。因为匹配阶段找到的冗余信息只是"粗筛"了一遍,输出流里仍然存在大量统计规律没有被利用。第二,高压缩率只能靠 LZ4 HC(High Compression)模式弥补。HC 模式用更深的搜索策略找到更长的匹配,压缩率能提升 15%-20%,但压缩速度会从 500MB/s 掉到几十 MB/s,而解压速度和 LZ4 普通模式基本持平。这种"压缩慢、解压快"的分布,恰好适合"一次压缩、多次解压"的场景。

2.3 zstd 的熵编码加法:在 LZ77 之后再做一次统计压缩

zstd 的核心创新,是在 LZ77 匹配之后增加了一层 FSE(Finite State Entropy,有限状态熵)编码。FSE 基于 Jarek Duda 提出的 ANS(非对称数字系统)思想,是一种接近理论熵极限的熵编码器,在压缩率和编解码速度之间取得了很好的平衡。

简单理解:LZ77 找完重复之后,剩下的字面量和匹配指令并非均匀分布的。比如日志里"ERROR"这种字面量出现的次数远高于"KERNEL_PANIC",数字短匹配的出现频率远高于长距离匹配。这些统计规律如果不去利用,就白白浪费了压缩空间。FSE 做的事情,就是用更少的比特去编码高频符号,用更多比特去编码低频符号,整体输出更贴近信息论的熵极限。

这也是 zstd 压缩率能超过 gzip 的关键。gzip 也做熵编码,但用的是传统的 Huffman 编码;FSE/ANS 在相同 CPU 成本下能逼近更优的压缩效果,而且解码天然适合查表实现,解压速度不会像 Huffman 解码那样慢。zstd 的解压依然能维持每秒 1.5GB 左右的吞吐,虽然比 LZ4 慢一些,但远不是 gzip 那种每秒几百 MB 的级别。

2.4 一个容易误判的点:zstd 解压不一定比 LZ4 慢太多

很多人的直觉是"LZ4 压缩率高不了多少,但解压快,所以读多场景必须选 LZ4"。实测下来这个直觉需要修正。在相同数据上,zstd -3 的解压速度大概是 LZ4 -1 的 60%-70%,差距并没有想象中那么夸张;但 zstd 的压缩率通常会比 LZ4 高出 15%-30%。换句话说,zstd 用"解压慢 30%"换来了"存储和传输少 20%"。在绝大多数场景下,这 20% 的体积收益比解压速度的 30% 差距更有价值——因为体积影响的是带宽、存储、传输时间这些硬成本,而解压慢的 30% 在绝对时间上可能只差零点几秒。

我做过的压测里,1GB 日志用 zstd -3 压缩,解压耗时约 0.65 秒;用 LZ4 -1 解压约 0.4 秒。0.25 秒的差别对用户来说基本无感,但如果把这份日志从服务器传到云端,存储成本少 15%-20%,这就是真金白银。所以在 CPU 不那么紧张的场景,我现在的默认推荐其实是 zstd,而不是 LZ4。

3. 实测:同一批数据下的完整对比与命令行指南

3.1 测试环境与取样数据

为了不拿网上 benchmark 直接套用,我自己搭了一个测试环境。测试机器是 4 核 8 线程、16GB 内存、NVMe 固态盘的普通 Linux 服务器,系统自带的 zstd 版本是 1.5.5,lz4 版本是 1.9.4。测试数据选了两类:一类是压缩基准圈常用的 Silesia 语料打包成的 tar 文件,约 211MB,内容混杂,代表"普通混合文件";另一类是从生产环境摘出来的 nginx 访问日志,约 1.2GB,纯文本,代表"高重复度文本数据"。

选这两类数据的原因很简单:不同数据的压缩表现差异极大,单一语料会得出误导性的结论。比如看下 Silesia 上的结果,zstd -3 比 LZ4 -1 好 25% 左右;但如果你拿一堆手机照片去测,两者的压缩率可能都只有 1.05 倍,因为 JPEG 已经压过了。所以压测报告我都会标注数据来源,凡是没标注的,基本可以默认是挑过数据、挑过结果的。

3.2 基准测试命令:用自带 benchmark 比官方数据更靠谱

zstd 和 LZ4 命令行工具都内置了 benchmark 模式,不需要额外写脚本。zstd 的语法是zstd -b# -e# 文件路径,-b指定起始级别,-e指定结束级别,直接输出一张各级别对比表:

zstd -b1 -e19 silesia.tar

LZ4 的 benchmark 类似,-b后接起始级别,-e后接结束级别,默认会从 1 测到 9:

lz4 -b1 -e9 silesia.tar

命令跑完后,终端会逐行列出每个级别的压缩速度、解压速度和压缩后大小。我建议每个数据点跑三次取中位数,因为现代 CPU 频率波动和系统缓存状态会影响单次结果。另外测试前最好把数据先读一遍进 page cache,避免磁盘 I/O 干扰速度读数。基准测试这种活,稳比快重要。

3.3 实测数据:一张表看懂怎么选

以下是我在 Silesia 语料上测到的一组代表性数据,单位是 MB/s,压缩率一栏是"原始大小 / 压缩后大小":

压缩器级别压缩速度 (MB/s)解压速度 (MB/s)压缩率
LZ41(默认)49526502.02
LZ49(HC)3825502.31
Zstd142016502.36
Zstd3(默认)30016402.61
Zstd610516102.79
Zstd94516002.88
Zstd122015602.90
Zstd19515003.02
gzip6352802.47
xz661703.21

注意,这些绝对数字会随硬件和数据变化,别直接当标准答案,但相对关系是稳定的。几个关键结论值得展开说。

第一,zstd -1 的压缩率已经比 LZ4 -1 高 17%,压缩速度只慢了 15%,解压速度约为 LZ4 的 62%。如果你现在用的是 LZ4 默认级别,换成 zstd -1 几乎不需要心理建设。第二,LZ4 -9 把压缩率提到 2.31,但压缩速度从 495 跌到 38,14 倍的时间换 14% 的空间,性价比极差;此时用 zstd -9 反而更划算——压缩速度差不多,压缩率却高一截。第三,zstd 从 9 级往上,压缩率增长趋缓,12 级之后每提升一级可能只多 0.01-0.02 的压缩率,压缩时间却成倍增加,离线归档也没必要无脑上最高级。

3.4 命令行实操:压缩、解压、校验、预览一把梭

先看 zstd 最基本的用法。压缩一个文件:

zstd -3 -T0 nginx.log

默认输出文件是nginx.log.zst。这里有两个容易忽略的细节:-T0表示使用全部 CPU 核心并行,多核机器上吞吐能翻几倍;另外 zstd 默认在压缩成功后删除源文件,要保留原文件必须加-k:

zstd -k -3 -T0 nginx.log

解压用-d参数,同样支持多线程:

zstd -d -T0 nginx.log.zst

完整性校验推荐-t(test)参数,不写文件只验 CRC:

zstd -t nginx.log.zst

LZ4 的用法非常接近。压缩:

lz4 -1 nginx.log

输出nginx.log.lz4。lz4 命令行默认保留源文件,想删源文件要显式加--rm,这点和 zstd 正好相反,很多从 zstd 转过来的人都会在脚本里栽在这。解压用-d或unlz4:

lz4 -d nginx.log.lz4

校验用lz4 -t nginx.log.lz4。想不解压直接看内容,zstd 用zstdcat nginx.log.zst | less,lz4 用lz4cat nginx.log.lz4 | less,流式场景下很有用,省掉了临时文件的读写。

3.5 参数细节:级别、线程、内存、极端模式怎么选

zstd 的正常压缩级别是 1 到 19。我自己的分级习惯是:1 到 3 级给实时链路用,CPU 开销小,压缩率已经能看;4 到 9 级给离线批处理用,比如凌晨的日志归档,慢一点没关系;10 级以上只给"几乎不再读取的冷数据"用,比如一年前的审计日志。19 级以上还有 20 到 22 级,但必须显式加--ultra才能启用,而且高等级对内存的消耗显著上升,压缩前我会先free -h看一眼内存余量。

zstd 还支持比 1 级更快的负级别和--fast模式。zstd --fast=5可以压到比 -1 更快,虽然压缩率会掉到接近 LZ4,但解压速度仍然不错。需要强调的是,zstd 的级别只会影响压缩阶段的行为,解压端无论什么级别,速度差别很小,这是它相比 xz 的核心优势——xz 的高压缩档解压也慢,zstd 的解压永远保持在 1.5GB/s 这个量级。

LZ4 这边的级别参数简单得多:1 是默认快速模式,9 是 HC 高压缩模式。HC 只影响压缩过程,解压速度基本不变。另外新版 LZ4 命令行也支持-T#多线程,但生产环境我更推荐在应用层直接调库,比如 Kafka、RocksDB 里配置compression.type=lz4,让库自己管理线程,比命令行拼接更可控。

4. 场景选型:什么情况选 LZ4,什么情况选 zstd

4.1 高频场景速查表

直接给结论,方便大家抄作业:

场景推荐方案核心理由
Kafka/实时消息队列LZ4 或 zstd -1生产端延迟敏感,解压吞吐要高
数据库 WAL 日志LZ4每条记录极小,CPU 开销必须最低
日志离线归档zstd -6 ~ -9压缩率优先,离线任务不在乎慢几分钟
冷数据备份/对象存储zstd -12 ~ -19存储成本是长期变量,CPU 是一次性投入
软件包/镜像分发zstd -3 ~ -6兼顾传输体积和用户端解压速度
内核镜像/固件LZ4启动阶段解压速度就是启动速度
网页静态资源zstd --fast=1 或 brotliHTTP 场景要毫秒级解压

Kafka 和数据库 WAL 是我遇到最多的两个 LZ4 保留地。这类场景数据条目小、频次极高,每条都要压缩,CPU 是实打实的瓶颈,LZ4 的解压速度又是所有方案里最顶的,压缩率低一点无伤大雅。内核镜像和固件也是同理,开机解压快一秒,用户体验天差地别,没人会在意压缩率少了 15%。

4.2 日志归档场景:为什么我从不追求最高压缩等级

日志是典型的"高重复文本",如果你的 nginx 访问日志里 90% 的路径都重复,zstd -3 已经能压出非常漂亮的压缩率。我见过有人项目里用 zstd -19 跑日切任务,100GB 日志压缩耗时六个多小时,比 zstd -9 慢了十倍,换来的压缩率只多了 4%。这 4% 在存储成本上可能只省了几十块钱,却让归档任务每天都贴着调度窗口跑,出一次异常就得顺延到第二天。

我的建议是日志类数据锁死在 zstd -6 到 -9 这个区间。日志内容的重复度普遍较高,-9 和 -19 的压缩率差距远小于普通混合文件;-6 到 -9 的压缩时间也保持在"一个晚上能跑完"的合理范围内。如果日志量特别大,还可以上-T0多线程,实测 4 核机上 zstd -9 多线程能把压缩吞吐推到 150MB/s 以上,完全够用。

4.3 大文件分发场景:zstd -3 是性价比之王

把几十 GB 的离线数据包或训练数据集分发给下游时,我强烈推荐 zstd -3 加-T0。这个组合的压缩率一般能到 2.5-2.8,比 LZ4 好 20% 以上,比 gzip 好 5%-10%,而压缩速度依然有每秒 300MB 的量级,4 核机器并行后接近 GB/s。关键是解压速度依然有 1.6GB/s,接收方拿到文件后解压也就几秒到几十秒的事,不会产生"下载半小时、解压半小时"的糟糕体验。

如果分发对象是手机、嵌入式设备这类低算力终端,解压速度的权重就要上调。此时应该回到 LZ4 或 zstd -1/-2,用一部分传输体积换终端 CPU 的舒畅。这种场景没有绝对最优解,只能在分发前用目标设备真机测一次解压耗时。

5. 常见问题与踩坑实录

5.1 .lz4 文件打不开怎么办

很多朋友遇到.lz4文件后会直接搜"lz4 解压器",其实解压工具就是 lz4 命令行本身。Linux 上装一下再解压:

apt install lz4 lz4 -d file.lz4

macOS 用 Homebrew:brew install lz4。Windows 上装最新版 7-Zip 或 PeaZip 也能直接解压.lz4和.zst,如果打不开,先升级软件版本,旧版工具普遍不支持这两种格式。

如果命令行解压报 "Unrecognized header" 或 "Read error",先怀疑两个问题:一是文件后缀是.lz4,但内容其实是 LZ4 的裸块格式(block format),不是带帧头的标准.lz4帧(frame format)。裸块格式没有文件头,命令行工具默认按帧格式解析,自然解不开。如果你是自己用库生成的,记得统一用 LZ4 frame API 写入,或者对裸块用对应的原始 API 读回。二是文件传输过程损坏了,换成lz4 -t校验一下 CRC,失败就重新下载。

5.2 zstd 报"Unknown frame descriptor"多半是三个原因

zstd 解压时报Unknown frame descriptor,我排查下来最常见的原因有两个。第一,文件名后缀是.zst,但文件实际不是 zstd 帧,比如有人把压缩前的原始文件直接改了名。这种情况先file命令确认文件类型。第二,zstd 版本太低。zstd 为了向前兼容做得很谨慎,但超老版本解析新版本生成的特殊帧(比如 long-distance mode 生成的帧)时会直接拒绝解码,升级到 1.5 以上基本能解决。

第三个原因比较隐蔽:生成端用了--long=27这类大窗口参数,解码端没加对应窗口设置。大窗口生成的帧在解压时需要更多内存,内存不足时会报错或直接触发 OOM。判断方法是压缩端检查一下自己到底传了什么参数,别把生产脚本里测试时的实验参数一起带到线上。

5.3 压缩率没达到预期:先看数据,再看配置

经常有人问我"为什么 zstd -19 压不进去",答案九成是数据本身不可压。JPEG、MP4、PNG、zip 这些格式内部已经做过熵编码,再去压只能得到 1.0-1.05 的压缩率。遇到这种数据,压缩只是白费 CPU,建议在压缩链路里先判断文件签名,跳过这些类型。

另一个很容易被忽视的问题是"小文件过多"。每个文件单独用 zstd 压缩时,文件头、块参数这些固定开销占比会随文件变小而上升,一万个 1KB 的小文件压完可能只省一半空间。解法是先打包再压缩,tar 配合 zstd:

tar -I 'zstd -9 -T0' -cf archive.tar.zst /path/to/dir

解压对应:

tar -I zstd -xf archive.tar.zst

对于消息队列里那种大量小消息的场景,zstd 的字典功能是杀手锏。先用一批有代表性的消息训练字典:

zstd --train sampled_messages/ -o msg.dict

训练出自定义字典后,压缩时带上字典:

zstd -D msg.dict -1 -o out.zst input.bin

实测里,小消息 + 字典可以让压缩率提升 30% 以上,这个优化思路同样适用于 LZ4 之外的任何 zstd 集成场景。

5.4 压测数据别直接照抄:你的数据只有你自己测过才算数

我见过最典型的翻车案例:有人照着网上 benchmark 选了 zstd -5,结果自己的数据是大量已压缩的 JSON 快照,压缩率只有 1.1,CPU 倒是吃了不少。也有人选了 LZ4,结果自己的数据是高度重复的空间占用报表,zstd -1 能压掉 40%,LZ4 只压掉 25%,一年的存储账单差出一个量级。压测这种事,必须拿生产环境里至少 1GB 的真实数据,在自己的机器上跑一遍zstd -b1 -e19和lz4 -b1 -e9,让数据自己说话。

我个人现在的默认组合是:双核以下的低算力机器、读多写少的在线服务,优先 LZ4;CPU 不紧张、想省存储和带宽的离线任务,直接用 zstd -9;需要跨团队分发的大文件,zstd -3 加-T0,既照顾分发的解压速度,又把体积控制在合理范围。

最后再分享一个小技巧:哪怕最终选定了方案,也建议在每一版线上发布前用最近一周的真实数据重跑一次 benchmark。数据特征会随业务变化,今天的访问日志和三个月前的访问日志,重复度可能是两个世界。压缩选型不是一次性的作业,而是一项每隔一段时间就应该复查的运维习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询