跳至正文

备份策略的一次调整

事情是这样的,前两天服务器磁盘报警了,一看使用率94%。我这台机器上跑着几个小项目的数据,还有一堆日志文件,平时也没太在意。结果一查才发现,之前的备份脚本已经半年没跑成功过——备份目录所在的磁盘居然和原数据在同一个分区上。对,你没看错,我居然犯了这么蠢的错误。备份和原数据放在同一个物理盘,真要出事,一起完蛋。

当时写那个脚本的时候,我的想法很简单:crontab每天跑一次,tar打包一下扔到/backup目录,完事。用的命令大概是这样的:

“`
tar -czf /backup/data_$(date +%Y%m%d).tar.gz /var/www/html /var/lib/mysql
“`

看起来很合理对吧?问题是/backup目录本身就在根分区下,根分区总共就200G,数据占了150G,备份再占个几十G,不出问题才怪。而且tar包越来越大,因为每次都是全量备份,从来没有清理过旧的。我检查了一下,最早的备份文件还是去年3月份的,一堆gz文件挤在一起,有的甚至超过10G。

这次我决定正经搞一下。毕竟数据这东西,丢了就是真的丢了,再多的技术文档也救不回来。

我重新规划了备份策略,核心思路就两条:备份到独立存储,增量备份代替全量。

先解决存储问题。我手上有一块挂载的NAS,路径是/mnt/nas_backup,通过NFS连过来的。之前一直闲置着,因为觉得网络备份慢。但仔细想想,慢总比没有强。我在/etc/fstab里加了一行:

“`
192.168.1.100:/volume1/backup /mnt/nas_backup nfs rw,noatime,vers=3 0 0
“`

挂载之后测试了一下写入速度,大概35MB/s,能接受。毕竟备份一般都是凌晨跑,不影响白天业务。

接下来是备份工具的选择。之前用tar,虽然简单,但全量备份太浪费空间。我决定改用rsync + hardlink的方式做增量备份,保留多个时间点的快照。这个方案其实网上很多人在用,核心是rsync的–link-dest参数,配合cp -al实现硬链接,让每个备份目录看起来像完整副本,实际上只存了变化的部分。

我的备份脚本长这样,放在/usr/local/bin/backup.sh:

“`bash
#!/bin/bash
BACKUP_DIR=”/mnt/nas_backup/snapshots”
SOURCE_DIRS=”/var/www/html /etc/nginx /var/lib/mysql”
DATE=$(date +%Y%m%d_%H%M%S)
LATEST_LINK=”$BACKUP_DIR/latest”

mkdir -p “$BACKUP_DIR/$DATE”

for dir in $SOURCE_DIRS; do
base=$(basename $dir)
if [ -d “$LATEST_LINK/$base” ]; then
rsync -a –delete –link-dest=”$LATEST_LINK/$base” “$dir/” “$BACKUP_DIR/$DATE/$base/”
else
rsync -a “$dir/” “$BACKUP_DIR/$DATE/$base/”
fi
done

rm -f “$LATEST_LINK”
ln -s “$BACKUP_DIR/$DATE” “$LATEST_LINK”
“`

解释一下逻辑:第一次跑的时候,没有latest软链接,所以直接rsync完整复制。之后每次跑,都会以latest目录作为参考,–link-dest会让rsync对比差异,对于没有变化的文件,直接创建硬链接指向之前的版本,而不是重新复制。所以每个快照看起来是完整的,实际占用的空间只等于变化的部分。

MySQL那块比较特殊,不能直接rsync数据文件,因为InnoDB的buffer pool还在内存里,直接拷可能拿到不一致的数据。我用了mysqldump先导出成SQL文件,再备份:

“`bash
mysqldump -u root –all-databases –single-transaction –routines –events > /tmp/mysql_dump.sql
“`

–single-transaction是关键参数,对于InnoDB表,它基于事务的快照来导出,不会锁表。–routines和–events是为了把存储过程和事件也带上。导出文件大概1.2G,然后rsync把这个文件也纳入备份范围。

还有一个坑:磁盘空间不足可能导致备份失败。我在脚本里加了一个检查:

“`bash
AVAIL=$(df /mnt/nas_backup | tail -1 | awk ‘{print $4}’)
if [ $AVAIL -lt 5000000 ]; then
echo “NAS剩余空间不足5G,停止备份” | mail -s “备份失败” root
exit 1
fi
“`

同时,为了控制快照数量,我写了个清理逻辑,保留最近30天的快照,更早的自动删除:

“`bash
find $BACKUP_DIR -maxdepth 1 -type d -name “20*” | sort | head -n -30 | while read old; do
rm -rf “$old”
done
“`

这里注意,因为使用了硬链接,删除旧快照并不会真正释放空间——只有当所有指向某个inode的硬链接都被删除后,文件才会被回收。所以不用担心误删导致数据丢失。

整个脚本跑起来之后,第一次全量备份花了大概40分钟,之后每天增量备份基本在1-3分钟内完成。我看了下NAS上的实际占用,第一次备份用了15G,后续每天增加200M到500M不等,完全在可接受范围内。

最后说几个踩坑的细节:

第一个,rsync的–delete参数要小心。我第一次测试的时候,目标目录里有个测试文件没删干净,结果rsync以为源端删掉了,就真的把快照里的同名文件删了。所以用–delete时一定要确认源目录是你想同步的完整状态。

第二个,mysqldump出来的SQL文件比较大,如果备份过程中MySQL还在写入,导出的数据可能不是同一时间点的。虽然–single-transaction保证了表级别的一致性,但跨表的话,如果业务逻辑强依赖多个表之间的关联,最好在mysqldump之前先FLUSH TABLES WITH READ LOCK,不过这会阻塞写入,看业务容忍度。

第三个,硬链接删除的速度。如果快照目录里文件数量很多,比如几十万个文件,用rm -rf删除整个目录会非常慢。我试过一次,删一个包含20万文件的快照,花了将近10分钟。后来改成用rsync –delete-before或者直接unlink每个文件,但也没快多少。目前我的做法是,在crontab里把清理任务放在备份之前跑,避免同时进行。

第四个,NFS挂载的稳定性。有一次NAS重启了,挂载点变成stale file handle,备份脚本直接报错退出。后来我在脚本里加了一步mountpoint检查:

“`bash
if ! mountpoint -q /mnt/nas_backup; then
mount /mnt/nas_backup
fi
“`

如果挂载没了就重新挂,至少保证备份能跑。

现在这套方案跑了三周,一切正常。上次磁盘报警的事让我明白一个道理:备份不是写个脚本就完事了,你得真的去检查它能不能恢复。我每个月会手动从NAS上拉一个快照下来,在测试环境里还原一次,确保数据可读。这个步骤虽然烦,但比出事后再后悔强得多。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注