当了解到分布式文件系统不合适之后,我们也就只能采用笨办法,分片,将存储库分布在不同的存储节点,Git 命令也在这个节点上运行,这样无论是计算还是 I/O 都能够通过存储节点的扩展实现扩容,这就是 Git 目前最主要的分布式解决方案。
通过这样的方案实现平台的伸缩性时,还需要解决一些分布式环境常见的问题,比如存储库的分布,存储库队列等等,当然这些都有可用的方案,在本文就不展开细说。
主从同步,读写分离和多写高可用架构探讨无论是公共代码托管平台还是私有化部署的代码托管服务,当代平台发展到一定程度,高可用这个问题就会被反复提及,分布式系统的架构设计难度较高,与传统的单机服务有很大的差别,而 Git 代码托管平台分布式系统与普通的分布式系统有更大的差异,高可用的设计不仅要吸纳主流的分布式系统的架构经验,还需要迎合 Git 的特性,另外还需要考虑到架构的经济性。
首先我们看一下分布式大型代码托管平台的简易架构(下图的架构是精简版本,与实际架构存在差距),从下图我们可以看到,用户的 Git 请求实际上并不是直接请求到存储节点上的 Git 服务,而是通过代理服务转发过去,这些代理服务通过路由模块获得存储库位于那个存储节点,从架构上讲,这些代理服务都可以做到无状态,通过部署多个服务副本再在前端入口添加负载均衡健康检查,可以很好地做到这些代理服务的高可用,但这个架构也意味着存储节点上的存储库并不能支持高可用。
存储库要支持高可用,应该在不同的存储节点上都存在副本,在一个副本所在的节点无法正常提供服务时,需要其他副本所在的节点能够顶上来提供服务,这些副本要始终保持一致,如果不一致,在切换的时候就会出现数据紊乱,这显然是不符合用户期望的。高可用可分为主从同步高可用,以及读写分离高可用,还有同时多写高可用(多写高可用),设计一个简单的主从同步高可用系统,我们首先需要保证存储库的一致性,这里可以通过 git hooks 触发存储库实时同步,存储库副本分布在不同的节点,在用户推送代码后,被更新的存储库副本及时将数据通过内部传输协议同步到其他副本。早期 GitHub 使用 DRDB 实现同步,目前大多使用 Git 传输协议实现同步,我个人更偏好于实现自定义的 git:// 提供存储库同步功能。
存储库实现了实时同步,还需要有一种机制保证存储库数据一致,GitHub 的方案是循环哈希校验和,而我的方案是使用 BLAKE3 计算引用哈希,原理很简单,就是将存储库的引用按字典排序计算哈希值,哈希值一致意味着两个存储库的引用一致,引用一致存储库克隆获得的数据也就是一致的,两个存储库肯定一致。
这里主从同步高可用如果支持将读取请求转发到其他副本而不仅是主副本,那么这种情况就叫读写分离高可用(简称读写分离),读写分离的好处就是对于特别活跃的存储库能够提供更高的并发。当然无论是看似简单的主从同步,还是复杂的读写分离,内里考虑的细节并不少,环环相扣,需要对整个代码托管架构有一个清晰的认识。
实施类似 Github Spokes (DGit is now Spokes) 一样的多写高可用要复杂一些,主要难点是要支持同时写入到多个副本,要做到这一点需要实现一些约束性条件:
写入到多个副本的前提是多个副本的数据是一致的,GitHub 使用了三阶段提交协议先判断是否可以写入,写入的前提就是服务正常,存储库一致。
存储库的引用更新应该是事务的,也就是说可以回滚事务,这样在写入到其中一个节点失败后,其他的节点上实时回滚。这一点可以考虑使用原子更新引用,可以修改 git receive-pack 源码增强实现该功能。