国产化软硬件适配这两年从“可选项”变成了很多团队的“必答题”,尤其是在信创环境下,把一套分布式数据库在国产 CPU 加国产操作系统上从源码跑起来,几乎是每个基础架构团队都会碰到的场景。这次我拿到的任务是在 Kylin V10 操作系统、ARM 架构服务器上,从源码编译一套 OceanBase 开源版。标题看着简单,实际动手才知道坑有多密:依赖版本对不上、编译器行为差异、ARM 特有的指令集适配、构建脚本里的硬编码路径,随便一个都能让你卡上大半天。这篇就把我完整的实操过程、踩过的坑和最后的解决方案摊开讲清楚,从环境准备到编译成功再到验证跑通,每一步都给到可复现的命令和参数。适合正在做国产化适配的后端工程师、DBA、基础架构同学参考,哪怕你之前没碰过 OceanBase 源码,跟着走也能少走弯路。
1. 整体方案设计与编译路线拆解
1.1 为什么选择源码编译而不是直接用现成包
很多人第一反应是找现成的 RPM 包或者官方预编译产物,省事。但在 Kylin V10 加 ARM 这个组合下,现成包往往不靠谱。原因有三层:第一,官方预编译产物大多面向 x86_64 的通用 Linux 发行版,glibc 版本、依赖库路径跟 Kylin V10 未必对得上,直接装大概率报动态库缺失;第二,ARM 平台上不同芯片厂商的微架构差异不小,预编译包的编译选项未必针对你的 CPU 做优化,性能上吃亏;第三,源码编译能让你精确控制编译参数、依赖版本和安装路径,出了问题能定位到具体环节,而不是面对一个黑盒二进制干瞪眼。
我这次的目标很明确:在一台 ARM 服务器上,用 Kylin V10 自带的工具链,把 OceanBase 开源版完整编译出来,产出的二进制能正常启动 observer 进程并完成基本的功能验证。整个路线分四步走:环境基线确认、依赖源码准备、编译构建、产物验证。每一步都有明确的验收标准,不达标不往下走,避免问题层层累积到最后无法定位。
1.2 编译方案的核心取舍
OceanBase 的构建体系用的是 CMake 加一套自研的构建脚本,依赖管理上它倾向于把第三方库源码一起拉下来编译,而不是依赖系统包管理器。这个设计对源码编译其实是好事——意味着你不用去系统里一个个找 dev 包,减少了版本冲突的概率。但它也带来一个副作用:首次构建要下载和编译大量第三方依赖,耗时很长,而且对网络和磁盘空间有要求。
我的取舍是这样的:优先用 OceanBase 自带的依赖构建脚本(也就是build.sh里那套--init逻辑)来拉取和编译第三方库,保证版本一致性;系统层面只补齐最基础的编译工具链和少数几个必须的系统库。这样既避免了手动管理几十个第三方库版本的噩梦,又不至于完全脱离系统环境。编译器方面,Kylin V10 默认的 GCC 版本需要确认,OceanBase 对 GCC 版本有下限要求,低于某个版本会直接编译失败或者产生运行时问题,这个后面会细说。
提示:不要试图用系统包管理器安装 OceanBase 的第三方依赖,版本几乎必然对不上。让构建脚本自己管依赖,是省心的关键。
1.3 环境基线的确认清单
动手之前先把基线摸清楚,这一步花十分钟,能省后面几小时。需要确认的项包括:操作系统版本号、CPU 架构与核心数、内存大小、磁盘可用空间、GCC 版本、CMake 版本、Python 版本、以及几个关键系统库的存在性。我整理了一张确认清单表,逐项打勾:
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| 系统版本 | cat /etc/os-release | Kylin V10 对应版本 |
| CPU 架构 | uname -m | aarch64 |
| CPU 核心数 | nproc | 建议 8 核以上 |
| 内存 | free -h | 建议 16G 以上 |
| 磁盘空间 | df -h | 构建目录所在分区 50G 以上 |
| GCC 版本 | gcc --version | 满足最低版本要求 |
| CMake 版本 | cmake --version | 3.x 以上 |
| Python 版本 | python3 --version | 3.6 以上 |
磁盘空间这一项特别容易被低估。OceanBase 的第三方依赖编译产物加上中间文件,轻松吃掉几十个 G,如果分区不够,编译到一半磁盘满,报错信息还特别隐晦,很容易误判成代码问题。我建议构建目录单独挂一个大分区,至少留 50G 余量。
2. 环境准备与依赖处理的关键细节
2.1 编译工具链的安装与版本核对
Kylin V10 默认装的 GCC 版本有时候偏低,而 OceanBase 对编译器版本有硬性要求。我这次环境里默认 GCC 是 7.x,编译过程中出现了 C++17 特性相关的报错,后来升级到 GCC 9 才顺利通过。安装高版本 GCC 的方式,Kylin V10 的软件源里一般有 devtoolset 或者直接有高版本 gcc 包,具体命令因源配置而异,核心是装完之后要用scl enable或者改 alternatives 把默认编译器切过去,并且确认gcc --version和g++ --version都指向新版本。
这里有个细节:光切 gcc 不够,CMake 在配置阶段会缓存编译器路径,如果你先跑了一次配置再切编译器,缓存里还是旧的。所以顺序一定是先装好、切好编译器,再开始配置构建。我吃过这个亏,切了编译器但没清 CMake 缓存,结果编译到一半链接报错,排查了半天才发现用的还是旧编译器。
CMake 版本也要留意,太老的 CMake 不认识一些新的语法特性,OceanBase 的 CMakeLists 里用了一些较新的写法。Kylin V10 自带的 CMake 如果版本不够,建议从源码装一个 3.20 以上的版本,装到独立路径再改 PATH,不要覆盖系统自带的,避免影响系统其他组件。
2.2 系统依赖库的补齐
虽然第三方库交给构建脚本管,但有几个系统级依赖是绕不开的。典型的有:libaio(异步 IO,数据库必备)、libncurses(终端界面相关)、zlib、openssl的开发包、以及numactl相关库。这些在 Kylin V10 上通过包管理器安装对应的 devel 包即可,注意 ARM 架构下包名可能带aarch64后缀,装之前用yum search确认一下。
还有一个容易被忽略的是libunwind,OceanBase 的某些组件依赖它做栈回溯,缺了会在链接阶段报错。另外bison、flex这类语法分析工具,如果构建脚本要编译某些依赖,也可能需要。我的做法是把常见的构建依赖一次性装齐,避免编译到一半停下来补包,中断后再续编有时候会有状态残留问题。
注意:安装系统包时留意不要顺手升级了 glibc 或核心系统库,Kylin V10 的系统库版本是经过适配的,随意升级可能引发系统级不稳定。只装缺的 devel 包,不动基础库。
2.3 内核参数的调整
数据库类软件对内核参数有要求,OceanBase 尤其明显。编译阶段本身对内核参数不敏感,但如果你打算编译完直接跑起来验证,那fs.aio-max-nr、共享内存相关参数、文件句柄上限这些都得提前调。我建议在编译前就把这些参数调好,因为验证阶段启动 observer 时如果参数不对,报错信息往往指向很底层的地方,容易误以为是编译产物有问题。
具体来说,fs.aio-max-nr要调大(默认值对数据库来说偏小),vm.max_map_count要调大,ulimit -n要放开到几万级别。这些调整写进/etc/sysctl.conf和 limits 配置里,重启或者sysctl -p生效。别小看这些,我见过有人编译一切顺利,结果启动时报异步 IO 初始化失败,折腾半天才发现是aio-max-nr太小。
2.4 用户与目录规划
不建议用 root 直接编译和运行,建一个专用用户更规范。目录规划上,我把源码目录、构建输出目录、安装目录分开:源码放一个路径,构建产物放另一个路径(CMake 支持 out-of-source 构建),最终安装到第三个路径。这样出问题的时候,删构建目录重来不影响源码,安装目录也能干净地整体替换。
磁盘布局上,构建目录所在分区要足够大,前面说了 50G 起步。如果条件允许,把构建目录放在本地 SSD 上,编译速度会明显快于网络存储或者机械盘,第三方依赖编译那一步对 IO 还是挺敏感的。
3. 源码获取与编译构建全流程
3.1 源码拉取与分支选择
OceanBase 开源版的源码托管在公开的代码托管平台上,拉取方式就是常规的版本控制工具克隆。分支选择上,建议选稳定的 release 分支而不是主干,主干代码变动频繁,编译脚本和依赖版本可能处于中间状态,不适合作为适配验证的基线。克隆的时候注意仓库体积不小,加上后续要拉第三方依赖,网络要稳定,最好在带宽充裕的环境下操作。
克隆完成后先别急着编译,花几分钟看一眼仓库根目录的构建说明文件和build.sh脚本的帮助信息,了解当前版本支持的编译选项。不同版本的构建脚本参数可能有差异,照搬网上旧教程的参数很可能报“未知选项”。我习惯先跑./build.sh --help把可用参数过一遍,心里有数再动手。
3.2 依赖初始化与编译参数确定
OceanBase 的构建脚本通常有一个依赖初始化步骤,会把第三方库源码拉到本地并编译。这一步耗时最长,也最容易出问题。ARM 平台上的坑主要集中在:某些第三方库的构建脚本里硬编码了 x86 相关的编译选项或者判断逻辑,导致在 aarch64 上走错分支;某些库的汇编优化代码只针对 x86 写了实现,ARM 上需要走通用 C 实现,如果构建配置没处理好会编译失败。
编译参数方面,核心关注几个:目标架构(确保是 aarch64)、优化级别、是否开启调试符号。验证性质的编译我建议带上调试符号(-g),出问题时栈信息更完整,方便定位。优化级别用默认的就行,没必要为了追求编译速度降到-O0,某些代码在低优化级别下反而会触发一些奇怪的编译错误。
并行编译的 job 数按 CPU 核心数来,make -j$(nproc)是常规操作,但内存要跟得上,每个编译进程大概吃 1-2G 内存,16G 内存配 8 核比较稳妥,核多内存少的话并行度要降下来,否则会因为内存不足被系统杀掉编译进程,报错信息还看不出是内存问题。
3.3 编译过程中的关键节点与耗时预期
整个编译过程可以分成几个阶段:依赖库编译、主工程配置、主工程编译、链接。依赖库编译阶段耗时占比很大,视机器性能可能从几十分钟到几小时不等。主工程编译阶段,C++ 代码量大,模板实例化多,也是耗时大户。我的经验是,8 核 16G 的 ARM 机器,完整编译一遍大概需要两到四小时,具体看磁盘和 CPU 性能。
编译过程中要盯住输出,尤其是警告信息。有些警告在 x86 上无害,在 ARM 上可能暗示对齐问题或者类型宽度问题。比如指针转整型的警告,在 64 位 ARM 上指针是 8 字节,如果代码里用 4 字节整型接指针,就会截断,这类问题在编译期表现为警告,运行期可能变成崩溃。看到这类警告不要放过,记下来后面验证阶段重点观察。
链接阶段是另一个容易出问题的节点。ARM 平台上链接器对某些重定位类型的处理跟 x86 不同,如果第三方库编译时用了不兼容的编译选项,链接阶段会报重定位错误。遇到这类错误,先确认报错的库是不是构建脚本编译出来的,如果是系统库,检查是不是混用了不同版本。
3.4 编译产物的安装与目录结构
编译成功后执行安装步骤,产物会落到指定的安装目录。安装完成后先别急着启动,检查一下目录结构是否完整:可执行文件、配置文件模板、依赖的动态库是否都在预期位置。用file命令确认可执行文件的目标架构是 aarch64,用ldd检查动态库依赖是否都能解析到,有没有“not found”的项。
ldd输出里如果有找不到的库,先看是不是安装目录下的库没被加入库搜索路径。OceanBase 的启动脚本一般会设置LD_LIBRARY_PATH,但如果你手动执行二进制,就得自己设。我建议把安装目录下的库路径写进环境变量或者 ldconfig 配置,避免每次手动指定。
4. 编译后验证与常见问题排查实录
4.1 启动验证与基本功能检查
编译产物验证分层次来:第一层是二进制能跑起来,observer --version之类的命令能正常输出版本信息,说明动态库链接没问题;第二层是能初始化并启动一个单机实例,完成建库建表插入查询的基本闭环;第三层才是性能和多节点相关的验证,那个不属于编译适配的范畴,先不做。
启动单机实例时,配置文件里的路径、端口、内存参数都要按实际环境改。ARM 平台上内存参数尤其要注意,不要照搬 x86 环境的配置,先给小一点的值把实例拉起来,确认没问题再逐步调大。启动过程中观察日志,重点看有没有跟架构相关的异常,比如对齐错误、非法指令之类的,这些往往指向编译选项或者第三方库的适配问题。
4.2 典型编译错误与排查思路
我把这次遇到的以及社区里常见的编译问题整理成了一张速查表,按错误现象、可能原因、解决方向来组织:
| 错误现象 | 可能原因 | 解决方向 |
|---|---|---|
| C++17 特性报错 | GCC 版本过低 | 升级 GCC 并切换默认编译器 |
| 找不到某第三方库头文件 | 依赖初始化未完成或路径错误 | 重跑依赖初始化,检查构建脚本参数 |
| 链接报重定位错误 | 第三方库编译选项与主工程不兼容 | 清理该库重新编译,统一编译选项 |
| 编译进程被 killed | 内存不足 | 降低并行度,减少-j数值 |
| 非法指令运行时崩溃 | 编译时用了当前 CPU 不支持的指令集 | 调整编译选项,去掉激进优化 |
| 动态库 not found | 库搜索路径未配置 | 设置LD_LIBRARY_PATH或 ldconfig |
“非法指令”这一类问题在 ARM 上要特别留意。ARM 不同代际的 CPU 支持的指令集不同,如果编译时用了较新的指令集优化,而运行环境 CPU 较老,就会在运行时崩。解决办法是编译时指定合适的目标架构参数,不要盲目追求最新指令集。这个参数在构建脚本里一般可以配置,具体写法看脚本帮助。
4.3 实操心得与避坑要点
第一个心得:编译前把环境基线确认表逐项过一遍,尤其是 GCC 版本和磁盘空间,这两项出问题的概率最高,而且报错信息最不直观。第二个心得:依赖初始化阶段如果失败,不要在原目录上反复重试,先清理干净再重来,残留的中间状态会让后续编译行为变得不可预测。第三个心得:编译日志一定要完整保存,用tee把输出同时写到文件,出问题时能回溯到具体是哪个文件哪一行报的错,比盯着终端滚屏强太多。
还有一个容易被忽略的点:编译时间戳和缓存。如果你改了编译参数重新编译,最好先清理构建目录,CMake 的增量构建在某些参数变更下不会自动重新编译所有目标,导致产物里混着新旧两种编译选项生成的目标文件,运行行为诡异。宁可多花时间全量重编,也不要留这种隐患。
提示:验证阶段如果遇到难以定位的崩溃,先用带调试符号的产物跑,拿到完整栈信息再分析,比盲目猜测高效得多。
最后分享一个我个人的习惯:整个编译适配过程用脚本记录下来,从环境检查、依赖安装、参数配置到编译验证,每一步都写成可重复执行的脚本。这样换一台机器或者换一个版本,改几个变量就能重跑,比对着文档手动敲命令可靠得多。国产化适配这种活儿,可重复性本身就是一种质量保证。