Atlassian的Subversion复制过程
Filed Under (Subversion Server) by rocksun on 29-11-2008
Tagged Under : subversion, svnsync
转载请注明本文地址:http://www.subversion.org.cn/submerged/?p=71
英文地址:http://blogs.atlassian.com/developer/2008/11/subversion_replication_at_atla.html
能够按照开放哲学在一个国际化公司工作非常酷,但是我们的分布式设置经常会导致一些系统管理的麻烦。其中一个是让我们三个洲的员工能快速的访问源代码版本库,并工作在同一个代码基。
Subversion是现存的版本控制系统,主要因为其支持的工具和尽人皆知的工作流程。但是它也有自己的问题,毕竟当有延迟时,网络的本性就会导致问题。当你的开发者在悉尼,而你的服务器在圣路易斯,就会有internet上的网络延迟。
Subversion 1.5引入了通过代理写的概念,但是细节是魔鬼。如何完成的文档非常少,开发一个健壮的复制方法完全留给了“读者的练习”。本文记录了一些这方面的考虑因素和我们在Atlassian使用的方法,以实现分布式环境下的可靠高速的Subversion服务器。
基本的复制结构非常直接:从属服务器通过本地缓存获得检出和元数据,而将检入透明的代理到主服务器,检入如何复制到从属服务器的概念也相当简单:
- 用户从从属服务器检出一个工作拷贝,并做出变更
- 用户发起‘svn commit’,将变更提交到从属服务器
- 从属服务器透明的将提交发送到主服务器
- 主服务器完成提交,并处罚它的post-commit钩子
- post-commit钩子将代码发送到所有已知的从属服务器
然而细节是魔鬼。发送到从属服务器的操作没有被详细说明;很不幸的是readme文件所说的方法是高度同步的,就像我们所说的,使用svnsync的可选方法是“留给读者的练习”。我们需要保持从属服务器不过期,并使用最少的提交时间。
问题
记录的SSH+转储/恢复方法的问题是需要在上传和导入增量转储的整个时间里,完全占用提交客户端和服务器,但是如果从属服务器因为某种原因挂起了,直到TCP的会话过期才能结束。此外,从属服务器就会与主版本库不同步,后面的提交会失败。我们需要的方法是让从属服务器异步更新,并补充遗失的提交。
解决方案
通过svnsync,允许subversion按照事物的方式镜像,而且只会下载没有同步的部分。它会在镜像版本库执行自己的锁定,所以冲突不会出现。
然而,在执行更新时还是会出现问题,我们可以通过cron脚本轮询版本库,但是这样会造成从属服务器有一段时间不能同步,除非同步一直在运行,这样非常浪费。然而,一个完全事件驱动的系统也会遭受前面所说的转储/恢复系统的问题;如果更新错过了从属服务器,那么下次更新之前数据就会不一致,此外,如果事件以同步方式实现,post-commit脚本会一直在执行。
最后,我选择了一种混合的方案,每个从属服务器运行一个服务器来接受单个UDP包,来触发更新(让post-commit脚本来执行触发并遗忘),并间歇的更新来补充错过的事件。
设置镜像
第一步是初始化镜像,这需要设置新的版本库并从主服务器初始化,并确保版本库只可以被特殊的svnsync用户写:
sudo su - svnsync svnadmin create /opt/svn/repositories/atlassian/private-mirror
在同步之前,还要让修订版本修属性可以修改。再次,只有特殊的用户可以执行这个操作,创建文件/opt/svn/repositories/atlassian.com/private-mirror/hooks/pre-revprop-change
,并添加这些内容:
#!/bin/sh USER="$3" if [ "$USER" = "svnsync" ]; then # Allow exit 0; fi echo "Only the svnsync user can change revprops" >&2 exit 1
然后将版本库转化为可同步的,通过设置远程源,然后执行初始化的同步:
svnsync init file:///opt/svn/repositories/atlassian.com/private-mirror http://svn.atlassian.com/svn/private svnsync sync file:///opt/svn/repositories/atlassian.com/private-mirror
这样就会将主服务器上的所有历史同步到从属服务器,这样取决于版本库的大小,会花费一定的时间,一旦完成,下面的命令就可以同步最新的主服务器修订版本:
svnsync sync file:///opt/svn/repositories/atlassian.com/private-mirror
这个设置你可能遇到的一个问题是因为你从头创建的版本库,有和主服务器不同的UUID,这对于检出没有问题,但是提交会有问题,但是你可以手工的复制主服务器的UUID:
cd /opt/svn/repositories/atlassian.com/private-mirror/db/ scp svn.atlassian.com:/opt/svn/repositories/atlassian.com/private-mirror/db/uuid .
现在你一定有了工作镜像,可以通过SVN 1.5的Apache代理工作(这个例子忽略了认证信息):
<Location /svn/private> DAV svn SVNPath /opt/svn/repositories/atlassian.com/private-mirror SVNMasterURI http://svn.atlassian.com/svn/private </Location>
下一步是保持镜像保持最新…
更新事件服务器
所以我们需要一个服务器接收UDP包,触发监控子进程,并触发基于时间的事件。我们可以使用inetd和cron之类的工具,但是我喜欢让所有的变量在一个地方,所以我实现了我自己的服务器,在一个地方处理所有的任务。当然,重新发明轮子很可恶,所以我修改了Python的Twisted框架,实现了所有必要的细节 …
import sys, re from twisted.internet.protocol import DatagramProtocol, ProcessProtocol from twisted.internet import reactor, task cmdline = ['svnsync', 'sync', 'file:///opt/svn/repositories/atlassian.com/private-mirror'] lockmsg = "Failed to get lock" _debug = False def debug(msg): if _debug: print >> sys.stderr, msg def error(msg): print >> sys.stderr, msg def log(msg): print >> sys.stdout, msg class SyncProcess(ProcessProtocol): def __init__(self): self.running = False def connectionMade(self): self.running = True log("SVN sync process started") def outReceived(self, data): log("stdout> %s" % data) if data.find(lockmsg) > -1: error("ERROR: The mirror repo has a lock on it") def errReceived(self, data): log("stderr> %s" % data) def inConnectionLost(self): debug("inConnectionLost! stdin is closed! (we probably did it)") def outConnectionLost(self): debug("outConnectionLost! The child closed their stdout!") def errConnectionLost(self): debug("errConnectionLost! The child closed their stderr.") def processEnded(self, status): self.running = False log("Sync process ended, status %d" % status.value.exitCode) class SyncListener (DatagramProtocol): def __init__(self): self.prochandler = SyncProcess() self.timeout = task.LoopingCall(self.runsync) def startProtocol(self): print "Starting UDP server and timeout" self.timeout.start(120, now=False) def datagramReceived(self, data, (host, port)): log("Received packet from %s:%d" % (host, port)) self.runsync() def runsync(self): if self.prochandler.running: log("Not running sync as another process is present") else: reactor.spawnProcess(self.prochandler, cmdline[0], cmdline, {}) reactor.listenUDP(9999, SyncListener()) reactor.run()
这个服务器在从属服务器上一直运行,并监听9999端口,当接收到一个包,它会触发svnsync进程(除非已经运行),此外,每过两分钟都会执行sync。服务器使用daemontools启动,可以保证服务器退出时,自动重启。
触发更新
当主服务器接受提交时,就会通过发送UDP包触发每个从属服务器上的更新。这时通过post-commit钩子的netcat网络工具实现的:
echo 1 | nc -w1 -u svn.sydney.atlassian.com 9999
就这么多了,下面是一些警告…
锁定
锁定如何与复制品交互并不清楚;然而分布式的锁定不应该轻视。因此,我关闭所有主服务器和从属服务器版本库的锁定,只需要将下面的内容加入到pre-lock钩子:
#!/bin/sh # Disable locking as we are doing replication and it's not clear how # they will interact. echo "Locking is disabled due to replication" >&2 exit -1
这样会返回有意义的错误信息,如果某人希望锁定的话。
客户端版本问题
当在复制的从属服务器上添加文件时,在一些版本的Subversion客户端有一些已知的问题,下面是我们测试的列表:
Client | Version | Working |
---|---|---|
Subversion commandline | 1.4.* | Yes |
Subversion commandline | 1.5.0 | Yes |
Subversion commandline | 1.5.\[1-4\] | No |
TortoiseSVN | 1.4.* | Yes |
TortoiseSVN | 1.5.* | No |
IDEA | 7.0.* | Yes |
IDEA | 8.0M1 | Yes |
分布式版本控制工具
一个明显的事实是上面所说的都不是必要的。现在有很多版本控制系统,商业和开源的,有一些天生就支持分布式,并不需要特别的处理。在这些系统中,提交分为两个阶段,包含本地的检入(可选),然后是合并到远程版本库(根据你的开发模型或者是一个拖)。这无疑是未来的方式,在Atlassian也有一些试用的讨论。然而,有一些短期问题阻止我们立刻迁徙:
- 工具支持。Fisheye,crucible,Maven,IDEA;除非我们工具链的这些部分都被下一代系统支持,否则我们的工作流程需要大量修改。
- 开发者过程:因为本地提交不能自动传递到主版本库,所以开发者需要更多纪律。在实践中,需要创建一个合并管理员的角色,来保证所有的工作树可以有规律的合并,并解决冲突。
所有这些问题都不是不可逾越的,我期望随着时间分布式把版本控制可以成为标准,而不是现在的小众地位。