资料库维护

维护一个Subversion资料库会是一个让人畏缩的任务,大部分是由于用数据库做后端的系统固有的复杂性。要做好这个任务全在在于了解 工具——它们是什么,什么时候用它们,以及如何使用它们。这一节将为你介绍Subversion提供的资料库管理工具,以及如何操纵他们 来完成诸如资料库迁移,升级,备份和清理等任务。

管理员的工具箱

Subversion提供了一系列工具,可以用来创建,检查,修改和修理你的资料库。让我们仔细看一下每个工具。然后,我们将简要介绍包括 在Berkeley DB 分发版里的一些工具,他们提供了特定于你资料库的数据库后端的功能,这些功能Subversion自己的工具没有提供。

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.
…

几乎svnlook的每个子命令都可以在一个修订版或者事务树上操作,打印这些树本身的信息,或者它们和资料库中 前一个修订版的差别。你可分别用--revision--transaction选项来指定检查某个修订版或事务。 注意修订版号码作为自然数出现,而事务名是由字母和数字组成的字符串。切记文件系统只允许浏览未提交的事务(还没有成为一个 新的修订版的事务)。大部分资料库不会有这样的事务,因为事务通常要么提交了(这使他们不能被察看)要么取消并删除了。

--revision--transaction选项都缺席的情况下,svnlook将检查资料库中 最年轻的(或“HEAD”)修订版。因此当19是位于/path/to/repos的资料库中的最新的修订版时,下面的两个命令完全相同:

$ 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子命令的输出定义为:

  1. 作者,跟着一个换行符。

  2. 日期,跟着一个换行符。

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

  4. 日志消息本身,跟着一个换行符。

输出是人可读的,意味着如日期戳等条目是用文本表示来显示的,而不是更加晦涩的形式(比如从the Tasty Freeze guy drove by 开始的纳秒数)。但是这输出也是机器可解析的——因为日志消息会包含很多行并且没有长度限制,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 catsvnlook propgetsvnlook proplist这样的命令来钻研文件和目录的细节。

svnlook可以执行其它各种查询:显示我们前面提到的信息的子集,报告在给定的修订版和事务里哪个路径被修改了, 显示目录和文件的文本和属性的差别,等等。下面是目前svnlook可接受的子命令的列表和这些子命令的输出:

author

打印树的作者。

cat

打印树中文件的内容。

changed

列出树中所有修改了的文件和目录。

date

打印出树的日期戳。

diff

打印出修改了的文件的标准化diff。

dirs-changed

列出树中那些本身被修改了,或它的文件被修改了的目录。

history

显示在一个版本化路径历史中感兴趣的点(修改或复制发生的地方)。

info

打印树的作者,日期戳,日志消息字符数,已经日志消息。

log

打印树的日志消息。

propget

打印树中某个路径上的一个属性值。

proplist

打印树中路径上设置的属性名和值。

tree

打印树列表,可以选择显示关联到每个路径的文件系统节点修订版ID。

uuid

打印资料库的UUID——全局(Universal)唯一(Unique)标识符(IDentifier)。

youngest

打印最新的修订版号码。

svnadmin

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)
…

我们已经提到svnadmincreate子命令。大部分其它子命令我们将在本章稍后更详细的介绍。 现在,让我们快速浏览一下可用的子命令:

create

创建一个新的Subversion资料库。

deltify

在一个指定的修订版范围内运行,执行在这些资料库中修改了的路径上的向前增量化。如果没有指定修订版,这命令 将简单的增量化HEAD修订版。

dump

转储资料库的内容,以给定的修订版集为界,使用可移植的转储格式。

hotcopy

对资料库做一个热复制。你可以在任何时候运行这个命令,对资料库做安全的复制,不管是否有其它进程在使用资料库。

list-dblogs

(只用于Berkeley DB 资料库。)列出关联于这个资料库的 Berkeley DB 日志文件路径。 这个列表包括所有的日志文件——包括仍在被Subversion使用的和不再被使用的。

list-unused-dblogs

(只用于Berkeley DB 资料库。)列出关联于这个资料库但不再被使用的 Berkeley DB 日志文件路径。 你可以从资料库布局里安全的删除这些日志文件,可能把它们存档以备你一旦要对资料库实行灾难恢复之需。

load

从一个数据流把一组修订版加载到资料库中,数据流要使用和dump子命令生成相同的可移植转储格式。

lstxns

列出目前存在于资料库中的未提交的Subversion事务名。

recover

在资料库需要的时候执行恢复步骤,通常在致命错误发生而导致进程和资料库的通讯无法被干净的关掉时。

rmtxns

从资料库中彻底的删除Subversion事务(通常用lstxns子命令的输出作输入)。

setlog

用新值替换资料库中给定的修订版的svn:log 属性的当前值。

verify

验证资料库的内容。除了别的,这还包括对存储在资料库中的版本化数据的校验和的比较。

svndumpfilter

既然Subversion把所有东西存在一个不透明的数据库系统中,那么试图手工修改它即使不是很难的,也是不明智的,。 并且一旦数据已经存储在你的资料库中,Subversion通常没有提供删除数据的简单办法。 [12] 但是不可避免的,有些时候你要操作你的资料库的历史。你可能需要除掉所有那些被意外加到你的资料库中的文件的实例(并且 决不应该在哪儿)。或者,可能你有多个项目共享一个资料库,而你决定把它们分开到各自的资料库中。要完成这样的任务, 管理员需要更加易于管理和易于改造的对资料库中数据的表示——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

只允许指定的一组路径通过到转储数据流。

让我们看一个如何使用这个程序的实际的例子。我们在别处(参见“选择资料库布局”一节)讨论如何选择你资料库中数据布局的过程——每个项目一个资料库还是联合它们,在你的 资料库中安排资料,等等。但是有时在新的修订版开始加入后,你从新考虑你的布局并可能想做些修改。常见的修改是决定把共享同一个 资料库的多个项目分割成每个项目一个资料库。

我们设想的资料库包含三个项目:calccalendar,和 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。如果你想让你的trunktags,和 branches目录居于资料库的根目录,你要编辑转储文件,调整Node-pathCopyfrom-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-pathCopyfrom-path头信息。

…
Node-path: spreadsheet/Makefile
…

如果路径有前导正斜杠,你应该在你传递给svndumpfilter includesvndumpfilter exclude的路径里包含前导正斜杠(如果没有,就不要)。而且,如果你的转储文件由于某些 原因在使用前导正斜杠上有不一致, [13] 你应该适当的统一这些路径,使它们都有或都没有前导正斜杠。

并且,复制的路径可能会给你带来麻烦。Subversion支持资料库中的复制操作,新路径可以通过复制某个已经存在的路径创建。可能 在你的资料库生存期的某个时候,你从某个svndumpfilter排除的地方复制了一个文件或目录到被它包括的地方。 为了使转储数据是自足的,svndumpfilter需要仍然显示新路径的添加——包含任何通过复制创建的内容——并且 不把这个添加表示为从一个不存在于你过滤后的转储数据流中的源的复制。但是因为Subversion资料库转储数据只显示每个修订版的修改, 所以复制来源的内容可能不是可用的。如果你怀疑你的资料库中有类似这样的复制,你也许该重新考虑你的保留/排除路径集合。

svnshell.py

Subversion源代码树也为资料库提供了一个shell式的界面。Python脚本svnshell.py(位于源代码树的 tools/examples/目录)使用Subversion语言绑定(因此为了让脚本能工作你必须正确的编译和安装了它们)来 连接资料库和文件系统库。

一旦被启动,该程序就像一个shell程序一样运行,允许你浏览你资料库中的各个目录。最初,你被“定位于HEAD修订版的根目录,并呈现一个命令行提示符。在任何时候,你都可以使用help 命令来显示可用的命令列表及它们都做什么用。

$ svnshell.py /path/to/repos
<rev: 2 />$  help
Available commands:
  cat FILE     : dump the contents of FILE
  cd DIR       : change the current working directory to DIR
  exit         : exit the shell
  ls [PATH]    : list the contents of the current directory
  lstxns       : list the transactions available for browsing
  setrev REV   : set the current revision to browse
  settxn TXN   : set the current transaction to browse
  youngest     : list the youngest browsable revision number
<rev: 2 />$

在你的资料库的目录结构中导航和你在通常的Unix或windows系统中导航的方法是相同的——用cd命令。 在任何时候,命令提示符将显示你目前在检查哪个修订版(以rev:为前缀)或事务(以txn:为前缀)。你可以分别用setrevsettxn命令来改变你当前的修订版或事务。如同在Unix shell中,你可以用ls命令 来显示当前目录的内容,而且你可以用cat命令显示文件内容。

例 5.1. 使用 svnshell 来航行于资料库中

<rev: 2 />$ ls
   REV   AUTHOR  NODE-REV-ID     SIZE         DATE NAME
----------------------------------------------------------------------------
     1    sally <     2.0.1>          Nov 15 11:50 A/
     2    harry <     1.0.2>       56 Nov 19 08:19 iota
<rev: 2 />$ cd A
<rev: 2 /A>$ ls
   REV   AUTHOR  NODE-REV-ID     SIZE         DATE NAME
----------------------------------------------------------------------------
     1    sally <     4.0.1>          Nov 15 11:50 B/
     1    sally <     a.0.1>          Nov 15 11:50 C/
     1    sally <     b.0.1>          Nov 15 11:50 D/
     1    sally <     3.0.1>       23 Nov 15 11:50 mu
<rev: 2 /A>$ cd D/G 
<rev: 2 /A/D/G>$ ls
   REV   AUTHOR  NODE-REV-ID     SIZE         DATE NAME
----------------------------------------------------------------------------
     1    sally <     e.0.1>       23 Nov 15 11:50 pi
     1    sally <     f.0.1>       24 Nov 15 11:50 rho
     1    sally <     g.0.1>       24 Nov 15 11:50 tau
<rev: 2 /A>$ cd ../..
<rev: 2 />$ cat iota
This is the file 'iota'.
Added this text in revision 2.

<rev: 2 />$ setrev 1; cat iota
This is the file 'iota'.

<rev: 1 />$ exit
$

如你在前面的例子里看到的,多个命令可以在一个命令提示符下指定,只要用分号隔开。并且,shell可理解绝对和相对路径形式, 并会适当的处理...这样的特殊路径。

youngest命令显示最新的修订版。这在确定你能在setrev命令参数中使用那些有效的修订版很 有用——你被允许浏览所有的在0和最新的(youngest)之间并包括它们在内的修订版(回忆一下,它们都被用整数命名)。 确定有效的能浏览的事务没那么方便。用lstxns命令来列出你能浏览的事务。能浏览的事务列表和 svnadmin lstxns所返回的列表是相同的,和对svnlook--transaction 选项有效的列表也是相同的。

当你用完了shell,你可以使用exit命令干净的退出。另外,你也可以用文件结束符——Control-D退出( 然而一些Win32 Python分发版用Windows 约定的Control-Z代替)。

Berkeley DB 工具

如果你在使用一个Berkeley DB资料库,那么所有你的版本化文件系统结构和数据位于你资料库db 子目录内的一系列的数据表中。这个子目录是一个常规的Berkeley DB 环境目录,所以可以与任何Berkeley 数据库工具协同使用( 你可以在SleepyCat的网站http://www.sleepycat.com/上看到这些工具的文档)。

对于Subversion的日常使用,这些工具是不必要的。绝大部分Subversion资料库通常需要的功能都在svnadmin工具中 重复了。例如,svnadmin list-unused-dblogssvnadmin list-dblogs实现了Berkeley 提供的db_archive命令的一个子集的功能, svnadmin recover反映了db_recover工具通常的用例。

也有一些Berkeley DB工具你可能会觉得有用。db_dumpdb_load程序分别写和读一个自定义的文件格式,它描述了Berkeley DB数据中的键和值。 既然Berkeley 数据库不能在不同的机器架构间移植,那么这个格式是从一台机器到另一台机器转移数据库的有用的方式,无论机器的 架构和操作系统是什么。还有,db_stat工具能提供给你Berkeley DB 环境状态的有用的信息,包括对 锁和存储子系统的详细的统计。

资料库清理

你的Subversion资料库一旦按你喜欢的配置好了,基本上不需要太多关心。然而,有时候来自管理员的一些手工辅助也是需要的。 svnadmin工具提供了一些有用的功能来帮助你完成这些任务,如:

  • 修改提交的日志消息,

  • 删除死事务,

  • 恢复“楔住的”的资料库,以及

  • 把资料库的内容移植到另一个不同的资料库

可能最常用的svnadmin子命令是setlog。当一个事务被提交到资料库并提升为一个修订版, 关联到这个新修订版的描述性日志消息(被用户提供)被作为一个附加于修订版本身的非版本化的属性来存储。换句话说, 资料库只记住属性的最新值,并抛弃前面的值。

有时用户会在她的日志消息中包含错误(可能是一个拼写错误或某些错信息)。如果资料库被配置 (使用 pre-revprop-changepost-revprop-change 钩子;参见 “钩子脚本”一节)为在提交完成后接受对这个日志消息的修改, 那么用户可以使用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命令来绕过这些保护。

警告

记住,尽管可以绕过这些钩子,但是你可能也绕过了像对属性修改的邮件通知,备份系统来跟踪非版本化数据修改等等操作。 换句话说,对你在修改的东西和如何修改要加倍小心。

svnadmin的另一个常见用途是来查询资料寻找未完成的——可能是死的——Subversion事务。如果一次提交失败了, 事务通常被清除了。就是说,事务本身被从资料库里删除了,任何和这个事务关联(并只和它关联)的数据也被删除了。但是偶尔失败 出现的方式导致事务的清理没有发生。这可能由于下面几种原因发生:可能客户端操作被用户非正常终止了,后者在操作中发生了网络 故障等等。不管原因是什么,那些死事务只会扰乱资料库并消耗资源。

你可以用svnadminlstxns命令列出当前未完成的事务名字。

$ svnadmin lstxns myrepos
19
3a1
a45
$

得到的输出中的每个条目都可以被用于svnlook(以及它的--transaction选项)来确定谁创建了 这些事务,什么时候创建的,在事务中作了那种修改——换句话说,这个事务是否可以被安全的删除!如果是,事务的名字被传递给 svnadmin rmtxns,它将执行对事务的清除。事实上,rmtxns子命令能直接把 lstxns的输出作为输入。

$ svnadmin rmtxns myrepos `svnadmin lstxns myrepos`
$

如果你像这样使用这两个子命令,你应该考虑让你的资料库暂时停止接受客户端访问。这样做的话,没有人能在你开始你的清理前开始 一个合法的事务。以下是一个很小的shell脚本,它能很快的生成你资料库中每个未完成的事务的信息:

例 5.2. txn-info.sh (报告未完成事务)

#!/bin/sh

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

SVNADMIN=/usr/local/bin/svnadmin
SVNLOOK=/usr/local/bin/svnlook

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
$

通常,如果你看到一个没有日志消息附加于其上的死事务,那么这是一个失败的更新(或类似更新)操作的结果。这些操作在底层 使用Subversion事务来模拟工作副本状态。既然它们从没打算被提交,那么Subversion不需要这些事务有日志消息。没有日志消息的 事务几乎一定是这种失败的提交。并且,事务的日期戳可以提供有趣的信息——例如,一个操作怎么可能九个月前就开始了到现在还是 活动的?

简而言之,事务清理决定不应该被轻率的作出。各种不同的信息来源——包括Apache的错误和访问日志,成功的Subversion提交日志等等 ——都可以在决策过程中使用。最后,管理员常可以简单的通过和那些像是死事务的拥有者沟通(例如,通过email)来验证这个事务 事实上是在僵尸状态。

管理磁盘空间

虽然在过去几年中,存储的成本已经令人难以置信的降低了,但是磁盘使用对于设法版本化大量数据的管理员来说仍然是需要考虑的问题。 使用中的资料库的每一个增加的字节都需要被备份在站外,可能作为循环备份计划的一部分备份多次。如果是 Berkeley DB 资料库,那么主要的存储机制是一个复杂的数据库系统, 了解哪部分数据需要留在活动站点,哪些需要备份,以及哪些可以安全的删除是有用的。这一节专门谈 Berkeley DB;FSFS资料库没有额外的 数据需要清理或回收。

直到最近,对于Subversion资料库来说最大的磁盘空间使用者是日志文件,Berkeley DB使用这些文件来在修改真正的数据库文件前 执行预写入。这些文件记录在数据库从一个状态转变到另一个的过程里的每一步行动——数据库文件反映了在给定时间的某些状态, 而日志文件包含了在状态转换之路上的全部的大量的修改。像这样,它们会非常快的累积起来。

幸运的是,从Berkeley DB的4.2发行版开始,数据库环境已经能删除它自己的没用的日志文件而不需要任何外部过程干预。 任何使用svnadmin创建的资料库,如果这个命令是基于Berkeley DB 4.2或更高版本编译的话,将被配置为自动 删除日志文件。如果你不想让这个特性激活,只要传递--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在资料库本身里使用增量化deltification (或,“(增量存储)deltified storage”)。增量化包括把数据块表示编码为对于其他数据块的差异的集合。 如果两块数据非常相似,增量化使得增量的数据块可节省存储空间——不需要占用和原始数据相同大小的空间,它只需要足够的空间 能说明:“我和这儿的另一块数据一样大,除了下面这些修改 。”特别是,每当文件的新版本被提交到资料库时,Subversion 把先前的版本(实际上,前面的几个版本)编码为对于新版本的增量。结果是绝大部分可能很大的资料库数据——也就是,版本化文件的内容 ——被用远小于这些数据原始的“(全文)fulltext”表示的大小来存储。

注意

因为所有的被增量化的Subversion资料库数据都存储在一个单独的Berkeley DB数据库文件里,减小存储内容的尺寸不一定会减小数据库文件 本身的尺寸。然而,Berkeley DB会保留对于数据库文件中未使用的区域的内部记录,并在增加数据库文件的大小前先使用这些区域。 因此,虽然增量化不能立即减小空间,它能有力的减缓数据库将来的增大。

资料库恢复

“Berkeley DB”一节曾提到,如果没有正确关闭,Berkeley DB有时会被置于冻结状态。当这种情况发生时, 管理员需要把数据恢复到一个一致的状态。

为了保护你资料库中的数据,Berkeley DB使用一种锁机制。这种机制确保数据库各部分不会同时被多个数据库访问者修改,并且在数据 被从数据库读出时每个进程看到的数据都处于正确的状态。当一个进程需要在数据库中修改什么时,它首先要检查目标数据上是否存在锁。 如果数据没有被锁定,这个进程锁定这个数据,做它想做的修改,然后解锁这个数据。其他进程在它们被许可继续访问数据库的那部分前 被强迫等待,直到锁被去除。

在使用Subversion资料库的过程中,致命错误(比如用光了磁盘空间或可用的内存)或中断会阻止进程得到删除它在数据库中放置的锁的 机会。结果是后端的数据库系统被“楔住了”。当这种情况发生时,任何访问资料库的尝试被无限期的挂起了(既然每个新的 访问者都在等锁被去掉——可是这不会发生)。

首先,如果你的资料库发生了这种情况,不要着急。Berkeley DB文件系统利用数据事务和检查点和预写入日志来确保只有最灾难性的事件 [14] 才会破坏数据库系统,一个非常多疑的资料库管理员会用某种方式来做资料库的外部备份,但先不要让你的系统管理员来用备份磁带恢复。

接下来,使用下面的处方来试着“解开”你的资料库:

  1. 确认没有进程在访问(或企图访问)资料库。对网络上的资料库,这也意味着要关掉Apache HTTP服务器。

  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. 重起Subversion服务器。

这个过程能修正几乎所有资料库锁起的情况。确认你作为拥有和管理数据库的用户来运行这个命令,而不仅仅是root。 这个恢复过程的一部分可能涉及重新从头创建各种数据库文件(例如,共享内存区域)。作为root来恢复将使得这些 文件被root所有,这意味着甚至在你恢复了资料库的连接后,常规用户仍将不能访问它。

如果以上过程由于某些原因没有成功的解开你的资料库,那么你应该做两件事。首先,删掉你坏掉的资料库并回复到你对它最新的备份。 然后,发一封email给Subversion用户列表()详细描述你的问题。数据完整性对于 Subversion开发人员来说有极高的优先级。

移植资料库

Subversion文件系统的数据散布在多个不同的数据表中,这种方式通常只有Subversion开发人员自己能理解(也只有他们关心)。 然而,可能有些情况需要数据的全部或某些子集被收集到一个单独的,可移动的,平常的文件格式中。Subversion提供了这样的机制, 用一对svnadmin子令来实现:dumpload

最常见的转储和加载一个Subversion资料库的原因是由于Subversion本身的修改。随着Subversion的成熟,可能有时对后端数据库模式的修改 导致Subversion和先前的资料库版本不兼容。另外要转储和加载的原因可能是把一个Berkeley DB资料库移植到一个新的OS或CPU架构上,或者 是在Berkeley DB和FSFS 后端之间切换。推荐的执行过程相对简单:

  1. 用你的svnadmin当前版本,转储你的资料库到转储文件。

  2. 升级到新版的Subversion

  3. 把你旧的资料库移开,并用你svnadmin命令在它们原来的地方创建新的空资料库。

  4. 再用你的svnadmin命令,加载你的转储文件到它们对应的,刚刚创建的资料库里。

  5. 确认把你旧资料库中所有定制内容复制到你的新资料库里,包括DB_CONFIG文件和钩子脚本。 你要注意新Subversion发行版的发行说明来查看你上次升级以来是否有任何修改会影响钩子和配置选项。

  6. 如果移植过程使你的资料库要通过不同的URL访问(例如,移动到不同的计算机,或通过不同的模式被访问),那么你可能会要告诉你的用户们 在他们现有的工作副本上运行svn switch --relocate。参见???。

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) >>>

注意因为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不会将第一个被转储的修订版仅仅表示为对于前一个修订版的差异。首先,在转储文件中没有前一个修订版! 第二,Subverson没法知道转储数据将被载入(如果这真的会发生的话)的那个资料库的状态。为了确保每次执行 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钩子,它只的把触发这个钩子的那一个修订版追加到资料库转储文件上。或者你可以有一个脚本, 它每晚运行来把上次脚本运行以来添加到资料库的所有修订版追加到转储文件。像这样来使用,svnadmindumpload命令能成为一个有价值的手段,用它可以随时备份资料库修改以防系统崩溃或 某些灾难性事件。

转储格式也能被用于把几个不同资料库的内容合并成一个资料库。通过使用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资料库转储格式的方式——从不同的存储机制或版本控制系统互相转换。因为转储文件格式的绝大部分是人可读的, [15] 用这种文件格式来描述普通的变更集应该比较容易——每个修改都应该被看作一个新的修订版。事实上,cvs2svn.py 工具(参见???)使用转储格式来表示CVS资料库的内容以使这些内容能被转移进SUbversion资料库。

资料库备份

尽管从现代计算机诞生以来有大量的技术进步,但是不幸的是,有个如水晶般透明的事实——有时,事情会变得非常非常糟糕。 能源耗尽,网络连接断掉,坏掉的RAM和崩溃的硬盘不过是灾祸的一角,即使最尽责的管理员也要面对这些命运安排的灾祸。 因此,我们到了一个非常重要的主题——如何做你资料库数据的备份拷贝。

对Subversion资料库管理员通常有两种备份的办法可用——增量式的和完全的。我们在本章的前面小节讨论了如何用 svnadmin dump --incremental来执行增量式的备份(参见“移植资料库”一节)。本质上讲,这种办法是在给定时间只备份上次你做备份以来资料库的修改。

资料库的完全备份是完全逐字复制整个资料库目录(它包括Berkely数据或FSFS环境)。现在,如果你不暂时禁止对你资料库的所有访问, 而只是对目录递归的复制,那么有生成错误备份的危险,因为某人可能正在写入数据库。

在Berkeley DB的情况下,Sleepycat文档描述了数据库文件可以被复制的一定的顺序,它能保证产生有效的备份拷贝。FSFS数据也有类似的 顺序。还好的是,你不必自己实现这些算法,因为Subversion开发团队已经实现了。 hot-backup.py 可以在 Subversion分发版的tools/backup/目录找到。给定一个资料库路径和一个备份位置,hot-backup.py——它实际只是svnadmin hotcopy命令的一个更智能化的封装——会执行必要的步骤来备份 你的活动资料库——完全不需要你禁止资料库公开访问权——然后将从你的活动资料库里清理掉无用的Berkely日志文件。

即使你已经有了增量化备份,你也可能会想按一定规律的运行这个程序。例如,你可能考虑把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自身版本间数据库模式改变了,那么有使用资料库转储格式的增量化备份在手头是最好了。既然把你的资料库 升级到新的模式需要完整的资料库转储和加载,那么已经完成了一半的过程(转储过程)是非常便利的。不幸的是,创建——和恢复—— 增量化备份花费时间较长,因为每次提交都要实际重做进转储文件或资料库。

在各个备份场景里,资料库管理员都需要注意对非版本化修订版属性的修改会怎样影响它们的备份。既然这些修改本身不产生新的修订版, 那么他们将不会触发post-commit钩子,而且可能甚至不会触发pre-revprop-change 和post-revprop-change钩子。 [16] 并且既然你可以不按时间顺序来修改修订版属性——你可以在任何时候修改任何修订版属性——那么对最后几个修订版的增量化备份可能 没有捕捉到在先前的备份里包含了的修订版上的属性的修改。

一般来讲,只有真正的妄想狂才需要在每次提交发生时备份他们的整个资料库。然而,假设给定的资料库有某些相对细粒度的(比如per-commit email) 冗余机制可用,资料库管理者可能会想把对数据的热备份作为系统范围每晚备份的一部分。对于大部分资料库,单是存档commit email就 提供了作为恢复源的足够的冗余性,至少对于最近的几次提交。但这是你的数据——你想怎样保护它都行。

通常,资料库备份的最好的办法多种方法共用。你可以利用完全的和增量的备份的结合,加上commit email的存档。例如,Subversion开发人员 在每个新修订版创建后备份Subversion源代码,并且保存对所有提交和属性修改的通知email的存档。你的解决方案可能相似, 但是应该迎合你的需要并在便利和保险之间巧妙的平衡。虽然所有这些都可能无法在命运的铁拳 [17]下拯救你的硬件, 但是它一定会帮助你从这些艰难的日子恢复过来。



[12] 顺便说一下,这是一个特性,而不是一个bug。

[13] 虽然svnadmin dump有一致的前导正斜杠方针——不包含它们——其它生成转储数据的程序不一定这么一致。

[14] 比如:硬盘+巨大的磁石=灾难

[15] Subversion资料库转储格式类似RFC-822格式,那是绝大部分email使用的同一种格式。

[16] svnadmin setlog可以用完全绕过钩子接口的方式调用。

[17] 你知道——所有你的“无常的手指”的集合名词。