ARM64构建:40分钟→8-12分钟——在GitHub Actions中消除QEMU
发布于 2026年4月14日

问题:x86运行器上的静默QEMU
一个GitHub Actions CI/CD流水线正在为OKE(Oracle Kubernetes Engine)集群构建Docker镜像,集群使用ARM64节点(OCI A1.Flex Ampere实例)。配置的运行器是x86机器上的自托管运行器。Dockerfile是标准的,buildx配置正确——但构建需要约40分钟。
原因不在于Dockerfile或应用大小(生成350个页面的Next.js)。而在于工作流中的一行:
- uses: docker/setup-qemu-action@v3
- run: docker buildx build --platform linux/arm64 .当x86运行器遇到--platform linux/arm64时,Docker使用QEMU在x86硬件上模拟ARM64架构。模拟带来的性能惩罚是编译时间的5-10倍——每条ARM64指令都由模拟器实时翻译。
setup-qemu-action不会警告构建会变慢。它只是启用模拟,buildx继续执行。结果是功能上正确的构建,但速度比在原生硬件上慢了几个数量级。
背景:从AWS(x86)迁移到OCI ARM64
该项目已从AWS ECS(x86实例)迁移到使用A1.Flex节点(ARM64 Ampere)的OKE以降低成本。在ECS上运行的生产镜像是amd64的——第一个ARM64构建是在Apple Silicon Mac(原生ARM)上手动用Docker buildx完成的。为了自动化CI/CD,现有的自托管x86运行器被复用,通过QEMU以避免搭建新基础设施。
数周内,生产部署仅在构建步骤就需要约40分钟。流水线的其余部分(推送到OCIR、kubectl rollout)只需几秒钟。
解决方案:GitHub Actions中的ubuntu-24.04-arm
GitHub自2024年起提供托管的ARM64运行器。ubuntu-24.04-arm运行器在真实ARM硬件上运行——没有模拟,没有QEMU,没有指令翻译惩罚。
工作流被分为两个独立的作业:
# 修改前——单个x86作业,对生产和测试环境都使用QEMU
jobs:
Build-and-Deploy:
runs-on: self-hosted # x86
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3 # ARM64模拟
- uses: docker/setup-buildx-action@v3
- run: |
docker buildx build \
--platform linux/arm64 \ # 通过QEMU强制ARM64
--push \
-t $IMAGE_TAG .# 修改后——按分支/环境分离的作业
jobs:
Build-and-Deploy-Stage:
runs-on: self-hosted # x86——测试环境使用amd64,没问题
if: github.ref == 'refs/heads/staging'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- run: docker buildx build --push -t $IMAGE_TAG .
Build-and-Deploy-Prod:
runs-on: ubuntu-24.04-arm # 原生ARM——无QEMU
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- run: |
docker buildx build \
--push \ # 无--platform:运行器已是ARM64
-t $IMAGE_TAG .关键点:在生产作业中,--platform linux/arm64被移除了。当运行器已经是ARM64时,指定平台是多余的——在某些版本中保留它甚至会让buildx产生困惑。构建变为原生执行,无需任何其他更改。
从生产作业中移除的内容
docker/setup-qemu-action@v3 — 不需要:运行器已是ARM64
--platform linux/arm64 — 不需要:运行器的原生架构
Set deploy Key — 未使用:通过kubectl部署,非SSH
Load Centralized Configuration — 变量迁移到GitHub仓库的secrets/vars
附带修复:kubectl rollout status的RBAC
构建完成后,流水线运行kubectl rollout status等待部署完成后再将作业标记为成功。该命令因权限错误而静默失败——CI的ServiceAccount在deployments上只有get、patch和update权限。
# 运行kubectl rollout status时报错
Error from server (Forbidden): deployments.apps "nextjs" is forbidden:
User "system:serviceaccount:myapp:cicd-deployer" cannot list resource
"deployments" in API group "apps" in the namespace "myapp"rollout status需要list和watch权限来观察滚动更新的进度。Role已更新:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cicd-deployer-role
namespace: myapp
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list"]第二个瓶颈:2.1 GB的镜像
解决构建问题后,下一个瓶颈变得明显:滚动更新期间的停机时间为4-5分钟——不是因为Kubernetes慢,而是因为镜像拉取时间。OKE上的每个新节点需要从OCIR(Oracle容器镜像仓库)下载2.1 GB,每个Pod耗时1分34秒。在2个副本和顺序滚动更新的情况下,部分不可用窗口是稳定的。
减小镜像大小是下一步:多阶段构建以排除开发用node_modules、Next.js output: 'standalone'(大幅减少进入最终镜像的内容)和层优化。约500 MB的镜像将把拉取时间从1分34秒减少到约20秒——几乎消除所有滚动更新的停机时间。
结果
切换到ubuntu-24.04-arm后的第一次部署确认了效果:构建成功,镜像发布到OCIR,Pod在OKE中以新标签启动。
# 部署后验证
kubectl get pods -n myapp
# nextjs-7f4cbbdffd-g595h 1/1 Running 0 6m
# nextjs-7f4cbbdffd-gt5jt 1/1 Running 0 5m
kubectl get deployment nextjs -n myapp \
-o jsonpath='{.spec.template.spec.containers[0].image}'
# registry.example.com/myapp/www:abc1234fQEMU是开发机器上一次性构建的有效工具。对于每次提交都要经过流水线的生产CI/CD来说,5-10倍的惩罚是不可接受的。GitHub Actions中存在原生ARM运行器,其成本与同等x86运行器相同——没有理由在长时间运行的流水线中保留QEMU。
迁移流水线的检查清单
1. 确认构建是否对运行器架构不同的平台使用--platform
2. 检查setup-qemu-action是否存在(模拟的明显标志)
3. 将runs-on切换为ubuntu-24.04-arm(或ARM自托管运行器)
4. 从生产作业中移除setup-qemu-action和--platform
5. 检查ServiceAccount的RBAC:rollout status需要list + watch权限
6. 测量镜像拉取时间——超过30秒就是优化候选


