在 GitHub Actions 上使用多 Job 并行构建,提升 Multi-Arch 镜像制作速度

在土豆大佬的文章 「使用 GitHub Actions 构建 Multi-arch Docker image」 中我们知道,如果希望在 GitHub Actions 上构建一个 Multi-Arch 的镜像,其中关键的 Workflow 内容类似如下:

如果你不懂什么是 Multi-Arch 的话,你可以先看看上面的文章.

- name: Set up QEMU
  uses: docker/setup-qemu-action@v1

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v1

- name: Build and push latest images
  uses: docker/build-push-action@v2
  with:
    context: .
    platforms: linux/arm64, linux/amd64
    push: true
    tags: |
        n0vad3v/bennythink:latest

这样我们就可以很方便地构建多架构镜像了,但是我们要知道一点:GitHub Actions 的机器是 Standard_DS2_v2,是一个 2 Core 7G 内存的小机器,而且甚至没法加钱换大机器,这样的话我们在构建一些比较鸡掰的包的时候就容易超时,比如…构建 CMake 可能需要 2hr…

再加上 ARM64 的部分是用 QEMU 模拟的,两个一起跑那真的是…等到跑了 6 个小时任务超时被 Kill 掉的时候…

只剩下滿腹的辛酸 無限的苦痛

为了解决这个问题(而且不花钱),我们就要充分利用 GitHub Actions 的 Jobs,我们知道,一个 Workflow 中的每个 Job 都是单独的机器在跑,所以这里的思路就从单机构建 Multi-Arch 镜像改为:

  1. 多个 Job 分别构建各自 Arch 的镜像,假设我们最终希望的镜像名称是 n0vad3v/bennythink:latest,那么这里分别构建 n0vad3v/bennythink-amd64:latestn0vad3v/bennythink-arm64:latest
  2. 等前构建任务完成之后利用一个单独的任务把这些镜像缝合在一起,使用 docker manifest amend
  3. Multi-Arch 镜像就出现了

Build Image

多个 Job 分别构建各自 Arch 的镜像

这一步非常简单,只需要多写几个 Job 就好了,类似这样:


  build-arm64-image:
    name: Build ARM64 Image
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWD }}
      
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
  
      - name: Build and push latest images
        uses: docker/build-push-action@v2
        with:
          context: .
          platforms: linux/arm64
          push: true
          tags: |
            n0vad3v/bennythink-arm64:latest

  build-amd64-image:
    name: Build AMD64 Image
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
    
      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWD }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
  
      - name: Build and push latest images
        uses: docker/build-push-action@v2
        with:
          context: .
          platforms: linux/amd64
          push: true
          tags: |
            n0vad3v/bennythink-amd64:latest

缝合镜像

上面两个 Job 完成后,我们应该已经得到并 push 了 n0vad3v/bennythink-amd64:latestn0vad3v/bennythink-arm64:latest 两个镜像,接下来使用一个任务把这两个镜像合并到一起,并使用 needs 保证只会在前面构建的任务完成后才会运行,示例如下:

combine-two-images:
  runs-on: ubuntu-latest
  needs:
    - build-arm64-image
    - build-amd64-image
  steps:
    - name: Login to Docker Hub
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_PASSWD }}
    
    - name: Combine two images
      run: |
        docker manifest create n0vad3v/bennythink:latest --amend n0vad3v/bennythink-amd64:latest --amend n0vad3v/bennythink-arm64:latest
        docker manifest push n0vad3v/bennythink:latest        

然后就成了.

缓存

由于通过 QEMU 模拟的构建镜像非常的慢,所以一定要用好缓存机制(比如使用 actions/cache@v3 ),不过一定要看清楚官方文档,了解清楚 keyrestore-keys 这类参数的用法,不要直接复制(不然就会像我一样两个 Job 写成了一个 key 然后 AMD64 的缓存总是把 ARM64 的缓存给覆盖掉了)。

用 QEMU 模拟 ARM64 真的是慢到吃屎都赶不上热乎的

后记

不过即使是这样你也没法成功地在 6hr 限制内用 GitHub Actions 的官方 Runner 编译完成 ClickHouse,但是对于一些没有那么重的任务来说,这样可以提升不少速度,目前我已经在如下仓库上实践这个策略:

顺便可以感受一下 QEMU 模拟的 ARM64 和 AMD64 原生之间的构建速度差距有多大:

最终我自己的需求还是通过前两天开源的 knatnetwork/github-runner 并搭配 DOKS 和 Eks 创建了一堆 AMD64 和 ARM64 的机器注册 Self-hosted Runner 在对应平台原生构建了,后续有机会我会写另一篇文章分享。