tar 命令本身不是瓶颈,瓶颈往往在压缩环节。我最早意识到这个问题,是在一台 32 核的服务器上打包一个将近 50GB 的日志目录,结果tar -czvf硬生生跑了快半个小时,CPU 占用却不到 10%,几乎全程单核在扛。那时候我第一反应是“磁盘太慢”,后来排查完才发现,真正卡住的是 gzip 的单线程实现。
这篇文章就围绕“tar 多线程加速”这件事,把我踩过的坑、对比过的方案、最后在用的脚本全部整理出来。适合谁看?运维、后端开发、数据分析师,只要你有在 Linux 服务器上频繁打包、解压大目录的痛点,这篇都能给你一套可以直接抄的作业。内容不绕弯子,全部基于 Linux 服务器上的实际场景。
1. tar 到底慢在哪:先搞清楚瓶颈再谈加速
1.1 tar 的“打包”和“压缩”其实是两件事
很多人用tar -czvf用得熟练,但未必意识到 tar 和 gzip 是两个独立的程序。tar干的事情是归档:把一堆文件合并成一个流,同时保留权限、属主、目录结构等元信息。它本身不做压缩,所以你会发现tar -cvf的时候,CPU 占用很低,速度完全取决于磁盘读写能力。
-z参数的意思,是让 tar 在输出归档流的时候,再调用 gzip 做压缩。这个 gzip 默认实现是单线程的,压缩时每次只处理一个数据块。现代服务器动辄十几二十个物理核,gzip 只用其中一个,自然成了整个链路的瓶颈。
我做过一个简单测试,同样一个 8GB 的 MySQL 备份目录,纯tar -cf只花 3 分钟左右,加上-z之后反而花了 15 到 20 分钟。磁盘没变,CPU 也没满,时间全耗在了单线程压缩上。
1.2 解压同样受限于单线程解压器
和压缩类似,tar -zxvf里的-z也是让 tar 调用 gzip 解压。解压同样吃 CPU,如果是单线程 gzip 解压,照样只能跑一个核。我曾经在一台 12 核的机器上解压一个 40GB 的 tar.gz 压缩包,大概用了 13 分钟,但top看 CPU 总和才 8%,其他核全闲着。
要判断瓶颈是 CPU 还是磁盘,方法很简单:解压时开个top看指标,再同时用iostat 1观察磁盘利用率。如果 CPU 单核已满但整体很低、磁盘利用率不到 60%,那瓶颈就是压缩/解压算法的单线程;如果磁盘利用率已经接近 100%,说明磁盘本身带不动,这时候开再多线程也没用。
这句话我反复讲给团队听:多线程加速只解决 CPU 单核瓶颈,不解决磁盘瓶颈。搞清楚这条,后面的方案才不会用错。
2. 换压缩器:给 tar 接上 pigz 这类并行实现
2.1 pigz:gzip 的并行方案,对 tar 来说是无痛替换
pigz 的定位很明确:gzip 的并行实现,完全兼容 gzip 的压缩格式。因为格式兼容,生成的 tar.gz 包跟传统 gzip 压缩出来的包没有任何区别,下游解压工具不需要任何改动。
我当时测试的第一个命令就是:
tar -cf - /path/to/dir | pigz -p 8 > backup.tar.gz这一步直接把压缩从单核变成 8 线程,压缩耗时从原来的 20 多分钟降到了 8 分钟左右。CPU 利用率明显起来了,磁盘利用率也跟着上升,整个流程终于“跑满”了。
更简洁的写法是借助 tar 的--use-compress-program参数(或者-I短参数),让 tar 自己托管 pigz:
tar -I pigz -p 4 -cf backup.tar.gz /path/to/dir这里-p 4是传给 tar 的,但实际同时也会被 pigz 作为线程参数使用,写法上很容易混淆。为了方便控制线程数,我习惯用管道形式,因为能明确把-p 8交给 pigz,避免歧义。
2.2 pbzip2 和 pxz:高压缩率场景的并行选择
gzip 压缩率不算高,如果追求压缩率更低(结果文件更小),传统做法是用 bzip2 或 xz,但这两个默认也是单线程,压缩速度惨不忍睹。并行版本分别是 pbzip2 和 pxz。
先看实际参数的直观对比(同一份 5GB 日志目录,8 线程环境实测):
| 压缩方式 | 命令 | 压缩耗时(约) | 压缩后大小(约) | 适用场景 |
|---|---|---|---|---|
| gzip 单线程 | tar -czf | 15 分钟 | 1.4GB | 兼容性要求极高 |
| pigz 8 线程 | tar -cf - | pigz -p 8 | 5 分钟 | 1.4GB | 日常备份,速度快 |
| pbzip2 8 线程 | tar -cf - | pbzip2 -p 8 | 12 分钟 | 1.1GB | 压缩率优先,可接受慢 |
| pxz 8 线程 | tar -cf - | pxz -T 8 | 14 分钟 | 1.0GB | 追求极限压缩率 |
但有一点要留意:pbzip2 和 pxz 解压时,你的服务器也必须装对应的并行工具,否则单纯用tar -xjf调起的还是系统默认的 bzip2/xz,单线程解压一样会慢。更麻烦的是,有些老系统默认没有 pxz,可能会提示找不到命令。
我现在的主力方案是:日常日志备份、临时打包用 pigz(兼容 gzip,省心);需要长期归档、对存储空间敏感的任务用 pxz,并且会在解压说明里标注需要安装 pxz。
3. 小文件海量场景:换个思路,用 xargs 并行打包
3.1 单流压缩的限制:一个归档流始终只有一个压缩器在跑
前面讲的是把“压缩器”换成并行版本,但这个方案有一个天然上限:不管多少个核,一个 tar 归档流在某一时刻只能由一个进程写。pigz 能提升单流压缩速度,但打包本身(读取大量小文件的 inode、维护目录结构)是串行的,所以当文件数量达到几十万甚至上百万时,你可能会看到 CPU 还在忙,但磁盘的随机读已经成为新瓶颈。
这种情况,思路就得换了——不要试图加速“一个 tar 包”,而是让多个 tar 进程同时打包不同的文件子集,最后再合并。
3.2 用 find + xargs -P 做并行包的实操方案
我最早看到tar|xargs这种关键字的时候,第一反应也是拿 xargs 直接并行追写一个 tar 包,后来发现这并不安全。多个 tar 进程同时-rf追加写同一个归档文件,文件偏移会互相覆盖,轻则包损坏,重则目录树错乱。
更稳的方案是分片打包,最后统一合并。比如我有 120 万个日志文件,分布在 20 个业务目录下,实际操作时按目录维度并行:
mkdir -p /backup/parts find /data/logs -mindepth 1 -maxdepth 1 -type d | xargs -P 8 -I {} tar -cf /backup/parts/{}.tar -C /data/logs {}这条命令会把/data/logs下面的第一级子目录一一打包,-P 8表示最多 8 个 tar 进程同时跑。每个目录独立打成一个 tar,之后需要的时候再合并或者直接拆分使用。
如果不想拆成多个文件,合并时把同一份 tar 包的各个分卷统一解压到一个临时目录,再重新打包:
find /backup/parts -name '*.tar' | xargs -P 8 -I {} tar -xf {} -C /tmp/allinone tar -I pigz -p 8 -cf /tmp/final.tar.gz -C /tmp/allinone .注意这里分了两步:第一步是并行解包多个独立包,第二步是重新串行压缩成一个完整包。第一阶段的并行解包速度非常可观,因为每个 tar 进程只处理一部分文件,没有锁竞争。临时目录的写入压力会增大,但对 SSD 服务器来说这个开销可以接受。
这种“并行打散、再合并”的做法,适合以下场景:
- 文件数量极多,单个 tar 包的打包时间被 inode 遍历拖慢
- 需要按子目录拆分归档,方便做增量备份或单独恢复
- 机器内存不太够,没法一次把所有文件读进缓存
- 磁盘随机读性能一般,但顺序写性能很好
4. 实战脚本:一个大目录的 tar 多线程完整加速流程
4.1 从零到一:一条命令和它背后的参数计算
我在生产环境用的脚本,核心思路很简单:先判断机器有多少物理核,再决定 pigz 开多少线程,同时给磁盘 IO 留出缓冲。
#!/bin/bash SRC_DIR=${1:-/data/applogs} DEST_FILE=${2:-/backup/applogs-$(date +%Y%m%d-%H%M%S).tar.gz} THREADS=${THREADS:-$(nproc)} # 计算内存上限:假设每个压缩线程占用约 100MB 内存 MAX_MEM_GB=$(free -g | awk '/Mem:/{print $7}') LIMIT_BY_MEM=$((MAX_MEM_GB / 2)) if [ "$THREADS" -gt "$LIMIT_BY_MEM" ]; then THREADS=$LIMIT_BY_MEM fi tar -cf - "$SRC_DIR" | pigz -p "$THREADS" > "$DEST_FILE" ls -lh "$DEST_FILE"为什么要用nproc拿物理核数而不是逻辑线程数?因为超线程环境下逻辑核翻倍,但每个核心的计算资源共享,pigz 开太多线程收益有限,反而可能因为上下文切换增多拖慢速度。我更建议直接看/proc/cpuinfo里的cpu cores字段,或者直接用lscpu确认物理核数。
LIMIT_BY_MEM那段是我后期加上去的。pigz 每个线程默认会分配约 128KB 的滑动窗口,再加上系统缓存,线程数开太猛时内存确实会有压力。对于生产环境的备份机,我通常限制线程数不超过物理核数,最多再留 1 个核给其他任务。
4.2 解压端如何同步提速
压缩端提速了,解压端也要跟上。很多人下载了一个我用的脚本,结果解压时还是老命令tar -zxvf,发现解压依旧慢,就以为自己没成功。实际上,用 pigz 压缩出来的包虽然格式是标准 gzip,但解压时用的是系统默认的单线程 gzip,自然快不起来。
解压端提速命令:
tar -I pigz -xzf /backup/applogs-20250101-120000.tar.gz -C /data/restore或者用管道写:
pigz -d -p 8 -c /backup/applogs-20250101-120000.tar.gz | tar -xf - -C /data/restore如果压缩包是 pbzip2 或 pxz 生成的,对应解压命令:
tar --use-compress-program=pbzip2 -xf archive.tar.bz2 -C /data/restore tar --use-compress-program=pxz -xf archive.tar.xz -C /data/restore核心逻辑是一样的:不要用默认单线程解压器,而是显式让 tar 调用并行版本。
另外提醒一个小细节,tar -I pigz这种方式要求 tar 版本不低于 1.29(RHEL 7 自带的 tar 老版本可能不支持-I,但支持--use-compress-program)。如果你在旧系统上执行遇到tar: invalid option -- 'I',就用管道形式替代。
5. 对比实测和选型建议:不同工具的使用边界
5.1 九种方案在同一测试环境下的表现
为了把选型说清楚,我在一台 8 核 16 线程、1TB NVMe SSD、内存 32GB 的机器上,对同一份 9.6GB 日志目录(约 60 万个文件)做了对照测试,结果如下:
| 方案 | 命令示意 | 耗时(约) | 包大小(约) | 说明 |
|---|---|---|---|---|
| tar + gzip 单线 | tar -czf | 22 分 40 秒 | 3.1GB | 基准线 |
| tar + pigz 8 线程 | tar -cf - | pigz -p 8 | 7 分 55 秒 | 3.1GB | 综合首选 |
| tar + pigz 16 线程 | tar -cf - | pigz -p 16 | 7 分 20 秒 | 3.1GB | 提升有限 |
| tar + pbzip2 8 线程 | tar -cf - | pbzip2 -p 8 | 13 分 12 秒 | 2.5GB | 压缩率更好 |
| tar + pxz 8 线程 | tar -cf - | pxz -T 8 | 16 分 05 秒 | 2.3GB | 更小更慢 |
| 纯 tar 不压缩 | tar -cf | 2 分 30 秒 | 9.6GB | 磁盘瓶颈 |
| 并行 tar 分片 | find | xargs -P 8 | 2 分 15 秒 | 9.6GB | 不压缩 |
| 并行分片 + pigz | 分片后再并行压缩 | 6 分 40 秒 | 3.1GB | 海量小文件推荐 |
| zstd 多线程 | tar -cf - | zstd -T8 | 4 分 20 秒 | 2.9GB | 更强性能 |
数据是参考值,不同磁盘负载、文件大小分布都会带来差异,但相对关系是稳定的。看到这张表的时候我挺感慨:gzip 单线程是真的慢,比 pbzip2 和 pxz 慢不是一点点。
5.2 选型建议:备份场景该怎么挑
先说结论,再说理由。
如果只是日常打包转移文件,首选pigz,理由有三点:格式兼容 gzip、性能提升明显、团队成员不需要额外学习。压缩率不是最优,但日常传输场景里,快比少几 MB 更有价值。
如果次月要把冷数据归档到对象存储,我会用pxz,毕竟少十个百分点就是实实在在的成本。代价是压缩和解压都要装 pxz,且耗时更长。
如果每次都是几十万级别的小文件日志,单流 tar + pigz 已经没有多少优化空间,这时候直接用find + xargs并行分片,先解耦文件系统瓶颈,再考虑多层压缩。
如果服务器硬盘很贵的云环境,我还试过zstd,它压缩速度和压缩率都做得不错,而且-T参数天然支持多线程。虽然 zstd 不在 gzip 格式家族里,但现代 tar 版本已经内置支持--zstd,直接写tar --zstd -cf archive.tar.zst就行。但它的问题是,接收方也要支持 zstd,跨团队协作时我一般不会默认用它。
6. 常见问题与排查技巧实录
6.1 命令执行报错“Cannot exec pigz”
最常见的坑就是系统没装 pigz,或者 tar 调不到对应的可执行文件。
tar -I pigz -cf backup.tar.gz /data tar (child): pigz: Cannot exec: No such file or directory解决办法很简单:
yum install -y pigz # CentOS / RHEL 系 apt install -y pigz # Debian / Ubuntu 系装完先验证:
pigz --version如果在管道里用了pigz -p 8,但前端 tar 没有加任何压缩参数,注意不要多此一举地再写一个tar -czf,否则 tar 会先调用 gzip 做一遍压缩,再让 pigz 压缩一遍,性能反而下降。正确姿势是:用管道的时候,tar 只负责打流,压缩交给 pigz。
6.2 压缩包大小为什么和别人给的不一样
pigz 的压缩级别默认是 6,和 gzip 默认一致。但 pigz 支持--fast(级别 1)和--best(级别 9),另外还能用-1到-9指定压缩级别。
tar -cf - /data | pigz -p 8 -9 > backup.tar.gz级别越高,压缩率越好,耗时越长。有些同学想让文件更小,就把-9加上,结果发现时间多了一倍,收益却差不到 1%,这就不太划算。我一般在两种场景下调整级别:
- 临时传输,相对紧急:用
-1或-3,压缩率足够,速度飞快 - 归档保存,不急着传:用
-6默认,或者最多-7,不再往上
另外要留意:pigz 压缩出来的 tar.gz 虽然格式上是 gzip,但因为默认用了更大的压缩窗口或块大小,个别老旧的解压工具可能识别不了。用-z参数的时候,pigz 会尽量兼容 gzip 默认参数,如果你的包要传给很老的系统,建议加上-z:
tar -cf - /data | pigz -z -p 8 > backup.tar.gz6.3 多线程之后 CPU 上去了,但速度没变,甚至更慢
这个问题的排查思路,先看磁盘。iostat -x 1观察%util,如果已经到 90% 以上,大概率是磁盘写不进了。开更多的线程只会增加 IO 队列长度,情况反而更糟。
再看内存。如果服务器内存很小(比如 2GB),而线程开到了 16,每线程的缓存加上系统 page cache 压力,可能会导致 swap。看到free -h里 swap 使用量明显增加,就要把线程数调低。
还有一个隐蔽的问题是源目录很大但文件很小,比如大量 1KB 的日志。tar 打包流本身就要维护海量 inode,这个阶段 CPU 在做元数据整理,压缩线程反而在空转。此时你观察 pigz 的 CPU 会很低,tar 的 CPU 也不算高,但速度就是上不去。这种情况只能用分片并行,减少单进程的 inode 遍历压力,或者干脆换用不依赖 tar 元数据重组的方案(比如直接按目录结构 rsync 传输)。
6.4 解压报错“unexpected end of file”
这个通常说明 tar 包本身不完整,多半是压缩过程中磁盘满了,或者管道中间某一段被信号打断了。排查方法:
gzip -t backup.tar.gz如果 gzip 测试通过,说明压缩包完整,问题在解压过程。如果 gzip 测试报错,那只能重新生成压缩包。多线程压缩比单线程更容易在磁盘满时“静默失败”吗?其实不会,管道中 pigz 写完时 tar 可能还在写,如果磁盘满,tar 会报write error,但人不在现场很容易忽略。所以我建议脚本里加一句:
set -o pipefail这个选项保证管道中任意一段失败,整个命令都会返回非零退出码。不加的话,前面的 tar 失败了,管道最后可能返回 pigz 的 0,脚本就会“成功”退出,生成一个残缺包。这是我在生产环境踩过一次大坑后补上的。
7. 一点个人体会:多线程加速并非万能,但确实是最具性价比的第一步
我在实际使用中养成的习惯是:任何 tar 开销超过 5 分钟的任务,先看一眼top,只要确认是 CPU 单核受限,立刻切到 pigz 或 zstd。这个动作不复杂,却能把一个半小时的备份流程压到半小时以内。
最后分享一个小技巧:不要只优化一条命令,把优化固化到脚本里。我会在服务器/usr/local/bin下放一个fasttar包装脚本,内容就是前面第 4 节那段逻辑,这样团队里任何人要打包就直接fasttar /data /backup/x.tar.gz,不会有人再绕回去敲tar -czvf。多线程加速这件事,真正的门槛不在命令本身,而在于每次都记得去用它。