CODING 代码托管架构升级之路

512

本文为 CODING 创始团队成员王振威在『CODING 技术小馆:上海站』的演讲实录。

CODING 技术小馆,是由国内专业的一站式软件服务平台 CODING 主办的一系列技术沙龙。将邀请数位业内知名大牛分享技术,交流经验。同时也将邀请以为当地用户进行技术分享,为开发者们带来一场纯粹的技术沙龙。

大家好,我叫王振威。我是 CODING 的初始创始团队成员之一。CODING 从 14 年创业到现在,主要做的是代码托管,我主要负责架构和运维方面的工作。今天给大家带来一个技术分享是关于我们代码托管的整个架构是如何从最开始残破不堪的产品,一步一步升级到现在的产品的。这是今天的主题『CODING 架构升级的演变过程』。

说起架构的话,稍微有点写程序经验的人来说,都可以理解架构对于整个服务的重要性。架构最核心的三个点就是:稳定性、扩展性、性能。一个好的架构主要通过这三点来看。

会不会宕机,你的服务会不会因为自身或者第三方的原因突然之间中断。可拓展性,当你的访问量增长的时候,你的服务能不能迅速的 Copy 出很多个副本出来以适应快速增长的业务。再一个就是比如说你要做电商啊秒杀啊之类的功能的时候,能不能扛得住这种压力。这就是评价一个架构好坏的三个基本点。

我们可以想想一下,一个架构比较乱是什么样子。就好像一个机房管理员面前所有的线乱成一团。

在这里输入图片描述

一个好的架构是什么样呢?这是Google 数据中心机房的排布,我们可以看到这是非常整齐的,看起来是赏心悦目的。

在这里输入图片描述

我们的软件架构和硬件架构甚至跟建筑学等都是有共通之处的。一个好的架构一定是逻辑清晰,条理明确的。

为什么要讲代码托管的架构呢。这要从 Coding 的最开始说起,Coding 做了一个代码托管,做了个代码的质量分析,一个演示平台,还有一个在线的协作工具。代码托管是我们非常重要的一个业务,在整个 Coding 的架构演变升级的过程中,包括对各种新技术的尝试中,代码托管一直走在最前列的,包括Docker 的尝试等等。所以我今天和大家分享一下代码托管架构的演变升级,我认为这个是整个 Coding 架构系列的一个典型。

我们从最开始开始讲,整个升级过程分为四大阶段。

第一阶段:远古时代

我们三月份开始写第一行代码,七月份把产品上线,所以当时非常仓促,但这也是很多创业公司不可避免的一个现状,就是你必须要快速上线,产品可以不够完美,但必须要快速切入市场。所以原则是先解决有没有的问题,再解决好不好的问题。我们当时以这样的思路来做这个工作。我们也参考了很多开源软件,比如我们的架构就是仿照 Gitlab。那时候说白了就是租了一台服务器,然后所有的服务跑在上面,用户所有的请求都通过这台服务器进行交互,我们来看一下当时的架构图。

远古时代:阶段一

用户从最左边,通过我们在 Ucloud 的 ULB 进来,到我们自己的应用服务器,应用服务器和数据库有些交互,和缓存等其他组件有些交互,和 Git 仓库有些交互。黑框框起来的部分就是我们的应用服务器和 Git 仓库,它们当时是部署在同一台服务器上的,也就是说应用程序,代码都是部署在一起的。所以显而易见,这个架构存在很多问题,如果 Ucloud 这台服务器出了问题,我们的网站就挂了。虽然是虚拟机,但是出问题是难保的,这就是一个很不好的架构。会不会宕机?会。能不能扩展?不能。能不能抗压?不能。所有东西都在一起。就像养宠物一样,把所有服务都放在你的宠物身上保护好,但一旦你的宠物出现问题整个服务就都瘫痪了。

后来我们上线之后就慢慢在做一些改进,我们发现我们遇到最严重的问题,就是代码托管和项目协作组合到一起了,但很多时候业务是相对独立的,那我们就把他拆开吧。另外一个是代码仓库数据量越来越大,单台服务器已经很难支撑了。我们的代码仓库必须具备负载均衡的策略和重新排布的这种能力。

第二阶段:农耕时代

我们的架构是这个样子的:

农耕时代 阶段二
用户还是通过 ULB 进来,但是我们的代码仓库已经分离到单独的机器上,我们每一个机器上都会装备一个 RepoManager,用来专门管理这台服务器上的所有代码仓库。它和我们的主站服务是通过 RPC 这种方式来通讯的。我们的 Git 仓库,也有了独立的备份,不需要和主站一起备份。这是第二阶段做的主要改动。我们的 Git 有多个服务器,不只图上所示三个。每一台都有一个 Repomanager 管理器,再通过 RPC 做交互。大概是这样的一个结构。

然后我们就发现了更多问题,需要作出更多改变以承受更多的用户量。

第三阶段:手工业时代

代码仓库的服务分为两部分,一个是用户可以在本地使用 Git 工具来 push/pull 代码。另外一个是,可以在网页上直接看代码、编辑代码、查看提交历史等。这两部分操作在底层实现上来说是比较独立的,所以我们选择把他们进行更彻底的拆分。还有就是专门的认证服务,我和大家解释一下,我们每次从网站上 clone 代码或者 push 代码的时候,我们必须要认证一下用户的身份。而这个认证服务是决定了我们服务是否稳定的一个重要组件。现在我们就把他单独的拆离了出来,就是说我们有一套专门的认证服务来处理这个事情。达成我们希望的各个环节可以实现规模化增长。具体我们来看一下架构图。

手工业时代:阶段三
这个图就稍微有点不同了,用户访问我们 Git 的服务分两条线,一部分是黑线标注出来的,另一部分是红线标注出来的。红线标注出来的实际上是使用 IDE 的插件、命令行工具来访问我们的代码仓库,通过 SHH 协议、或者 Git 协议、HTTP 协议到 ULB 之后,会到我们的 Git 的 Server,Server 会交由我们的认证服务去做一个用户权限的认证。这个认证服务是独立的,比如去数据库中校验密码、校验权限,回来之后 Git Server 会在内网发一个 SSH 请求到我们的具体的代码仓库的存储机器上,最终完成代码的交互。

另外一条线,黑线表示的是我们在网页上操作的时候,比如查看代码的文件数,编辑代码等等,这条线上的请求全部都是 HTTP 请求。所以用户到 ULB 之后,就直接代理到 Web Server,和阶段二一样,通过发送 RPC 请求,到具体的 Git 仓库的存储的 RepoManager 上面从而产生数据的交互。这是我们第三阶段做的主要改进。

然而随着时间的增长我们又发现了更多的问题。一个是我们把代码仓库按分区给分离开来,但会发现代码仓库的活跃是不均匀的。如果一台机器刚好这段时间的访问量非常大,那这台机器的压力就很大,尤其是计算方面的压力,其他的服务器又可能几乎处于闲置状态。存储方面的话我们一般会做 500G-1T 的存储,但是 CPU 我们一般不会配置太高,因为大多数都属于冷数据的。这时候我们就需要一个弹性的计算池。计算和存储是分离的,就是我们的存储可以任意搭配计算池来进行计算。另外一个就是自动化监控,我们的服务从单台机扩展到很多台机,还有分区。组件也越来越多,我们有很多独立的服务,比如有独立的发邮件啊,有独立的 markdown 编译器啊,还有 qc 的服务,还有 CodeInsight 的服务、WebIDE 等等等。服务一多,运维的压力就会成倍增长。这个时候我们需要自动化监控来帮我们解决一些问题。

第四阶段:工业时代

工业时代 阶段四
用户通过 Web、HTTP 或者 SSH Git 协议链接到我们的 ULB 之后,内网做转发,网页的访问这边我就没画,到 Web Server,还是通过 RPC 请求到 Repomanager。不一样的是红线区域。用户到 GitServer 之后,先认证之后会连到服务器池,下面也是一个服务器池,组成一个计算池,主要是 CPU 和内存的配备,并没有什么磁盘这种配置。下面是存储池,存储池通过网络文件系统挂载到计算池上,所以现在就形成了这样的结构。存储由存储的负载均衡策略来决定,但是计算池由计算的负载均衡决定。这样压力大时的请求并不会同时发在同一台机器上,就能解决我们之前说的不均匀问题。这个结构里还有很多细节我们接下来探讨。

其中一个细节是,我们所有的 Git 服务都用 CDN 将用户连接起来。哪怕是中国最好的带宽资源,都比不上用户访问的服务节点在他所在城市的骨干节点,这时候我们就找了 CDN 的供应商,CDN 的地域性骨干网络节点把我们的请求转发到 UCloud 源服务器上,虽然这样成本很高,我们付出了两倍带宽的价格,但是最终的使用效果还是不错的,很多用户反映速度和稳定性有明显提升。另外我们计划推广 CDN 到全站,所有的服务。使用 CDN 另外一个好处是可以防 DDOS 攻击,我们同行包括我们自己经常遭受这样的困扰。DDOS 的原理是针对一个 IP 地址,肉鸡不断往这个 IP 地址发送垃圾包,从而导致带宽被占满。使用 CDN 之后,我们给用户报 IP 地址都是全国性的,有几百个 IP 地址,DDOS 往往都是针对单个 IP 地址来攻击的。当我们的节点收到攻击的时候,供应商可以立马将节点替换掉,从而导致大范围问题。所以用 CDN某种程度上来说可以避免 DDOS 攻击。大家都知道 Git 服务关系着公司的线上部署,对稳定性要求非常高。所以我们还是愿意花很大的成本来做这个。看一下,CDN 大概是这样一个结构。

工业时代的细节:CDN
用户通过 CDN 节点,转发到 ULB 的相关端口,和传统的静态分发不太一样的是,传统的静态分发,CDN 节点往往都缓存一些图片、CSS、JS 这些东西,但我们现在所有的数据都是从我们的原站流出去的,CDN 节点并没有给我们节省流量。所有的流量只是用 CDN 节点做了个分发,因为我们的数据都是动态的,而且有些协议不是 HTTP 的。

第二个细节是 LB,LB 就是负载均衡,我们现在的服务器中大量的部署了这种形式的服务。LB 把无状态的服务接口实现了统一。什么叫无状态服务,就是服务不会在内存中做一些状态的存储,比如说缓存,无状态服务的请求应该是和前后文无关的,下一个请求不会受前面的请求影响导致数据改变。其优点是可以部署很多实例,这些实例没有任何差别。针对这些服务,我们通过LB 把这些服务统一了起来。内部服务的相互依赖都通过 LB 完成。

然后是一个监控系统,我们用了 Google 一个团队开源出来的叫 Prometheus 的一个监控器。据我们的 CTO 孙宇聪说这个监控器比较像谷歌内部的监控系统。目前我们使用的感觉还是很不错的。

为什么要做这个,大家都知道木桶原理,很多服务相互依赖的时候,必须所有的服务都可靠,你的服务最终才是可靠的。LB 系统的目的在于:当某个实例出现问题的时候,自动剔除掉,就是做监控级别的自动运维。

LB 和监控系统配合的工作流程是这个样子的。

工业时代的细节: LB
这张图展示了我们内部系统的服务是如何相互依赖的。这里有几个角色,一个是 LB,一个是监控系统,一个是运维人员,就是左上角这个 ops。ops 操作线上的服务的时候,我么是直接操作这个 LB 配置。举个例子,我们有个 markdown 的编译器,以服务的形式存在,给我们服务中的其他服务提供 markdown 编译的底层的支持。假如有一天你发现 markdown 编译器的承载量已经不够了,必须新加一个实例。这时候上线之后,往往要做一大堆配置才能生效,但是我们操作 LB 让这个实例挂载在 LB上,LB 可是实现动态 reload ,所以可以实现快速上下线。LB 的工作是这样的,我们的 LB 是用配置文件来描述的,ops 操作完 LB,Confd 会生成最终的配置文件。把这个配置文件发送到 Etcd 集群,Etcd 就是一个配置中心,有很多配置项在里面。发到 Etcd 之后,会有另外一个程序就是 Confd,他会一直监控在这个 ETCD 的状态,当 LB 状态发生变化的时候,它就把这个变化过状态的配置拿下来,生成最终的 LB,生成最终配置文件,reload 后服务就上线了。

还有一个是监控系统的角色,我们的监控器是用 Prometheus 搭建的,监控系统有一个配置项,是用来配置服务监控的数据的接口,我们每一个服务都会起一个 HTTP 端口,提供基础的“关键指标”。“关键指标”能显示你的服务是否健康,压力有多少。还是举 markdown 编译器的例子,他的“关键指标”是每秒的处理量,一个 markdown 文本编译完的时间,就是一些自己健康状态的指标。每个服务必须统计好自己的关键指标,再把这些信息以 HTTP Metric 的形式暴露出来,我们的监控系统每隔一段时间去抓取一下这个数据,如果抓取不到或者抓取的关键指标出现了异常,根据配置的警报策略和自动处理策略开始行动,比如他认定某个实例 down 的时候,他就去通知 Etcd 某个实例 down 了, Confd 侦测到后 ,LB 就把这个实例下线。另外,当某个实例出现问题的时候,监控会通过 WebHook 的形式,去通知我们的通知中心,把这个实例有问题的信息发给我们的运维人员,比如发短信,发 App push、发邮件等让运维人员进行下一步处理。这是工业时代一个几乎半自动化的架构。无人值守的时候这套流程也基本可以正常运转。我们从系统上可以允许 Ucloud 内网出现波动,因为不论是任何情况,只要是“关键指标”有变化,我们的报警和自动处理策略就会生效。

工业时代,就是自动化生产的时代,另外要讲的一点,就是容器化。其实我们在第三阶段就开始尝试容器化了。到目前我们 95% 以上的服务都是用 Docker 来提供的。我来介绍一下我们目前是如何使用 Docker 的。一个是我们自己搭建了 Docker Rigestry,目前发了第二版,叫做 distribution ,考虑过迁移到新版但居然不兼容,迁移工具也不够好用,第一版又没有明显的问题,所以我们一直沿用到现在。

此外,我们线上的代码,都是编译在Docker 中,运行在 Docker 中,为什么要这么做呢?编译在 Docker 中有一个明显的好处,比如我们在本地开发的时候,我们是用 JDK8,那我们线上不可能用 JDK7 去编译这个版本,如果更严格我们可能要求小的版本号也一致。想保证版本使用严谨,用容器是一个非常方便的选择。如果在服务器上 linux 操作系统上装这个 JDK,可能过两天就要升级一下,非常麻烦,但是如果在 Docker 中使用,很容易指定版本。另外我们在 Docker 中编译程序,在 Docker 中运行程序。

然后是 Docker Daemon 的管理,每一台服务器上都装一台 Docker 的守护进程,它来管理上面的 Container,我们写了一套工具,原理就是给 Docker 发请求,告诉他应该起哪个 Container,应该停哪个 Container。

这个图大概展示了一下我们是如何用 Docker 的。

工业时代的细节:容器化
运维在操作的时候,有两个接口,有一个 UI Dashboard,我们可以在 Dashboard 上去控制某一个实例,也就是某一个容器,还可以通过命令行的形式来处理,最终形成 Docker 运行的指令,我们的程序由此进行管理和运行,比如我们发一个请求说要构建 markdown 编译器,代码被检出后,在 Docker 中构建,构建完了之后再把 Runtime 进行打包。打包了之后就把这个 Image 推到自建的 Docker Registry,这个时候我们的而其他服务器都可以从这里把 Image pull 下来,在推送完之后,他就通知某一台服务器,比如我们指定了某一台 markdown 编译器是运行在某一个服务器上,他就通知这个服务器,从上面拉下来 Image,然后去把他启动起来,我们一般情况下不会直接操纵这些服务器,都是直接在 UI Dashboard 完成了运维的操作。

下一阶段:信息时代

第四阶段我认为是一个自动化的阶段,下一步就是信息时代,也就是数据驱动的时代,自动化,规模化的生产才是我们的目标。最初农耕时代,可能所有都是程序员或者运维上去执行,搞完了之后进入手工业时代,有一些脚本和程序可以协助我们,后来又进入了工业时代,工业时代就有一些自动化的流程做一些自动化的运维处理,最终的目标是进入信息化时代,信息化时代就是整个我们的服务集群是一个云,这个云是弹性的,只需要告诉他我们需要什么就行了,后面的事情他自己会解决。当然这个目标离实现还是有一定距离。

来展望一下:要做到这些,一个是自动化监控,我们现在有了,但是这个监控还是有些问题,比如说每个组件的关键指标都不一样,每一个都需要单独去配置,我们希望把一些关键指标统一化,更好的量化,这样我们统一写一些监控的报警规则或者自动化处理规则就可以了。日志数据分析决策,这是什么呢,我们很多组件每天在产生上百万行上千万行的日志,这些日志靠运维去看是不可能的,我们希望能对日志进行一些分析,做一些自动化的决策,比如说某一个组件,当他数据出来,我们可能就认为他有问题了,通过 LB 把他下线,再把相关的问题发邮件给相关的人员去看,做自动化的目的是可以解放运维。

架构全球化,多机房异地部署,CODING 是肯定会走向国际的,这是我们已经在规划的一件事情。尤其是码市,会面向全球去接项目的,早晚有一天我们要向全球部署服务,所以我们的服务必须兼容异地化、高延时的跨机房部署。最后一个主要是为了节省成本,当我们服务越来越多的时候,有些服务这几天要求计算资源高,有些服务哪几天要求计算资源高,会存在浪费,所以我们希望实现整个系统可以自动的扩容,形成运维的闭环,在运维人员很少干涉的情况下,自动帮我们节省成本又不失稳定性,我相信在一个超大型的公司,几百万台服务器,肯定都是自动化处理的,希望有一天,我们也能实现这样的愿景。

这是我们希望最终实现的模型。

最底层我们还是会选择云服务商,比如说 Ucloud,AWS,包括我们在香港也部署了一些服务。像最底层的服务,物理机房、CDN,这些我们都选择找供应商,这些供应商都可以水平大规模扩展的。上面一层也是我们不用考虑的,这层主要是 VM,就是说这些服务商把下面这些硬件资源抽象化成上面的这些虚拟的计算机虚拟的网络,把虚拟的网络以 api 的形式提供给我们,我们去编写一些程序,这个程序可能会在某个适当的时机帮我们启动服务器,帮我们自动增加带宽,自动增加 CDN 节点,这就是Resource API。在这一层上面,是我们的 Docker Container 层,所有服务都运行在这层。再上面一层就是我们自己的服务了,例如一个 markdown 编译器,一个 Web 网站,一个 Git 服务,还有我们的 LB,监控、缓存、消息队列等等。我们的运维只通过 Job Manganer 告诉整个集群:我需要起一个实例,大概计算量是多少,那这个云就会自动帮我们调 Resource API、帮我们开虚拟机,配置网络,监控等等把事情全部搞定。最终就是希望实现的架构运维的闭环。

信息时代:下一阶段

今天我的分享就到这里,谢谢大家。