乐趣区

关于devops:4000字深度总结Pipeline五大性能实践招招制敌

本文来自:
毛超 极狐(GitLab) 研发工程师

太长不读版

你将看到「Pipeline 运行很慢」的问题剖析和解决思路,并理解极狐 GitLab CI/CD 解决该问题的最佳实际。

– END

Hold on, hold on! 精彩才刚开始,且听我缓缓道来~


一千个人的心中,就有一千种 DevOps。从 2009 年 DevOps 概念的衰亡到明天,衍生出各种各样的工具、标准、流程、准则,足以让人目迷五色。

作为软件工程师,我习惯探索一种技术或者概念的外围逻辑,在我看来,DevOps 的外围逻辑就是通过继续集成和继续交付扭转团队交付软件的形式 ,所以 继续集成 继续交付 就天然成为 DevOps 的两大外围能力,在各种 DevOps 的书籍中也有着重要的篇幅。

极狐 GitLab 作为 一体化 DevOps 平台,提供了丰盛且弱小的 CI/CD 性能 ,然而依据我在极狐 GitLab 用户微信群中察看到的状况看,仍然有研发团队在实际 CI/CD 过程中,呈现「现实很现实,事实很事实」 的困境,碰到各种各样的问题。

明天我就来分享下 极狐 GitLab CI/CD 解决典型问题的最佳实际,心愿能抛砖引玉,给更多研发团队提供帮忙。(下文中的微信群聊都曾经过批改,请勿对号入座)

这些问题你遇上过吗?

从下面的群聊中能够看出,大家的 Pipeline 运行的都不快,甚至能够说很慢。如果你也有上述相似的问题,首先要恭喜你,因为只有保持实际 CI/CD 的团队,才会碰到这种「苦涩的懊恼」。Pipeline 在日常开发中表演重要角色,一旦呈现性能问题,会极大地影响团队效率,必须加以器重。我置信有些简略的解决思路,例如「性能不够,机器来凑」,惋惜这种有钱任性的形式不肯定可能解决问题。

作为软件工程师,碰到性能问题,正确的做法应该是查看运行 Log 确定性能瓶颈,剖析具体性能问题,最初再进行相应的优化。所以,首先要做的就是查看 Pipeline 的运行 Log,明确每一个 Job 的运行状况,能力定位出某些有性能问题的 Job 并给出解决方案。上面我将分享解决 Pipeline 性能问题的一些最佳实际,供参考~

最佳实际分享

实际一:优化下载内部依赖

一般来说,Pipeline 在运行过程中都会从内部获取依赖,比方拉取我的项目的依赖包,Java 用 Maven/Gradle,Ruby 用 bundler,NodeJS 用 npm/yarn 等,这时咱们就能够在 .gitlab-ci.yml 文件(Pipeline 的定义文件)中应用 cache 关键字,将内部依赖包缓存起来,这样就防止了每次反复下载的动作,能够节俭不少工夫。cache 的应用也很简略,相似上面的代码:

cache-job:
  script:
    - echo "This job creates a cache."
  cache:
    key: third_party_dependency
    paths:
      - vendor/ruby
      - node_modules

在下面的 Job 中,cache-job 缓存了 cache:paths 中指定目录 vendor/ruby 和 node_modules 外面的内部依赖包,cache:key 指定了缓存的惟一标识。这样在 cache-job 运行完结后,会将 cache:paths 中的所有文件打包上传到极狐 GitLab 配置的 cache 地址中,当 Pipeline 再次运行时,会主动下载 cache 中的文件,并存放在 cache:paths,这样就能够防止反复下载。极狐 GitLab 有更多的 cache 用法,具体应用办法请查看文档 .gitlab-ci.yml 参考。

另外在 docker 大行其道的明天,有不少团队都会应用 docker 来对立运行环境,极狐 GitLab CI/CD 也反对在 Job 中应用 docker,pull docker image 也可能成为性能瓶颈。

如果极狐 GitLab Runner 和极狐 GitLab 在同一个内网中,那么能够应用极狐 GitLab 的 Container Registry 性能,将团队罕用的 docker image 上传到极狐 GitLab 中,这样 Pipeline 在下载 docker image 时,Runner 会间接从内网下载 docker image,速度会大幅度晋升,具体应用办法请查看文档 GitLab Container Registry。

build_image:
  image: docker:20.10.10
  stage: build-image
  services:
    - docker:20.10.10-dind
  script:
    - echo "Build app"
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -t $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest

下面的 Job 演示了 build image 并 push 到极狐 GitLab Container Registry 的过程,应用的变量是极狐 GitLab 预约义的变量 $CI_REGISTRY_USER,具体介绍请查看文档 预约义变量参考。

实际二:选择性的运行 Job

在我的项目的日常开发中,很多动作都会触发 Pipeline 运行,比方「性能分支代码更新」,「骨干分支代码更新」,「定时运行的性能测试」,而在默认状况下,每次 Pipeline 运行时都会执行所有的 Job,但聪慧的你兴许曾经留意到,下面不同场景中,不是每个 Job 都须要执行。

lint-js: ... # 前端代码动态查看
lint-java: ... # 后端代码动态查看

test-js: ... # 前端测试
test-java: ... # 后端测试

build-package: ... # 打包

performance-test: ... # 性能测试,运行工夫较长

deploy-to-qa: ... # 部署到测试环境
deploy-to-production: ... # 部署到产品环境

举个例子,在一个典型的我的项目中,可能会有下面的 Job,别离实现 动态查看,测试,打包,部署 等工作。在日常开发过程中,当「性能分支代码更新」时,不须要在 Pipeline 中执行 打包、性能测试、部署 等一系列的 Job,只须要关注 动态查看 和 测试 Job 即可,其余 Job 能够在骨干分支的 Pipeline 运行。

而当性能分支合并之后,在「骨干分支代码更新」时,再启用 打包、性能测试、部署 等工作,保障骨干分支分支的实现 测试,打包,部署 等工作。对于 性能测试 Job,处于老本思考,能够抉择不追随每次 Pipeline 运行时执行,设置成定期运行(比方一天运行一次)即可。也就是说,在不同的场景中,咱们能够选择性的运行某些 Job,疏忽另外一些,进步 Pipeline 运行效率。

要实现上述性能,须要应用极狐 GitLab 提供的 rules 关键词,能够定义 Job 适合运行或者不运行,最大限度的管制 Pipeline。

build-package:
  stage: build
  rules: # 骨干分支的 pipeline 才会运行 build-package job
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

在下面的例子中,咱们通过设置 rules:if 配合应用极狐 GitLab Pipeline 的预约义变量,让 build-package Job 只运行在骨干分支的 Pipeline 中。

lint-js:
  rules:
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      changes: # js 改变 + 性能分支 才运行 lint
        paths:
          - src/javascripts
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
          # 骨干分支须要始终运行 lint,保证质量

对于 lint-js Job 则有两个判断规定,如果不是骨干分支(是性能分支)并且改变了 src/javascript 文件夹下的内容,就运行该 Job。另外如果是骨干分支也会运行。通过这种设置,既兼顾了性能分支上的运行效率,也能保障骨干分支的代码品质,岂不美哉。下面给出的优化策略只是简略的例子,各位读者能够依据我的项目中的理论状况进行调整。具体 rules 具体介绍请查看文档 指定作业何时应用 rules 运行。

实际三:调整 Job 运行程序

在默认状况下,Pipeline 中的 Job 是依照 stage 的程序执行的,假如在 Pipeline 中有 lint 和 test 这两个 stage,那么 test stage 中的 Job 要期待所有 lint stage 的 Job 执行实现后能力开始,这看起来没什么问题,不过仍然有优化的空间。

举个例子,在 Pipeline 别离对前端和后端代码做 lint 和 test,那么 lint_frontend Job 执行完后能够间接开始执行 test_frontend Job,不用期待 lint stage 中的 lint_backend 执行完,相似下图。

在极狐 GitLab 中,这种 Pipeline 叫做「有向无环流水线 DAG Pipeline」,它可能冲破一般 Pipeline 中前序步骤 (stage) 所有工作执行完后,能力执行后续步骤 (stage) 中工作的限度。以 Job 为粒度进行跨 stage 的关联建设,升高工作等待时间的节约,晋升 Pipeline 的执行效率。

lint_frontend:
  stage: lint
  script: echo "Lint JS ..."

lint_backend:
  stage: lint
  script: echo "Lint Java ..."

test_frontend:
  stage: test
  needs: ["lint_frontend"]
  script: echo "Running JS Test ..."

test_backend:
  stage: test
  needs: ["lint_backend"]
  script: echo "Running Java Test ..."

下面的代码通过 need 关键字实现 DAG Pipeline,能够看到 test_frontend 依赖 lint_frontend,在 lint_frontend job 完结之后就间接开始运行 test_frontend,不用期待所有 lint stage 都实现。在 「生成多平台制品」「单仓库多模块微服务利用」 场景中十分适合,具体请查看文档 有向无环图流水线。

实际四:并行运行 Job

如果某些 Job 的运行工夫比拟长,该如何优化呢?比方测试 Job,不论是单元测试还是集成测试,尽管运行单个测试速度很快,但数量积攒到肯定水平之后,往往意味着须要较长的运行工夫(当然我也见过十年零碎没有一个自动化测试的,我只能祝它好运)。这种状况下能够应用 parallel 关键字,将运行工夫较长的 Job 拆分成多个并行运行的 Job,能够大幅度晋升 Job 运行效率

不同的技术栈有不同的办法来实现并行化测试,我这里举一个 ruby 的例子,上面 test Job 被拆分成 3 个独立的 Job 运行,每个 Job 在运行测试时,会应用 semaphore_test_boosters,承受参数 $CI_NODE_INDEX/$CI_NODE_TOTAL,这是每个独立的并行 Job 的标识。当然并行化测试并不是一个简略的设置,还须要思考给并行测试 Job 调配对应的数据库,缓存等组件,能力真正保障测试并行运行。另外还有一个前提须要满足,给极狐 GitLab 配置多个 runner,否则有再多的 Job 只有一个 runner,那 Job 仍然是程序执行的。具体能够请查看文档 Plan and operate a fleet of shared runners。

test:
  parallel: 3
  script:
    - bundle exec rspec_booster --job $CI_NODE_INDEX/$CI_NODE_TOTAL

除此之外,在进行并行化测试时,有一个十分乏味的问题,如何把运行工夫不同的测试正当调配到不同的 Job 中。在极狐 GitLab 我的项目中,咱们应用 knapsack 进行测试工作的划分,保障测试运行的总体工夫最优,感兴趣的读者能够具体理解。

实际五:拆分简单的单条 Pipeline 为多条 Pipeline

如果应用了下面的办法,仍然无奈将 Pipeline 优化到一个称心的后果,那可能不是 Job 不够快,而是 Job 切实太多了。

这里我倡议能够思考将以后 Pipeline 进行拆分,拆分 Pipeline 指的是把本来在一条 Pipeline 运行的工作,拆分成多个 Pipeline 程序执行。拆分 Pipeline 和重构代码形式十分相似,参考「高内聚、低耦合」准则,依照 stage 进行分类,把关联比拟严密的 stage 内聚成一个 Pipeline,关联较弱的 stage 就离开,这样就能失去多个 Pipeline 了。

将一个简单的 Pipeline 转换成多个 Pipeline 后,能够利用极狐 GitLab trigger 关键字 将多个 Pipeline 连接起来,保障上游 Pipeline 运行完结后触发上游 Pipeline 的运行。在极狐 GitLab 中,有「父子 Pipeline」和「多我的项目 Pipeline」两种模式。父子 Pipeline 能够在同一我的项目中启用多条 pipeline,而多我的项目 Pipeline 会把不同我的项目的 Pipeline 关联起来。实质上讲,这两种形式都是在升高单条 Pipeline 的复杂度,晋升单条 Pipeline 的执行效率

比方上面的例子中,咱们把 lint,test,build-image 放到主 Pipeline 中,将 performance-test,integration-test,deploy stage 放到子 Pipeline 中,这样做即能够保障主 Pipeline 的执行效率,肯定水平上也减少了 Pipeline 的成功率。当主 Pipeline 执行实现后,能够用 trigger:include 触发同一我的项目的子 Pipeline,或者用 trigger:project 触发拆分到其余 project 中的 Pipeline。整体来讲 Pipeline 执行的 Job 没有缩小,只是分阶段执行了而已。具体介绍请查看文档 上游流水线。

# 原有 .gitlab-ci.yml
stages:
  - lint
  - test
  - build-image
  - integration-test
 - performance-test
  - deploy

# 拆分后的主 pipeline .gitlab-ci.yml
stages:
  - lint
  - test
  - build-image
 - downstream-pipeline

# 场景一:触发以后我的项目中的子 pipeline
trigger-child-pipeline:
  stage: downstream-pipeline
  trigger:
    include: path/to/child-pipeline.gitlab-ci.yml

# 场景二:触发其余我的项目中的 pipeline
trigger-multi-project-pipeline:
 stage: downstream-pipeline
  trigger:
    project: my-group/downstream-project

总结

看到下面五种实际,你是不是曾经一拍大腿筹备开干了?先别着急,再听我说两句。每个我的项目的状况都有所不同,不能生吞活剥到本人的我的项目中,还是须要依据理论状况进行剖析和尝试,能力找出最优解。

另一方面,下面五种实际「施行老本」和「预期收益」截然不同,所以我总结了上面的表格,你能够从你心中的最高性价比实际开始尝试,循序渐进的优化你的 Pipeline。

团队在实际 DevOps 的过程中,必定不会一帆风顺,须要继续投入资源,针对问题做度量,剖析,施行,能力真正确保实际落地扭转团队

想要把 CI/CD 最佳实际讲透彻,一篇文章必定是不够的,在前面的文章中,我会持续探讨相似 「Pipeline 不够强壮常常失败」「应用 Pipeline 做平安方面工作」 等问题,尽量笼罩 CI/CD 的常见问题,分享极狐 GitLab CI/CD 更多最佳实际,欢送大家多多关注。

退出移动版