本手册编写用于描述 Subversion 1.4。如果您正在运行更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的本书版本。

仓库维护

维护 Subversion 仓库可能很令人生畏,主要是因为拥有数据库后端的系统固有的复杂性。做好这项工作关键在于了解工具——它们是什么,何时使用它们以及如何使用它们。本节将向您介绍 Subversion 提供的仓库管理工具,以及如何使用它们来完成仓库数据迁移、升级、备份和清理等任务。

管理员工具集

Subversion 提供了一些实用程序,可用于创建、检查、修改和修复您的仓库。让我们更仔细地看一下这些工具。之后,我们将简要介绍 Berkeley DB 分发版中包含的一些实用程序,它们提供了与您的仓库数据库后端相关的功能,而这些功能并非 Subversion 自身工具提供的。

svnadmin

程序 svnadmin 是仓库管理员的最佳帮手。除了提供创建 Subversion 仓库的功能外,该程序还允许您对这些仓库执行多种维护操作。的语法 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
…

我们已经提到了 svnadmincreate 子命令(参见 名为“创建仓库”的部分)。我们将在本章后面介绍大多数其他命令。您可以查阅 名为“svnadmin”的部分 以获取有关子命令及其功能的完整概述。

svnlook

svnlook 是 Subversion 提供的一个工具,用于检查仓库中的各种修订版和 事务(正在创建的修订版)。该程序的任何部分都不会尝试更改仓库。 svnlook 通常由仓库挂钩用来报告即将提交的更改(对于 pre-commit 挂钩)或刚刚提交的更改(对于 post-commit 挂钩)到仓库。仓库管理员可以使用该工具进行诊断。

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”)修订版。因此,当 /path/to/repos 中的仓库中最新的修订版是 19 时,以下两个命令执行的操作完全相同

$ svnlook info /path/to/repos
$ svnlook info /path/to/repos -r 19

关于子命令的唯一例外是 svnlook youngest 子命令,它不接受任何选项,只打印出仓库中最新的修订版号。

$ svnlook youngest /path/to/repos
19

注意

请记住,您可以浏览的唯一事务是非提交事务。大多数仓库不会有这样的事务,因为事务通常是提交的(在这种情况下,您应该使用 --revision (-r) 选项将它们作为修订版访问)或中止并删除。

来自 svnlook 的输出旨在同时可读和可解析。以 info 子命令的输出为例

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

info 子命令的输出定义为

  1. 作者,后跟换行符。

  2. 日期,后跟换行符。

  3. 日志消息中的字符数,后跟换行符。

  4. 日志消息本身,后跟换行符。

此输出是人类可读的,这意味着日期戳之类的项目使用文本表示来显示,而不是使用更模糊的东西(例如自 Tasty Freeze 小伙子经过以来的纳秒数)。但输出也是机器可解析的——因为日志消息可以包含多行并且长度不受限制, svnlook 在消息本身之前提供该消息的长度。这允许围绕该命令的脚本和其他包装器对日志消息做出明智的决策,例如如何为消息分配内存,或者至少在该输出不是流中的最后一段数据的情况下跳过多少字节。

svnlook 可以执行各种其他查询:显示我们先前提到的信息的子集、递归列出版本控制的目录树、报告在给定修订版或事务中修改了哪些路径、显示对文件和目录进行的文本和属性更改,等等。参见 名为“svnlook”的部分 以获取 svnlook 功能的完整参考。

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)

只有两个有趣的子命令。它们允许您在明确或隐式包含流中的路径之间进行选择

exclude

从转储数据流中过滤掉一组路径。

include

只允许请求的路径集通过转储数据流。

您可以在 名为“过滤仓库历史记录”的部分 中了解有关这些子命令和 svndumpfilter 的独特用途的更多信息。

svnsync

程序 svnsync 是 Subversion 1.4 版本中新增的功能,它提供了维护 Subversion 仓库只读镜像所需的所有功能。该程序实际上只有一个工作——将一个仓库的版本控制历史记录转移到另一个仓库。虽然有几种方法可以做到这一点,但它的主要优势是可以远程操作——“”和“接收[32] 仓库可以位于与 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
   help (?, h)
$

我们将在 名为“仓库复制”的部分 中进一步讨论使用 svnsync 的复制仓库。

Berkeley DB 实用程序

如果您使用的是 Berkeley DB 仓库,那么您所有版本控制的文件系统结构和数据都存储在仓库 db/ 子目录中的数据库表集中。该子目录是一个常规的 Berkeley DB 环境目录,因此可以与任何 Berkeley 数据库工具结合使用,这些工具通常作为 Berkeley DB 分发版的一部分提供。

对于日常的 Subversion 使用,这些工具是不必要的。Subversion 仓库通常需要的功能大多已在 svnadmin 工具中复制。例如, svnadmin list-unused-dblogssvnadmin list-dblogs 执行 Berkeley db_archive 命令提供的一组子集,而 svnadmin recover 反映了 db_recover 实用程序的常见用例。

但是,您可能会发现一些 Berkeley DB 实用程序仍然很有用。程序 db_dumpdb_load 分别写入和读取一个自定义文件格式,该格式描述了 Berkeley DB 数据库中的键和值。由于 Berkeley 数据库在不同的机器架构之间不可移植,因此这种格式是在机器之间传输这些数据库的实用方法,无论架构或操作系统如何。正如我们将在本章后面介绍的,您也可以使用 svnadmin dumpsvnadmin load 来完成类似的工作,但 db_dumpdb_load 可以完成某些工作,并且速度要快得多。它们在经验丰富的 Berkeley DB 黑客出于某种原因需要对 BDB 支持的仓库中的数据进行就地调整时,也可能很有用,这是 Subversion 实用程序不允许的操作。此外, db_stat 实用程序可以提供有关 Berkeley DB 环境状态的实用信息,包括有关锁定和存储子系统的详细统计信息。

有关 Berkeley DB 工具链的更多信息,请访问 Oracle 网站上 Berkeley DB 部分的文档部分,地址为 http://www.oracle.com/technology/documentation/berkeley-db/db/

提交日志消息更正

有时,用户在她的日志消息中会犯错误(例如拼写错误或某些错误信息)。如果仓库配置(使用 pre-revprop-change 挂钩;参见 名为“实现仓库挂钩”的部分)以接受提交完成后对该日志消息的更改,那么用户可以使用 svn 程序的 propset 命令(参见 svn propset)来远程“修复”她的日志消息。但是,由于可能永远丢失信息,因此 Subversion 仓库默认情况下不会配置为允许更改未版本化的属性——除非由管理员进行操作。

如果管理员需要更改日志消息,可以使用 svnadmin setlog 来完成。该命令更改仓库中给定修订版的日志消息(svn:log 属性),从提供的文件中读取新值。

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

默认情况下, svnadmin setlog 命令仍然受制于与远程客户端相同的修改未版本化属性的保护—— pre-post-revprop-change 挂钩仍然会触发,因此必须设置它们以接受此类更改。但是,管理员可以通过将 --bypass-hooks 选项传递给 svnadmin setlog 命令来绕过这些保护措施。

警告

但是请记住,通过绕过挂钩,您可能正在避免诸如属性更改的电子邮件通知、跟踪未版本化属性更改的备份系统等。换句话说,要非常小心您要更改的内容以及如何更改。

管理磁盘空间

虽然近几年存储成本大幅下降,但对于需要对大量数据进行版本控制的管理员来说,磁盘使用仍然是一个需要关注的问题。存储在活动仓库中的每个版本历史信息都需要备份到其他地方,可能还需要作为轮换备份计划的一部分进行多次备份。了解 Subversion 仓库数据中哪些部分需要保留在活动站点上,哪些需要备份,哪些可以安全删除,非常有用。

Subversion 如何节省磁盘空间

为了保持仓库的大小,Subversion 在仓库本身内部使用增量化(或“增量存储”)。增量化涉及将数据块的表示编码为相对于其他数据块的差异集合。如果两个数据块非常相似,则此增量化将为增量化数据块节省存储空间——它不会占用与原始数据大小相同的空间,而只占用足够的空间来表示“我看起来就像这里其他数据块一样,除了以下几个更改”。结果是,大多数仓库数据(即版本化文件的內容)存储的大小远小于原始“完整文本”表示。对于使用 Subversion 1.4 或更高版本创建的仓库,空间节省更为显著——现在这些文件的完整文本表示本身也被压缩了。

注意

由于所有在 BDB 支持的仓库中进行增量化的数据都存储在一个 Berkeley DB 数据库文件中,因此减小存储值的的大小不会立即减小数据库文件本身的大小。但是,Berkeley DB 会保留未使用数据库文件区域的内部记录,并在扩展数据库文件大小之前首先使用这些区域。因此,虽然增量化不会立即节省空间,但它可以大大减缓数据库的未来增长。

删除无效事务

虽然这种情况很少见,但有些情况下,Subversion 提交过程可能会失败,并在仓库中留下尚未提交的修订版本——一个未提交的事务以及与之关联的所有文件和目录更改。这可能是由于多种原因导致的:可能是用户不优雅地终止了客户端操作,也可能是操作过程中发生了网络故障。无论原因如何,无效事务都可能发生。它们不会造成任何实际损害,除了占用磁盘空间。然而,一个一丝不苟的管理员可能希望删除它们。

您可以使用svnadminlstxns 命令列出当前未完成的事务的名称。

$ svnadmin lstxns myrepos
19
3a1
a45
$

然后,可以使用每个结果项与svnlook(及其--transaction (-t)选项)一起确定谁创建了事务,何时创建的,事务中进行了哪些类型的更改——这些信息有助于确定事务是否为安全的候选对象以供删除!如果您确实要删除事务,则可以将其名称传递给svnadmin rmtxns,它将执行事务清理。实际上,rmtxns 子命令可以直接从 lstxns 的输出中获取输入!

$ svnadmin rmtxns myrepos `svnadmin lstxns myrepos`
$

如果您像这样使用这两个子命令,则应考虑使您的仓库暂时无法访问客户端。这样,在您开始清理之前,没有人可以开始合法的事务。示例 5.1,“txn-info.sh(报告未完成的事务)” 包含一些 shell 脚本,可以快速生成有关仓库中每个未完成事务的信息。

示例 5.1. 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
$

长期废弃的事务通常表示某种失败或中断的提交。事务的时间戳可以提供有趣的信息——例如,九个月前开始的操作还有可能处于活动状态吗?

简而言之,无需不明智地做出事务清理决定。各种信息来源——包括 Apache 的错误和访问日志、Subversion 的操作日志、Subversion 修订历史记录等——可以用于决策过程。当然,管理员通常可以简单地与看似无效事务的所有者(例如通过电子邮件)进行沟通,以验证事务是否确实处于僵尸状态。

清除未使用的 Berkeley DB 日志文件

直到最近,对于 BDB 支持的 Subversion 仓库而言,磁盘空间使用量最大的罪魁祸首是 Berkeley DB 在修改实际数据库文件之前执行预写操作的日志文件。这些文件捕获了在将数据库从一种状态更改为另一种状态的路径中采取的所有操作——虽然数据库文件在任何给定时间都反映了特定状态,但日志文件包含所有在状态之间进行的许多更改。因此,它们可能会非常快地增长和累积。

幸运的是,从 Berkeley DB 4.2 版本开始,数据库环境能够自动删除其自身未使用的日志文件。使用针对 Berkeley DB 4.2 或更高版本编译的svnadmin 创建的任何仓库都将为此自动日志文件删除功能配置。如果您不想启用此功能,只需将--bdb-log-keep 选项传递给svnadmin create 命令。如果您忘记这样做,或者稍后改变主意,只需编辑仓库 db 目录中的 DB_CONFIG 文件,注释掉包含 set_flags DB_LOG_AUTOREMOVE 指令的行,然后在您的仓库上运行svnadmin recover 以强制配置更改生效。有关数据库配置的更多信息,请参见名为“Berkeley DB 配置”的部分

如果没有某种形式的自动日志文件删除功能,日志文件将在您使用仓库时累积。这实际上是数据库系统的一个功能——您应该能够仅使用日志文件重新创建整个数据库,因此这些文件对于灾难性数据库恢复很有用。但通常,您需要存档不再被 Berkeley DB 使用的日志文件,然后从磁盘中删除它们以节省空间。使用svnadmin list-unused-dblogs 命令列出未使用的日志文件

$ svnadmin list-unused-dblogs /path/to/repos
/path/to/repos/log.0000000031
/path/to/repos/log.0000000032
/path/to/repos/log.0000000033
…
$ rm `svnadmin list-unused-dblogs /path/to/repos`
## disk space reclaimed!

警告

将日志文件用作备份或灾难恢复计划一部分的 BDB 支持的仓库不应使用日志文件自动删除功能。仅当所有日志文件可用时,才能从日志文件重建仓库数据。如果在备份系统有机会将日志文件复制到其他地方之前,一些日志文件从磁盘中删除,则不完整的备份日志文件集实际上是无用的。

Berkeley DB 恢复

名为“Berkeley DB”的部分所述,如果 Berkeley DB 仓库未正确关闭,有时会处于冻结状态。发生这种情况时,管理员需要将数据库倒带回一致状态。但这仅限于 BDB 支持的仓库——如果您使用的是 FSFS 支持的仓库,则这并不适用。对于那些使用 Subversion 1.4 与 Berkeley DB 4.4 或更高版本的人来说,您应该发现 Subversion 在这些类型的环境中已经变得更加健壮。尽管如此,卡住的 Berkeley DB 仓库确实会发生,管理员需要知道如何安全地处理这种情况。

为了保护仓库中的数据,Berkeley DB 使用锁定机制。这种机制确保数据库的各个部分不会被多个数据库访问器同时修改,并且每个进程在从数据库读取数据时都能看到数据处于正确状态。当某个进程需要更改数据库中的某些内容时,它首先检查目标数据上是否存在锁。如果数据未锁定,则进程会锁定数据,进行其想要进行的更改,然后解锁数据。其他进程被迫等待锁被移除,然后才能继续访问数据库的该部分。(这与您作为用户可以对仓库中版本化文件施加的锁无关;我们尝试在”的三个含义中消除由这种术语冲突造成的混淆。)

在使用 Subversion 仓库的过程中,致命错误或中断可能会阻止进程有机会移除其在数据库中放置的锁。结果是后端数据库系统“卡住”。发生这种情况时,任何尝试访问仓库的尝试都会无限期挂起(因为每个新的访问器都在等待锁消失——这不会发生)。

如果这种情况发生在您的仓库上,请不要惊慌。Berkeley DB 文件系统利用数据库事务、检查点和预写日志记录来确保只有最灾难性的事件[33] 会永久性地破坏数据库环境。一个足够偏执的仓库管理员会以某种方式对仓库数据进行异地备份,但现在还不要跑到磁带备份存储室去。

相反,请使用以下方法尝试“解卡”您的仓库

  1. 确保没有进程正在访问(或尝试访问)仓库。对于网络仓库,这意味着也要关闭 Apache HTTP 服务器或 svnserve 守护程序。

  2. 成为拥有和管理仓库的用户。这一点很重要,因为以错误的用户身份恢复仓库可能会调整仓库文件权限,以至于即使仓库“解卡”后,普通用户也无法访问它。

  3. 运行命令svnadmin recover /path/to/repos。您应该看到类似的输出

    Repository lock acquired.
    Please wait; recovering the repository may take some time...
    
    Recovery completed.
    The latest repos revision is 19.
    

    此命令可能需要几分钟才能完成。

  4. 重新启动服务器进程。

此过程几乎可以修复所有仓库锁定情况。确保以拥有和管理数据库的用户身份运行此命令,而不仅仅是以 root 身份运行。恢复过程的一部分可能涉及从头开始重新创建各种数据库文件(例如共享内存区域)。以 root 身份恢复将创建这些文件,使其归 root 所有,这意味着即使您恢复了仓库的连接,普通用户也将无法访问它。

如果由于某种原因,上述过程无法成功地解卡仓库,则应该执行两项操作。首先,将损坏的仓库目录移到一边(也许将其重命名为类似于 repos.BROKEN 的东西),然后恢复其最新的备份。然后,向 Subversion 用户列表(位于 )发送电子邮件,详细描述您的问题。数据完整性对于 Subversion 开发人员来说是重中之重。

将仓库数据迁移到其他地方

Subversion 文件系统将其数据散布在仓库中的各个文件中,其方式通常只有 Subversion 开发人员自己才能理解(并且感兴趣)。但是,可能会出现需要将所有或部分数据复制或移动到另一个仓库的情况。

Subversion 通过仓库转储流提供此功能。仓库转储流(通常在磁盘上存储为文件时称为“转储文件”)是一种可移植的平面文件格式,它描述了仓库中各种修订版——更改的内容、更改者、更改时间等等。此转储流是用于在仓库之间编排版本化历史记录的主要机制——全部或部分,有修改或无修改。Subversion 提供了创建和加载这些转储流所需的工具——分别是 svnadmin dumpsvnadmin load 子命令。

警告

虽然 Subversion 仓库转储格式包含人类可读的部分和熟悉的结构(类似于 RFC-822 格式,与大多数电子邮件使用的格式相同),但它不是纯文本文件格式。它是一种二进制文件格式,对干预高度敏感。例如,许多文本编辑器会通过自动转换行尾来破坏文件。

转储和加载 Subversion 仓库数据的理由有很多。在 Subversion 的早期,最常见的原因是 Subversion 本身的演变。随着 Subversion 的成熟,有时对后端数据库模式所做的更改会导致与以前版本的仓库不兼容,因此用户必须使用以前版本的 Subversion 转储他们的仓库数据,然后将其加载到使用新版本的 Subversion 新创建的仓库中。现在,自从 Subversion 1.0 版本发布以来,这些类型的模式更改不再发生,Subversion 开发人员承诺不会在 Subversion 的次要版本之间(例如,从 1.3 到 1.4)升级时强迫用户转储和加载他们的仓库。但是,仍然有其他转储和加载的原因,包括在新的操作系统或 CPU 架构上重新部署 Berkeley DB 仓库、在 Berkeley DB 和 FSFS 后端之间切换,或者(正如我们将在 名为“过滤仓库历史记录”的部分 中介绍的那样)从仓库历史记录中清除版本化数据。

无论您迁移仓库历史记录的原因是什么,使用 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 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 客户端对该仓库进行提交相同的结果。与提交一样,您可以使用钩子程序在加载过程中的每次提交之前和之后执行操作。通过将 --use-pre-commit-hook--use-post-commit-hook 选项传递给 svnadmin load,您可以指示 Subversion 分别为每个加载的修订版执行预提交和后提交钩子程序。例如,您可以使用它们来确保加载的修订版通过与常规提交相同的验证步骤。当然,您应该谨慎使用这些选项——如果您的后提交钩子程序为每个新的提交向邮件列表发送电子邮件,您可能不想向该列表连续发送数百或数千封提交电子邮件!您可以在 名为“实现仓库钩子”的部分 中阅读有关使用钩子脚本的更多信息。

请注意,由于 svnadmin 使用标准输入和输出流进行仓库转储和加载过程,因此感觉特别爽的人可以尝试以下操作(甚至可以使用不同版本的 svnadmin 在管道的每一侧)

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

默认情况下,转储文件将非常大——比仓库本身大得多。这是因为默认情况下,每个文件的每个版本都以完整的文本形式表达在转储文件中。这是最快捷且最简单的行为,如果您将转储数据直接管道到其他进程(例如压缩程序、过滤程序或加载进程),则很有用。但是,如果您正在创建用于长期存储的转储文件,则可能需要使用 --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 的输出都是自包含的,因此默认情况下第一个转储的修订版是仓库中该修订版中的每个目录、文件和属性的完整表示。

但是,您可以更改此默认行为。如果您在转储仓库时添加 --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-dumpfilecal-dumpfiless-dumpfile,您可以首先创建一个新的仓库来容纳它们

$ svnadmin create /path/to/projects
$

然后,在仓库中创建新的目录,这些目录将封装前三个仓库中每个仓库的内容

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

最后,将各个转储文件加载到它们在新仓库中的相应位置

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

我们还将提到使用 Subversion 仓库转储格式的最后一种方式——从完全不同的存储机制或版本控制系统进行转换。由于转储文件格式在很大程度上是人类可读的,因此使用此文件格式描述通用的一组更改(每个更改都应被视为一个新的修订版)应该比较容易。事实上,cvs2svn 实用程序(请参阅 名为“将仓库从 CVS 转换为 Subversion”的部分)使用转储格式来表示 CVS 仓库的内容,以便这些内容可以复制到 Subversion 仓库中。

过滤仓库历史记录

由于 Subversion 使用二进制差异算法和数据压缩(可选地在一个完全不透明的数据库系统中)来存储您的版本化历史记录,因此尝试手动调整是不明智的,即使不是完全困难,也强烈建议不要这样做。并且,一旦数据存储在您的仓库中,Subversion 通常不提供简单的方法来删除这些数据。[34] 但是,您不可避免地会希望有时操作仓库的历史记录。您可能需要删除所有不小心添加到仓库的(出于某种原因不应存在)文件的实例。[35] 或者,您可能有多个项目共享同一个仓库,并且您决定将它们拆分为各自的仓库。为了完成此类任务,管理员需要一个更易于管理和灵活的仓库中数据的表示形式——Subversion 仓库转储格式。

正如我们在 名为“将仓库数据迁移到其他地方”的部分 中介绍的那样,Subversion 仓库转储格式是对您对版本化数据所做的更改(随着时间的推移)的人类可读表示形式。您可以使用 svnadmin dump 命令生成转储数据,并使用 svnadmin load 来用它填充新的仓库(请参阅 名为“将仓库数据迁移到其他地方”的部分)。转储格式的人类可读性方面最棒的地方在于,如果您不粗心大意,您可以手动检查和修改它。当然,缺点是,如果您有三年仓库活动封装在一个可能非常大的转储文件中,您可能需要很长时间才能手动检查和修改它。

这就是 svndumpfilter 发挥作用的地方。这个程序充当仓库转储流的基于路径的过滤器。只需提供要保留的路径列表或要排除的路径列表,然后将您的仓库转储数据通过此过滤器。结果将是一个修改后的转储数据流,其中只包含您(显式或隐式)请求的版本化路径。

让我们看一个如何使用此程序的现实例子。我们在其他地方讨论了(参见 名为“规划您的仓库组织”的部分)如何选择仓库中数据的布局——每个项目使用一个仓库还是将它们组合在一起,在您的仓库中安排内容,等等。但是,有时在新的修订开始飞入后,您会重新考虑您的布局,并希望进行一些更改。一个常见的更改是将共享单个仓库的多个项目移动到每个项目的单独仓库。

我们假设的仓库包含三个项目:calccalendarspreadsheet。它们在这样的布局中并排存在:

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

为了将这三个项目放入它们自己的仓库中,我们首先转储整个仓库

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

接下来,将该转储文件通过过滤器运行,每次只包含一个顶级目录,并生成三个新的转储文件

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

此时,您必须做出决定。您的每个转储文件都将创建一个有效的仓库,但会完全保留它们在原始仓库中的路径。这意味着即使您有一个仅用于 calc 项目的仓库,该仓库仍然会有一个名为 calc 的顶级目录。如果您希望您的 trunktagsbranches 目录位于仓库的根目录,您可能希望编辑您的转储文件,调整 Node-pathNode-copyfrom-path 标头,不再具有第一个 calc/ 路径组件。此外,您还需要删除创建 calc 目录的转储数据部分。它看起来像这样

Node-path: calc
Node-action: add
Node-kind: dir
Content-length: 0
  

警告

如果您确实计划手动编辑转储文件以删除顶级目录,请确保您的编辑器未设置为自动将行尾字符转换为本机格式(例如,\r\n 为 \n),因为内容将不再与元数据一致。这将使转储文件变得毫无用处。

现在剩下的就是创建三个新的仓库,并将每个转储文件加载到正确的仓库中

$ svnadmin create calc; svnadmin load 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 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 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 的路径中包含前导斜杠(如果没有,则不应包含)。此外,如果您的转储文件由于某种原因在使用前导斜杠方面不一致,[36] 您可能应该规范化这些路径,以便它们都具有或不具有前导斜杠。

此外,复制的路径可能会给您带来一些麻烦。Subversion 支持仓库中的复制操作,其中通过复制某个已存在的路径来创建一个新路径。在仓库的生命周期中的某个时刻,您可能已经从 svndumpfilter 正在排除的某个位置复制了一个文件或目录,到它正在包含的某个位置。为了使转储数据自包含,svndumpfilter 仍然需要显示新路径的添加——包括复制创建的任何文件的內容——并且不要将该添加表示为从不会出现在您的过滤转储数据流中的源复制。但由于 Subversion 仓库转储格式只显示每个修订中发生了什么更改,因此复制源的內容可能无法立即获得。如果您怀疑您的仓库中存在任何这种类型的复制,您可能需要重新考虑您的包含/排除路径集,也许还要包括用作您麻烦的复制操作源的路径。

最后,svndumpfilter 对路径过滤非常严格。如果您尝试复制以 trunk/my-project 为根的项目的历史记录,并将其移动到一个单独的仓库中,您当然会使用 svndumpfilter include 命令来保留 trunk/my-project 中和下方的所有更改。但生成的转储文件不会对您打算将这些数据加载到的仓库进行任何假设。具体来说,转储数据可能以添加 trunk/my-project 目录的修订开始,但它不会包含会创建 trunk 目录本身的指令(因为 trunk 与 include 过滤器不匹配)。您需要确保在尝试将该流加载到该仓库之前,新转储流期望存在的任何目录在目标仓库中确实存在。

仓库复制

有几种情况下,拥有一个 Subversion 仓库非常方便,该仓库的版本历史记录与其他仓库的版本历史记录完全相同。也许最明显的一种是维护一个简单的备份仓库,当主仓库由于硬件故障、网络中断或其他类似的烦恼而变得无法访问时使用。其他场景包括部署镜像仓库以将繁重的 Subversion 负载分布在多个服务器上,用作软升级机制,等等。

从 1.4 版本开始,Subversion 提供了一个用于管理类似场景的程序——svnsyncsvnsync 的工作原理本质上是要求 Subversion 服务器“重放”修订,一次一个。然后,它使用该修订信息来模拟对另一个仓库的相同提交。两个仓库都不需要在运行 svnsync 的机器上本地访问——它的参数是仓库 URL,它通过 Subversion 的仓库访问 (RA) 接口完成所有工作。它只需要对源仓库的读访问权限和对目标仓库的读/写访问权限。

注意

当针对远程源仓库使用 svnsync 时,该仓库的 Subversion 服务器必须运行 Subversion 1.4 或更高版本。

假设您已经有一个要镜像的源仓库,那么您接下来需要的是一个空的目標仓库,它将充当该镜像。该目標仓库可以使用两种可用的文件系统数据存储后端(参见 名为“选择数据存储”的部分),但它目前不能有任何版本历史记录。svnsync 通过其通信修订信息的协议对源仓库和目標仓库中包含的版本化历史记录之间的不匹配非常敏感。因此,虽然 svnsync 无法要求目標仓库为只读,[37] 允许目標仓库中的版本历史记录通过除镜像过程之外的任何机制发生变化,将是灾难的根源。

警告

不要以导致其版本历史记录偏离其镜像的仓库的方式修改镜像仓库。在该镜像仓库上发生的唯一提交和修订属性修改应该是由 svnsync 工具执行的。

目標仓库的另一个要求是允许 svnsync 过程修改某些修订属性。svnsync 在目标仓库的修订 0 上的特殊修订属性中存储其簿记信息。因为 svnsync 在该仓库的钩子系统框架内工作,所以仓库的默认状态(即不允许修订属性更改;参见 pre-revprop-change)是不够的。您需要明确实现 pre-revprop-change 钩子,并且您的脚本必须允许 svnsync 设置和更改其特殊属性。有了这些规定,您就可以开始镜像仓库修订了。

提示

最好实现授权措施,允许您的仓库复制过程执行其任务,同时阻止其他用户完全修改您的镜像仓库的內容。

让我们在一些典型的镜像场景中逐步讲解 svnsync 的使用。我们将在本次讨论中加入一些实际建议,如果您不需要或不适合您的环境,可以随意忽略它们。

为了服务于我们最喜欢的版本控制系统的优秀开发者,我们将镜像公共 Subversion 源代码仓库,并在互联网上公开展示该镜像,该镜像托管在与原始 Subversion 源代码仓库所在的机器不同的机器上。该远程主机有一个全局配置,允许匿名用户读取该主机上仓库的內容,但要求用户进行身份验证才能修改这些仓库。(请原谅我们暂时忽略了 Subversion 服务器配置的细节——这些细节在 第 6 章,服务器配置 中进行了全面介绍。)并且出于没有其他原因,只是为了使示例更有趣,我们将从第三台机器(即我们目前正在使用的机器)驱动复制过程。

首先,我们将创建将用作镜像的仓库。这一步以及接下来的几步需要访问镜像仓库所在的机器的 shell 权限。不过,一旦仓库配置完成,我们就不需要再直接操作它了。

$ ssh admin@svn.example.com \
      "svnadmin create /path/to/repositories/svn-mirror"
admin@svn.example.com's password: ********
$

现在,我们已经有了仓库,而且由于我们的服务器配置,这个仓库现在在互联网上是“在线”的。现在,因为我们不希望任何东西修改仓库,除了我们的复制过程,我们需要一种方法来区分这个过程和其他潜在的提交者。为此,我们为我们的进程使用一个专用的用户名。只有由特殊用户名 syncuser 执行的提交和修订版本属性修改将被允许。

我们将使用仓库的钩子系统来允许复制过程执行它需要做的事情,并确保只有它在执行这些操作。我们通过实现两个仓库事件钩子来实现这一点——pre-revprop-change 和 start-commit。我们的 pre-revprop-change 钩子脚本位于 示例 5.2,“镜像仓库的 pre-revprop-change 钩子脚本” 中,它基本上验证了尝试更改属性的用户是否是我们的 syncuser 用户。如果是,则允许更改;否则,则拒绝。

示例 5.2:镜像仓库的 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.3,“镜像仓库的 start-commit 钩子脚本” 中的那个。

示例 5.3:镜像仓库的 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 子命令来完成此操作。请注意,各种 svnsync 子命令提供了一些与 svn 相同的与身份验证相关的选项:--username--password--non-interactive--config-dir--no-auth-cache

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

Initialize a destination repository for synchronization from
another repository.

The destination URL must point to the root of a repository with
no committed revisions.  The destination repository must allow
revision property changes.

You should not commit to, or make revision property changes in,
the destination repository by any method other than 'svnsync'.
In other words, the destination repository should be a read-only
mirror of the source repository.

Valid options:
  --non-interactive        : do no interactive prompting
  --no-auth-cache          : do not cache authentication tokens
  --username arg           : specify a username ARG
  --password arg           : specify a password ARG
  --config-dir arg         : read user configuration files from directory ARG

$ svnsync initialize http://svn.example.com/svn-mirror \
                     http://svn.collab.net/repos/svn \
                     --username syncuser --password syncpass
Copied properties for revision 0.
$

我们的目标仓库现在将记住它是一个公共 Subversion 源代码仓库的镜像。请注意,我们为 svnsync 提供了用户名和密码作为参数——这是我们的镜像仓库上的 pre-revprop-change 钩子所要求的。

注意

提供给 svnsync 的 URL 必须分别指向目标和源仓库的根目录。该工具不处理仓库子树的镜像。

注意

最初发布的 svnsync(在 Subversion 1.4 中)有一个小缺陷——提供给 --username--password 命令行选项的值被用于针对源和目标仓库进行身份验证。显然,不能保证同步用户的凭据在两个地方都是相同的。如果它们不相同,则尝试在非交互模式下(使用 --non-interactive 选项)运行 svnsync 的用户可能会遇到问题。

现在到了激动人心的时刻。只需使用一个子命令,我们就可以告诉 svnsync 将所有尚未镜像的修订版本从源仓库复制到目标仓库。[38] svnsync synchronize 子命令将查看之前存储在目标仓库上的特殊修订版本属性,并确定它正在镜像哪个仓库以及最近镜像的修订版本是修订版本 0。然后,它将查询源仓库并确定该仓库中的最新修订版本。最后,它要求源仓库的服务器开始重播 0 和最新修订版本之间的所有修订版本。当 svnsync 从源仓库的服务器收到结果响应时,它开始将这些修订版本转发到目标仓库的服务器,作为新的提交。

$ svnsync help synchronize
synchronize (sync): usage: svnsync synchronize DEST_URL

Transfer all pending revisions from source to destination.
…
$ svnsync synchronize http://svn.example.com/svn-mirror \
                      --username syncuser --password syncpass
Committed revision 1.
Copied properties for revision 1.
Committed revision 2.
Copied properties for revision 2.
Committed revision 3.
Copied properties for revision 3.
…
Committed revision 23406.
Copied properties for revision 23406.
Committed revision 23407.
Copied properties for revision 23407.
Committed revision 23408.
Copied properties for revision 23408.

这里特别值得注意的是,对于每个镜像的修订版本,首先是将该修订版本提交到目标仓库,然后是属性更改。这是因为初始提交是由(并归功于)用户 syncuser 执行的,并且带有该修订版本创建时的日期时间戳。此外,Subversion 的底层仓库访问接口没有提供在提交时设置任意修订版本属性的机制。因此,svnsync 会立即进行一系列属性修改,这些修改将从源仓库中复制该修订版本中找到的所有修订版本属性到目标仓库中。这也具有将修订版本的作者和日期时间戳修复为与源仓库一致的效果。

另一个值得注意的是,svnsync 执行细致的簿记,允许它安全地中断和重新启动,而不会破坏镜像数据的完整性。如果在镜像仓库时出现网络故障,只需重复 svnsync synchronize 命令,它会很乐意从它停止的地方继续执行。事实上,当源仓库中出现新的修订版本时,这正是为了保持镜像更新而需要做的事情。

然而,在这个过程中,确实存在一个不太优雅的地方。因为 Subversion 修订版本属性可以在仓库的生命周期中的任何时候更改,并且不会留下指示它们何时更改的审计跟踪,所以复制过程必须特别注意它们。如果你已经镜像了仓库的前 15 个修订版本,然后有人更改了修订版本 12 上的修订版本属性,svnsync 将不会知道要返回并修补它对修订版本 12 的副本。你将需要使用(或使用一些额外的工具)svnsync copy-revprops 子命令来手动告诉它这样做,该命令只是重新复制特定修订版本的修订版本属性。

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

Copy all revision properties for revision REV from source to
destination.
…
$ svnsync copy-revprops http://svn.example.com/svn-mirror 12 \
                        --username syncuser --password syncpass
Copied properties for revision 12.
$

这就是仓库复制的概括。你可能想要围绕这个过程进行一些自动化。例如,虽然我们的示例是一个拉取和推送设置,但你可能希望你的主仓库将更改推送到一个或多个授权镜像,作为其 post-commit 和 post-revprop-change 钩子实现的一部分。这将使镜像能够尽可能接近实时地保持最新。

此外,虽然这样做并不常见,但 svnsync 会优雅地镜像仓库,其中它进行身份验证的用户只拥有部分读取权限。它只复制它被允许查看的仓库部分。显然,这样的镜像不能用作备份解决方案。

就用户与仓库和镜像的交互而言,它确实有可能拥有一个与两者交互的单个工作副本,但你必须跳过一些步骤才能实现它。首先,你需要确保主仓库和镜像仓库具有相同的仓库 UUID(默认情况下并非如此)。你可以通过将包含主仓库的 UUID 的转储文件存根加载到镜像仓库中来设置镜像仓库的 UUID,方法如下

$ cat - <<EOF | svnadmin load --force-uuid dest
SVN-fs-dump-format-version: 2

UUID: 65390229-12b7-0310-b90b-f21a5aa7ec8e
EOF
$

现在这两个仓库具有相同的 UUID,你可以使用 svn switch --relocate 将你的工作副本指向你希望对其进行操作的任何一个仓库,这是一个在 svn switch 中描述的过程。但是,这里可能存在一个危险,即如果主仓库和镜像仓库没有保持紧密同步,则如果将一个与主仓库保持最新并指向主仓库的工作副本重新定位为指向一个过时的镜像,它会对突然丢失它完全期望存在的修订版本感到困惑,并抛出相应的错误。如果发生这种情况,你可以将你的工作副本重新定位回主仓库,然后要么等待镜像仓库更新,要么将你的工作副本回溯到你知道存在于同步仓库中的一个修订版本,然后重试重新定位。

最后,请注意,svnsync 提供的基于修订版本的复制仅仅是基于修订版本的复制。它不包括诸如钩子实现、仓库或服务器配置数据、未提交的事务或有关用户对仓库路径的锁定信息等内容。只有由 Subversion 仓库转储文件格式携带的信息可用于复制。

仓库备份

尽管自现代计算机诞生以来技术取得了长足进步,但有一件事不幸的是,它以水晶般清晰的清晰度响彻——有时,事情会变得非常非常糟糕。停电、网络连接中断、内存损坏和硬盘崩溃仅仅是命运即将对即使是最尽职尽责的管理员施加的邪恶的一小部分。因此,我们来到一个非常重要的主题——如何备份仓库数据。

Subversion 仓库管理员可以使用两种类型的备份方法——完整备份和增量备份。仓库的完整备份涉及在一次行动中隐藏所有重建仓库所需的信息,以防发生灾难。通常,这意味着,从字面上说,复制整个仓库目录(其中包括 Berkeley DB 或 FSFS 环境)。增量备份是较小的备份,只备份自上次备份以来已更改的仓库数据部分。

就完整备份而言,天真的方法可能看起来很明智,但是,除非你暂时禁用对仓库的所有其他访问,否则简单地进行递归目录复制可能会导致备份失败。在 Berkeley DB 的情况下,文档描述了可以复制数据库文件的特定顺序,这将保证有效的备份副本。类似的顺序也存在于 FSFS 数据中。但是,你不必自己实现这些算法,因为 Subversion 开发团队已经做到了。 svnadmin hotcopy 命令负责进行仓库热备份时涉及的细枝末节。它的调用与 Unix 的 cp 或 Windows 的 copy 操作一样简单

$ svnadmin hotcopy /path/to/repos /path/to/repos-backup

生成的备份是一个功能齐全的 Subversion 仓库,如果出现严重问题,可以作为你的活动仓库的替代品。

当复制 Berkeley DB 仓库时,你甚至可以指示 svnadmin hotcopy 在完成复制后从原始仓库中清除任何未使用的 Berkeley DB 日志文件(请参阅 名为“清除未使用的 Berkeley DB 日志文件”的部分)。只需在命令行上提供 --clean-logs 选项即可。

$ svnadmin hotcopy --clean-logs /path/to/bdb-repos /path/to/bdb-repos-backup

此命令还提供了一些额外的工具。Subversion 源代码分发版中的 tools/backup/ 目录包含 hot-backup.py 脚本。此脚本在 svnadmin hotcopy 之上添加了一些备份管理功能,允许您只保留每个存储库配置的最大数量的最新备份。它会自动管理备份存储库目录的名称,以避免与以前的备份发生冲突,并将“轮换掉”旧的备份,删除它们,以便只保留最新的备份。即使您还有增量备份,您可能也希望定期运行此程序。例如,您可以考虑使用 hot-backup.py 来自程序调度程序(例如 Unix 系统上的 cron),它会导致该程序在夜间(或在您认为安全的任何时间粒度)运行。

一些管理员使用围绕生成和存储存储库转储数据构建的不同备份机制。我们在 名为“将存储库数据迁移到其他位置”的部分 中描述了如何使用 svnadmin dump --incremental 执行给定修订或修订范围的增量备份。当然,还有此命令的完整备份变体,通过省略 --incremental 选项实现。这些方法具有一定的价值,因为备份信息的格式很灵活,不受特定平台、版本化文件系统类型或 Subversion 或 Berkeley DB 版本的约束。但是这种灵活性是有代价的,即恢复这些数据可能需要很长时间,随着您对存储库提交的每个新修订版,恢复时间会更长。此外,与许多不同的备份方法一样,对已备份修订版所做的修订版属性更改不会被非重叠的增量转储生成捕获。由于这些原因,我们建议不要完全依赖基于转储的备份方法。

如您所见,各种备份类型和方法各有优缺点。最简单的方法是完整的热备份,它将始终生成存储库的完美工作副本。如果您的实时存储库出现问题,您可以通过简单的递归目录复制从备份中恢复。不幸的是,如果您维护存储库的多个备份,这些完整副本将分别消耗与实时存储库一样多的磁盘空间。相比之下,增量备份的生成速度更快,存储空间更小。但是恢复过程可能很麻烦,通常涉及应用多个增量备份。其他方法也有自己的特点。管理员需要在进行备份的成本和恢复备份的成本之间找到平衡。

svnsync 程序(请参见 名为“存储库复制”的部分)实际上提供了一种非常实用的折中方法。如果您定期将只读镜像与主存储库同步,那么在紧急情况下,您的只读镜像可能是主存储库出现故障时替换它的一个不错的选择。这种方法的主要缺点是只有版本化存储库数据会同步,存储库配置文件、用户指定的存储库路径锁和其他可能存在于物理存储库目录中但不 存储库的虚拟版本化文件系统内的项目不会被 svnsync 处理。

在任何备份方案中,存储库管理员都需要了解对未版本化修订版属性的修改如何影响他们的备份。由于这些更改本身不会生成新的修订版,因此它们不会触发 post-commit hook,甚至可能不会触发 pre-revprop-change 和 post-revprop-change hook。[39] 并且由于您可以更改修订版属性而不考虑时间顺序,您可以在任何时候更改任何修订版的属性,因此最新几个修订版的增量备份可能无法捕获对作为先前备份的一部分包含的修订版的属性修改。

一般来说,只有真正疑心重的人才会需要备份他们的整个存储库,例如在每次提交时进行备份。但是,假设给定存储库具有其他冗余机制,并且具有相对精细的粒度(例如每次提交的电子邮件或增量转储),存储库管理员可能希望将数据库的热备份作为系统级夜间备份的一部分。这是您的数据,请尽可能地保护它。

通常,存储库备份的最佳方法是多样化的,利用此处描述的方法的组合。例如,Subversion 开发人员使用 hot-backup.py 和这些完整备份的脱机 rsync 每晚备份 Subversion 源代码存储库;保留所有提交和属性更改通知电子邮件的多个存档;并让各种志愿者使用 svnsync 维护存储库镜像。您的解决方案可能类似,但应该根据您的需求以及便利性和疑心病之间的微妙平衡进行调整。无论您做什么,都要定期验证您的备份,因为一个有洞的备用轮胎有什么用呢?虽然所有这些可能无法将您的硬件从命运的铁拳中拯救出来,[40] 但它肯定可以帮助您从那些艰难的时刻中恢复过来。



[32] 还是“sync”?

[33] 例如:硬盘 + 巨大的电磁体 = 灾难。

[34] 这正是您使用版本控制的原因,对吧?

[35] 有意识地谨慎地删除某些版本化数据实际上受到实际用例的支持。这就是为什么“obliterate”功能一直是 Subversion 最受请求的功能之一,也是 Subversion 开发人员希望尽快提供的功能。

[36] 虽然 svnadmin dump 具有一致的领先斜杠策略,即不包含它们,但其他生成转储数据的程序可能不那么一致。

[37] 事实上,它不能真正是只读的,否则 svnsync 本身在将修订版历史记录复制到其中会很困难。

[38] 请注意,虽然普通读者只需要几秒钟就能解析本段和随后的示例输出,但完成这种镜像操作所需的实际时间,怎么说呢,要长得多。

[39] svnadmin setlog 可以以一种完全绕过 hook 接口的方式调用。

[40] 你知道,所有“淘气的手指”的总称。