1. 为什么SVN备份不能靠“复制粘贴”
1.1 先理解仓库的物理构成,才能明白备份的逻辑
很多团队用SVN好几年,仓库积累了上万次提交,但真正做过备份的少之又少。我见过不少同事把“备份”理解为把仓库目录整个拷贝一份到移动硬盘,然后心安理得地继续开发。直到某天服务器硬盘故障、或者误操作删了仓库,才发现这份“拷贝”根本没法用。
要搞清楚SVN备份怎么做,先得知道仓库里到底装了什么。以最常见的FSFS格式为例,一个标准仓库目录的结构大致是:
text复制repo/
├── README.txt
├── conf/
│ ├── authz
│ ├── passwd
│ └── svnserve.conf
├── db/
│ ├── current
│ ├── format
│ ├── revprops/
│ ├── revs/
│ └── transactions/
├── format
├── hooks/
│ ├── post-commit.tmpl
│ ├── pre-commit.tmpl
│ └── ...
└── locks/
└── db.lock
如果你直接在运行中的仓库目录上执行文件系统的复制,大概率会碰到几个问题:
第一,写操作正在进行时,数据文件可能处于不一致状态。 SVN的每次提交涉及多个文件写入,包括revs目录下的新版本文件、revprops下的属性文件、current指针的更新。如果在某个提交过程中直接复制,可能出现版本文件存在但current指针还没更新、或者revprops缺失的情况。
第二,直接复制出来的仓库无法通过完整性校验。 复制完成后你跑一下 svnadmin verify,很容易报出校验错误。
第三,FSFS格式本身就是为“仓库目录内原子操作”设计的,但文件系统的拷贝并不保证原子性。 数据库类型的仓库(BDB格式)更不用说,直接复制几乎必坏。
所以,正确的SVN备份思路是:要么通过SVN提供的管理工具导出为独立文件(svnadmin dump),要么使用专门的热备份命令(svnadmin hotcopy),要么搭建跨仓库的同步镜像(svnsync)。
1.2 备份目标不是“存下来”,而是“能恢复”
我在很多项目和团队中推进过SVN备份方案,最强调的一点就是:备份的价值只有在恢复时才能体现。你辛辛苦苦跑完备份脚本,生成了几十GB的dump文件,但如果从来没验证过它能load回来,那和没备份没本质区别。
所以正常备份方案应该包含三个核心指标:
- 可恢复性:备份文件是否完整、能否通过
svnadmin verify、能否在干净环境里load成功。 - 可追溯性:备份文件中包含哪些版本号范围、备份时间、仓库UUID是否保留。
- 可自动化:能否定时执行、增量备份、自动清理过期文件。
如果把这三点记牢,后面设计备份脚本和恢复预案时就不会跑偏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种SVN备份方式选型:dump、hotcopy还是svnsync
2.1 svnadmin dump:慢但通用,跨版本迁移的可靠选择
svnadmin dump 是SVN自带的导出工具,它把仓库中的所有版本数据按顺序输出为一个纯文本格式的dump文件。这个文件包含了从版本0到指定版本的所有提交记录、路径变更、文件内容和属性信息。
基本用法如下:
bash复制# 全量备份仓库 repo 的所有版本
svnadmin dump /path/to/repo > full_backup.dump
# 备份指定版本范围
svnadmin dump /path/to/repo -r 1000:2000 > range_backup.dump
# 增量备份某个版本区间
svnadmin dump /path/to/repo -r 1500:2000 --incremental > incremental_1500_2000.dump
dump方式最大的优势是通用性强。它输出的文件是SVN定义的标准格式,不依赖仓库的物理存储格式,也不依赖版本库的FSFS/BDB类型。所以当你需要做跨大版本升级(比如从SVN 1.6升到1.14)、或者要把仓库迁移到另一台完全不同的服务器上,dump和load是最稳妥的组合。
缺点也很明显:速度慢、文件体积大。对一个几千个版本、占用几十GB磁盘的仓库跑全量dump,可能需要好几个小时。这也是为什么实际生产中要做周期性全量+定期增量的组合方案,而不能每天都全量dump。
2.2 svnadmin hotcopy:物理级热备份,速度快的首选
svnadmin hotcopy 是在线复制仓库目录,它能在SVN服务不停止的情况下,产生一个一致性快照。
bash复制svnadmin hotcopy /path/to/repo /backup/repo_hotcopy
我用hotcopy的感受是,它的备份速度比dump快得多,因为本质上是在文件系统层面复制整个仓库目录,不需要遍历逐个版本生成文本格式。恢复也很快——直接把hotcopy出来的目录挪到目标位置、配置好服务就行。
不过要注意,hotcopy有它的限制:
- 不能跨大版本。hotcopy默认只能在相同或相近版本的SVN之间使用,跨大版本时可能因为内部格式变化导致恢复失败。
- 备份内容包含配置和hooks。这是优势也是隐患,它会把conf目录下的权限配置、passwd用户文件、hooks目录全部一起复制。好处是恢复仓库时不需要再手工配置一遍;坏处是如果仓库的authz、passwd文件里存了敏感信息,备份文件本身也要做好权限管理。
- 不便于增量备份。hotcopy每次都是全量物理复制,不像dump那样可以针对版本区间做增量。
对于日常单机备份、同版本恢复,hotcopy是性价比最高的方案。我在公司内部的SVN服务器上,就是每周做一次hotcopy全量备份。
2.3 svnsync:跨机镜像,适合容灾场景
svnsync 是SVN的仓库同步工具,它把源仓库的版本数据持续同步到一个镜像仓库。它的核心使用场景是异地容灾——当主仓库发生故障时,可以从镜像仓库继续提供服务或恢复数据。
bash复制# 在目标机创建镜像仓库并初始化
svnadmin create backup_repo
svnsync initialize file:///backup/backup_repo https://svn.example.com/original_repo
# 同步数据
svnsync synchronize file:///backup/backup_repo
svnsync的本质是增量同步,每次同步只拉取新增的版本。相比定时跑dump,它能让灾备仓库的版本号跟主仓库保持非常接近。
但它有个很关键的坑:镜像仓库不允许直接写提交。它只能通过svnsync同步,不能作为普通仓库供开发人员直接提交代码。如果主仓库故障,需要把镜像仓库转正或调整服务配置,才能接管开发。
此外,svnsync要求源仓库的pre-revprop-change hook允许修改revprops,否则初始化时设置同步标记会失败。如果你要配置svnsync,这个hook必须提前配置好。
2.4 方案对比与选择建议
| 维度 | svnadmin dump | svnadmin hotcopy | svnsync |
|---|---|---|---|
| 备份速度 | 慢 | 快 | 持续增量,较快 |
| 文件大小 | 大(文本格式) | 与仓库体积相当 | 镜像仓库与源仓库相当 |
| 增量支持 | 支持(--incremental) | 不支持 | 天然增量 |
| 跨版本迁移 | 支持 | 不支持 | 不支持 |
| 恢复复杂度 | 需建新库再load | 直接替换目录 | 转正或load |
| 包含配置/hooks | 不包含 | 包含 | 不包含 |
| 适用场景 | 跨版本迁移、长期归档 | 同机/同版本快速备份 | 异地容灾、实时镜像 |
真实场景中,这三个工具不是互斥的,可以组合使用。比如我在公网服务器上就是“每周hotcopy全量 + 每日svnsync镜像”双保险;在需要长期归档的仓库上,会额外跑季度级的dump。
3. 落地:一套能直接跑的全量+增量备份脚本
3.1 核心思路:全量定期、增量穿插、日志留痕
理解了工具选型后,接下来要解决的是实操问题。如果只靠手动执行命令,运维人员早晚会漏掉某次备份。所以必须脚本化。
我推荐的备份策略是:
- 每月1号凌晨执行一次全量
dump(或hotcopy,看需求); - 每周执行一次
hotcopy作为快速快照; - 每天执行一次增量
dump,记录从上次备份以来的新增版本; - 所有备份文件保留N份,自动清理过期文件;
- 每次备份生成日志,至少记录版本号、文件大小、校验值。
3.2 增量备份的关键点:版本号追踪
增量dump的原理很简单:根据版本号区间导出新增部分。但这里必然要回答一个问题——从哪个版本开始增量?
我见过很多新手脚本,直接写死 -r 1000:2000,这等于把增量备份做成了定时导出某个固定区间,下一轮又重复导同一批版本,毫无意义。
正确的做法是,把“上一次备份到哪个版本号”记录下来。比如每次执行完备份,把最新的仓库版本号(HEAD revision)写入一个last_rev文件。下次增量备份时,读取这个文件,用 last_rev+1 : HEAD 作为dump区间。
获取当前仓库HEAD版本号,可以用 svnlook youngest:
bash复制svnlook youngest /path/to/repo
完整脚本可以采用如下思路(此处给出一份Linux bash版本):
bash复制#!/bin/bash
# SVN增量备份脚本
REPO_DIR="/data/svn/repos/myproject"
BACKUP_DIR="/backup/svn"
TODAY=$(date +%Y%m%d)
LAST_REV_FILE="$BACKUP_DIR/incremental_last_rev.txt"
# 如果还没有记录文件,则从0开始(即全量)
if [ ! -f "$LAST_REV_FILE" ]; then
START_REV=0
else
START_REV=$(cat "$LAST_REV_FILE")
fi
HEAD_REV=$(svnlook youngest "$REPO_DIR")
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始增量备份,start_rev=$START_REV, head_rev=$HEAD_REV"
if [ "$START_REV" -lt "$HEAD_REV" ]; then
svnadmin dump "$REPO_DIR" -r "$((START_REV+1)):$HEAD_REV" --incremental \
| gzip > "$BACKUP_DIR/incremental_${TODAY}_${START_REV}_${HEAD_REV}.dump.gz"
echo "$HEAD_REV" > "$LAST_REV_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 备份完成,文件: incremental_${TODAY}_${START_REV}_${HEAD_REV}.dump.gz"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 没有新增版本,跳过备份"
fi
需要注意,脚本里的 svnadmin dump -r $((START_REV+1)):$HEAD_REV --incremental 和普通dump的区别在于,--incremental 导出的内容包含了被修改目录的完整路径信息,便于后续load时正确合并。如果你不指定 --incremental,会默认导出每个版本的完整目录树快照,导致文件变得异常巨臃肿,这违背了增量备份的意义。
3.3 全量备份脚本:结合gzip压缩与验证
全量备份不建议直接裸dump,因为文本格式的dump文件压缩率很高,gzip之后再存储能节省大量空间。以一个1万+提交、仓库体积20GB的项目为例,dump文件可能达到5~8GB,但gzip后往往能压缩到1GB左右。
bash复制#!/bin/bash
REPO_DIR="/data/svn/repos/myproject"
BACKUP_DIR="/backup/svn"
TODAY=$(date +%Y%m%d)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始全量备份"
svnadmin dump "$REPO_DIR" | gzip > "$BACKUP_DIR/full_${TODAY}.dump.gz"
if [ $? -eq 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 全量备份完成"
# 更新增量备份的基础版本号
svnlook youngest "$REPO_DIR" > "$BACKUP_DIR/incremental_last_rev.txt"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 全量备份失败,请检查"
exit 1
fi
# 清理:只保留最近60天的全量备份
find "$BACKUP_DIR" -name "full_*.dump.gz" -mtime +60 -delete
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 清理完成"
这段脚本里有一个容易被忽略的细节:全量备份完成后,必须更新增量备份的基准版本号文件。否则全量备份之后紧接着的增量备份,可能从旧基准开始,导致中间版本数据重复。虽然重复数据一般不会导致load失败,但会让备份文件白白膨胀,也容易在恢复时造成困惑。
3.4 Windows环境下的等价做法
很多个人开发者或者小团队用的是Windows服务器,脚本没法直接用bash。Windows下可以写批处理脚本,配合任务计划程序来跑。
下面是一个简化的PowerShell版本:
powershell复制$repoDir = "D:\svn\repos\myproject"
$backupDir = "D:\svn\backup"
$today = Get-Date -Format "yyyyMMdd"
$lastRevFile = Join-Path $backupDir "incremental_last_rev.txt"
$headRev = & svnlook youngest $repoDir
if (Test-Path $lastRevFile) {
$startRev = [int](Get-Content $lastRevFile)
} else {
$startRev = 0
}
if ($startRev -lt $headRev) {
$outFile = Join-Path $backupDir "incremental_${today}_${startRev}_${headRev}.dump"
& svnadmin dump $repoDir -r "$($startRev+1):$headRev" --incremental | Out-File -FilePath $outFile -Encoding utf8
Set-Content -Path $lastRevFile -Value $headRev
}
Windows下的计划和清理逻辑,可以直接用任务计划程序触发,清理策略用PowerShell脚本定时执行即可。核心思路和Linux版本一致:记录版本号、增量区间、定期全量。
3.5 定时任务配置示例
Linux推荐使用crontab:
bash复制# 每天2:30执行增量备份
30 2 * * * /usr/local/scripts/svn_incremental_backup.sh >> /var/log/svn_backup.log 2>&1
# 每月1号3:00执行全量备份
0 3 1 * * /usr/local/scripts/svn_full_backup.sh >> /var/log/svn_backup.log 2>&1
这里要提醒一句,crontab的环境变量和交互Shell不同。SVN的bin目录(通常位于 /usr/bin 或 /usr/local/bin)可能不在脚本的PATH中,导致crontab执行时报“command not found”。稳妥做法是在脚本开头显式加:
bash复制export PATH=/usr/local/bin:/usr/bin:/bin
4. 恢复才是备份的终点:load、验证与演练
4.1 dump文件如何恢复为新仓库
备份做完后,真正考验人的是恢复环节。先用dump文件恢复一次看看。
步骤非常简单:
bash复制# 1. 创建新仓库
svnadmin create /data/svn/repos/myproject_restored
# 2. 如果需要恢复到指定路径,可以用 --parent-dir
svnadmin load /data/svn/repos/myproject_restored < full_backup.dump
如果是增量备份的恢复,需要注意恢复顺序:先做最近一次全量恢复,然后按时间顺序依次load各个增量dump文件。
bash复制svnadmin load /data/svn/repos/myproject_restored < full_20250301.dump
svnadmin load /data/svn/repos/myproject_restored < incremental_20250302_1000_1200.dump
svnadmin load /data/svn/repos/myproject_restored < incremental_20250303_1200_1400.dump
如果dump文件是用gzip压缩的,记得先解压再load:
bash复制gunzip -c full_20250301.dump.gz | svnadmin load /data/svn/repos/myproject_restored
恢复完成后,跑一遍 svnadmin verify,确认版本库结构完整。
4.2 hotcopy备份的恢复方式
hotcopy的恢复就更直接了——因为hotcopy出来的本身就是一个完整的仓库目录。只需要把它放到你希望的位置,然后启动SVN服务指向该目录即可。
bash复制# 假设备份目录是 /backup/svn/repo_hotcopy
cp -a /backup/svn/repo_hotcopy /data/svn/repos/myproject_restored
# 然后调整权限并启动服务
这里有一个容易踩的坑:SVN服务运行账号对仓库目录必须有写权限,尤其是db目录下的事务文件。用 cp -a 全量复制后,文件的所有者可能仍然停留在备份时的用户上,这会导致svnserve或Apache无法写入日志和事务文件,表现为提交时报 authorization failed 或 “Can't open file ... Permission denied”。恢复后一定要记得:
bash复制chown -R svn:svn /data/svn/repos/myproject_restored
chmod -R 700 /data/svn/repos/myproject_restored
4.3 定期验证备份的可用性
我强烈建议把“验证备份”也做成一个定期任务,而不是等到真的发生灾难时才想起验证。最简单的验证方式:
- 每个月从备份文件中随机挑一个,恢复到临时目录;
- 执行
svnadmin verify检查仓库完整性; - 用
svn log、svn ls或checkout部分文件随机比对版本号; - 验证完成后删除临时恢复目录,不占用生产空间。
如果这个流程没跑通,备份脚本产出的只是一堆占硬盘的数据,不是保障。
4.4 恢复演练中经常遇到的几个坑
我做过几次完整恢复演练,把真实的坑记录在这里:
坑一:增量备份恢复顺序或区间搞错。 比如全量备份的HEAD版本是1200,但增量脚本从1000开始记录,导致后面load增量时出现重复版本。SVN的load遇到重复版本不会直接报错,但会提示跳过。最尴尬的情况是你以为恢复完整了,最后发现版本号缺失或revprops错乱。所以每次增量备份后,建议在脚本里同时输出备份的版本区间,恢复前先核对。
坑二: --incremental 参数缺失,导致dump文件异常巨大。 如果你在全量dump时忘了写 --incremental,只要不加上它,默认导出的就是每个版本的完整快照信息,文件会超大;反过来,如果你在增量dump时写了 --incremental,但传入的版本区间却覆盖了从0开始的版本,就会出现部分版本有完整数据、部分版本是增量数据,虽然load时能识别,但恢复过程会非常慢。所以,全量要裸dump,增量必须加 --incremental,这个习惯一定要养成。
坑三:钩子脚本未随dump恢复。 dump文件只包含版本库数据和版本属性,不包含conf、hooks、locks等目录内容。用dump恢复出的仓库,必须手动重新配置authz、passwd、post-commit邮件通知等钩子。我在一次演练中恢复了仓库之后,发现提交代码没有邮件通知,排查了半天才发现hooks目录是空的。所以如果仓库有自定义钩子,建议把hooks目录和conf目录单独备份一份。
5. 备份之外的连带问题:迁移、客户端与日常维护
5.1 备份迁移后客户端的relocate操作
当备份用于服务器迁移时,仓库的URL地址大概率会变化。对于团队成员来说,最直观的问题是TortoiseSVN或IDEA里的工程无法正常update和commit。
TortoiseSVN的解决方法是在工作副本根目录上选择 SVN Relocate,然后把URL从旧地址改成新地址。注意,Relocate不同于Checkout,它不会重新下载文件,只更新工作副本中的元数据URL指向。如果版本库的UUID没有变化,Relocate通常非常快。
IDEA里的操作为:VCS -> Subversion -> Relocate,在弹出的对话框中输入新仓库地址即可。如果IDEA版本较老,可能菜单路径不同,但关键词就是Relocate。
如果迁移过程中仓库的UUID发生了变化(比如用 svnadmin create 新建库再load,默认会生成新UUID),Relocate可能不生效,此时需要更新UUID。可以在新仓库上执行:
bash复制svnadmin setuuid /path/to/new_repo <旧UUID>
或者干脆让每个开发成员重新checkout一份工作副本,一了百了。
5.2 hook脚本与备份的联动
很多人不知道,SVN仓库的hooks目录并不在版本库数据流中。你写一个post-commit脚本,实际上不会通过 svnadmin dump 导出。因为dump导出的是“版本数据”,而hooks是服务器侧配置。
所以前面提到的“dump备份不包含hooks”,在恢复时就要额外处理。我见过的实践是:在备份脚本中额外打包一个config_backup目录,把每个仓库的conf和hooks目录都单独tar一份:
bash复制tar czf /backup/svn/config_backup_${TODAY}.tar.gz /data/svn/repos/*/conf /data/svn/repos/*/hooks
这样恢复时只需要解压覆盖,就能保留所有自定义逻辑。
5.3 备份文件损坏的排查思路
如果你在恢复过程中遇到 svnadmin load 突然报错,或者客户端checkout时报“核验码不一致”(常见于 svn: E200014: Checksum mismatch 这类错误),首先不要慌,按顺序排查:
- 检查dump文件本身:用
gzip -t backup.dump.gz验证压缩包是否完整。如果这一步就报错,说明备份文件在传输或存储过程中损坏了。 - 检查版本库完整性:对源仓库执行
svnadmin verify,如果源仓库本身有问题,后续备份再多次也没用。 - 检查磁盘空间:load过程中需要临时存储增量数据,如果可用空间不足,会导致load中途失败,留下半成品的仓库。
- 对比校验值:备份完成后立即计算MD5/SHA256,传输到异地后再校验一次,防止网络传输损坏。
这几条中,第一条——用 gzip -t 检查压缩包完整性——是最容易且最有效的排查手段。我习惯在备份脚本末尾自动执行这条校验。
5.4 仓库体积增长后的优化方向
仓库跑几年以后,体积会膨胀得很厉害。SVN自带了一个 svnadmin pack 命令,可以把FSFS仓库中对某个版本区间的多个revs文件合并打包成单个packed文件,减少inode数量、加快访问速度。
bash复制svnadmin pack /path/to/repo
pack命令可以在线执行,不影响仓库的正常访问。做完pack之后,备份速度通常会有所提升,因为文件系统元数据减少了。对于大仓库,我建议在每月全量备份前执行一次pack,让备份时的磁盘读取更高效。
另外,如果仓库里有大量历史大文件(比如误提交过几个GB的二进制文件),dump文件会非常大,也很难瘦身。这时候可以考虑用 svnadmin dump -r 分段导出,把旧版本归档到冷存储,新版本保留在热仓库。不过这种情况较少见,一般团队用不上。
6. 最后再分享一点我个人的切身体会
SVN备份这件事,表面上看是无脑跑命令,但真正决定成败的往往是那些“看起来不起眼”的细节——版本号追踪、增量区间、恢复验证、hooks备份、客户端relocate。任何一个环节断了,都可能在关键时刻给你“惊喜”。
我自己踩过最大的坑,就是某次在备份脚本里图省事,把增量备份的版本号记录直接写死在脚本里。结果服务器重启后,脚本重跑了一轮相同的增量,随后又隔了一周才做全量,导致恢复时我把重复数据load了两遍,费了好大劲才清理干净。从那以后,我把“版本号文件必须由脚本动态生成、每次备份后自动更新”写成了团队备份规范,再也没出过同类事故。
如果你现在还没有一套成型的SVN备份方案,强烈建议别再拖了。仓库不等人,数据也不等人。从今天开始,按文章里的步骤把全量脚本挂上定时任务,每周验证一次备份文件,顺手做一次恢复演练。等你真正经历过一次完整的备份—恢复流程,那种踏实感是任何文档和攻略都给不了的。
