您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件开发专栏 > 开发技术 > 正文

基于CI的服务端自动化设计与实践

发表于:2023-06-30 作者:Wgrape 来源:今日头条

一、写在前面

1、开发模式的演进

(1) 传统的开发模式

在传统的开发模式下,开发、运维、物理机三者之间的关系是非常紧密的。当开发完成项目后,运维会负责把项目部署到一台物理机上,由这台物理机向外提供服务。

由于服务和物理机关系紧密,导致服务非常依赖于物理机的环境。一旦需要调换物理机器,运维同事又需要在另一台物理机上安装服务依赖的环境,经过一顿折腾后,才能完成服务的部署。

(2) 容器的颠覆革命

为了解决这个问题,出现了一种名为虚拟机的操作系统虚拟化产品。不过还发展没有太久,就已经被一种更轻量级的操作系统容器化产品替代了,它就是Docker。

运用Docker容器技术,运维可以把服务依赖的环境资源都编入 Image 中,然后把服务运行在 Container 上,实现服务与物理机的解耦。开发人员也可以将运维的工作以可编程的方式编入 Dockerfile 文件中,从此打破了开发和运维之间的壁垒,大大降低了部署成本。

(3) 敏捷开发的创新

敏捷开发把一个产品交付过程拆分为了多个小周期,创新性的通过不断重复迭代的方式,实现产品的逐步改进和提前交付。从此改变了以往瀑布流模式下的大周期开发问题,可以提前了解市场需求,规避风险。

(4) Devops文化运动

正是在基于容器、敏捷开发、CI/CD 等各种前瞻技术思想的发展下,催生了一场名为 Devops 的文化运动。在这场文化运动中,更加强调加强开发、测试、运维之间的沟通。现在已经越来越多的公司或团队开始重视和投入到 Devops 的建设中,基于 CI/CD 构建服务端的自动化,让整个开发模式高效高质量的同时,也更加流程化、规范化。

2、演进过程中的特点

从上可以发现,随着技术发展,RD 与测试、运维之间的不再是泾渭分明的工作线,反而他们之间的关系愈加紧密,RD 的整个开发模式也开始向自动化方向演进。

3、为什么要写这篇文章

在目前的实际工作中,遇到了一系列影响效率和质量的问题。虽然目前有现成的 CI/CD 机制可以使用,但它的接入成本较高且不易扩展。

所以开始尝试运用 Devops 思想并结合 CI/CD 技术,创建一个基于CI的服务端自动化,把项目的开发、单测、集测、部署、检测、监控等整个完整的项目流程,都闭环集成至这个自动化中,实现更加快速高效高质量地交付产品。

经过一段时间的实践,效果很好,验证了想法的正确性,故总结和分享此文章,为大家提供思路与经验。

4、这篇文章主要写了什么

本文会以 CI/CD 技术为核心,重点介绍它能为我们带来什么,我们为什么要设计自己的CI服务端自动化,以及如何设计。

5、阅读文章前的注意内容

  • CI/CD 这项技术,下文中会简称为 CI
  • 文章内容如果出现错误理解、病句、错别字等,请见谅并欢迎大家的指出

二、问题现状

1、项目迭代速度快

在瀑布流开发模式下,一个项目的周期是足够长的,一般有充足的时间来完成整个项目流程。但在敏捷开发模式下,开发内容的缩减,伴随而来的也是需求、测试等环节时间的缩减,导致迭代速度也越来越快。

举例 :今天需要开发一个功能,但可能不经过测试,产品验收后就可以上线。

2、第三方工具太重

市面上流行的各种第三方工具大都非常重,功能繁多,依赖复杂,甚至还都会附带一个后台管理系统。导致很多时候我们只需要某个工具10%的功能,但不得不接受它90%附加的功能,这类重型工具只适合稳定发展的项目。

对于快速迭代的项目,需要的是轻量、轻量、还是轻量!

举例 :接口文档自动生成工具中,大部分都需要私有服务器部署而且会附有各种管理系统。

3、测试的时间紧张

在传统项目流程中,测试一直是时间分配最少的环节,再加上多端测试、Bug验证以及整体回归,导致测试时间不足的问题很常见也很难避免。

举例 :QA在验某个功能时发现一直有问题,由于RD没有写单元测试,较久后才排查出原因,导致QA测试时间更加紧张

4、需求的满足较慢

在上面已经说道,敏捷开发下的节奏整体是非常快的。对于RD而言,无论是需要 QA、OP、还是 SRE 等任何一方的需求,满足速度虽然可能不慢,但还是不能支持敏捷开发下迭代的速度。

举例1 :某个业务接口需要自动化测试支持,但 QA 可能需要排期才能完成
举例2 :后端服务上线时可能由于配置缺少等问题发生 panic,需要 SRE 在 CI 中新增配置检查,但需要排期才能完成

三、如何解决

在面对以上种种问题下,如果没有一个完整健全的机制,是难以轻松应对敏捷开发这种快迭代速度的,交付质量也会大打折扣。

所以为了解决这个问题,我们开始运用 Devops 思想,基于 CI 来建立一套完整的、覆盖广的服务端自动化,打破不同部门之间的壁垒,适应快速迭代,满足质量要求。

四、我们的实践

1、设计方案

(1) 轻量化设计

在设计之初,最主要的目标就是轻量化 。轻量绝不代表着不完整或不成熟,反而是省去了所有的细枝末节后,用一种更少的成本,更快速的满足实现了我们的需求。 所以在整个设计中,都会贯穿轻量化的思想。

  • 仅依赖 Gitlab 现有的 Runner 机器作为服务器,没有再使用额外的机器资源
  • 没有使用额外的后台管理系统,直接选择了 Gitlab Repository 作为托管服务,接口文档分别放置在 Gitlab仓库的 /apidoc.md 和 Wiki 中
  • 虽然使用了第三方缓存仓库,但为了速度足够快,我们希望可以不使用50MB以上的工具。其实到目前为止,大部分使用的工具都在10MB以下。

(2) 多项目共用

在微服务架构下,一套代码被划分到多个代码库中,多个代码库下都有自己的 CI 代码,一旦 CI 中任意一个流程有变动,那么所有项目都需要配合修改,造成的整体联动调整过大。

为了解决这个问题,采用了如下多项目共用的设计。在这个设计中,不需要再把 CI 运行逻辑写在 gitlab-ci.yml 文件中了,而是写在 CommonCI 这样的仓库中,并由 start.sh 脚本启动。

(3) 插拔式任务

基于插拔式扩展的思想,所有的任务也都是可插拔、易扩展且完全可控的,还可以实现多任务的编排。

(4) 第三方缓存

在运行过程中,虽然 gitlab CI 提供了如 cache 和 artifacts 这样的中间产物功能,但它们会有很多限制,有时候可能无法良好满足。所以会设计自己的第三方 Cache 库,用来存放已经编译好的二进制文件,加快 CI 的执行时间。

2、技术和思想

(1) 使用到的技术

  • Gitlab 技术栈 :基于 gitlab CI 和 gitlab API 实现流水线的自动工作和相关托管功能
  • Shell 编程 :为了实现流水线中不同任务的插拔和编排,需要使用大量的 Shell 编程
  • Go 技术栈 :对于配置检查、依赖检查、接口文档生成、自动化测试等一系列需要对业务代码处理的工作,都依赖 Go 技术栈
  • 容器技术栈 :虽然目前仅第三方缓存库基于 Docker 对各种源码包编译实现,不过为了方便支持日后的服务容器化管理,容器技术栈会在计划之中

(2) 使用到的思想

  • Devops :「为什么要做服务端自动化」「这样做的意义是什么」「如何去做」等等,它们的背后都是 Devops 思想

3、整体的架构

基于 CI 建立的服务端自动化架构如下所示,它一共分为三层 :

  • 代码仓库层 :是代码仓库的总称,包括业务和通用仓库
  • Runner 层 :是 Gitlab 配置的 Runner ,是实际运行 CI 的服务器
  • CI 自动化层 :是对服务端自动化的抽象,包括了一系列的插拔式任务,它们共同构成了整个自动化的流水线。

4、代码仓库层

从上图架构中的代码仓库层可知,除了业务代码仓库外,还有通用代码仓库,其中最重要的就是我们设计的 CommonCI 仓库。CommonCI仓库是解决多项目 CI 共用的核心实现,它的目录结构如下所示。

  • /.gitlab :包括通用方法和不同任务的 CI 目录
  • /start.sh :启动 CI 的脚本。当执行这个脚本时,会自动执行 /.gitlab 目录下的 *.sh 脚本文件 ,它们就是服务端自动化中的所有任务

5、CI 自动化层

(1) 预安装

详情请见源代码 .gitlab/apidoc_gen.sh

在执行 CI 前,可能需要大量的安装操作,我们都放在了 .gitlab/pre_install.sh 预安装脚本中。

(2) 代码检查

详情请见源代码 .gitlab/check_code.sh

一般情况下,对于代码检查工具的使用,不仅仅是为了规范代码,更多更强烈的需求是希望它能尽可能的帮助我们检查出大部分的代码错误。

通过代码扫描、词法/语法分析、控制流分析等技术实现程序的静态分析,甚至还可以针对我们的业务做定制化的 Bug 分析。

所以我们在 CI 中添加了 golangci-lint 代码检查工具,让RD可以更加放心的提交代码。

① 定制化检查

如以下代码是在业务中比较常见且易犯的错误,基于定制化 Bug 分析的这种场景需求,还可以开发更加轻量的内部代码检查工具,它可以简单的分析识别出以下语法错误。

// 正确
var logger = logging.For(ctx, "arg1", "value1", "arg2", value2)

// 错误
var logger = logging.For(ctx, "arg1", "value1", "arg2")
 

(3) 配置检查

在开发阶段,在测试环境添加了某个(Kafka、Redis、Server)的配置后,可能由于疏忽在线上忘记了添加对应的配置,导致服务一上线就发生 panic 或触发某个逻辑后产生 panic ,严重可能会影响到业务。

为了避免这种情况,基于这种场景,我们基于 Go 开发了一个轻量的配置检查工具,并通过 Shell 集成至 CI 中,它可以基于测试和线上两种环境的配置内容,做出相应的 diff ,帮助我们检查是否缺少相应的线上配置。

(4) 依赖检查

在微服务架构下,一般每个业务至少都会有几十个代码库,对于相似的逻辑,有时候避免不了需要跨项目甚至跨业务的复制粘贴,如果稍加不注意,就很容易出现把A项目的 package 错误添加到了B项目的依赖中。

如果你够幸运的话,代码可能会跑不起来,这时候就会发现原来是引入了错误的依赖,修改后即可避免一次错误。如果不幸运,代码可能会运行起来,导致上线后可能才会在某个条件下触发这个错误,进而影响业务。

为了避免这种情况,基于这种场景,我们基于 Go 开发了一个轻量的依赖检查工具,并通过 Shell 集成至 CI 中,它会解析项目的 Godeps.json 文件,从中找出错误的依赖。

(5) 单元测试

详情请见源代码 .gitlab/unit_test.sh

相信大部分的 RD 都有这样的经历,开发了一期大需求,虽然QA也正常测试完成了,但看着几十个文件、上千行代码的 diff ,总感觉心里没底。出现这个问题的大部分原因,是因为 RD 自己没有做好单元测试。

由于 RD 的代码对 QA 来说是透明的,他们根本不知道代码的逻辑是什么,只能从用户的使用角度去测试,但用户的动作是无法枚举完的,总会有想象不到的地方。

所以为了避免这个问题,也为了降低代码改动对以前业务逻辑的影响,我们在项目代码中编写了较大量的单元测试并集成至 CI 中。

(6) 本地构建

详情请见源代码 .gitlab/local_build.sh

现代DevOps涉及软件应用程序在整个开发生命周期内的持续集成和持续部署。所以我们在 CI 中集成了本地构建的任务,它会完成整个项目部署构建的过程,包括打包上传等后续操作。

(7) 启动自检

详情请见源代码 .gitlab/health_check.sh

根据以往经验,项目在线上或者测试环境部署时直接出现 panic 的情况是时有发生的。为了解决这个问题,上面的配置检查是其中一个方案,但它还远远不够,因为我们的服务并没有真正的启动起来,如果不启动就无法确定服务是否真的可以正常运行。

所以为了避免这种情况,我们在 CI 中集成了服务启动和自检查的功能。

(8) 接口文档生成

详情请见源代码 .gitlab/apidoc_gen.sh

众所周知,接口文档的治理一直是一个开发流程中非常难真正解决掉的问题,所以也由此催生很多和 Swagger 类似的接口文档自动生成工具。

不过我们并没有使用 Swagger ,虽然它足够应对遇到的所有场景,但是它太过庞大,完全不适合我们的业务使用。

所以我们用 Go 开发了一个基于 Model 的 API Mock 工具,它是一个绝对轻量且能满足业务需求的接口文档生成工具,不但不需要服务器托管,还节省了大量成本。

(9) 接口自动化测试

详情请见源代码 .gitlab/api_test.sh

虽然我们已经有了单元测试,但单元测试只是对某个逻辑中细小的一个单元进行测试,无法确保整个接口层面的正常工作。

所以为了更高一层的服务稳定考虑,我们决定加入自动化测试,它不但可以替代一部分 QA 的工作,而且还可以提高服务的稳定性。

实现原理是我们创建了一个基于 Go 语言的接口自动化测试仓库,RD 负责编写相关的接口测试用例,最后通过 Shell 接入至 CI 中。

(10) 任务的添加删除

当需要添加新的任务或删除任务时,只需要在 start.sh 脚本中添加或删除即可。

比如现在需要新增一个自动部署的任务,先添加 auto_deploy.sh 脚本 ,然后添加到 start.sh 脚本中即可 ,如下所示。

6、项目如何接入

在以往的项目中,当接入 CI 时,需要在 .gitlab-ci.yml 文件中写大量复杂的代码逻辑,可维护性非常差。为了支持多业务的快速接入,必须尽可能减小接入成本。

(1) 只需要创建一个配置文件

不同项目接入时,只需要创建一个非常简单的 .gitlab-ci.yml 文件,然后按照如下模板化的方式配置业务方所需要的变量即可。

# this example yml file: https://jihulab.com/WGrape/apimock-example/-/blob/main/.gitlab-ci.yml
image: golang:1.17
variables:
  # variable configuration for [your private gitlab host]
  GITLAB_HOST: ""
  GITLAB_API_TOKEN: ""

  # variable configuration for [your project]
  PROJECT_NAME: "apimock-example"
  PROJECT_ID: 48845

  # variable configuration for [DingDing WebHook]
  DING_KEYWORD: "apimock-example"
  DING_ACCESS_TOKEN: ""
  DING_NOTICE_SWITCH: "off"

  # variable configuration for [check code]
  CHECK_CODE_SWITCH: "on"

  # variable configuration for [unit test]
  UNIT_TEST_TRIGGER_CMD: "cd mock && go test -v . && cd .. && \
                          cd service && go test -v . && cd ..
                         "
  UNIT_TEST_SWITCH: "on"

  # variable configuration for [apidoc generator]
  APIDOC_TRIGGER_CMD: "cd mock && go test -v . && cd .."
  APIDOC_FILE: "apidoc.md"
  APIDOC_SWITCH: "off"

  # variable configuration for [local build]
  LOCAL_BUILD_TRIGGER_CMD: "go mod download && go build -o project && nohup ./project &"
  LOCAL_BUILD_SWITCH: "on"

  # variable configuration for [health check]
  HEALTH_CHECK_TRIGGER_CMD: "curl -X GET 127.0.0.1:8000/ping"
  HEALTH_CHECK_SUCCESS: "ok"
  HEALTH_CHECK_SWITCH: "on"

before_script:
  - echo '====== CIManager Start Running ========='

after_script:
  - echo '====== CIManager Stopped Successfully ========='

stages:
  - CIManager

CIManager:
  stage: CIManager
  script:
    - git clone -b testing https://github.com/wgrape/CIManager.git ; cp -an ./CIManager/. ./ ; rm -rf ./CIManager ; bash start.sh
 

(2) 自动创建一个配置文件

创建一个配置文件的方式十分简单方便,但这种方式还是需要相应的人工成本。

为了更加低成本的接入,可以使用我们的CLI工具,它基于 read -p 命令和模板替换的思想,通过人机交互的输入,可以完成配置文件的自动创建。

(3) 配置文件的构成

从上面的配置文件中可以发现,它主要由 image 、variables 、before_script 、after_script 、stages 这5个部分构成。

  • image :指定一个镜像
  • stages :定义了 uniteci 这唯一的一个 Stage
  • before_script :在 UniteCI Stage 执行前需要执行的命令
  • after_script :在 UniteCI Stage 执行后需要执行的命令
  • variables :整个配置的核心,它定义了在 UniteCI Stage 中所有需要的变量

(4) 配置文件的特点

它不同于传统配置内容主要体现在以下几个方面。

  • 只有一个Stage
  • 主要是基于变量驱动的方式
  • 极简的配置设计,降低了编写和接入的门槛,提高可维护性

7、缓存带来的性能提升

一切的设计都是有原因的!之所以设计中使用第三方缓存,是因为在早期整个 CI 运行过程中,大量的耗时都在 golangci-lint 工具的下载和编译上面,正常耗时都在5分钟~10分钟,有时甚至直接挂起到几十分钟以上,严重影响正常使用。

在尝试了artifacts和cache方案无法解决时,决定开始使用第三方缓存,我们把下载编译好的 golangci-lint 工具放在了第三方缓存库中,这样每次直接下载这个编译后的二进制文件即可。

后期使用了缓存服务后,几乎每次运行整个 CI 时,都可以在1分钟内即可完成,速度提升了足足5倍以上!

五、我们的目标

在经过我们的实践后,最终确立了最终要实现的目标 :保证服务稳定且高效的迭代

1、稳定

在整个项目过程中,特别是上线流程中,不出现各种低级错误导致的部署失败问题,比如以下情况

  • 缺少某个服务配置,导致panic
  • 资源未初始化导致的panic,如map未初始化
  • 代码在修改时影响到了其他的逻辑,导致其他地方出现bug
  • 代码有低级错误,但是没有自测,导致服务部署时根本无法部署成功

2、高效

在整个项目过程中,特别是开发联调和测试跟进流程中,尽可能少出现低效率或工作进度阻塞的问题,比如以下情况

  • 提测的项目根本无法达到提测标准,测试工作严重阻塞
  • 开发过程中由于接口文档等问题,导致前后端工作效率都受到较大影响

六、回顾和总结

阅读至此,本文已经临近结束了。下面我们再来回顾下主要内容 :

1、在敏捷开发的快速迭代下,我们必须选择一种合适的服务端自动化方案,来提高整个开发周期的速度、质量、和流程规范化。

2、正是基于这个背景,我们才运用 Devops 思想,基于 CI 建立了一套完整的、覆盖广的服务端自动化。

3、在设计的过程中,我们的目标是更加的轻量、可扩展和低接入成本,方便随时随地快速迭代。

4、在实践的过程中,我们遇到了如多项目共用、执行速度慢等诸多问题并逐一解决。

言而总之,本文从原因到方案,为大家分享了一个比较全面的《基于CI的服务端自动化》解决方案,希望对大家有所帮助。不过还有一点必须要清楚的是,我们做这个东西不是为了做而做的,而是有切实的背景需求。这样即使在快节奏的迭代下,它也可以为整个开发流程提高效率和质量。