
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/wwwWeb 服务源码、/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_userbackup_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_DAYS7 find /opt/caveman/backup/ -maxdepth 1 -type d -mtime ${KEEP_DAYS} -exec rm -rf {} \; 2$LOG_FILEmtime 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 21每天凌晨 3 点执行加上 ... 21重定向 cron 自己的输出。为什么要单独重定向因为 cron 默认会把 stdout/stderr 通过邮件发给你但在很多云服务器上 postfix 没装输出直接丢了而且一旦脚本报错没有日志根本不知道发生了什么。重定向后任何输出都落入文件排查问题不看邮件也不看系统日志直接看这个文件就行。邮件告警我做得比较原始在脚本尾部判断退出码非 0 就调用sendmail或mailx发一封标题为[CAVEMAN] Backup Failed的信。如果你用的是 Postfix 之类的本机发信那就够了。如果本机没有邮件服务我建议退而求其次把日志输出到文件另外用一个独立的心跳监控比如 UptimeRobot 定时探测某个文件是否更新来兜底。4.3 rsync 远程同步的落地细节本地留 7 份备份只能防硬盘坏道和误删防不了机房起火或整台机器被盗。远程同步是让“备份”真正成立的另一半。Caveman 项目里我用 rsync 实现一份异地拷贝REMOTE_HOSTbackupnas.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 backupnas.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-setutf8mb4。5. 常见问题与排查技巧实录5.1 tar 报错 file changed as we read it这是 tar 打包时最常碰见的坑背景是某文件正在被写入打包过程中文件大小或 mtime 发生了变化。我用过的方式是用--warningno-file-changed压制这个警告让备份任务继续完成tar --warningno-file-changed -czf ...原因很简单对于正在被写入的应用日志即使拿到了那个瞬间的快照它本身也不具备一致性与其让整个备份失败不如记录警告并继续至少让其他目录的备份正常落盘。5.2 mysqldump 备份超大数据库太慢当单库超过 5GB 后mysqldump 的性能下降非常明显。我的经验是如果数据量还在这个量级以下默认参数完全够用如果持续增长就要考虑换用mydumper或者用物理备份方案了。我在项目文档里明确留了一行注释单库超过 10GB 或业务要求恢复点目标小于 30 分钟时请改用专业方案。另外一个提速小技巧mysqldump 的--single-transaction会开启一个长事务备份期间如果有大查询可能拖慢生产库。可以在夜间低峰执行或者用--innodb-read-only配合从库备份效果更好。5.3 备份文件占太多磁盘空间的优化手段我按 7 天全量保留磁盘占用在实际运行中是个持续增长的压力。有一次我跑到第 5 天的备份发现磁盘告警当时排查下来发现是被/var/www下的用户上传目录塞满了。解决办法并不是改脚本逻辑而是调整备份策略用户上传目录用 rsync 单独做增量同步不进 tartar 只管系统配置和代码。这样整体体积一下就降下来了。另一个实用技巧是调整归档内文件权限属性把--no-same-owner加进参数备份文件在恢复到非 root 环境时不容易出现属主错乱。空间占用上不会有直接帮助但恢复时会省很多事。5.4 一个问题速查表我把日常使用中最容易遇到的状况整理成了一张表方便照着排查现象可能原因处理方式备份日志为空cron 环境没加载 PATH脚本开头export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binrsync 同步一直失败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文件出现而不需要打开日志逐行读。这是最不起眼但最省心的一个设计你也值得拥有一个类似的“健康信号”。