本文为 CODING 创始团队成员王振威在『CODING 技术小馆:上海站』的演讲实录。linux
CODING 技术小馆,是由国内专业的一站式软件服务平台 CODING 主办的一系列技术沙龙。将邀请数位业内知名大牛分享技术,交流经验。同时也将邀请觉得当地用户进行技术分享,为开发者们带来一场纯粹的技术沙龙。程序员
你们好,我叫王振威。我是 CODING 的初始创始团队成员之一。CODING 从 14 年创业到如今,主要作的是代码托管,我主要负责架构和运维方面的工做。今天给你们带来一个技术分享是关于咱们代码托管的整个架构是如何从最开始残破不堪的产品,一步一步升级到如今的产品的。这是今天的主题『CODING 架构升级的演变过程』。数据库
提及架构的话,稍微有点写程序经验的人来讲,均可以理解架构对于整个服务的重要性。架构最核心的三个点就是:稳定性、扩展性、性能。一个好的架构主要经过这三点来看。api
会不会宕机,你的服务会不会由于自身或者第三方的缘由忽然之间中断。可拓展性,当你的访问量增加的时候,你的服务能不能迅速的 Copy 出不少个副本出来以适应快速增加的业务。再一个就是好比说你要作电商啊秒杀啊之类的功能的时候,能不能扛得住这种压力。这就是评价一个架构好坏的三个基本点。缓存
咱们能够想一想一下,一个架构比较乱是什么样子。就好像一个机房管理员面前全部的线乱成一团。服务器
一个好的架构是什么样呢?这是Google 数据中心机房的排布,咱们能够看到这是很是整齐的,看起来是赏心悦目的。markdown
咱们的软件架构和硬件架构甚至跟建筑学等都是有共通之处的。一个好的架构必定是逻辑清晰,条理明确的。网络
为何要讲代码托管的架构呢。这要从 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 节点,转发到 ULB 的相关端口,和传统的静态分发不太同样的是,传统的静态分发,CDN 节点每每都缓存一些图片、CSS、JS 这些东西,但咱们如今全部的数据都是从咱们的原站流出去的,CDN 节点并无给咱们节省流量。全部的流量只是用 CDN 节点作了个分发,由于咱们的数据都是动态的,并且有些协议不是 HTTP 的。
第二个细节是 LB,LB 就是负载均衡,咱们如今的服务器中大量的部署了这种形式的服务。LB 把无状态的服务接口实现了统一。什么叫无状态服务,就是服务不会在内存中作一些状态的存储,好比说缓存,无状态服务的请求应该是和先后文无关的,下一个请求不会受前面的请求影响致使数据改变。其优势是能够部署不少实例,这些实例没有任何差异。针对这些服务,咱们经过LB 把这些服务统一了起来。内部服务的相互依赖都经过 LB 完成。
而后是一个监控系统,咱们用了 Google 一个团队开源出来的叫 Prometheus 的一个监控器。据咱们的 CTO 孙宇聪说这个监控器比较像谷歌内部的监控系统。目前咱们使用的感受仍是很不错的。
为何要作这个,你们都知道木桶原理,不少服务相互依赖的时候,必须全部的服务均可靠,你的服务最终才是可靠的。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、帮咱们开虚拟机,配置网络,监控等等把事情所有搞定。最终就是但愿实现的架构运维的闭环。
今天个人分享就到这里,谢谢你们。