1. 压缩备份在运维工作中的真实定位
1.1 为什么压缩备份是绕不开的基本功
干运维这行十几年,我越来越觉得压缩和备份这两个动作,看着简单,实际上是最能拉开水平差距的地方。你随便问一个刚入行的朋友tar命令怎么用,他大概率能给你背出tar -czvf这个组合,但你要是问他“为什么生产环境里有时候反而不能用-z”“增量备份和差异备份在什么场景下选哪个”“打包的时候遇到文件正在被写入怎么办”,能答利索的人就不多了。
压缩备份这件事,本质上解决的是三个问题:空间、时间、可靠性。空间好理解,日志文件动辄几十个G,不压缩根本存不下;时间是说传输和恢复的效率,一个没压缩的数据库备份传到异地可能要几个小时,压缩之后可能只要几十分钟;可靠性则是最容易被忽视的——你备份出来的东西,到底能不能恢复?这个问题不解决,前面两个做得再好都是白搭。
这篇文章适合谁看?我觉得有三类人。第一类是刚接触Linux、需要把日常操作跑通的新手,你需要一套能直接抄的模板;第二类是已经会用命令、但没系统梳理过的中级运维,你需要搞清楚每个参数背后的取舍逻辑;第三类是要做备份方案设计的人,你得知道不同压缩算法的性能差异到底有多大,才能做出合理选型。
1.2 压缩与备份是两个动作,别混为一谈
很多人把压缩和备份当成一件事,其实它们是两个独立的动作,只是经常一起用而已。压缩解决的是数据体积问题,核心指标是压缩率和压缩/解压速度;备份解决的是数据副本问题,核心指标是完整性、可恢复性和版本管理。
你可以只备份不压缩,比如数据库的物理备份直接拷贝数据文件;也可以只压缩不备份,比如把一堆日志压成一个包然后删掉原文件——但后者严格来说不叫备份,因为你没有保留副本,压完删了原文件,一旦压缩包损坏就全没了。
我见过最典型的翻车案例,是有人用tar -czf backup.tar.gz /data把数据目录打包,然后rm -rf /data清理空间,结果解压的时候发现压缩包是坏的,因为打包过程中有个大文件正在被写入,tar读到一半文件变了,报了个warning但被忽略了。所以记住一句话:压缩包不等于备份,除非你验证过它能完整恢复。
2. 压缩工具选型:gzip、bzip2、xz到底怎么选
2.1 三种主流压缩算法的性能对比
Linux下最常用的三个压缩工具是gzip、bzip2和xz,它们背后用的是不同的压缩算法,性能差异非常明显。我做过一组实测,拿一个2.3GB的文本日志目录做测试,结果大致如下:
| 工具 | 压缩后大小 | 压缩耗时 | 解压耗时 | 压缩率 | CPU占用 |
|---|---|---|---|---|---|
| gzip -6 | 412MB | 38秒 | 12秒 | 约5.6:1 | 单核中等 |
| bzip2 -9 | 318MB | 2分15秒 | 48秒 | 约7.2:1 | 单核较高 |
| xz -6 | 246MB | 5分40秒 | 1分20秒 | 约9.3:1 | 单核很高 |
| zstd -3 | 385MB | 9秒 | 4秒 | 约6.0:1 | 多核友好 |
这张表里最值得说的是zstd,虽然它不在标题提到的传统三件套里,但近几年在备份场景里用得越来越多,因为它的速度优势太明显了——压缩比gzip还快,压缩率却接近bzip2。如果你的场景是“每天都要压、压完就要传”,zstd基本是首选。
那传统三件套怎么选?我的经验是:gzip用于日常快速压缩,bzip2基本可以淘汰,xz用于归档存储。为什么说bzip2可以淘汰?因为它夹在中间很尴尬——压缩率不如xz,速度不如gzip,CPU占用还不低。除非你有历史脚本依赖它,否则新方案里没必要用它。
2.2 压缩级别不是越高越好
每个工具都有压缩级别参数,gzip是-1到-9,xz是-0到-9,zstd是-1到-19。很多人想当然地觉得“级别越高越好”,直接上-9,结果压缩时间翻了好几倍,压缩率只提升了几个百分点。
这里有个边际效应的问题。以gzip为例,从-1到-6,压缩率提升很明显;但从-6到-9,压缩率可能只提升2%到3%,耗时却增加50%以上。所以gzip的甜点级别是-6,这也是默认值。xz的甜点级别在-6左右,再往上提升有限但内存占用会飙升——xz的高级别压缩需要大量内存,-9可能需要几百MB甚至上G的内存,在小内存机器上直接会OOM。
提示:压缩级别选择的核心原则是“看瓶颈在哪”。如果瓶颈是磁盘IO,压缩慢一点无所谓,可以上高级别;如果瓶颈是CPU或者要求快速完成,就选低级别。别盲目追求最高压缩率。
2.3 多核并行压缩的实战价值
单核压缩在现代多核服务器上其实是浪费。pigz是gzip的多线程版本,pbzip2是bzip2的多线程版本,pixz是xz的多线程版本。用法上,pigz基本可以无缝替换gzip,参数几乎一样,只是多了-p指定线程数。
我实测过,在一台8核机器上压缩同一个2.3GB目录,gzip -6用了38秒,pigz -6 -p 8只用了7秒,压缩后大小几乎一样。这个差距在每天都要备份的场景里非常可观。所以我的建议是:只要机器核数大于2,压缩就用多线程版本,pigz和zstd是首选,pixz在需要高压缩率时用。
不过要注意一点,多线程压缩对内存的消耗也会增加,每个线程都需要独立的缓冲区。在内存紧张的容器环境里,线程数别开太多,一般设成CPU核数的一半到全部之间就行。
3. tar打包的核心参数与实战组合
3.1 tar命令参数的分组记忆法
tar的参数多到让人头大,但其实可以分组记忆。我把它分成四组:
第一组:操作模式,决定tar要干什么。
-c:create,创建归档-x:extract,解压归档-t:list,列出归档内容-r:append,向归档追加文件-u:update,只追加比归档中更新的文件
第二组:压缩方式,决定用什么算法。
-z:gzip-j:bzip2-J:xz--zstd:zstd- 不加:纯打包不压缩
第三组:输出与文件,决定读写哪个文件。
-f:指定归档文件名,这个必须紧跟着文件名-C:切换目录后再操作
第四组:行为控制,决定细节行为。
-v:verbose,显示处理过程-p:保留权限--exclude:排除文件-T:从文件读取要打包的列表
记住这个分组,你看到任何tar命令都能快速拆解出它在干什么。比如tar -czvf backup.tar.gz -C /data .,就是创建(c)一个gzip压缩(z)的归档,显示过程(v),文件名是backup.tar.gz(f),先切换到/data目录(C),打包当前目录下所有内容(.)。
3.2 排除规则是打包的灵魂
生产环境打包最怕的就是把不该打的东西打进去。比如你打包一个应用目录,里面可能有node_modules、.git、日志文件、临时文件,这些打进去会让包体积暴涨,恢复的时候还可能覆盖掉目标环境的配置。
--exclude参数支持通配符,可以多次使用。我常用的排除组合是这样的:
tar -czf app_backup.tar.gz \ --exclude='*.log' \ --exclude='*.tmp' \ --exclude='.git' \ --exclude='node_modules' \ --exclude='cache/*' \ -C /opt/app .这里有个坑要注意:--exclude的匹配是相对于打包根目录的路径。如果你写--exclude='logs',它只会排除根目录下的logs,不会排除subdir/logs。要排除所有层级的同名目录,得用--exclude='*/logs'或者--exclude='logs'配合--exclude='*/logs'。
另一个更稳妥的做法是用-T配合排除列表文件,把要打包的文件明确列出来,而不是用排除法。排除法是“除了这些都要”,列表法是“只要这些”,后者更可控,尤其是在目录结构复杂的时候。
3.3 增量备份:tar的--newer参数与快照文件
tar本身支持简单的增量备份,靠的是--newer或者--newer-mtime参数,意思是“只打包比某个时间点更新的文件”。用法是这样的:
# 全量备份 tar -czf full_backup.tar.gz -C /data . # 记录时间戳 date +%Y%m%d%H%M%S > /var/backup/last_backup_time # 下次增量备份 tar -czf incr_backup.tar.gz --newer="$(cat /var/backup/last_backup_time)" -C /data .但这个方案有个明显缺陷:它依赖时间戳,如果文件的时间戳被修改过(比如从别处拷贝过来的),就会判断失误。而且恢复的时候必须按顺序先恢复全量再恢复增量,管理起来比较麻烦。
更靠谱的增量方案是用rsync配合硬链接做快照式备份,或者用tar的--listed-incremental参数。--listed-incremental会维护一个快照文件,记录每个文件的元信息,下次备份时对比快照判断哪些文件变了。用法:
# 首次全量 tar -czf backup_level0.tar.gz --listed-incremental=/var/backup/snapshot.snar -C /data . # 后续增量 tar -czf backup_level1.tar.gz --listed-incremental=/var/backup/snapshot.snar -C /data .注意,增量备份的恢复必须从level0开始按顺序恢复,不能跳。而且--listed-incremental对文件删除的处理不太理想——它不会记录删除操作,恢复增量后可能残留已删除的文件。所以这个方案适合“只增不减”的场景,比如日志归档。
4. 完整备份脚本的编写与落地
4.1 一个可直接复用的备份脚本框架
光会敲命令不够,生产环境需要的是可重复执行的脚本。我写一个自己用了很多年的备份脚本框架,你可以直接改成自己的版本:
#!/bin/bash set -euo pipefail # 配置区 BACKUP_SRC="/data/app" BACKUP_DST="/backup" BACKUP_NAME="app_$(date +%Y%m%d_%H%M%S)" RETENTION_DAYS=7 LOG_FILE="/var/log/backup.log" # 日志函数 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } # 检查源目录 if [ ! -d "$BACKUP_SRC" ]; then log "ERROR: 源目录 $BACKUP_SRC 不存在" exit 1 fi # 检查目标目录空间 AVAIL=$(df -m "$BACKUP_DST" | awk 'NR==2 {print $4}') SRC_SIZE=$(du -sm "$BACKUP_SRC" | awk '{print $1}') if [ "$AVAIL" -lt $((SRC_SIZE / 3)) ]; then log "WARN: 目标空间可能不足,可用 ${AVAIL}MB,源大小 ${SRC_SIZE}MB" fi # 执行备份 log "开始备份 $BACKUP_SRC -> $BACKUP_DST/$BACKUP_NAME.tar.zst" tar --zstd -cf "$BACKUP_DST/$BACKUP_NAME.tar.zst" \ --exclude='*.log' \ --exclude='*.tmp' \ --exclude='.git' \ -C "$(dirname "$BACKUP_SRC")" "$(basename "$BACKUP_SRC")" # 验证压缩包完整性 if tar --zstd -tf "$BACKUP_DST/$BACKUP_NAME.tar.zst" > /dev/null 2>&1; then log "备份完成,完整性校验通过" else log "ERROR: 备份文件校验失败" exit 1 fi # 清理过期备份 find "$BACKUP_DST" -name "app_*.tar.zst" -mtime +$RETENTION_DAYS -delete log "已清理 $RETENTION_DAYS 天前的备份"这个脚本里有几个关键设计点值得说。set -euo pipefail让脚本遇到错误就退出,避免错误被吞掉;空间检查是提前预警,不是硬性阻止,因为压缩后大小不好精确预估;最重要的是备份后立刻做完整性校验,用tar -tf列出内容,能列出来说明归档结构没坏。
4.2 备份验证:比备份本身更重要的一步
我见过太多人备份做得很勤快,但从来没验证过。等到真需要恢复的时候,发现压缩包是坏的、权限不对、或者少了关键文件。没有验证的备份等于没有备份,这句话怎么强调都不过分。
验证分三个层次。第一层是结构验证,用tar -tf或者gzip -t检查归档和压缩是否完整,这个成本最低,每次备份后都应该做。第二层是内容验证,随机抽取几个文件解压出来,对比大小和校验和,确认内容没损坏。第三层是恢复演练,定期在测试环境完整恢复一次,确认整个流程能跑通。
第一层可以自动化,第二层可以抽样做,第三层建议至少每季度做一次。我自己的做法是:每次备份后自动做结构验证,每周抽一个备份做内容验证,每季度做一次完整的恢复演练并记录耗时。
4.3 保留策略:别让备份把磁盘撑爆
备份文件会越积越多,必须有清理策略。常见的策略有三种:
按时间保留:保留最近N天的备份,用find -mtime清理。简单直接,适合日常备份。
按数量保留:保留最近N个备份,用ls -t | tail -n +N+1 | xargs rm清理。适合备份频率不固定的场景。
分层保留(GFS):保留最近7天的日备份、最近4周的周备份、最近12个月的月备份。这个策略最科学,但实现起来复杂一些,通常需要脚本维护不同层级的目录。
我一般用时间保留加数量保留的组合:日常备份保留7天,每周日的备份额外复制一份到周备份目录保留4周。这样既控制了空间,又保留了较长时间跨度的恢复点。
注意:清理脚本一定要测试好再上生产。我踩过一次坑,
find的-mtime参数理解错了,-mtime +7是“修改时间在7天前”,不是“7天内的”,结果把新备份全删了。测试的时候先用-delete,确认列表对了再执行删除。
5. 常见问题排查与避坑经验
5.1 打包时文件被占用怎么办
这是最经典的问题。数据库文件、日志文件在打包时可能正在被写入,tar读到的内容可能是不一致的,轻则warning,重则归档损坏。
处理思路分几种。对于数据库,正确做法是先用数据库自己的备份工具做一致性备份(比如逻辑导出或者物理备份),再打包备份文件,而不是直接打包数据目录。对于日志文件,如果应用支持日志轮转,可以在轮转后打包旧日志;如果不支持,可以用cp先复制一份再打包,或者接受轻微不一致。
tar本身有个--ignore-failed-read参数,遇到读不了的文件不报错继续,但这只是掩盖问题,不解决一致性。真正要一致性,得靠应用层配合或者文件系统快照(如LVM快照)。
5.2 压缩包跨平台解压的编码问题
在Linux上打包的中文文件名,拿到Windows上解压经常乱码。这是因为Linux默认用UTF-8编码文件名,而Windows的解压工具可能按GBK解析。反过来也一样。
解决办法是在打包时明确指定编码,或者用支持编码转换的工具。tar本身没有编码参数,但可以用--format配合一些技巧。更简单的办法是打包前把文件名统一成英文,或者用7z这类支持编码指定的工具。
如果只是Linux之间传输,一般不用担心编码问题,只要两边的locale一致就行。跨平台传输时,我建议在打包时加个说明文件,记录原始编码,解压时用unzip -O或者tar --to-command配合iconv处理。
5.3 大文件打包的内存与临时空间问题
打包超大文件或者超多小文件时,可能遇到内存不足或者临时空间不足。tar本身是流式处理,内存占用不大,但压缩工具会占用内存,尤其是xz的高级别压缩。临时空间方面,有些压缩工具会写临时文件,如果/tmp空间小就会失败。
排查这类问题,先看dmesg有没有OOM记录,再看df -h /tmp和df -h .的空间。解决方法是降低压缩级别、换用内存占用小的工具(如gzip或zstd)、或者把临时目录指到大分区(设置TMPDIR环境变量)。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| tar报"file changed as we read it" | 文件打包时被修改 | 看warning具体是哪个文件 | 排除该文件或先停写 |
| 压缩包解压报"unexpected EOF" | 压缩包不完整 | gzip -t或xz -t测试 | 重新打包,检查磁盘空间 |
| 解压后权限不对 | 打包时没保留权限 | tar -tvf看权限位 | 加-p参数重新打包 |
| 压缩率异常低 | 文件本身已压缩 | 看源文件类型 | 已压缩文件不必再压 |
| 打包速度极慢 | 小文件太多或磁盘慢 | iostat看磁盘 | 用多线程压缩或先打包再压 |
| 恢复后文件缺失 | 排除规则写错 | 对比tar -tf列表 | 检查--exclude规则 |
| 中文文件名乱码 | 编码不一致 | 看locale设置 | 统一UTF-8或转码 |
这张表里的每一条我基本都踩过。印象最深的是“压缩率异常低”那次,我打包一个目录,压完发现几乎没变小,查了半天才发现里面全是已经压过的图片和视频,再压当然没效果。所以打包前先看看文件类型分布,file命令或者看扩展名,已经压缩过的格式(jpg、mp4、zip、gz)就别指望再压了。
5.5 几个容易被忽视的实操心得
第一,打包前先cd到源目录的父目录,用相对路径打包。这样解压的时候不会把绝对路径带出来,恢复位置更灵活。用-C参数切换目录比先cd再打包更清晰。
第二,-v参数在脚本里慎用。tar -cvf会把每个文件名都输出,文件多的时候日志会爆炸,而且-v本身会拖慢速度。脚本里一般不加-v,需要看进度的时候单独跑一次。
第三,压缩包的命名带上时间戳和主机名。app_20240115_030000_web01.tar.zst这样的名字,一看就知道是什么、什么时候、从哪台机器备的。别用backup.tar.gz这种名字,过两天你自己都分不清哪个是哪个。
第四,备份脚本要幂等。重复执行不应该产生副作用,比如重复追加日志、重复删除文件。清理逻辑尤其要注意,用精确的匹配模式,别误删。
第五,定期检查备份目录的inode使用率。备份大量小文件时,inode可能先于磁盘空间耗尽。df -i看inode,如果快满了,要么合并小文件,要么清理旧备份。
6. 从单机备份到集中备份的演进思路
单机备份脚本跑通之后,下一步自然是集中管理。我自己的演进路径是这样的:最开始每台机器一个脚本,各自备份到本地;后来发现本地磁盘坏了备份也没了,就改成备份到一台专门的存储机,用rsync或者scp传输;再后来机器多了,手动管理太累,就上了配置管理工具统一分发脚本和收集状态。
传输环节有个细节要注意:先压缩再传输,还是边压缩边传输。tar -czf - /data | ssh user@host "cat > backup.tar.gz"这种管道方式,压缩和传输同时进行,不占本地临时空间,但一旦传输中断就得重来。先压到本地再传,占本地空间但可以断点续传。数据量大的话,我建议先压本地,用rsync --partial传输,支持断点续传更稳妥。
集中备份还要考虑去重。多台机器可能有很多相同的系统文件,全量备份浪费空间。可以用rsync的硬链接快照,或者borgbackup这类支持去重的工具。不过去重工具会增加复杂度,小规模场景不一定值得。
最后说个心态问题。备份这件事,平时不出事的时候感觉不到价值,一出事就是大事。我经历过一次磁盘阵列故障,靠备份在4小时内恢复了全部数据,那种“幸好做了备份”的感觉,比任何技术成就感都实在。所以别嫌麻烦,把备份脚本写好、验证做好、保留策略定好,剩下的就是让它安安静静地跑。