GitLab CI/CD 从入门到实践:Runner架构与流水线配置详解

发布时间:2026/10/23 18:15:47
GitLab CI/CD 从入门到实践:Runner架构与流水线配置详解 GitLab CI/CD 这支“开关”没打开之前很多人以为持续集成是 Jenkins 的专利或者觉得搭建一套流水线需要运维同学单独部署一套平台。实际上GitLab 从 8.0 开始就把 CI/CD 能力直接内建在代码仓库里你不需要额外安装什么 Web 服务只要在项目根目录放一个.gitlab-ci.yml文件剩下的任务调度、状态回传、日志展示、产物归档全部由 GitLab 实例自己消化。作为一个写代码的人最直接的感受就是代码提交、测试、构建、部署这些动作第一次和版本管理长在了一起。这篇是“CICD详解”系列第九篇我把 GitLab 从零到一的使用路径掰开揉碎讲一遍。内容面对的是那些听说过 GitLab CI/CD 但没真正跑通过一条流水线的读者重点放在“简单使用”但又不至于变成纯点点点的操作。我会从 Runner 的架构认知开始讲到.gitlab-ci.yml的最小配置再到串并行调度、产物传递、私有化部署场景下的常见坑全部是基于我在真实项目里跑流水线的理解不是文档搬家的那种写法。1. 先想清楚一件事GitLab 的流水线到底跑在哪里1.1 代码仓库不是执行器很多第一次接触 GitLab CI/CD 的人会下意识认为流水线的执行发生在 GitLab 服务本身.gitlab-ci.yml里写了script之后GitLab 后端就会跑这些命令。这个认知是错的。GitLab 的设计里有一个核心角色叫Runner它是一个独立于 GitLab 服务之外的程序可以装在自己的服务器上、装在开发机上、装在 Kubernetes 集群里甚至装在你的笔记本电脑上。Runner 负责真正执行.gitlab-ci.yml里定义的作业。GitLab 服务端只负责调度和展示你提交代码后GitLab 会解析仓库里的配置文件判断哪些 job 需要跑、按什么顺序跑、是否有依赖关系然后把一个个 job 分配给当前可用的 Runner。Runner 收到任务后在执行环境里运行具体的命令再把日志和状态传回 GitLab。这个“服务端调度 Runner 执行”的分离设计是理解 GitLab CI/CD 一切行为的基础。为什么你的流水线偶尔卡在 pending大概率不是 GitLab 挂了而是没有可用 Runner 来接任务。为什么同一个 job 在不同时刻跑出来的产物可能不一致因为两次可能被分给了不同环境的 Runner。想清楚这一点后面很多疑问都会迎刃而解。1.2 三种 Runner 层级和两种执行器逻辑Runner 在 GitLab 里有三种注册层级Shared Runner注册在 GitLab 实例级别实例上的所有项目都能使用。适合做通用构建资源池。Group Runner注册在某个用户组下组内项目共享。这是多数中型团队推荐的层级既隔离了团队资源又避免了每个项目单独维护 Runner。Project Runner注册在单个项目下只有该项目专属使用。适合有特殊环境要求、不希望被其他项目挤占资源的场景。层级选择的逻辑并不复杂共享越广资源利用率越高但环境差异和排队概率也越大隔离越细可控性越好但维护成本直线上升。我自己在项目里的习惯是通用 Java 构建任务放 Group Runner涉及特殊硬件或者固定测试环境的时候才用 Project Runner。Runner 还有一个维度是执行器也就是它用什么方式来跑 job。最常用的三种执行器运行方式适合场景缺点shell直接在 Runner 主机的 shell 里执行命令构建流程简单、环境依赖少环境隔离差多个 job 可能互相污染docker每次启动一个新容器来执行 job绝大多数标准构建/测试任务需要 Runner 主机支持 Docker镜像拉取耗时kubernetes动态创建 Pod 执行大规模、弹性调度需求配置复杂资源掌控要求高简单使用阶段我强烈建议直接上docker executor。原因不复杂每次 job 都是干净环境以同一个基础镜像为起点命令执行完容器销毁不会在 Runner 主机上积累垃圾依赖Jobs 之间互不干扰。这也是后面我们将讲到的一个注册命令里面带一堆参数的原因。2. 从注册 Runner 到构建机准备跑通第一条流水线的完整链路2.1 Runner 安装和注册的关键动作安装 Runner 最简单的方式是直接用官方提供的二进制包。以 Debian/Ubuntu 为例curl -L https://packages.gitlab.com/install/repositories/gitlab/gitlab-runner/script.deb.sh | sudo bash sudo apt-get install gitlab-runner安装完之后需要把 Runner 注册到 GitLab 实例。注册时需要用到两个关键参数GitLab 实例的 URL 和项目或实例/组的注册令牌。项目里可以在Settings → CI/CD → Runners页面看到这些信息。sudo gitlab-runner register \ --url https://gitlab.example.com \ --token glrt-xxxx \ --executor docker \ --docker-image alpine:latest \ --description docker-runner-01 \ --docker-privileged false这里逐个说明参数含义--url你的 GitLab 服务地址。私有化部署的朋友最容易在这写错注意不要带/v4、不要带项目路径只要域名根地址。--token注册令牌。Project Runner 是项目专属的Group Runner 是组内共用的权限范围差距大要如实填写。--executor docker执行器类型。这里选 docker 意味着每一个 job 都会在容器中执行。--docker-image默认镜像job 里如果没有显式指定 image就使用这个。--docker-privileged false非特权模式安全一些。只有当你明确要用 Docker-in-Docker 场景时才需要开 true。注册成功之后在配置管理页面能看到 Runner 显示为绿色可用状态。记住这条命令它是你整个 GitLab CI/CD 使用生涯里最重要的一步后面出问题也大都在这里埋下伏笔。2.2 Runner 的并发和并发安全的真相Runner 注册完成只是第一步。接下来一个容易被忽略的参数是config.toml里的concurrent设置。默认情况下一个 Runner 同一时间只能执行一个 job。如果团队中小伙伴同时提交代码、触发多个流水线后面的 job 全部排队。这个排队不是我们希望的“按顺序来”而是纯粹的阻塞——第一个任务跑完之前其他任务连容器都不会被创建。/etc/gitlab-runner/config.toml里既有全局的并发数也有每个 Runner 自己的并发数concurrent 4 [[runners]] name docker-runner-01 limit 2这里的经验是不要盲目把并发数调到很大。docker executor 每个 job 会拉镜像、起容器、跑命令都消耗宿主机 CPU 和内存。我在某次内部压测中发现一台 4 核 8G 的机器Runner 并发调到 6 以后很容易出现容器启动超时和 OOM。更稳妥的做法是先按 CPU 核心数的一半起步观察一段时间再慢慢加。如果你只是想“简单使用”GitLab CI/CD在团队规模不超过 10 人时一个 2 并发的 Docker Runner 完全够用。等出现排队再考虑你需不需要加机器而不是憋着调并发。2.3 验证 Runner 可用性的关键操作Runner 注册完成后最直接的一次验证是用一个最简单的 job 来测试。在项目根目录创建.gitlab-ci.ymlstages: - test smoke-test: stage: test script: - echo hello gitlab ci - python3 --version提交推送到远端。正常情况下在项目的CI/CD → Jobs页面能看到这条 job 被 Runner 领取状态从 pending 变成 running最后 success。如果一直停在 pending先回 Runner 页面看状态是否为绿色再检查 Runner 主机是否能连通 GitLab 实例。这里的连通指的不只是 ICMP 层面的 ping而是 Runner 的 HTTP 请求能不能到达 GitLab 的 API 端点。很多时报错都是因为网络策略阻断了连接尤其在私有化部署环境里。这一条流水线跑通意味着整个环境链路已经打通了。接下来才有资格讨论.gitlab-ci.yml里怎么写、怎么写好。3. 一次性吃透 .gitlab-ci.yml从最小可运行配置到常用关键字3.1 最小的骨架和 job 概念.gitlab-ci.yml是 GitLab CI/CD 的配置中心。它的核心概念是job。job 是你希望流水线执行的一个单元一个 job 至少包含一个script段。多个 job 组合起来就是整个流水线job 之间可以用stages来安排顺序。一个稍微接近真实的骨架stages: - build - test - deploy variables: APP_NAME: demo-app BUILD_ENV: production cache: paths: - node_modules/ compile-job: stage: build image: node:18 script: - npm ci - npm run build artifacts: paths: - dist/ expire_in: 7 days tags: - docker unit-test: stage: test image: node:18 script: - npm run test dependencies: - compile-job先解释stages。它定义了流水线的阶段顺序默认情况下同阶段内的 job 可以并行执行不同阶段之间是串行的。前面阶段全部成功后后面阶段才会开始。如果某一条 job 失败默认行为是后面的阶段不会执行。这就是 GitLab CI/CD 作为一条流水线的基本门槛逻辑。再说variables。变量可以在全局定义也可以在 job 内部重写甚至可以在脚本里动态定义并传递。变量的作用域优先级是job 内定义 全局定义 预定义变量。这个优先级的记忆方式很直白——离 job 越近的定义越权威。3.2 预定义变量流水线给你的免费礼物GitLab 提供了一套非常丰富的预定义变量这些变量在每一个 job 里天然可用不需要你手动设置变量含义CI_COMMIT_SHA当前流水线对应的 commit SHACI_COMMIT_REF_NAME分支名或 tag 名CI_PROJECT_NAME项目名CI_PROJECT_DIR项目在 Runner 上的路径CI_JOB_NAME当前 job 名称CI_JOB_TOKEN当前 job 的临时令牌用于 GitLab API 鉴权CI_PIPELINE_ID流水线唯一 IDCI_REGISTRY_USER/CI_REGISTRY_PASSWORD用于往 GitLab Container Registry 推送镜像我最常用的是$CI_COMMIT_SHA比如在部署脚本里会把当前 commit 写入远程服务器的版本文件而$CI_JOB_TOKEN用在需要调用 GitLab API 的场景比如自动创建 MR 或者给某个 commit 打标签。使用预定义变量时注意一个细节在script里使用直接用$VAR就行但如果把变量塞进某些程序的命令行参数最好用双引号包住避免变量内含空格时被 shell 拆词。3.3 image、artifacts、cache 这三个关键字必须分清楚初学者最容易混的是cache和artifacts二者都涉及文件的保存和传递但作用完全不同。artifacts是 job 的产物。它会把指定路径的文件打包上传到 GitLab作为后续 job 的输入。比如构建 job 产出的dist/目录测试 job 依赖它来跑埋点校验。artifacts按 job 和流水线维度保存可以设定过期时间。cache是构建依赖的缓存用来加速。比如前端项目里的node_modules/如果每次 npm ci 都从零装依赖会非常慢用 cache 机制可以把 node_modules 缓存下来下次直接复用。cache 是按 key 来区分的默认情况下每个 job 有独立的 cache也可以自己指定cache:key来控制共享范围。用一句话总结记忆artifacts 是“我要交给下一个 job 的东西”cache 是“我为了省时间想保留的东西”。前者必须存在后者只是优化手段。image关键字则指定 job 运行时的容器镜像。docker executor 模式下每个 job 会基于这个镜像创建容器。没有写image的 job 使用 Runner 注册时的默认镜像如果用 shell executorimage关键字无效命令直接在宿主机执行。这里再给你们一个真实经验的提醒dependencies关键字可以控制某个 job 依赖哪个上游 job 的 artifacts。默认情况下某条 job 会自动下载所有前面阶段产生的 artifacts条件允许你尽可能指定dependencies这样不但能减少数据传递的时间还能避免不同 job 之间因为下载了不必要的产物而产生意外冲突。3.4 rules 和 only/except什么时候跑、什么时候不跑控制 job 的触发条件是流水线里最有意思的部分。旧版本里大家习惯用only/except现在新版推荐用rules它更灵活支持条件组合。简单的例子deploy: stage: deploy script: - ./deploy.sh rules: - if: $CI_COMMIT_BRANCH main when: manual - if: $CI_COMMIT_TAG这个规则的意思是当提交发生在 main 分支时deploy job 会进入等待状态需要人工点击才执行当提交方式是打 tag 时deploy job 自动执行。二者都不满足的提交不会进入这个 job。rules的判断顺序是从上往下一个一个匹配第一个命中的规则生效。所以写规则时要把最具体的条件放在前面避免被后面的宽泛条件提前吞掉。这是一个极易踩坑的地方我见过不少同事把规则顺序搞反结果所有分支都触发了生产部署。4. 流水线里常见的三种作业调度模式全并行、串行茶馆和带人工卡点的部署4.1 全并行测试矩阵当你的代码需要在一组不同的环境里跑同一套测试时最简单的做法是定义多个同样的 job只是参数不同。但 GitLab CI/CD 出了更优雅的写法——parallel。在 job 里使用parallel: 5可以让同一个 job 被拆成 5 个并行实例执行它们有相同的硬件资源和脚本只有签名不同。更精细的做法是配合变量矩阵unit-test: stage: test image: python:3.11 parallel: matrix: - PYTHON_VERSION: [3.10, 3.11, 3.12] script: - python -m pytest这种写法可以在同一个 job 配置里提起多组测试变量不需要复制粘贴整套 yaml。流水线图表上会显示多个小卡片一眼就能看出哪个 Python 版本挂了。4.2 串行门禁测试不过就不让去部署在真实项目里流水线不是越快越好而是要在关键节点关好门。最简单的门禁结构是 build → test → deploytest 不通过deploy 不启动。这个逻辑已经隐含在 GitLab 的 stage 里了。但我不建议把所有验证都塞进同一个 stage。比如你的项目既有单元测试又有静态代码扫描还有安全扫描从设计上应该拆成多个 stage 串起来而不是全部并行在一个 stage 里跑。分线的意义在于如果安全扫描失败你希望它挡住后面的部署了而不是和单元测试的结果混在一起等跑完才发现还得从头来过。4.3 人工卡点让部署主动权回到人类手里生产环境的部署应当谨慎。GitLab 里有一种when: manual的 job它会生成一个按钮只有有权限的人员点击后才会继续执行。deploy-prod: stage: deploy script: - export RELEASE_ENVproduction - ./release.sh rules: - if: $CI_COMMIT_BRANCH main when: manual environment: name: production加上environment关键字之后流水线里的部署就不再只是一串脚本了GitLab 环境页面会记录每一次部署对应到哪个 commit、部署时间、部署人。配合Settings → CI/CD → Protected Environments里设置部署权限就能做到只有指定的角色可以点这个部署按钮。这个能力对生产环境的价值极高——部署动作不依赖哪个人恰好记得刷一遍命令而是固化成流水线里的一个可审计、可追溯的步骤。这里要提醒从 Jenkins 转过来的网友GitLab 的手动执行不是在页面上点一个“Build Now”而是 job 本身有自己的生命周期。你点按钮的那个瞬间只是触发它后续该分配 Runner 就分配 Runner该拉镜像就拉镜像。所以对慢 job 记得设置合理的timeout避免它挂着占用 Runner。5. 五个我在真实项目里踩过的 GitLab 坑5.1 共享 Runner 被不安全的 job 拖垮用 Group Runner 跑团队里所有项目是一件省事但又有风险的事。曾有一次组里的某人提交了一个带docker build的 job他的构建脚本里没有清理镜像每次构建都在 Runner 宿主机上留下一堆中间层镜像。仓库积累了几十 G 的垃圾后面别人的 job 全都因为磁盘空间不足而失败。这个问题的排查费了不少劲明明自己的页面显示 job stuck可 Runner 列表里状态是绿色的。最后登到 Runner 主机上一看/var/lib/docker爆掉了。解决思路是双管齐下一方面在 Runner 主机上配置定期清理机制比如 GitLab 官方容器镜像自带的gitlab-runner定时清理机制另一方面在敏感 job 的script末尾加清理步骤把构建时产生的临时镜像和容器显式删掉。如果你是那个团队里管 Runner 的人我想说不要以为 Runner 装完就一劳永逸了它和服务器上的其他进程一样需要维护和治理。5.2 cache key 不生效导致每次从零构建有段时间内部前端项目一直很慢流水线平均耗时 8 分钟而本地相同配置只要 40 秒。排查后发现cache 配置里虽然写了 node_modules但没指定keyGitLab 对每个 job 默认以分支号作为 cache key 的一部分不同分支之间缓存不共享长期不合并其他分支的 feature 分支全部得从零装依赖。修复后的配置cache: key: files: - package-lock.json paths: - node_modules/这个cache:key:files做得非常聪明它拿package-lock.json的文件内容做哈希作为 cache key。结构一致则 key 相同所有分支、所有 job 共享同一套 node_modules 缓存一旦依赖锁定文件变化key 失效自动重新构建缓存。这样既保证缓存有效性又最大化复用率是前端项目里标准的 cache 配置方式。5.3 Docker executor 里的 Docker一条隐藏的歧途很多人想在 GitLab Runner 里直接 rundocker build于是用--docker-privileged true开启了特权模式然后在 job 的 script 里执行 docker 命令发现报错找不到 docker socket。这个坑在 Linux 环境下几乎人人都会踩。典型的错误信息是Cannot connect to the Docker daemon at unix:///var/run/docker.sock。新手上路最常犯的错是在脚本开头加了句apk add docker然后失败因为容器里那个 docker 客户端不是用来调宿主机 daemon 的。正确解法有两个——要么使用 GitLab 官方提供的docker:dind服务在同一 job 里运行一个内部的 docker daemon要么把宿主机的 docker socket 通过 volume 挂载到 job 容器里/var/run/docker.sock:/var/run/docker.sock。前者隔离性更好、较安全后者的执行效率高、配置简单但意味着 job 容器实际上拿到了宿主机的 docker 控制权安全性要自己想清楚。在“简单使用”的阶段我劝你别开 privileged老老实实在模板里加一个 docker 登录步骤或者直接把镜像构建放到更合适的专用流水线去。5.4 分支合并瞬间的 pipeline 重复GitLab 的基础触发逻辑是每次 push 即触发流水线。当一个 MR 尚未合并目标分支上来了一个新的提交MR 的源分支可能又会 push 一次导致同一个改动在多个上下文里产生流水线。如果这些流水线都在跑部署 job很可能出现部署竞态两条流水线同时尝试往同一个环境部署不同的版本。解决方法是在较新版本里用workflow:rules控制整个流水线的触发范围或者明确用rules过滤掉部分 job 的触发场景。我在线上团队里有一条不成文的规矩部署类 job 的 rules 永远比构建类的严一档宁可少触发一次也不能并发部署。5.5 artifacts 文件命中率低的反直觉建议有时你在artifacts里写了dist/但 GitLab 上传的包一直很大明明 dist 里只有几个文件。这是因为artifact默认会把从 Runner 工作目录到dist/路径之间的所有相对路径原样打包而 Runner 工作目录又往往是一个很大的 checkout 目录里面包含了.git本身。如果你确定产物不需要保留历史版本且部署脚本可以直接拉取仓库那么可以把artifacts的路径写得更精确或者压根就不上传某些不需要的文件。否则产物包体积膨胀传到下一个 job 以及从 GitLab 下载都要花费额外时间得不偿失。6. 从“简单使用”到“顺手用”三个立竿见影的进阶习惯6.1 把常见的 job 抽成 include 模板当多个项目要用同一个构建/测试脚本时复制一份.gitlab-ci.yml不是长期办法。GitLab 的include机制允许你在一个项目的仓库里引用另一个仓库的 YAML 片段include: - project: devops/templates ref: main file: /ci-templates/frontend-build.yml这样前端项目的构建、测试步骤就统一由仓库devops/templates维护。团队在模板里修复一个 bug所有引用它的项目都会立刻生效。这就是流水线配置从“复制粘贴”进化到“共享治理”的关键一步——运维要更新的不再是一个个项目而是一份模板文件。6.2 合理设置流水线超时别让任务挂着吃资源每个 job 可以单独设置执行超时时间优先级从高到低是 job 级 Runner 级 实例级。我在实际使用中都会给构建 job 设置一个相对宽裕但有限的值比如timeout: 30 minutes。四小时、八小时这种超时值不是为 CI 任务准备的更像是在给不正常的任务开绿卡。如果某个 job 遇到死锁Runner 被它占住后面整个团队都会被拖累。从运维的角度出发宁可让一个偶尔因为编译太慢而超时的 job 失败重跑也不要去承担一个卡死的 job 无限期占资源的风险。6.3 CI/CD 变量尽量用受保护的变量不要裸写在 .gitlab-ci.yml 里把数据库密码、云厂商密钥、部署令牌直接硬编码到.gitlab-ci.yml相当于把生产环境的钥匙贴在办公室门口。GitLab 的Settings → CI/CD → Variables页面提供了两种变量类型普通变量和受保护变量。受保护变量只在受保护的分支或 tag 上触发流水线时才会被注入。简单说你永远不要指望 feature 分支上的流水线能读到生产环境的密钥。真正安全的设计是开发分支能读开发变量的值、主分支只能读到预发布变量、生产环境密钥只在 tag 发布时出现并且开启掩蔽保证它在日志里永远不显示明文。这些设置都是 CI/CD 的“水电基础设施”花十分钟做公道。结束语GitLab CI/CD 的“简单使用”最迷人的地方是上手成本比我们想象中低。一个文件、一条注册命令、一台能装 docker 的机器流程就能转起来。但随着流水线越来越多、覆盖的项目越来越杂它对你的要求也会慢慢从一个“会写 yaml 的开发者”变成一个“会治理流水线的基础设施管理者”。我希望这篇博客可以把你的起点拉得足够高少走那些我撞得直冒烟的弯路。个人的小经验是第一次跑通 GitLab CI/CD 之后与其急着把公司所有项目都接进来不如先用一个低风险的项目做试验把构建、测试、部署三个阶段的配置都跑一遍踩够姿势再大规模铺开。流水线一旦成为团队默认的开发习惯后续的优化才有真正的意义。这一篇讲的是门路后面的系列还会往变量注入、多环境部署、父子管道这些方向走得更深你们先跑起来咱们下一篇见。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询