毛剑 B站 平台架构师&EP负责人
我是在大概2015年的时候加入B站,之前是负责整个B站的后端,大概在2018年的时候转架构师,监管一个EP的团队。其实之前虽然没有做EP的一些事情,但是在转团队的时候,其实也进行了这样的实践。自己也有一些想法,如何快速交付、如何监管代码质量、如何测试等等。
我的分享会分为三大块, 首先什么是大仓库。
这个东西是大概一两年前,我当时听别人一个分享,介绍的是他们把所有的代码放在仓库里面如何工作。后来我也看到一些文章包括一些论文,有一篇文章非常经典,叫做谷歌为什么把数十亿代码放在一个仓库,包括陆陆续续我发现有一些国外在硅谷创业的公司,都在做这方面的实战,我就在思考包括自己的一些痛点,后来就走到这个方向。
第二块在实施工具链上的一些建设。
第三个如何结合大仓库,做一些CI/CD。
首先介绍一下背景,2015年加入的时候,我们当时基于一个叫织梦(音),所以我们要做转型,包括引发一些开发人员在这个项目更好地迭代,因为当时的代码就是B站KFC全家桶,所有的代码都在里面。
后来我们做微服务演进的时候,发现我们的基础库经常跟着我们的业务发展或者需求不断优化或者叠加。老实话来说,我们一开始做基础库的时候,或者我们的抽象不是很合理,或者破坏了一些原则等等。其实基础库都会频繁升级。
我印象比较深,之前是靠管理手段推技术库,包括所有的分仓库的基础库的代码推进也好,其实都是靠管理手段。比如我在群里面,几月几号必须在几点钟把基础库全部升级,这次变更可能是破坏性的,意味着我们的业务代码可能也会跟着被破坏,这个其实是比较糟糕的体验。包括我自己,每天在群里面靠吼,这个是非常低效的。
我当时管了大概有五六十个人,其实我每个同事的一些代码我都会关注。虽然有口号,但是流程是不统一的,基本也是靠每个组长去要求团队,但是整体不够自动化。
第三点我们移动端,小bug非常多。我们大量测试的工作,积压在发版前,所以说大家其实一开始都是在憋大招,憋到最后要发版的时候,在上车前大家全部提测,这也是非常糟糕的。因为你没有一个持续的,测试的工作也是无法前进的,就导致交付的速度低下。
最后一个问题版本管理非常复杂,包括后端相对来说还好,因为基础库相对统一以后,这个版本的1.0版本号,另外一个基础库的2.0非常复杂。我当时看的时候,一个超大的图,各种版本号,组件是不统一的,非常混乱。
我们有一个B站的评论模块,一个评论组件,可能因为平台他在做这个需求的时候,没有考虑的那么周全,我一个业务使用的时候,发现你功能不齐,我是不是可以代码改一改,这种情况非常常见。这些代码分散在各个仓库里面,所以这种拷贝代码的情况非常多。
所以我们交付到安卓渠道的时候,因为只有一个包,你要把所有代码合成打一个包,所以他的量是非常大的。这些都是我们在这两三年的过程中遇到的问题。所以我们后来引入了Mono-Repository,只有单一产品的仓库包含了多个基础库,应用等等。我们是把所有的代码托管到叫平台的一个库,我们取了一个名字叫做Kratos。
说一下为什么转型到大仓库,有哪些优势?我们一一讲一下。
首先第一个觉得比较爽的,有一个一致的版本。以前我们基础库可能1.0、2.0,非常烦,每次不同的业务要发布的时候,都要依赖不同的版本号,这都是比较麻烦的。使用大仓以后,第一个体验就是一致的版本,我们基础库的升级,版本是通透透明可见的,版本是一致的。
间接的我们的代码始终保持先进性。为什么这么说?比方说我的基础库从1.0迭代到2.0,如果说只能靠行政手段去吼,我的架包到2.0,你去申请一下。其实对业务方来说,他其实是没有动力的。一般都会有所谓的变更管理的思路,我就不改,但是你必须要保证我的兼容性。
第二个就是极致的代码复用,代码拷来拷去,非常混乱。我们现在合完大仓以后,如果一个团队想要依赖另外一个团队代码,其实就是跨目录的依赖,没有所谓的用某个jar包几点零,这个是比较麻烦的。
如果我有一个好多的仓库,对于一个新人来说,就像一个大城市一样,他进去以后找不到他在哪里。
目前我们一个新同学入职以后,只要开一个仓库的权限,他所有的东西都可以浏览到,这个其实是非常简单的。我们另外一些同事,可能要开发基础功能的时候,也会倾向先在大仓库里面找有没有已经存在的功能。
第三个点简单的依赖管理,怎么讲?我们构建系统,可以轻松地在目录之间挂上代码,整个依赖非常简单。这里面还有些特殊,因为我们可能还会引用一些第三方的开源库,我们现在也是在尝试像go 1.1的功能,目前我们是把第三方的库,使用版本号下载下来,再提交到大仓库里面。
还有一个优势会简单的依赖管理,我以前也经常遇到过移动端的同事。比如说A这个库会依赖B和C,B和C再依赖D。我们发现有可能出现依赖D的库会有两个版本,这两个版本可能是不兼容的,包括服务端其实也遇到过。比方说卡夫卡有一次升级,他代码就做了一些破坏性的变更。这个时候我们要升级的话,其实就比较难推动的了,有些人用的比较老,有些人用的比较新,类似这样的。
当然对于我们自己的组建库来说也是一样的,有些人用的新的,有些人用的老的,引入第三方的库,在编的时候其实就没有办法处理了,这个时候就要做一些取舍。我们移动端因为分仓库开发了,每个业务线在自己的仓库下开发,一开始使用的平台公共组建,有些人是1.0,有些人是1.1,这个时候在合包的时候也容易出错。
最终合入总包的时候,也会导致一些库的变更,又要重新测,这也是得不偿失的。
我们发现在其他语言或者是,比方说以go为例子,为什么会比较推崇静态连接方式?因为你相当于交付二进制,其实我觉得思路上是和大仓库的思路一致的,就是我把所有东西自我包含,我就可以很简单地交付出去。所以他是可以简单的依赖管理。
我们看一下还有一个他有一个原子化的代码更改,我们开发人员在一致的操作中,原子化的操作中,可以对代码库里的数百甚至数千的文件进行重大更改。
比如我们包的名字处理不是特别好,假如我是在分开的仓库中,我要把包名改一下,我们会把这一次变更为什么这样做,涉及的影响会是什么,由谁破坏他的,谁就要修复他,我们用这种思路引导所有人配合技术部的迭代和升级。
还有更好的支撑大规模代码库的重构和更新,我在单一的代码库里面可以捕获所有的依赖关系,我可以大胆地删除API。我们在一个目录迁移的时候,就做了一个计划。
首先叫大仓库一键包装,把包名的路径改了,第二个保留老的代码一个月。有些人因为是分支开发,有些老的主干时间特别久,所以我们把老的保留时间长一点,一个月。
一个月以后我们会把老的代码标记为过期,合入的时候会告诉他这是过期的不可以合入。这样的话就可以完成基础库的迭代和更新。
灵活的团队界限以及代码归属权。首先代码库里面,一般会有业务的名字或者部门的名字,我们其实要使用别人的API非常简单,我只要找到他的目录在哪里,就能看到他所有的API,我就可以非常简单地使用他的API。
包括有一些业务,因为组织结构在调整,团队经常变更,其实只要做目录的操作,就可以从一个部门划到另外一个部门。
团队之间的更好合作,由于代码库的结构,以前开发人员需要决定代码库的边界,比如我是开发分享组界,你是开发评论组界。现在不需要这种共享式的开发,因为现在非常方便了,我们只要把目录调整一下,各自的归属就可以变更了,所以说也是非常好合作的。
另外最大化的代码透明度,以及自然而然的按团队划分命名空间。这个变成了你API的路径或者命名空间。比如我们内部主要使用GRPC,那我们API路径统一为什么?比如他是在一个应用,他是一个会员服务,他的API对应的是GRPC还是什么,他是一个V1版本。
我们其实已经很少去使用文档,我记得以前对接的时候就是别人问我要请求什么接口,然后我就各种找,找到我自己的文档分给他,然后这个文档可能过段时间还忘记更新了,这个事件经常发生,最终可能有一些代码生成器之类的,保证我们的文档更新。
这种工作模式,会导致一个开发人员频繁的上下楼切换,就是一种找文档、一种找代码。我们现在其实就是文档接代码,他也可以非常清楚地告诉你我这个接口是干什么的,里面有什么参数,都描述的非常清楚。也不会到处问了,基于我想要的东西,比如我想获得VIP的状态,我可能在整条代码仓库里面搜VIP这个文件我可能就能找到,也不用到处找文本了。
我们使用大仓库以后,虽然解决了一些问题,但是也带来一些问题。
首先非常复杂的就是构建系统,早期的时候体验非常糟糕的,因为我那个时候是比较急切地引入这么一个思路,但是我们团队大概50多个人。在整体协作上其实非常困难,为什么?经常会出现某一个开发同学被默认到主干了,我们虽然会跟他说你提交代码的时候或者合并到主干的时候一定要点主干,他这个就是编辑全部。
这个因为需要大量时间,我整个代码规模非常大,可能要花非常长的时间,就导致开发人员不愿意做这个事情。后来就想能不能集成到一个Gitlab的CI/CD里面,我们后面做了一些改进,我们先没有用那个Gitlab的CI/CD,而是我们基于Gitlab hook的API,我们做了一个事件的触发。我们当时的指令叫加默值,就会触发一个HOOK,这个hook会做什么?我们做了几件事情。
首先我们用Gitlab的那个包,能把他的依赖关系整个原文件解析出来,我就知道他破了哪个包。把这些依赖关系,这些数据存到依赖的一个图,我们每一次在加默值这个操作触发以后,就会查这个依赖数,那么我就知道你哪些被依赖的需要被重新构建,所以我们就做了一个增量变异,我们当时其实是自己实践的。
后来我们也发现越来越多的公司,包括后来我听facebook使用一个构建系统,我们就发现其实谷歌内部有一个叫Bazel的一个东西,我们后面就逐渐从自己模改的方式切换到Bazel。
为什么我会去推进Bazel?我发现各个团队都有自己的语言,如果基于每一个语言做同样的事情其实成本会比较高。所以说我们后来发现Bazel其实可以跟语言结合的。
你可以认为在每个目录里面有个bulid文件,这个bulid文件会把这个目录描述出来我依赖谁。那么通过这种方式,我就可以知道全局的依赖关系,他还有个好处,就是所有的构建方式是统一的,所有语言都支持。你只要实现自己语言的录入就行了。
还有个好处,我们分析出他的依赖关系以后,其实就可以做一个增量变异。比如说我改了A库,A其实只有B在使用,我用那种增量变异的话,我只需要变异B,其他是不需要的。第二他有一个工具支持多个语言。
第三就是他可扩展,像Bazel为什么会出来?就是因为谷歌内部也是一个超大仓库,这是第三点。第四你可以扩展他,因为你可以编写自己的,所以我们目前在IOS上的大仓实践做的还不错。
大仓还有一个就是良好的目录结构和依赖规范,这个看起来很简单,实际我们在实践过程中,我自己包括我们团队都犯了很多错。
现在可以简单看一下IOS的仓库有点类似像这个样子,他可能有多个包,还有各个基础库,还有第三方的依赖库。
我们现在在go的仓库里面也会类似,有三个库,一个是第三方的库、基础库、APP,APP对后端来说像管理后台、微服务、网关等等一些模块。
当我们写代码的时候,库与库之间的依赖关系处理不好的时候,你会发现改一个仓库,会导致整个仓库全变,你变异的缓存中间结果特别容易失效,就会导致每次都要重叠。
我们为此修复了很长时间,各种各样的历史原因,比如发现某一些库被所有人使用,他可能会被经常破坏,你就要考虑是不是出现一些问题。还有就是CHANGELOG,简单的依赖环境。
还有大仓另外一些问题,就是超高代码质量要求。我们之前犯过一些错,就是基础库一些小的变动,导致所有都受影响。
假设你有一个bug被合流主干那么就会有风险,因此我们在大仓库以后,就更有理由或者要求我们的同学写更多的自测,包括代码规范,这个要求都是非常严格的。
同时我们也会利用Bazel跑一个增量的UT。另外如果我们现在是全量的UT开发人员可能都出去玩了,我们也做了一个增量的UT。
另外我们以前这种人肉的REVIEW,这种工具我们不仅仅引入了很多业界经常用的,我们还做了很多定制的开发,把这些库打到流程里面,变成一种强制的手段。
刚刚说了我们即使有大量的UT,很多静态扫描工具,仍然避免不了我们犯错。所以我们同学非常喜欢写UT,即使是这样我们还是会出现一些bug。
我们后来使用一个灰度机制,首先希望我们这次改动能够尽快合入主干,但是我不希望他立马生效,所以我们通过一个Feature Flags。
比如下面有一个Feature gates,结合我们的paas平台,通过这种方式启用。当我们这种基础库的灰度三四天或者七天我们发现比较稳定的时候,才会把flags去掉。
另外我们需要大量工具链的投入,比如我们的Bazel你是不是足够了解他,还有管理Bazel远程的缓存。
另外代码托管,如果你的代码仓库超级大,是不是需要一个存储。或者如果你是一个超大仓库,你是不是需要一个IDE,这些都是你供需的投入,都需要时间和人力。另外你test-infra非常复杂的。另外重点强调一下CodeReview,我们很鼓励沟通的。
CodeReview有几个好处,我自己感触比较深的首先他是可以保证代码质量的,第二他是人员的备份。我们鼓励跨团队,其实目前B站以前是一个部门内是跨组的Review,现在我们有跨部门的Review,其实是一个非常好的人员备份工具。当然任何一个人,其实你的代码好多人都读过,好多人都授信,任何人都可以补上来。
另外就是说他是一个知识共享和教学工具,因为他是一个良性的吐槽别人代码写的不好,因为我去看你的代码,我经常会在下面说你这写的不好,那写的不好,其实我是有压力的。
我自己就会说写的好点。那么我再看你的代码,因为你老喷我,所以我也会喷你,所以这种良性循环,会促使这个代码越写越好。另外还有一个非常好的是一个知识共享,可以发给不同的同事分享。
像之前管理职责非常重,我也会看代码。因为有些新同学可能在某些点上,他可能写出了你自己没想到的一些亮点。另外我们通过CodeReview,强调的是每个人对自己代码一定要负责,不仅仅是你要写出来,你要对他的质量复杂。
后面再讲一下CodeOwner,你对整个生命周期负责,没有运维,没有测试,所有人都是工程师。首先大仓放弃了对读权限,因为大仓基本上大家进去什么代码都能看到,所以他对读权限基本是放弃了,但是他是对写权限进行了一些微控制,你不是什么目录都可以随意破坏别人代码的。我们怎么做的呢?
我当时想的比较简单,当时我就想是不是在每个目录放一个Owner文件描述一下,后来我才发现像谷歌内部好多项目都已经采用这种机制了。后来我看了一下别的代码,他也是加工的文件。可能有一些细节的差异,我们内部定义了三个角色,一个是Owner,第二谁来负责,第三是谁编写的。
另外我们怎么去集成起来,后来我们使用Gitlab的一个to do。我们养成一个习惯,我们进界面里面,我们先点一下看我今天有什么工作要做,有点像腾讯的一个产品,不是每天在群里面吼。
另外就是邮件,这也是沟通的方式,大家在邮件里面形成很长的对话。当然我们比较紧急的一些通知也会发送企业微信。因为我们刚说了通过一个加默值的指令,gitlab master这种属于比较紧急的机制,这个也需要有的。
最后再讲一下还有一些比较复杂的问题,一个完整的持续集成,我是把他推崇的图拿出来的。首先我们有提交代码、发布代码,会有几个流程。
我觉得比较好的首先Gitlab的CI/CD特别友好的UI。在我们讨论区,会提示你这一次的代码变更,我们会提示是这些人,这些信息都会增加 Gitlab todo里面。
还有一个非常重要的一点,我见过无数公司做了好多CI/CD的系统,都是脱离开发场景。为什么这么说?我们非常鼓励只在一个平台做所有的事情,因为你不需要切各种各样的系统,因为这些都是有成本的,而且会导致一个开发人员不断切换,特别麻烦。所有的CI/CD流程都不应该被定制,而是说自由发挥。
我一开始不是特别理解为什么喜欢这种方式,后来想想因为在谷歌内部,所有人都是工程师,都可以使用代码。所以我觉得这是非常好的,包括大家有没有发现一些文件都是偏声明式的描述,本质上是希望自我都可以去描述的。
另外我们做的一个hook的插件,这个插件解释每个目录的一些信息。比方说这个目录需要谁来,我们这里有个小箭头,可以点赞,在Gitlab上只要点个赞,只要拿到以后我们就知道,那么这个就被合并到主干了。
我们在使用CodeOwner的时候也会发现一些问题,首先我们不是技术型的专家,非常难维护。还有他的CodeOwner基本不需要装什么东西,我们把所有变异的环境全部做成了容器。所以我们所有语言变异的环境,全部是有镜像(音)的。我们后来越来越鼓励你自己制作镜像,我们内部也会有一个私有的,你可以自己去构建你的环境。
我们后来发现GitlabCI/CD有什么问题呢?第一个是分支亲缘性调度,你需要不断拖代码,这些文件因为都很小,他要发好多次网络请求。还有我们需要更进一步的抽象CI/CD。
后来我们发现其实K8S有一个Prow,目前我们也是逐步往这个方向演进。
这是一个Prow的一个架构,他其实多了一层抽象,除了hook抽象出来以后,他又单独出了一个模块。目前我们已经跟Prow官方沟通了,所以目前做了很多Gitlab的一些代码的植入,这个我们也是持续地跟官方在沟通。时间关系这个图简单来说就是分为调度、分为处理任务等等。
我们还有一个非常有意思的话题,我们后来发现他里面有一个叫Helm的工具。因为UT覆盖率很高,基础库不用说,但是业务代码真正做好UT是非常困难的。我们依赖的中间件越来越多,其实非常复杂,你要把所有的引擎全部代码化,我觉得非常复杂,而且有各种各样的语言。
所以我们一开始在想自己造一个东西,后来发现有点傻,工作量非常大,是不是可以把他容器化掉。所以我们数据操作层,他依赖的资源,我们也是上容器的。
我们依赖一个物理机,你跑进去数据放进去了,如果你不清理非常麻烦。helm就是帮你管理这些镜像,帮助你升级,他也是自描述的文件表示,他依赖什么版本,应该在什么时候被启动,我觉得这是非常好用的。
我们在合代码的时候,因为只有一个主干,其他都是分支代码。在合的时候,首先一定要通过第一个合完以后,进主干的代码要重新跑一遍Pipeline,因为有可能会失败。只跑一次Pipeline,包括集成测试也是跑一次。这样的话后来我看了一个风险数据,他以前一天只能合可能几个或者十几个,他现在一天可以合几十个了。
欢迎加入哔哩哔哩的工程效率团队,大家可以扫我。B站工程团队比较有意思,因为我本来是做业务以及做其他架构师的,我们公司也非常重视工程效率。
本文为毛剑老师在 GOPS 2018 · 上海站的分享,经毛剑老师审核授权发布。
如果您想进一步了解哔哩哔哩工程效率实践
毛剑老师会在 GOPS 2019 · 深圳站 做 B站高可用实践之路的分享,敬请期待~
更多 GOPS 2019 · 深圳站精彩内容,点击 阅读原文