news 2026/10/7 6:53:50

手写极简备份工具caveman:基于rsync硬链接的快照增量备份

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写极简备份工具caveman:基于rsync硬链接的快照增量备份

1. 项目概述与核心思路

1.1 “caveman”是什么,解决什么问题

caveman 是我自己写的一个极简备份工具,整个项目就一个 bash 脚本加一个纯文本配置文件,加起来不到 500 行。名字取的是“穴居人”的意思——我在里面刻意抛弃了所有现代软件常见的花活:没有 Web 管理界面,没有数据库,没有配置文件解析框架,没有依赖安装器,甚至连加密都没做。

这个工具解决的痛点是:家里和公司里有几台 Linux 机器、一台 NAS,我需要把一些关键目录(代码、nginx 配置、家庭照片、几份数据库导出文件)定时备份到移动硬盘和另一台机器上。市面上现成的备份软件要么太重(安装要拉一堆依赖,配置要看文档半小时),要么是黑盒(跑完你不知道它到底备份了什么、坏了能不能恢复),要么要花钱买授权。可我真正需要的功能其实特别朴素:能增量、能保留多个历史版本、出问题时能干净利落地把文件拉回来。

如果你也是那种“不想把数据交给看不懂的工具”的人,或者你正在找一个小而可靠的备份方案,caveman 的实现思路可以直接拿来参考。它不挑发行版,只要你的机器有 rsync、coreutils 和 cron(或 systemd timer)就能跑。我实测下来这套方法运行了半年多,中间经过了一次真实的数据恢复,结论是:越简单的方案,越不容易在关键时刻掉链子。

1.2 为什么选择“穴居人”式的设计

在设计 caveman 时,我先列了一个“绝对不做什么”的清单,而不是“要做什么”。这条决策路径可能和多数人相反,但对个人工具来说非常有效:

  • 不做加密。备份的数据全在家里 / 公司内网传输,加密层会增加恢复时的复杂度,一旦密钥丢失,备份等于不存在。
  • 不做界面。浏览器管理面板意味着要跑一个常驻服务,要处理端口、会话、权限,这些都不是备份本身的需求。
  • 不引入数据库。备份列表就是几十行纯文本,用 grep 就能查,存进 SQLite 反而多一个需要维护的文件。
  • 不做跨平台图形化。macOS 和 Linux 的终端就是最好的环境,Windows 用户可以直接跳过这个项目。

真正保留的核心只有一个:用 rsync 的硬链接机制做快照式增量备份。这个方案极具“穴居人”精神——rsync 是上世纪 90 年代就存在的工具,硬链接更是 Unix 文件系统最基础的特性,但两者组合起来的效果,甚至比不少商业备份软件还优雅。每次备份生成一个完整的时间戳目录,看起来像全量备份,实际上未变化的文件只是多了一个硬链接,既不占额外空间,恢复时又不需要任何特殊软件去“解包”,直接拷贝文件就行。

从技术上对比一下常见方案:

方案增量能力恢复复杂度依赖适合场景
cp -a 全量拷贝无,每次全量极低无小目录、低频备份
rsync 镜像同步单向增量低仅 rsync单一最新副本够用
tar 压缩包归档需手动管理轮转中,需解包tar / gzip需要打包分发
rsync 硬链接快照完美增量极低rsync多版本历史保留
商业备份软件多支持各不相同较重企业统一管理

caveman 明显站在“极低恢复复杂度”和“完美增量”的交汇点上,这也是我把项目做成这样的根本原因。

2. 核心实现与关键细节

2.1 项目结构与运行入口

整个项目的目录结构非常简单:

~/caveman/ ├── caveman # 主脚本,chmod +x ├── conf/ │ └── backup.list # 备份配置,一行一个任务 ├── logs/ # 运行日志目录 └── backups/ # 快照根目录

主脚本用 bash 写的,入口逻辑是标准的子命令分发。我不喜欢一个脚本干太多事,所以只暴露了三个子命令:backup(执行备份)、list(查看快照)、restore(恢复文件)。

#!/usr/bin/env bash set -euo pipefail BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONF_FILE="${BASE_DIR}/conf/backup.list" BACKUP_ROOT="${BASE_DIR}/backups" LOG_DIR="${BASE_DIR}/logs" cmd="${1:-}" case "$cmd" in backup) shift; cmd_backup "$@" ;; list) shift; cmd_list "$@" ;; restore) shift; cmd_restore "$@" ;; *) echo "用法: $0 {backup|list|restore}" >&2; exit 1 ;; esac

set -euo pipefail三件套是脚本可靠性的底线。-e让脚本在出错时立即退出,-u避免变量未定义的坑,pipefail保证管道中任何一环失败都会让整个命令失败。没有这三行,脚本很容易在某个命令静默失败后继续往下跑,直到你发现备份坏了才追悔莫及。

2.2 快照式备份的核心原理

caveman 的灵魂在cmd_backup()函数里。每条配置格式是:

源路径::备份标签

例如:

/home/user/workspace::workspace /etc/nginx::nginx-config

执行备份时,脚本会为每个标签创建一个带时间戳的目录,然后用 rsync 把源目录同步进去。核心命令如下:

run_rsync() { local src="$1" dest="$2" link_dest="$3" label="$4" rsync -a --delete \ --link-dest="$link_dest" \ --exclude-from="$CONF_FILE.exclude" \ "$src"/ "$dest"/ }

关键在--link-dest参数。它告诉 rsync:目标目录中如果某个文件的内容、大小、权限与link_dest指向的目录里的同名文件完全一致,就不要重复复制内容,而是直接建一个硬链接指向link_dest中的那个文件。这样,这次“看起来像全量拷贝”的快照,实际只存储了自上次备份以来真正变化的数据。

打个比方:如果把文件比作一本书,硬链接就是同一个书架上多贴了一个标签,标签都指向同一本书。你没买新书,只是增加了索引。只有真正改了内容的文件,rsync 才会重新“买一本新书”放到新目录里,老标签仍然指旧书。

这种设计带来的好处很实际:

  • 每个快照目录都是完整的,可以直接浏览、可以直接 cp,不需要任何工具“还原”。
  • 磁盘空间只受“每次变化量”影响,而不是受“数据总量”影响。
  • 恢复了上一个快照之后,旧文件仍然可用,不会因为覆盖而丢失数据。

2.3 配置文件与排除规则

配置文件故意保持“弱语法”。每行一个任务,::作为分隔符,#开头是注释。不搞 JSON、不搞 YAML,因为备份配置的典型形态是“半年前写一次,之后基本不动”,没必要为了这种静态内容引入解析器和转义规则。

排除规则单独放在conf/backup.list.exclude文件里,和 rsync 的--exclude-from配合。这个文件支持 rsync 的通配符语法:

*.tmp .git/ node_modules/ __pycache__/ *.log .DS_Store

注意排除规则里如果写node_modules/,那么所有层级名为 node_modules 的目录都会被排除;如果只写node_modules(不带斜杠),同样的效果,但建议统一带斜杠,语义更清晰,也避免匹配到同名文件。

2.4 日志、锁与通知

脚本里的日志处理没用什么 loguru、syslog,就是简单的文本追加:

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "${LOG_DIR}/backup.log" }

backup.log按天自然追加,配合 grep 就能查某天的记录。从不轮转日志,因为我本来就希望它“无限期保留”——备份日志这种一年不过几 MB 的文件,没必要让程序费心管理。

单实例锁是备份脚本必须要有的,不然 cron 触发和手动触发一旦重叠,两个 rsync 同时写同一目标目录,会产生不可预测的结果。我用flock实现:

exec 9>"${BACKUP_ROOT}/.lock" flock -n 9 || { echo "已有备份任务在运行"; exit 2; }

flock -n是非阻塞模式,拿不到锁就直接退出,配合 cron 的下一次触发自然就跳过了,不会造成任务堆积。执行完备份后,脚本还会打印一份简短摘要:本次处理了多少文件、新增多少数据、总耗时多少,日志里同样记录一份,方便事后核对。

3. 实操过程与核心环节实现

3.1 环境准备与初始化

caveman 的依赖少到可以在两分钟内部署完。在 Debian/Ubuntu 系上:

sudo apt-get install rsync

macOS 自带 rsync,不过版本可能偏旧。如果你要用到--link-dest的某些新特性,建议用 Homebrew 升级一下:

brew install rsync

目录初始化直接用脚本内置命令:

mkdir -p ~/caveman/{conf,logs,backups} chmod +x ~/caveman/caveman

我这里没有用install命令做全局安装,原因是这个工具和它的配置、日志、备份数据应该作为一个整体待在一起,挪到/usr/local/bin只放脚本反而会让路径关系变乱。让它就住在~/caveman下,一切数据都在自己的院子里,迁移时直接拷贝整个目录即可。

3.2 编写备份配置

拿我的实际配置举例:

# 工作区 /home/user/workspace::workspace # 数据库导出 /var/backups/mysql::mysql # nginx 站点配置 /etc/nginx::nginx-config # 家庭照片库 /media/photo::photos

写配置时有一个容易忽略的细节:源路径末尾的斜杠。rsync 的语义是,/home/user/workspace/(带尾斜杠)表示“拷贝目录内的内容”,而/home/user/workspace(不带尾斜杠)表示“拷贝目录本身及其内容”。我在调用 rsync 时统一给源路径加了斜杠"$src"/,所以配置里写不带斜杠的路径即可,行为一致。这一点新手最容易踩,建议看到这里的人直接记住:决定“拷贝目录本身还是内容”的是命令末尾的斜杠,不是配置文件的写法。

3.3 手动运行与验证

第一次运行推荐加-v手动执行:

~/caveman/caveman backup

正常的输出应该是这样的:

[备份] workspace 开始 [备份] workspace 完成,新增数据量 12.3MB [备份] mysql 开始 [备份] mysql 完成,新增数据量 0KB [备份] 全部完成,耗时 35s

看到“全部完成”之后,不要急着开香槟,先做一次验证。进入backups/目录,看每个标签下的时间戳目录:

find ~/caveman/backups -maxdepth 2 -type d | sort

你会看到类似这样的结构:

backups/ ├── workspace/ │ ├── 2025-01-15_033000/ │ └── 2025-01-16_033000/ └── mysql/ └── 2025-01-16_033000/

第一次备份必然是完整重放所有文件,第二次再跑时,观察du -sh结果,新增目录的总大小应该远小于源目录总大小,说明增量生效了。用ls -i查看某个未变化文件的 inode 编号,会和上一个快照里对应文件完全一致,这就是硬链接起作用的铁证。

3.4 定时任务与恢复演练

备份工具不做定时等于没有备份。我用 cron 实现最简单的定时策略:

# 每天凌晨 3:30 执行备份 30 3 * * * /home/user/caveman/caveman backup >> /home/user/caveman/logs/cron.log 2>&1

注意这里我写的是完整的绝对路径。cron 的 PATH 环境变量很精简,通常只有/usr/bin:/bin,如果你把 rsync 装在了/usr/local/bin,不写绝对路径或者不显式设置 PATH,cron 会报rsync: command not found,但脚本因为set -e会静默退出,日志里只留下一行模糊的错误。另一个细节是>> ... 2>&1,这样标准输出和标准错误都会被追加到 cron 日志,排查问题时有据可查。

恢复演练是很多个人备份方案的死角。我建议你至少做一次完整恢复测试,而不是等真出事再去研究。caveman 提供的恢复命令其实只是 rsync 的反向操作:

# 从最近的 workspace 快照恢复到本目录 ~/caveman/caveman restore workspace 2025-01-16_033000 /home/user/workspace

实际上我更推荐恢复时直接手动敲 rsync,因为恢复场景差异太大(恢复到原目录、恢复到临时目录、只恢复部分子目录),一个抽象命令反而限制了灵活性:

rsync -a ~/caveman/backups/workspace/2025-01-16_033000/ /home/user/workspace/

这个过程和日常复制文件的感觉一样,没有任何“恢复工具”的存在感——这就是硬链接快照方案最大的价值:备份时省空间,恢复时无感。

4. 常见问题与排查技巧实录

4.1 符号链接、硬链接与文件属性带来的坑

用 rsync 备份时,-a参数默认会保留符号链接本身,而不解引用它指向的目标,这通常是对的。但有一种情况很坑:如果源目录里有符号链接指向/proc或/dev下的路径,恢复出来的符号链接依然指向这些路径,在备份机上看是“坏的”。这正常,不用管。真正要注意的是,绝对路径符号链接在恢复后会指向原机器的绝对位置,如果你把备份恢复到另一台环境不同机器上,这些链接大概率失效。

解决方法是备份核心配置时确认源目录里没有脆弱的绝对符号链接,或者把符号链接作为普通文件备份(去掉-l参数),但这样会丢失链接语义。我一般倾向于接受链接失效的现实,因为配置文件恢复后手动重新指一下,代价远比在备份方案里引入复杂处理逻辑小。

4.2 增量备份越跑越大的排查

有次我发现第二天的快照目录du出来比源目录还大,而且每跑一次就大一倍。用du -sh对比多个快照目录后,定位到是--link-dest的路径写错了,导致每次备份都找不到上一次快照作为参考,自然每次都全量拷贝。

另一个导致“假增量”的常见原因是权限或时间戳变化。rsync 判断文件是否变化,默认看大小和修改时间。如果你用-a保留了属主和权限,但从另一台机器同步过来的文件因为 UID 不同被判定为“属性变化”,它也会重新拷贝文件内容。这个问题的排查可以用:

# 比较两个快照目录中同一文件的 inode ls -i backups/workspace/2025-01-15_033000/file.txt ls -i backups/workspace/2025-01-16_033000/file.txt

如果 inode 不同,说明这个文件没有走硬链接,属于“真变化”(内容真的改了)。如果 inode 相同,说明硬链接生效。如果大量文件 inode 都不同但内容明明没改,就去查文件属主、权限或 mtime 差异。

4.3 cron 运行时的环境差异

cron 下跑脚本,最容易栽跟头的就是环境变量。我遇到过的情况:脚本手动执行一切正常,加到 crontab 后就日志为空,或者日志只有一行提错误。原因就是PATH里没有/usr/local/bin,rsync 调不到。

解决办法有两种,任选其一:

# 方案一:crontab 里显式设置 PATH 30 3 * * * PATH=/usr/local/bin:/usr/bin:/bin /home/user/caveman/caveman backup # 方案二:脚本开头强制锁定 PATH export PATH="/usr/local/bin:/usr/bin:/bin"

第二个方案更稳妥,因为 cron 之外还有 systemd timer、anacron 等触发方式,它们的 PATH 可能又不一样。哪怕脚本里已经写了set -u,也建议显式export PATH,避免环境差异成为隐患。

4.4 恢复文件的权限问题

有一次我恢复 nginx 配置时,因为当时是用普通用户执行的 rsync,恢复后的文件属主变成了普通用户,nginx 起来时报权限错误。后来我养成一个习惯:恢复涉及系统目录时,先检查原文件属主,再用sudo rsync -a --numeric-ids来恢复。

--numeric-ids参数很重要。它让 rsync 不去把 UID 翻译成用户名,而是直接按数字 ID 恢复,避免备份机和恢复机的用户数据库不一致时出现属主错乱。

提示:不要用chown -R去粗暴修正恢复出来的文件,这个命令会连符号链接的目标也一起改,非常危险。宁可在 rsync 命令里多花 10 秒想清楚属主和权限参数。

5. 后续扩展思路

5.1 增加完整性校验

caveman 目前的校验机制依赖 rsync 的传输校验,但传输校验不能防止“文件在备份完成后被篡改”的问题。如果你备份的是长期不动的重要档案,可以加一个校验清单:

# 每次备份后生成校验文件 cd backups/workspace/2025-01-16_033000 find . -type f -exec sha256sum {} \; > .checksum.sha256

之后要验证时就跑sha256sum -c .checksum.sha256。对大多数家庭 / 小型办公场景,这个做法已经够硬核了。不要过度追求每次都全量校验,因为对于大目录来说sha256sum跑一遍的耗时可能超过备份本身。

5.2 快照保留策略

硬链接快照方案唯一的“缺点”是如果不做清理,每次快照目录本身会越来越多(虽然每份增量小,但目录数量和部分文件 meta 信息会累积)。我加了一个简单的清理函数:

# 保留最近 30 天快照,其余删除 find "$BACKUP_ROOT/$label" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

这个策略的优雅之处在于,它只删“目录入口”,不真正删数据——只要同一天内还有后续快照引用了这些硬链接,数据块仍被保留。只有当所有引用该文件数据的快照都被清理后,磁盘空间才会真的释放。

5.3 一点通知能力

备份完成后如果能给你发个消息,你就不会再“担心它没跑”。最简单的方式是写一行摘要,配合邮件命令:

mailx -s "[caveman] 备份完成" you@example.com < "${LOG_DIR}/latest_summary.txt"

或者接入微信群机器人 / Telegram Bot,本质就是 curl 发一个 POST 请求。我个人的原则是:只有当通知本身简单到不行时,才值得加入项目。如果你需要写 50 行代码才能完成通知,说明这个需求根本不重要。

5.4 后续还能往哪走

如果你照着这个思路继续做,有几个方向值得尝试:把备份根目录换成异地挂载的 NFS/SMB 共享,就实现了异地备份;在恢复命令上再包一层交互式列表选择,就变成了一个更像“软件”的东西;甚至可以把配置文件拆成多份,每台机器有自己独立的备份清单,但共享同一个 caveman 脚本。每一条路都比我一开始用 Python + SQLite + FastAPI 尝试做的“备份管理系统”务实得多。

我个人在实际使用中的体会是:这个工具最值钱的部分不是那几百行脚本,而是“知道自己每一步在干什么”的确定性。商业软件再黑盒,也不如自己手写方案出问题时能直接查看的最后一行日志来得踏实。如果你也准备动手写一个这样的工具,我的建议是:先把备份核心逻辑用最笨的方式跑通,然后再考虑要不要封装,千万不要一上来就设计得“面面俱到”。大多数人的备份需求,真的不需要一个比备份本身更复杂的系统。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 6:53:37

MOSFET热失效实战分析:从热阻模型到保护电路设计的完整指南

1. 项目概述1.1 核心需求解析先交代一下背景。这个项目来自我最近处理的一批电源板返修案例&#xff1a;客户反馈有十几块板子在高温老化测试阶段出现炸管&#xff0c;损坏位置高度集中在同步整流MOSFET和Boost升压MOSFET上。拆开看&#xff0c;封装开裂、源漏击穿、栅极氧化层…

作者头像 李华
网站建设 2026/10/7 6:52:57

superpowers扩展:把AI嵌入VS Code的智能开发工作台

最近我几乎把主力开发环境切回到了 VS Code 上&#xff0c;原因是一个叫 superpowers 的开源扩展。很多人可能和我一样&#xff0c;先在热搜上看到这个名字&#xff0c;第一反应是“这不又是一个 AI 编程助手”。直到我把它真正装进编辑器、连续用了两三周之后&#xff0c;才意…

作者头像 李华
网站建设 2026/10/7 6:52:15

用命令行管理个人技能树:从 YAML 数据结构到 CLI 工具实战

最近把自己折腾过的一个小项目重新整理了一遍&#xff0c;名字就叫skills。起因很朴素&#xff1a;我有段时间觉得自己什么都在学&#xff0c;但真到要写简历、做团队技能盘点的时候&#xff0c;反而一句话都说不出来。技能点分散在简历、笔记、聊天记录、各个项目的 README 里…

作者头像 李华
网站建设 2026/10/7 6:51:56

交越失真与甲乙类偏置:用LTspice仿真彻底看清输出级那道坎

如果你调过功放或者运放的输出级&#xff0c;一定见过这个画面&#xff1a;输入一个干干净净的正弦波&#xff0c;输出波形却在过零那一下突然“迟钝”&#xff0c;像是被什么东西绊了一脚&#xff0c;形成一道肉眼可见的台阶。我最早做音频功放时&#xff0c;为这道台阶折腾了…

作者头像 李华
网站建设 2026/10/7 6:51:07

场效应管放大电路静态工作点计算与偏置电路设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 6:50:56

ponytail 插件与 skill 实战:用收拢思维打造高效工作流

1. 从“ponytail”这个标题说起&#xff1a;它到底是什么第一次看到“ponytail”这个词&#xff0c;很多人脑子里浮现的是发型——马尾辫。但在技术社区和效率工具圈子里&#xff0c;ponytail 已经变成了一个特定的符号&#xff1a;它代表一种“把散乱的东西扎起来、收拢成一股…

作者头像 李华