
过去几年我经手过好几个从 x86 向鲲鹏迁移的项目踩过的坑一个个都能写本书了。最深的体会是光把代码在鲲鹏上编译通过、跑起来那只是第一步真正要落地到生产还得让整个编译、测试、部署过程可重复、可追踪、能自动跑。这就是 CI/CD 流水线要解决的事。而鲲鹏 DevKit 这套工具链恰好能在迁移和流水线搭建过程中帮上大忙。这篇文章会从实际项目出发讲清楚怎么在一台鲲鹏服务器上从零把 CI/CD 流水线搭起来。我会把 DevKit 的代码迁移、编译调优、性能分析这几个核心组件和 GitLab CI/CD 的完整流程串在一起包含可以直接复制的流水线脚本、常见报错排查和优化技巧。不管你刚拿到鲲鹏机器还是已经在做迁移评估这篇文章都能给你一个能落地的参照。1. 先说清楚鲲鹏 DevKit 到底是什么为什么要用它搭建 CI/CD很多第一次接触鲲鹏的朋友看到 DevKit 这个词容易误解成“一个 IDE 插件”。其实它是一整套面向鲲鹏处理器的开发工具包覆盖了从 x86 代码迁移、编译构建、性能分析到系统调优的全流程。单独拿来做手工开发还行但真正体现价值的地方是把它集成到自动化流水线里让每一次代码提交都能自动走“迁移检查-编译-测试-打包-部署”这条路。1.1 它不是一套 IDE而是一整套“工具链”DevKit 包括几个重要组件代码迁移工具、毕昇编译器、性能分析工具、调优助手以及一系列针对鲲鹏优化的加速库。代码迁移工具可以扫描源码中不兼容 x86 的地方比如特定头文件、内联汇编、SSE/AVX 指令然后给出修改建议毕昇编译器则是基于 LLVM 的编译器提供了针对 aarch64 架构的深度优化选项性能分析工具可以抓取 CPU 热点、锁竞争、内存访问异常等。举个生活化的例子之前团队在 x86 上写了一个 C 服务里头用了不少 SIMD 指令做图像缩放。拿到鲲鹏机器上直接编译结果一堆汇编报错。最原始的办法是手动一行行找几百个文件能找一晚上。用 DevKit 的代码迁移工具扫描一遍它直接定位到所有使用到 x86 指令的位置还提示改成 NEON 指令或者使用对应的加速库。这种效率差距是单纯靠“人肉适配”没法比的。1.2 迁移场景里的真实痛点从一个真实项目来看从 x86 迁移到鲲鹏通常会在这些地方卡住编译期问题第三方库没有 aarch64 版本或者源码里有 x86 架构判断编译直接报错。运行期问题隐式依赖了 x86 指令集比如汇编代码、CPU 特性检测代码在 ARM 上行为不对。性能问题同样的程序在双方 CPU 上运行性能差距明显需要做针对性优化。交付问题即使手工能在服务器上编译运行但怎么把这条链路固化让开发每次提交代码都能自动验证这是多数团队面临的最后一公里。这些坑我在实际迁移中都踩过。有一次项目依赖了一个内部封装的加密库只提供了 x86 的.a静态库。在鲲鹏上链接的时候直接报“cannot find libxxx.a”。当时 DevKit 的依赖分析功能列出了该库的源码地址我们把库源码拉下来用毕昇编译器重新编译才解决问题。所以工具不是万能的但它能帮你快速定位问题大大缩短排查时间。1.3 为什么 CI/CD 要单独拉出来讲手工迁移编译成功后团队最想做的事往往是“赶紧上线”。但如果只靠开发在自己电脑上手工编译部署后续每次代码变更都会产生新的风险。今天这个人改了代码明天那个人加了依赖没人盯着架构兼容性很快又会出现编译失败。把 CI/CD 流水线放在迁移之后做等于给整个项目上了一道保险。每次代码提交runner 都会在鲲鹏环境里重新走一遍编译和测试流程任何不兼容问题在合并代码之前就会被发现。同时流水线会把构建产物自动化打包成容器镜像部署到测试环境形成一套完整的交付机制。这也是为什么我建议大家在迁移的第一时间就把 CI/CD 搭好而不是等手工流程跑了很久再补。2. 流水线整体设计先用一张图想清楚再动手很多人搭 CI/CD 喜欢拿到工具就写脚本写一步算一步。我的建议恰恰相反先花半小时把整个流程在脑子里过一遍想清楚“这张流水线到底要完成哪些事、跑到哪一台机器上、产物是什么、最后怎么交付”。这样后面写.gitlab-ci.yml或者 Jenkinsfile 的时候思路才会清晰。2.1 目标从代码提交到镜像推送一条龙我这次要做的流水线目标很明确开发把代码推送到 GitLab 仓库。流水线自动拉取代码。在鲲鹏环境下执行代码扫描、编译、单元测试。测试通过后构建 Docker 镜像推送到镜像仓库。触发部署作业把镜像部署到测试服务器。每一步都产生日志和报告方便追踪。这个目标基本覆盖了一整套持续集成和持续部署的闭环。如果你暂时不想做自动部署也可以只做到镜像推送后续再扩展。关键在于结构留好。2.2 跑在鲲鹏上的 Runner 到底怎么选CI/CD 里的“跑腿工人”就是 Runner。在鲲鹏环境下有两种常见选择Docker Runner用 Docker 容器执行流水线任务优点是环境隔离好、每个 job 都是干净环境缺点是拉镜像需要时间且对 Docker daemon 依赖性强。Shell Runner直接在宿主机上执行命令优点是简单、速度快缺点是环境不隔离多个 job 可能互相污染。我在项目里选的是 Docker Runner。原因是流水线里有迁移检查、编译、测试等多个阶段如果用 Shell Runner上一个阶段的残留文件会影响下一个阶段。用 Docker 容器隔离每个 job 都在全新容器里跑更可控。需要注意Runner 本身要装在一台 aarch64 架构的机器上才能拉取 ARM 架构的镜像。如果拿一台 x86 Runner 去执行 ARM 镜像任务就会遇到“exec format error”这个坑后面单独说。2.3 流水线阶段划分与工具选型我把整个流水线划分为 5 个阶段scan代码迁移与合规扫描用 DevKit 的命令行工具检查源码架构兼容性。build用毕昇编译器编译项目产出可执行文件或静态库。test跑单元测试和基础功能测试。package构建容器镜像并推送。deploy更新测试环境服务。工具选型方面GitLab CI 作为流水线平台Docker Docker Compose 处理环境DevKit 提供编译器和扫描工具Harbor 或本地的 Docker Registry 做镜像存储。如果你团队用 Jenkins也不影响下面的脚本思路同样可以迁移。2.4 整个方案的取舍理由为什么不用现成的云构建服务两个原因一是很多云构建服务跑在 x86 架构上虽然能模拟 ARM但性能和真实性差很多二是鲲鹏场景涉及内部依赖和网络要求内网自托管更符合实际。所以最终方案是一台鲲鹏物理服务器在这台上装 GitLab Runner所有流水线任务都在机器内部完成。好处是构建性能和最终部署环境完全一致坏处是这台上要是挂了流水线就断了。解决办法是后面再扩第二个 runner 做冗余这是后话。3. 环境准备与基础配置把根基打牢开始之前先把硬件和系统准备好。我这里用的是一台鲲鹏 920 双路服务器操作系统是银河麒麟高级服务器版 V10,内核版本满足 Docker 运行要求。当然使用 openEuler 或 Ubuntu ARM 版也是可以的。不过要注意不同操作系统的包管理器不一样下面涉及安装命令的地方我会按麒麟/Debian 系为主openEuler 的 RPM 系命令也顺便提一句。3.1 鲲鹏服务器与操作系统选型做 CI/CD 的服务器一般不建议和生产业务机混用。我单独拿了一台 16 核 32GB 内存的鲲鹏服务器。选型时注意存储要够因为 Docker 镜像、缓存、构建产物加起来很容易占掉几十 GB。系统盘建议 200GB 起步数据盘单独挂一块给 Docker 和构建缓存。系统推荐首选 openEuler 22.03 LTS 或麒麟 V10这两个系统对鲲鹏的适配都很成熟内核自带的驱动也比较全。装系统时注意选择 aarch64 版本这和 x86_64 的安装包不通用。安装完成后先用uname -m确认架构输出应该是aarch64。如果输出x86_64那说明你装错镜像了后面所有流程都会白费。3.2 安装 Docker 和 GitLab Runner 的实操在麒麟 V10 上安装 Docker我习惯直接用官方脚本curl -fsSL https://get.docker.com | sh systemctl enable docker systemctl start docker如果服务器无法访问外网那就通过内网镜像安装 docker-ce 的 ARM 版本。装完后验证一下docker info docker run --rm hello-worldhello-world 能够输出提示信息说明 Docker 正常。接下来安装 GitLab Runner。推荐使用二进制方式直接下载 aarch64 版本# 以 gitlab-runner 用户运行避免权限问题 curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64 chmod x /usr/local/bin/gitlab-runner useradd --comment GitLab Runner --create-home gitlab-runner --shell /bin/bash gitlab-runner install --usergitlab-runner --working-directory/home/gitlab-runner gitlab-runner start注册 Runner 时需要从 GitLab 项目的 Settings - CI/CD - Runners 页面拿到注册 tokengitlab-runner register \ --non-interactive \ --url https://gitlab.example.com \ --registration-token 你的token \ --executor docker \ --docker-image registry.gitlab.com/xxx/ci-base:aarch64 \ --description kunpeng-runner-01 \ --tag-list kunpeng,aarch64,linux这里我注册了一个标签kunpeng 和 aarch64方便后续流水线指定跑在哪台机器上。注意--docker-image要是一个 arm64 的基础镜像比如arm64v8/ubuntu或者你内网自己构建的镜像。3.3 用 DevKit 的编译组件跑一次最简单的编译DevKit 提供了毕昇编译器可以直接通过命令行使用。把它安装在/opt/DevKit下其中编译器在/opt/DevKit/compiler/bin/clang。我一般会给毕昇编译器做一个软链ln -s /opt/DevKit/compiler/bin/clang /usr/local/bin/bisheng-clang ln -s /opt/DevKit/compiler/bin/clang /usr/local/bin/bisheng-clang然后在/etc/profile.d/bisheng.sh里写export PATH/opt/DevKit/compiler/bin:$PATH export LD_LIBRARY_PATH/opt/DevKit/compiler/lib:$LD_LIBRARY_PATH验证编译。写一个最简单的 C 文件#include stdio.h int main() { printf(Hello Kunpeng CI\n); return 0; }用毕昇编译bisheng-clang -O3 hello.c -o hello ./hello如果能正常输出说明编译器工作正常。还可以用file hello看一下输出 ELF 64-bit LSB executable, ARM aarch64。确认没问题后这一步就算过了。3.4 挂载缓存与私有镜像仓库流水线频繁构建时每次从头编译会非常耗时。GitLab Runner 支持使用 S3 或本地目录作为分布式缓存。对小团队来说直接在/etc/gitlab-runner/config.toml里配置 Docker executor 的 volumes把宿主机的构建缓存目录挂载进容器[[runners]] name kunpeng-runner-01 url https://gitlab.example.com token token executor docker [runners.docker] image registry.gitlab.example.com/ci-images/build-base:aarch64 privileged true volumes [/cache, /opt/DevKit:/opt/DevKit:ro] [runners.cache] Type shell Path /cache注意这里我把宿主机的/opt/DevKit只读挂载进容器这样就可以在容器里直接用毕昇编译器不用每个镜像都重新安装 DevKit。这个做法在实际项目中能省下非常多时间。4. 代码迁移环节把 x86 代码搬到 ARM 上环境就绪后第一项正式的工作就是把项目源码从 x86 迁移到 ARM 兼容状态。这个环节建议先做一次全局扫描再针对报错逐个修。DevKit 的代码迁移工具既可以提供 Web 界面也支持命令行批处理。在流水线里我用的是命令行模式方便自动化。4.1 迁移前的检查清单在实际操作前我总结了一个检查清单避免漏项检查所有源码文件后缀.c/.cpp/.h 里是否包含 x86 intrinsics 头文件比如immintrin.h。检查内联汇编是否包含asm关键字里面有rax、xmm等寄存器。检查第三方库是否提供了 aarch64 版本是否能在鲲鹏上重新编译。检查编译选项是否有-mavx2、-msse4.2等 x86 专属选项。检查运行时依赖是否通过 cpuid 指令判断 CPU 特性。大部分问题集中在头文件和编译选项上。用 DevKit 扫描时它能自动识别这些模式效率远高于 grep。4.2 DevKit 代码迁移工具的实际用法先看命令行工具。在 DevKit 安装目录下有一个code-migration的可执行文件。假设项目在/data/project/opt/DevKit/code-migration/bin/porting --source /data/project --target /data/project_migrated它会生成一份报告默认在/data/project_migrated/porting_result.csv。打开这个 CSV里面会列出来每一处不兼容的代码位置、错误类型和修改建议。比如文件行号类型建议src/demo.c23x86 内联汇编使用 ARM NEON 内联函数或 C 语言重写src/simd.h67immintrin.h替换为 arm_neon.h拿到报告后我通常会把每个问题分类能直接改的标记为“quick fix”需要重写算法的标记为“dev task”。流水线里可以在扫描阶段直接检查 CSV 中是否还有残留的未处理项如果有就让这次构建失败强制开发处理完再合入代码。4.3 处理典型不兼容点内联汇编、SSE 指令、字节序内联汇编是最难处理的。比如一段做 SIMD 加法并返回最大值的内联汇编在 x86 上用了 SSE 指令。在 ARM 上最简单的方式是用 NEON 内联函数替换。举一个精简版的例子原 x86 代码#include immintrin.h float sum_sse(const float* a, int n) { __m128 sum _mm_setzero_ps(); for (int i 0; i n; i) { sum _mm_add_ps(sum, _mm_loadu_ps(a[i])); } // 简化处理忽略尾部 float temp[4]; _mm_storeu_ps(temp, sum); return temp[0] temp[1] temp[2] temp[3]; }改成 ARM NEON#include arm_neon.h float sum_neon(const float* a, int n) { float32x4_t sum vdupq_n_f32(0.0f); for (int i 0; i n; i) { sum vaddq_f32(sum, vld1q_f32(a[i])); } float temp[4]; vst1q_f32(temp, sum); return temp[0] temp[1] temp[2] temp[3]; }代码结构上对应得很整齐但函数名、寄存器类型都不一样。如果你对 SIMD 不熟建议先搜对应头文件再查 NEON Intrinsics 官方文档。纯靠猜很容易写错而且编译期不一定报错运行结果错了才让人头大。字节序问题也容易踩。x86 通常是小端ARM 也支持小端但如果项目里曾经做过多字节数据手工转换就要仔细检查。DevKit 的扫描工具能识别一些常见的字节序操作但还是需要人工复核。4.4 依赖库的 aarch64 版本替换除了源码本身依赖库往往是大头。项目里可能用了某个私有库或某个官方只提供 x86 版本的动态库。经验是先去库的官方仓库看有没有 aarch64 版本没有的话找源码自己编译实在不行的考虑换功能等价的替代库。我之前遇到过 OpenBLAS 依赖。x86 上直接apt install libopenblas-dev就行但在鲲鹏上系统仓库里的 OpenBLAS 有可能因为编译时没有针对鲲鹏优化而性能一般。后来改用 DevKit 提供的优化版本或者自己在源码编译时开启-marcharmv8-a等选项性能才有明显提升。在流水线的依赖安装阶段建议写一个deps.sh脚本把所有依赖的安装和编译操作固定下来避免每次有人手工敲命令导致环境差异。5. 流水线脚本的完整实现可以抄作业接下来进入重头戏把整个 CI/CD 流水线用 GitLab CI 的.gitlab-ci.yml文件实现出来。我直接给出一个完整的可落地版本然后逐段解释。这份脚本我在内网项目里实际用过框架稳定你可以根据项目路径、仓库地址和版本做修改。5.1 .gitlab-ci.yml 整体框架stages: - scan - build - test - package - deploy variables: IMAGE_REGISTRY: registry.gitlab.example.com/kunpeng IMAGE_TAG: $CI_COMMIT_SHORT_SHA workflow: rules: - if: $CI_PIPELINE_SOURCE merge_request_event - if: $CI_COMMIT_BRANCH $CI_DEFAULT_BRANCH - if: $CI_COMMIT_TAG cache: key: $CI_COMMIT_REF_SLUG paths: - .build/ before_script: - echo Pipeline starts at $(date)这里定义了五个 stage变量中IMAGE_TAG用 GitLab 提供的短提交 SHA 作为镜像标签保证每次构建的镜像版本一致。cache配置了.build/目录作为缓存用于存放编译中间文件加速增量编译。workflow的规则保证流水线只在合并请求、默认分支以及打标签时触发避免每次推送都浪费资源。5.2 各阶段脚本详解代码扫描、编译、单元测试、镜像构建、部署scan 阶段scan-code: stage: scan tags: - kunpeng image: $IMAGE_REGISTRY/ci-scan:aarch64 script: - /opt/DevKit/code-migration/bin/porting --source ./src --target ./report - if grep -q High ./report/porting_result.csv; then echo 存在高优迁移项; exit 1; fi - cp -r ./report/porting_result.csv scan-report.csv artifacts: paths: - scan-report.csv - report/ expire_in: 1 week这段的意思是用 DevKit 迁移工具扫描src目录然后把报告作为构建产物留存。如果扫描结果里有 High 级别的迁移项说明代码还未完成兼容会让流水线失败。这样能强制团队在合入代码前把兼容问题处理完。build 阶段build-release: stage: build tags: - kunpeng image: $IMAGE_REGISTRY/ci-build:aarch64 script: - source /opt/DevKit/compiler/env.sh - mkdir -p build - cd build - cmake .. -DCMAKE_C_COMPILERbisheng-clang -DCMAKE_CXX_COMPILERbisheng-clang -DCMAKE_BUILD_TYPERelease - make -j$(nproc) - cd .. - cp build/hello-service hello-service artifacts: paths: - hello-service - build/ cache: key: $CI_COMMIT_REF_SLUG paths: - build/这里的重点是使用毕昇编译器作为 CMake 的编译器并开启了多核并行。-j$(nproc)会在构建时使用所有 CPU 核对鲲鹏服务器这种多核机器有明显的速度优势。cache把 build 目录缓存下来下一次构建时如果源码没有很大变化CMake 会使用增量编译大幅缩短时间。test 阶段unit-test: stage: test tags: - kunpeng image: $IMAGE_REGISTRY/ci-test:aarch64 dependencies: - build-release script: - chmod x ./hello-service - ./hello-service --selftest - for f in test/test_*.sh; do bash $f; done artifacts: paths: - test/reports/ when: always单元测试脚本可以五花八门这里给了一个简单模板先跑程序自带的自检再跑所有测试脚本。关键是when: always表示即使测试失败也保留报告文件方便定位问题。package 阶段docker-build-push: stage: package tags: - kunpeng image: docker:24 services: - docker:24-dind dependencies: - unit-test script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -f Dockerfile.aarch64 -t $IMAGE_REGISTRY/hello-service:$IMAGE_TAG . - docker push $IMAGE_REGISTRY/hello-service:$IMAGE_TAGDocker 镜像构建是天然适合放在鲲鹏机器上的因为镜像本身要跑在鲲鹏上。这里用了 Dockerfile注意基础镜像要使用 arm64v8 版本。如果程序是纯静态编译也可以用 scratch 镜像。deploy 阶段deploy-test: stage: deploy tags: - kunpeng image: $IMAGE_REGISTRY/ci-kubectl:aarch64 dependencies: - docker-build-push script: - sed s|IMAGE_TAG|$IMAGE_TAG|g k8s/deployment.yaml.template k8s/deployment.yaml - kubectl apply -f k8s/deployment.yaml - kubectl rollout status deployment/hello-service -n test environment: name: test如果目标环境是 Kubernetes 集群可以使用上面这种部署方式如果没有 k8s也可以在目标主机上用 SSH 拉镜像、重启容器ssh deploytest-server docker pull $IMAGE_REGISTRY/hello-service:$IMAGE_TAG docker tag $IMAGE_REGISTRY/hello-service:$IMAGE_TAG hello-service:latest cd /opt/app docker compose up -d部署方式可以根据团队基础设施习惯来选择核心思路都一样把镜像标签动态替换到部署文件中然后触发更新。5.3 常见参数与变量说明GitLab CI 提供了一些预定义变量在脚本中直接用不需要自己定义CI_COMMIT_SHORT_SHA当前提交的短哈希适合做镜像 tag。CI_COMMIT_REF_SLUG当前分支或标签的 URL 安全版本适合做缓存 key。CI_REGISTRY_USER/CI_REGISTRY_PASSWORDGitLab 镜像仓库的登录账号和密码。CI_PIPELINE_SOURCE流水线触发来源比如 push、merge_request_event、web 等。注意CI_REGISTRY_USER和CI_REGISTRY_PASSWORD只有在 GitLab 自带的 Container Registry 启用时才会自动注入。如果你使用独立的 Harbor建议用 Masked 变量保存用户名密码在 CI/CD 设置里配置。5.4 如何让 DevKit 的编译器和工具在流水线中自动化生效这里有一个常见疑问既然流水线跑在 Docker 容器里怎么调用 DevKit我提供了两种方案方案一基础镜像里预先安装 DevKit也就是把 DevKit 直接打进 CI 镜像里。好处是执行时无需挂载坏处是镜像体积会很大。方案二在 Runner 的 config.toml 里把宿主机的 DevKit 目录挂载进容器并在流水线脚本中设置环境变量。这种方式我在 3.4 节提到过命令是before_script: - export PATH/opt/DevKit/compiler/bin:$PATH - export LD_LIBRARY_PATH/opt/DevKit/compiler/lib:$LD_LIBRARY_PATH由于 config.toml 中已经挂载了/opt/DevKit:/opt/DevKit:ro容器内就能直接使用毕昇编译器。这种方式避免了重新制作大体积镜像更推荐。6. 性能分析与优化光能用不够还得快流水线跑通只是起点。实际上线以后你就会发现CI 速度可能越来越慢构建要十几分钟甚至半小时。这个时候必须上手做性能分析这里仍然会用到 DevKit 的性能分析工具还有一些我在实践中总结的流水线优化技巧。6.1 构建时间瓶颈分析先别急着改脚本先看时间花在哪。我习惯在 GitLab CI 的 Pipeline 页面按 job 查看耗时把每个阶段的时间列出来找出占用最大的几个。常见瓶颈依赖安装时间过长每次都要 apt 安装几百个包。没有利用缓存每次全量编译。测试阶段串行执行没有并行跑。Docker 镜像体积过大拉取和推送耗时长。编译选项没有开优化导致编译产物运行效率低。我曾经遇到一个项目流水线总耗时 25 分钟其中依赖安装就要 12 分钟。后来把依赖打包进基础镜像只留增量更新依赖安装时间降到 1 分钟以内。这个优化给流水线速度带来的提升是最直观的。6.2 用 DevKit 性能分析工具看吐槽点DevKit 的性能分析工具能够采集程序运行时的 hotspot 函数、调用次数、缓存命中率等。在流水线里可以在 test 阶段跑一个“性能回归测试”程序内部会执行核心算法并统计耗时用 DevKit 的bisheng-prof命令捕获数据。举一个例子bisheng-prof --app ./hello-service --output perf_data bisheng-prof --report perf_data --top 20输出的报告中会显示耗时前 20 的函数。比如发现某个图像处理函数明显耗时展开汇编后能看到大量循环。此时可以尝试优化用 NEON 指令重写。开启编译器自动向量化选项-O3 -mcputsv110或-marcharmv8.2-afp16simd等。使用 DevKit 提供的优化库替换手写算法。注意-mcpu参数要针对具体 CPU 型号比如鲲鹏 920 对应的 CPU 代号在不同系统中有差异。如果不确定可以用lscpu查 CPU 型号再选择合适的优化参数。6.3 并行编译、缓存分层、依赖预装这两个优化方向作者我讲两个真实案例。并行编译很简单在构建脚本中把make -j$(nproc)用起来。但这里的坑是如果项目比较大同时开 16 个编译线程会吃爆内存。我在一台 32GB 的机器上编译过一个大项目加上 CMake 自身内存开销最高占用超过 25GB。后来限制-j8虽然总时长略有增加但避免了 OOM。这是我踩过比较多次的坑。缓存分层我常用四个层面目录缓存把.build/目录缓存到 Runner下次增量编译。镜像层缓存在 Dockerfile 里把变动频率低的依赖先复制比如requirements.txt、CMakeLists.txt再执行安装这样代码变更时前面几层不会被重建。包管理器缓存把 apt、pip、npm 的下载缓存挂载到 mirror 或本地目录。中间产物上传把编译好的静态库作为包上传到制品库下游项目直接下载避免重复编译。关于依赖预装最省时的方式是做一个专门的“基础构建镜像”。把毕昇编译器、常用依赖、DevKit 命令行工具全部打进镜像流水线的 build job 直接使用这个镜像连挂载都可以省掉。但镜像体积非常可观第一次推送耗时较长需要在推送服务器带宽够用。6.4 典型优化效果对比拿我最近负责的一个 C 服务来举例。优化前流水线总耗时 18 分钟其中依赖安装 7 分钟编译 6 分钟测试 3 分钟推送部署 2 分钟。优化后依赖安装预装基础镜像每次只增量安装少量新包耗时 30 秒。编译使用缓存加并行编译耗时 2 分 30 秒。测试并行跑多个测试 job耗时 1 分 20 秒。推送部署镜像用 Docker 缓存耗时 1 分钟。整体从 18 分钟降到了 5 分钟左右。这一个改动让开发每天提交代码的反馈速度有了本质提升。对于高速迭代的产品团队这种收益远比手工构建节省的那点时间更关键。7. 常见问题与排障实录搭流水线的过程中绝大多数时间其实不是在写代码而是在排障。我把真实遇到的高频问题整理成速查表每一个都有对应的排查思路和解决办法。7.1 Runner 报错“exec format error”怎么查这个报错是跨架构环境中最常见的问题。原因是 Runner 本身跑在 x86 机器上但拉取了一个 ARM 架构的镜像或者反过来。Docker 尝试执行二进制时发现 CPU 架构不匹配直接报exec format error。排查步骤用docker inspect image查看镜像的Architecture字段确认是不是arm64。用uname -m查看 Runner 机器架构确保与镜像一致。检查 Runner 是否被注册到了正确机器上尤其是你用 tag 指定 runner 时tag 要匹配。如果确实需要在 x86 机器上跑 ARM 镜像一种做法是使用 QEMU 模拟docker run --rm --privileged multiarch/qemu-user-static --reset -p yes但这只适合快速测试性能较差正式 CI 不建议这么干。最稳的做法还是让 Runner 跑在物理鲲鹏机器或云上的 ARM 虚拟机上。7.2 编译不过找不到头文件/NASM 指令编译阶段经常出现“fatal error: immintrin.h: No such file or directory”。这是源码还残留了 x86 专属头文件。解决思路很简单用 DevKit 迁移工具定位具体文件。将immintrin.h替换为arm_neon.h。将对 x86 intrinsics 的调用改成 NEON 对应函数。如果代码使用 GNU 风格内联汇编且包含 x86 指令必须重写为 ARM 汇编或使用内建函数。还有一种情况是在构建脚本里用了-msse4.2之类的参数需要从 CMakeLists.txt 或 Makefile 里去掉改成熟知的架构中肯参数set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} -marcharmv8.2-a)不要把 x86 独有的优化参数直接搬到 ARM 上编译器会不识别可能报“unrecognized command-line option”。7.3 动态链接库找不到程序编译完成后运行时提示error while loading shared libraries: libXXX.so: cannot open shared object file原因很可能是依赖库没有安装。这时候在部署机或测试容器里执行ldd ./hello-service会看到哪个库 missing。解决方式是在构建机器的LD_LIBRARY_PATH里加入库路径或使用rpath把链接库路径固化到二进制中bisheng-clang -Wl,-rpath,/opt/libs main.c -o hello-service如果是自己编译的第三方库注意安装路径是否在默认搜索路径内。在流水线里最好在部署阶段也做一次ldd检测防止开发环境有库但测试环境没带的尴尬场景。7.4 内存不够导致 OOM并行编译容易触发内存不足现象是编译过程中有进程被杀掉日志末尾出现Killed字样。排查方法dmesg | tail -20如果看到Out of memory说明编译进程触发了内核 OOM。解决办法是降低并行度比如make -j4或-j8。另一个办法是给 Docker 容器设置内存限制[runners.docker] memory 8g cpus 8限制以后编译反而更稳定不容易把整台机器搞死。7.5 Docker 层缓存不命中很多项目都会遇到 Dockerfile 构建时缓存不命中。一个常见原因是频繁使用COPY . .只要源码任何一个文件变化了后续所有层都会失效。解决办法是尽量把依赖拷贝放在前面FROM arm64v8/ubuntu:22.04 AS base COPY requirements.txt /tmp/ RUN pip install -r /tmp/requirements.txt COPY . . RUN make这样只有 source 变化时依赖安装层不会重跑。同时在 GitLab Runner 的 config.toml 里开启镜像缓存功能也能加速带相同基础镜像的构建。8. 最后再分享几点经验流水线搭好之后我前后又调了将近一个月才算真正稳定。这中间有几条经验想单独拎出来分享给后来者。第一条尽量把 DevKit 的扫描和性能分析放进流水线而不是只在第一次迁移时用。因为代码是持续演进的今天编译通过明天某个同学加一个新的 x86 内联函数流水线里如果没有架构扫描很容易悄悄引入不兼容代码。在 MR 阶段卡住它比上线前再救火成本低太多。第二条基础镜像和缓存是 CI/CD 速度的生命线。第一次构建镜像确实慢但一旦把依赖都沉淀进去后续构建速度能快一个数量级。建议每周自动重建一次基础镜像把系统安全更新也打进去避免基础镜像越用越旧。第三条不要迷信“CPU 核数越多就并行度拉满”。鲲鹏服务器核数不少但内存和 IO 带宽一样重要。先做一次全量慢测试观察 CPU 和内存占用再决定并行参数比盲目堆-j靠谱。第四条流水线的日志和制品要有保留策略。我遇到过一个场景一个版本发布半年后突然要回溯当时用的二进制结果构建产物早就过期删掉了。后来把所有可执行文件和镜像 tag 都保存至少三个月关键版本永久留存。从最开始的 x86 环境一把梭到后来在鲲鹏上用 DevKit 做扫描、用毕昇编译、在 GitLab 里跑完整流水线整个流程走通之后最大的感受是迁移到鲲鹏这件事真正难点不在于“能不能跑”而在于“如何保证可持续地、高效地跑”。把 DevKit 和 CI/CD 结合起来相当于给项目的架构兼容性加了一道自动防线。以后每次代码提交它都会默默帮你把住质量关。这才是持续集成部署的真正价值。