2024 年同事发来一个 logs.tar.gz,让我看看里面的报错日志。我图省事,直接在自己的家目录 ~/ 下敲了 tar -xzf logs.tar.gz。回车那一刻还没反应过来,几十个文件就"哗"地一下散落在我家目录里——access.log、error.log、config.yml……和我自己的文件混作一团,我精心整理的目录瞬间一片狼藉,甚至有两个同名文件被直接覆盖。我盯着满屏的文件愣了半天:这个 tar 包里根本没有顶层目录,解压就是"原地爆炸"。这件事逼着我把 tar 这个天天用、却从来没真正搞懂的工具彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,日常用 tar 打包/解压
事故现象:
- 同事发来 logs.tar.gz,我在 ~/ 下直接解压
- 解压后几十个文件直接散落进家目录,没有任何顶层目录兜着
- 我自己的 config.yml 被压缩包里的同名文件【覆盖】了
现场排查:
# 1. 先别急着解压,看看这个包里【到底装了什么】
$ tar -tzf logs.tar.gz | head
access.log # ★ 第一层就是文件,没有目录!
error.log
config.yml
app/run.log
...
# —— 这种包业界叫 "tar bomb"(tar 炸弹):
# 解压会把内容直接铺在当前目录,炸得到处都是
# 2. 对比一个"规范"的包长什么样
$ tar -tzf good.tar.gz | head
myproject/ # ★ 第一层是一个目录,所有东西在它下面
myproject/access.log
myproject/error.log
# —— 解压只会多出一个 myproject/ 目录,干净利落
根因(后来想清楚的):
1. tar 解压默认就在【当前目录】展开,不会自己建目录。
2. 那个包打包时,是在内容目录里 tar *,没带上层目录,
于是包里第一层就是一堆裸文件 —— 解压必然铺一地。
3. 我解压前没有先 tar -t 看一眼内容,
更没有用 -C 指定一个干净的解压目录。
说到底:不是 tar 的错,是我对 tar 的解压行为一无所知。
修复 1:tar 的基本形态——c / x / t / f
# === tar 的命令结构:一个动作 + 选项 + 文件 ===
# tar 的核心是四个互斥的"动作字母",一次只能用一个:
# -c create 创建一个新归档(打包)
# -x extract 从归档里解出文件(解压)
# -t list 只列出归档内容,不解压(★ 解压前必用)
# -r append 往已有归档里追加文件
# 还有一个几乎永远要带的:
# -f file 后面紧跟归档文件名(f 必须放选项最后)
# === 打包:把一个目录归档成一个 .tar 文件 ===
$ tar -cf myproject.tar myproject/
# -c 创建 -f 指定输出文件 最后是要打包的目录
# === 解压:把 .tar 解开 ===
$ tar -xf myproject.tar
# -x 解出 -f 指定要解的文件
# === ★ 解压前先列内容:养成肌肉记忆的一步 ===
$ tar -tf myproject.tar
# -t 只列出包里有什么,一个文件都不动。
# 这一步能让你提前发现"tar bomb"、看清目录结构。
# === 加 -v 看详细过程 ===
$ tar -cvf myproject.tar myproject/ # 打包时打印每个文件
$ tar -xvf myproject.tar # 解压时打印每个文件
# -v 在排查"到底打进去/解出来了什么"时很有用。
# === 一个关键认知:c/x/t 三选一,f 后面接文件名 ===
# 记住 "f 永远紧贴文件名" —— 写成 -xf a.tar 没问题,
# 写成 -fx a.tar 也行,但别把 -f 和文件名拆开。
修复 2:压缩格式——z / j / J 与自动识别
# === tar 本身只"打包",不"压缩",压缩靠额外的选项 ===
# 纯 .tar 是把多个文件【拼】成一个,体积不会变小。
# 要变小,得叠加一个压缩算法:
# -z gzip .tar.gz / .tgz 速度快,压缩率中等(最常用)
# -j bzip2 .tar.bz2 压缩率较高,速度较慢
# -J xz .tar.xz 压缩率最高,速度最慢
# === 打包并用 gzip 压缩(日常首选)===
$ tar -czf logs.tar.gz logs/
# c 创建 + z 走 gzip + f 指定文件名
# === 打包并用 xz 压缩(要极致小体积,比如发版)===
$ tar -cJf release.tar.xz release/
# === 解压:现代 tar 能【自动识别】压缩格式 ===
$ tar -xf logs.tar.gz # 不写 -z,tar 自己认出来是 gzip
$ tar -xf release.tar.xz # 不写 -J 也能解
# 新版 GNU tar 加了 -a/自动探测,解压时压缩字母可省。
# 但打包时必须显式写 -z/-j/-J,否则它只打包不压缩。
# === 三种格式实测对比(同一份 200MB 日志目录)===
# 格式 体积 打包耗时
# .tar 200 MB 几秒(没压缩)
# .tar.gz 38 MB ~8 秒
# .tar.bz2 31 MB ~25 秒
# .tar.xz 24 MB ~50 秒
# —— 日常传输用 gz 够了;长期归档/分发包追求小,用 xz。
# === 想看压缩进度,配合 pv ===
$ tar -cf - logs/ | pv | gzip > logs.tar.gz
# tar -cf - 把归档写到标准输出,管道给 pv 看进度再 gzip。
修复 3:tar bomb——解压前先看,解压时指定目录
# === 这次的坑:"tar bomb" —— 包里第一层就是裸文件 ===
# 解压它,内容会【直接铺在当前目录】,和你已有的文件混在一起,
# 同名文件还会被悄无声息地覆盖。
# === 防御第一招:解压前永远先 -t 看一眼 ===
$ tar -tzf logs.tar.gz
access.log # ← 第一层是文件 = 危险,是 tar bomb
error.log
# 对比:
$ tar -tzf good.tar.gz
myproject/ # ← 第一层是单个目录 = 安全
myproject/access.log
# === 防御第二招:用 -C 指定一个干净的解压目录 ===
$ mkdir /tmp/unpack
$ tar -xzf logs.tar.gz -C /tmp/unpack
# -C 让 tar 先 cd 到那个目录再解压。
# 哪怕这个包是 tar bomb,炸的也是 /tmp/unpack,
# 炸不到你的家目录。★ 解压陌生包,一律先建空目录 + -C。
# === 防御第三招:--one-top-level 自动套一层目录 ===
$ tar -xzf logs.tar.gz --one-top-level
# GNU tar 的这个选项:自动建一个和包同名的目录(logs/),
# 把所有内容塞进去 —— 等于强制给 tar bomb"兜底"。
$ tar -xzf logs.tar.gz --one-top-level=mydir # 也可指定名字
# === 只解压包里的某个文件/目录 ===
$ tar -xzf logs.tar.gz error.log # 只解出 error.log
$ tar -xzf logs.tar.gz app/ # 只解出 app/ 这个目录
# 路径要和 tar -t 列出来的【完全一致】。
# === 打包时就别制造 tar bomb:带上顶层目录 ===
# 【错误】在内容目录里打包,包里全是裸文件:
$ cd logs/ && tar -czf ../logs.tar.gz *
# 【正确】在上层目录打包,把 logs 这个目录本身打进去:
$ tar -czf logs.tar.gz logs/
# 这样别人解压,只会规规矩矩多出一个 logs/ 目录。
修复 4:绝对路径的坑——Removing leading /
# === 用绝对路径打包,会埋下一个危险的雷 ===
$ tar -czf etc-backup.tar.gz /etc/nginx/
tar: Removing leading `/' from member names
# ★ 这行 warning 不是报错 —— 它在告诉你 tar 干了什么:
# 它把包里的路径从 /etc/nginx/... 改成了 etc/nginx/...
# (去掉了开头的斜杠,变成了相对路径)
# === tar 为什么要去掉开头的斜杠 ===
# 设想包里存的是绝对路径 /etc/nginx/nginx.conf,
# 那不管你在哪里解压,它都会去写【真正的 /etc/nginx/】,
# 直接覆盖系统的线上配置 —— 这太危险了。
# 所以 tar 默认把绝对路径转成相对路径来"自保"。
# === 危险的写法:强行保留绝对路径 ===
$ tar -czf bad.tar.gz -P /etc/nginx/
# -P / --absolute-names:保留开头的斜杠。
# 这种包一旦在别的机器解压,会【直接覆盖那台机器的 /etc】。
# ★ 除非你非常清楚自己在做什么,否则永远别用 -P。
# === 正确姿势:用 -C 切到上层,再用相对路径打包 ===
$ tar -czf etc-nginx.tar.gz -C /etc nginx/
# -C /etc:先 cd 到 /etc nginx/:相对它的相对路径
# 这样包里存的是 nginx/...,解压时落在【当前目录】下,
# 不会去碰系统的 /etc,安全可控。
# === 解压时也能强制"安全模式" ===
$ tar -xzf somepkg.tar.gz --no-same-owner --no-overwrite-dir
# 解压来历不明的包,可以加这类选项收紧行为。
# 更稳的还是修复 3 那招:建空目录 + -C 进去解。
# === 一句话总结这一节 ===
# 看到 "Removing leading /" 别忽略,它是 tar 在提醒你:
# "你用了绝对路径,我已经帮你转成相对路径了。"
# 正确做法是主动用 -C,而不是用 -P 去对抗它。
修复 5:排除、增量、权限与软链接
# === 排除不想打包的东西 ===
$ tar -czf src.tar.gz --exclude='*.log' --exclude='node_modules' src/
# --exclude 可以写多次,支持通配符。
# ★ 注意:--exclude 要写在【要打包的目录之前】才稳。
# 也可以把规则写进文件:
$ cat .tarignore
*.log
*.tmp
node_modules/
.git/
$ tar -czf src.tar.gz --exclude-from=.tarignore src/
# === 保留权限、属主(做系统级备份时重要)===
$ tar -czpf backup.tar.gz /data
# -p 保留权限位。解压时想完整还原属主属组:
$ tar -xzpf backup.tar.gz --same-owner
# 普通用户解压默认按自己的身份;root + --same-owner 才能还原属主。
# === 软链接:默认存"链接本身",-h 存"链接指向的内容" ===
$ tar -czf a.tar.gz mydir/ # 软链接原样打包成软链接
$ tar -czhf a.tar.gz mydir/ # -h 跟随软链接,把目标文件【实体】打进去
# 打包时如果链接的目标在包外,解压后就成了死链 ——
# 这种情况用 -h 把实体内容打进去更稳妥。
# === 增量备份:只打"变化的部分" ===
$ tar -czf full.tar.gz -g snapshot.snar /data # 第一次:全量
$ tar -czf inc1.tar.gz -g snapshot.snar /data # 之后:只打变化
# -g 指定一个"快照文件",tar 靠它记住上次备份的状态,
# 第二次只打包【新增/修改】的文件。适合每天增量备份。
# === 校验:打包后核对,解压前体检 ===
$ tar -czf data.tar.gz data/ && tar -tzf data.tar.gz >/dev/null && echo OK
# 打完包立刻 -t 过一遍,能解析成功才算这个包没坏。
$ tar -dzf data.tar.gz -C /原目录 # -d 把包和磁盘上的文件做 diff
# === 看包里有多少东西、占多大 ===
$ tar -tzvf data.tar.gz | wc -l # 包里文件数
$ tar -tzvf data.tar.gz | awk '{s+=$3} END{print s}' # 解压后总字节
修复 6:tar 打包备份的纪律
# === 这次事故暴露了用 tar 的随意,定几条纪律 ===
# === 1. ★ 解压任何包,先 tar -t 看,再决定怎么解 ===
$ tar -tzf 包.tar.gz | head
# 第一层是单个目录 -> 可以直接解
# 第一层是一堆裸文件 -> tar bomb,必须 -C 到空目录
# === 2. 解压陌生包,一律建空目录 + -C 进去 ===
$ mkdir /tmp/x && tar -xzf 包.tar.gz -C /tmp/x
# 永远别在家目录、/tmp 根、项目根这种地方直接解陌生包。
# === 3. 打包带上顶层目录,别在内容目录里 tar * ===
$ tar -czf logs.tar.gz logs/ # 对:别人解压只多一个 logs/
# 不要:cd logs && tar -czf ../x.tar.gz *
# === 4. 打包用相对路径,看到 "Removing leading /" 就改 ===
$ tar -czf x.tar.gz -C /上层 目标/ # 用 -C 切目录,而不是用 -P
# === 5. 一个稳妥的备份脚本骨架 ===
#!/bin/bash
set -euo pipefail
SRC=/data/www
DST=/backup
DATE=$(date +%F)
NAME="www-${DATE}.tar.gz"
# 打包(用 -C 切到上层,相对路径,排除垃圾)
tar -czpf "${DST}/${NAME}" \
--exclude='*.log' --exclude='cache' \
-C "$(dirname "$SRC")" "$(basename "$SRC")"
# ★ 打完立刻校验能否解析,坏包当场发现
tar -tzf "${DST}/${NAME}" >/dev/null || { echo "包损坏!"; exit 1; }
# 清理 7 天前的旧备份
find "$DST" -name 'www-*.tar.gz' -mtime +7 -delete
echo "备份完成: ${NAME}"
# === 6. 备份文件名带日期,别固定一个名一直覆盖 ===
# www-2024-05-14.tar.gz 这样,出事了还能回退到前几天的版本。
# === 7. 重要备份要校验、要异地 ===
$ sha256sum backup.tar.gz > backup.tar.gz.sha256 # 留校验和
# 备份只放本机 = 没有备份。务必再同步一份到别的机器。
命令速查
需求 命令
=============================================================
打包(不压缩) tar -cf x.tar 目录/
打包 + gzip 压缩 tar -czf x.tar.gz 目录/
打包 + xz(体积最小) tar -cJf x.tar.xz 目录/
★ 解压前先列内容 tar -tzf x.tar.gz
解压到指定目录 tar -xzf x.tar.gz -C /目标目录
自动套一层顶层目录 tar -xzf x.tar.gz --one-top-level
只解压包里某个文件 tar -xzf x.tar.gz 路径/文件
打包时排除 tar -czf x.tar.gz --exclude='*.log' 目录/
用相对路径打包(避绝对路径) tar -czf x.tar.gz -C /上层 目标/
增量备份 tar -czf x.tar.gz -g snap.snar 目录/
校验包能否正常解析 tar -tzf x.tar.gz >/dev/null
口诀:解压前必 tar -t 看一眼 -> 陌生包建空目录 + -C
-> 打包带顶层目录 -> 看到 Removing leading 改用 -C
避坑清单
- tar 解压默认就在当前目录展开,不会自己建目录,陌生包必须先 -t 看内容
- 包里第一层是裸文件就是 tar bomb,解压会铺一地并覆盖同名文件
- 解压来历不明的包一律先 mkdir 空目录再 tar -xzf 包 -C 空目录
- --one-top-level 能强制给 tar bomb 套一层目录兜底
- tar 只打包不压缩,要变小得叠 -z(gz)/-j(bz2)/-J(xz)
- 打包必须显式写压缩字母,解压时现代 tar 能自动识别可省略
- 打包带上顶层目录,别 cd 进内容目录 tar *,否则做出 tar bomb 害别人
- 看到 Removing leading / 是 tar 在把绝对路径转相对,应主动用 -C 而不是 -P
- -P 保留绝对路径极危险,会让包在别的机器直接覆盖系统目录
- 备份文件名带日期别固定覆盖,打完包立刻 tar -t 校验,并异地再存一份
总结
这次解压一个日志包引发的小事故,把我对 tar 那种"不就是个打包解压工具吗"的轻慢,彻底纠正了过来。出事那一刻我特别懊恼,因为我什么命令都没敲错——tar -xzf logs.tar.gz,标准得不能再标准,可就是这一条标准命令,把几十个文件直接糊了我一脸,还覆盖掉了我自己的同名文件。事后我冷静下来复盘,才发现问题根本不在那条命令上,而在于我对 tar 解压这个动作的"行为模型"一无所知。我一直下意识地以为,解压一个压缩包,理所当然会得到一个和包同名的目录,所有内容都规规矩矩待在那个目录里——这其实是 Windows 上很多解压软件惯出来的错觉。但 tar 不是这样,tar 的解压行为朴素到了近乎"原始"的程度:它会把归档里记录的每一个路径,原原本本地、相对于你【当前所在的目录】铺开来。如果这个包在打包时,第一层就是一堆裸文件,那解压的结果就必然是这堆裸文件直接散落在你当前的目录里,和你已有的东西混在一起,撞了名字的就被无声地覆盖。这种第一层是裸文件、解压即"原地爆炸"的包,业界有个很形象的名字,叫 tar bomb,tar 炸弹。理解了这一点,我才真正想清楚,正确的解压姿势里,有两步是绝对不能省的。第一步,是在解压之前,永远先用 tar -t 把包里的内容列出来看一眼。这个动作几乎不花时间,一个文件都不会动,但它能让你在动手之前就看清楚:这个包的第一层,到底是一个规规矩矩兜住一切的顶层目录,还是一堆即将炸开的裸文件。第二步,是对任何来历不明的包,都先 mkdir 建一个干净的空目录,然后用 -C 选项让 tar 切到那个空目录里去解压。这样一来,哪怕这个包真的是个 tar bomb,它炸开的范围也被牢牢限制在那个我专门为它准备的空目录里,炸不到我的家目录,炸不到我的项目,炸不到任何我在乎的东西。这次排查也顺带让我读懂了另一个我过去一直忽略、甚至有点烦它的提示——打包时那行 Removing leading /。我以前总把它当成一个无关紧要的 warning 一扫而过,这次才明白,它其实是 tar 在郑重地向我交代它做了一件什么事:当我用绝对路径去打包时,tar 出于安全考虑,主动把归档里记录的路径从绝对路径改成了相对路径,因为如果它老老实实存绝对路径,那这个包不管在哪台机器上解压,都会去写那台机器真实的系统目录,直接覆盖掉线上的配置——这是灾难性的。所以 tar 默认帮我"拆弹",而我要做的,不是用 -P 选项去强行对抗它、把这颗雷重新装回去,而是顺着它的思路,主动用 -C 先切换到上层目录,再用相对路径来打包。从这次事故开始,我给自己立下了几条关于 tar 的铁律:解压任何一个包之前,必须先 tar -t 看一眼里面装的是什么;解压任何来历不明的包,必须先建一个空目录再用 -C 进去解;自己打包时,必须带上顶层目录,绝不在内容目录里 tar * 制造出 tar bomb 去坑别人;做备份时,文件名一定带上日期,打完包立刻用 tar -t 校验它没坏,并且再多存一份到别的机器上——只放在本机的备份,根本不算备份。这次从一地狼藉的家目录出发,我最大的收获,是终于把 tar 从一个"我以为闭着眼都会用"的简单工具,重新认识成了一个有着自己明确而朴素的行为逻辑的归档工具——而用好它的全部秘诀,就藏在那两个最容易被跳过的动作里:解压前,先看一眼;解压时,进空目录。
—— 别看了 · 2026