本文档用于描述 Subversion 1.2。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbooks.subversion.org.cn/ 并查阅适合您 Subversion 版本的版本。
维护 Subversion 存储库可能是一项艰巨的任务,主要是因为具有数据库后端的系统固有的复杂性。做好这项工作关键在于了解工具——它们是什么、何时使用它们以及如何使用它们。本节将向您介绍 Subversion 提供的存储库管理工具,以及如何使用这些工具来完成存储库迁移、升级、备份和清理等任务。
Subversion 提供了一些工具,可用于创建、检查、修改和修复您的存储库。让我们更仔细地看看这些工具中的每一个。之后,我们将简要介绍 Berkeley DB 分发版中包含的一些实用程序,这些实用程序提供了特定于存储库数据库后端的某些功能,而这些功能 Subversion 自身的工具没有提供。
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. …
几乎所有 svnlook 的子命令都可以对修订版树或事务树进行操作,打印有关树本身的信息,或者打印它与存储库的先前修订版的差异。您使用 --revision
和 --transaction
选项分别指定要检查的修订版或事务。请注意,虽然修订版号显示为自然数,但事务名称是字母数字字符串。请记住,文件系统只允许浏览未提交的事务(尚未导致新修订版的事务)。大多数存储库将没有此类事务,因为事务通常要么被提交(这使得它们无法查看),要么被中止并删除。
在没有 --revision
和 --transaction
选项的情况下,svnlook 将检查存储库中最新的(或“HEAD”)修订版。因此,当位于 /path/to/repos
的存储库中最新的修订版为 19 时,以下两个命令执行完全相同的操作
$ svnlook info /path/to/repos $ svnlook info /path/to/repos --revision 19
关于子命令的这些规则的唯一例外是 svnlook youngest 子命令,它不接受任何选项,只打印出 HEAD
修订版号。
$ svnlook youngest /path/to/repos 19
来自 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
子命令的输出为
作者,后面跟一个换行符。
日期,后面跟一个换行符。
日志消息中的字符数,后面跟一个换行符。
日志消息本身,后面跟一个换行符。
此输出是人可读的,这意味着日期戳之类的项目使用文本表示显示,而不是更模糊的东西(例如自 Tasty Freeze 小伙子经过以来的纳秒数)。但是,此输出也是机器可解析的——因为日志消息可以包含多行并且长度不受限制,因此 svnlook 在日志消息本身之前提供该消息的长度。这使脚本和其他围绕此命令的包装器能够对日志消息做出明智的决策,例如为消息分配多少内存,或者至少在该输出不是流中的最后一段数据的情况下跳过多少字节。
另一种常见的 svnlook 使用方式是实际查看修订版或事务树的内容。 svnlook tree 命令显示请求的树中的目录和文件。如果您提供 --show-ids
选项,它还会显示每个路径的文件系统节点修订版 ID(这通常对开发人员比对用户更有用)。
$ svnlook tree /path/to/repos --show-ids / <0.0.1> A/ <2.0.1> B/ <4.0.1> lambda <5.0.1> E/ <6.0.1> alpha <7.0.1> beta <8.0.1> F/ <9.0.1> mu <3.0.1> C/ <a.0.1> D/ <b.0.1> gamma <c.0.1> G/ <d.0.1> pi <e.0.1> rho <f.0.1> tau <g.0.1> H/ <h.0.1> chi <i.0.1> omega <k.0.1> psi <j.0.1> iota <1.0.1>
在您看到树中目录和文件的布局之后,您可以使用 svnlook cat、svnlook propget 和 svnlook proplist 等命令来深入了解这些文件和目录的详细信息。
svnlook 可以执行各种其他查询,显示我们之前提到的部分信息,报告给定修订版或事务中哪些路径被修改,显示对文件和目录进行的文本和属性更改,等等。以下是 svnlook 当前接受的子命令列表及其输出的简要说明
author
打印树的作者。
cat
打印树中文件的內容。
changed
列出树中更改的所有文件和目录。
date
打印树的日期戳。
diff
打印已更改文件的统一差异。
dirs-changed
列出树中自身发生更改的目录或其文件子项发生更改的目录。
history
显示版本控制路径的历史中的重要点(发生修改或复制的地方)。
info
打印树的作者、日期戳、日志消息字符计数和日志消息。
lock
如果路径被锁定,则描述锁定属性。
log
打印树的日志消息。
propget
打印树中路径上属性的值。
proplist
打印设置在树中路径上的属性的名称和值。
tree
打印树列表,可以选择显示与每个路径关联的文件系统节点修订版 ID。
uuid
打印存储库的 UUID——Universal Unique IDentifier。
youngest
打印最年轻的修订版号。
svnadmin 程序是存储库管理员的最佳帮手。除了提供创建 Subversion 存储库的功能之外,该程序还允许您对这些存储库执行多个维护操作。 svnadmin 的语法类似于 svnlook
$ svnadmin help general usage: svnadmin SUBCOMMAND REPOS_PATH [ARGS & OPTIONS ...] Type "svnadmin help <subcommand>" for help on a specific subcommand. Available subcommands: create deltify dump help (?, h) …
我们已经提到了 svnadmin 的 create
子命令(参见 名为“存储库创建和配置”的部分)。本章后面我们将更详细地介绍大多数其他子命令。现在,让我们快速浏览一下每个可用子命令提供的功能。
create
创建一个新的 Subversion 存储库。
deltify
遍历指定的修订版范围,对这些修订版中更改的路径执行前身三角化。如果未指定修订版,则此命令将简单地三角化 HEAD
修订版。
dump
使用可移植的转储格式转储存储库的内容,这些内容以给定的修订版集为界。
hotcopy
制作存储库的热备份。无论其他进程是否正在使用存储库,您都可以随时运行此命令并制作存储库的安全备份。
list-dblogs
(仅限 Berkeley DB 存储库。)列出与存储库关联的 Berkeley DB 日志文件路径。此列表包括所有日志文件——Subversion 仍在使用的日志文件,以及不再使用的日志文件。
list-unused-dblogs
(仅限 Berkeley DB 存储库。)列出与存储库关联,但不再使用的 Berkeley DB 日志文件路径。您可以安全地从存储库布局中删除这些日志文件,并可能将其归档以备不时之需,以便在您需要执行存储库的灾难性恢复时使用。
load
从使用与 dump
子命令生成的相同可移植转储格式的数据流中,将一组修订版加载到存储库中。
lslocks
列出并描述存储库中存在的任何锁定。
lstxns
列出存储库中当前存在的未提交的 Subversion 事务的名称。
recover
对需要恢复的存储库执行恢复步骤,通常是在发生致命错误后发生,该错误阻止了进程干净地关闭其与存储库的通信。
rmlocks
无条件地从列出的路径中删除锁定。
rmtxns
从存储库中干净地删除 Subversion 事务(方便地由 lstxns
子命令的输出提供)。
setlog
将存储库中给定修订版上的 svn:log
(提交日志消息)属性的当前值替换为新值。
verify
验证存储库的内容。这包括但不限于存储在存储库中的版本控制数据的校验和比较。
由于 Subversion 将所有内容存储在不透明的数据库系统中,因此尝试手动调整是不明智的,即使不是非常困难。并且一旦数据存储在您的存储库中,Subversion 通常不会提供一种简单的方法来删除这些数据。 [15] 但是不可避免地,总会有时候您希望操纵存储库的历史记录。您可能需要删除所有意外添加到存储库的(并且由于某种原因不应该存在于其中的)文件的实例。或者,您可能有多个项目共享一个存储库,并且您决定将它们拆分为各自的存储库。为了完成此类任务,管理员需要对存储库中的数据进行更易于管理和更具可塑性的表示——Subversion 存储库转储格式。
Subversion 存储库转储格式是对您随时间推移对版本控制数据所做的更改的人类可读表示。您可以使用 svnadmin dump 命令生成转储数据,并使用 svnadmin load 将其填充到一个新存储库中(参见 名为“迁移存储库”的部分)。转储格式的人类可读性方面最棒的一点是,如果您不粗心,就可以手动检查和修改它。当然,缺点是如果您有两年的存储库活动封装在一个可能非常大的转储文件中,那么手动检查和修改它可能需要很长时间。
虽然它不会成为管理员最常用的工具,但svndumpfilter 提供了一种非常特殊的实用功能——能够通过充当基于路径的过滤器来快速轻松地修改转储数据。只需向它提供您想要保留的路径列表或您不想要保留的路径列表,然后将您的存储库转储数据通过此过滤器传递。结果将是修改后的转储数据流,其中仅包含您(显式或隐式)请求的版本化路径。
svndumpfilter 的语法如下
$ svndumpfilter help general usage: svndumpfilter SUBCOMMAND [ARGS & OPTIONS ...] Type "svndumpfilter help <subcommand>" for help on a specific subcommand. Available subcommands: exclude include help (?, h)
只有两个有趣的子命令。它们允许您在显式或隐式包含流中的路径之间进行选择
exclude
从转储数据流中过滤掉一组路径。
include
仅允许请求的路径集通过转储数据流。
让我们看一下如何使用此程序的一个现实示例。我们将在其他地方讨论(参见名为“选择存储库布局”的部分)如何决定为存储库中的数据选择布局——每个项目使用一个存储库或将它们组合在一起,在存储库中排列内容,等等。但有时,在新的修订开始传入后,您会重新考虑您的布局,并且希望进行一些更改。常见的更改是决定将共享单个存储库的多个项目移动到每个项目的单独存储库中。
我们假设的存储库包含三个项目:calc
、calendar
和 spreadsheet
。它们并排在一个这样的布局中
/ 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. … $
接下来,每次只包含一个顶级目录,将该转储文件通过过滤器运行,从而生成三个新的转储文件
$ cat repos-dumpfile | svndumpfilter include calc > calc-dumpfile … $ cat repos-dumpfile | svndumpfilter include calendar > cal-dumpfile … $ cat repos-dumpfile | svndumpfilter include spreadsheet > ss-dumpfile … $
此时,您需要做出决定。每个转储文件都将创建一个有效的存储库,但会完全保留它们在原始存储库中的路径。这意味着,即使您拥有一个专门用于 calc
项目的存储库,该存储库仍然会有一个名为 calc
的顶级目录。如果您希望您的 trunk
、tags
和 branches
目录位于存储库的根目录中,您可能希望编辑您的转储文件,调整 Node-path
和 Node-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-path
和 Node-copyfrom-path
标头。
… Node-path: spreadsheet/Makefile …
如果路径具有前导斜杠,则您应该在传递给 svndumpfilter include 和 svndumpfilter exclude 的路径中包含前导斜杠(如果它们没有,则不应包含)。此外,如果您的转储文件由于某种原因对前导斜杠的使用不一致,[16] 您可能应该规范这些路径,以便它们都具有或不具有前导斜杠。
此外,复制路径可能会给您带来一些麻烦。Subversion 支持存储库中的复制操作,其中通过复制某些已经存在的路径来创建一个新路径。在存储库的生存期中,您可能将文件或目录从 svndumpfilter 要排除的某个位置复制到它要包含的某个位置。为了使转储数据自给自足,svndumpfilter 仍然需要显示新路径的添加——包括复制创建的任何文件的內容——并且不将该添加表示为从将不存在于您的已过滤转储数据流中的源进行复制。但由于 Subversion 存储库转储格式只显示每个修订中发生了什么更改,因此复制源的內容可能无法立即获得。如果您怀疑存储库中存在任何此类复制,您可能需要重新考虑包含/排除路径集。
如果您使用的是 Berkeley DB 存储库,那么您的所有版本化文件系统的结构和数据都驻留在存储库 db
子目录中的一组数据库表中。此子目录是一个常规的 Berkeley DB 环境目录,因此可以与任何 Berkeley 数据库工具结合使用(您可以在 Sleepycat 的网站上查看这些工具的文档,http://www.sleepycat.com/)。
对于日常 Subversion 使用,这些工具是不必要的。通常用于 Subversion 存储库的大多数功能已在 svnadmin 工具中复制。例如,svnadmin list-unused-dblogs 和 svnadmin list-dblogs 执行 Berkeley db_archive 命令提供的功能的一个子集,而 svnadmin recover 反映了 db_recover 实用程序的常见用例。
您仍然会发现一些 Berkeley DB 实用程序很有用。程序 db_dump 和 db_load 分别写入和读取一个自定义文件格式,该格式描述了 Berkeley DB 数据库中的键和值。由于 Berkeley 数据库无法跨机器体系结构移植,因此这种格式是将这些数据库从一台机器转移到另一台机器的实用方法,无论体系结构或操作系统如何。此外,db_stat 实用程序可以提供有关 Berkeley DB 环境状态的有用信息,包括有关锁定和存储子系统的详细统计信息。
配置好您的 Subversion 存储库后,通常只需要很少的关注。但是,有时管理员可能需要进行一些手动操作。svnadmin 实用程序提供了一些有用的功能来帮助您执行以下任务,例如
修改提交日志消息,
删除无效的事务,
恢复“卡住”的存储库,以及
将存储库内容迁移到另一个存储库。
也许 svnadmin 子命令中最常用的命令是 setlog
。当事务提交到存储库并提升为修订时,与该新修订关联的描述性日志消息(由用户提供)将存储为附加到修订本身的非版本化属性。换句话说,存储库只记住该属性的最新值,并丢弃以前的属性。
有时,用户会在她的日志消息中出现错误(例如拼写错误或一些错误信息)。如果存储库被配置为(使用 pre-revprop-change
和 post-revprop-change
钩子;请参见名为“钩子脚本”的部分)在提交完成后接受对该日志消息的更改,那么用户可以使用 svn 程序的 propset
命令(参见第 9 章,Subversion 完整参考)远程“修复”她的日志消息。但是,由于可能会永远丢失信息,因此 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 命令来绕过这些保护。
但请记住,通过绕过钩子,您可能会避免诸如属性更改的电子邮件通知、跟踪非版本化属性更改的备份系统等等。换句话说,请务必谨慎更改内容以及更改方式。
svnadmin 的另一个常见用途是查询存储库以查找未完成的(可能是无效的)Subversion 事务。如果提交失败,则通常会清理该事务。也就是说,事务本身会从存储库中删除,并且与该事务关联的任何数据(以及仅与该事务关联的数据)也会被删除。但是,有时会以一种无法清理事务的方式发生故障。这可能出于多种原因:也许客户端操作被用户粗鲁地终止,或者操作过程中可能发生了网络故障,等等。无论原因如何,无效事务都可能发生。它们不会造成任何实际的损害,除了消耗一小部分磁盘空间。尽管如此,一丝不苟的管理员可能仍然希望删除它们。
您可以使用 svnadmin 的 lstxns
命令列出当前未完成的事务的名称。
$ svnadmin lstxns myrepos 19 3a1 a45 $
然后,可以使用 svnlook(及其 --transaction
选项)来确定每个结果输出项是由谁创建的,何时创建的,以及在事务中进行了哪些类型的更改——换句话说,该事务是否是一个安全的删除候选者!如果是,则可以将事务的名称传递给 svnadmin rmtxns,它将执行事务的清理。实际上,rmtxns
子命令可以直接从 lstxns
的输出中获取输入!
$ svnadmin rmtxns myrepos `svnadmin lstxns myrepos` $
如果您使用这两个子命令,您应该考虑将您的仓库暂时对客户端不可访问。这样,在您开始清理之前,没有人可以开始合法的交易。以下是快速生成有关仓库中每个未完成交易的信息的 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}" --transaction "${TXN}" done
您可以使用 /path/to/txn-info.sh /path/to/repos 运行前面的脚本。输出基本上是几个 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 提交日志等等——可以用于决策过程。最后,管理员通常可以简单地与看似死亡交易的所有者(例如通过电子邮件)进行沟通,以验证该交易实际上是否处于僵尸状态。
虽然存储成本在过去几年中大幅下降,但磁盘使用仍然是管理员寻求对大量数据进行版本控制的有效关注点。实时仓库消耗的每个额外字节都是需要备份到异地的一个字节,可能是作为轮换备份计划的一部分而多次备份。如果使用 Berkeley DB 仓库,主要存储机制是一个复杂的数据库系统,了解哪些数据需要保留在实时站点上,哪些需要备份,哪些可以安全删除是有用的。本节专门针对 Berkeley DB;FSFS 仓库没有需要清理或回收的额外数据。
直到最近,与 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 $ svnadmin list-unused-dblogs /path/to/repos | xargs rm ## disk space reclaimed!
为了使仓库的尺寸尽可能小,Subversion 在仓库本身内部使用了 增量化(或“增量存储”)。增量化涉及将数据块的表示编码为针对某个其他数据块的差异集合。如果两个数据块非常相似,则此增量化会导致增量化块的存储节省——它不会占用与原始数据大小相等的存储空间,而只占用足够的空间来表示“我看起来与这里面的另一个数据块完全一样,除了以下几个更改”。具体来说,每次将文件的最新版本提交到仓库时,Subversion 都会将前一个版本(实际上是几个前一个版本)编码为针对最新版本的增量。结果是,大多数倾向于较大尺寸的仓库数据——即版本化文件的内容——以比原始“全文”表示形式小得多的尺寸存储。
由于所有经受增量化的 Subversion 仓库数据都存储在一个 Berkeley DB 数据库文件中,因此减小存储值的尺寸不一定能减小数据库文件本身的尺寸。但是,Berkeley DB 会保留数据库文件中未使用区域的内部记录,并在扩展数据库文件大小之前优先使用这些区域。因此,虽然增量化不会产生即时的空间节省,但它可以极大地减缓数据库的未来增长。
如 名为“Berkeley DB”的部分 所述,如果 Berkeley DB 仓库没有正确关闭,有时会处于冻结状态。发生这种情况时,管理员需要将数据库倒带到一致状态。
为了保护仓库中的数据,Berkeley DB 使用了一种锁定机制。这种机制确保数据库的各个部分不会被多个数据库访问器同时修改,并且每个进程在从数据库中读取数据时都能看到数据处于正确的状态。当一个进程需要更改数据库中的某些内容时,它首先检查目标数据上是否存在锁。如果数据没有被锁定,该进程就会锁定数据,进行它想要进行的更改,然后解锁数据。其他进程必须等到锁被移除才能继续访问数据库的该部分。(这与您作为用户可以对仓库中版本化的文件应用的锁无关;有关更多信息,请参见 关于“锁”的三个含义)。
在使用 Subversion 仓库的过程中,致命错误(例如磁盘空间或可用内存不足)或中断可能会阻止进程有机会移除它在数据库中放置的锁。结果是后端数据库系统被“卡住”。发生这种情况时,任何尝试访问仓库的尝试都会无限期地挂起(因为每个新的访问器都在等待锁消失——这不会发生)。
首先,如果您的仓库发生了这种情况,请不要惊慌。Berkeley DB 文件系统利用数据库事务、检查点和预写日志来确保只有最灾难性的事件[17] 才能永久破坏数据库环境。足够偏执的仓库管理员会以某种方式对仓库数据进行异地备份,但请不要立即打电话给您的系统管理员以恢复备份磁带。
其次,使用以下方法尝试“解卡”您的仓库
确保没有进程访问(或尝试访问)仓库。对于网络仓库,这意味着也关闭 Apache HTTP 服务器。
成为拥有和管理仓库的用户。这一点很重要,因为以错误的用户身份恢复仓库可能会以某种方式调整仓库文件的权限,即使仓库“解卡”后,普通用户也将无法访问它。
运行命令 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.
此命令可能需要几分钟才能完成。
重启 Subversion 服务器。
此过程修复了几乎所有仓库锁定情况。确保您以拥有和管理数据库的用户身份运行此命令,而不仅仅是以 root
身份运行。恢复过程的一部分可能涉及从头开始重建各种数据库文件(例如共享内存区域)。以 root
身份恢复会创建这些文件,使得它们由 root
拥有,这意味着即使您恢复了对仓库的连接,普通用户也将无法访问它。
如果出于某种原因,前面的过程未能成功解卡您的仓库,您应该执行两件事。首先,将损坏的仓库移开,并恢复其最新的备份。然后,向 Subversion 用户列表(位于 <[email protected]>
)发送一封电子邮件,详细描述您的问题。数据完整性对于 Subversion 开发人员来说是一个极高的优先级。
Subversion 文件系统将其数据分散在各种数据库表中,这种方式通常只有 Subversion 开发人员自己才能理解(并且对此感兴趣)。但是,可能会出现需要将所有数据或其子集收集到单个可移植的平面文件格式中的情况。Subversion 提供了这样的机制,它在两个 svnadmin 子命令中实现:dump
和 load
。
转储和加载 Subversion 仓库最常见的原因是 Subversion 本身的变化。随着 Subversion 的成熟,有时对后端数据库模式进行的某些更改会导致 Subversion 与以前版本的仓库不兼容。转储和加载的其他原因可能是将 Berkeley DB 仓库迁移到新的操作系统或 CPU 架构,或在 Berkeley DB 和 FSFS 后端之间切换。建议的操作步骤相对简单
使用您 当前 版本的 svnadmin 将您的仓库转储到转储文件。
升级到 Subversion 的新版本。
将旧的仓库移开,并使用您 新的 svnadmin 在它们的位置创建新的空仓库。
再次使用您 新的 svnadmin 将转储文件加载到各自的、刚刚创建的仓库中。
请务必将旧版本库中的所有自定义内容复制到新版本库中,包括 DB_CONFIG
文件和钩子脚本。您需要关注 Subversion 新版本的发布说明,以查看自上次升级以来是否有任何更改会影响这些钩子或配置选项。
如果迁移过程使您的版本库可通过不同的 URL 访问(例如,迁移到不同的计算机或通过不同的架构访问),那么您可能需要告诉用户在他们现有的工作副本上运行 svn switch --relocate。请参见 svn switch。
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 分别对每个加载的修订版执行 pre-commit 和 post-commit 钩子脚本。例如,您可以使用它们来确保加载的修订版通过与常规提交相同的验证步骤。当然,您应该谨慎使用这些选项,如果您 post-commit 钩子将电子邮件发送到邮件列表以供每个新提交使用,那么您可能不希望在每个加载的修订版中连续快速地向该列表发送数百或数千封提交邮件!您可以在 “钩子脚本”部分 中详细了解钩子脚本的使用情况。
请注意,由于 svnadmin 使用标准输入和输出流进行版本库转储和加载过程,因此那些感觉特别大胆的人可以尝试以下操作(甚至可以在管道两端使用不同的 svnadmin 版本)
$ svnadmin create newrepos $ svnadmin dump myrepos | svnadmin load newrepos
默认情况下,转储文件将非常大,比版本库本身大得多。这是因为每个文件的每个版本都以转储文件中的完整文本形式表示。这是最快、最简单的行为,如果您将转储数据直接管道传输到其他进程(例如压缩程序、过滤程序或加载过程),这很好。但是,如果您要为长期存储创建转储文件,那么您可能希望使用 --deltas
开关来节省磁盘空间。使用此选项,文件的连续修订版将以压缩的二进制差异形式输出,就像文件修订版存储在版本库中一样。此选项速度较慢,但会导致转储文件的大小更接近原始版本库。
之前我们提到 svnadmin dump 输出一系列修订版。使用 --revision
选项指定要转储的单个修订版或修订版范围。如果您省略此选项,则将转储所有现有的版本库修订版。
$ svnadmin dump myrepos --revision 23 > rev-23.dumpfile $ svnadmin dump myrepos --revision 100:200 > revs-100-200.dumpfile
当 Subversion 转储每个新修订版时,它只输出足够的信息,以便未来的加载器能够基于上一个修订版重新创建该修订版。换句话说,对于转储文件中任何给定的修订版,只有该修订版中更改的项目将出现在转储中。此规则的唯一例外是使用当前 svnadmin dump 命令转储的第一个修订版。
默认情况下,Subversion 不会将第一个转储的修订版表示为仅需应用于上一个修订版的差异。首先,转储文件中没有上一个修订版!其次,Subversion 无法知道将加载转储数据的版本库的状态(如果确实发生了这种情况)。为了确保每次执行 svnadmin dump 的输出都是自包含的,默认情况下,第一个转储的修订版是对该版本库中该修订版中的每个目录、文件和属性的完整表示。
但是,您可以更改此默认行为。如果您在转储版本库时添加 --incremental
选项,svnadmin 将将第一个转储的修订版与版本库中的上一个修订版进行比较,与它处理其他每个转储的修订版的方式相同。然后,它将完全按照它处理转储范围中的其他修订版的方式输出第一个修订版,只提及该修订版中发生的更改。这样做的好处是,您可以创建多个可以连续加载的小型转储文件,而不是一个大型转储文件,如下所示
$ svnadmin dump myrepos --revision 0:1000 > dumpfile1 $ svnadmin dump myrepos --revision 1001:2000 --incremental > dumpfile2 $ svnadmin dump myrepos --revision 2001:3000 --incremental > dumpfile3
可以使用以下命令序列将这些转储文件加载到新的版本库中
$ svnadmin load newrepos < dumpfile1 $ svnadmin load newrepos < dumpfile2 $ svnadmin load newrepos < dumpfile3
您可以使用此 --incremental
选项执行的另一个巧妙技巧涉及将新的转储修订版范围附加到现有转储文件。例如,您可能有一个 post-commit
钩子,它只是将触发该钩子的单个修订版的版本库转储附加到该钩子。或者,您可能有一个脚本,它会在夜间运行以附加自上次脚本运行以来添加到版本库的所有修订版的转储文件数据。像这样使用时,svnadmin 的 dump
和 load
命令可以成为一种有价值的方式,用于备份版本库随时间的更改,以防发生系统崩溃或其他灾难性事件。
转储格式也可以用来将多个不同版本库的内容合并到单个版本库中。通过使用 svnadmin load 的 --parent-dir
选项,您可以为加载过程指定一个新的虚拟根目录。这意味着,如果您有三个版本库的转储文件,例如 calc-dumpfile
、cal-dumpfile
和 ss-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 版本库转储格式的一种方法,即从其他存储机制或版本控制系统完全转换。由于转储文件格式在很大程度上是人类可读的,[18] 因此使用此文件格式来描述一组通用的更改(每个更改都应被视为新的修订版)应该相对容易。实际上,cvs2svn 实用程序(请参见 名为“将版本库从 CVS 转换为 Subversion”的部分)使用转储格式来表示 CVS 版本库的内容,以便这些内容可以复制到 Subversion 版本库中。
尽管自现代计算机诞生以来,技术取得了无数进步,但有一点不幸地以水晶般清晰的方式清晰地显现出来,即有时事情会变得非常糟糕。电源中断、网络连接中断、内存损坏和硬盘崩溃只是命运即将在即使是最尽责的管理员身上释放的邪恶的滋味。因此,我们来到了一个非常重要的主题,即如何备份版本库数据。
对于 Subversion 版本库管理员来说,一般有两种类型的备份方法可用,即增量备份和完整备份。我们在本章的前面部分讨论了如何使用 svnadmin dump --incremental 执行增量备份(请参见 名为“迁移版本库”的部分)。基本上,想法是在给定时间仅备份自上次备份以来版本库的更改。
版本库的完整备份实际上是对整个版本库目录(包括 Berkeley 数据库或 FSFS 环境)的复制。现在,除非您暂时禁用对版本库的所有其他访问,否则简单地执行递归目录复制会冒生成错误备份的风险,因为有人可能正在写入数据库。
在 Berkeley DB 的情况下,Sleepycat 文档描述了可以按特定顺序复制数据库文件,以确保有效备份副本。FSFS 数据也存在类似的排序。更重要的是,您不必自己实现这些算法,因为 Subversion 开发团队已经这样做了。hot-backup.py 脚本位于 Subversion 源代码分发版中的 tools/backup/
目录中。给定版本库路径和备份位置,hot-backup.py(实际上只是一个更智能的 svnadmin hotcopy 命令包装器)将执行备份实时版本库所需的步骤,而无需您完全禁止公共版本库访问,然后将从您的实时版本库中清除无用的 Berkeley 日志文件。
即使您还有增量备份,您也可能希望定期运行此程序。例如,您可能考虑将 hot-backup.py 添加到程序调度程序中(例如,Unix 系统上的 cron)。或者,如果您更喜欢细粒度的备份解决方案,则可以让您的 post-commit 钩子脚本调用 hot-backup.py(请参见 名为“钩子脚本”的部分),这将导致在创建每个新修订版时对版本库进行新的备份。只需将以下内容添加到实时版本库目录中的 hooks/post-commit
脚本中
(cd /path/to/hook/scripts; ./hot-backup.py ${REPOS} /path/to/backups &)
生成的备份是一个功能齐全的 Subversion 版本库,可以作为实时版本库的替代品,以防发生严重错误。
两种类型的备份方法都有优点。最简单的是完整备份,它始终会导致版本库的完美工作副本。这再次意味着,如果实时版本库发生故障,您可以使用简单的递归目录复制从备份中恢复。不幸的是,如果您维护着多个版本库备份,那么这些完整副本将消耗与实时版本库一样多的磁盘空间。
如果 Subversion 本身的版本之间数据库模式发生变化,使用仓库转储格式的增量备份非常有用。由于通常需要完整的仓库转储和加载才能将您的仓库升级到新的模式,因此提前完成该过程的一半(转储部分)非常方便。不幸的是,创建和从增量备份中恢复都需要更长时间,因为每个提交实际上都会被重新播放到转储文件或仓库中。
在这两种备份情况下,仓库管理员都需要了解对未版本控制的修订属性的修改如何影响其备份。由于这些更改本身不会生成新的修订版本,因此它们不会触发提交后钩子,甚至可能不会触发 pre-revprop-change 和 post-revprop-change 钩子。[19] 并且由于您可以在不考虑时间顺序的情况下更改修订属性 - 您可以在任何时间更改任何修订属性 - 对最新几个修订版本的增量备份可能无法捕获对作为先前备份一部分的修订版本的属性修改。
一般来说,只有真正偏执的人才需要备份整个仓库,例如,每次提交都备份一次。但是,假设某个仓库具有其他具有相对精细粒度的冗余机制(例如,每个提交的电子邮件),那么仓库管理员可能会希望将数据库的热备份作为系统范围的夜间备份的一部分。对于大多数仓库来说,存档的提交电子邮件本身提供了足够的冗余作为恢复源,至少对于最近的几个提交来说是这样。但这是您的数据 - 请尽可能地保护它。
通常,仓库备份的最佳方法是多样化的。您可以利用完整的和增量备份的组合,以及提交电子邮件的存档。例如,Subversion 开发人员在创建每个新修订版本后都会备份 Subversion 源代码仓库,并保留所有提交和属性更改通知电子邮件的存档。您的解决方案可能类似,但应该根据您的需求以及便利性和偏执症之间的微妙平衡进行调整。虽然所有这些可能无法将您的硬件从命运的铁拳中拯救出来,[20] 但它肯定会帮助您从那些艰难时期中恢复过来。