前阵子接到一个适配任务:把 seren-0.0.21-1 这个开源指标采集工具搬到 KeyarchOS 上。KeyarchOS 是浪潮信息基于 openEuler 内核做的一款 Linux 发行版,底层继承了不少欧拉系的特性,但又和传统 CentOS 的 RPM 体系高度相通。刚开始我以为就是 yum install 一下的事,真上手才发现,glibc、openssl、systemd 的版本差异足以让人血压升高。这次任务里最关键的不是去改 seren 的代码,而是把“为什么在别的发行版能跑,到了 KeyarchOS 上就崩”这个问题彻底搞清楚。
这篇内容就以这次适配为线索,把从依赖梳理、编译打包到运行验证的完整链路掰开揉碎讲一遍。如果你手头也有类似的 Rust 或 C 语言项目要移植到 KeyarchOS、openEuler 这类系统上,不管是 x86_64 还是 aarch64 环境,都可以把这篇当一份直接能抄作业的参考。适配上最不值钱的阶段是编译报错后到处搜索,最值钱的阶段是把前置信息盘清楚,所以我会把大量篇幅放在“开始编译之前”。
1. 适配前必须想清楚的事
1.1 先搞清楚 KeyarchOS 是哪一路 Linux
很多人看到 RPM 三个字母就觉得“跟 CentOS 差不多”,这个想法在适配任务里很容易吃亏。KeyarchOS 的二进制兼容层面确实和 Red Hat 系比较接近,传统 RPM/dnf 管理,软件包路径也是 /usr/lib64、/etc/yum.repos.d 这一套,但它的用户态和内核又有明显的 openEuler 血统。openEuler 上常见的编译链接标识、默认选用的加密库版本、甚至 systemd 的某些配置分支,都和 CentOS 7、CentOS 8 有微妙差异。
我每次拿到一台新机器,第一件事不是装软件,而是先跑一轮“体检”命令,把系统底牌看清楚:
bash复制cat /etc/os-release
uname -r
ldd --version
rpm -q glibc openssl-libs ncurses-libs systemd
这几条命令分别回答了:当前系统版本是什么、内核和用户态的匹配度如何、glibc 能不能支撑新版 Rust/Go 程序、系统自带的基础库里有没有我要链接的目标版本。适配中最怕的是默认假设“glibc 肯定够新”“openssl 肯定有”,这两个一旦判断错,后面所有编译错误都会往一个错误方向排查,浪费时间。
KeyarchOS 还有一个特点:内核版本往往偏新,但用户态软件包不会像 Arch Linux 那样激进更新。这意味着 seren 这类用最新工具链编译的项目,跑在旧一点的 libssl 或 ncurses 上时,经常出现“动态库版本不满足”的提示。遇到这种情况别急着骂系统旧,先确认一下系统库和编译期 SDK 的对应关系,distro 的稳定性和开发库的滞后本来就是一对天生的矛盾。
1.2 seren-0.0.21-1 到底依赖了什么
seren 是一个 Rust 写的轻量指标采集工具,主要功能是定时抓取 CPU、内存、磁盘、进程等指标,并通过 HTTP API 对外暴露,还能在指标超出阈值时把告警推送到 Webhook。它相比 Zabbix 这类全家桶方案的体积小很多,很受边缘计算节点用户的欢迎,所以把它适配到 KeyarchOS 上是有实际价值的。不过这里我不打算假设读者一定熟悉它,反而想借这个例子讲一个通用思路:拿到一个“不明底细”的软件包,该怎么判断它需要什么。
拿到代码后第一件事不是急着 make 或 cargo build,而是先看它的构建系统。我按这个顺序做了四步:
- 看根目录的 Cargo.toml,明确 Rust edition、最低版本和直接依赖列表;
- 检查有没有 build.rs,看它是否针对 Linux 发行版做了特征判断;
- 如果已经有了预编译二进制,用 readelf 扫它的动态段,看看链接了哪些 .so;
- 跑一遍 cargo tree,把传递依赖也拉出来,避免漏掉嵌套的 crate 需要系统库。
例如 seren 依赖 openssl 和 libsystemd,那么在 KeyarchOS 上就要保证 openssl-devel 和 systemd-devel 都装了。Rust 生态里不少 crate 会通过 pkg-config 去查找系统库,如果少了对应的 .pc 文件,编译阶段直接报错。这里有个容易踩的坑:不要把目光只放在顶层依赖上,有些二级或三级 crate 会悄无声息地引入 libsqlite3、libzstd、libcurl 等系统库。你只装了主依赖,它依然会报“找不到头文件”或“无法链接”。
针对这种情况,我习惯在项目根目录跑一次:
bash复制cargo tree -e normal --prefix none | sort -u
输出里出现的每个 crate,如果是 ffi 绑定类库,就值得去它的构建脚本里确认是否需要系统库。这一步看起来花时间,但和后面反复编译报错相比,性价比极高。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与依赖补齐
2.1 搭一个最小可复现的编译环境
适配工作最忌讳直接在个人笔记本上交叉编译,然后丢到服务器上跑。我在做 KeyarchOS 适配时,第一步是在虚机里装了一个和目标环境大版本完全相同的 KeyarchOS,然后基于这个干净环境做编译和打包。这样做的好处是,交付给对方的 RPM 包是在一个可复现的环境里生成的,不容易出现“我这能跑,你那不能跑”的甩锅问题。
编译工具链直接用 dnf groupinstall 安装即可:
bash复制dnf groupinstall "Development Tools"
dnf install -y openssl-devel systemd-devel ncurses-devel pkgconfig
如果目标环境是内网隔离的,就得先解决仓库源问题。可以找局域网内网源,也可以直接用同版本的 ISO 搭建本地 yum 源,核心是让 dnf 能顺利 makecache。ISO 搭本地源的操作不复杂,把 ISO 挂载后写一个 repo 文件,指向 file:// 路径,再用 dnf clean all && dnf makecache 验证一遍。这个源要保留到整个适配完成,别装完几个包就把挂载点卸载了,后面随时可能要补装依赖。
Rust 工具链的安装建议使用 rustup,这样可以锁定版本。seren-0.0.21-1 的 Cargo.toml 大概率会声明一个 rust-version 下限,但为了减少变量,我会固定到一个稳定且略高于下限的版本,然后通过 rustup override 让当前目录强制使用该版本:
bash复制rustup toolchain install 1.70
rustup override set 1.70
这里特意强调“别一上来就装最新版”。Rust 版本之间对 FFI 定义、链接行为和 panic 信息的处理有差异,虽然 seren 这种项目不一定受影响,但既然做适配,追求的是“行为可预期”,而不是“能跑就行”。我踩过一次很深的坑:某次用最新 Rust 编译一个老 crate,编译阶段完全正常,运行十来分钟后直接段错误,后来才查到是 Rust 新版对某些静态变量的初始化顺序做了调整。锁定工具链版本,等于把这类问题直接掐死在入口。
2.2 用工具把依赖缺口一次照清楚
依赖梳理不能靠肉眼。我的执行顺序仍是前文说的那三步:先编译期依赖,再运行期依赖,最后是 dlopen 依赖。这三类依赖属于不同阶段,用到的工具也不一样。
编译期依赖靠 cargo build --release 去报错,然后针对报错用 dnf provides '*/libxxx.so*' 或者 dnf search xxx-devel 定位对应 RPM 包。比如报错里出现 openssl-sys 的 custom build 失败,就先执行:
bash复制dnf provides '*/openssl.pc'
能直接看到该文件属于 openssl-devel,再装它。一般三轮以内能把编译期依赖解决完。
运行期依赖用 ldd 看:
bash复制ldd target/release/seren
但这里要特别说明:ldd 只能看到动态加载器链接的库,对于使用 dlopen 方式在运行时加载的插件模块是看不到的。seren 如果有插件机制,那它运行时可能额外加载某个 .so,这种隐藏依赖必须在功能验证阶段手工测出来。
为了之后交付方便,我会把依赖整理成一张表,这也是这次适配里最值得带走的习惯:
| 组件 | 依赖库 | 对应的 RPM 包 |
|---|---|---|
| seren 主程序 | libssl.so.3 | openssl-libs |
| seren 主程序 | libsystemd.so.0 | systemd-libs |
| webhook 插件 | libcurl.so.4 | libcurl |
| 编译期 SDK | /usr/lib64/pkgconfig/openssl.pc | openssl-devel |
表的作用不是给人看的,而是给后续排查的人看的。实际交付时,我把这三张表放到了 README 里,后面对方运维环境缺库时,一眼就能找到答案。
3. 编译与安装的完整流程
3.1 源码编译最常见的三个坑
如果环境准备阶段做得足够细,编译本身一般一次能过。但我在实际适配中还是遇到过三个有代表性的坑,这里一个个说。
第一个坑是 openssl-sys 自动探测到错误的版本。比如系统里同时存在 openssl 1.1 和 3.0 的兼容库,但 crate 默认去找 1.1 版本,导致 API 不匹配。遇到这种事情,不要急着改代码,先通过环境变量强制行为:
bash复制export OPENSSL_NO_VENDOR=1
cargo clean && cargo build --release
OPENSSL_NO_VENDOR 的作用是让 openssl-sys 老老实实链接系统的 openssl,而不是自己下载源码编一套 vendored 版本。如果你私下想快速验证,也可以编译 vendored 版本,但正式交付时建议还是使用系统库,这样能跟随系统补丁升级,避免安全漏洞留死角。
第二个坑是链接器版本过低。老版本 gcc 携带的 GNU ld 可能不识别某些新特性,报出 relocation R_X86_64_32S against .rodata can not be used 这类错误。解决办法有两个:一是升级 binutils,二是用更快的链接器 mold。在 KeyarchOS 上安装 mold 很直接,通过 cargo 或 dnf 都能搞定。mold 不仅是性能好,有些老 binutils 处理不干净的重定位,mold 也能容忍,能省不少闹心事。
第三个坑是动态库路径。程序在编译机上能跑,拷到另一台机器上就报 error while loading shared libraries。这多半是 RPATH 和 RUNPATH 没有设置。Rust 编译时如果依赖了非标准路径下的动态库,产物的 RPATH 不会自动包含该路径。我的习惯是在编译参数里显式加上:
bash复制export RUSTFLAGS="-C link-args=-Wl,-rpath,/usr/local/lib"
cargo build --release
但要注意,这种做法只适用于把动态库放到固定路径的场景。如果希望产物可以随目录移动,更合理的方式是设置 $ORIGIN 相对路径:-Wl,-rpath,'$ORIGIN/../lib'。这样 seren 的二进制放在 bin 下,动态库放在同级 lib 下,整个目录拷到哪都能跑。
顺带提醒:不要轻易对 glibc 做静态链接。Rust 程序即使把依赖的 crate 全部静态编译进去,glibc 仍会以动态方式链接;如果强制静态链接 glibc,DNS 解析、NSS 查找用户信息时很容易出现奇怪故障。想要一个真正的静态二进制,最靠谱的方案是使用 musl 目标编译,但这要求项目本身没有引用 glibc 专有接口。seren 这种偏系统层的工具,我不建议一开始就奔着静态编译去,老老实实做 RPM 才是正道。
3.2 打成 RPM 包的 spec 文件写法
如果只是自己临时用,编译完的二进制可以直接跑;但如果要在 KeyarchOS 上正式交付,我建议打成 RPM。RPM 的好处是可以用 dnf 安装、升级、卸载,还能做依赖检查,后续交给运维团队时也更标准化。
先在构建机上安装打包工具并建立目录结构:
bash复制dnf install -y rpm-build
mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
然后写 seren.spec。一个能正常构建的基础版本大概是这样的:
spec复制Name: seren
Version: 0.0.21
Release: 1
Summary: Lightweight metrics collector and alerting tool
License: Apache-2.0
URL: https://example.com/seren
Source0: seren-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: openssl-devel
BuildRequires: systemd-devel
Requires: openssl-libs
Requires: systemd-libs
%description
Seren is a lightweight Rust-based metrics collector and alerting tool.
%prep
%setup -q
%build
export OPENSSL_NO_VENDOR=1
cargo build --release --locked
%install
install -Dm755 target/release/seren %{buildroot}/usr/bin/seren
install -Dm644 packaging/seren.service %{buildroot}/usr/lib/systemd/system/seren.service
install -Dm644 packaging/config.toml %{buildroot}/etc/seren/config.toml
%files
/usr/bin/seren
/usr/lib/systemd/system/seren.service
%config(noreplace) /etc/seren/config.toml
%post
systemctl daemon-reload
%preun
if [ $1 -eq 0 ]; then
systemctl stop seren
systemctl disable seren
fi
写 spec 文件时我有几个习惯:
- 使用
--locked强制使用 Cargo.lock,保证交付产物可复现,避免依赖漂移; install -Dm755会自动创建缺失目录,比较省事;- 配置目录用
%config(noreplace),这样用户改过的配置在 RPM 升级时不会被覆盖; - 在
%preun里卸载前停掉服务,避免出现残留进程。
构建时执行:
bash复制rpmbuild -ba ~/rpmbuild/SPECS/seren.spec
如果一切顺利,RPM 会生成在 RPMS/x86_64/ 下。用 rpm -qip 和 rpm -qlp 检查一下包的元信息和文件列表,确保文件路径、依赖声明都没有问题。
3.3 安装后的 systemd 服务注册
RPM 里已经放了 service 文件,安装后系统就能识别到 seren 服务。这个 service 文件看起来不大,但决定了程序能不能开机自启、是否能被 systemd 好好管理:下面是我常用的一份模板:
ini复制[Unit]
Description=Seren Metrics Collector
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/seren -c /etc/seren/config.toml
Restart=on-failure
RestartSec=5
User=seren
Group=seren
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/seren /var/log/seren
[Install]
WantedBy=multi-user.target
这里的重点在安全沙箱参数上。ProtectSystem=strict 会让整个文件系统变为只读,除 ReadWritePaths 明确列出的目录外都不能写。Rust 程序通常比较老实地只写自己的目录,所以用这种配置没问题。但如果你不注意,seren 可能会在某个瞬间尝试更新配置或写临时文件,结果被 systemd 静默拦截,表现为服务没有 crash 但就是没有正常产出。排查这种问题很费时间,最好的办法是提前给程序建好专用的数据目录,并明确写入 ReadWritePaths。
安装完成后启动服务,再检查运行状态:
bash复制systemctl enable seren
systemctl start seren
systemctl status seren
如果启动失败,先别急着翻日志,运行 systemd-analyze verify /usr/lib/systemd/system/seren.service 看看权限和路径声明是否合理。这个命令会直接告诉你 systemd 认为哪些配置有问题,比在日志里大海捞针快得多。
4. 运行期验证与性能摸底
4.1 先用最小配置跑通功能
安装完成并不代表适配成功。我习惯先给 seren 写一份最小配置,让它只采集最基本的几个指标,先确认主流程没有大问题:
toml复制listen = "127.0.0.1:9091"
interval = 15
collectors = ["cpu", "memory", "disk", "process"]
启动后,访问 http://127.0.0.1:9091/metrics,如果能看到 Prometheus 格式的指标,说明数据采集和 HTTP API 都正常。然后调低告警阈值,配一个测试用的 webhook 地址,再观察是否真的能收到告警。这个验证清单虽然简单,但覆盖了 seren 最核心的三条链路:采集、输出、通知。
如果 curl 能拿到数据,基本就能说明二进制主体在该系统上没问题;如果拿不到,马上抓日志:
bash复制journalctl -u seren -f
Rust 程序 panic 时会直接打出一段 backtrace,里面能看到具体是哪个 crate 的哪一行出了问题,定位速度比 C 程序快很多。这里我唯一要提醒的是:不要忽略日志里的 warning 级别内容,很多“看起来不影响”的 warning,比如配置字段不识别、时区解析失败,在长时间运行后会变成隐性 bug。
4.2 资源占用和压力测试要盯紧
适配是否成功,还得看软硬件资源消耗是否符合预期。一个本来在普通 x86 机器上占用 50MB 内存的工具,到了目标环境有可能因为其他库的实现差异,涨到 200MB。我一般在同一个虚机上分别跑 seren、Telegraf 和 Zabbix Agent,然后对比资源占用。
重点看三个指标:
- 启动后 RSS 是否随着采集轮次持续上涨。如果每轮采集内存都涨一点,且不回落,那就是内存泄漏,时间越长问题越严重;
- 采集任务本身是否在一段时间内稳定。比如 interval 配置 15 秒,每轮实际时间是否在 15 秒附近抖动严重;
- 指标接口在高并发访问时,HTTP 响应是否变慢。这个问题不一定在 seren 本身,也可能是网络栈或系统防火墙的干扰。
用 pidstat 可以做持续监控:
bash复制pidstat -u -r -p $(pgrep seren) 1
长时间稳定性测试建议至少跑 48 小时。观察 RSS 是否平稳,/proc/<pid>/fd 下的句柄数是否持续增加。只要句柄数稳定、RSS 不无节制上涨,基本可以放心。很多看似“玄学”的生产环境崩溃,根源就是某次适配后的资源泄漏,跑长时间测试真的很有必要。
4.3 跨架构验证别忽略
如果交付目标包含 arm64(KeyarchOS 在飞腾、鲲鹏服务器上很常见),那就必须在真实的 arm64 机器或虚机上再跑一遍完整流程,不能只在 x86_64 交叉编译完就交付。交叉编译时最常见的坑是项目代码对 x86 架构做了特殊优化,比如默认启用了依赖 x86 指令的汇编库,这类代码交叉编译后可能完全不变,但运行时直接非法指令。
我遇到的典型例子是某次交叉编译出来的 arm64 二进制启动就崩,用 gdb 查看才发现是某个 crate 在编译时自动启用了 CPU 特性检测,检测时误用了 intel 指令。这类问题几乎只能在真实 arm64 环境中复现。所以我的建议是:如果甲方明确要求 arm64 支持,那就要在适配计划一开始就把 arm64 构建机列入准备清单,避免后期返工。
5. 常见问题与排查技巧实录
5.1 速查表:从报错到答案
适配过程中总会遇到一些重复率极高的报错。下面这张表是我多次实践下来的总结,可以直接贴到自己的知识库:
| 报错信息 | 可能原因 | 处理方式 |
|---|---|---|
error: failed to run custom build command for openssl-sys |
openssl-devel 缺失或版本不匹配 | 安装对应版本 openssl-devel,或用 OPENSSL_DIR 指定路径 |
error while loading shared libraries: libseren_plugin.so |
RPATH 未设置,插件路径不对 | 先用 LD_LIBRARY_PATH 临时验证,确认后再修 RUNPATH |
Failed to connect to system bus |
libsystemd 版本过旧 | 升级 systemd-libs,重编 seren 使其引用新 API |
Segmentation fault with backtrace |
跨架构或 glibc 版本差异 | 在真实目标环境重新编译,不要依赖交叉编译产物 |
pkg-config not found |
基础工具链没装 | dnf install pkgconfig |
cargo: warning: unused manifest key: package.rust-version |
Cargo.toml 中 rust-version 字段格式问题 | 检查版本号是否带引号,改成 rust-version = "1.70" 这种标准格式 |
Failed to write /var/lib/seren/xxx |
systemd ProtectSystem 拦截写操作 | 在 service 的 ReadWritePaths 中显式加入 /var/lib/seren |
第2条和第7条是最容易被忽略的。因为它们不是编译期错误,而是运行期才暴露,很多新人容易直接把问题定位到程序 bug,实际上罪魁祸首是链接路径和服务沙箱。
5.2 三个我一直保留的排查习惯
第一个习惯是编译时保存日志。无论编译成功还是失败,我都会执行:
bash复制cargo build --release 2>&1 | tee build.log
这样后面排查问题可以直接全文搜索 error,不会因为输出刷屏而丢掉关键信息。有些 warning 在第一次编译时看起来无关紧要,但换一个平台或换一个版本后,它就变成了真正的错误,有日志在手会从容很多。
第二个习惯是适配阶段不随便升级工具链。Rust 工具链、系统库、Cargo.lock 三样东西最好全部固定。我有一段时间图省事,每次编译前都 rustup update,结果某一天编译出来的二进制在目标机上跑半个小时就崩溃,最后花了半天才发现是编辑器版本变化导致的行为差异。从那以后,固定版本成了铁律。
第三个习惯是交付时附带一份“环境白名单”。把系统版本、glibc 版本、openssl 版本、Rust 版本、seren 编译参数全部写清。这个文件不是为了好看,而是为了将来对方环境不一致时,能快速判断是系统差异还是程序问题。这份白名单在售后服务里救了我好几次,自己也能通过它快速复现问题。
5.3 最后一个避坑:离线环境的应急预案
KeyarchOS 有不少部署在隔离内网的项目,编译时无法访问 crates.io,也没法联网拉包。这种场景必须在规划阶段就准备好离线依赖。我的做法是先在一台能上网的同架构机器上执行:
bash复制cargo vendor vendor
这样会把所有依赖源码下载到本地 vendor 目录,同时生成 .cargo/config.toml 配置,将 cargo 的源指向这个目录。然后把整个项目目录、vendor 目录、以及所有需要的 RPM 包拷贝进内网。配置写好之后,内网里执行 cargo build --offline 就能完成编译,完全不依赖外网。
如果连 RPM 依赖也无法通过 dnf 在线安装,那还要提前准备一个完整的内网 yum 源,把 openssl-devel、systemd-devel 等 RPM 全部打包带进去。这个环节很枯燥,但一旦漏掉一个库,整个内网环境就会反复卡在同一个依赖错误上。
想到哪说到哪。这次 KeyarchOS 适配 seren-0.0.21-1,前后折腾了大约两天,难度不算大,但每一步都踩到了“版本一致性”这个暗礁上。我个人的体会是:这类适配工作,七成时间花在解决依赖和验证环境上,三成时间花在真正的编译安装上。只要前面的依赖梳理足够细,后面的过程基本是顺水推舟。最后分享一个小技巧:在关键节点上,先跑一个超级精简的可执行文件,比如只打印 hello 的 Rust 程序,确认整个工具链和动态库路径在这个发行版上没问题,再引入业务代码,能减少很多不必要的变量。这个“最小可运行验证”的思路,在做任何系统适配时都值得推广——它比任何文档都直观,也比反复试错更省时间。
