
1. 压缩这件事到底在压缩什么想聊压缩算法就不能只说压缩算法本身。我最早接触压缩是读大学那会儿往U盘里塞电影一个动辄1.4GB的DVDRip拿WinRAR压了半天结果纹丝不动还差点把室友的电脑搞死机。那时候我不理解为什么有些文件压缩率能到90%以上有些文件压了等于没压后来做后端开发处理日志、缓存、数据库备份、静态资源分发天天跟压缩打交道才把这个问题彻底想清楚。压缩的本质一句话就能说透去掉数据里的冗余信息。举个例子一份“AAAAAABBBBB”这样的数据如果逐字节存要占11个字节。但如果你换一种记法用“6个A 5个B”来表示只需要记录字符和它重复的次数这就是最简单的游程编码RLERun-Length Encoding思路。再比如一篇文章里“的”“了”“是”这些字出现的频率极高那就可以给常用字分配更短的编码给生僻字分配更长的编码整体算下来总长度就变小了这就是Huffman编码的思想。但问题在于不同类型的文件冗余的形式完全不一样。文本文件有极强的统计规律常用字出现频率高可以按“频率”下手。图片、音频、视频相邻像素、相邻帧之间的信息极度相似可以按“相似性”下手。已压缩过的文件JPEG、MP4、ZIP里的内容冗余已经被榨干过一次二次压缩空间极低。随机数据密钥、加密后的数据几乎没有冗余任何算法都压不动。明白了这一点你再看市面上那些五花八门的压缩算法本质就是回答两个问题怎么识别冗余以及怎么用最短的码来表示冗余。识别方式不同、编码方式不同就有了GZIP、Brotli、Zstandard、LZMA、LZ4这一大堆名字。这篇文章我从工程实践的角度把主流的几种压缩算法放在一起从压缩率、压缩速度、解压速度、内存消耗、适用场景几个维度做一次横向对比顺便把选型思路和实际踩过的坑也交代清楚。无论你是做后端存储、前端打包、移动端资源管理还是单纯好奇“为什么7-Zip压出来比WinRAR小”这篇应该都能帮到你。2. 先拆开看三种核心压缩思路在对比具体算法之前我建议先把压缩算法背后的三套底层思路搞清楚。因为后面所有算法不管名字多花哨归根结底都是这三套思路的组合变体。2.1 统计编码从“频率”下手统计编码的思路是高频用短码低频用长码。最经典的是Huffman编码。它的做法是把每个符号按出现频率排序频率最高的符号分配最短的二进制编码频率最低的分配最长的。整个过程会构建一棵二叉树所以也叫Huffman树。Shannon-Fano编码更早一些思路类似但它用二分法递归切分符号集效果通常不如Huffman。算术编码Arithmetic Coding是Huffman的进阶版它不是给每个符号单独分配编码而是把整段数据映射到一个区间的小数上压缩率更适合符号分布极度不均匀的场景现代的H.264视频编码、JPEG 2000里都能看到它的影子。2.2 字典编码从“重复”下手字典编码的思路是另一个方向把重复出现的字符串替换成更短的引用。最经典的实现是LZ77和LZ781977年和1978年由两位以色列人提出后面几乎所有主流压缩算法都继承了这个血统。LZ77的思路是这样的它维护一个“滑动窗口”窗口里是最近处理过的数据。每当读入新的数据时它就在窗口里查找有没有相同的字符串。如果有就把重复的内容替换成“偏移量长度”这样一个二元组。比如窗口里有“abcdefg”新数据是“defg…”它就可以说“往前数第4个位置开始取4个字符”省去了重复存储4个字符的开销。LZ78不太一样它维护一张动态字典把见过的字符串逐条登记进去之后遇到相同的就直接用字典编号引用。LZWLempel-Ziv-Welch是LZ78的改良版当年GIF图片格式用的就是它。你可能听过的DEFLATE算法就是LZ77的滑动窗口 Huffman编码的统计压缩两者叠加。GZIP、ZIP、PNG图片全部建立在DEFLATE之上。2.3 预测编码从“趋势”下手统计和字典都是在“找重复”预测编码是更激进的一条路根据前文推测下一个数据大概是什么只记录预测偏差。这听起来有点玄但应用极广。图片里一个像素的值通常跟它左边、上边的像素非常接近JPEG压缩时就会做这种预测音频和视频更是重灾区因为相邻采样点之间差值很小编码差值而非原始值能省下大量比特。预测编码往往是有损压缩的基石因为“预测不准的部分”可以直接被丢掉或者粗糙化人眼和人耳很难察觉。这也是为什么无损压缩和有损压缩在思路上有本质区别无损压缩只做可逆的数学变换有损压缩会主动舍弃人眼不敏感的信息。3. 主流无损压缩算法横向对比这一节进入正题。我把工程上最常见的几种算法放在一起按“年代、家族、特点、优缺点”挨个拆。为了让你有体感我先给一组实测数据再逐个分析原理。测试环境说明一下我的测试机器是公司的Linux服务器CPU是AMD EPYC 7K62内存64GB磁盘NVMe SSD压缩对象是一份313MB的Nginx访问日志文本文件这份日志有典型的重复性和高熵混合特征能反映大多数服务端文本压缩的真实场景。算法压缩后大小压缩率压缩耗时解压耗时算法家族原始文件313 MB----gzip -6默认89 MB71.6%2.1s0.4sDEFLATEgzip -987 MB72.2%3.4s0.4sDEFLATEbzip280 MB74.4%5.6s2.1sBWT Huffmanxz -665 MB79.2%12.8s0.7sLZMA2zstd -384 MB73.2%0.6s0.2sLZ77 FSEzstd -1967 MB78.6%11.2s0.3sLZ77 FSEbrotli -1163 MB79.9%23.5s0.4sLZ77 Huffman变体lz4 -1116 MB62.9%0.1s0.1sLZ77变体数据看完你大概能感受到一个普遍规律没有免费的午餐。压得越狠通常压得越慢但解压速度不一定慢。下面每个算法单独说。3.1 DEFLATEGZIP / ZLIB统治了三十年的老将DEFLATE算法1989年由Phil Katz设计被用于ZIP格式后来广泛进入GZIP、ZLIB。它的结构是LZ77负责找重复Huffman负责做统计编码。GZIP是目前互联网上最普及的压缩格式。HTTP协议里的Content-Encoding: gzipLinux里各种.tar.gz包安卓APK资源的压缩几乎都有它的身影。为什么它能统治这么多年因为在“压缩率、速度、生态支持”三者之间它取得了极好的平衡点。任何一门语言、任何一家CDN、任何一台服务器对GZIP的支持都是开箱即用的。工程上的默认选择是gzip -6因为-6在压缩率和耗时之间最均衡。-9能多压1%左右但耗时往往要多出60%以上收益很低。如果你在Web服务器上开gzip压缩用默认级别就好纠结-9意义不大。3.2 BWT 哈夫曼BZIP2老一代的压缩率王者BZIP2走的是完全不同的路线。它不直接做LZ匹配而是先用一个叫Burrows-Wheeler TransformBWT的变换把文本内容按“前后文关系”重排让相同前缀的字符聚到一起产生大量连续重复的片段然后再交给游程编码和Huffman处理。BWT的手法天生适合文本。比如一段英文所有跟在the后面的单词会被排在一起所有以tion结尾的片段也会聚在一堆原本分散的重复模式局部化压缩率就上来了。但BZIP2的问题也很明显压缩慢解压更慢。它的压缩虽然比gzip高几个百分点但解压速度只有gzip的一半左右这在高频读取场景里很致命。我上次用BZIP2还是好几年前压缩一批归档日志之后就再没碰过——同样的活交给Zstandard效果更好、速度还快十倍。3.3 LZMA / LZMA2XZ / 7-Zip高压缩率守门员LZMA是7-Zip作者Igor Pavlov在1998年设计的全称Lempel-Ziv-Markov chain Algorithm。它在LZ77的基础上做了巨大改良能匹配的重复长度更长、偏移范围更大并引入区间编码Range Coder做熵编码。LZMA的压缩率长期霸榜XZ格式也就是.xz文件用的是LZMA2改进版在Linux发行版软件包、内核压缩里非常常见。我实测日志文件xz -6就能压到65MB比bzip2还好一截比gzip整整小了四分之一。代价是压缩慢。压同样的313MB日志xz用了12.8秒gzip只要2.1秒差了六倍。但有趣的是它的解压速度并不算慢只用了0.7秒就解出来了。所以LZMA适合“压一次、解很多次”的场景比如软件发行包、固件、数据归档。常规Web服务动态压缩用它就太亏了。3.4 ZstandardZSTDFacebook出品的新一代全能选手Zstandard是Facebook工程师Yann Collet在2015年发布的目前已经被当成下一代默认压缩器来用。它的思路是把LZ77的匹配找重复和一种叫FSEFinite State Entropy的熵编码组合在一起。FSE本质上是Huffman和算术编码的杂交拥有接近算术编码的压缩率同时解码速度极快。Zstandard还有一个杀手锏多级压缩档位跨度极大。从zstd -1到zstd -22快的时候比gzip还快几倍压得狠的时候接近LZMA的压缩率。它还能配合--long参数把匹配窗口从几MB拉到几十MB甚至上百MB专门用来处理超大重复距离的数据。最让我满意的是它的解压速度。同样一份日志zstd -19压缩后67MB比gzip小20MB但是解压只用了0.3秒比gzip还快。这意味着你可以在存储层放心用高压缩档不用担心中间读取时解压太慢。我在日志归档、特征数据缓存、消息队列消息压缩这几个场景都换成了ZSTD效果立竿见影。3.5 BrotliGoogle为Web传输定制的利器Brotli是Google在2015年随HTTP压缩需求推出的目的是替代GZIP做Web静态资源的传输压缩。它的核心框架跟DEFLATE一样是LZ77 Huffman但它做了两个重大改进。一是词典。Brotli内置了一个足足120KB的常用词汇、短语列表内容覆盖HTML标签、CSS属性、JavaScript关键字、英文常用词等Web资源的高频片段。压缩时命中的词典内容可以直接用一个极短的编码引用Web资源里的高频片段根本不需要从零开始训练模型。二是上下文建模。Brotli在熵编码阶段不是简单统计每个符号的频率而是根据“前一个符号是什么”来动态调整概率模型让编码更精准。这两个改进让Brotli在压缩Web静态资源HTML、CSS、JS时比GZIP普遍能再多压15%~25%。我在实际项目里测过几个前端打包产物gzip -6压完的JS文件brotli -11重新压一遍体积大约还能小20%。付出是压缩速度慢到离谱23.5秒压一份313MB日志——这是这几个算法里最慢的。所以Brotli的正确用法是只压一次放CDN上给用户反复下载。每次请求动态算Brotli是不现实的。它是压缩耗时换用户流量消耗的典型。3.6 LZ4 / LZ5极致速度代价是压缩率前面几个算法都在追求“更小”LZ4从设计之初就把目标定为“更快”。它在LZ77的基础上做了简化匹配时只找最短的重复并用极其轻量的字节码格式记录输出。LZ4有多快呢我的实测里1秒左右就能压缩完1GB级别的文件解压速度能到每秒钟几个GB这是内存拷贝级别的速度。代价是压缩率比较差62.9%只比不压强一点点。它最适合的场景你一定遇到过Redis的RDB持久化有一种模式就是LZ4压缩Kafka的压缩协议里也支持ZSTD和LZ4Linux内核里很多临时数据交换用的也是LZ4。这类场景的特征是数据量巨大、对压缩率不敏感、但绝对受不了延迟。3.7 有损压缩算法简述文章标题是“各种压缩算法对比”如果不提有损压缩那对比就不完整。有损压缩主要在多媒体领域核心是信号处理。JPEG对图像做离散余弦变换DCT丢掉高频细节压缩率可达90%以上。MP3 / AAC基于心理声学模型丢掉人耳听不到的频率成分压缩率可以到1/10甚至1/14。H.264 / HEVC利用帧内预测和帧间运动补偿视频压缩率远高于任何静态序列压缩。Opus目前音质和延迟综合表现最好的语音编解码器VoIP应用的标配。有损压缩的思路跟无损完全不同它是“允许失真只要人眼人耳看不出来听不出来”。不要拿压缩率去跟GZIP、ZSTD比它们的评价体系就不一样。如果你在开发中遇到“图片要压缩后传输”那你要考虑的不是无损算法而是WebP、AVIF这些现代图片格式它们的压缩率在同等视觉质量下比JPEG普遍再小30%~50%。4. 怎么选按场景对号入座算法清单拉完了接下来是灵魂拷问你到底该用哪个我按常见的工程场景给一份选型建议这些建议基本来自我在实际项目里反复测试后的结论。4.1 日志归档和数据存储日志文件的特点是量大、文本为主、重复模式多、要留存很久、偶尔要被翻出来查。首选ZSTD建议zstd -10~zstd -15兼顾体积和归档时的速度。如果磁盘非常紧张可以上zstd -19压缩率接近xz解压速度却快几倍。备选XZxz -6。如果你们公司有那种十年不动的冷数据存储XZ的体积确实更小能省一点算一点。一个额外经验归档日志时先做排序或分组再做压缩压缩率能提升不少。比如把多条相同来源IP的日志排到一起LZ77能找到更长的重复串实际压缩率普遍能再提升5%~10%。4.2 网络传输压缩HTTP / APIHTTP响应体的压缩要分两类看通用API返回的JSON首选GZIP兼容性最好。JSON数据本身不大Brotli带来的收益不明显反而压缩耗时高。而且很多老客户端、代理服务器对gzip之外编码的支持不完整。静态资源HTML/CSS/JS首选Brotli而且要用brotli -q 11预压缩产物直接放CDN。同时保留一份GZIP版本做降级在Nginx里开启brotli_static和gzip_static自动选择。我实测过一个React项目的bundle.js未压缩808KBgzip -9后247KBbrotli -11后213KB差距34KB。单次请求确实不多但一天上百万次访问一年省下的流量成本相当可观。4.3 文件打包和软件发布软件发布包的核心诉求是“体积尽量小 用户解压尽量快”。如果用户是开发者和技术人员ZIP格式的兼容性最好Windows/macOS/Linux开箱即解。内部实现用DEFLATE也可以加/zstd扩展。如果追求极致体积Linux下用.tar.zsttar zstd几乎是目前的最优解比.tar.gz小10%以上比.tar.xz小一点但解压快得多。.tar.zst还有一大优势是支持多线程压缩。我之前归档一个1.2TB的数据库备份目录用zstd -T16 -19大概跑了一个多小时压缩率几乎和xz持平但耗时只有xz的三分之一。多核服务器上差距会被拉得非常大。4.4 实时数据管道消息队列 / 缓存Kafka、Redis、RocksDB这类需要极低延迟的组件一般建议优先选LZ4它几乎不增加写入延迟CPU开销可以忽略不计。如果消息体很大、网络带宽又窄可以用ZSTD的快速档zstd -1或zstd -3做折中压缩率好很多速度也只是LZ4的三分之一到二分之一。我自己在Kafka里就踩过这个坑一开始用gzip压缩消息topic的写入吞吐直接掉了一半生产者和消费者的CPU飙升。后来改成ZSTD -3吞吐恢复了体积还比gzip小。核心原因是Kafka的gzip实现只支持单线程ZSTD在底层做了SIMD加速几个KB的单条消息处理效率天差地别。4.5 移动端和嵌入式场景移动端App内的资源包或者嵌入式Linux的固件空间是硬约束图片资源用WebP或AVIF替代JPEG/PNG保留质量的同时体积明显下降。代码/配置资源用Brotli离线压缩打包进App资源目录运行时解压到内存或临时文件。固件镜像用XZ或ZSTD --ultra -22。嵌入式CPU解压速度通常不是瓶颈闪存空间才是。5. 几个入门级但反直觉的细节这些细节很多是实验和实测后才明白的。如果你的项目里已经在用压缩下面这几条很可能也踩过或即将踩中。第一个坑不要对已经压缩过的数据再压缩。这看起来像是废话但实际项目里经常有人犯。比如从CDN拉了一个.gz的包存到本地又做了一层ZIP归档比如日志里混着Base64编码的图片数据压缩率奇低。Base64会把二进制数据摊平成ASCII字符引入大量无意义的冗余GZIP压Base64后的数据压缩率往往只有5%~10%不如原始二进制直接压。遇到这种情况正确的做法是先解开Base64还原二进制再压。第二个坑压缩级别不是越高越好关键看瓶颈在哪。很多人习惯无脑gzip -9甚至bzip2 -9其实完全是心理安慰。级别越高压缩时间呈指数增长压缩率只线性微增。而且很多场景里压缩是写一次读多次压缩慢一点无所谓但如果是Web服务实时压缩每一毫秒CPU都是钱。我一直用的默认档是gzip -6、zstd -3到-8之间线上压效果差别微乎其微但CPU占用率差异感人。第三个坑多线程可以救回压缩级别的损失。XZ和ZSTD都支持多线程压缩。XZ的-T参数可以指定线程ZSTD的-T参数更多可以自动检测CPU核心数。在8核16线程的机器上zstd -T8 -19的耗时能压到单线程的三分之一左右但压缩率跟单线程几乎一样。如果你的服务器CPU有多余余量压归档任务时千万别浪费。第四个坑衡量压缩率要分清楚“肉眼体积”和“信息熵”。同样是100MB的数据文本能压到30MB随机数据压到99MB这并不代表文本压缩算法更好。数据的可压缩性上限由信息熵决定算法只是尽量逼近这个上限。我见过产品经理拿着一个已经加密过的备份文件测试各种算法然后得出“这些算法都没用”的结论。那当然没用加密的目的就是把数据变成高熵伪随机任何压缩算法面对它都无能为力。6. 实测演示一份日志的完整压缩对比这一节是实打实的操作记录照着做你也能完整复现出让这些结论生效的实验。我用一台普通的Linux服务器分别用gzip、bzip2、xz、zstd、brotli、lz4压同一份日志文件。这个过程能帮你直观理解每个算法的真实差异。6.1 准备测试环境系统是Ubuntu 22.04自带gzip、bzip2、xz-utils另外我装zstd和brotlisudo apt update sudo apt install -y zstd brotli准备测试文件。我生成了一份模拟Nginx访问日志一共320万行约313MB# 用已有日志或自己生成都可以关键是文件要足够大、内容有代表性 # 这里假设你已经有一份日志文件 access.log ls -lh access.log6.2 逐算法压一遍# gzip 默认级别-6 gzip -6 -k access.log # gzip 最高级别-9 gzip -9 -k access.log # bzip2 默认级别 bzip2 -k access.log # xz 默认级别-6 xz -k access.log # zstd 快速档 zstd -3 -k access.log # zstd 高压缩档 zstd -19 -k access.log # brotli 最高档 brotli -q 11 -k access.log # lz4 默认档 lz4 -k access.log每个命令都记录一下耗时time gzip -6 -k access.log # 实测 2.1s time zstd -3 -k access.log # 实测 0.6s time zstd -19 -k access.log # 实测 11.2s再用ls -lh看所有输出文件的大小基本上就是你前面看到的那张表的现实还原。6.3 解压性能测试解压速度同样重要甚至更重要。统一用time来测量time gzip -dc access.log.gz /dev/null time zstd -dc access.log.zst /dev/null time xz -dc access.log.xz /dev/null把输出重定向到/dev/null避免写盘干扰。6.4 一个更有参考价值的对比维度压缩解压总耗时单纯比压缩耗时意义有限因为解压才是高频操作。我建议算一个“单文件往返总耗时”压缩耗时加解压耗时。以我的测试数据为例算法压缩耗时解压耗时往返总耗时压缩后大小gzip -62.1s0.4s2.5s89 MBzstd -30.6s0.2s0.8s84 MBzstd -1911.2s0.3s11.5s67 MBxz -612.8s0.7s13.5s65 MBbrotli -1123.5s0.4s23.9s63 MB你会发现zstd -3几乎全面碾压gzip -6压缩更快解压更快文件还更小。这也是为什么我强烈建议你把现有压缩链路全面评估一遍换成ZSTD。zstd -19虽然往返总耗时不占优但如果你只需要压一次解多次稳定在体积上的优势依然很值。7. 关于压缩算法我想多说两句压缩不是越高越好也不是越快越好最终要看你站在系统的哪一端。存储型系统备份、归档、数据仓库更关心体积可以选择高压缩档位实时型系统消息队列、缓存、Web响应更关心延迟和吞吐低延迟和低CPU占用才是第一优先级传输型系统HTTP响应、CDN资源介于两者之间离线压一次再分发给海量用户是成本收益最理想的方案。我在实际项目里最常用的一组组合是日志归档用zstd -T0 -15消息队列压缩用zstd -3Web静态资源用brotli -q 11预压缩临时文件交换用lz4。这套组合几乎没有翻过车也基本覆盖了日常开发中90%的压缩需求。最后分享一个调优时比较顺手的小技巧如果你不确定某个压缩配置在你们生产数据上的效果不要凭感觉拍板直接在生产环境随机的几份真实样本上跑一轮“压缩率 耗时”对比几分钟就能出结论。压缩算法这事的benchmark永远是“你的数据 你的机型”说了算其他任何人给你的经验值都只能当参考。