This text is a work in progress—highly subject to change—and may not accurately describe any released version of the Apache™ Subversion® software. Bookmarking or otherwise referring others to this page is probably not such a smart idea. Please visit http://www.svnbook.com/ for stable versions of this book.

仓库维护

维护 Subversion 仓库可能会令人望而却步, 大部分是由于后端存储所固 有的复杂度. 完成任务的关键在于如何充分地利用工具—它们是什么, 什么时候使用它们, 以及如何使用. 本节将会介绍如何使用 Subversion 提供的 管理工具完成常见的仓库维护工作, 例如数据迁移, 升级, 备份和清理.

管理员工具箱

Subversion 提供了几个用于创建, 检查, 修改和修复仓库的工具, 下面简 单介绍一下这些工具.

svnadmin

命令 svnadmin 是仓库管理员最优秀的助手. 除了可以 用来创建仓库外, 它还提供了几种维护操作. svnadmin 的使用方法和其他的 Subversion 命令行工具非常类似:

$ svnadmin help
general usage: svnadmin SUBCOMMAND REPOS_PATH  [ARGS & OPTIONS ...]
Type 'svnadmin help <subcommand>' for help on a specific subcommand.
Type 'svnadmin --version' to see the program version and FS modules.

Available subcommands:
   crashtest
   create
   deltify
…

在本章的前面 (“创建仓库”一节) 我们已经介绍了子命令 svnadmin create, 稍后就会介绍 svnadmin 的其他子命令. 关于 svnadmin 完整的子命令列表, 以及它们的功能, 见 svnadmin 参考手册—Subversion 仓库管理工具.

svnlook

svnlook 用于查看仓库的各个版本号和 事务 (transactions, 正在生成过程中的版本号), 不会向仓库执行写操作. svnlook 经常被钩子使用, 用于报告仓库即将提交或刚刚提交的修改, 用到的钩子分别是 pre-commit 和 post-commit. 管理员还可以用 svnlook 诊断问题.

svnlook 的语法非常直观:

$ svnlook help
general usage: svnlook SUBCOMMAND REPOS_PATH [ARGS & OPTIONS ...]
Note: any subcommand which takes the '--revision' and '--transaction'
      options will, if invoked without one of those options, act on
      the repository's youngest revision.
Type 'svnlook help <subcommand>' for help on a specific subcommand.
Type 'svnlook --version' to see the program version and FS modules.
…

svnlook 的大部分子命令都能根据一个给定的版本 号或事务, 输出与前一个版本号的不同之外或事务本身的信息, 指定版本号和 事务的选项分别是 --revision (-r) 和 --transaction (-t). 如果没有 指定 --revision (-r) 和 --transaction (-t), svnlook 将查看仓库最年轻的版本号 (即 HEAD). 如果版本号 19 是仓库 /var/svn/repos 目前最年轻的版本号, 则下面的 2 个命令是等价的:

$ svnlook info /var/svn/repos
$ svnlook info /var/svn/repos -r 19

有一个例外是子命令 svnlook youngest 不接受 任何选项, 只是打印出仓库最年轻的版本号:

$ svnlook youngest /var/svn/repos
19
$
[注意] 注意

始终记住, 管理员只能浏览未提交的事务, 但这种事务在大多数仓库中 都不存在, 因为事务要么是已提交了的 (此时应该使用选项 --revision (-r)), 要么被中止并 删除.

svnlook 的输出既能被人类理解, 也能被程序解析. 例如, 假设 svnlook info 的输出是:

$ svnlook info /var/svn/repos
sally
2002-11-04 09:29:13 -0600 (Mon, 04 Nov 2002)
27
Added the usual
Greek tree.
$

根据输出顺序, svnlook info 的输出由以下几 部分构成:

  1. 作者, 然后是换行符

  2. 日期, 然后是换行符

  3. 日志消息的字符个数, 然后是换行符

  4. 日志消息本身的内容, 然后是换行符

打印的内容是人类可读的, 例如对于日期来说, 它是以文本形式表示, 而不是某种很费解的形式 (例如把日期表示成距离某个特殊日期的 纳秒数). 打印的内容同时也能被程序解析, 因为日志消息可能有很 多行, 在长度上也没有限制, 所以 svnlook 在打印日 志消息本身的内容之前, 先打印消息的长度, 这就允许处理 svnlook 输出的程序针对日志消息做出更明智的决策, 例如为日志消息分配多少内存, 或者是应该跳过多少字节才能完全跳过日志 消息.

svnlook 还能执行很多查询: 只显示上面提到的 信息的部分内容, 递归地列出被版本控制的目录树, 报告哪些路径在给定 的版本号或事务中被修改了, 显示文件和目录的内容和属性的变化, 等等. svnlook 完整的参考手册, 见 svnlook 参考手册—Subversion 仓库检查工具.

svndumpfilter

虽然 svndumpfilter 不是管理员最常使用的工具 之一, 但它却提供了一种非常特别且有用的功能—它可以作为一种基 于路径的过滤器, 快速地修改 Subversion 仓库历史的数据流.

svndumpfilter 的使用语法是:

$ svndumpfilter help
general usage: svndumpfilter SUBCOMMAND [ARGS & OPTIONS ...]
Type 'svndumpfilter help <subcommand>' for help on a specific subcommand.
Type 'svndumpfilter --version' to see the program version.
  
Available subcommands:
   exclude
   include
   help (?, h)

svndumpfilter 包含了 2 个子命令: svndumpfilter excludesvndumpfilter include, 它们允许管理员从历史数据流中排除或保留指定的 路径. 在本章的 “过滤仓库历史”一节 读者将会学习到子命令的用法, 以及它们的典型应用.

svnrdump

简单点说, svnrdump 本质上就是把 svnadmin dumpsvnadmin load 与网络有关的部分单独拿出来, 形成一个单独的程序.

$ svnrdump help
general usage: svnrdump SUBCOMMAND URL [-r LOWER[:UPPER]]
Type 'svnrdump help <subcommand>' for help on a specific subcommand.
Type 'svnrdump --version' to see the program version and RA modules.

Available subcommands:
   dump
   load
   help (?, h)

$

我们将在 “迁移仓库数据”一节 介绍 svnrdump 和上面提到的 2 个 svnadmin 子命令.

svnsync

svnsync 提供了维护 Subversion 仓库只读镜像 所需的全部功能. 该命令只做一件事—把一个仓库的历史传送到另一 个仓库, 完成这项工作的方式有很多种, 但它最方便的地方在于操作可以 远程执行—同步 仓库, 以及 svnsync 程序都可以在不同的主机上.

svnsync 的语法和本章介绍的其他命令非常 类似.

$ svnsync help
general usage: svnsync SUBCOMMAND DEST_URL  [ARGS & OPTIONS ...]
Type 'svnsync help <subcommand>' for help on a specific subcommand.
Type 'svnsync --version' to see the program version and RA modules.

Available subcommands:
   initialize (init)
   synchronize (sync)
   copy-revprops
   info
   help (?, h)
$

关于如何使用 svnsync 复制仓库的更多内容, 我们 将在 “仓库复制”一节 介绍.

fsfs-reshard.py

虽然脚本 fsfs-reshard.py (放在 Subversion 源代码目录的 tools/server-side 子目录内) 不是 Subversion 工具集的正式成员, 但是如果仓库使用 FSFS 作为后端存储, 那么管理员就可以用 fsfs-reshard.py 优化性能. 基于 FSFS 的仓库把版本号的相关信息记录在单独的文件里, 有时候这些 文件会全部存放在一个目录里, 有时候分散在多个目录里.

FSFS 的早期版本把所有的版本号文件—每一个版本号都有对应的 一个文件—放在一个目录里, 在仓库的整个生命周期内, 目录内的文件 数量都在增长. 有些系统对同一目录内的文件数量限制较大, 即使是限制较 宽或没有限制的系统, 如果目录内的文件数量较多也会产生严重的性能问题.

从 Subversion 1.5 开始, 基于 FSFS 的仓库在布局上稍有变化: 存放 版本号文件的目录 (和其他不断增长的目录) 是 碎化地 (sharded), 或 者说版本号文件分散在多个目录内. 这可以大幅减少系统定位文件的时间, 从而提高仓库的整体读取性能.

在同一个目录内可以存放的文件数量是可配置的 (虽然默认值对于大多 数平台而言都是比较合理的), 但是如果在仓库使用一段时间后, 再去修改 这个配置, 将导致 Subversion 无法定位当前正在搜索的文件. 这时候 fsfs-reshard.py 就派上的用场.

fsfs-reshard.py 重新编排仓库的目录结构, 以满 足需要被碎化的子目录的个数要求, 并更新仓库的配置, 以便修改能够持久 化地保留下来. fsfs-reshard.pysvnadmin upgrade 配合使用时, 对于把 1.5 版之前 的仓库升级成最新的文件系统格式和碎化文件非常有用 (Subversion 不会 自动地对文件进行碎化). fsfs-reshard.py 还能 继续优化已经碎化过的仓库.

修正提交日志消息

有时候, 用户可能会写错提交日志消息 (例如拼写错误, 或包含了错误的 信息). 如果仓库的配置允许提交结束后仍然可以对日志消息进行修改 (使用 钩子 pre-revprop-change, 见 “实现仓库钩子”一节), 用户就可以用命令 svn propset (见 svn 参考手册—Subversion 命令行客户端svn propset (pset, ps)) 修正 包含错误的日志消息. 然而, 这种做法可能会导致信息 永久丢失, 因此 Subversion 仓库的默认配置是禁止修改未版本化的属性— 只有管理员除外.

如果管理员需要修改日志消息, 将会用到的命令是 svnadmin setlog. 命令从给定的文件中读取新的日志 消息, 覆盖掉指定的版本号的日志.

$ echo "Here is the new, correct log message" > newlog.txt
$ svnadmin setlog myrepos newlog.txt -r 388

默认情况下, svnadmin setlog 受到的限制与企图 修改未版本化属性的客户端受到的限制是一样的—钩子 pre-revprop-change 和 post-revprop-change 仍然会被触发, 因此命令若想 成功执行还要求仓库进行相应的配置. 但管理员可以通过向 svnadmin setlog 添加选项 --bypass-hooks, 绕开这条限制.

[警告] 警告

记住, 如果选择绕开钩子, 很可能会导致属性发生变化时, 不发送通知 邮件, 备份系统不对未版本化的属性进行备份, 以及诸如此类的事情. 管理 员必须非常清楚自己在做什么.

管理磁盘空间

虽然最近几年, 存储的成本已经下降了很多, 但是如果是对大量的数据进行 版本控制, 那么管理员仍然需要关注磁盘的使用情况. 在一个活跃的仓库中, 每增加一条版本历史, 就需要把历史信息备份到其他地方, 有可能还会为了 保险起见而多次备份. 为了降低备份的数据量和磁盘消耗, 知道仓库的哪些 数据需要保持在线, 哪些数据需要备份, 哪些数据可以删除—就显得非常 重要.

Subversion 如何节约磁盘空间

为了尽量降低仓库的存储空间消耗, Subversion 使用了 增量存储技术 (deltification). 增量存储技术通过一块数据及 相对于它的一系列差异来表示另一块数据, 如果两块数据的差异非常小, 增量存储技术就可以仅保存其中一组数据以及两组数据之间的差异, 而不 需要同时保存两组数据, 从而节省存储空间. 采用增量存储技术的结果是 原本体积庞大的文件, 其所消耗的存储空间与全文保存相比, 只占很小的 一部分.

在一开始设计 Subversion 时, 就已经包含了增量存储技术, 后来也对其进 行不断地改进. 从 1.4 开始, Subversion 将对全文表示的文件内容进行 压缩. 从 1.6 开始, 新特性 表示共享 (representations sharing) 为 Subversion 节 省了更多的空间. 该特性允许内容相同的多个文件或多个版本号引用 到一个单一的共享数据实例上, 而不是每个人都存一份自己的副本.

删除僵死的事务

虽然不太常见, 但仍然存在提交失败的情况, 此时便会在仓库中留下 残骸—一个未提交的事务和与之相关的文件或目录修改. 造成提交 失败的原因很多, 可能是客户端操作被用户粗暴地终止, 也可能是在操作 执行中发生了网络错误. 不管是什么原因, 僵死事务总是有可能出现, 除 了占用存储空间外, 它们并不会产生实际的危害, 但一个讲究的管理员的眼 睛里是揉不得沙子的.

管理员可以用命令 svnadmin lstxns 列出未完成 事务:

$ svnadmin lstxns myrepos
19
3a1
a45
$

输出中的每一项都能用作 svnlook (添加选项 --transaction (-r)) 的参数, 用来 判断是谁, 在什么时候创建了这个事务, 在这个事务中做了哪些类型的修改. 这些信息有助于管理员判断该事务是否可以安全地删除. 如果确实是要删除 一个事务, 就把它的名字作为命令 svnadmin rmtxns 的参数. 实际上, svnadmin lstxns 的输出可以直接 作为 svnadmin rmtxns 的输入!

$ svnadmin rmtxns myrepos `svnadmin lstxns myrepos`
$

如果管理员打算以上面这种形式执行这两个命令, 就得考虑暂时禁止 客户端对仓库的访问, 避免在你清理期间有合法的事务产生. 例 5.3 “txn-info.sh (打印未完成的事务)” 展示 了如何编写一个 shell 脚本来打印仓库中未完成的事务.

例 5.3. txn-info.sh (打印未完成的事务)

#!/bin/sh

### Generate informational output for all outstanding transactions in
### a Subversion repository.

REPOS="${1}"
if [ "x$REPOS" = x ] ; then
  echo "usage: $0 REPOS_PATH"
  exit
fi

for TXN in `svnadmin lstxns ${REPOS}`; do 
  echo "---[ Transaction ${TXN} ]-------------------------------------------"
  svnlook info "${REPOS}" -t "${TXN}"
done

脚本的输出基本上就是多个 svnlook info 输出 内容 (见 “svnlook”一节) 的拼接, 就像下面这样:

$ txn-info.sh myrepos
---[ Transaction 19 ]-------------------------------------------
sally
2001-09-04 11:57:19 -0500 (Tue, 04 Sep 2001)
0
---[ Transaction 3a1 ]-------------------------------------------
harry
2001-09-10 16:50:30 -0500 (Mon, 10 Sep 2001)
39
Trying to commit over a faulty network.
---[ Transaction a45 ]-------------------------------------------
sally
2001-09-12 11:09:28 -0500 (Wed, 12 Sep 2001)
0
$

一个被废弃了很久的事务通常表示一个失败或中止了的提交. 事务的时间 戳可以提供很有用的信息—比如说, 9 个月前开始的操作有没有可能 还是活跃的?

简言之, 决定清理事务不用太过深思熟虑, 很多信息—包括 Apache 的错误和访问日志, Subversion 的操作日志和版本号历史—都可以作为 决策的参考. 当然, 管理员也可以通过与事务的所有者沟通 (比如说通过 电子邮件) 来判断一个看似僵死的事务, 是否真得处于僵死状态.

FSFS 文件系统压缩

在基于 FSFS 的仓库中, 有些文件描述了在一个单独的版本号做了哪些 修改, 有些文件包含了与一个单独的的版本号相关的版本号属性. Subversion 1.5 之前创建的仓库, 把这两种文件分别放在两个目录里. 当仓库中有新的 版本号生成时, Subversion 就向这两个目录存放更多的文件, 随着时间的推 移, 目录内的文件数量将增长到非常大的规模. 人们已经发现, 这会在某些 基于网络的文件系统上造成严重的性能问题.

第一个问题是操作系统必须在短时间内引用大量不同的文件, 这会导致 磁盘缓存被快速消耗, 于是操作系统会在读取磁盘上花费更多的时间, 表 现在 Subversion 身上, 就是在访问数据时性能低下.

第二个问题比较微妙. 受到大多数文件系统的磁盘空间分配方式的影响, 每一个文件实际消耗的存储空间会多于它要求的存储空间. 为了管理一个文 件所消耗的额外存储空间在 2 KB 到 16 KB 之间, 具体的大小取决于操作 系统的文件系统的实现. 反映到以 FSFS 作为后端存储的仓库身上, 就是每 一个版本号都会产生额外的存储空间消耗. 对于含有大量小版本号的仓库 来说, 最明显的影响就是为了存放版本号文件而消耗的磁盘空间, 会迅速超 过数据的实际大小.

为了解决这 2 个问题, Subversion 1.6 增加了命令 svnadmin pack, 它的作用是把一个完整的碎片内的所 有文件都打包到一个单一的文件内, 然后再删除原来的文件, 从而降低因文 件过多而导致的空间与性能开销.

只要是 1.6 版及以上的文件系统格式, 都可以用 svnadmin pack 进行压缩 (如果文件系统格式较旧, 可以用 svnadmin upgrade 对仓库进行升级, 见 svnadmin 参考手册—Subversion 仓库管理工具svnadmin upgrade). 为了压缩文件系统, 只需要 对仓库执行 svnadmin pack:

$ svnadmin pack /var/svn/repos
Packing shard 0...done.
Packing shard 1...done.
Packing shard 2...done.
…
Packing shard 34...done.
Packing shard 35...done.
Packing shard 36...done.
$

因为打包过程会事先获取所需要的锁, 所以管理员可以在活动仓库上 执行这个操作, 甚至可以作为钩子 post-commit 的一部分. 压缩已经压缩过 的碎片是合法的操作, 但不会对仓库的磁盘使用产生影响.

如果仓库以 BDB 作为后端存储, 则 svnadmin pack 不会对仓库产生任何效果.

迁移仓库数据

Subversion 文件系统的数据分布在仓库的很多文件中, 其存储方式通常 只有 Subversion 开发人员才能理解 (或对它们感兴趣). 然而, 在某些情况 下文件系统的全部或部分数据需要被复制或移动到其他仓库中.

Subversion 通过 仓库转储流 (repository dump streams) 实现这个功能. 仓库 转储流 (如果作为文件存储到磁盘上, 习惯上称为 转储文件) 是一种可移植的平面文件格式, 描述了仓库中的各个版本号—是谁, 在 什么时候, 做了哪些修改等. 这种转储流是仓库之间组织版本历史— 全部的或部分的, 未修改或修改过的—的主要机制. Subversion 提供了 创建与加载转储流所必需的工具: 命令 svnadmin dumpsvnadmin load, 以及 svnrdump.

[警告] 警告

虽然 Subversion 仓库的转储文件格式包含了人类可读的部分内容 和让人感到熟悉的结构 (它的格式很像 RFC 822 格式, 这是大多数邮件 所使用的格式), 但它 不是 一个纯文本文件, 而是二进制文件, 即使是非常细微的修改也会非常敏感. 比如说, 很多 文本编辑器会自动转换行结束符, 但这样做会损坏文件.

有很多情况都需要对 Subversion 仓库数据进行转储和加载. 在 Subversion 的早期阶段, 最常见的原因是 Subversion 的演变. 随着 Subversion 的不断成 熟, 可能会出现这样一种情况: 后端数据库概要的变化会导致旧版本的仓库出现 兼容性问题, 于是管理员必须使用旧版的 Subversion 转储仓库数据, 再把转 出的数据加载到新版 Subversion 创建的仓库中. 从 Subversion 1.0 开始, 不会再出现这种需要转储和加载仓库数据的概要变化, 并且 Subversion 开发 人员承诺在次版本之间升级时 (例如从 1.3 到 1.4), 不会强迫用户转储和加 载仓库. 但除了升级 Subversion 外, 还有其他需要用到转储和加载的场景, 例如重新部署 Berkeley DB 仓库到新的操作系统或 CPU 平台上, 或者在 Berkeley DB 和 FSFS 两种后端存储之间切换, 以及从仓库历史中清除被版本 控制的数据 (在本章的 “过滤仓库历史”一节 介绍).

[注意] 注意

Subversion 仓库的转储文件只是描述了版本历史, 它不会携带任何与 未提交的事务, 文件系统路径上的用户锁, 仓库或服务器的配置 (包括钩子 脚本) 等有关的信息.

利用 Subversion 仓库的转储文件, 管理员还可以实现在不同的后端存储 方式或版本控制系统之间转换. 因为转储文件的绝大部分内容是人类可读的, 用它来描述一般的修改集—修改集中的每一个修改都应当看作是一个新 的版本号—相对来说比较容易. 实际上, 工具 cvs2svn (见 “把 CVS 仓库转换成 Subversion 仓库”一节) 可以通过转储文件, 把 CVS 仓库的内容复制到 Subversion 仓库中.

现在, 我们只关心如何在 Subversion 仓库之间迁移数据, 具体的内容 将在接下来的一节里进行介绍.

使用 svnadmin 迁移仓库数据

无论迁移仓库历史是出于什么样的原因, svnadmin dumpsvnadmin load 的用法都非常 简单直接. svnadmin dump 按照 Subversion 的文件 系统转储格式, 输出一段范围内的版本号. 转储的结果会被打印到标准输出, 而提示性的信息则会打印到标准错误, 这就允许管理员把输出重定向到文件 的同时, 在终端窗口中查看命令的状态输出, 例如:

$ svnlook youngest myrepos
26
$ svnadmin dump myrepos > dumpfile
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
…
* Dumped revision 25.
* Dumped revision 26.

命令执行结束时, 你将得到一个文件 (在上面的例子里, 文件名是 dumpfile), 这个文件包含了在指定的版本号范围 内, 存放在仓库中的所有数据. 因为 svnadmin dump 从仓库中读取版本号的过程和其他 读者 (例如 svn checkout) 读取仓库的过程是一样的, 所以 可以在任意时刻, 安全地执行 svnadmin dump.

svnadmin dump 配对的命令 svnadmin load 从标准输入读取 Subversion 仓库的转储文件, 把文件中 的版本号重放到目标仓库中. 在命令的执行过程中仍然会输出提示性的信息, 不过这次是打印到标准输出:

$ svnadmin load newrepos < dumpfile
<<< Started new txn, based on original revision 1
     * adding path : A ... done.
     * adding path : A/B ... done.
     …
------- Committed new rev 1 (loaded from original rev 1) >>>

<<< Started new txn, based on original revision 2
     * editing path : A/mu ... done.
     * editing path : A/D/G/rho ... done.

------- Committed new rev 2 (loaded from original rev 2) >>>

…

<<< Started new txn, based on original revision 25
     * editing path : A/D/gamma ... done.

------- Committed new rev 25 (loaded from original rev 25) >>>

<<< Started new txn, based on original revision 26
     * adding path : A/Z/zeta ... done.
     * editing path : A/mu ... done.

------- Committed new rev 26 (loaded from original rev 26) >>>

加载的结果是有新的版本号被添加到仓库中—相当于从 Subversion 客户端向仓库提交. 和普通的提交一样, 可以利用钩子, 在加载产生的提交 之前或之后执行特定的操作, 向 svnadmin load 添加 选项 --use-pre-commit-hook--use-post-commit-hook, 可以分别指示 Subversion 为每一个加载的版本号, 执行钩子 pre-commit 和 post-commit. 比如说 管理员可能会利用钩子, 以确保加载的版本号能够经历与普通提交一样的验证 过程. 当然, 在使用这两个选项时要小心—如果钩子 post-commit 会为 每一个新的版本号发送一封邮件, 你可能不希望在几分钟内收到几百上千封 邮件. 关于钩子的更多内容, 在 “实现仓库钩子”一节 介绍.

因为 svnadmin 在转储和加载过程中会用到标准 输出和标准输入, 感兴趣的读者可以试一下像下面这样执行命令 (在管道的 两边甚至可以使用不同版本的 svnadmin):

$ svnadmin create newrepos
$ svnadmin dump oldrepos | svnadmin load newrepos

默认情况下, 转储文件会很大—比仓库要大得多, 这是因为每个 文件的每个版本在转储文件中都是全文本表示. 这是最快速和最简单的方式, 如果转储文件还会被其他程序 (例如压缩程序, 过滤程序等) 处理, 处理 起来也会非常方便. 但是如果管理员需要长时间保存转储文件, 向 svnadmin 添加选项 --deltas 可以 减小转储文件的大小, 从而节省存储空间. 添加选项 --deltas 后, 文件的连续版本号将会以压缩的二进制差 异格式输出—和仓库保存文件版本号的方式是一样的. 添加选项后命令 会执行得更慢一些, 但得到的转储文件大小会更接近仓库的大小.

我们前面已经提到 svnadmin dump 可以输出一段 范围内的版本号. 使用选项 --revision (-r) 指定一个单独的, 或一段范围内的版本号进行转 储. 如果省略该选项, 仓库内所有的版本号都会被转储.

$ svnadmin dump myrepos -r 23 > rev-23.dumpfile
$ svnadmin dump myrepos -r 100:200 > revs-100-200.dumpfile

Subversion 在转储新的版本号时, 输出的信息仅能满足加载过程根据 前面的版本号来重新创建新的版本号. 换句话说, 对于转储文件中给定的 任意一个版本号, 只有在该版本号中被修改了的项目, 才会出现在转储文件 里, 唯一的例外是被当前 svnadmin dump 转储的第一 个版本号.

默认情况下, Subversion 不会把第一个被转储的版本号表示成与前一 个版本号的差异. 第一个原因是对于第一个版本号来说, 它的前一个版本号 在转储文件中不存在, 第二个原因是 Subversion 无法预知加载转储文件的 仓库状态. 为了确保每次执行 svnadmin dump 所产生 的输出是自我完备的, Subversion 在默认情况下将会完整地表示被转储的 第一个版本号, 包括它的每一个目录, 文件, 和版本号的每一个属性.

然而, 管理员可以修改这种默认行为. 如果在转储时添加了选项 --incremental, svnadmin 会把 转储的第一个版本号与前一个版本号作比较, 跟转储范围中剩下的其他 版本号那样, 只输出在版本号中被修改了的内容. 这样做的好处是管理员 可以创建出几个较小的转储文件—而不是一整个大文件—它们被 加载时可以连续进行, 就像下面这样:

$ svnadmin dump myrepos -r 0:1000 > dumpfile1
$ svnadmin dump myrepos -r 1001:2000 --incremental > dumpfile2
$ svnadmin dump myrepos -r 2001:3000 --incremental > dumpfile3

可以用下面的命令序列, 把这几个转储文件加载到新的仓库中:

$ svnadmin load newrepos < dumpfile1
$ svnadmin load newrepos < dumpfile2
$ svnadmin load newrepos < dumpfile3

选项 --incremental 的另一个奇妙用法是向已有的 转储文件添加新的版本号. 比如说, 管理员可能会利用钩子 post-commit, 将每次触发钩子的版本号转储到同一个转储文件中. 又或者是每晚都运行 一个脚本, 把上一次脚本运行结束后, 仓库中新增的版本号转储到同一个 文件中. 利用这种方式, svnadmin dump 就能实现仓库 的备份.

利用转储文件, 还能把不同的几个仓库合并成一个仓库. 为 svnadmin load 添加选项 --parent-dir, 就能为加载过程指定一个新的虚拟根目录, 这就意味着如果你有三个仓库的转储文件—假设文件名分别是 calc-dumpfile, cal-dumpfile, ss-dumpfile—先创建一个将会用来加载所有 转储文件的仓库:

$ svnadmin create /var/svn/projects
$

然后, 在仓库中为每一个转储文件创建一个对应的目录:

$ svn mkdir -m "Initial project roots" \
            file:///var/svn/projects/calc \
            file:///var/svn/projects/calendar \
            file:///var/svn/projects/spreadsheet
Committed revision 1.
$ 

最后, 把转储文件加载到各自对应的目录内:

$ svnadmin load /var/svn/projects --parent-dir calc < calc-dumpfile
…
$ svnadmin load /var/svn/projects --parent-dir calendar < cal-dumpfile
…
$ svnadmin load /var/svn/projects --parent-dir spreadsheet < ss-dumpfile
…
$

使用 svnrdump 迁移仓库数据

Subversion 1.7 引入了一个新工具, svnrdump. 它提供了较为特殊的功能, 本质上就是 svnadmin dumpsvnadmin load (见 “使用 svnadmin 迁移仓库数据”一节) 的跨网络 版本. svnrdump dump 从一个远程仓库转储数据, 打印 到标准输出; svnrdump load 从标准输入读取转储数据, 加载到一个远程仓库上. svnrdump 可以像 svnadmin dump 那样生成增量转储, 甚至可以只转储 仓库的某个子目录, svnadmin 却无法做到这一点.

svnadminsvnrdump 之间 最关键的区别在于后者不需要直接访问仓库, svnrdump 使用与 Subversion 客户端相同的仓库访问 (Repository Access, 简称 RA) 协议完成操作 的远程执行, 因此用户可能需要提供认证证书, 除此之外, 远程交互可能还会 受到 Subversion 服务器的授权限制.

[注意] 注意

svnrdump dump 要求远程服务器运行的是 Subversion 1.4 或更新的版本. 它能生成的转储流的唯一类型, 就是添加 了选项 --deltas 后, svnadmin dump 所生成的转储流. 在典型的使用情况下这并没有什么 特殊之处, 但是如果你想在生成的转储流上执行一些特定类型的转换, 这 些转换可能会受到影响.

[注意] 注意

因为在提交完新的版本号后, 版本号的属性将被修改, 所以 svnrdump load 要求目标仓库通过钩子 pre-revprop-change, 允许修改版本号属性, 更多信息见 Subversion 仓库钩子参考手册pre-revprop-change.

管理员可以一起使用 svnadminsvnrdump, 例如用 svnrdump dump 从远程仓库获取转储流, 然后再把结果通过管道输送给 svnadmin load, 把远程仓库历史复制到本地仓库, 或者反之, 把本地仓库 的历史复制到远程仓库.

[提示] 提示

如果使用 file:// 形式的 URL, svnrdump 也能访问本地仓库, 但仍然需要借助 Subversion 的仓库访问 (Repository Access, 简称 RA) 抽象层— 这种情况下使用 svnadmin 会比较划算.

过滤仓库历史

因为 Subversion 在存储版本历史时, 大量地使用了二进制差异算法和 数据压缩 (在完全封闭的数据库系统中是可选的), 如果管理员觉得不是很 困难而手工地修改历史, 这是非常不明智的做法, 大家应该极力避免这样操作. 数据一旦存储到仓库中, Subversion 通常不会允许管理员轻易地删除数据. [50] 但总会出现需要修改仓库历史的情况, 例如从历史中抹去与某个文件相关的所 有记录, 这个文件出现在历史中只是个意外 (或者因为其他一些原因). [51] 又或者 是多个项目本来共享同一个仓库, 现在你想把它们分别存放到自己独享的一个 仓库中. 为了完成这些任务, 仓库数据需要向管理员提供更加容易管理和延展 的表示形式—Subversion 仓库转储格式.

我们已经在 “迁移仓库数据”一节 说过, Subversion 的仓库转储格式是用户提交到仓库中的修改的表示形式, 它是人类 可读懂的. 使用 svnadmin dumpsvnrdump dump 获取转储数据, 用 svnadmin loadsvnrdump load 把转储数据加载到仓库中. 采用人类 可读的转储格式的最大好处是用户可以手工地查看和修改转储文件. 当然, 坏 处是如果转储文件非常庞大 (例如包含了三年历史的转储数据), 手工地查看和 修改转储文件将会非常耗时.

所以我们需要 svndumpfilter. 它可以作为仓库 转储流的基于路径的过滤器, 用户所要做的就是向它提供希望保留或删除的路 径列表, 然后把仓库转储流以管道的方式输送给 svndumpfilter, 最终得到的转储数据就只会包含用户 (显式地或隐式地) 希望保留的路径.

现在介绍一个使用 svndumpfilter 的实际例子. 本章 早些时候 (见 “规划仓库的组织方式”一节), 我们讨论了如何规划仓库的布局—为每一个项目创建一个单独的仓库, 或 者把所有的项目都放在一个仓库中, 或其他布局. 但是随着工作的进行, 用户 可能会觉得仓库的布局需要做一些修改, 比较常见的修改是把原本放在同一个 仓库中的多个项目, 分别放到属于自己的一个单独的仓库中.

假设我们的仓库包含了三个项目: calc, calendarspreadsheet, 它们在 仓库中的位置如下:


/
   calc/
      trunk/
      branches/
      tags/
   calendar/
      trunk/
      branches/
      tags/
   spreadsheet/
      trunk/
      branches/
      tags/

为了把三个项目分开存放到三个仓库, 首先转储整个仓库:

$ svnadmin dump /var/svn/repos > repos-dumpfile
* Dumped revision 0.
* Dumped revision 1.
* Dumped revision 2.
* Dumped revision 3.
…
$

然后, 用 svndumpfilter 过滤转储文件, 每次过滤 一个项目的顶层目录, 得到三个新的转储文件:

$ svndumpfilter include calc < repos-dumpfile > calc-dumpfile
…
$ svndumpfilter include calendar < repos-dumpfile > cal-dumpfile
…
$ svndumpfilter include spreadsheet < repos-dumpfile > ss-dumpfile
…
$

这时候, 你必须做出决定. 每一个转储文件都将创建一个有效的仓库, 但保留的路径与原仓库中的路径一模一样, 也就是说即使你想为项目 calc 创建一个单独的仓库, 根据转储文件创建出的 仓库也会有顶层目录 calc 存在. 如果想把目录 trunk, tagsbranches 放在仓库的根目录下, 你可能希望能够 修改转储文件, 调整头部 Node-pathNode-copyfrom-path, 使得它们不再包含路径分量 calc/. 并且, 你可能还想删除创建目录 calc 的转储数据, 这部分的转储数据看起来就像下面 这样:

Node-path: calc
Node-action: add
Node-kind: dir
Content-length: 0
  
[警告] 警告

如果你已经决定手工地修改转储文件, 以便删除顶层目录, 一定要确保 你所用的编辑器不会自动地把行结束符转换成本地格式 (例如把 \r\n 转换成 \n), 以免文件的 内容与元数据不一致. 否则的话, 转储文件将变成一堆废纸.

剩下的工作就是创建三个新的仓库, 然后分别加载对应的转储文件, 并忽略 在转储流中发现的 UUID:

$ svnadmin create calc
$ svnadmin load --ignore-uuid calc < calc-dumpfile
<<< Started new transaction, based on original revision 1
     * adding path : Makefile ... done.
     * adding path : button.c ... done.
…
$ svnadmin create calendar
$ svnadmin load --ignore-uuid calendar < cal-dumpfile
<<< Started new transaction, based on original revision 1
     * adding path : Makefile ... done.
     * adding path : cal.c ... done.
…
$ svnadmin create spreadsheet
$ svnadmin load --ignore-uuid spreadsheet < ss-dumpfile
<<< Started new transaction, based on original revision 1
     * adding path : Makefile ... done.
     * adding path : ss.c ... done.
…
$

svndumpfilter 的两个子命令都支持用于决定如何 处理 版本号的选项. 如果一个版本号只包含了被过滤掉的 路径的修改, 就可以把这个空的版本号当成不需要的版本号. 为了允许用户 决定如何处理这两种版本号, svndumpfilter 提供了以 下选项:

--drop-empty-revs

不要生成空版本号—直接忽略它们.

--renumber-revs

如果空版本号被丢弃 (通过选项 --drop-empty-revs), 修改后面所有的版本号的号码, 使得版本号号码是连续的.

--preserve-revprops

如果空版本号未被丢弃, 则保留它们的版本号属性 (日志消息, 作者, 日期, 和其他自定义的属性). 如果未指定该选项, 则该版本号 将只会包含原始的提交日期和一条生成的日志消息, 该消息指出版本号 是被 svndumpfilter 清空的.

虽然 svndumpfilter 非常实用, 而且可以节省 大量的时间, 但它仍然有几点需要特别注意的地方. 首先, 命令对路径语义 非常敏感. 要特别注意转储文件中的路径是否以斜杠开始, 即头部 Node-pathNode-copyfrom-path.

…
Node-path: spreadsheet/Makefile
…

如果路径以斜杠开始 (也就是说路径含有前导斜杠), 那么 svndumpfilter includesvndumpfilter exclude 的路径参数也应该以斜杠 开始 (反之亦然). 更进一步, 如果转储文件对前导斜杠的使用不太一致, [52] 那你应该对路径进行规范化处理, 使得它们全部 都有 (或都没有) 前导斜杠.

另外, 被复制的路径也可能会带来一些麻烦. Subversion 支持在仓库中 执行复制操作—通过复制已存在的路径来创建新的路径. 在仓库的生命 周期中, 有可能出现这种时刻: 你从一个被 svndumpfilter 排除的位置复制了一个文件或目录, 放到另一个被 svndumpfilter 包含的位置上. 为了保证转储数据是自我 满足的, svndumpfilter 仍然需要显示新路径的添加 —包含了通过复制而创建的文件的所有内容—但并不把新路径的添加 表示成某个源路径的复制, 因为这个源路径在已过滤的转储数据中并不存在. 但是由于 Subversion 的转储格式只会显示每个版本号中发生变化的内容, 因此被复制的数据源可能不是现成的. 如果管理员觉得在仓库中存在这种 类型的复制, 那就要重新考虑被包含或排除的路径, 或许应该包含在复制操作 中充当数据源的路径.

最后, svndumpfilter 在过滤路径时采取的是非常 字面的理解. 如果你试图复制一个根目录为 trunk/my-project 的仓库的历史, 到它自己的仓库中, 那你就要用 svndumpfilter include 保留 trunk/my-project 内的所有修改, 但是生成的转储 文件对于将来加载它的仓库没有任何假定. 特别地, 转储文件可能以添加目录 trunk/my-project 的版本号作为开始, 但它 不会 包含创建目录 trunk 的 版本号 (因为 trunk 不匹配包含过滤器). 管理员需要 确保在加载转储文件前, 转储文件期望存在的目录在目标仓库中确实存在.

仓库复制

有时候, 如果仓库的历史能和另一个仓库保持一模一样, 那么在完成很 多事情时将会变得非常方便. 比如最明显的一个就是当主仓库无法访问时 (可能是系统硬件故障, 网络故障等), 把服务切换到备份仓库. 其他场景 还包括利用镜像仓库, 把访问负载分散到多台服务器上, 实现软升级等.

Subversion 提供了 svnsync 实现仓库的复制. svnsync 的工作本质上就是要求 Subversion 服务 重放 版本号, 每次一个, 然后利用这个版本号的相关信息, 在另一个仓库中模拟一个相同的提交. 仓库所在的主机和执行 svnsync 的主机不必是同一台—如果命令的参数 是仓库的 URL, svnsync 将通过 Subversion 的仓库访问 (Repository Access, RA) 接口完成工作. svnsync 所要求的就是源仓库的读权限和目标仓库的读写权限.

[注意] 注意

svnsync 要求远程的源仓库的 Subversion 版本 必须至少是 1.4.

使用 svnsync 复制仓库

假设你已经有了一个源仓库, 现在想为它创建一个镜像, 下一件需要准备 的东西就是充当镜像的目标仓库. 这个目标仓库可以使用与源仓库不同的 后端存储机制 (见 文件系统) —Subversion 的抽象层保证了具体的后端存储不会对复制操作产生 影响. 但是在默认情况下, 目标仓库此时不能包含任何版本历史 (我们将在 本节的后面介绍一种例外情况).

svnsync 用于交流版本号信息的协议对源仓库与 目标仓库不匹配的版本历史非常敏感, 因此, 虽然 svnsync 并没有 要求 目标 仓库是只读的,[53]除了 svnsync, 如果还允许其他进程或 用户修改目标仓库的历史, 常常会导致灾难性的后果.

[警告] 警告

不要用除了 svnsync 之外的其他方法修改镜像 仓库, 使得镜像仓库偏离源仓库的历史. 发生在镜像仓库的提交和版本号 属性修改只能由 svnsync 完成.

对目标仓库的另一项要求是允许 svnsync 修改版 本号属性. 因为 svnsync 仍然受到目标仓库的钩子的 影响, 而仓库的默认配置还不全面 (见 Subversion 仓库钩子参考手册pre-revprop-change). 管理员需要 显式地实现钩子 pre-revprop-change, 在钩子脚本中允许设置和修改版本号 属性. 如果这些条件都满足了, 那么创建镜像前的工作就已经准备就绪了.

[提示] 提示

一种很好的做法是实现一种授权方式, 使得除了执行复制操作的 进程外, 不允许任何其他用户或程序修改镜像仓库.

现在我们将介绍一个使用 svnsync 的典型场景, 如果读者觉得这里介绍的情况与自己的环境不太符合, 大可不必理会我们 的例子.

我们将会对存放本书源代码的 Subversion 仓库创建镜像仓库, 并把镜 像仓库发布到 Internet 上, 镜像仓库将托管在与源仓库不同的主机上. 本书的源代码仓库允许匿名只读访问, 但修改仓库需要验证身份. (这里先 不介绍如何配置 Subversion 服务器, 这是 第 6 章 服务器配置 的主题.) 为了使例子更加有趣, 我们 将在第三台主机上发起复制操作—例如笔者当前正在使用的主机.

首先, 创建用作镜像的仓库. 这一步及后面的两个步骤都要求托管镜像 仓库的主机提供 shell 访问权限, 一旦仓库配置完成, 就不用再直接访问 主机.

$ ssh admin@svn.example.com "svnadmin create /var/svn/svn-mirror"
admin@svn.example.com's password: ********
$

现在, 我们已经有了一个自己的仓库, 得益于服务器的配置, 我们可以 在 Internet 上访问到仓库. 因为我们不希望除了复制之外的其他过程修改 仓库, 所以需要把除了复制之外的其他修改操作区分出来. 为了实现这个 目标, 为复制过程使用一个专用的用户名, 只有使用用户名 syncuser 提交的修改才会被允许.

我们将使用 Subversion 的钩子系统保证复制过程可以完成它需要 完成的操作, 并且只有它能够做这些事情. 为此我们需要实现两个钩子 —pre-revprop-change 和 start-commit. 我们的 pre-revprop-change 钩子脚本的内容见 例 5.4 “镜像仓库的 pre-revprop-change 钩子脚本”, 基本思路是如果试图修改属性的用户是 syncuser, 则 允许; 否则禁止修改属性.

例 5.4. 镜像仓库的 pre-revprop-change 钩子脚本

#!/bin/sh 

USER="$3"

if [ "$USER" = "syncuser" ]; then exit 0; fi

echo "Only the syncuser user may change revision properties" >&2
exit 1

上面的脚本针对的是版本号属性的修改. 现在考虑如何做到只允许用户 syncuser 向仓库提交新的版本号, 这需要用到 start-commit 钩子脚本, 如 例 5.5 “镜像仓库的 start-commit 钩子脚本” 所示.

例 5.5. 镜像仓库的 start-commit 钩子脚本

#!/bin/sh 

USER="$2"

if [ "$USER" = "syncuser" ]; then exit 0; fi

echo "Only the syncuser user may commit new revisions" >&2
exit 1

安装了我们的钩子脚本后, 并确保 Subversion 服务器对脚本具有可 执行权限, 镜像仓库这边的设置就算结束了, 现在开始真正地创建镜像.

使用 svnsync 做的第一件事就是告诉目标仓库, 它是源仓库的镜像, 这会用到子命令 svnsync initialize, 目标仓库与源仓库的 URL 将作为命令的参数. Subversion 1.4 要求镜像必须针对整个仓库, 从 Subversion 1.5 开始, 允许只对仓库的子目录创建镜像.

$ svnsync help init
initialize (init): usage: svnsync initialize DEST_URL SOURCE_URL

Initialize a destination repository for synchronization from
another repository.
…
$ svnsync initialize http://svn.example.com/svn-mirror \
                     https://svn.code.sf.net/p/svnbook/source \
                     --sync-username syncuser --sync-password syncpass
Copied properties for revision 0 (svn:sync-* properties skipped).
NOTE: Normalized svn:* properties to LF line endings (1 rev-props, 0 node-props).
$

目标仓库现在已经记住它是本书源码仓库的镜像. 需要注意的是我们向 svnsync 提供了用户名和密码—这是镜像仓库 的 pre-revprop-change 钩子所要求的.

[注意] 注意

在 Subversion 1.4, svnsync 选项 --username--password 的值 同时用于源仓库和目标仓库的身份验证, 如果用户在两个仓库中的证书 不是完全一样, 就会造成问题, 尤其是命令在非交互模式下运行时 (通过添加选项 --non-interactive), Subversion 1.5 通过引入两对新的选项解决了这个问题. 选项 --source-username--source-username 用于指定源仓库的身份验证证书; 选项 --sync-username--sync-password 用于指定目标仓库的身份验证证书. (为了兼容旧版本, 仍然保留旧选项 --username--password, 但我们建议使用新选项.)

现在到了最有趣的部分. 只要一个命令, 就能要求 svnsync 把源仓库中未复制的版本号, 复制到目标 仓库.[54] 子命令 svnsync synchronize 查看 存放在目标仓库中的特殊的版本号属性, 从而确定源仓库的镜像进度— 在这个例子里, 最近一次创建镜像的版本号是 r0. 然后 svnsync 查询源仓库, 确定源仓库最新的版本号是多少, 最后请求源仓库的服务器开始重放从 0 到最新版本号之间的所有版本号. 随着 svnsync 不断地从源仓库服务器获取到结果, 它开始把版本号作为新提交, 转发到目标仓库服务器上.

$ svnsync help synchronize
synchronize (sync): usage: svnsync synchronize DEST_URL [SOURCE_URL]

Transfer all pending revisions to the destination from the source
with which it was initialized.
…
$ svnsync synchronize http://svn.example.com/svn-mirror \
                      https://svn.code.sf.net/p/svnbook/source
Committed revision 1.
Copied properties for revision 1.
Committed revision 2.
Copied properties for revision 2.
Transmitting file data .
Committed revision 3.
Copied properties for revision 3.
…
Transmitting file data .
Committed revision 4063.
Copied properties for revision 4063.
Transmitting file data .
Committed revision 4064.
Copied properties for revision 4064.
Transmitting file data ....
Committed revision 4065.
Copied properties for revision 4065.
$

其中比较有趣的地方是, 对于每一个需要创建镜像的版本号, 首先向 目标仓库提交该版本号, 然后再修改版本号属性. 之所以需要这种两步骤 的复制是因为提交操作由用户 syncuser 完成, 版本 号的时间戳是创建时的时间, svnsync 接下来通过 一系列的属性修改操作, 使得源仓库的版本号属性与目标仓库的版本号属性 保持一致, 其中就包括修改版本号的作者和时间戳.

还值得注意的一点是 svnsync 会记住工作的当 前进度, 从而允许操作被中断, 并在以后的某个时间人上次中断的地方 重新开始. 如果在命令执行的过程中, 网络临时断开, 只要再次执行 svnsync synchronize 即可. 实际上, 如果有新的 版本号出现在源仓库中, 你所需要做的也就是执行 svnsync synchronize 而已.

[警告] 警告

作为记账操作的一部分, svnsync 在镜像仓库 记录了源仓库的 URL. 正是因为这个原因, 在初始化后执行的 svnsync 命令不会 要求 在命令行上必须提供源仓库的 URL. 但是为了安全起见, 我们建议用户继 续在命令行上提供源仓库的 URL 参数. 取决定于具体的部署方式, 在镜像仓库检索源仓库信息, 或推送版本化数据时, 任凭 svnsync 信任源仓库 URL 可能不太安全.

然而, svnsync 无法做到面面俱到. 因为用户可 以在任意时刻修改版本号属性, 而且这些修改不会留下任何历史信息, 所以 复制操作要特别注意这种情况. 假设你已经复制了仓库的前 15 个版本号, 如果有人修改了版本号 12 的版本号属性, 则 svnsync 不会知道这件事, 更不会回过头来重新复制版本号 12. 你必须使用 子命令 svnsync copy-revprops (或者还需要一些额外 工具的辅助), 把特定的或指定范围内的版本号的所有属性都复制过来.

$ svnsync help copy-revprops
copy-revprops: usage:

    1. svnsync copy-revprops DEST_URL [SOURCE_URL]
    2. svnsync copy-revprops DEST_URL REV[:REV2]

…
$ svnsync copy-revprops http://svn.example.com/svn-mirror 12
Copied properties for revision 12.
$

为了使用 svnsync 复制仓库, 你可能想设置 一个自动化过程. 比如说, 我们展示的例子的模式是 抓取与推送, 你可能觉得更方便的做法是在钩子 post-commit 和 post-revprop-change 完成版本号到镜像仓库的推送, 这种 做法有助于镜像仓库时刻保持最新的状态.

使用 svnsync 进行部分复制

svnsync 不仅限于复制仓库的全部内容, 它也能 进行部分复制. 一个常见的例子是, 如果用户只对仓库的部分内容具有读取 权限, 那么 svnsync 也能优雅地对仓库进行镜像, 但是它只会复制它能读取到的仓库内容. 显然, 这种镜像并不能作为有效 的仓库备份策略.

从 Subversion 1.5 开始, svnsync 支持对仓库 的子目录进行复制. 在复制时, 除了用把 svnsync init 的 URL 参数指定成待复制的仓库子目录的 URL 外, 其余步骤和复制整个 仓库是完全一样的. 现在同步过程就只会复制源仓库的子目录内的版本号, 但有些限制条件需要注意. 首先, svnsync 不支持把 源仓库内的多个不连贯的子目录复制到一个镜像仓库中—此时正确的 做法应该是复制它们的公共父目录. 然后, 因为过滤操作是完全基于路径的, 所以说如果被复制的子目录曾经被重命名过, 则镜像仓库只会包含在指定的 URL 中出现的版本号. 类似的, 如果源仓库的子目录在将来被重命名了, 则同步过程将无法继续, 因为你所指定的源仓库的子目录 URL 已经不再有效.

创建镜像的小窍门

前面我们已经介绍了为了给一个已存在的仓库创建镜像需要完成哪些 工作. 对于很多人而言, 使用 svnsync 传送成千— 甚至成百万—的版本号历史所带来的代价, 就像是看一场被掌声中断 了很久的表演. 幸运的是, Subversion 1.7 提供了一个变通方 法, 通过为 svnsync initialize 添加新选项 --allow-non-empty, 该选项允许用户在把仓库初始化成 另一个仓库的镜像时, 不去检查将被初始化的镜像仓库是否含有版本历史. 通过前面几次使用过程中的警告, 读者应该很快就能看出必须小心使用这个 选项. 但是, 如果用户拥有源仓库的管理员权限, 那么这个选项就会非常 方便, 因为用户可以直接复制仓库, 然后把复制出的仓库初始化成新镜像:

$ svnadmin hotcopy /path/to/repos /path/to/mirror-repos
$ ### create /path/to/mirror-repos/hooks/pre-revprop-change
$ svnsync initialize file:///path/to/mirror-repos \
                     file:///path/to/repos
svnsync: E000022: Destination repository already contains revision history; co
nsider using --allow-non-empty if the repository's revisions are known to mirr
or their respective revisions in the source repository
$ svnsync initialize --allow-non-empty file:///path/to/mirror-repos \
                                       file:///path/to/repos
Copied properties for revision 32042.
$

如果管理员使用的 Subversion 版本低于 1.7 (即 svnsync initialize 不支持选项 --allow-non-empty), 还可以通过其他手段实现相同的目的, 那就是 认真地 修改仓库副本 (该副本将作为源仓库的镜像) 的版本号 r0 的属性, 使得 r0 的属性与 svnsync 将会创建的记账属性相同.

复制小结

本节讨论了几种用于复制版本号历史到其他仓库的方法, 现在从普通 用户的角度看待这些操作: 仓库复制和各种不同情况下的执行方式将会如 何影响客户端?

如果用户需要同时和仓库及其镜像打交道, 那么使用一个 单独的 工作副本同时与多个仓库交互是有可能做到 的, 但要满足一些条件. 首先, 用户要确保主仓库和镜像仓库拥有相同的 UUID (默认情况下并不相同), 关于仓库 UUID 的更多内容, 见 “管理仓库的 UUID”一节.

一旦两个仓库拥有相同的 UUID, 用户就可以用命令 svn relocate 把工作副本重定向到任意一个仓库, 见 svn 参考手册—Subversion 命令行客户端svn relocate. 但其中有一个潜在的问题: 如果 主仓库和镜像仓库不是完全同步, 而工作副本当前指向主仓库, 并且处于最 新的状态, 如果此时把工作副本重定向到过时的镜像仓库, 工作副本就会 报错, 因为本来应该存在的版本号突然无缘无故的消失了. 如果发生了这种 情况, 可以再把工作副本重定向回原来的主仓库, 然后等镜像仓库与主仓库 完全同步, 或者把工作副本回退到在镜像仓库中存在的版本号, 然后再重 定向工作副本.

最后, 要注意 svnsync 所提供的基于版本号的 复制流程只会复制版本号. 只有仓库转储文件格式所携带的信息类型才能 用于复制, 因此, svnsync (以及 svnrdump, 见 “使用 svnrdump 迁移仓库数据”一节) 受到的 限制与仓库转储流受到的限制类似, 它们不会复制已实现的钩子, 仓库或 服务器的配置, 未提交的事务, 或用户施加在仓库路径上的锁.

仓库备份

自现代计算机诞生以来, 不管出现了多么先进的技术, 有一点总是挥之不 去—有时候, 事情会变得非常糟糕. 即使是最小心翼翼的管理员, 也不得 不面对断电, 网络故障, 内存与硬盘损坏. 因此本节的主题是如何备份仓库数 据.

对于 Subversion 仓库管理员来说, 有两种备份策略—全量备份与 增量备份. 全量备份涉及到在一个操作中记录所有的信息, 这些信息可以在 灾难发生后, 重新构造出原来的仓库. 通常意味着复制整个仓库目录 (包括 后端存储 Berkeley DB 或 FSFS 的数据). 增量备份每次记录的数据量相对 较少: 它只备份上一次备份以来, 发生变化的仓库数据.

对于全量备份来说, 这个笨拙的方法看起来似乎很合理, 但是除非管理员 临时禁止对仓库的其他访问, 否则的话, 如果只是简单地复制目录, 得到的 备份也可能是有问题的. Berkeley DB 的文档描述了一种特定的数据库文件 复制顺序, 它可以保证得到的备份是有效的, 类似的复制顺序同样存在于 FSFS. 管理员不用考虑如何实现它们所要求的复制顺序, 因为 Subversion 开发团队 已经帮你实现好了, 命令 svnadmin hotcopy 会处理好 仓库热拷贝涉及到的各种细枝末节, 它们调用方式就像 Unix 的 cp 或 Windows 的 copy 那样简单:

$ svnadmin hotcopy /var/svn/repos /var/svn/repos-backup

得到的备份是一个完整的 Subversion 仓库, 能够在原仓库出现故障时 顶替上去.

围绕该命令, 还有其他一些额外的工具可供使用. Subversion 源代码目录 tools/backup 存放了一个脚本: hot-backup.py. hot-backup.pysvnadmin hotcopy 之上增加了一些备份管理策略, 允许用户只保留最近几次的仓库备份. hot-backup.py 自动管理通过备份得到的仓库目录的名字, 避免出现名字冲突, 而且会 轮换 旧备份, 只保留最近的几次备份. 即使管理员使用的是 增量备份策略, 也有使用 hot-backup.py 的需求. 例如 管理员可以在调度程序 (比如 Unix 系统中的 cron) 中执行 hot-backup.py, 从而实现每晚运行一次 (或 管理员认为合适的其他时间间隔) 脚本.

围绕仓库转储数据的生成与存放, 有些管理员会使用不同的备份方法. 在 “迁移仓库数据”一节 我们介绍了如何 使用带有选项 --incrementalsvnadmin dump, 在给定的版本号或版本号范围内执行增量备份. 当然, 也可以忽略选项 --incremental, 实现一个完全备份. 使用仓库转储数据的好处是这种备份信息的格式非常灵活—它与特定 的平台, 仓库文件系统类型, Subversion 的版本及其所使用的函数库都是 无关的. 但灵活性也带来了一定的开销, 就是需要较长的时间才能完全恢复 数据—每当有一个新的版本号提交时, 时间就会变得更长一点. 另外, 和其他备份方法相比, 如果修改了已经备份过的版本号的版本号属性, 这些 修改将不会体现在增量的转储数据中. 由于这些原因, 我们建议读者不要单独 依靠基于转储的备份策略.

从 Subversion 1.8 开始, svnadmin hotcopy 支持 选项 --incremental, 允许对 FSFS 仓库进行增量热拷贝, 在增量热拷贝模式下, 已经复制到目标仓库的版本号数据不会再复制一次. 如果 svnadmin hotcopy 带上选项 --incremental, Subversion 将只会复制新的版本号, 以及 上一次热拷贝后, 大小或时间戳发生变化的版本号. 而且, 与 svnsyncsvnadmin dump --incremental 有所不同的是 svnadmin hotcopy --incremental 仅受限于磁盘的读写性能, 在备份大型仓库时, 增量热拷贝可以节省大量的时间.

可以看到, 不同的备份方式都有各自的优点和缺点, 目前最简单的选择就 是全量的热拷贝, 它总能得到一份可用的仓库副本, 一旦主仓库出现故障, 只要简单地递归复制目录, 就能从备份仓库中恢复. 不幸的是, 如果同时存 在多个仓库副本, 这些全量拷贝所消耗的磁盘空间将会是很可观的. 与此相对, 生成增量副本更快, 消耗的磁盘空间也更少, 但是复原过程就比较痛苦了, 经常需要应用多个增量备份. 其他几种备份方法也有各自的特点. 管理员需要 在备份和复原的开销之间, 找到最适合的平衡点.

svnsync (见 “仓库复制”一节) 提供了非常方便的折衷 方案. 如果管理员周期性地把主仓库同步到只读的镜像仓库, 在主仓库发生 故障时, 镜像仓库就会是一个很好的替补. 这种方法的主要缺点是只有版本化 的仓库数据才会被同步—仓库的配置文件, 用户指定的仓库路径锁, 以 及其他存放在仓库目录中的项目, 只要它们不在仓库的版本化文件系统中, 就不会被 svnsync 处理.

在任何一种备份场景下, 管理员都要注意版本号属性将如何影响他们的 备份. 版本号属性的修改不会产生新的版本号, 不会触发钩子 post-commit, 甚至也不会触发钩子 pre-revprop-change 和 post-revprop-change. [55] 而且用户可以按照任意的时间顺序修改 版本号属性—在任意时刻修改任意一个版本号的属性—在增量备份 的情况下, 如果某个版本号已经包含在前一次备份中, 它的属性在后来被修改 了, 那么修改将不会包含在后面的增量备份中.

一般来说, 只有真正偏执的人才想在每次提交后备份整个仓库, 然而, 如果仓库已经具备了一些相对细致的冗余机制 (例如每次提交后的邮件通知 或增量转储), 那么仓库管理员可能会把数据库的热拷贝作为每夜例行工作的 一部分去执行. 这是你的数据—你想怎么保护它们都不为过.

很多时候, 备份仓库的最佳方式是本节所描述的各种方法的组合. 比如说 Subversion 开发人员备份 Subversion 源代码仓库的方式是每晚使用 hot-backup.py 和异地的 rsync 完成全量备份; 为所有的提交和属性修改通知邮件维护多份归档; 由不同的 志愿者使用 svnsync 维护多份仓库镜像. 你的最终 方案可能与此类似, 但应该根据你的具体需求维持好易用性与安全性之间的 平衡. 无论你怎么做, 应该偶尔检查备份是否可用—如果连备胎都有漏洞, 那还怎么用? 虽然这一切都无法避免硬件遭受命运的捶打[56], 但备份确实能够帮助你从灾难中恢复.

管理仓库的 UUID

每个 Subversion 仓库都有一个全局统一标识 (universally unique identifier, 简称 UUID) 与之关联. 当其他手段不够完善时 (例如检查仓库的 URL, 但 URL 可以会变化), 客户端可以使用 UUID 识别仓库. 大多数管理员 极少需要考虑仓库的 UUID, 对他们而言, UUID 只是 Subversion 的一个实现 上的细节而已. 然而, 少数情况下这个细节也需要引起注意.

一般来说, 管理员希望活动仓库的 UUID 是独一无二的, 毕竟这就是 UUID 的主要特点. 但在某些情况下需要两个仓库拥有一模一样的 UUID, 比如 说管理员为仓库制作了一个副本, 并且希望该副本是源仓库的完美镜像, 因为 管理员希望当备份仓库替换掉活动仓库时, 用户不会突然看到一个似乎不同的 仓库. 在转储和加载仓库历史时 (见 “迁移仓库数据”一节), 管理员可以根据实际情况 决定是否向目标仓库应用封装在转储流中的 UUID.

有若干种方式可以用来设置或重置仓库的 UUID, 对于 Subversion 1.5 而言, 用到的命令是 svnadmin setuuid. 如果在命令行 上显式地提供了 UUID 参数, 命令将验证 UUID 的格式是否正确, 如果正确就 把它设置到仓库上. 如果省略了 UUID 参数, 命令就自动为仓库生成一个全新 的 UUID.

$ svnlook uuid /var/svn/repos
cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
$ svnadmin setuuid /var/svn/repos   # generate a new UUID
$ svnlook uuid /var/svn/repos
3c3c38fe-acc0-11dc-acbc-1b37ff1c8e7c
$ svnadmin setuuid /var/svn/repos \
           cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec  # restore the old UUID
$ svnlook uuid /var/svn/repos
cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
$

如果你用的是 1.5 版之前的 Subversion, 那么设置 UUID 的过程会更复杂 一点. 为了设置 UUID, 管理员可以把带有新 UUID 的桩转储文件, 以管道地方式 传递给 svnadmin load --force-uuid REPOS-PATH , 从而显式设置仓库的 UUID.

$ svnadmin load --force-uuid /var/svn/repos <<EOF
SVN-fs-dump-format-version: 2

UUID: cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
EOF
$ svnlook uuid /var/svn/repos
cf2b9d22-acb5-11dc-bc8c-05e83ce5dbec
$

使用旧版的 Subversion 来生成全新的 UUID 并不是一件非常容易的事情, 管理员最好找到生成 UUID 的其他方法, 然后再通过上面的方式为仓库显式地 设置新 UUID.



[50] 这不正是你使用版本控制系统的原因吗?

[51] 小心谨慎地从版本化的数据中删除某些数据实际上是允许的, 清除 特性是 Subversion 必须提供的功能之一, 也是 Subversion 开发人员想尽快实现的功能之一.

[52] svnadmin dump 对前导斜杠的处理 策略总是一致的 (不包含前导斜杠), 生成转储数据的其他程序就不一定 了.

[53] 实际上, 目标仓库不能是完全只读的, 否则 的话, svnsync 就不能有效地复制历史.

[54] 虽然阅读这段文字和例子只需要几秒钟的时间, 但 svnsync 实际消耗的时间比这长得多.

[55] 命令 svnadmin setlog 可以完全旁路 掉钩子接口

[56] 你知道的—这只是对各种 变化莫测的问题 的统称.