Atlassian的Subversion复制过程

Filed Under (Subversion Server) by rocksun on 29-11-2008

Tagged Under : ,

英文地址:http://blogs.atlassian.com/developer/2008/11/subversion_replication_at_atla.html

能够按照开放哲学在一个国际化公司工作非常酷,但是我们的分布式设置经常会导致一些系统管理的麻烦。其中一个是让我们三个洲的员工能快速的访问源代码版本库,并工作在同一个代码基。

Subversion是现存的版本控制系统,主要因为其支持的工具和尽人皆知的工作流程。但是它也有自己的问题,毕竟当有延迟时,网络的本性就会导致问题。当你的开发者在悉尼,而你的服务器在圣路易斯,就会有internet上的网络延迟。

Subversion 1.5引入了通过代理写的概念,但是细节是魔鬼。如何完成的文档非常少,开发一个健壮的复制方法完全留给了“读者的练习”。本文记录了一些这方面的考虑因素和我们在Atlassian使用的方法,以实现分布式环境下的可靠高速的Subversion服务器。

基本的复制结构非常直接:从属服务器通过本地缓存获得检出和元数据,而将检入透明的代理到主服务器,检入如何复制到从属服务器的概念也相当简单:

  1. 用户从从属服务器检出一个工作拷贝,并做出变更
  2. 用户发起‘svn commit’,将变更提交到从属服务器
  3. 从属服务器透明的将提交发送到主服务器
  4. 主服务器完成提交,并处罚它的post-commit钩子
  5. 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包,触发监控子进程,并触发基于时间的事件。我们可以使用inetdcron之类的工具,但是我喜欢让所有的变量在一个地方,所以我实现了我自己的服务器,在一个地方处理所有的任务。当然,重新发明轮子很可恶,所以我修改了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;除非我们工具链的这些部分都被下一代系统支持,否则我们的工作流程需要大量修改。
  • 开发者过程:因为本地提交不能自动传递到主版本库,所以开发者需要更多纪律。在实践中,需要创建一个合并管理员的角色,来保证所有的工作树可以有规律的合并,并解决冲突。

所有这些问题都不是不可逾越的,我期望随着时间分布式把版本控制可以成为标准,而不是现在的小众地位。