相信很多做后端服务的同学在看到单机、读写分离、分片这些字眼一定不会觉得陌生。没错,代码服务在发展的开始阶段面临的问题和其他web服务大体一致,所以使用的解决方案也大体一致。
单机服务
众所周知,Git是一种分布式的版本控制软件,每个人的本地都有一份完整的代码版本数据。但为了解决多人协同开发和流程管控(评审、测试卡点等),需要一个集中式的远端中央仓库来完成这些功能,这就是单机服务的来源。
读写分离
随着协同人数的增多,以及提交数量的变多,单机升配也无法解决调用量过高的问题。而通过统计我们发现,对于Git服务读写比例大概是20:1,为了保证主链路提交的正常,我们做了读写分离,来分散压力。通过主备同步来完成数据的同步,并使用一写多读的架构来扩展读服务的能力。
分片
但无论如何做读写分离,所有机器的规格始终是一样的。虽然仓库数量的增加、平台用户的增加,无论是存储还是计算都会达到单机所能承载的极限。这个时候我们采用了分片的方式,即将不同的仓库划分到不同的片中,而每个片都是一个完整的读写分离架构。当一个请求到来时,服务会根据查询库特征信息,决定这个仓库所在的分片,而后又根据接口的读写特性,将请求转发到具体的机器上。有点类似于数据库中的分库分表。这样以来,通过分片+读写分离的架构,我们理论上可以支持水平上的无限扩容。
问题及思考
那么分片+读写分离是否是解决代码服务大规模、高并发问题的银弹呢?在我们看来,答案是否定的。
伴随着代码服务体量的发展,我们解决的核心问题一直以来主要是两个:
集中式的Git存储服务,既是I/O密集型也是计算密集型(Git的压缩算法);
文件数量众多,单个仓库的文件数量也可能是十万甚至百万级,对数据一致性的保证和运维可靠性的挑战极大。
实际上,代码平台架构的发展,就是在这两个问题之间找平衡,以在一定规模情况下保证整个平台的稳定性。但一直没有根本性地解决掉这两个问题。然而随着规模逐步上涨,上述的两个核心问题引发的劣势又逐步变得明显起来。
代码服务主备架构:有状态服务带来的问题
对高可用系统比较熟悉的同学,从名字上应该就看出些许端倪。主备架构的读写分离方案其天然引入的就是有状态服务的问题。
整个系统的请求流转,主要分两次转发:
通过统一的代理层,可将用户的不同客户端请求转发到对应的系统上,如Git命令行客户端的SSH协议和HTTP协议、页面的访问及API接口请求等。
然后对接的模块会将用户不同协议的请求转换为内部的RPC调用,并通过统一的RPC代理模块RPC Proxy和分片服务Shard Config将请求按分片和读写转发到对应的服务上。
读和写操作如何处理
如果是一个写操作(如:push,从页面上对文件、分支等进行新增、删除、修改等操作),请求则会落到仓库对应分片的RW/WO服务器上,在RW/WO服务写入完成以后,再通过Git协议的同步方式,同步到同分片的其他机器上,这样一次写操作就完成了。而对于读操作,则是在仓库对应分片中随机找一个RO的机器进行转发。
问题分析
当某个分片的主节点发生异常(服务crash或服务器宕机等),分片内的机器状态会发生变化。原本的RW/WO状态会置为不可用,Backup的机器会取代原来的RW/WO服务来承接写操作的请求。
从系统的角度上分析,主备架构存在以下四个问题:
可用性:
由于读写操作是分离的,所以在写服务器failover期间,服务的写功能是无法使用的;
对于单片而言,写操作是单点的,一台服务波动则整个分片都波动。
2、性能:
主备机器在同步上需要额外的时间开销。对于松散文件、文件压缩的Git仓库,这个耗时比单文件拷贝耗时更久。
3、安全:
用户侧的短时间内的瞬时操作,对于节点同步来说可能是并发的,无法保证同步中的事务顺序。
4、成本:
同分片写,主备机器要求规格完全一致。但由于接收的请求不同,存在严重的资源消耗不均;
由于同步的小文件多,对延时敏感,跨机房异步同步,机器规格一比一复制。
这四个系统上缺陷带来的问题,在一定使用规模和服务稳定性要求下是可以容忍的。但随着商业化的深入和用户规模的增长,这些问题的解决变得迫在眉睫。接下来我将和大家分享在过去的一年中,我们团队对这些架构上问题的思考和解决思路。
代码服务多副本架构:消灭有状态的存储服务
在上一个小节中,我们已经比较清晰地认识到架构上面临的四个问题主要是有状态服务带来的。那么在新架构的设计中,我们的目标只有一个——消灭有状态的服务。目标有了,如何去实现?我们首先对业内几个流行的分布式系统做了深入的了解和学习,比如ETCD、Paxos协议的学习等。同时我们也学习了代码服务的老大哥——Github开源的寥寥文章,但Github认为分布式架构是他们的核心竞争力,所以可参考的文章较少,但从这些文章中我们依然深受启发。首先对于任何架构升级,要能做到“开着飞机换引擎”,让架构软着陆是架构升级的保底要求。因此在grpc的代理层之上没有任何的改动。从内部的RPC调用以下则是我们新架构实施的地方。
和前一节的不同,在新的底层设计中:
我们希望设计一个GPRC D-PROXY的模块,能将gRPC的请求做到写时复制,从而达到多写的第一步;
其次在Proxy Config的模块中来存放仓库、副本、机器等元数据;
再次通过一个分布式的锁(D-Lock)来完成对仓库级别的锁控制;
最后我们希望有一个快速的算法能计算仓库的checksum,快速识别仓库的副本是否一致。
通过这些模块的组合完成仓库多副本的并发写入、随机读取,我们就可以去除底层存储节点的状态,从而能将仓库的副本离散到不同机房中。此外得益于机器与副本的解耦,每个服务器都可以有独立的配置,这也为后续的异构存储打下了基础。
代码服务多副本架构的实现
有了基础的设计,通过MVP的实现,我们验证了这个架构的可行性。并通过一年多的时间进行开发和压测,最终将我们的多副本架构成功上线并逐步开始提供服务。在实现过程中,我们为这个系统起了一个颇有意义的名字——伽利略,因为在我们的多副本架构中,最小的副本数是3,而伽利略在发明了天文望远镜观察到火星的卫星恰好就是3个。我们希望秉承这个不停探索的精神,所以起了这个有意义的名字。在具体的设计中,我们通过对gRPC proxy模块的改写,让来自用户的一次写操作完成写复制,并通过对git的改写以支持分段提交。我们基于git的数据特性,编写了checksum的模块,可以对git仓库进行快速的全量和增量一致性计算。
伽利略架构在系统架构层面解决了之前主备架构带来的问题:
可用性提升:多写和随机读让底层的git存储服务不存在写单点和failover的切换问题;
写性能提升:副本并发多写,让底层的副本间不存在主备复制的时间开销,性能比肩单盘;
安全:写操作的分段提交和锁的控制,在解决分布式系统写安全的基础上也控制了用户写操作的事务性;
成本大幅度降低:
每个副本都会承担读写操作,水位平均
副本与机器解耦,释放机器的规格限制,可以根据仓库的访问热度采用不同涉及机型和存储介质
说了这么多,当用户执行push后,在伽利略中会发生什么呢?我们用一个动画来简单演示下:
用户3和用户4是两个着急下班的同学,他们本地master分支分别是提交3和提交4;
底部灰色的圆圈代表的是服务端一个仓库三个副本的装填,可以看到其中两个master分支指向的是提交2,而其中有一个可能因为网络或者其他原因导致其master的指向为1;
当用户3和用户4前后提交了代码,由于用户3更快达到服务,所以会率先启动写的流程;
在一个写的流程开始,首先会对当前仓库副本的一致性进行检查,系统很容易就发现落后的副本1与其他两个副本不一致,因此会将其标记为unhealthy。对于unhealthy的副本,是不会参与伽利略写或读的任何操作;
当用户4的请求也被受理后,也会触发一致性检查,但是由于副本1已经被标记为unhealthy,所以对于用户4的流程,这个副本就“不可见”了;
在用户3的进程检查后,发现多数副本是一致,则认为是可以写入的,便会将非引用数据传输到副本中;同理用户4的进程也是一样,且因为非引用数据不会变更分支信息,所以不需要加锁,可以同时操作;
当用户3的进程收到非引用的数据传输成功后,便要开始进行引用的更新,这时第一步先去抢占这个仓库的锁。很幸运锁是空闲的,用户3的进程加锁成功,开始改写副本的引用指向;
当用户4的进程也完成了非引用数据传输后也开始进行引用的更新。同样,再更新操作前要去抢锁,但发现锁已经被占用,则用户4的进程进入等待阶段(如果等待超时,则直接告诉用户4提交失败);
当用户3的进程改写引用成功后,会释放掉仓库锁。此时服务端的仓库的master分支已经被修改为指向3。用户3可以开心下班了;
当锁被释放后,用户4的进程快速抢占,并尝试修改引用指向,但发现更改的目标引用master已经和之前不同(之前是2,现在是被用户3更改的3),此时用户4的进程会失败并释放掉锁。在用户4的本地会收到类似“远端分支已经被更新,请拉取最新提交后再推送”的类似提示。
以上就是多个用户同时提交时,伽利略系统内部发生的事情。眼力好的同学可能会问,那在系统中被判定unhealthy的副本会怎么办呢?这个就是刚才在架构实现中提到的运维系统会做的事务补偿了,运维系统在收到unhealthy上报和定时扫描后都会触发对unhealthy副本的修复,修复后的副本在确认一致后会置为healthy并继续提供服务,当然这个过程在判断修复是否一致也是加锁操作的。
代码托管运维管理平台
除了上小节提到的副本修复,在系统运行中,还需要很多的运维操作。以往这些都是由运维同学操作的。Git服务涉及数据和同步,比单纯的业务系统要运维复杂度要高,对应运维风险也比较高。在过去的一年中我们结合以往的运维经验,以及新架构的需要,以工具化、自动化、可视化为目标,为伽利略系统打造了对应的运维管理平台:
通过这个平台我们大大提升了运维的质量、也充分释放了运维人员的精力。