# CI/CD development documentation

> 原文：[https://docs.gitlab.com/ee/development/cicd/](https://docs.gitlab.com/ee/development/cicd/)

*   [CI Architecture overview](#ci-architecture-overview)
*   [Job scheduling](#job-scheduling)
    *   [Communication between Runner and GitLab server](#communication-between-runner-and-gitlab-server)
    *   [`Ci::RegisterJobService`](#ciregisterjobservice)

# CI/CD development documentation[](#cicd-development-documentation "Permalink")

此处列出了特定于 CI / CD 的开发指南.

如果要创建新的 CI / CD 模板，请阅读[GitLab CI / CD 模板的开发指南](templates.html) .

## CI Architecture overview[](#ci-architecture-overview "Permalink")

以下是 CI 体系结构的简化图. 为了集中在主要组件上，省略了一些细节.

[![CI software architecture](img/8069500bf085102224933129529dfa12.png)](img/ci_architecture.png)

在左侧，我们有一些事件可以根据各种事件触发管道（由用户或自动化触发）：

*   `git push`是触发管道的最常见事件.
*   The [Web API](../../api/pipelines.html#create-a-new-pipeline).
*   用户单击 UI 中的"运行管道"按钮.
*   [创建或更新合并请求时](../../ci/merge_request_pipelines/index.html#pipelines-for-merge-requests) .
*   将 MR 添加到[合并列车时](../../ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.html#merge-trains-premium) .
*   A [scheduled pipeline](../../ci/pipelines/schedules.html#pipeline-schedules).
*   当项目被[订阅到上游项目时](../../ci/multi_project_pipelines.html#trigger-a-pipeline-when-an-upstream-project-is-rebuilt) .
*   启用[自动 DevOps 时](../../topics/autodevops/index.html) .
*   当 GitHub 集成用于[外部请求请求时](../../ci/ci_cd_for_external_repos/index.html#pipelines-for-external-pull-requests) .
*   当上游管道包含[桥接作业时](../../ci/yaml/README.html#trigger) ，该[作业](../../ci/yaml/README.html#trigger)会触发下游管道.

触发任何这些事件将调用[`CreatePipelineService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/ci/create_pipeline_service.rb) ，后者将输入事件数据并触发用户，然后尝试创建管道.

`CreatePipelineService`很大程度上依赖于[`YAML Processor`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/yaml_processor.rb)组件，该组件负责将 YAML Blob 作为输入并返回管道的抽象数据结构（包括阶段和所有作业）. 该组件还可以在处理 YAML 时验证其结构，并返回任何语法或语义错误. 在`YAML Processor`组件中，我们定义了[所有](../../ci/yaml/README.html)可用于构建管道[的关键字](../../ci/yaml/README.html) .

`CreatePipelineService`接收`YAML Processor`返回的抽象数据结构，然后将其转换为持久化模型（管道，阶段，作业等）. 之后，就可以处理管道了. 处理管道意味着按执行顺序（阶段或 DAG）运行作业，直到以下任一情况为止：

*   所有预期的作业均已执行.
*   故障会中断管道执行.

处理管道的组件是[`ProcessPipelineService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/ci/process_pipeline_service.rb) ，它负责将所有管道的作业移至完成状态. 创建管道时，其所有作业最初都处于`created`状态. 该服务根据流水线结构查看在`created`阶段可以处理哪些作业. 然后，它们将它们移到`pending`状态，这意味着它们现在[可以被 Runner 拾取](#job-scheduling) . 执行作业后，它可以成功完成或失败. 管道中作业的每个状态转换都会再次触发此服务，该服务会寻找下一个要转换为完成的作业. 在此过程中， `ProcessPipelineService`更新作业，阶段和整个管道的状态.

在图的右侧，我们有一个列表[运动员](../../ci/runners/README.html#configuring-gitlab-runners)连接到 GitLab 实例. 这些可以是共享运行者，组运行者或项目特定的运行者. Runners 与 Rails 服务器之间的通信通过一组 API 端点（称为`Runner API Gateway` .

我们可以注册，删除和验证运行器，这也将导致对数据库的读/写查询. 连接了 Runner 之后，它会继续询问要执行的下一个作业. 这将调用[`RegisterJobService`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/services/ci/register_job_service.rb) ，后者将选择下一个作业并将其分配给 Runner. 此时，作业将转换为`running`状态，由于状态更改，该状态再次触发`ProcessPipelineService` . 有关更多详细信息，请参阅" [作业调度"](#job-scheduling) .

在执行作业时，运行程序将日志以及任何可能需要存储的工件发送回服务器. 此外，作业可能依赖于先前作业中的工件才能运行. 在这种情况下，Runner 将使用专用的 API 端点下载它们.

工件存储在对象存储中，而元数据保留在数据库中. 工件的重要示例是报表（JUnit，SAST，DAST 等），这些报表在合并请求中进行了解析和呈现.

作业状态转换并非全部自动化. 用户可以运行[手动作业](../../ci/yaml/README.html#whenmanual) ，取消管道，重试特定的失败作业或整个管道. 导致作业更改状态的任何事件都将触发`ProcessPipelineService` ，因为它负责跟踪整个管道的状态.

一种特殊类型的作业是[桥接作业](../../ci/yaml/README.html#trigger) ，当过渡到`pending`状态时，该[作业](../../ci/yaml/README.html#trigger)在服务器端执行. 这项工作负责创建下游管道，例如多项目或子管道. 每次触发下游管道时，工作流程循环都将从`CreatePipelineService`重新开始.

## Job scheduling[](#job-scheduling "Permalink")

创建管道时，将为所有阶段一次创建所有作业，初始状态为`created` . 这使得可视化管道的全部内容成为可能.

跑步者将不会看到具有`created`状态的作业. 为了能够将作业分配给 Runner，该作业必须首先转换为`pending`状态，这在以下情况下可能发生：

1.  作业是在管道的第一阶段创建的.
2.  该作业需要手动启动，并且已被触发.
3.  前一阶段的所有作业均已成功完成. 在这种情况下，我们将所有工作从下一阶段过渡到`pending` .
4.  该作业使用`needs:`指定了 DAG 依赖项`needs:`并且所有依赖项都已完成.

连接了 Runner 时，它将通过连续轮询服务器来请求下一个`pending`作业运行.

**注意：** Runner 用于与 GitLab 交互的 API 端点在[`lib/api/runner.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/runner.rb)中定义

服务器收到请求后，将根据[`Ci::RegisterJobService`算法](#ciregisterjobservice)选择`pending`作业，然后将其分配并发送给 Runner.

在当前阶段完成所有作业后，服务器通过将其状态更改为" `pending` "，从下一阶段"解锁"所有作业. 现在，当 Runner 请求新作业时，可以由调度算法选择这些内容，并像这样继续进行，直到完成所有阶段.

### Communication between Runner and GitLab server[](#communication-between-runner-and-gitlab-server "Permalink")

使用注册令牌[注册](https://docs.gitlab.com/runner/register/)了 Runner 之后，服务器便知道其可以执行的作业类型. 这取决于：

*   它注册的赛跑者类型为：
    *   共享跑步者
    *   团体赛跑者
    *   项目特定的跑步者
*   任何关联的标签.

跑步者通过请求作业执行`POST /api/v4/jobs/request`来启动通信. 尽管轮询通常每隔几秒钟发生一次，但如果作业队列不变，我们将通过 HTTP 标头利用缓存来减少服务器端的工作量.

该 API 端点运行[`Ci::RegisterJobService`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/services/ci/register_job_service.rb) ，该命令：

1.  从`pending`作业池中选择要运行的下一个作业
2.  分配给跑步者
3.  通过 API 响应将其呈现给 Runner

### `Ci::RegisterJobService`[](#ciregisterjobservice "Permalink")

此服务使用 3 个顶级查询来收集大多数作业，并且根据 Runner 注册到的级别选择它们：

*   选择共享的 Runner（实例级别）的作业
*   选择组级别运行器的作业
*   选择项目亚军的工作

This list of jobs is then filtered further by matching tags between job and Runner tags.

**注意：**如果作业包含标签，则与**所有**标签都不匹配的跑步者将不会选择该作业. 跑步者可能具有比该工作定义的标签更多的标签，但反之则没有.

最后，如果 Runner 仅能选择带标签的作业，则所有未带标签的作业都会被过滤掉.

在这一点上，我们遍历剩余的`pending`作业，然后尝试根据其他策略分配"可以选择" Runner 可以选择的第一个作业. 例如，标记为`protected`运行者只能选择针对受保护的分支（例如生产部署）运行的作业.

当我们增加池中的"奔跑者"数量时，如果将同一工作分配给不同"奔跑者"，也会增加发生冲突的机会. 为防止这种情况，我们会适当地挽救冲突错误并在列表中分配下一个作业.