鲲鹏 920 arm64 机器适配经验
文章简介:介绍迁移鲲鹏 arm64 架构的方案及遇到的坑
背景介绍
项目背景
客户要求 arm64 机器的需求。
ARM64 指令集特性
AMD64 到 ARM64 的迁移策略
由于容器化,需要支持 k8s 在 arm64 下运行,所有使用的容器支持 amd64+arm64;有源码的项目采用重新构建的方式,无源码且只支持 amd64 的项目采用 binfmt_misc 方式运行;最终完成全部迁移过程。
准备工作
看看我们的硬件信息
|
|
安装和配置 ARM64 版本的 Kubernetes
当前社区的 k8s 发行版已经支持 arm64,根据社区的文档安装即可
迁移过程中的语言挑战
华为有一本鲲鹏软件迁移一本通,可以后续看一下
Go 语言应用的特殊注意事项
golang 从 1.5 开始支持 cross compile, arm 的支持进展见这里。
对于纯 golang 编写的项目,可通过简单的修改 CICD 适配构建 arm64 架构的可执行文件即可。GOOS=linux GOARCH=arm64 go build ...
对于使用了 cgo 的项目,则需要付出更多努力。使用 gcc 在 go build 过程中编译的项目,需要修改编译方式,做 crosscompile,一个例子比如使用了 sqlite 的项目交叉编译。一些项目在项目中已经将多种架构的动态链接库/静态链接库文件 commit 到仓库中,运行时直接使用即可,比如 confluent-kafka-go。部分使用了 cgo 项目可能不支持 arm64,比如 github.com/bytedance/sonic
是不支持 arm64 的,需要寻找替代方案,比如替换成标准库或者其他支持 arm64 的库(如 github.com/json-iterator/go
)
Python 项目的适配和修改
修改 python 版本为支持 arm64 的 python 版本,python2.7 以及 python 3 都有比较好的 arm64 支持
含 C 模块或全 C 模块的迁移,下载模块源码,并使用 -fsigned-char
选项重新编译。另外 python 官方提供了一种能够方便构建不同架构的 wheel 的基础设施(基于 docker)pypa/manylinux
Java 应用的迁移考虑
Java 不同于一般的编译语言或解释型语言。它首先将源代码编译成字节码,再依赖各种不同平台上的虚拟机来解释执行字节码,从而具有“一次编写,到处运行”的跨平台特性。但是 java 支持的 JNI 可以在 java 中调用 c/c++ 等其他语言编写的动态链接库,这种本地程序是不跨平台的,导致不能直接采用更换 arm64 jre 不修改程序的方式做迁移。
更换 arm64 架构的 JRE
如下图所示,对于纯 Java 的程序,可以考虑直接更换 ARM64 的 JRE 来运行服务。如果第三方库是用到了 jni 调用本地方法,就需要对这个库适配 arm64
jni
如果是开源软件,直接基于代码做 arm64 适配,如果为非开源项目,则需要寻找替代方案、或寻求商业支持
设置 JVM 参数解决程序运行是改动点
三大经验:首先,完成一次 Full GC 后,应该释放出 70% 的堆空间(30% 的空间仍然占用)。其次,假设老年代存活对象 (即 Full GC 后老年代内存占用) 大小为 X,建议堆的总大小是 X 的三到四倍,年轻代的大小是 X 的 1 到 1.5 倍,老年代的大小是 X 的两到三倍,永久代的大小是 X 的 1.2 到 1.5 倍。第三,JDK 官方的建议是年轻代大小占整个堆空间大小的 3/8 左右。
在具体的项目过程中,会有如下差异:
- 线程大小的 Xss 参数在 ARM 上默认值是 2m,在 x86 为 1m。因此如果线程开的太多,就需要调整 Xss 参数大小,防止出现耗时。
- 由于 ARM 和 x86 指令集的差异性,导致 JDK 的 JIT 编译存在差异。可以通过设置参数 ReservedCodeCacheSize,调整 CodeCache 大小。
C/C++ 等编译型语言如何迁移
不过,C/C++ 代码在迁移中也会有诸多问题存在,最具代表性的五类迁移问题如下:
1、编译脚本和编译选项的移植。不同的架构平台会有独特的编译选项支持硬件特性,与当前编译平台属性强相关这种带有架构属性的编译选项需要进行移植,这些编译选项一般以–m 开头;
2、编译宏的移植。编译宏的作用是确定平台下需要执行哪个分支代码,一般分为 x86 自定义宏和用户自定义的宏。两类宏的编译移植方式各不相同;
3、builtin 函数问题。builtin 函数是编译器自定义的函数,有较好的性能,可以实现一些简单快捷的功能,根据相应需求进行使用优化,助力程序编写;
4、内联汇编移植,常用迁移方法有汇编指令方式替换以及 builtin 函数替换两种;
5、SSE intrinsic 函数移植。一般在多媒体技术开发以及数学矩阵库中应用较多的 SSE intrinsic 函数移植较为复杂,为重难点。比如 clickhouse 中使用到了 SIMD 指令,需要替换成 arm64 下对应的使用方式,并且因为鲲鹏已经是比较旧的指令集了,对于一些新的指令不支持导致 clickhouse 报错非法指令集,需要删除新的指令集特性重新编译,或者降级 clickhouse 版本到未使用新指令级特性的版本。
非开源项目的迁移挑战
对于非开源商业项目,可以直接找商业公司寻求支持。但是对于部分非开源商业项目,已经过了维护期,就需要我们自己想办法迁移了。
比较显而易见的方式是使用 qemu 在 arm64 上直接执行 amd64 的二进制可执行程序。支持的原理可以简单理解为,linux 提供了一种机制,使内核能够将不支持的 bin 格式转交给虚拟机/转换程序执行,详细见 binfmt_misc, 注意此中方法在部分应用下是可用的,比如 golang 的二进制程序、部分 c 程序,对于像 redis、java 等程序,会有 segment fault 或者卡住不可用的情况,需要亲自测试确认一下应用是否可用。
使用 Docker 构建支持多架构的镜像
期望要给镜像支持多个架构,这样由客户端决定拉取哪一种架构的镜像。 根据 docker 官方的文档描述,使用 docker 构建多架构 docker image 一般有如下几种方式:
- 使用内核支持的 QEMU emulation
- 创建一个 builder,后端为多个不同架构的物理机
- 使用 cross-compile 在本地架构构建不同架构的可执行程序
不同的方式的使用条件不同,我们逐个分析。
使用内核支持的 QEMU emulation
使用内核支持的 qemu emulation,可以 在本地使用 qemu 启动不同的架构的类似 vm 的进程,在对应架构上执行代码。由于是基于 qemu emulation,性能会比较低,对于一些 cpu 密集型的任务比如编译、压缩、解压速度会有很大的影响。
|
|
默认的 docker driver 是不支持多架构构建的,需要使用如下方式创建一个新的 builder。
|
|
|
|
在编译 arm java 程序时候,可能会遇到 hang 的情况,这种方法需要实际试验一下看是否可行。
创建一个 builder,后端为多个不同架构的物理机
此种方式首先需要多台不同种架构的硬件机器。构建过程会在对应架构机器中执行,比如 arm64 会在 arm64 的机器中执行,amd64 会在 amd64 的机器中执行,所以有更好的性能、更原生的体验,支持的场景更多一些。
使用如下方式在本地构建一个支持 amd64 和 arm64 的 buildkit:
|
|
使用 cross-compile 在本地架构构建不同架构的可执行程序
如果编程语言支持 cross-compilation,在不同的架构构建过程中,可以指定某个 stage 在当前机器架构中构建跨架构的二进制程序,后复制到运行时 stage 中。
官方文档见这里
如下为一个例子:
|
|
|
|
性能优化和调整
故障排除
clickhouse Illegal instruction (core dumped)
原因可以追溯上游 issue 上游的镜像构建可能在较新的机器中编译,新机器可能支持更多的特性,而 kunpeng ARMv8.2 的指令集无此特性导致不能运行。
可以切换到版本 clickhouse/clickhouse-server:23.3.9.55
或者 bitnami/clickhouse:23.3.9-debian-11-r0
,2024.03.05 测试可用。
k3s-root binaries cause segmentation fault on aarch64 nodes with 64k page size
|
|
OS | 页表选择 | VA/PA BITS | NR_CPUS | NODES_SHIFT(NODES) |
---|---|---|---|---|
RHEL/CentOS 7 Series | 64K | 48 | 4096 | 2 (4) |
RHEL/CentOS 8 Series | 64K | 48/52 | 4096 | 3 (8) |
RHEL/CentOS 9 Series | 4K | 48 | 4096 | 6 (64) |
SLE12-SP1-ARM | 64K | 42 | 128 | 2 (4) |
SLE12-SP2 | 4K | 48 | 128 | 2 (4) |
SLE12-SP3/SP4 | 4K | 48 | 256 | 2 (4) |
SLE15-SP1 | 4K | 48 | 480 | 2 (4) |
SLE15-SP2/SP3/SP4 | 4K | 48 | 768 | 6 (64) |
SLE15-SP4 (64K) | 64K | 52 | 768 | 6 (64) |
ubuntu 20.04 | 4K | |||
uos 1040d | 64K | |||
kylinv10sp3 | 64K | |||
openEuler 20.03 | 64K | |||
openEuler 22.03 | 4K |
dockerhub 中镜像几乎都为在 4k pagesize 中编译的;当前 CI arm64 CI pagesize 为 4k;长期看新的操作系统几乎都为 4k,决定全栈 4k,不支持的操作系统通过重新编译 4k 的 linux 内核支持。
redis Unsupported system page size
|
|
出这个错误的原因是因为 jemalloc 是在编译时确定 pagesize 的大小;在 pagesize 4k 的环境中编译放到 64k 环境中运行,即会报错。详细信息可以看 这里。基于当前遇到的问题,决定重新编译内核,改成 4k 的环境。
WARNING Your kernel has a bug that could lead to data corruption during background save. Please upgrade to the latest stable kernel
编译内核 到4.19.90
出现新的问题,需要在 redis common 配置文件中添加
ignore-warnings ARM64-COW-BUG
|
|
这是一个内核 bug:
|
|
换了新的内核 4.19.x 修复。
chrome/chromium Trace/breakpoint trap (core dumped)
错误原因同样是因为 pagesize,chromium 社区对 pagesize 64k 的支持不感兴趣,并且 chromium 当前由于SlotSpanMetadata 还不支持 64k,导致 chrome/chromium 是不支持 64k pagesize 的,如果想在 arm64 下运行,pagesize 要求 4k。同样的,还有一些类似的 issue:
- [https://github.com/electron/electron/issues/25387](Electron segmentation fault on CentOS7 aarch64(arm64))
- browserless When the kernel PAGESIZE is 64k
postgres 在 pagesize 64k 下的 sql 查询会比 4k 下慢几个数量级
具体可以看这里,此文中说同一个 sql/同数据量下, pg 在 64k 的执行时间是 4k 下的 80+ 倍。
hadoop 版本 3.1.1 不支持 arm64,需要重新源码编译
TODO:
学到的经验和教训
- linux kernel pagesize 尽量选择 4k,问题会少很多
- 善用 docker multiarch build