见过不少开发者拿着 LZMA 的时候,第一反应是“压缩率确实牛”,第二反应就是“参数怎么这么多,到底该怎么设”。LZMA 这套解压缩编码算法,在 7-Zip 里默认格式是它,Linux 系统里最常见的.tar.xz底层也是它,就连很多软件安装包、固件镜像、日志归档,背后都是 LZMA 或者它的改进版 LZMA2。压缩能力比传统的 ZIP、GZIP 高出一截,代价是压缩过程更吃 CPU 和内存。这篇东西不打算从头推公式,就说一个实际写代码、做运维的人怎么把它真正用起来:算法到底在干什么、关键参数怎么理解、命令行和代码里怎么调,以及在真实项目里踩过的一堆坑。适合把 LZMA 当工具用、不想被文档绕晕、希望压缩和解压过程少出幺蛾子的开发者。
1. 先搞清楚 LZMA 到底是什么东西
1.1 名字拆开看,它其实干了三件事
LZMA 的全称是 Lempel-Ziv-Markov chain Algorithm,分成三块来理解就很简单了。
第一块是 LZ,也就是 LZ77 滑动窗口算法。它的思路是拿一份“已经处理过的历史数据”当词典,遇到重复出现的内容时,不重复存储,只记录“回看多远、匹配多长”这两个数字。这个方案对文本、日志、结构化数据特别有效,因为里面重复片段非常多。
第二块是 Markov chain,马尔可夫链。它干的事是预测下一个字节或下一个符号出现的概率。比如一段英文文本里,前面是 qu,下一个字母大概率是 e,概率模型可以给不同情况分配不同的 bit 长度,让高频情况占的空间更小。
第三块是区间编码,一种算术编码的变体。LZ77 输出的是匹配对和零散字面量,马尔可夫模型给这些输出分配概率,区间编码再根据概率把整串输出编码成几乎接近信息熵极限的二进制流。
这三块叠在一起,就是 LZMA 压缩率高得离谱的原因:LZ77 负责找重复结构,概率模型负责优化编码效率,区间编码负责把结果压到极限。我之前做过一次日志文件的压缩对比,同样一份 Nginx 访问日志,GZIP 压出来大概是原体积的 15%,LZMA 直接压到 7% 左右。数据越“有纹理”,这种差距就越明显。
1.2 LZMA 和 LZMA2 的关系
现在你用 7-Zip 的工具箱,看到的通常是 LZMA2,已经很少有人直接用原始 LZMA 了。LZMA2 不是另起炉灶,它是在 LZMA 基础上加了一个“分块”机制:把输入数据切成若干 chunk,每个 chunk 用 LZMA 压缩,chunk 之间可以独立设置参数,也能中途改字典。
这个分块带来的最大好处是,数据流可以被切成多个独立段,解压时可以只解某一个 chunk,不用从开头一路解到目标位置,并且多线程并行压缩成为可能。LZMA2 还支持“字典重置”和“未压缩块”,让压缩器遇到高度随机、压不下去的数据时直接原样存储,不浪费 CPU。
所以现在做新项目,基本都选 LZMA2,而不是老 LZMA。Python 的lzma标准库,以及 7-Zip 的默认设置,用的也都是 LZMA2。
1.3 为什么压缩时间总是比解压长这么多
使用 LZMA 过程中很多人会有一个体感:压缩半天,解压一瞬间。这不是错觉,而是算法设计本身造成的。
压缩器需要在滑动窗口里做大量搜索,试图找到最长的重复匹配,并且还要尝试不同的匹配策略,看哪种方案最终编码出来的位数最少。这个“最优匹配搜索”是非常耗时的过程,越追求极限压缩率,搜索越彻底,时间越长。
解压则是“照着指示干活”的过程:先解码区间编码,还原出 LZ77 的匹配对和字面量,再拿着匹配对去字典里拷贝数据。查找和拷贝都是线性操作,几乎不需要决策。所以 LZMA 解压速度通常比压缩快好几倍甚至一个数量级。
知道这个特性很重要。如果你手里的场景是“数据只压缩一次,但会被反复解压很多次”,比如应用每次启动都要读取发布包,那 LZMA 的性价比非常高。如果数据需要频繁来回压缩解压,比如临时缓存、中间传输,LZMA 不见得是最优选择,这时候考虑 ZSTD 或 GZIP 更合适。
2. 动手之前必须搞懂的参数
2.1 字典大小,影响压缩率和内存的元凶
LZMA 的字典(Dictionary Size)直接决定压缩时能在多大范围内查找重复内容。字典越大,能找到的重复片段越多,压缩率越高,但内存占用和计算开销也会同步上涨。
内存占用有个简单的计算规则:压缩端大约需要“字典大小 + 额外开销”,解压端大概需要一个字典大小的缓冲区,额外开销相对小一些。也就是说,如果你设了 64MB 字典,解压一个文件,进程可能就要留出接近 64MB 的内存。
实际项目里选多少字典,得看数据本身。假设你压缩的是大量短 JSON 记录,每条记录可能就几百字节,那 1MB 字典已经非常充裕。如果你压的是动辄几十 GB 的数据库导出文件,里面存在跨大范围的重复模式,那就值得给到 64MB 甚至更大的字典。
我个人比较推荐的原则是:字典不要超过待压缩数据平均块大小的 4 到 8 倍。比如每块数据 4MB,那字典 16MB 到 32MB 就够用了,再大边际收益很低,内存压力倒是直线上升。
2.2 压缩级别:从 0 到 9 的中间逻辑
LZMA 的压缩级别(Preset)通常从 0 到 9,数字越大,压缩时间越长,压缩率越高,但并不是线性增长。我用 7-Zip 压过一个接近 1GB 的文本文件,用-mx=6大概是 20 秒,用-mx=9直接涨到 3 分钟,体积差异却只有大约 2%。
所以基本可以认为:级别从 6 往上,是在花大量 CPU 时间换取很小的体积收益。日常通用场景,-mx=7已经属于甜点区间;批量处理大量文件时,我会降到-mx=5或-mx=6平衡效率。
LZMA2 的级别还有更细的粒度,比如 7-Zip 里可以指定-m0=LZMA2:dict=64M:lc=3:lp=0:pb=2:mf=bt4:mode=normal这种完整参数串,每一个字段都有明确的含义。不过一般使用者,记下“level 越大越慢越省空间”就够了。
2.3 几组容易混淆的模式参数
LZMA 家族里还有一些参数,我最早用的时候经常搞混,单独拿出来说一下。
lc是“字面量上下文位数”,意思是预测未匹配字节时参考前面多少个已解码字节的高位。默认是 3,对大多数数据已经很好。
lp是“字面量位置位数”,参考的是当前处理位置在字典中的对齐信息,默认 0。只有数据有明显的位置规律时才需要动它。
pb是“匹配距离位置位数”,控制匹配距离按多少字节对齐分组,默认 2。这个参数在二进制数据上偶尔有效果,文本上基本不用改。
还有两个模式选择:mode=normal和mode=fast。normal 模式压缩率更高,fast 模式速度快一些,但压缩率和普通 GZIP 拉不开太大差距。如果你想压得小同时又不想等太久,可以先试试 normal 加一个中等字典,不要轻易调 fast。
2.4 固实模式与多线程的影响
7-Zip 的.7z格式支持固实模式(Solid Archive),意思是所有文件被当成一整块连续数据来压缩。固实块带来的收益是,文件与文件之间的重复内容也能被利用,很多小文件放在一起压缩率能明显上升。
但固实的缺点同样明显:任何位置的数据损坏,都会导致整个固实块从损坏点开始无法解压;更新归档里的某一个小文件,也可能需要重新压缩整个固实块。所以对备份、分发这类场景,固实模式要谨慎。
多线程方面,LZMA2 支持按块并行压缩,所以现在多核 CPU 上用 7-Zip 压缩大文件时,指定线程数能显著提速。解压通常只能单线程,这是 LZMA 的固有特性,不是软件 bug。
3. 命令行实操:最常见的两种用法
3.1 7-Zip 命令行处理.7z文件
如果你的工作环境是 Windows 或跨平台环境,7-Zip 的命令行工具基本是处理.7z格式的标准方案。需要注意:Windows 上图形界面的 7-Zip 安装目录里,那个7z.exe才是真正的命令行程序;图形界面快捷键不算。
最常用的一组命令:
# 压缩,使用 LZMA2,级别 9 7z a -t7z -m0=LZMA2 -mx=9 archive.7z /path/to/input # 压缩,指定字典大小 64MB,4 线程 7z a -t7z -m0=LZMA2 -mx=9 -md=64m -mmt=4 archive.7z /path/to/input # 解压到指定目录 7z x archive.7z -o/path/to/output # 只查看列表,不实际解压 7z l archive.7z这里我实际用下来,-md参数指定字典大小是最容易出效果的参数,比单纯提高-mx更直观。同一个文件夹,包含几百个小文本文件,字典从 1MB 提到 32MB,压缩体积能下降不少,时间增长也还能接受。
压缩时如果数据里有大量已经压缩过的文件,比如.jpg、.mp4,建议直接别用 LZMA,改用-m0=Copy模式存储原文件,或者干脆换 ZIP 格式,否则 CPU 烧了,体积也没小多少。
3.2 Linux 下和 tar 打包配合
Linux 生态里,LZMA 最常见的是以.xz格式出现,也就是tar -cJf的J选项。很多人知道这么用,但不清楚背后的参数调整。
# 基础打包压缩 tar -cJf app.tar.xz /opt/app/ # 指定 xz 压缩选项,用字典 64MB 和极限模式 XZ_OPT="-9e --threads=0 --memory=4GiB" tar -cJf app.tar.xz /opt/app/这里的XZ_OPT环境变量会传给 xz 工具。-9e表示级别 9 加极致模式(extreme),--threads=0是让 xz 根据 CPU 核心数自动并行。--memory会限制 xz 最大可用内存,防止在内存受限的构建机直接把机器压到卡死。
解压.tar.xz通常就用tar -xJf,虽然解压速度比压缩快很多,但遇到大字典的归档,内存占用仍然需要考虑。我曾经在一台只有 1GB 内存的云主机上解压一个 64MB 字典的包,虽然最后成功了,但系统几乎进入了交换分区疯狂换页的状态,非常痛苦。
3.3 并发压缩大目录的一点心得
接着前面说,如果目录文件特别多,直接对目录压缩,固实模式会让所有文件进入同一个数据流,压缩率很高,但解压单文件需要从固实块开头找到目标文件。反过来,如果关闭固实模式或使用传统 xz 的独立流,每个文件单独压缩,解压快,但总体压缩率下降。
实际场景里我是这样取舍的:
- 发布安装包、固件镜像,要的是体积小,用固实模式,多线程压缩。
- 备份服务器日志、跨机房传输,要的是稳定和可重试,建议每个文件独立压缩,或者至少不要把所有文件揉进一个超大固实块,避免一处损坏全盘皆输。
用 tar 配合 xz 时,如果要给多个独立文件分别压缩但又要保持 tar 结构,可以先 tar 成一个无压缩归档,再用 xz 分块压缩,或者直接对每个文件做.xz再归档。按场景选就好,不用迷信哪一种。
4. 代码里调用 LZMA:Python 与 C/C++
4.1 Python 标准库lzma模块,最简单也最可靠
Python 从 3.3 开始就内置了lzma模块,不需要装任何第三方依赖。这个模块对普通开发者来说,是我最推荐的上手入口。
一次性压缩小数据的写法非常简单:
import lzma data = b"This is a log line with lots of repeated content. " * 1000 # 默认 preset=6,压缩效果和时间比较平衡 compressed = lzma.compress(data, preset=6) # 用自定义过滤器,指定 LZMA2 和 64MB 字典,级别 9 filters = [ { "id": lzma.FILTER_LZMA2, "preset": 9, "dict_size": 64 * 1024 * 1024, } ] compressed_high = lzma.compress(data, format=lzma.FORMAT_XZ, filters=filters) # 解压 decompressed = lzma.decompress(compressed_high) print(len(compressed), len(compressed_high), len(decompressed))这里有个特别容易踩的雷:lzma.compress的默认格式是FORMAT_XZ,生成的是.xz文件格式;而 7-Zip 生成的.7z格式,Python 标准库是不支持的。所以你在 Python 里压出来的文件,看起来只是一个.xz流,不是.7z归档。区别在于.7z本身是一个容器格式,里面可以包含目录结构、文件名、多个数据流,而FORMAT_XZ只负责一段原始字节流的压缩。
4.2 大文件流式压缩与读取
处理几百 MB 甚至几 GB 的文件时,不能一次性read()再compress(),内存会直接爆炸。正确做法是用lzma.open当成普通文件对象来用。
import lzma import shutil # 流式压缩:从 source 读一块,压缩后写进 target.xz with open("access.log", "rb") as src: with lzma.open("access.log.xz", "wb", preset=7) as dst: shutil.copyfileobj(src, dst, length=1024 * 1024) # 流式解压:逐块读出来处理 with lzma.open("access.log.xz", "rt", encoding="utf-8") as f: for line in f: # 逐行处理日志 process_line(line)shutil.copyfileobj内部会按固定块大小读写,配合length参数可以控制每次读多少字节。这个方式做超大日志转储非常稳,内存占用几乎恒定。
lzma.open的第二个参数可以传"rt"表示文本模式,这样可以直接按行迭代,对处理日志场景来说省去手动解码的麻烦。当然前提是文件本身是 UTF-8 编码,如果遇到 GBK 日志,需要先指定encoding或自己解码。
4.3 C/C++ 集成:LZMA SDK 的基本套路
在 Python 之外,C/C++ 项目要集成 LZMA,通常直接使用 LZMA SDK,它的核心 API 是LzmaCompress和LzmaUncompress,除此之外还有更底层但更完整的流式接口。
#include <LzmaLib.h> // 声明输入输出缓冲区 unsigned char inBuf[64 * 1024]; unsigned char outBuf[64 * 1024]; // 编解所需的属性头 unsigned char props[5]; size_t propsSize = 5; // 输出大小必须初始化为缓冲区容量 size_t outSize = sizeof(outBuf); size_t inSize = readInput(...); int ret = LzmaCompress( outBuf, &outSize, // 压缩输出缓冲和大小 inBuf, inSize, // 输入缓冲和大小 props, &propsSize, // 自定义属性,解压时要用 9, // 等级 64 * 1024 * 1024, // 字典大小 -1, -1, -1, // lc, lp, pb 默认值 -1, // 算法模式,-1 表示 normal -1 // 写结束标记,-1 表示默认 );C 接口最麻烦的地方在于:LzmaCompress会把一组内部参数编码进props这个 5 字节数组,而解压端必须拿到这 5 字节,才能正确初始化解压器。很多人第一次对接时,忘了把props存下来或传出去,解压端一直报参数错误。
如果你做一个自定义文件格式,建议把props作为文件头的一部分固定保存。或者偷懒的做法,固定编解码参数,比如写死“字典 16MB,level 7”,然后props每次一样,这样可以直接在解压端写死 5 字节内容。缺点是将来想升级参数就麻烦了。
补充一点,LZMA SDK 的 C++ 封装与 C API 行为和参数结构并不完全一致,用的时候留意一下版本。集成进大型项目时,最稳妥的方式是单独封装一个独立的解压缩模块,对外只暴露“输入字节流、输出字节流”的简单接口,内部再对接 SDK,这样后续换算法也方便。
5. 参数怎么调才不白折腾
5.1 一张表看懂常用级别
很多开发工具会在压缩级别后面加一个小写e,比如-9e。这个e代表“极限模式”,让压缩器花更多时间去搜匹配,压缩率略有提升,但时间可能成倍增加。以下是一份我自己常用的参考,基于中等规模的文本数据统计出来的,具体值会因为机器和数据特征有浮动。
| 级别 | 字典大小 | 相对压缩时间 | 压缩率 | 适用场景 |
|---|---|---|---|---|
| 1 | 1MB | 极快 | 较低 | 临时缓存 |
| 3 | 4MB | 快 | 中下 | 中小文件备份 |
| 6 | 8MB | 中等 | 中上 | 通用默认 |
| 7 | 16MB | 较慢 | 高 | 大多数发布包 |
| 9 | 64MB | 非常慢 | 很高 | 极限压缩、离线归档 |
| 9e | 64MB | 接近无法等待 | 最高 | 长期存储 |
注意,以上字典大小是参考值,不同的实现会有差异,比如 Python 标准库 preset=9 的字典大小是 64MB,而 7-Zip 的-mx=9在不同版本里字典也可能不同。所以要精确控制,还是显式指定dict_size更靠谱。
5.2 五类典型场景的参数建议
场景一:日志归档。数据量大、重复率高、对压缩速度有一定要求。建议用 preset 7,字典 16MB 到 32MB。如果日志按天滚动,每天一个文件,直接把文件一整个压缩即可。
场景二:软件发布包。要发给用户的安装包,压缩率最敏感,但也要考虑用户解压时的内存消耗。建议字典不超过 32MB,级别 7 到 9 都可以。用户机器配置未知时,别用超过 64MB 的字典,不然低配机器解压会卡死。
场景三:运行中的小缓存块。数据条数多、体积小、实时性要求高,用 LZMA 反而亏本,不如直接用 LZ4 或 ZSTD。如果一定要用 LZMA,选 preset 1 或 3,字典 1MB 就够。
场景四:虚拟机镜像或磁盘备份。数据内部会有大量的空闲块和重复分区,压缩率收益很高,但数据量极其庞大。建议用-mx=9 -md=64m开启多线程,压缩时间按小时计算也可以接受。
场景五:内存中数据序列化。比如把一个大对象压成字节流再放进内存缓存,这时要控制压缩耗时,可能只压 5MB 到 10MB 的小块,用 preset 6、字典 8MB 比较合适。再大就收益递减了。
5.3 自己动手做参数命中测试
与其听我建议,不如在你的真实数据上做一组对照测试。方法很简单:取一个具有代表性的样本文件,分别用不同级别和字典压一遍,记录压缩后体积、压缩耗时、解压耗时。
import lzma import time data = open("sample.log", "rb").read() for preset in [1, 3, 6, 7, 9]: t0 = time.time() comp = lzma.compress(data, preset=preset) t_comp = time.time() - t0 t0 = time.time() decomp = lzma.decompress(comp) t_decomp = time.time() - t0 print(f"preset={preset}: ratio={len(comp)/len(data):.3f}, " f"comp={t_comp:.2f}s, decomp={t_decomp:.2f}s")用这个结果画个简单表格,你对“这个参数组合在项目里值不值”就有了直接答案。别在样本过小或者临时拼接的假数据上测,结果会误导你。
6. 常见问题与排查技巧实录
6.1 解压报错字典参数不匹配
用 LZMA SDK 写自定义格式时,最容易遇到的就是“字典参数不匹配”的报错。问题根源通常不是字典大小本身,而是解压端没有使用当初压缩时写进属性头的 lc/lp/pb 值。
我在对接一个内部数据格式时,压缩端写死了lc=3 lp=0 pb=2,解压端用的却是默认的lc=0 lp=0 pb=0,结果前几百个字节能正常解出来,数据量一大就开始乱码和报错。排查了很久才意识到,问题不在字典大小,而在上下文参数。
解决思路:自定义格式的头部一定要带上完整的属性信息。最简单的做法,直接把 SDK 生成的 5 字节props存进文件头。解压时用LzmaProps_Decode解析这 5 字节,再初始化解压器,这样永远不会错。
6.2 内存占用失控
压缩一个 10GB 文件,发现机器内存飙到 4GB 以上,不一定是 bug,很可能是字典设太大了。LZMA 的字典本身占内存,多线程还会乘线程数。在 7-Zip 里设了 64MB 字典加 8 线程,压缩缓冲加线程工作内存,整体吃掉两三 GB 很正常。
排查方法:先试试单线程压缩,看内存下降幅度是否符合预期;如果还是高,调小字典。要记住,多线程压缩时,总内存占用大体等于“线程数 × 单线程工作集”,所以在内存受限的机器上并行压缩大文件,很容易触发内存不足。
给运维环境使用的建议是,在脚本里显式限制压缩工具的可用内存:
XZ_OPT="--threads=0 --memory=2GiB" tar -cJf archive.tar.xz /data6.3 压缩速度慢到怀疑人生
遇到压缩太慢,优先检查两个地方。
第一个是字典里面匹配搜索器的选择。LZMA 的mf参数可以设bt4、hc4等,bt4更快但需要更多内存,hc4更省内存但会慢一些。实际操作中,级别越高,匹配策略越复杂,慢是正常的。
第二个是数据特性。如果处理的是随机字节或多字节编码的国际化文本,LZMA 的优势会大幅缩水,压缩率提升有限,还白白浪费时间。这时换个思路,先用 ZSTD 测一下压缩速度和压缩率,很可能更合适。
如果确定就是要用 LZMA,最直接有效的优化是开多线程:
7z a -t7z -m0=LZMA2 -mx=7 -mmt=4 archive.7z input/四线程相对单线程通常能带来翻倍以上的压缩吞吐,压缩率几乎不受影响。
6.4 不同实现之间的兼容性问题
这个问题我见识过不止一次:在 A 环境用某个库压缩的.xz文件拿到 B 环境解不开。
最典型的原因是容器格式的差异。.xz和裸 LZMA 流是两码事。裸 LZMA 流没有容器头,解压时需要手动提供props参数;.xz格式的容器会自动携带这些信息。如果你把一段裸 LZMA 流保存为.xz后缀,很多解压工具会识别不了。
另一个常见原因是字典超出实现上限。比如某个语言的 LZMA 绑定只支持最大 64MB 字典,你用 128MB 字典压缩出来的流,放到这个环境解压就直接报错。所以跨平台、跨库传输压缩数据时,别用极限参数,64MB 以内字典基本不会遇到实现限制。
格式统一策略也很简单:所有对外交换的数据统一用.tar.xz,内部缓存可以用裸 LZMA 或 LZMA2 流,但一定要在配置里记录压缩参数。
6.5 元数据灾难:损坏后的可恢复性
LZMA 不像某些高级压缩算法那样具有强纠错能力,它的设计重点是压缩率。固实模式下,数据损坏的传播范围会更广,一个字节的错误可能导致后续一大段数据无法解压。
做长期归档时,我的习惯是给每个压缩包生成一个独立的校验文件,比如 SHA-256 哈希值,在传输前后校验。对特别重要的数据,宁可牺牲点压缩率,也把固实块切小,或者将文件分别压缩再归档,这样单块损坏不会拖垮整个包。
很多压缩工具本身也支持-p加密,但如果用了加密,任何一位数据错误都会导致整个文件解压失败,建议重要数据“先加密再压缩”和“先压缩再加密”的顺序要考虑清楚,前者会让压缩率下降,后者会让数据泄露风险略高,两者各有利弊。
一点收尾的个人经验
按我自己的习惯,LZMA 不是所有压缩问题的默认答案。它最强的地方是“一次性压缩、多次解压、对体积有极致要求”的场景,比如发布包、固件、归档、日志冷存储。而实时传输、临时缓存、超大流量过滤这类场景,LZMA 的速度短板就会盖过压缩率优势,ZSTD、LZ4 之类的算法反而更顺手。
最后分享一个小技巧:在你还没确定场景时,先别急着调参。拿真实数据在默认参数下跑一遍压缩和解压,记录体积和耗时,再往上提一档字典或级别,看收益曲线。通常一个半小时就能找到适合你数据特征的甜点参数。这套流程比我上面写的任何参数建议都更可靠。把 LZMA 当“工具”而不是“黑魔法”来对待,熟悉它的脾气,它就真能在你的项目里省下不少磁盘和带宽。