1. 项目概述:一个叫 Caveman 的极简备份工具
如果你的服务器上跑着几个服务、存着几份数据库、积累了不少配置和脚本,你迟早会撞上那个经典问题:数据要备份,但备份方案怎么选都别扭。商业软件太重,云服务太贵,开源全家桶又有一堆依赖和配置要维护。我用过不少方案,最后却是一个叫 Caveman 的极简备份脚本让我省下了所有力气。
Caveman,穴居人,这名字起得很妙。它不做花哨的增量、去重、加密、压缩算法比拼,整个工具的哲学就一句话:像原始人一样简单,但可靠到让你忘记它的存在。
这个项目本质是一个基于 Shell 的轻量级定时备份方案,核心能力包括:
- 用 tar 对指定目录做全量快照(支持排除规则)
- 数据库层面提供 MySQL/PostgreSQL 的定向导出,再落盘归档
- 默认保留最近 7 天备份,过期的自动清理
- 可选远程同步(rsync 到另一台机器或挂载点),避免“单机备份等于没备份”
- 日志记录与失败邮件提醒,让你只在该出手时才被惊动
它适合谁?适合那些手头有一两台 Linux 服务器、需要备份的关键数据量在几十 GB 以内、不想折腾复杂备份软件、但也不想裸奔的个人开发者和小团队。如果你已经能用 crontab 写定时任务,Caveman 这套结构你半小时就能看懂,改造成自己的也就一杯茶的功夫。
2. 设计思路拆解:为什么这个项目非要“返祖”不可
2.1 先搞清楚备份方案的痛点再动手
我见过太多人一上来就选型:Bacula、Amanda、Restic、BorgBackup,甚至商业软件。工具本身都没问题,但引入它们意味着什么?意味着你要维护一套配置体系,要理解“备份卷”“存储池”“保留策略”这些抽象概念,还要祈祷升级兼容性不出岔子。对于三五台小服务器的场景,这是典型的杀鸡用牛刀,而且牛刀还会生锈——半年后你可能连配置文件里每个参数什么意思都忘光了。
Caveman 的设计初衷就是反其道而行之。它的核心选择是:用 tar + rsync + cron 这几个“石器时代”的工具组合出一个整体方案。为什么这么选?因为 tar 和 rsync 是两个经过了 30 年以上生产环境考验的工具,行为可预期、文档丰富、几乎没有“惊喜”。换句话说,你不需要信任一个庞大系统的黑盒逻辑,你只需要信任两个老朋友。
2.2 关键取舍:全量备份也可以是偷懒的正义
很多人一听“全量备份”就摇头:太慢、太占空间。但 Caveman 坚持每天做全量快照,原因很实在:对于小体量数据,全量远比增量简单可靠。增量方案需要维护基础链,任何一环断了,恢复就麻烦。全量快照则不同,任何一个备份文件都是自包含的,恢复时直接解压即可,不存在“先恢复周一的再打周二补丁”这种连环依赖。
空间占用也可以用简单办法压制:tar 本身支持压缩,备份文件夹里还能通过硬链接(--hard-dereference配合文件系统层手动ln)或者直接保留旧文件来实现类似“时间点快照”的效果。我在项目的 2.0 版本里给“保留 7 天”做了配置化,如果数据量在 10GB 以下,7 个全量快照占用的空间完全在可控范围内。
2.3 这个方案的边界在哪里
必须说清楚,Caveman 不是什么银弹。它不擅长处理 PB 级数据,不适合做跨机房多活容灾,也没有图形化监控面板。它的舒适区是“个人服务器 / 小团队内部服务”的日常备份。我开发它的时候给自己定的标准是:数据能丢的容忍度以小时计,恢复操作必须在 10 分钟内完成。如果哪天这个需求变了,需要分钟级恢复或异地双活,那确实应该考虑更重的方案。但在那之前,简单就是最大的竞争力。
3. 核心细节解析与实操要点
3.1 目录规划和备份内容分层
Caveman 项目一开始就要确定“备份什么”。我强烈建议把内容分成三类:系统配置、应用数据、数据库导出。千万不要一股脑把整个根目录打进去,否则备份文件里塞满日志和缓存,恢复时还得自己挑。
我的项目结构长这样:
/opt/caveman/ ├── caveman.sh # 主执行脚本 ├── conf.d/ │ ├── path.list # 需要备份的目录列表,一行一个 │ ├── exclude.list # tar 排除规则 │ └── db.conf # 数据库连接信息 ├── backup/ │ └── 20240412_0300/ # 按日期时间命名的备份目录 └── log/ └── caveman.logpath.list里我分别写了/etc(系统配置)、/var/www(Web 服务源码)、/opt/data(应用生成的数据文件)这几个目录。写的时候要克制,只写真正有价值的东西。一个判断标准很简单:假设这台机器明天突然消失,你从备份恢复之后,哪个目录缺了会让你想撞墙,那就把它加进去。
3.2 tar 归档的可重复性与压缩参数选择
备份脚本的核心是一条 tar 命令。我一开始用的是普通写法,后来踩了几个坑才改成下面这版:
SNAPSHOT_DIR="/opt/caveman/backup/$(date +%Y%m%d_%H%M%S)" mkdir -p "$SNAPSHOT_DIR" tar --exclude-from="$EXCLUDE_LIST" \ --exclude="$SNAPSHOT_DIR" \ -czf "$SNAPSHOT_DIR/files.tar.gz" \ --files-from="$PATH_LIST" 2>>"$LOG_FILE"几个关键点:
--files-from比在命令行末尾写一堆目录更清晰,也方便脚本动态增删备份项,不需要改脚本主体。--exclude-from排除列表很重要。我的 exclude.list 里固定有几行:*/cache/*、*/tmp/*、*.log、*/node_modules/*。node_modules 这种东西恢复时跑一次包管理器就能重新生成,完全不值得占用备份体积。- 压缩级别选了默认(-z 就是 gzip 默认级别,相当于 -6)。我之前试过 -9,结果几分钟的压缩时间变成十几分钟,体积只小了几个百分点,对小规模数据来说完全不划算。
3.3 数据库导出的处理顺序
有数据库的服务,光备份文件是不够的。文件系统快照能抓住数据库文件没错,但容易产生“热备状态下拷贝数据文件导致的不一致”。Caveman 的做法是先用 mysqldump 或 pg_dump 做逻辑导出,再备份导出的 SQL 文件:
# 以 MySQL 为例,读取 db.conf 中的配置 mysql_user="backup_user" mysql_password="$(grep '^password=' "$DB_CONF" | cut -d= -f2-)" mysqldump -u"$mysql_user" -p"$mysql_password" \ --single-transaction --quick --routines --triggers \ --all-databases > "$SNAPSHOT_DIR/mysql_full.sql" 2>>"$LOG_FILE" gzip "$SNAPSHOT_DIR/mysql_full.sql"这里--single-transaction是关键。对 InnoDB 引擎来说,这个参数能在不加锁的情况下拿到一个一致性的快照,避免备份过程中写入导致的数据错乱。--routines和--triggers不能漏,否则恢复后存储过程、触发器全部丢失,应用跑起来会莫名报错。
数据库导出的顺序应该在 tar 打包之前。也就是说,先把 SQL dump 生成到一个临时目录,再把这个目录纳入 tar 的打包列表。这样最终只有一个files.tar.gz文件,备份管理更整洁。
3.4 保留策略与清理机制
“保留 7 天”是我在项目里写死的默认值。实现方式是用find找出超过保留天数的备份目录并删除:
KEEP_DAYS=7 find /opt/caveman/backup/ -maxdepth 1 -type d -mtime +${KEEP_DAYS} -exec rm -rf {} \; 2>>"$LOG_FILE"mtime +7的意思是“修改时间超过 7 天的”,这个判断是自然日,不是精确到 168 小时,所以偶尔一天没跑(比如机器关机)不会立刻触发误删。这个策略实用且好解释:你有 7 个灾难恢复回退点,而且磁盘占用是可控的。
清理动作放在备份成功之后执行。因为只要备份成功了,昨天的旧目录就是真正可以被替代的“过去式”;如果备份失败,旧目录还能留着兜底。
4. 实操过程与核心环节实现
4.1 从零部署 Caveman 的完整路径
部署过程不复杂,但每一步都有讲究。以 Ubuntu 22.04 为例,完整走一遍。
第一步,确认基本工具已安装:
sudo apt update sudo apt install -y tar rsync mysql-client postgresql-clienttar和rsync是核心,几乎每个发行版都有。mysql-client / postgresql-client 看你的数据库类型选装,如果只有 MySQL 就装 mysql-client 即可。
第二步,创建目录结构并放置脚本。我在项目仓库里放的是模板文件,你需要在服务器上做几处“本地化”修改:path.list里的备份目录换成自己的;db.conf里的数据库账号换成自己的;顶部的REMOTE_HOST和REMOTE_PATH变量按需填写。
第三步,给脚本执行权限并手动跑一次:
chmod +x /opt/caveman/caveman.sh /opt/caveman/caveman.sh首次执行不要用 cron 跑,一定手动执行。看下日志输出的三行关键信息:tar 是否成功、数据库导出是否有 WARNING、清理阶段删除了哪些目录。我第一次跑的时候遇到 mysqldump 提示Using a password on the command line interface can be insecure,这是正常提示,不影响备份动作,但如果你介意,可以在db.conf里用[client]段配置password字段并让 mysqldump 读取配置文件,我后来就是这么改的。
第四步,验证备份文件的可读性。这一步很多人会偷懒跳过,但恰恰是备份方案里最重要的一环。我用一个临时目录做解压验证:
mkdir -p /tmp/restore_test tar -xzf /opt/caveman/backup/20240412_0300/files.tar.gz -C /tmp/restore_test能正常解压,并且打开几个文件确认内容非空,说明这条链路是通的。
4.2 定时任务配置与邮件告警
Cron 配置是整个环节里最容易出低级错误的点。我在 crontab 里这样写:
0 3 * * * /opt/caveman/caveman.sh >> /opt/caveman/log/caveman_cron.log 2>&1每天凌晨 3 点执行,加上>> ... 2>&1重定向 cron 自己的输出。为什么要单独重定向?因为 cron 默认会把 stdout/stderr 通过邮件发给你,但在很多云服务器上 postfix 没装,输出直接丢了;而且一旦脚本报错,没有日志根本不知道发生了什么。重定向后,任何输出都落入文件,排查问题不看邮件也不看系统日志,直接看这个文件就行。
邮件告警我做得比较原始:在脚本尾部判断退出码,非 0 就调用sendmail或mailx发一封标题为[CAVEMAN] Backup Failed的信。如果你用的是 Postfix 之类的本机发信,那就够了。如果本机没有邮件服务,我建议退而求其次:把日志输出到文件,另外用一个独立的心跳监控(比如 UptimeRobot 定时探测某个文件是否更新)来兜底。
4.3 rsync 远程同步的落地细节
本地留 7 份备份只能防硬盘坏道和误删,防不了机房起火或整台机器被盗。远程同步是让“备份”真正成立的另一半。Caveman 项目里我用 rsync 实现一份异地拷贝:
REMOTE_HOST="backup@nas.local" REMOTE_PATH="/volume1/caveman_backup" rsync -avz --delete \ /opt/caveman/backup/ \ "$REMOTE_HOST:$REMOTE_PATH" 2>>"$LOG_FILE"两个参数特别重要:--delete使远端目录与本地严格一致,本地过期清理后远端也同步删除,不会让远端塞满;-z开启压缩,对于配置文件、SQL dump 这类文本文件效果显著,传输量能省掉一半以上。
第一次连接需要手动输密码,之后我配置了 SSH 密钥:
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 ssh-copy-id backup@nas.localed25519密钥比传统 RSA 更短更安全,这是现在比较推荐的默认选择。配置完成后,rsync 就能免密跑,cron 环境下也不会卡在密码输入上。
4.4 恢复流程的完整演练
一个备份方案如果没演练过恢复,等于没做。我专门挑了一个周末,分别在恢复目录和恢复整机两种场景下做了验证。
目录级别恢复很简单,核心就一条命令:
tar -xzf files.tar.gz -C /where/you/want/to/restore整机级别的恢复要麻烦一点,但有一份最近的/etc备份和数据库 SQL dump,再加上操作系统的 mini 安装盘,整个恢复链条是可行的。我在测试服务器上把关键服务的数据目录删掉一部分,然后从备份里挑出对应目录解压回去,服务秒级恢复。数据库部分则是先建库再导入:
mysql -u root -p < mysql_full.sql导入前确认字符集和排序规则一致,这算是老生常谈了,但我在测试时确实因为默认字符集不一致出现过中文乱码。建议 dump 命令里显式加上--default-character-set=utf8mb4。
5. 常见问题与排查技巧实录
5.1 tar 报错 "file changed as we read it"
这是 tar 打包时最常碰见的坑,背景是某文件正在被写入,打包过程中文件大小或 mtime 发生了变化。我用过的方式是用--warning=no-file-changed压制这个警告,让备份任务继续完成:
tar --warning=no-file-changed -czf ...原因很简单:对于正在被写入的应用日志,即使拿到了那个瞬间的快照,它本身也不具备一致性,与其让整个备份失败,不如记录警告并继续,至少让其他目录的备份正常落盘。
5.2 mysqldump 备份超大数据库太慢
当单库超过 5GB 后,mysqldump 的性能下降非常明显。我的经验是,如果数据量还在这个量级以下,默认参数完全够用;如果持续增长,就要考虑换用mydumper或者用物理备份方案了。我在项目文档里明确留了一行注释:单库超过 10GB 或业务要求恢复点目标小于 30 分钟时,请改用专业方案。
另外一个提速小技巧:mysqldump 的--single-transaction会开启一个长事务,备份期间如果有大查询可能拖慢生产库。可以在夜间低峰执行,或者用--innodb-read-only配合从库备份,效果更好。
5.3 备份文件占太多磁盘空间的优化手段
我按 7 天全量保留,磁盘占用在实际运行中是个持续增长的压力。有一次我跑到第 5 天的备份发现磁盘告警,当时排查下来发现是被/var/www下的用户上传目录塞满了。解决办法并不是改脚本逻辑,而是调整备份策略:用户上传目录用 rsync 单独做增量同步,不进 tar;tar 只管系统配置和代码。这样整体体积一下就降下来了。
另一个实用技巧是调整归档内文件权限属性,把--no-same-owner加进参数,备份文件在恢复到非 root 环境时不容易出现属主错乱。空间占用上不会有直接帮助,但恢复时会省很多事。
5.4 一个问题速查表
我把日常使用中最容易遇到的状况整理成了一张表,方便照着排查:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 备份日志为空 | cron 环境没加载 PATH | 脚本开头export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| rsync 同步一直失败 | SSH 密钥权限不对 | 检查~/.ssh目录权限为 700,私钥文件权限为 600 |
| 数据库 dump 中途断连 | 备份时间太长超过net_read_timeout | 在 mysqldump 命令中加--net-buffer-length或提高服务端超时 |
| 备份目录大小异常膨胀 | 某个数据目录增长速度过快 | 按du -sh /opt/caveman/backup/*找出大头,确认是否应纳入排除规则 |
| 解压时报权限错误 | tar 归档了特殊的设备文件或 socket | 打包时加--exclude排除/proc、/sys等虚拟文件系统,或用--no-recursion控制路径深度 |
5.5 几个必须亲测过才明白的细节
日志文件记得做转储。logrotate默认不会管理你自建项目目录里的日志,所以我的做法是用 cron 每月归档一次:
0 0 1 * * mv /opt/caveman/log/caveman.log /opt/caveman/log/caveman.log.$(date +%Y%m) && touch /opt/caveman/log/caveman.log否则日志文件会无限增长,一两年后你会得到一个几 GB 的纯文本文件,打开都卡。
还有一个细节是关于脚本内变量引用的:所有路径都用双引号包起来,比如"$SNAPSHOT_DIR/files.tar.gz"。如果备份目录路径里出现空格(比如你把备份目录放在挂载盘、挂载点恰好在带空格路径下),不加引号的脚本会直接炸掉。此外,建议set -u(变量未定义即报错)和set -e(任一命令失败则退出)放在脚本开头,Caveman 的主脚本我就加了set -euo pipefail,这让整个脚本的行为变得非常可控——任何一步失败了,后续动作都会停下来,不会带着残废状态继续跑。
6. 踩过坑之后的经验沉淀
说实话,把 Caveman 从“一个临时小脚本”打磨到“一个拿得出手的方案”,中间最大的收益不是我学会了多少新命令,而是彻底理解了备份这件事的度量衡。
备份的可靠性永远不是由备份动作本身决定的,而是由恢复演练的次数决定的。Caveman 的设计里处处体现这个原则:全量快照是为了恢复时不需要依赖任何中间状态;保留 7 天是为了出问题时你有足够多的回退点;远端同步是为了本地整个机房挂了还能东山再起;日志和告警是为了让你在事态发酵之前就被唤醒。
如果你也想在自己服务器上落地这套方案,我给你几个从实操里提炼出来的非技术建议。
第一,不要一开始就追求自动化。先把备份脚本手动跑一周,确认每天的日志都正常,再用 cron 接替。这个过渡期不会损失什么,但能让你对方案建立直觉。
第二,挑个不忙的下午做一次完整的“删库演练”。把数据库目录真的停掉,从 Caveman 备份里恢复,感受一下时间线:从发现问题到服务恢复,一共用了多少分钟。这个数字比任何工具评测都有说服力。你会发现,恢复操作熟练度提升以后,同样一套脚本能救回的服务,远超你最初的预期。
第三,别让备份方案变成新的维护负担。如果你发现自己在配置上花的时间超过了当初想节省的时间,那就该停下来重新思考。Caveman 的全部配置文件加起来不超过 30 行,任何一个参数都能在 5 分钟内解释清楚,这是它至今没被我换掉的原因。
最后再分享一个小技巧。Caveman 里我特意在备份完成时生成一个带时间戳的.done标记文件,内容写的是备份耗时和文件大小。日常巡检时我只看有没有新的.done文件出现,而不需要打开日志逐行读。这是最不起眼但最省心的一个设计,你也值得拥有一个类似的“健康信号”。