ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

tar压缩太慢?用pigz和zstd多线程并行压缩,备份提速8倍

tar压缩太慢?用pigz和zstd多线程并行压缩,备份提速8倍 1. 单线程瓶颈tar 的压速不快根因不在 tar 本身1.1 打包和压缩其实是两个阶段很多人第一次优化 tar 速度时习惯性把“tar 太慢”归类成工具问题其实 tar 自己很冤枉。tar 的核心职责是打包读文件元数据、把内容按顺序拼接成归档流这个阶段本质是 I/O 密集型的。真正拖慢整体速度的是压缩也就是-z参数背后的 gzip。我在给一台老服务器做日志归档时9.6GB 的日志目录一条tar -zcvf logs.tar.gz /data/logs敲下去进度条慢到让人怀疑机器是否卡死。用top一看CPU 占用只有 12% 左右而且集中在单核。这种表现非常典型tar 在单线程写归档流gzip 在单线程做压缩两头都是一个人在干活机器剩下的核心全在旁边看热闹。关键在于gzip 使用的 DEFLATE 算法本身是高度串行的。它靠 LZ77 滑动窗口和 Huffman 编码来压缩数据前面 32KB 的历史内容会作为后续匹配的参考字典每一块数据的压缩结果都依赖前一块的状态。这类有全局依赖的算法很难像把文件切碎后各自压那样直接并行。很多人只知道 gzip 慢但不知道为什么慢理解这一点后你就明白“多线程加速 tar”的核心思路不是去改 tar而是换掉 gzip 这个压缩器。1.2 用两条命令快速判断瓶颈在哪在动手引入任何并行工具之前我建议先做一次瓶颈检测。方法很简单把打包和压缩分开测# 只打包不压缩输出丢给空设备 time tar -cf /dev/null /data/logs # 打包并压缩同样丢给空设备 time tar -czf /dev/null /data/logs如果第一条命令耗时已经很高说明瓶颈在磁盘读取、文件数量、目录遍历这些环节压缩算法快不快影响不大。如果第一条很快、第二条突然变成几十倍的时间才说明压缩环节是主要矛盾并行压缩工具才有意义。我在机械硬盘和 NVMe 固态上分别跑过这个测试结果差异非常明显。机械硬盘上单纯打包含大量小文件的目录时I/O 等待时间能占到 70% 以上这时候即使把压缩换成 zstd 这类极快算法整体提升也有限。而在 NVMe 固态上纯打包 9.6GB 大文件只要十几秒加上 gzip 压缩后立刻变成四五十秒CPU 立刻成为瓶颈这才是多线程压缩的主场。所以先定位瓶颈再优化是我做所有性能调优的第一条原则。不要一上来就堆线程否则可能花了大力气最后发现磁盘才是那个拖后腿的。2. 并行压缩工具怎么选pigz、pbzip2、pxz 与 zstd 的取舍2.1 四类并行工具横向对比替换 gzip 的成熟方案不少常见的是这四种pigz、pbzip2、pxz或新版 xz 自带多线程以及 zstd。它们解决的问题相同但算法特点、压缩比、兼容性各有差异。工具对应传统工具并行原理压缩比解压速度常见参数pigzgzip分块并行 DEFLATE输出与 gzip 完全兼容与 gzip -9 相当快且并行-p 线程数pbzip2bzip2按块并行 BWT高于 gzip低于 xz快-p 线程数pxz / xz -Txz按块并行 LZMA最高相对慢-T 线程数zstd自定义算法原生多线程字典与块并行略高于 gzip速度远超极快--threadsNlz4自定义算法极低压缩比超高吞吐很低极快适合临时打包选型时我通常会先确认一个问题这个压缩包要在哪里解压如果只在 Linux 服务器之间传输zstd 和 pigz 都可以如果对方环境非常老旧或者包要交给其他团队、客户pigz 是最稳妥的选择因为它压缩出来的文件和 gzip 格式一致目标机器上即使没有安装 pigz用常规tar -xzf也能正确解压。这点很关键我踩过 zstd 高压缩级别版本不兼容的坑后面细说。2.2 pigz 的兼容性为什么重要pigz 的全称是 Parallel Implementation of GZIP它的输出格式和 gzip 没有区别。系统没有 pigz 时tar -xzf能解系统有 gzip 但没有 pigz 时也能解。这意味着你可以放心地在自己的机器上用 pigz 打包把.tar.gz文件丢给任何一台标准的 Linux 机器对方不需要安装额外软件。实际使用中pigz 的常用方式有两种# 方式一通过 --use-compress-program 指定压缩器 tar --use-compress-programpigz -p 8 -czf logs.tar.gz /data/logs # 方式二管道方式灵活度更高 tar -cf - /data/logs | pigz -p 8 logs.tar.gz方式二是我个人更喜欢的因为可以在中间插入pv命令实时查看进度也可以更方便地控制压缩器参数。对应的解压命令是pigz -dc logs.tar.gz | tar -xf -注意tar -xzf解开 pigz 压缩的包没问题但如果你想在解压时也享受多线程加速就得用管道方式调用pigz -dc因为-z参数调用的 gzip 依然是单线程的。2.3 zstd 是新一代备份利器但别忽略兼容性如果机器上能自由安装工具我强烈建议试试 zstd。它最大的优势是速度极快同时压缩比还不差。我用同一个 9.6GB 日志目录测试zstd 在压缩率接近甚至略高于 gzip 的情况下速度快了好几倍解压速度更是碾压 gzip。对需要频繁打包、回滚、传输的场景来说节省的时间非常可观。zstd 的并行参数也很简单tar -cf - /data/logs | zstd -T8 -3 -o logs.tar.zst-T8表示 8 线程-3是压缩级别级别越低速度越快体积越大。生产环境我一般用-5到-8之间太高级别收益递减明显。但 zstd 有一个必须注意的问题压缩时指定的版本和参数决定了解压端的最低版本要求。低版本的 zstd 解不了高版本创建的包尤其是用了--long长窗口模式后兼容性更差。所以如果要跨环境分发我建议先用 pigz或者在对方环境里确认 zstd 版本后再用。3. 实测 9.6GB 日志目录各方案耗时与体积的真实差异3.1 测试环境与方法先说测试环境一台 16 核 32 线程的 Xeon 服务器NVMe 固态硬盘操作系统是 CentOS 7.9数据是 9.6GB 的应用日志大约 12 万个文本文件。这类数据非常典型——服务器日志、业务备份、容器镜像导出基本都是“文件多、单文件不大”的形态。测试命令统一用管道方式保证 tar 和压缩器是并行的两个进程time tar -cf - /data/logs | 【压缩器命令】 /dev/null压缩级别尽量选各自比较常用的档位比如 gzip 默认级别 6pigz 也用 6pbzip2 默认zstd 用 5。为了公平我把每种方案都跑三遍取平均值避免 I/O 缓存影响判断。3.2 实测结果方案耗时产物体积体积对比tar gzip 默认单线程46.2s2.13GB基准tar pigz -p 169.8s2.15GB0.9%tar pbzip2 -p 1638.5s1.62GB-23.9%tar xz -T16 -6112.6s1.24GB-41.8%tar zstd -T16 -55.7s1.98GB-7%单看耗时zstd 快得离谱只有 gzip 的八分之一左右pigz 排第二9.8 秒。体积方面xz 最小但耗时是 pigz 的十倍以上。pbzip2 则是个折中方案体积比 gzip 小不少速度也不算太差。3.3 结果分析不可能三角这组数据背后是一个典型的“不可能三角”速度、压缩比、兼容性最多同时满足两个。要速度 兼容性选 pigz体积略涨 1% 左右但谁都能解压。要速度 高压缩比选 zstd但目标机器需要对应新版本工具。要高压缩比 兼容性选 xz但对时间敏感的场景不建议。我的习惯是日常备份用 zstd反正机器上已经装了给外部客户或跨团队分发用 pigz避免对方环境问题归档存长期冷备用 xz 或 pbzip2空间比时间更值钱。当然这只是个人选择你可以根据自己业务的数据量级、传输带宽、存储成本来做决定。4. 直接可用的命令管道式多线程打包与自动备份脚本4.1 单目录打包的标准写法把前面的内容落地成可直接使用的命令我平时最常用的是这三条# 打包并多线程压缩显示进度 tar -cf - /data/logs | pv -s $(du -sb /data/logs | awk {print $1}) | pigz -p 8 /backup/logs-$(date %F).tar.gz # 解压并多线程解压 pigz -dc /backup/logs-2025-01-10.tar.gz | tar -xf - -C /restore # 指定压缩器参数的完整写法 tar --use-compress-programpigz -p 8 -czf /backup/logs.tar.gz /data/logspv能显示实时进度尤其在打包几个小时的超大目录时没有进度条会让人焦虑到怀疑进程卡死。du -sb是预估总字节数管道后段跟压缩器整体逻辑是“tar 读文件 → 管道 → 压缩器压缩 → 落盘”。这里有个细节-p 8不是越大越好我推荐先设为物理核心数而不是逻辑线程数。比如 16 核 32 线程的机器pigz 开 16 就好开 32 在超线程下提升有限反而可能因为内存带宽和缓存争抢造成性能波动。这个在第 7 部分还会展开。4.2 多个子目录并行备份的 xargs 写法当你需要一次性备份多个独立目录时不要把它们全塞进一个 tar 命令里等它慢慢跑。更好的方式是用 xargs 的-P参数按目录并行执行多个 tar 压缩任务find /data/services -maxdepth 1 -mindepth 1 -type d -print0 | \ xargs -0 -P 4 -I {} sh -c tar -cf - $1 | pigz -p 4 $1.tar.gz _ {}这段命令的作用是找出/data/services下所有一级子目录最多同时跑 4 个压缩任务每个任务内部用 4 线程压缩。注意_ {}的写法sh -c后面第一个参数会被当作$0所以_占位后{}才能以$1传给命令。直接去掉_会遇到$1取不到值的坑。这种并发方式比单进程快但有一个前提目标磁盘能够承受并发写入压力。如果是机械硬盘4 个并发任务同时写会导致磁头反复寻道整体时间反而可能变长。SSD 上则很稳。4.3 放进 crontab 的完整备份脚本实际生产中我用得最多的是下面这个脚本模板已经把多线程压缩、排除目录、日志记录和失败告警都集成进去了#!/bin/bash set -euo pipefail BACKUP_DIR/backup SOURCE_DIR/data/services DATE$(date %F) LOGFILE/var/log/backup.log # 使用 pigz 多线程压缩排除缓存和临时文件 tar --use-compress-programpigz -p 8 \ --exclude*.cache \ --exclude*.tmp \ --exclude${SOURCE_DIR}/session \ -cf ${BACKUP_DIR}/services-${DATE}.tar.gz \ ${SOURCE_DIR} ${LOGFILE} 21 # 生成校验文件用于传输后完整性校验 md5sum ${BACKUP_DIR}/services-${DATE}.tar.gz ${BACKUP_DIR}/services-${DATE}.tar.gz.md5 echo [$(date %F %T)] backup finished, size: $(du -h ${BACKUP_DIR}/services-${DATE}.tar.gz | cut -f1) ${LOGFILE}set -euo pipefail是脚本安全的基础。没有-o pipefail时一个经典的坑是tar 中途失败但管道后端的 pigz 还在正常工作整条命令返回码仍然是 0备份脚本就会“假装成功”日志里看不出任何问题等恢复数据时才发现包是坏的。加上pipefail后任何一环失败脚本都会立刻报错退出。5. 排除与增量比堆线程更先考虑的备份加速5.1 --exclude 的正确用法和常见误区很多人的备份体积之所以巨大是因为把大量不需要归档的临时文件、缓存文件、日志切片也一并打包了。与其花力气让压缩跑得更快不如先减少压缩包体积。tar 的--exclude参数可以帮你过滤掉这些内容。# 排除单个目录 tar --exclude/data/logs/cache -czf backup.tar.gz /data/logs # 排除一类文件 tar --exclude*.log --exclude*.tmp -czf backup.tar.gz /data/logs # 从文件读取排除列表 tar --exclude-from/etc/backup-exclude.list -czf backup.tar.gz /data/logs注意一个非常容易踩的坑--exclude必须放在要备份的路径参数之前。如果你写成tar -czf backup.tar.gz /data/logs --exclude*.log某些版本的 tar 不会报错但这个排除规则根本不生效。我亲眼见过有人因为这个顺序问题备份包里全是.log文件体积直接翻了好几倍排查了半天才发现是参数顺序导致的。排除规则写在--exclude-from文件里也很实用每行一条规则可以用通配符注释以#开头。维护一套长期稳定的排除规则比每次手动拼参数靠谱得多。5.2 增量备份另一条“加速”路径多线程压缩优化的是 CPU 利用率但如果数据总量本身巨大那么每次全量备份的时间天花板就在那里。这时候增量备份更值得考虑。GNU tar 支持--listed-incremental它通过一个快照文件记录文件状态变化第一次运行会生成全量备份之后再运行只备份变化的部分# 第一次全量备份 tar --listed-incremental/var/lib/backup/snapshot.snar \ -czf /backup/full-$(date %F).tar.gz /data/logs # 后续增量备份 tar --listed-incremental/var/lib/backup/snapshot.snar \ -czf /backup/incr-$(date %F).tar.gz /data/logs恢复时按“先全量后增量”的顺序解包即可。这个方案的真正价值在于大多数数据的日常变化量只有 1%-5%增量备份只需要压缩少量新增或变更的文件再配合多线程压缩原本需要一小时的任务能缩短到几分钟。不过增量备份有几个要注意的地方快照文件.snar一旦丢失后续增量包可能无法正确应用所以这个文件本身要妥善备份。其次如果业务数据有大量文件删除tar 增量备份默认不会把这些删除同步到备份中恢复出来的是一份“历史完整状态”。这个行为是否符合业务需求需要提前确认否则恢复时会发现预期外的旧文件。5.3 特殊场景docker save 出来的 tar 包怎么加速Docker 镜像导出是另一个经常遇到 tar 大包的场景。docker save产生的是一份未压缩的 tar 归档文件体积通常很大。有人直接将这个 tar 再压成.tar.gz速度慢因为 docker save 本身是单线程写文件的后续压缩又是单线程。正确的做法是分两步先docker save生成 tar 文件再用 pigz 对文件本身做并行压缩docker save my-image:latest my-image.tar pigz -p 16 my-image.tar注意pigz直接接文件名时压缩完成后会生成my-image.tar.gz并删除原文件所以命令执行前要确认磁盘空间足够。加载镜像时同样可以用管道配合多线程解压pigz -dc my-image.tar.gz | docker load如果你要导出多个镜像还可以用一个小循环配合后台任务并行处理但注意磁盘 I/O 的承受能力建议限制并发数。6. 踩坑记录多线程压缩中最容易翻车的四个场景6.1 并发跑去写同一个 tar 包产出直接损坏我曾经帮同事排查一个“备份包无法解压”的问题。他当时为了省时间在脚本里写了这样一段逻辑把多个目录用多个tar -czf进程同时跑然后想着反正输出路径不同应该没问题。结果发现目标磁盘正好是机械硬盘大量并发写入导致磁盘 I/O 排队严重部分进程写文件时因为等待超时被 kill生成了好几个残缺的 tar 包。更隐蔽的问题是有些脚本会用重定向把多个tar -cf的结果直接拼进同一个文件for dir in /data/*/; do tar -cf - $dir | pigz -p 4 all.tar.gz done这样做的本意是把多个目录的归档流合并成一个压缩包但实际会得到一个“多个独立 tar 文件头拼接在一起”的畸形文件。GNU tar 在解压时虽然能通过顺序读取解出部分内容但这个文件不是标准的单个 tar 包很多工具会拒绝处理或直接报错。正确做法是想办法把多个目录合并成一个归档流比如在 tar 命令后显式列出多个路径或者用tar -cf - dir1 dir2这种形式而不是用拼接。6.2 线程数开满反而更慢我第一次用 pigz 的时候脑子一热写了-p 32想着 32 线程肯定比 8 线程快。但实测下来32 线程的耗时反而比 16 线程多了接近 20%。后来查资料和分析才发现原因有两个一是这台机器的 32 是超线程逻辑核物理核只有 16 个超线程对分块压缩这类计算密集型任务帮助有限反而因为线程切换和内存带宽争抢增加了开销二是压缩过程中每个线程都要占一块内存缓冲区线程数增加后缓存命中率下降反而拖慢了速度。真实世界的性能曲线不是线性的而是“先上升、到达峰值、然后逐渐下降”的抛物线形。我的经验是线程数先从物理核心数开始再上下浮动测试找到自己的峰值点。对于大多数 Xeon 和 Ryzen 机器pigz 设成物理核数左右zstd 可以稍微多开一点xz 则要保守一些因为它的单线程块内存占用更大。6.3 高压缩级别兼容性问题zstd 版本差异一个我印象很深的线上事故我用 zstd 19 级压缩了一个生产包发给合作方后他们那边解压时直接报错“unsupported frame parameter”。查了半天发现是因为我在压缩时用了--ultra -22而这个参数在高版本才支持对方环境里的 zstd 版本过低识别不了这个新格式标记。从此以后我给自己定了一条规矩跨团队、跨公司分发包之前先确认对方环境的解压工具版本或者直接用 pigz 这种格式兼容性最好的方案。zstd 更适合自己团队内部、版本可控的场景。如果你确实要用 zstd 分发建议先查一下目标机器的zstd --version并且压缩级别控制在安全范围内。6.4 备份脚本的“假成功”问题这个问题我在 4.3 中提过但值得单独再强调一遍。管道方式虽然好用但有一个天然风险tar -cf - | pigz file.tar.gz这条命令里如果 tar 在打包中段因为文件被删除、磁盘报错而失败但 pigz 还在正常接收前面的数据并写完压缩输出整条命令的退出码可能是 0。解决办法就是set -o pipefail。在 bash 脚本里加上这一行后只要管道中任何一个进程失败整条命令都会返回非零状态码配合set -e就能在脚本里及时中断。另外我还会在备份完成后做一次gzip -t完整性校验或者用 md5sum 生成校验文件确保数据真正可用。7. 线程数与 I/O 的联动调优不是核越多越快7.1 怎么根据磁盘类型选择并发度很多调优建议只讲 CPU不讲磁盘但在真实的 tar 场景里磁盘 I/O 往往才是真正的天花板。我的经验可以总结成一个粗略的判断框架NVMe / 高性能 SSD单线程读写速度可能达到 1GB/s 以上CPU 压缩速度跟不上磁盘此时可以放心开满物理核心数甚至略高于物理核心数瓶颈在 CPU 不在磁盘。SATA SSD / 普通固态单线程 400-500MB/s使用 pigz 16 线程左右是甜点区再多线程提升很小。机械硬盘连续读写 150-200MB/s随机读写更差此时开 4 线程以上意义不大。并发任务数更应该限制在 1-2 个否则磁头寻道会拖垮一切。网络存储NFS / CIFS多线程压缩反而可能加剧网络延迟建议先看网卡吞吐和协议性能通常 4-8 线程足够。判断当前瓶颈的最快方法是看top和iostat。如果top里 compress 进程的 CPU 使用率几乎满核说明是 CPU 瓶颈可以加线程如果%waI/O wait很高说明磁盘已经忙不过来加线程只是让更多进程排队等磁盘。7.2 内存占用与压缩级别的联动线程数和压缩级别还会影响内存占用这点容易被忽略。pigz 每个线程的内存占用不算高几十 MB 级别16 线程一般没问题。但 xz 就不一样了尤其是高压缩级别下每个线程可能占用几百 MB 甚至上 GB 内存。如果你用xz -T16 -9去压缩一个大目录内存轻轻松松被吃满触发 swap 后性能直接崩盘。我的经验参数是xz 的线程数最多设为物理核数的一半压缩级别不超过 6。zstd 相对友好线程内存占用比 xz 低很多可以大胆一点但也要注意--long长窗口模式下内存消耗会显著上升。7.3 实际调参建议与一个小技巧结合多个项目的实测我给出一份比较稳妥的参考配置场景推荐工具线程数压缩级别日常备份追求速度zstd物理核数-5 到 -8跨机器传输兼顾兼容性pigz物理核数-6冷备归档追求最小体积xz物理核数一半-6临时打包越快越好lz4 / pigz -1物理核数低级别另一个小技巧是配合 nice 和 ionice 控制备份任务的优先级。如果你是在业务高峰期做备份建议用以下方式启动避免压缩进程抢掉线上服务的 CPU 和磁盘资源nice -n 19 ionice -c2 -n7 tar -cf - /data/logs | nice -n 19 ionice -c2 -n7 pigz -p 8 /backup/logs.tar.gzionice -c2 -n7表示使用 best-effort 类别的低优先级尽量不影响其他 I/O 请求。这个做法在共享服务器上尤其有用不会因为你跑一个备份把整台机器的响应都拖垮。最后说下我个人的实操感受多线程压缩不是万能的但确实是 tar 提速性价比最高的一步。先把tar换成tar | pigz或者tar | zstd大部分场景能立刻感受到差距再根据磁盘和内存情况定线程数最后用--exclude和增量备份把数据量本身降下来。这四层优化做完备份和迁移的效率基本不会差到哪去。
返回列表