CI/CD 最佳实践

CI/CD 指的是 Continuous Integration(持续集成)Continuous Delivery(持续交付),从实践来看应该还有 Continuous Deployment(持续部署) 这一步。我们先来区分下这三者的区别:

  • 持续集成(Continuous Integration) :持续集成关注的是将开发人员的代码不断集成到代码库中。为了保证新的代码没有问题,我们需要在开发人员提交代码后,执行代码编译、质量审查、单元测试等操作,如果期间遇到问题需要通知开发人员进行修正,在没有问题后进行构建打包,并将制品(二进制文件、镜像)推送到制品库中。

  • 持续交付(Continous Delivery):持续交付是将集成后的代码部署到更贴近真实运行环境的「类生产环境」(production-like environments) 中。在这里可以进行更进一步的功能测试、性能压测等。如果有单独的测试团队,通常也是在这个环境进行测试工作。如果测试没有问题,就可以准备自动或者手动部署到生产环境中。

  • 持续部署(Continous Deployment):在持续交付的基础上,把部署到生产环境的过程自动化。

虽然功能有所不同,但上述三步强调的都是自动化,做到自动化编译构建,自动化测试,自动化审查,自动化部署。我们这里对 CI/CD 的一些最佳实践做简要总结。

持续集成规范

  1. 使用版本控制系统(比如 Git) 管理所有组件源代码、相关操作和变更记录。这样可以监控仓库中的变更,实现流程自动化。
  2. 创建 CI/CD 管道流水线,将尽可能多的步骤纳入流水线统一管理,并提供特定步骤是否执行的控制开关。
  3. 基础架构即代码,包括流水线在内的业务服务代码、配置代码等,都应该以代码形式纳入版本管理。
  4. 测试阶段左移,在 CICD 流水线中纳入自动测试,包括单元测试、集成测试和功能测试等,可以在开发早期发现错误。对于单个项目,每次代码变更至少要保证执行单元测试和静态代码分析。
  5. 使用专门的服务器执行 CICD。
  6. 要追踪 CICD 执行流程的关键指标,包括但不限于
    • 代码变更频率
    • 部署频率
    • 从需求到变更的持续时长
    • 构建时间,比如 maven 编译时间,镜像构建时间。
    • CICD 持续时长,整个 CICD 流程从开始到结束的耗时。

镜像标记规范

鉴于容器化编译环境的普及,编译产物通常是镜像。为了便于管理和追踪,镜像需要使用合理的命名和标记规范。

Docker 镜像名分为两部分:镜像名称和镜像标记(tag)。例如 registry.k8s.io/kube-apiserver:v1.32.8 。 这里registry.k8s.io/kube-apiserver 是镜像名称,v1.32.8 是镜像标记。如果在构建镜像时没有明确指定标记,则系统默认使用 latest 标记。

镜像名称通常是根据服务组件来命名的,一旦确定基本不会改变,这里重要的是标记 Tag 的处理。关于标记,通常有以下几种方式进行标记:

语义化版本控制

语义化版本控制(Semantic Versioning)是一种常用的版本标记方式,通常格式为 MAJOR.MINOR.PATCH。其中:

  • MAJOR:主版本号,当进行不兼容的API修改时递增,表示破坏性变更。

  • MINOR:次版本号,当添加向后兼容的新功能时递增,表示功能性增强,但不会破坏现有代码。

  • PATCH:补丁版本号,当进行向后兼容的问题修复时递增,表示bug修复、安全补丁等维护性更新

版本号递增遵循以下原则:

  • 主版本号递增时,次版本号和修订号重置为0
  • 次版本号递增时,补丁号重置为0
  • 补丁号独立递增

除了正式版本号外,Semantic Versioning 还支持预发布标识符,格式为:1.0.0-alpha.1、2.1.0-beta.2、1.0.0-rc.1。常见的预发布标识符有:

  • alpha:内部测试版本,功能不完整。
  • beta:公开测试版本,功能基本完整但可能有bug。
  • rc(Release Candidate):候选发布版本,接近最终版本。

在使用语义化版本控制管理镜像时,一般将最新版本的稳定版标记为 latest,其他版本则采用语义化控制,设置独立的标记。

Git 哈希标识

另一种常用的标记方式是使用 Git 提交的哈希值。Git 每次提交都会生成一个唯一的哈希值,可以用来标记镜像。

// 查看完整哈希值
~/cloudnative on main !2git rev-parse HEAD                                                     
8e921f5db6cfa52edee196c1e173ffdc08bf941f

// 查看段哈希值。
~/cloud on main !2git rev-parse --short HEAD
8e921f5

混合标记

如果 CI/CD 流水线支持获取更加丰富的信息,可以考虑使用混合标记的方式。比如版本号、Git 提交哈希、分支和构建时间等信息,也可以通过自定义 label 进行更加精细化的管理。

笔者常用的发布流水线就采用了混合标记的方式,因为每次构建的分支是固定的,因此采用了 version-commitID-buildTime 格式进行标记。比如v3.0.0-ae2bb521a-230528161535 表示构建的软件版本为 v3.0.0,对应的代码提交为 ae2bb521a,构建时间为 2023-05-28 16:15:35

代码审查规范

代码审查目的在于提高整体代码品质,提前暴露代码中隐藏的风险,对代码采用静态分析工具执行审查已经成为普遍共识。常用工具包括 SonarQube、Checkstyle、PMD 等。

首先开发人员最好在本地的 IDE 中集成相应的审查工具,像 IDEA、VSCode 都有相关的插件,做到在开发时就能发现问题,像笔者自己的 VSCode、IDEA、Goland 等常用 IDE 均集成了 SonarQube for IDE 插件。

其次我们需要将代码审查集成到 CI/CD 流程中,确保每次代码提交都经过审查,并且定期生成审查报告。

持续部署规范

部署计划

对于正在运行的生产系统,任何变动都会具有一定的风险,因此在部署前需要做好充分的准备工作。在执行部署前最重要的就是要制定一份发布计划,将发布所需的资源、步骤、应对方案以及相关责任人明确到位,一般包含但不限于以下内容:

  • 组件信息:需要发布更新的服务组件、PR 信息、功能描述等。

  • 发布流程:组件之间有依赖,有的要先发布,有的要后发布,需要明确发布顺序和依赖关系。为此要将整个流程步骤细化,确保每个环节都有明确的责任人和时间节点。

  • 发布时间:要计划好步骤的开始时间以及预期持续时间,尽量在预期时间内完成,以免影响后续的部署工作。

  • 检查清单:各个步骤需要检查的事项,比如镜像是否构建好,相关脚本是否准备好、验收标准是否制定好。要以清单的方式记录留痕,保证执行时不会缺失相关实现。

  • 责任人:每个步骤的执行者。

  • 复核人:如果任务比较重要最好再设置一个复核人,确保任务的顺利完成。

  • 决策人:当流程执行遇到问题且执行责任人无法自行做决定处理时,需要进行问题上报,此时需要有相关决策人提供进一步的支持工作,比如技术负责人、架构师等。

  • 回滚方案:在发布过程中,如果出现严重问题需要回滚时,必须有明确的回滚方案,包括回滚的步骤、责任人以及回滚后的验证工作。

  • 悲观分析:事先分析最悲观的的情况下可能出现的问题,并制定相应的应对和折中方案。

资源准备

部署过程中依赖的所有资源必须事先明确并准备好。比如:

  • 某些第三方依赖,比如 Maven 依赖,基础镜像依赖等。
  • 执行特定任务的脚本。比如数据修正、同步脚本等;
  • 第三方组件,比如某些中间件,需要实现准备好并加到服务配置中去。确保服务启动后能连接到正确的组件。

部署策略

对于大型的分布式微服务系统,任何小的改动都可能引发意料之外的错误,为了保证发布结果的可靠,通常会采用一些发布策略,保证即使出现问题也不会影响到整个系统。常见的部署策略如下:

停机部署 Recreate

把现有服务停止,部署好新的版本后再对外提供服务。好处是操作简单粗暴,并且在部署过程中不会出现新老版本同时在线的情况,可以保证状态的一致性。但其缺点是停机时间较长,对用户的影响会很大。

在实际操作中应该尽量避免停机部署。一般只有当新版本和老版本完全不兼容时才会考虑使用停机部署,比如数据库表结构的变更。在部署前需要发布停机公告,并且选择用户访问量较低的时段进行部署。

滚动部署 Rolling Update

通过逐个替换现有服务实例,来缓慢发布应用。每部署成功一个或多个实例并可以对外提供服务后,在将旧版本的实例删除下线。

滚动部署好处在于操作方便,尤其是在 Kubernetes 等容器编排平台上,最常用的 Deployment 资源对象本身默认就是滚动部署,这样可以做到不停机升级。但其升级过程中会有两个版本同时对外提供服务,可能会导致预期之外的问题。比如

  • 同时有两个版本对外提供服务,用户的请求访问可能会在 A、B 两个版本之间切换导致问题。
  • 新版的程序没有在生产环境经过完整的验证就对外提供服务,存在一定的风险。
  • 新旧版本同时存在,相关的回滚、依赖处理、流量管理等工作会变得更加复杂。

蓝绿部署 Blue/Green

蓝绿部署指的有两套一样的生产环境,一个是当前的生产环境(蓝),另一个是预发环境(绿)。我们先在预发环境中进行新版本的部署和测试,确认无误后,在将流量切换到新版本,这通常需要网关组件的配合。

蓝绿部署的好处在于无需停机更新,并且不会出现同时有两个版本在运行的场景,可以在用户无感的情况下进行服务升级。但和停机部署、滚动部署相比,蓝绿部署需要维护两套生产环境,运维成本会相对较高。

灰度部署

灰度部署又叫金丝雀部署,来源是 17 世界英国矿井工人在下井时都会带一只金丝雀,它对瓦斯浓度十分敏感,一旦浓度超标就会停止鸣叫,这样工人们就得到了预警,从而可以及时撤离。

灰度发布指的就是发布这样一个金丝雀版本,并将一部分生产流量切换到该版本,观察其运行情况。通常流量是按比例分配的。例如90% 的请求流向老版本,10% 的流向金丝雀版本。如果发现问题就及时修正;如果没问题就可以逐步扩大新版本的流量,直到彻底替换老版本。

灰度发布多用于某些缺少足够测试的场景,通过将一部分用户切到新版本使用,来测试功能的稳定性和可靠性。

A/B 测试

A/B 测试和灰度发布、蓝绿部署类似,但其目的完全不同:

  • 蓝绿部署是为了确保服务不停机更新,并且避免出现同时有两个版本在运行的场景。
  • 灰度部署是为了更好的验证服务的稳定性和可靠性。
  • A/B 测试是同时上线两个版本,然后做相关的对比,通过分析用户行为数据来评估哪个版本更优。

前两者注重服务的稳定性和可靠性,而 A/B 测试注重的是用户体验和功能的优化。像是网站 UI 大改版、关键算法更新等场景,因为不知道是否得到用户的认可,因此往往需要选择 A/B 测试的方式,将一部分用户拉来当“小白鼠”,然后进行数据收集和分析来得出更加科学的结论,然后基于结论做后续的优化和上线。

影子部署

影子部署和蓝绿部署类似,也是部署一套和生产环境一致的影子环境,区别在于影子服务会运行一段时间。用户请求到的生产环境的真实流量会被复制到影子环境中进行处理,只是影子环境不做任何的返回。

策略选择

在实际项目上线时,具体实际采用哪种方式取决于需求和预算:

  • 当发布到开发、测试环境时,停机或者滚动部署是一个好选择,因为干净和快速。
  • 当发布到生产环境时,滚动部署或者蓝绿部署通常是一个好选择,但要做好部署后的主流程验收测试。
  • 如果应用缺乏测试或者对软件的功能和稳定性影响缺乏信心,那么可以使用金丝雀部署或者A/B测试发布。
  • 如果资源充足,可以考虑蓝绿部署。
  • 如果对上线的安全性要求极高,可以考虑采用影子部署。
总字数:3683