胡萝卜烤地瓜
胡萝卜烤地瓜
发布于 2026-07-25 / 5 阅读
0
0

NO.A1 调度与执行分离:self-hosted runner 为什么只负责「拉起 Docker」

⚠️ 法律与用途声明:本文讨论 GitHub Actions self-hosted runner 与 Docker 容器化 CI 的架构设计,基于开源工具链(GitHub Actions / Docker / Conan 2),用于技术学习与工程研究。所有企业名、组织名、域名、token 均已脱敏。读者须遵守所在地区法律法规。


一、为什么 runner 不能在宿主机上直接构建

最朴素的 self-hosted runner 用法是:把 runner 装在一台 Linux 服务器上,job 直接在宿主机里跑 conan installcmakemake。能用,但很快会撞墙:

  • 环境污染:今天这个 job 装了 GCC 13,明天那个 job 要 GCC 11,宿主机上的编译器版本被反复覆盖,最后谁也说不清"这台机器现在到底是什么环境"。
  • 构建不可复现:同一个 commit,今天构建过、明天失败——因为宿主机上某个依赖被另一个 job 改了。CI 的核心价值"可复现"直接归零。
  • 清理噩梦:交叉编译要装一堆 arm-none-eabi-* 工具、Conan cache 越攒越大、临时文件到处都是,宿主机逐渐变成垃圾场。
  • 并发冲突:多个 job 同时跑,抢同一个 Conan cache 目录、同一个临时路径,互相踩踏。

这些问题的根因是同一个:调度(决定跑什么 job)和执行(真正跑构建)搅在了宿主机这一层。runner 既当指挥又当施工队,宿主机既是调度节点又是构建环境,职责混在一起,必然互相污染。

解法很清晰:让 runner 只做调度,把执行交给一次性的 Docker 容器。宿主机永远保持干净。


二、self-hosted runner 的接入:把"调度员"装好

接入一个组织级 self-hosted runner,本质上是在服务器上跑通 GitHub 官方的 actions-runner,并把它注册成 systemd 服务。核心步骤(已脱敏):

  • 创建专用用户(不共用个人账号):useradd --create-home --shell /bin/bash ghrunner
  • 下载官方 runner 到独立目录:/opt/actions-runner
  • 注册到组织(用一次性注册 token):sudo -u ghrunner /opt/actions-runner/config.sh --url https://github.com/<企业组织> --token <TOKEN> --name <runner名> --labels <标签> --unattended --replace
  • 装成系统服务并开机自启:./svc.sh install ghrunner && systemctl enable --now <service>

几个关键设计值得说:

专用用户隔离。runner 跑在 ghrunner 用户下,不共用任何开发者的账号。这样 runner 的 .conan2、工作目录、权限边界都和开发环境物理隔离。构建用的是 Docker,但 runner 服务本身的身份是独立的,便于审计和回收。

标签即调度契约。注册时打上标签 self-hosted,linux,x64,<企业>,docker。workflow 里写 runs-on: [self-hosted, linux, x64, <企业>, docker],GitHub 据此把 job 路由到这台 runner。标签是平台和 CI 之间的调度接口——加一台新 runner 只要打对标签,workflow 不用改。

Docker 使用权限。runner 要能拉起 Docker 容器执行构建,所以 usermod -aG docker ghrunner 把它加进 docker 组。注意这一步给的是"调用 Docker 守护进程"的权限,不是 root;真正的构建发生在容器里,宿主机文件系统仍然受保护。


三、调度与执行分离的工程实现:job 跑在 container 里

分离的核心机制是 GitHub Actions 的 container: 字段——job 不在宿主机跑,而是在一个指定镜像的容器里跑:

jobs:
  linux-self-hosted:
    runs-on: [self-hosted, linux, x64, <企业>, docker]
    strategy:
      fail-fast: false
      matrix:
        image: [ubuntu:22.04, ubuntu:24.04]
    container:
      image: ${{ matrix.image }}
    steps:
      - uses: actions/checkout@v4
      - name: Install build tools
        run: apt-get update && apt-get install -y python3 cmake ninja-build g++ git
      - name: Build with Conan
        run: conan install . --build=missing && cmake --build build

这一段配置藏着分离的全部秘密:

"runner 只负责拉起容器"。GitHub runner 收到 job 后,启动一个 ubuntu:22.04(或 24.04)容器,把 job 的所有 step 放进容器执行。构建里装的 GCC、Conan cache、临时文件,全部在容器内,job 结束容器销毁,宿主机连一个字节都没被动过。

矩阵即多环境matrix.image 让同一个 job 在 ubuntu:22.04 和 24.04 两个环境里各跑一遍,验证兼容性。这在"宿主机直接构建"模式下几乎不可能(你不可能让一台机器同时是 22.04 和 24.04),而容器让它成了声明式的一行配置。

可复现回来了。每次构建都从一个干净的官方镜像开始,依赖从私有 Conan 仓库(Artifactory)拉取,环境完全确定。今天和明天、这台 runner 和那台 runner,构建结果一致。

这就是"调度与执行分离"的工程落点:runner 是调度员(决定跑哪个 job、在哪个容器跑),Docker 是施工队(真正编译),宿主机是机房,提供算力和 Docker 守护进程但不碰构建。三者职责清晰,互不污染。


四、代理注入:让容器里的工具能上网

嵌入式 CI 有个绕不开的问题:构建过程要访问外网(拉 conancenter 包、装 apt 依赖),但企业内网往往要走代理。而容器是隔离环境,宿主机的代理设置不会自动传进去。

这套方案的解法是两层代理注入

第一层:runner 服务环境注入(host 侧)。安装脚本为 runner 的 systemd 服务生成一个 drop-in 文件 /etc/systemd/system/<service>.d/proxy.conf

[Service]
Environment="HTTP_PROXY=http://127.0.0.1:20171"
Environment="HTTPS_PROXY=http://127.0.0.1:20171"
Environment="NO_PROXY=localhost,127.0.0.1,<企业域名>,.<企业域>"

这样 runner 进程及其拉起的容器默认继承这组代理变量。关键在 NO_PROXY——私有 Conan 仓库的域名必须走直连,否则访问自己的制品库还要绕一圈代理,又慢又容易断。

第二层:容器内显式注入。container 型 job 里,workflow 会再次显式传入同一组代理变量,确保容器内 aptpipconan 都能正确走代理访问外网,访问内网制品库时又走直连。

这套"host drop-in + 容器显式"的双层注入,解决了"外网要代理、内网要直连"的矛盾,而且对算法仓库透明——开发者写 workflow 时不用操心代理,模板已经处理好。


五、混合 CI:Linux 自托管 + Win/Mac 云端,成本与能力的平衡

不是所有平台都值得自托管。这套方案做了一个清晰的取舍:

  • Linux 构建自托管:嵌入式交叉编译的主战场在 Linux(工具链、Docker、Conan 生态都在 Linux 上最成熟),且构建密集、对网络延迟敏感(要访问内网 Artifactory),自托管收益最高。
  • Windows / macOS 构建用 GitHub 托管 runner:这两类构建是低频需求(比如验证 MSVC / Apple-Clang 兼容性),维护专用 Win/Mac 宿主机成本高、利用率低,直接用 GitHub 云端配额更划算。

实现上,workflow 里把 Win/Mac 的 job 限定为手动触发(workflow_dispatch),日常 push 只触发 Linux 自托管构建:

  windows-cloud:
    if: ${{ github.event_name == 'workflow_dispatch' }}
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - run: Write-Host "Windows build steps here"

这是个典型的"按收益分配资源"的架构判断:把重资产(自托管 Linux runner)投在高频主战场,把低频需求外包给云端弹性资源。既保证了主链路的速度和稳定性,又控制了总体成本。


六、架构哲学:调度面与执行面解耦

回头看,self-hosted runner 这一篇落地的核心哲学只有一条:调度面与执行面解耦

  • 调度面(GitHub Actions + runner)关心的是"什么时候、跑什么 job、路由到哪台机器"——它是逻辑层,应该是无状态、可水平扩展的。
  • 执行面(Docker 容器)关心的是"在这个确定的环境里把代码编译出来"——它是物理层,应该是隔离、可复现、用完即弃的。

把两者分开后,好处自然涌现:宿主机干净、构建可复现、环境可矩阵化、清理自动化、并发不冲突。而如果搅在一起(runner 直接在宿主机构建),这些问题会以"偶发构建失败""环境漂移""磁盘爆满"的形式反复折磨你。

这条哲学不止适用于 CI。它其实是平台工程的通用原则:调度(编排)和执行(落地产物)永远要分两层。Kubernetes 这么做(控制平面 vs 节点)、CI 系统这么做(调度器 vs 执行器)、这套嵌入式 DevOps 平台也这么做。

下一篇,我们走进执行面:Docker 三层构建模型——纯净镜像、模板缓存、即时容器,是怎么让交叉编译做到可复现、可缓存、可销毁的。


评论