当前位置: 首页 > news >正文

把数百个软件包迁移到 ARM64,Cloudflare 踩了哪些坑

原文:Porting Our Software to ARM64,作者 Alexander Huynh,Cloudflare Blog。

ARM64 服务器这几年越来越普遍。AWS 的 Graviton、Ampere 的 Altra,包括苹果 M 系列芯片的 Mac,都在把 ARM 架构从移动端推向数据中心和开发者桌面。对于只跑过 x86 的团队来说,迁移这件事听起来简单,做起来往往不是那么回事。

这篇文章是 Cloudflare 工程师 Alexander Huynh 在 2018 年写下的迁移记录,记录了他们把整个边缘软件栈迁移到 ARM64 的过程——包括用了哪些方法,碰了哪些壁,最后怎么解决的。虽然时间稍早,但里面的问题和思路到今天都有参考价值。


规模有多大

要理解这件事的难度,先看看 Cloudflare 的软件栈有多厚。

底层是 Linux 内核,发行版选的是 Debian,往上是他们自己维护的数百个软件包,其中一部分是基于开源项目做了定制,另一部分是完全内部开发的。编程语言横跨 C、C++、Go、Lua、Python、Rust 六种。

好消息是,ARM64 的生态支持已经相当成熟。Linux 内核很早就支持 ARM64,Debian 从 Stretch 版本(2017 年)起把 ARM64 列为一类发行架构,这意味着操作系统本身能比较顺滑地跑起来。真正的工作量在于让那数百个内部包也在 ARM64 上正确构建和运行。


两条主路:Go 和 Rust 的交叉编译

对于 Go 和 Rust,情况相对乐观,两个语言都有成熟的跨平台交叉编译支持。

Go 的迁移方式

Go 官方把 ARM64 列为一类支持架构,交叉编译只需要在 Debian 上额外安装crossbuild-essential-arm64,然后把原来的go build替换为:

GOARCH=arm64CGO_ENABLED=1go build

注意CGO_ENABLED=1是必须显式指定的,因为交叉编译时 Go 默认会关掉 cgo。他们的做法是把原来的单次构建改成一个循环,同时为amd64arm64各跑一遍,产出的二进制再跑测试框架验证。

Rust 的迁移方式

Rust 的交叉编译支持同样完善。同样先安装crossbuild-essential-arm64,然后在cargo buildrustc里指定--target aarch64-unknown-linux-gnu即可。

唯一需要留意的地方:如果你的包依赖了很多第三方 crate,每一个 crate 都需要能正确交叉编译。依赖树越深,碰到问题 crate 的概率就越高。


第三条路:QEMU 用户态仿真

C、C++ 以及其他一些语言在交叉编译时就没那么顺了——调CCLD各种环境变量往往费时费力,还不一定能覆盖所有情况。

他们选择的方案是QEMU 用户态仿真(user-space emulation),核心思路是:不去改动构建工具链,而是提供一个仿真层,让 x86 机器(包括开发者的 MacBook)能直接"运行" ARM64 程序。

具体实现借助了 Docker。目标是让开发者能直接docker run进入一个 ARM64 环境,就像这样:

host$uname-mx86_64 host$dockerrun--rm-itstretch-arm64/master:latest guest# uname -maarch64

在 x86 的宿主机上,uname -m输出aarch64,仿真层对开发者完全透明。

实现方式的关键在于一个打了补丁的qemu-user。这个补丁让 QEMU 在每次execve系统调用时自动把仿真器前置进去,效果类似于 Linux 内核的binfmt_misc机制——每个新进程都会被自动带入仿真环境,整个容器因此形成一个自洽的 ARM64 沙盒。

Dockerfile 的核心结构是这样的:

# 内部构建的带补丁 QEMU FROM qemu-aarch64/master:latest as qemu # ARM64 版 Debian 基础镜像 FROM arm64v8/debian@sha256:841bbe... # 把仿真器复制进来,设为 ENTRYPOINT COPY --from=qemu /qemu-aarch64 /qemu-aarch64 ENTRYPOINT ["/qemu-aarch64", "--execve", "/qemu-aarch64"]

有了这个镜像,99% 以上的内部代码库都能在仿真环境里正常构建和测试。


真正让人头疼的:四个技术坑

方案看起来很美,但落地时碰到了几个有意思的问题。

坑一:环境变量失效

开发者最早反映的问题之一是LD_LIBRARY_PATH不生效。排查之后发现问题不只是这一个变量——所有通过命令行或export设置的环境变量,都无法传递进qemu-user进程。

根源是 Dockerfile 里的一行setcap

RUN setcap cap_setuid,cap_setgid+ep /qemu-aarch64

这行命令是为了让容器内的sudo能正常工作,但它同时阻断了环境变量的透传。两个需求直接冲突,没有两全的解法,最终只能告知开发者:在容器内,sudo和环境变量传递只能选其一。

坑二:Go 程序不定时崩溃

CI 系统里跑了大量 Go 代码,很快发现一个规律:Go 程序会以不可预测的间隔发生 segfault。

定位到原因是Go 运行时与 QEMU 的多线程兼容性问题。Go 的 goroutine 调度器会自由地把 goroutine 分配到不同系统线程上,而 QEMU 的用户态仿真在处理多线程时存在已知问题,上游也明确表示短期内不会修复。

他们的解决方案很实用:在 Go 二进制的.deb安装后脚本里,检测当前是否处于 ARM64 仿真环境,如果是,就通过taskset把进程限制为只使用单个 CPU:

# 检测是否在 ARM64 仿真下运行if["$(uname-m)"="aarch64"]&&["$(uname-r|grep-cqemu)"-gt0];thentaskset-c0"$@"fi

单核运行性能有损耗,但在仿真环境里本来就慢,慢点跑总比随机崩强。加了这个限制之后,随机崩溃降为零。

坑三:动态库加载顺序不一致

Cloudflare 有一个习惯:不覆盖系统目录/usr/lib下的库,而是把自己的最新版本装在/usr/local/lib,保持系统库的稳定性。

这套方案在 x86 上工作了很久,到 ARM64 就出了问题:有团队反映 ARM64 版本的程序无法加载正确的动态库符号。

排查下来,发现根因藏在 Debian 的一个细节里。动态链接器通过/etc/ld.so.conf.d/目录下的文件来确定库的搜索顺序,而这个目录是按文件名字母序遍历的。

在 x86_64 机器上,目录内容是:

libc.conf ← 含 /usr/local/lib,字母序靠前,先搜索 x86_64-linux-gnu.conf ← 含 /usr/lib/x86_64-linux-gnu,后搜索

在 ARM64 机器上,目录内容变成了:

aarch64-linux-gnu.conf ← 含 /usr/lib/aarch64-linux-gnu,字母序靠前,先搜索 libc.conf ← 含 /usr/local/lib,后搜索

a排在l前面——这个字母序的差异,导致 ARM64 上系统库的优先级高于/usr/local/lib,而 x86 上恰好相反。同样的代码,行为截然不同。

解决方案是不动系统配置,改为在链接器参数里显式加--rpath /usr/local/lib,强制运行时先从这里搜索。

值得一提的是,这个问题在仿真环境和物理 ARM64 机器上都存在,说明仿真层在这一点上是忠实地复现了真实环境的行为。

坑四:少数包仍然需要原生编译

99% 的包通过交叉编译或仿真解决了,剩下的 1% 是真正的硬骨头。

llvm为例,它的构建高度并行化,在原生 x86 机器上跑得很快,但一旦套上仿真层,并行度反而成了负担,构建时间超过 6 小时。还有一些包调用了 QEMU 尚未实现的系统调用,直接失败。

对这部分包,他们的选择是给开发者分配少量真实的 ARM64 机器,以及一台专用的原生 ARM64 CI 节点。这样做的代价是从可用机器池里划走了一些资源,但对于长尾问题来说,这是性价比最高的处理方式。


如何推动整个工程团队跟进

技术方案定了之后,还有一个不小的工程管理问题:数百个包分散在几十个团队手里,怎么让大家都动起来。

初期由移植团队承包了所有包的 ARM64 构建工作,同时与各包的维护者密切协作,在代码变动时保持同步。等 ARM64 平台被评估为生产就绪之后,他们整理了一套自助操作文档,向全工程部门发出了"把 ARM64 作为一等公民支持"的要求,之后各个团队自行负责自己包的 ARM64 兼容性。


这件事给出的几个判断

回顾整个迁移过程,有几个判断值得记下来。

交叉编译能解决大多数问题,但不是万能的。Go 和 Rust 的交叉编译体验已经很好,但仍然有依赖、crate 兼容性等边界情况。对于 C/C++ 这类语言,交叉编译工具链的配置复杂度明显更高。

仿真层是务实的折中。QEMU 用户态仿真让团队不需要为每个开发者配一台 ARM64 机器,大幅降低了迁移成本。代价是引入了一些仿真特有的问题(环境变量透传、多线程不稳定),需要有人专门去踩和处理。

架构差异往往藏在细节里。动态库搜索顺序这个问题,在 x86 上完全感知不到,到 ARM64 就直接导致运行失败。这类问题不是逻辑 bug,而是对平台差异不够了解的结果,只有在真实的 ARM64 环境下跑才能暴露。

原生编译始终是最可靠的。仿真和交叉编译都是务实的过渡手段。长期来看,随着 ARM64 开发机和 CI 机器越来越普及,直接在原生环境里编译和测试,才是最终的稳态。


http://www.jsqmd.com/news/698730/

相关文章:

  • 【Kubernetes专项】温故而知新,重温技术原理(1)
  • Ubuntu 22.04 系统上完整安装 ROS 2 Humble
  • 告别Express?用Hono在Cloudflare Workers上5分钟搭建一个超快API
  • 2026年天津新能源汽车推荐去哪里买?101汽车文化广场一站式选车体验深度评测 - 优质企业观察收录
  • 苹果触控板在Windows上的完美重生:mac-precision-touchpad开源驱动深度解析
  • 缠论分析太复杂?ChanlunX:3分钟让你从新手变高手!
  • 终极指南:Switch大气层系统1.7.1完整安装与功能解锁
  • 基于SSH的多跳远程访问工具PKURemote:原理、实现与配置管理
  • Klipper共振补偿:彻底解决3D打印“幽灵纹路“的专业指南
  • D6.2.熟练使用kubernetes的高级调度策略实战(nodeSelector、Pod亲和反亲和、污点及容忍)
  • 2026年天津新能源汽车推荐去哪里?101汽车文化广场一站式选购指南 - 优质企业观察收录
  • 3分钟精通TrollInstallerX:iOS 14-16.6.1设备安全安装TrollStore终极指南
  • InkOS:基于多Agent协作与长期记忆的AI小说创作系统深度解析
  • real-anime-z创意拓展:结合‘雨景’‘霓虹’‘樱花’等氛围词激发新构图
  • Botty:暗黑2重制版自动化助手,解放双手的智能刷宝方案
  • 从 IP 包到 HTTP 请求,Cloudflare 的 Oxy 代理框架是怎么做到
  • 终极指南:让Apple触控板在Windows上完美运行
  • 别再手动抄数据了!手把手教你用WinCC用户归档+SQL Server自动生成报表(附VBS脚本)
  • 以太网端口的ESD防护器件选型
  • 三步快速对接 gpt-image-2 图像生成 API 教程
  • Windows 11上Autopsy 4.19.3性能调优实战:从卡顿到流畅,我调整了这两个关键设置
  • 服务不停,升级照常:Cloudflare 是怎么做到零中断重启的
  • Ryujinx:终极Switch模拟器完整使用指南
  • git中,实用命令集合
  • Windows平台下构建定制化GDAL-C++开发环境:从核心依赖到高级扩展
  • 深度体验:8款AI网课总结工具使用心得,看看哪款适合你?
  • Unity UI粒子特效完整解决方案:高效实现专业级视觉效果
  • 告别软件轮询!用STM32G474的COMP比较器实现纳秒级硬件过压保护(CubeMX配置详解)
  • iOS与tvOS非越狱自定义工具Misaka深度解析与实战指南
  • 5分钟掌握AI纹理生成:智能法线贴图工具的完整指南