CI/CD 最佳实践
CI/CD 指的是 Continuous Integration(持续集成) 和 Continuous Delivery(持续交付),从实践来看应该还有 Continuous Deployment(持续部署) 这一步。我们先来区分下这三者的区别:
持续集成(Continuous Integration) :持续集成关注的是将开发人员的代码不断集成到代码库中。为了保证新的代码没有问题,我们需要在开发人员提交代码后,执行代码编译、质量审查、单元测试等操作,如果期间遇到问题需要通知开发人员进行修正,在没有问题后进行构建打包,并将制品(二进制文件、镜像)推送到制品库中。
持续交付(Continous Delivery):持续交付是将集成后的代码部署到更贴近真实运行环境的「类生产环境」(production-like environments) 中。在这里可以进行更进一步的功能测试、性能压测等。如果有单独的测试团队,通常也是在这个环境进行测试工作。如果测试没有问题,就可以准备自动或者手动部署到生产环境中。
持续部署(Continous Deployment):在持续交付的基础上,把部署到生产环境的过程自动化。
虽然功能有所不同,但上述三步强调的都是自动化,做到自动化编译构建,自动化测试,自动化审查,自动化部署。我们这里对 CI/CD 的一些最佳实践做简要总结。
持续集成规范
- 使用版本控制系统(比如 Git) 管理所有组件源代码、相关操作和变更记录。这样可以监控仓库中的变更,实现流程自动化。
- 创建 CI/CD 管道流水线,将尽可能多的步骤纳入流水线统一管理,并提供特定步骤是否执行的控制开关。
- 基础架构即代码,包括流水线在内的业务服务代码、配置代码等,都应该以代码形式纳入版本管理。
- 测试阶段左移,在 CICD 流水线中纳入自动测试,包括单元测试、集成测试和功能测试等,可以在开发早期发现错误。对于单个项目,每次代码变更至少要保证执行单元测试和静态代码分析。
- 使用专门的服务器执行 CICD。
- 要追踪 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 !2 ❯ git rev-parse HEAD
8e921f5db6cfa52edee196c1e173ffdc08bf941f
// 查看段哈希值。
~/cloud on main !2 ❯ git 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测试发布。
- 如果资源充足,可以考虑蓝绿部署。
- 如果对上线的安全性要求极高,可以考虑采用影子部署。