参考资料(svnbook):
http://svnbook.red-bean.com/nightly/zh/svn-book.html
一. 当前仓库结构
仓库路径为/home/svn/project,仓库根目录下是dev文件夹,dev文件夹下则是一系列的android项目、ios项目、server项目以及若干文档,具体如下:
project
|
dev
|
android_a
android_b
android_c
***
ios_a
ios_b
ios_c
***
server_a
server_b
server_c
***
doc_a
doc_b
doc_c
***
other
二. 仓库迁移需求
由于原svn仓库所在服务器运行多年,并且已经出现过一次硬件事故,因此本次需要将该svn仓库迁移至新的服务器,需求如下:
1. 迁移后保留仓库操作日志,客户端工作副本能够通过svn relocate操作直接绑定至新的仓库。
2. 原仓库过于庞大,迁移后将原仓库由1个仓库拆分为4个仓库,放到4台不同的物理机上,提高冗余、提升性能、增加可维护性。
拆分时按照android项目、ios项目、server项目、其他(如doc等)进行区分,新的4个仓库结构如下:
a. android仓库:
dev_android/dev/android_*
b. ios仓库:
dev_ios/dev/ios_*
c. server仓库:
dev_server/dev/server_*
d. dev仓库:
dev/dev/doc_*等,即存放除去android项目、ios项目和server项目之后的其他资源。
三. 迁移步骤
1. 取得仓库备份
a. 关闭仓库
在获取仓库转储文件之前,关闭仓库,确保获取到的仓库转储文件是仓库的最新版本。
killall svnserve
b. 获取转储文件
以下是svn的日常备份操作,其将转储文件进行了gzip压缩。
svnadmin dump /home/svn/project | gzip > /home/svndump/project.gz
解压之:
gunzip –c /home/svndump/project.gz > /home/svndump/project.dump
c. 筛选转储文件
使用svndumpfilter的过滤功能,按照拆分需求将转储文件拆分为4个子转储文件。
svndumpfilter include --pattern "dev/android_*" < project.dump > android.dump
svndumpfilter include --pattern "dev/ios_*" < project.dump > ios.dump
svndumpfilter include --pattern "dev/server_*" < project.dump > server.dump
svndumpfilter exclude --pattern "dev/android_*" --pattern "dev/ios_*" --pattern "dev/server_*" < project.dump > dev.dump
这里需要指出的是,以上拆分方式将会导致拆分后的转储文件中存在大量的无效仓库版本号。例如android.dump转储文件中,仍然会包括ios项目相关的版本号,这些版本号在新的android仓库中显然是无效的。然而,由于拆分需求强调了在拆分后,新的仓库需要保持和旧仓库的兼容,即客户端工作副本够通过svn relocate操作直接绑定至新的仓库,而不必重新check out,因此,这是无奈的选择。
假如忽视上面的需求,则可以在拆分转储文件时丢弃掉那些无效的版本号,并对版本号进行重新规划,使其连续,令拆分后的转储文件更加干净。
#svndumpfilter include --pattern "dev/android_*" --drop-empty-revs --renumber-revs < project.dump > android.dump
#svndumpfilter include --pattern "dev/ios_*" --drop-empty-revs --renumber-revs < project.dump > ios.dump
#svndumpfilter include --pattern "dev/server_*" --drop-empty-revs --renumber-revs < project.dump > server.dump
#svndumpfilter exclude --pattern "dev/android_*" --pattern "dev/ios_*" --pattern "dev/server_*" --drop-empty-revs --renumber-revs < project.dump > dev.dump
当然,这就要求客户端在连接拆分后的新仓库时,必须执行check out操作,重新从新的仓库获取文档。因为此时如果不进行check out操作,客户端虽然同样能够成功执行svn relocate操作,但绑定新的仓库后,由于新仓库的版本号和老仓库的版本号不兼容,将导致客户端后续无法执行svn update等操作。
两种拆分方式中:
第一种拆分方式维持了与老仓库的兼容,令客户端可以方便地直接重定向至新的仓库,但会导致新仓库中存在大量无效的版本号。
第二种拆分方式则得到了一个全新的,纯净的仓库,它体积上也更小,但客户端必须重新check out仓库,导致客户端付出更多的时间成本。
2. 转移仓库备份
这里我们通过ServerDog中的backup模块,将仓库备份拆分后获得的4个新的转储文件分别同步至4台新的仓库服务器上。
3. 创建新仓库
在新的仓库服务器上,分别创建新的仓库:
svnadmin create /home/svn/dev_android
svnadmin create /home/svn/dev_ios
svnadmin create /home/svn/dev_server
svnadmin create /home/svn/dev
4. 将备份导入新仓库
这里需要注意的是,svndumpfilter在过滤路径时采取的是非常字面的理解。例如,如果你已经通过svndumpfilter的include trunk/my-project获得了一个根目录为trunk/my-project的仓库的转储文件,生成的转储文件对于将来加载它的仓库没有任何假定。特别地,转储文件可能以添加目录trunk/my-project的版本号作为开始,但它不会包含创建目录trunk的版本号(因为trunk不匹配include|exclude过滤器)。此时管理员需要确保在加载转储文件前,转储文件期望存在的目录在目标仓库中确实存在。
在我们的例子中,如果我们直接执行svnadmin load操作将拆分后获得的转储文件导入新的仓库,则导入操作最终不一定会成功。以转储文件android.dump为例,该转储文件以添加目录dev/android_?的版本号作为开始,在执行导入时,导入操作不会自动添加dev目录,因此导入操作将因为找不到dev目录而失败。
a. 在客户端建立新仓库的工作副本
即将新的仓库check out至客户端,此时新仓库中内容为空。
b. 在客户端为新仓库准备好需要的根目录
在我们的例子中,除dev.dump外,android.dump、ios.dump、server.dump都需要在导入新仓库前在新仓库上准备好基础目录,即在客户端的工作副本中为新仓库创建dev目录,然后svn add,svn commit。
c. 在新的仓库服务器上,分别将转储文件导入
svnadmin load /home/svn/dev_android < android.dump
svnadmin load /home/svn/dev_ios < ios.dump
svnadmin load /home/svn/dev_server < server.dump
svnadmin load /home/svn/dev < dev.dump
5. 修改仓库的UUID
每个svn仓库都有一个全局统一标识(universally unique identifier, 简称UUID)与之关联。当其他手段不够完善时(例如检查仓库的URL,但URL可以变化),客户端可以使用UUID识别仓库。管理员极少需要考虑仓库的UUID,对他们而言,UUID只是svn的一个实现上的细节而已,在执行svnadmin create命令创建仓库时,svn仓库会自动生成自己的UUID。然而,少数情况下这个细节也需要引起注意。
一般来说,管理员希望活动仓库的UUID是独一无二的,毕竟这就是UUID的主要特点。但在某些情况下需要两个仓库拥有一模一样的UUID,比如说管理员为仓库制作了一个副本,并且希望该副本是源仓库的完美镜像,因为管理员希望当备份仓库替换掉活动仓库时,用户不会突然看到一个似乎不同的仓库。在转储和加载仓库历史时,管理员可以根据实际情况 决定是否向目标仓库应用封装在转储流中的UUID。
有若干种方式可以用来设置或重置仓库的UUID,对于Subversion 1.5以及更高版本而言,用到的命令是svnadmin setuuid。如果在命令行上显式地提供了UUID参数, 命令将验证UUID的格式是否正确,如果正确就把它设置到仓库上。如果省略了UUID参数,命令就自动为仓库生成一个全新的UUID。
在执行svnadmin load操作时,有以下两个参数是用于设置仓库的UUID的:
--ignore-uuid
即使目标版本库是空的,也不用标准输入流中的UUID替换版本库的 UUID。
--force-uuid
即使目标版本库非空(含一次以上的提交),如果流中存在UUID,则设定为版本库的UUID。
在我们的例子中,在执行仓库迁移时,我们创建了4个新的仓库,然后将4个拆分后的转储文件分别导入,我们没有显示使用--ignore-uuid或--force-uuid参数,因此,4个新仓库的UUID将执行默认行为,情况如下:
dev_android仓库:load前手动添加了dev目录,仓库非空,load后其UUID与原svn仓库project的UUID不一致。
dev_ios仓库:load前手动添加了dev目录,仓库非空,load后其UUID与原svn仓库project的UUID不一致。
dev_server仓库:load前手动添加了dev目录,仓库非空,load后其UUID与原svn仓库project的UUID不一致。
dev仓库:load前仓库为空,load后该仓库的UUID与原svn仓库project的UUID一致。
svn relocate注意事项
当迁移后新仓库的UUID与原仓库UUID不一致时,在客户端的工作副本上执行svn relocate操作将会报错:新仓库的UUID与客户端本地工作副本的UUID不匹配。
因此,我们需要将新仓库的UUID与客户端本地工作副本的UUID调整为一致,这里就有两种方案,其一是在新仓库的服务器执行修改,将新仓库的UUID修改为与旧仓库的UUID一致,即与客户端本地工作副本的UUID一致;其二则是在客户端的本地工作副本上修改,将工作副本上的UUID修改为与新仓库一致。
方案一. 服务器端修改新仓库的UUID为旧仓库的UUID
a. 查看旧仓库的UUID
svnlook uuid /home/svn/project
结果显示uuid为:
4d53c578-272b-4626-b3ae-5a3074314a57
或者直接cat查看:
cat /home/svn/project/db/uuid
显示结果有两行,其中第一行就是uuid:
4d53c578-272b-4626-b3ae-5a3074314a57
d7b93810-f91c-420b-b967-7c4e26f3ac87
这里的第二行是仓库的实例ID,即instance-id,其格式与uuid一致。instance-id是Subversion 1.9才引入的,其目的是为了更进一步的区分不同的svn仓库。我们知道,svn仓库使用uuid作为识别标识符,理论上不同的svn仓库理应拥有不同的uuid,但是在实际使用中,有时我们会要求不同的svn仓库使用相同的uuid,例如svn relocate时,或者svn仓库和它的备份仓库,也可能会拥有同样的uuid。即使同一台物理机上的两个svn仓库,拥有相同的uuid也是可以的。因此,为了协调区分不同的svn仓库实例,引入了仓库实例ID,即instance-id,每个svn仓库,其instance-id应该保持唯一性。有关instance-id的内容,svnbook上缺少说明,网络上资料也比较少,有兴趣请参考:
https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_fs_fs/structure
http://subversion.1072662.n5.nabble.com/FSFS-instance-id-and-on-disk-representation-td198508.html
b. 设置新仓库的uuid
svnadmin setuuid /home/svn/dev_android 4d53c578-272b-4626-b3ae-5a3074314a57
svnadmin setuuid /home/svn/dev_ios 4d53c578-272b-4626-b3ae-5a3074314a57
svnadmin setuuid /home/svn/dev_server 4d53c578-272b-4626-b3ae-5a3074314a57
方案二. 客户端修改本地工作副本的UUID为新仓库的UUID
a. 查看新仓库的uuid
既可以到新仓库的服务器上查看:
svnlook uuid /home/svn/dev_android
svnlook uuid /home/svn/dev_ios
svnlook uuid /home/svn/dev_server
也可以在客户端执行svn relocate时产生的报错中直接看到新仓库的uuid。
b. 更新工作副本的uuid
这里我们通过使用sqlite-tools工具来修改svn本地工作副本中的wc.db数据库,以dev_android为例:
//打开数据库
sqlite> .open D:/workspace/svn/dev_android/.svn/wc.db
//显示数据库中的表名
sqlite> .tables
ACTUAL_NODE NODES PRISTINE WC_LOCK
EXTERNALS NODES_BASE REPOSITORY WORK_QUEUE
LOCK NODES_CURRENT WCROOT
//令查询结果显示表头
sqlite> .header on
//查询仓库表
sqlite> select * from repository;
id|root|uuid
1|svn://svn_dev_android.ad.com/home/svn/dev_android|4d53c578-272b-4626-b3ae-5a3074314a57
//更新仓库的uuid
sqlite> update repository set uuid = '780b2df8-d432-4f37-a923-fb1074d196c2' where id = 1;
//确认更新成功
sqlite> select * from repository;
id|root|uuid
1|svn://svn_dev_android.ad.com/data/svn/gzy_dev_android|780b2df8-d432-4f37-a923-fb1074d196c2
6. 令客户端绑定新的仓库
在客户端的本地工作副本上执行svn relocate操作,将旧仓库的url替换为新仓库的url即可。