伴鱼发布系统实践:集泳道、灰度、四端和多区域于一体的设计与权衡

发布于: 2020 年 06 月 01 日
伴鱼发布系统实践:集泳道、灰度、四端和多区域于一体的设计与权衡

在伴鱼发展早期,四端的系统发布实践比较简单、直接:web 端直接登录线上机器替换静态文件;服务端所有项目共用一个仓库,直接利用脚本部署不同仓库子目录到固定机器,并且在发布的时候才尝试解决代码冲突;移动端从打包到发版流程基本靠手动保证。尽管比较粗糙,但是在公司早期人员少、产品迭代频繁的大背景下,这些实践方案的确起到很大的作用。

随着公司业务逐步稳定,技术团队扩张,早期的发布实践方案逐渐暴露出问题:

  • 直接登录线上机器替换文件是高危操作,运维团队也开始逐步收回所有主机访问权限

  • 服务端共享一个仓库,一个 go.mod,随着人员增加,代码冲突的概率也呈指数增加

  • 各端都有公共基础代码,这些基础库的版本控制、灰度控制问题需要各自解决

  • 测试环境需要支持不同泳道,生产环境中需要新增预发布环境,方便测试人员回归、冒烟

  • 生产环境除了全量发布,还需支持灰度发布,以减少新版本的影响范围,保证服务质量

  • ...

一个统一各端的发布系统呼之欲出,这便是技术中台各组通力协作,共同打造的东风发布系统。目前东风发布系统已经逐步接管各端的日常发布需求,成为伴鱼全体研发每天都需要访问的系统之一。

系统设计

基本流程

东风发布系统负责将各项目的仓库拉取到编译机器上,编译打包后发布到相应的环境,基本流程如下图所示:

由于历史原因,许多老项目都托管在 gerrit 上。出于统一管理和优化团队合作开发体验的目的,东风发布系统要求所有项目都托管在私有部署的 Gitlab 上,顺便依托这次迁移梳理、解决以前代码仓库管理混乱的问题。由于发布操作耗时较长,在发布结束后,发布系统会将相关消息同步给研发人员,方便他们及时了解发布结果。

逻辑分层

东风发布系统内部分为三个逻辑抽象层,自底向上依次是资源层 (Resource Layer)、基础层 (Base Layer)、服务层 (Service Layer):

资源层

资源层向上提供存储资源计算资源。伴鱼在较早技术选型阶段时就选中 TiDB 作为核心持久化存储,因此发布系统内部的各种关系型数据存放在 TiDB 中;发布系统的计算资源主要用于编译项目,生成产物,由于需要支持四端项目,需要使用拥有不同编译环境的机器。构建项目时,发布系统利用 ansible 远程驱动构建主机拉取仓库、编译项目、上传产物,做到了 agentless。

基础层

基础层向上提供服务打包和部署的基础模块,包括环境 (Environment)、区域 (Region)、泳道 (Lane)、主机 (HostManager)、运行时参数 (RuntimeArg) 以及鉴权 (Authorization) 等。一次发布的最小单元由区域、环境、泳道以及被发布服务共同确定,如:在日本区域的测试环境的 stable 泳道发布 report 服务。在一些比较复杂的发布任务中,不同环境、不同区域的发布参数不同,因此需要运行时参数模块对此进行集中管理。

在发布系统上的操作通常都有风险,因此发布系统也遵从最小权限原则,借助内部统一鉴权服务,仅赋予需要的人操作权限。我们从两个维度为研发人员赋权,角色。其中端就是四端,用于端与端之间的权限隔离;角色分为普通研发和管理员,如服务端的管理员、web 端的普通研发等,普通研发只需要关心服务层,对基础层与资源层无感知,而管理员可以配置运行时参数等,需要了解基础层。

服务层

服务层直接对用户提供任务编排、流水线执行、观察、取消、重试、通知,服务重启、回滚等功能,满足研发人员日常发布需求。

任务编排与流水线执行

考虑到各端、各服务发布的流程既有共同部分,也有不同部分,因此发布系统需要提供灵活的流程编排功能。在发布系统中,一个完整的发布流程定义在一条发布流水线 (Pipeline) 上,每次发布执行就是流水线运行的过程。一条完整的流水线由多个阶段 (Stage) 构成:

其中开始阶段 (Start Stage) 与结束阶段 (End Stage) 是系统的固定阶段,中间若干阶段为用户自定义阶段,不同发布流程可以按需编排,如项目编译阶段通常属于自定义阶段,因为各端的编译流程大相庭径。流水线中的每个阶段又可以进一步划分为多个任务:

流水线执行时,不同阶段串行执行,相同阶段内的任务并行。例如,开始阶段包含两个任务 clonecode 和 sendmsg,前者负责从 Gitlab 拉取项目代码;后者负责发送即时消息到项目群组中通知发布启动。因为流水线中的最小执行单位是任务,因此我们称定义流水线的过程为任务编排

流水线观察

流水线执行过程中会产生两种日志,任务的运行日志及流水线的执行历史,二者由日志 (Log) 模块负责管理。任务的运行日志会实时地从打包机器、运行脚本采集,通过 websocket 暴露给用户;流水线的执行历史会被持久化,以便日后复盘、审计时有据可查。

流水线取消、重试

如果开发者意外启动某发布流水线,他可以在阶段级别上取消流水线,即:取消流水线时将停止当前阶段之后的所有阶段的任务运行。如果某阶段因为一些不稳定因素执行失败,如网络抖动,开发者可以在界面上重试该阶段。由于我们完整保存了流水线的执行历史,流水线在阶段级别上的停止、重启都能够方便地实现,这也为后续的灰度发布奠定基础。

重启、回滚

每次发布流水线执行成功后会生成服务的一个新版本 (多区域发布时会产生多个新版本),版本模块就负责管理各个服务的不同版本,研发人员可以在当前版本上重启服务,也可以回滚到服务的历史版本。

核心功能

泳道发布

在服务迭代的过程中,常常有多个功能在同时开发,如果测试环境只有一套,就意味着要么团队内部商量好时间,大家轮流使用测试环境,要么就先把不同功能的代码合到某个分支再统一解冲突。无论使用哪种方式,都令人感觉十分别扭,于是我们做了泳道发布功能。

当用户指定某泳道发布服务时,发布系统会为该服务的实例打上相应的泳道标记,服务注册和发现模块就能知道同一服务的不同实例所属的泳道。

如上图所示:当服务 A 要访问服务 B 时,服务 A 会先从服务发现模块中找到服务 B 上某实例的地址,由于服务 A 在请求中打上了 t2 泳道的标记,这时服务发现模块就会从服务 B 在 t2 泳道上的实例中选择一个返回,接着服务 A 就能直接访问相应实例。如果请求中没有打上任何泳道标记,服务发现模块就会从服务 B 的默认 (default) 泳道上的实例中选择一个返回,通过默认泳道可以实现单泳道测试环境到多泳道测试环境的平滑过渡,也符合研发人员的使用预期。

为了更方便地使用泳道环境,我们的移动端和 web 端团队都配合做了相应的改造。测试时,移动端和 web 端的测试人员能够自行切换至指定泳道,切换完毕后,所有发送至服务端的请求都会带上相应的泳道标记,这样泳道测试就实现了端到端统一。

预演环境

在特定场景下,测试团队需要在线上环境才能测试产品功能,这时如果直接将服务发布到线上环境再测就等于同时把未确认的功能暴露到用户面前。为此,发布系统在线上环境提供了类似泳道的功能,即预演环境。预演环境中,所有服务、数据、中间件都与用户使用的一样,只不过由于线上流量不带预演环境的泳道标签,请求不会进入到预演环境,这样测试团队就能够在用户无感知的前提下完成测试。

灰度发布

服务功能的快速迭代和稳定运行是一堆矛盾体,我们既不希望步子迈得太快摔跟头,也不希望过于谨慎错过机会。发布系统也需要在二者中寻求平衡,即便新功能可能存在问题,也要做到影响范围可控,为此东风发布系统引入了灰度发布功能。服务端的研发人员在发布时可以指定流量灰度比例,就能将新版本功能以指定的比例部署到生产环境中。

基于目前业务发展阶段和微服务基础设施,服务端暂不支持精准控制流量的能力。我们假设负载均衡是完美的,就可以通过控制不同版本服务的实例数量间接控制服务的灰度。目前我们在 k8s 中使用 StatefulSet 管理 Pod,保证实例的唯一标识不会因为发布而产生变化:

  • 灰度发布时:容器编排服务会依据服务现有实例数,根据流量灰度比例,计算出灰度版本实例数,向上取整;然后重建一个 Statefulset,其中 Pod 镜像即为灰度版本,副本数即为计算出灰度实例数;灰度实例启动成功后,最后对主版本 StatefulSet 进行缩容,确保最终服务实例数不变。

  • 灰度回滚时:容器编排服务先将主版本 StatefulSet 中 Pod 镜像回滚到指定版本,并且扩容到目前服务总实例数,成功后再将灰度版本 StatefulSet 删除。

用户在使用灰度发布时,可以选择自动或手动的方式将灰度版本进行全量部署:

  • 自动:用户指定一段校验时长后,发布系统就会生成一个延时任务,到期后,系统就会自动将灰度版本全量部署到线上环境。

  • 手动:当灰度版本经过线上流量验证,没有问题后,用户就可手动触发全量部署。

得益于灵活的流水线设计,实现自动和手动部署并非难事。

多区域发布

为了配合公司业务发展,今年我们也开始在海外逐渐部署核心服务。原本发布系统只需要关心一个区域的发布,现在则需要支持多区域发布。考虑到在不同区域发布服务的过程大体一致,只是具体到阶段任务中的运行时参数有所差异,我们可以为单条流水线的运行时新增一个参数,部署区域,这个参数可能会影响到每个阶段任务在哪执行使用哪些运行时参数

经过在用户体验与实现复杂度二者间反复权衡权衡,我们从使用者角度出发,在启动发布时能选定多个区域同时发布;从实现复杂度角度出发,多区域发布会生成多个相互独立的流水线的运行时同时执行,这样做的好处在于:

  • 减少使用者操作次数;

  • 避免漏发某个区域;

坏处在于:

  • 单条流水线的多个运行时日志分开展示,不方便使用者实时观察;

  • 如果不同区域的发布流程中,希望某些阶段任务共用,如编译得到产物,很难支持;

  • 多区域的回滚不能优雅地处理,需要分别操作;

思考与权衡

解耦

发布系统是所有伴鱼服务的造物者,因此发布系统不能强依赖于需要它部署的服务,否则当它依赖的服务不可用时,发布系统将无法发布任何服务。但发布系统自身又不能做得大而全,任何轮子都亲自造一遍。为了平衡这两种对立的设计要求,我们梳理了发布系统对其它服务的依赖特点。从依赖内容上分为数据性依赖逻辑性依赖,从依赖方式上分为强依赖弱依赖

当弱依赖不可用时,我们可以直接使用降级数据或降级逻辑;面对数据性强依赖,发布系统的基本思路是在本地缓存一份数据,当相应服务不可用时则使用缓存数据,另外本地缓存的数据也会有一个硬编码的兜底版本,以应对相应服务长时间不可用的情况;对于逻辑性强依赖,发布系统只能自行实现,如判断是否为法定节假日的逻辑。

资源节省

由于一些不合理实践,有些项目的仓库大小已经达到 GB 级别,这既增加了代码拉取的耗时,又占用了网络带宽资源。尤其是遇到海外部署的场景,从国内 Gitlab 拉取较大的仓库十分昂贵,也可能造成专线阻塞,影响线上服务跨区域调用。采用增量拉取的方式,可以有效减少带宽资源消耗,但需要依赖编译环境本来的状态,这样编译的过程就不再是纯函数式的,不利于后期问题复现与排查。

经过权衡,发布系统采用增量拉取代码,同时尽可能保存必要的运行时环境和产物的折衷方案。另外发布系统的流水线如果在某个阶段执行失败,使用者可以直接从失败的阶段继续运行,而无需从头开始执行,这种设计也间接避免了重复消耗计算、存储和网络资源。

控制力

在各端项目接入东风发布系统后,我们就能够对项目的发布流程拥有更强的控制力,如:

  • 对各端的基础库进行管理,制定严格的上线流程。必要时甚至可以封禁问题版本、强行升级到某最低版本,达到修补漏洞、推广功能等目的;

  • 对异常时间点的发布行为进行监控和通知,避免紧急发布与日常发布之间的界限变模糊;

  • 为后续统一支持代码静态检查、回归测试等奠定基础;

强大的控制力也得益于灵活的流水线设计,使得我们能够在系统固定阶段和内置任务脚本中精准控制各端服务的发布流程。

未来工作

东风发布系统在推广过程中,也发现一些不足:

  • 流水线只支持不同阶段串行和同阶段内部任务并行的运行方式,一些复杂的场景需要阶段级别的分支逻辑;

  • 目前编译任务都是在指定编译机器的本地执行,当同时发布的服务数量增多后,这种方式可能会拖慢服务和产品的持续交付速度,需要考虑引进编译集群来分摊计算消耗资源;

这也是未来东风发布系统的前进方向。

参考

发布于: 2020 年 06 月 01 日 阅读数: 1253
用户头像

伴鱼技术团队

关注

一群崇尚简单与极致效率的工程师~ 2018.01.26 加入

伴鱼少儿英语是目前飞速成长的互联网在线英语教育品牌,我们期望打造更创新、更酷、让学英语更有效的新一代互联网产品。(技术博客官网:https://tech.ipalfish.com/blog/)

评论

发布
暂无评论
伴鱼发布系统实践:集泳道、灰度、四端和多区域于一体的设计与权衡