2.1 研发效率系统
图 2-1 展示了在互联网场景中研发项目的典型工作流程,其主要包括项目管理、代码研发和服务部署这几个核心环节。项目管理通常需要经历项目立项、设计文档撰写以及任务分解这几个步骤,每个任务的达成也通常需要多次的代码开发和迭代来实现。当有重大的版本更新时,我们需要执行上线部署的流程将新的功能应用到对外服务上。
图2-1 研发项目的典型工作流程
本节将重点介绍和以上流程息息相关的研发技术和基础设施,包括代码的组织和构建、代码审查、任务管理、持续集成以及通用压测平台。在探讨的过程中,也将对 Phabricator、Blade 和 Jenkins 这些优秀的开源工具进行介绍。
2.1.1 代码组织和构建
代码的组织、管理和构建是一个较为复杂的系统性工程,它的选型与组织文化、开发习惯以及基础设施的成熟度都息息相关。不同于传统多代码仓库的组织和构建方式,Google 和Facebook 都不约而同地使用了单一代码仓库模式(Monolithic-Repo)。在这种模式下,几乎所有研发项目的源代码都在同一个代码仓库中,并且代码的存储和构建都是在云端的分布式集群中进行的。本节我们将对这种模式下的代码组织和构建方式进行探讨。
图 2-2 展示了单一代码仓库模式下的大规模代码仓库和构建系统,我们可以看到所有的项目源代码都并列地存放在根目录下,项目内部的模块依赖关系以及项目之间的依赖关系都通过 BUILD 文件来进行描述。在进行项目构建时,系统会以源代码和 BUILD 文件为依据来自动分析依赖关系,并指挥分布式构建集群来并发执行相关的构建命令。
下面对代码研发和构建的流程进行描述。
(1)在本地开发机上,利用 FUSE(用户空间文件系统)和本地代理服务与云端的代码仓库系统和分布式构建系统进行联系。
(2)在开发人员访问相关的源代码文件时,FUSE和代理服务会依据内容指纹从云端的代码仓库中即时进行文件下载。这个过程对于开发人员来说是完全透明的。
(3)在开发人员运行代码构建命令时,命令行工具会依据BUILD文件自动解析依赖关系,并将最终的构建执行计算图借由代理服务发送给云端的分布式构建集群。
(4)分布式构建系统可以依据任务大小来动态调整构建命令执行的并发度,它也会利用分布式缓存将可复用的中间文件进行持久化存储,构建系统可以依据内容指纹来提供高效的检索方案。正是由于分布式缓存的存在,大规模构建任务的推进速度在经历短暂的冷启动之后通常是极快的。
(5)构建系统在完成了构建任务之后,会把诸如可执行文件等构建结果存储在分布式缓存系统中,开发人员依然可以利用 FUSE和本地代理服务来按需下载。
图2-2 大规模代码仓库和构建系统
由此可见,这种基于单一代码仓库和分布式构建系统的开发方式是极为高效和方便的,它具有如下一些显著特性。
● 所有的开发都是在代码仓库的主干上进行的,有效地提高了代码合并的效率,避免了分支与主干之间因长时间的脱离而在代码合并时引入大量的冲突和兼容性修复工作。
● 鼓励大范围的代码复用和实时持续集成,新代码的提交可以立刻触发所有依赖项目的单元测试和可选的回归测试。此外,代码修复或性能提升效果也都可以被立刻复制到所有项目中。
● 所有项目均从源代码开始进行构建,避免了库之间的二进制兼容问题,并保持了接口的向下兼容性,不兼容的修改可以在实时的持续集成过程中被立刻捕捉。
● 按照依赖关系按需下载,极大地缩短了代码下载时间和文件下载时间,可以有效地应对单一代码仓库和多地办公带来的挑战。
凡事有利必有弊,这种代码组织和构建方式也会带来一些新的挑战。例如:
● 某些错误的意外引入可能会影响到很多研发人员。但是由于实时持续集成的存在,此类问题通常在第一时间就可以被发现和解决。
● 这种开发模式极度依赖强大的代码仓库系统和代码构建系统,因此它并不适合所有的组织和项目。
Google 和 Facebook 的内部研发基本也都是按照单一代码仓库的模式来进行的,其内部也都建设了诸如 Blaze、Buck等构建系统来完成代码构建任务和持续集成任务。值得一提的是,Bazel 作为 Blaze 的开源方案,被用来负责 TensorFlow 的代码构建工作;而 Buck 已经是一个成熟的开源方案,可以被直接使用。还有一个类似的构建工具是由腾讯开源的 Blade,它主要定位于 Linux 系统下的大型 C++ 项目,就目前而言,它在国内许多公司里的知名项目中均有应用。
2.1.2 代码审查和任务管理
规范的任务管理和代码审查可以有效地提高迭代效率和研发质量,目前市面上有诸如 Review Board、Phabricator、Gerrit 和 GitLab 等众多优秀的产品可供使用。本节主要围绕 Phabricator 的功能和使用方式进行讨论。Phabricator 是一个现代化的任务管理和代码审查平台,它在 Facebook 以及其他许多互联网公司内部被广为使用。图 2-3 展示了 Phabricator 的功能界面,它通过 Web 管理界面和 arc 命令行工具来提供项目管理、任务管理和代码审查等功能。此外,我们也可以通过 Phabricator 提供的API 来高效地集成诸如 Jenkins 等第三方组件。除了这些基本功能,Phabricator 还提供了如下一些十分实用的高级功能。
● 规范检查:Phabricator 提供了 arclint 工具来进行自动化的规范检查。首先开发者需要配置与编程语言相关的代码规范,随后系统在代码审查的过程中会自动触发 arclint 的验证功能。
● 单元测试:首先开发者需要定义验证脚本或者编写单元测试来指导代码验证,在代码审查请求被提交到 Phabricator 后,系统会自动触发相关的验证工作。
● 持续集成:Phabricator 的 Harbormaster 组件为第三方插件的集成提供了便利,我们可以利用此功能与Jenkins 进行互操作,从而满足实时持续集成的需求。
● 审查规则:Phabricator 的 Herald 组件可以定义灵活的代码审查规则,研发人员可以通过设定代码提交条件来跟踪和规范自己关心的变更与提交。由此可见,Herald 组件在单一代码仓库的开发模式下具有非常重要的作用。
● 代码镜像:Phabricator 自带了代码仓库和代码管理功能,它也可以通过代码镜像组件将代码同步到 GitLab 等其他的代码管理系统中。
图2-3 Phabricator的功能界面
图 2-4 展示了开发人员利用 Phabricator 平台在单一代码仓库的开发模式下工作的基本流程,这些操作都是基于配套的 arc 命令行工具来协同完成的。
(1)在单一代码仓库的开发模式下,代码仓库只维护代码主干,而开发人员则可以通过本地多分支来管理不同的开发需求。
(2)两个开发者 dev1 和 dev2 ,他们都从主干的提交节点 A 拉取代码到本地来进行功能开发。
(3)开发者 dev1 利用 arc diff 来提交代码审查请求,在代码审查者接受了代码变更之后,他可以利用 arc land 将代码提交到远程的主干上,此时主干向前步进到节点 AC。
(4)开发者 dev2 在和代码审查者进行多次交互后满足了代码变更的要求,此时由于主干节点已经到达了 AC,因此他需要执行 git pull --rebase 操作将远程修改同步到本地。开发者dev2在修复了可能的代码冲突后,可以利用 arc land 来提交代码并推进主干到节点 ABC。当然,开发者 dev2 也可以在修复了代码冲突后自愿要求再次进行代码审查。
图2-4 开发人员利用Phabricator 平台在单一代码仓库的开发模式下工作的基本流程
2.1.3 持续集成
持续集成(CI)是一种广为人知的研发方式,它强调通过实时和自动化的代码持续构建与单元测试,尽早发现可能的功能缺陷和性能缺陷。这种研发方式有效地规避了线上故障,并提高了团队的研发效率。Jenkins 是最知名的自动化构建平台,它可以很好地支持各种语言的项目构建,并完全兼容 Ant、Maven 和 Gradle 等多种第三方构建工具。此外,它可以与 SVN 和 Git 等代码仓库进行无缝集成,它也支持直接与 GitHub 和 BitBucket 等知名的代码托管网站进行对接。
通过 Jenkins 和 Phabricator 的配合,我们可以流畅地将代码审查和持续集成这两个密切相关的环节串联起来。Phabricator 上的代码审查提交请求会触发 Jenkins 的持续构建和单元测试,并且执行结果随后也会被公布到 Phabricator 的审查界面中。Phabricator 系统本身和代码审查者完全可以把 Jenkins 的构建结果和测试结果作为代码审查通过的一个必要条件。由此可见,两者的有机结合大大提高了代码审查的效率和回归测试的时效性。图 2-5 展示了 Jenkins与Phabricator平台的互操作流程。
图2-5 Jenkins 与 Phabricator 平台的互操作流程
(1)Jenkins 与 Phabricator 两个系统都对外提供了 API 接口,以便进行第三方组件的扩展。
(2)在 Phabricator 系统中可以设置 Harbormaster CI 任务,代码审查请求则可以触发这些 CI 任务的执行。
(3)相关的 CI 指令可以调用 Jenkins 的 API 来发起持续构建任务和代码测试任务。
(4)Jenkins 在完成了代码构建、代码测试和代码覆盖率检查之后,它可以利用 Phabricator 的 API 来上报结果,这些信息通常会被反馈到代码审查的评论区以及状态栏中。
(5)Phabricator 系统本身和代码审查者可以把 Jenkins 的持续集成结果作为代码审查通过的评判标准之一。
Jenkins与Phabricator的集成效果如图2-6所示。
图2-6 Jenkins 与 Phabricator 的集成效果
2.1.4 通用压测平台
上面章节中介绍的工具和流程可以有效地提高代码开发的效率和代码功能的正确性,但是仅此而已是不够的。在项目的开发过程中,每一次迭代,除应该满足功能性需要之外,也需要保障运行性能不受负面影响。通用压测平台通过自动化的手段来辅助开发人员进行快速的性能回归测试,可以将开发人员从烦琐的人工压力测试中解放出来。不仅如此,通用压测平台也可以被作为持续集成的核心部件以及正式上线前的验证工具来使用。
图 2-7 展示了通用压测平台的核心组件,它由压测管理平台、数据生产模块、压测任务执行引擎以及数据分析模块组成。开发人员可以利用压测管理平台来可视化管理所有的压测任务,并通过它来设定压测方式、压测时间以及其他压测参数。数据生产模块可以利用人工构造的方式或者拦截线上请求的方式来提供充足的压测数据。压测任务执行引擎则根据系统设定来进行周期性的或者触发式的压测操作,常见的压测方式包括本地压测、四层压测和七层压测。数据分析模块会实时收集压测任务的性能指标来产生压测报告,并将结果反馈到压测管理平台上。
图2-7 通用压测平台的核心组件
正如上文所提到的,常见的压测方式包括本地压测、四层压测和七层压测,开发人员可以根据项目的特性和成熟度来选择合适的压测方式。图 2-8 主要展示了四层压测和七层压测的实现过程。
图2-8 四层压测和七层压测的实现过程
● 本地压测:它是一种最基本的压测方式,压测管理平台利用开发人员提交的压测客户端和构造的请求数据对程序进行压力测试并产生测试报告。这种方式具有配置简单、可解释性强、易于与持续集成系统进行衔接等特点,因此它可以满足大多数项目的基准压测需求。但是,本地压测无法完全模拟线上的真实请求和流量特性,因此压测结果可能和真实的线上性能指标有所差异。
● 四层压测:它工作在 OSI 网络模型的传输层。四层压测首先会将线上真实请求的传输层报文进行复制,然后利用目的地址篡改的方式将复制的网络报文导流到被压测的服务进程中。四层压测的优势在于其非侵入性和通用性,但是由于缺乏对请求内容的理解,它无法进行精细化的压测参数调节。
● 七层压测:它工作在 OSI 网络模型的应用层。七层压测将应用层的请求消息序列化之后注入消息队列中,然后利用请求回放系统来精细化控制压测过程和压测参数。七层压测的优势在于其灵活性,因为请求回放系统可以理解应用层请求的内容并灵活控制压测的全过程。这种方式也有一定的代码侵入性,因为它通常需要将必要的代码片段植入线上服务中对应用层流量进行采样。