摘要

DevOps 一词源于 Development 和 Operations 的组合,即将软件交付过程中开发与测试运维的环节通过工具链打通,并通过自动化的测试与监控,减少团队的时间损耗,更加高效稳定地交付制品。

本篇文章将着重探讨 DevOps 在 持续集成阶段需要提供的能力,将对工作流的设计及流水线的优化思路做一个简要讲解。

随着项目规模越来越大,功能特性与维护人员越来越多,特性交付频率与软件质量之间的矛盾日渐尖锐,如何平衡两者成为了目前团队亟需关注的一个重点,于是,落地一个完善的 DevOps工具链便被提上日程。

我们认为,从代码集成、功能测试,到部署发布、基础设施架构管理,每一个环节都应该有全面且完善的自动化监控手段,并尽量避免人工介入。只有这样,软件才能同时兼顾质量与效率,在提高发布频率的情况下保证可靠性。这是每一个成功的大型项目最终一定要实现的目标。

本篇文章将着重探讨 DevOps持续集成阶段 需要提供的能力,将对工作流的设计及流水线的优化思路做一个简要讲解。

当我们在谈论 CI 时,我们在谈论什么

CI(Continuous Integration),即持续集成,指频繁地(一天多次)将代码集成到主干的行为。

注意,这里既包含持续将代码集成到主干的含义,也包含持续将源码生成可供实际使用的制品的过程。因此,我们需要通过 CI,自动化地保证代码的质量,并对其构建产物转换生成可用制品供下一阶段调用。

因此,在 CI 阶段,我们至少有如下阶段需要实现:

  1. 静态代码检查

这其中包括,ESLINT/TSLINT 静态语法检查,验证 git commit message 是否符合规范,提交文件是否有对应 owner 可以 review 等等。这些静态检查不需要编译过程,直接扫描源代码就可以完成。

  1. 单元测试/集成测试/E2E 测试

自动化测试这一环节是保障制品质量的关键。测试用例的覆盖率及用例质量直接决定了构建产物的质量,因此,全面且完善的测试用例也是实现持续交付的必备要素。

  1. 编译并整理产物

在中小型项目中,这一步通常会被直接省略,直接将构建产物交由部署环节实现。但对于大型项目来说,多次频繁的提交构建会产生数量庞大的构建产物,需要得到妥善的管理。产物到制品的建立我们接下来会有详细讲解。

利于集成的工作流设计

在正式接入 CI 前,我们需要规划好一种新的工作流,以适应项目切换为高频集成后可能带来的问题与难点。这里涉及到的改造层面非常多,除了敦促开发人员习惯的转变以及进行新流程的培训外,我们主要关心的是源码仓库的更新触发持续集成步骤的方式。

流水线的组织形式

我们需要一个合适的组织形式来管理一条 CI 流水线该在什么阶段执行什么任务。

市面上有非常多的 CI 工具可以进行选择,仔细观察就会发现,无论是 Drone 这样的新兴轻量的工具,亦或是老牌的 Jenkins 等,都原生或通过插件方式支持了这样一个特性: ConfigurationasCode,即使用配置文件管理流水线。

这样做的好处是相当大的。首先,它不再需要一个 web 页面专门用于流水线管理,这对于平台方来说无疑减少了维护成本。其次对于使用方来说,将流水线配置集成在源码仓库中,享受与源码同步升级的方式,使得 CI 流程也能使用 git 的版本管理进行规范与审计溯源。

确立了流水线的组织形式后,我们还需要考虑版本的发布模式以及源码仓库的分支策略,这直接决定了我们该以什么样的方式规划流水线进行代码集成。

版本发布模式的取舍

在《持续交付 2.0》一书中提到,版本发布模式有三要素: 交付时间、特性数量以及交付质量

这三者是相互制衡的。在开发人力与资源相对固定的情况下,我们只能对其中的两个要素进行保证。

传统的项目制发布模式是牺牲了交付时间,等待所有特性全部开发完成并经历完整人工测试后才发布一次新版本。但这样会使得交付周期变长,并且由于特性数量较多,在开发过程中的不可控风险变高,可能会导致版本无法按时交付。不符合一个成熟的大型项目对于持续交付的要求。

对于持续集成的思想来说,当我们的集成频率足够高,自动化测试足够成熟且稳定时,完全可以不用一股脑的将特性全堆在一次发布中。每开发完成一个特性就自动进行测试,完成后合入等待发布。接下来只需要在特定的时间周期节点自动将已经稳定的等待中的特性发布出去即可。这对于发布频率越来越高,发布周期越来越短的现代大型项目中无疑是一个最优解。

分支策略

与大部分团队一样,我们原有的开发模式也是 分支开发,主干发布的思想,分支策略采用业界最成熟也是最完善的 Git-Flow模式。

可以看出,该模式在特性开发,bug 修复,版本发布,甚至是 hotfix 方面都已经考虑到位了,是一个能应用在生产环境中的工作流。但整体的结构也因此变得极为复杂,不便管理。例如进行一次 hotfix 的操作流程是:从最新发布前使用的主干分支拉出 hotfix 分支,修复后合入到 develop 分支中,等待下一次版本发布时拉出到 release 分支中,发布完成后才能合回主干。

此外,对于 Git-Flow的每一个特性分支来说,并没有一个严格的合入时间,因此对于较大需求来说可能合入时间间隔会很长,这样在合入主干时可能会有大量的冲突需要解决,导致项目工期无端延长。对此,做大型改造与重构的同学应该深有体会。

针对这一点,我们决定大胆采用 主干开发,主干发布的分支策略。

我们要求,开发团队的成员尽量每天都将自己分支的代码提交到主干。在到达发布条件时,从主干直接拉出发布分支用于发布。若发现缺陷,直接在主干上修复,并根据需要 cherry pick 到对应版本的发布分支。

这样一来,对于开发人员来说需要关注的分支就只有主干和自己 working 的分支两条,只需要 push 与 merge 两条 git 命令就能完成所有分支操作。同时,由于合入频率的提高,平均每人需要解决的冲突量大大减少,这无疑解决了很多开发人员的痛点。

需要说明的是,分支策略与版本发布模式没有银弹。我们采用的策略可能并不适合所有团队的项目。提高合入频率尽快能让产品快速迭代,但无疑会让新开发的特性很难得到充分的手工测试及验证。

为了解决这一矛盾点,这背后需要有强大的基础设施及长期的习惯培养做支持。这里将难点分为如下几个类型,大家可以针对这些难点做一些考量,来确定是否有必要采用主干开发的方式。

  1. 完善且快速的自动化测试。只有在单元测试、集成测试、E2E 测试覆盖率极高,且通过变异测试得出的测试用例质量较高的情况下,才能对项目质量有一个整体的保证。但这需要团队内所有开发人员习惯 TDD(测试驱动开发)的开发方式,这是一个相当漫长的工程文化培养过程。

  2. Owner 责任制的 Code Review 机制。让开发人员具有 Owner 意识,对自己负责的模块进行逐行审查,可以在代码修改时规避许多设计架构上的破坏性修改与坑点。本质上难点其实还是开发人员的习惯培养。

  3. 大量的基础设施投入。高频的自动化测试其实是一个相当消耗资源的操作,尤其是 E2E 测试,每一个测试用例都需要启动一个无头浏览器来支撑。另外,为了提升测试的效率,需要多核的机器来并行执行。这里的每一项都是较大的资源投入。

  4. 快速稳定的回滚能力和精准的线上及灰度监控等等。只有在高度自动化的全链路监控下,才能保证该机制下发布的新版本能够稳定运行。这里的建设我会在之后的文章里详细介绍。

大型项目中产物->制品的建立

对于大多数项目来说,在代码编译完成生成产物后,部署项目的方式就是登录发布服务器,将每一次生成的产物粘贴进发布服务器中。生成的静态文件由于 hash 不同可以同时存放,html 采用直接覆盖的方式进行更新。

直接使用复制粘贴的方式来操作文件的更新与覆盖,这样既不方便对更新历史的审计与追溯,同时这样的更改也很难保证正确性。

除此之外,当我们需要回滚版本时,由于服务器上并没有存放历史版本的 html,因此回滚的方式其实是重新编译打包生成历史版本的产物进行覆盖。这样的回滚速度显然不是令人满意的。

一个解决方法是,不要对文件进行任何的覆盖更新,所有的产物都应该被上传持久化存储。我们可以在请求上游增设一个流量分发服务,来判断每一条请求应该返回哪一个版本的 html 文件。

对于大型项目来说,返回的 html 文件也不一定不是一成不变的。它可能会被注入渠道、用户自定义等标识,以及 SSR 所需要的首屏数据,从而改变其代码形式。因此,我们认为 html 文件的制品提供方应该是一个单独的动态服务,通过一些逻辑完成对模板 html 的替换并最终输出。

总结一下,在每次编译完成后,产物将会进行如下的整理以生成最终的前端制品:

  1. 针对静态文件,如 CSS、JS 等资源将会发布到云对象存储中,并以此为源站同步给 CDN 做访问速度优化。

  2. 针对 HTML 制品,需要一个直出服务做支撑,并打包成 docker 镜像,与后端的微服务镜像同等级别,供上游的流量分发服务(网关)根据用户请求选择调起哪些服务负载进行消费。

速度即效率,流水线优化思路

对于一个好的工具来说,内部设计可以很复杂,但对于使用者来说必须足够简单且好用。

在主干开发这样高频的持续集成下,集成速度即效率,流水线的执行时间毫无疑问是开发人员最关心的,也是流水线是否好用的决定性指标。我们可以从几个方面着手,提高流水线执行效率,减少开发人员的等待时间。

流水线任务编排

对流水线各个阶段需要执行的任务我们需要遵循一定的编排原则: 无前置的任务优先执行时间短的任务优先无关联的任务并行

根据这一原则,我们可以通过分析流水线中执行的各个任务,对每一个任务做一次最短路径依赖分析,最终得出该任务的最早执行时机。

巧用 Docker Cache

Docker 提供了这样一个特性:在 Docker 镜像的构建过程中,Dockerfile 的每一条可执行语句都会构建出一个新的镜像层,并缓存起来。在第二次构建时,Docker 会以镜像层为单位逐条检查自身的缓存,若命中相同镜像层,则直接复用该条缓存,使得多次重复构建的时间大大缩短。

我们可以利用 Docker 的这一特性,在流水线中减少通常会重复执行的步骤,从而提高 CI 的执行效率。

例如前端项目中通常最耗时的依赖安装 npm install,变更依赖项对于高频集成来说其实是一个较小概率的事件,因此我们可以在第一次构建时,将 node_modules这个文件夹打包成为镜像供下次编译时调用。Dockerfile 示例编写如下:

FROM node:12 AS dependencies
WORKDIR /ci
COPY . .
RUN npm install
ENV NODE_PATH=/ci/node_modules

我们给流水线增加一条检查缓存命中的策略:在下次编译之前,先查找是否有该镜像缓存存在。并且,为了保证本次构建的依赖没有更新,我们还必须比对本次构建与镜像缓存中的 package-lock.json文件的 md5 码是否一致。若不一致,则重新安装依赖并打包新镜像进行缓存。若比对结果一致,则从该镜像中直接取到 node_modules文件夹,从而省去大量依赖安装的时间。

流水线拉取镜像文件夹的方法示例如下,其中 --from 后跟的是之前缓存构建镜像的别名:

COPY --from=dependencies node_modules/ .# 其他步骤执行

同理,我们也可以将这一特性扩展到 CI 过程中所有更新频率不高,生成时间较长的任务中。例如 Linux 中环境依赖的安装、单元测试每条用例运行前的缓存、甚至是静态文件数量极多的文件夹的复制等等,都能利用 Docker cache 的特性达到几乎跳过步骤,减少集成时间的效果。由于原理大致相同,在此就不赘述了。

分级构建

众所周知,流水线的执行时间一定会随着任务数量的增多而变慢。大型项目中,随着各项指标计算的接入,各项测试用例的数量逐渐增多,运行时间迟早会达到我们难以忍受的地步。

但是,测试用例的数量一定程度上决定着我们项目的质量,质量检查决不能少。那么有没有一种方法既可以让项目质量得到持续保障的同时,减少开发者等待集成的时间呢?答案就是分级构建。

所谓分级构建,就是将 CI 流水线拆分为主构建和次级构建两类,其中主构建需要在每次提交代码时都要执行,并且若检查不通过无法进行下一步操作。而次级构建不会阻塞工作流,通过旁路的方式在代码合入后继续执行。但是,一旦次级构建验证失败,流水线将会立即发出通知告警,并阻塞其他所有代码的合入,直到该问题被修复为止。

对于某任务是否应放入次级构建过程,有如下几点原则:

  1. 次级构建将包含执行时间长(如超过 15 分钟)、耗费资源多的任务,如自动化测试中的 E2E 测试。

  2. 次级构建应当包含用例优先级低或者出错可能性低的任务,尽量不要包含重要链路。如果自动化测试中的一些测试用例经过实践发现失败次数较高,应当考虑增加相关功能单元测试,并移入主构建过程。

  3. 若次级构建仍然过长,可以考虑用合适的方法分割测试用例,并行测试。

结语

工欲善其事,必先利其器。腾讯文档项目高频稳定发布的背后,必定需要拥有强大基础设施的支持。

本篇文章仅主要介绍了持续集成阶段对项目进行的改造,持续部署、持续运营等阶段的具体改造思路将在笔者接下来的文章中详细说明。也欢迎大家多多探讨,对其中需要改进或有误的部分提出建议与斧正。

参考资料

  1. 《持续交付 2.0》—— 乔梁 著
  2. https://www.redhat.com/zh/topics/devops/what-is-ci-cd
  3. https://www.36kr.com/p/1218375440667012

关于我们

更多关于云原生的案例和知识,可关注同名【腾讯云原生】公众号~

福利:

①公众号后台回复【手册】,可获得《腾讯云原生路线图手册》&《腾讯云原生最佳实践》~

②公众号后台回复【系列】,可获得《15个系列100+篇超实用云原生原创干货合集》,包含Kubernetes 降本增效、K8s 性能优化实践、最佳实践等系列。

③公众号后台回复【白皮书】,可获得《腾讯云容器安全白皮书》&《降本之源-云原生成本管理白皮书v1.0》

【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!

大型前端项目 DevOps 沉思录 —— CI 篇的更多相关文章

  1. 前后端分离之前端项目构建(grunt+require+angular)

    前言 前段时间做了一个项目,前端开发页面,然后把代码给到后端同学,后端同学通过vm再来渲染页面.后来才发现,这种方式简直是太low了,因为前端代码在服务端同学那里,每次前端需要更改的时候都需要去到服务 ...

  2. 部署基于Gitlab+Docker+Rancher+Harbor的前端项目这一篇就够了

    部署基于Gitlab+Docker+Rancher+Harbor的前端项目这一篇就够了 安大虎 ​ momenta 中台开发工程师 6 人赞同了该文章 就目前的形势看,一家公司的运维体系不承载在 Do ...

  3. 基于云原生DevOps服务自动化部署前端项目学习总结

    本文主要以部署前端Vue项目为例,讲述了如何基于云原生DevOps服务自动化部署前端项目~从开发完成到线上环境,我们只需提交代码即可~ 一.引言 作为一名开发人员,日常工作中我们除了需要负责代码的开发 ...

  4. (转)BAT及各大互联网公司2014前端笔试面试题--Html,Css篇

    BAT及各大互联网公司2014前端笔试面试题--Html,Css篇   很多面试题是我自己面试BAT亲身经历碰到的.整理分享出来希望更多的前端er共同进步吧,不仅适用于求职者,对于巩固复习前端基础更是 ...

  5. 如何打造一个"逼格"的web前端项目

    最近利用空余的时间(坐公交车看教程视频),重新了解了前后端分离,前端工程化等概念学习,思考如何打造一个“逼格”的web前端项目. 前端准备篇 前端代码规范:制定前端开发代码规范文档. PS:重中之中, ...

  6. react 前端项目技术选型、开发工具、周边生态

    react 前端项目技术选型.开发工具.周边生态 声明:这不是一篇介绍 React 基础知识的文章,需要熟悉 React 相关知识 主架构:react, react-router, redux, re ...

  7. 结合docker发布前端项目(基于npm包管理)的shell脚本

    结合docker发布前端项目(基于npm包管理)的shell脚本 本教程依据个人理解并经过实际验证为正确,特此记录下来,权当笔记. 注:基于linux操作系统 目前主流的前后端分离的项目中,常常在部署 ...

  8. 蒲公英 · JELLY技术周刊 Vol.13 跟 VSCode 学习如何开发大型 IDE 项目

    开发一个 IDE 很难么?这或许是件很难的事情,但当我们参考 VSCode 的技术构架来看,整个开发流程就会平滑顺畅很多,从内核开发.代码编辑器.视图结构到插件系统,在这整个技术构架中我们可以看到很多 ...

  9. 手把手教你用SonarQube+Jenkins搭建--前端项目--代码质量管理平台 (Window系统)

    前言 网上教程大多介绍的是Linux系统下SonarQube+Jenkins如何使用,这是因为这两款软件一般都是部署在服务器上,而大多数服务器,采用的都是Linux系统.大多数服务器用Linux的原因 ...

随机推荐

  1. 单源最短路径算法:迪杰斯特拉 (Dijkstra) 算法(一)

    一.算法介绍 迪杰斯特拉算法(英语:Dijkstra's algorithm)由荷兰计算机科学家艾兹赫尔·迪杰斯特拉在1956年提出.迪杰斯特拉算法使用了广度优先搜索解决赋权有向图的单源最短路径问题. ...

  2. 深入理解和运用Pandas的GroupBy机制——理解篇

    GroupBy是Pandas提供的强大的数据聚合处理机制,可以对大量级的多维数据进行透视,同时GroupBy还提供强大的apply函数,使得在多维数据中应用复杂函数得到复杂结果成为可能(这也是个人认为 ...

  3. 求1+2+3...+n 牛客网 剑指Offer

    求1+2+3...+n 牛客网 剑指Offer 题目描述 求1+2+3+...+n,要求不能使用乘除法.for.while.if.else.switch.case等关键字及条件判断语句(A?B:C). ...

  4. Bzoj P2054 疯狂的馒头 | 并查集

    题目链接 思路:因为每次染色都会将某些馒头的颜色彻底更改,所以每个馒头的最终的颜色其实是由最后一次染色决定的,那么我们只考虑最后一次染色即可.对此,我们可以从后往前倒着染色,当目前的染色区间中存在白色 ...

  5. hdu 2190 重建希望小学(数学,递推)

    题意: N*3的教室,有2种砖,2*2.1*1. 问铺设教室的方案有多少种.(要铺满) 思路: 画一下图可以很快发现递推公式 代码: int main(){ int a[35]; mem(a,0); ...

  6. docker+nginx搭建tomcat集群(附录)——nginx.conf文件

    附录:nginx.conf修改后的文件内容 user root;worker_processes 2; #error_log logs/error.log;#error_log logs/error. ...

  7. 【数据结构&算法】02-复杂度分析之执行效率和资源消耗

    目录 前言 复杂度 分析方法 大 O 复杂度表示法 例子-评估累加和的各种算法执行效率 算法 1(for 循环): 算法 2(嵌套 for 循环): 大 O 表示 时间复杂度分析 关注执行最多的一段代 ...

  8. 【Go语言学习笔记】Go语言的基础语法

    上一篇已经说了,Go的语法和C的很接近,直接看看异同即可. 变量 变量名还是一样,字母或下划线开头,区分大小写.不能是关键字. Go定义了int32和int64这种类型来显示声明大小,和C里面的sho ...

  9. k8s入坑之路(14)scheduler调度 kubelet管理及健康检查 更新策略

    kubelet 主要功能 Pod 管理 在 kubernetes 的设计中,最基本的管理单位是 pod,而不是 container.pod 是 kubernetes 在容器上的一层封装,由一组运行在同 ...

  10. robot_framewok自动化测试--(1)Robot Framework 环境搭建及常见日志问题解决办法

    一.Robot Framework 介绍 Robot Framework 的架构是一个通用的验收测试和验收测试驱动开发的自动化测试框架(ATDD).它具有易于使用的表格来组织测试过程和测试数据. 它使 ...