Greptime 車雲一體化解決方案顛覆了從前傳統的車雲協同模式,採用更加低成本、高效率的方案來滿足當前的市場需求。其中 GreptimeDB Edge 作為核心元件,專為車機環境量身打造。本文旨在詳盡探討在 Android 平臺利用 Rust 語言進行開發過程中所積累的經驗和教訓。
交叉編譯
在車機場景下,GreptimeDB Edge 通常以服務的形式部署在 Android 環境上,這要求我們將其編譯成適用於 Android 平臺的可執行檔案。一個初步的方案可能是購置一款 Android 開發板,安裝 Rust 工具鏈以進行編譯工作。然而,這種做法可能會面臨以下挑戰:
在 Android 開發板上配置 Rust 編譯環境可能比較複雜(作者實際也沒配置過);
多數 Android 開發板的 CPU 效能較弱,編譯大型專案時速度緩慢,效率低下;
本地 Android API 版本可能與目標裝置上的 API 版本存在差異,甚至 CPU 架構也可能不同,從而導致相容性問題。
相對而言,交叉編譯提供了一個更為高效的替代方案。它允許開發者在一個系統平臺上(例如 x86 的 PC)編譯出能在另一種系統平臺上(例如 ARM 移動裝置)執行的程式,這在目標系統上直接編譯困難時尤其有用。
Rust 對交叉編譯的支援非常出色,加之 Android NDK 提供了必要的工具鏈和庫,這進一步簡化了交叉編譯的過程。由於我們的開發或編譯環境通常是 macOS 或 Linux,所以選擇透過交叉編譯的方式來生成 Android 可執行檔案是一個理想的解決方案。
Rust 編譯
首先,我們需要大致瞭解一下 Rust 編譯過程。Rustc 先把 Rust 程式碼編譯為 LLVM-IR,然後再由 LLVM 將 LLVM-IR 編譯為各個平臺的二進位制,最終由 linker 連結在一起,生成最終的二進位制檔案。
Rustc 是 Rust 的編譯器,以 LLVM 作為後端(也可以說 Rustc 是 LLVM 的前端)。
下面是一個簡化版本的 Rust 編譯架構圖:
(圖 1 :簡化版本的 Rust 編譯架構圖)
GreptimeDB 交叉編譯實戰
GreptimeDB Edge 是以開源版本的 GreptimeDB 為核心進行構建。所以,我們接下來,以開源版本的 GreptimeDB 為例, 一步一步向大家展示如何在 x86 Linux 上進行交叉編譯,生成 aarch64-linux-android 架構的可執行檔案。
首先安裝 Android NDK,下載地址為: https://developer.android.com/ndk/downloads?hl=zh-cn。此外設定一個環境變數,方便後續操作,如下所示:
export ANDROID_NDK_HOME=<YOUR_NDK_ROOT>
示例
export ANDROID_NDK_HOME=/home/fys/soft/ndk/android-ndk-r25c
接下來,從 GitHub 上拉取 GreptimeDB 的原始碼:
git clone https://github.com/GreptimeTeam/greptimedb.git --depth 1
然後,新增 Target 到 Rust 工具鏈是實現跨平臺編譯的關鍵步驟。這允許 Rustc 將中間表示層 LLVM-IR 程式碼編譯成目標平臺的機器語言。在這個例子中,目標平臺架構是 aarch64-linux-android,在 GreptimeDB 專案根目錄下執行以下命令:
rustup target add aarch64-linux-android
Rust 平臺支援詳見這裡。
這時候,嘗試編譯可能會報錯: “-lgcc” 找不到。原因是:Android NDK 的 libgcc.a 已經被 libunwind.a 替代,解決方案是複製一份 libunwind.a 並重新命名為 libgcc.a,詳見 Rust blog。
具體路徑可能隨著ndk版本的不同,需要改動。
cd $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64/
cp libunwind.a libgcc.a
在僅涉及 Rust 語言的專案中,開發者通常需要配置連結器(Linker)和歸檔器(AR)。然而,Rust 支援在構建指令碼(build.rs)中執行構建任務,因此在 Rust 專案的編譯過程中,可能需要整合 C 和 C++ 等其他語言的編譯工作。這通常需要向編譯工具(如 cc 或 cmake 提供一些必要的資訊,包括編譯器路徑(CC 和 CXX)、庫檔案和標頭檔案的位置等,這一過程往往較為複雜。
cargo-ndk 這個專案幫我們解決了大部分問題。透過執行以下命令,就可以編譯出適用於 aarch64-linux-android 平臺的 GreptimeDB 二進位制程式。
cargo ndk --platform 30 -t aarch64-linux-android build --bin greptime --release
此外,針對那些不相容特定目標平臺的庫,處理起來確實較為棘手。一種解決方案是替換為相容的庫;如果涉及到功能並非必需,可以使用 feature guard ,在編譯階段將其去掉。
在編譯過程中,如果遇到錯誤提示缺少 protobuf 庫或其他,正確安裝即可。
常見問題
之前遇到一個問題,當開啟 LTO 最佳化時,交叉編譯 GreptimeDB 就會失敗。報錯資訊如下所示:
= note: ld.lld: error: duplicate symbol: pthread_atfork
>>> defined at crtbegin.c
>>> /home/fys/soft/ndk/android-ndk-r26d/toolchains/llvm/prebuilt/linux-x86_64/bin/../sysroot/usr/lib/aarch64-linux-android/23/crtbegin_dynamic.o:(pthread_atfork)
>>> defined at build_jemalloc.6cd863fbc26b10-cgu.0
>>> /home/fys/source/build_jemalloc/target/aarch64-linux-android/nightly/deps/build_jemalloc-c1434931e7fc5ee2.build_jemalloc.6cd863fbc26b10-cgu.0.rcgu.o:(.text.pthread_atfork+0x0)
clang-17: error: linker command failed with exit code 1 (use -v to see invocation)
當 API level >= 21 時,Android 會提供一個 pthread_atfork 的宣告。而 tikv-jemallocator 中也有一個 pthread_atfork 的宣告,都是強符號型別,當開啟 LTO 最佳化時,就會導致了符號衝突。解決方案:將 tikv-jemallocator 中 pthread_atfork 設定為弱符號型別。
最新版本的 tikv-jemallocator 已經解決了這個問題,詳見這裡。
Backtrace on Android
在開發 GreptimeDB Edge 專案的過程中,我們觀察到 Rust 語言的標準庫的 backtrace 在 Android 環境中無法提供預期的堆疊資訊。具體來說,當程式 panic 時,相關的堆疊資訊未能正確捕獲,而是顯示為 unknown,這為問題的診斷帶來了極大的困擾。
問題復現
為了復現這一問題,我們編寫了一個簡化的示例程式。
在 main 方法中觸發了一個 panic,模擬程式出現異常:
fn main() {
panic!("Panic here.");
}
指定 rust-toolchain 為 stable 1.81 或者更低的版本:
[toolchain]
channel = "1.81"
交叉編譯, 生成在 Android 上執行的二進位制檔案。在此過程中,我們可以回顧並鞏固上一節的內容:
export ANDROID_NDK_HOME=<YOUR_NDK_ROOT>
rustup target add aarch64-linux-android
cargo ndk --platform 28 -t aarch64-linux-android build --release
將二進位制 push 到 Android 虛擬機器,執行:
RUST_BACKTRACE=full ./<二進位制檔案路徑>
執行結果表明,未能成功獲取到預期的 backtrace 資訊。問題復現了!
thread 'main' panicked at src/main.rs:2:5:
Panic here
stack backtrace:
0: 0x5d908f7a7535 -
1: 0x5d908f7b336b -
2: 0x5d908f7a617f -
3: 0x5d908f7a8541 -
4: 0x5d908f7a817e -
5: 0x5d908f7a8fe8 -
6: 0x5d908f7a8ed3 -
7: 0x5d908f7a7a09 -
8: 0x5d908f7a8b94 -
9: 0x5d908f7b2753 -
10: 0x5d908f7a1e0c -
11: 0x5d908f7a1db3 -
12: 0x5d908f7a1da9 -
13: 0x5d908f7a4eb9 -
14: 0x5d908f7a1e35 -
15: 0x7d6d9c64478d -
解決方案
我們先介紹一下解決方案,以便對問題原因不感興趣的的小夥伴可以跳過下一節。
升級 Rust 工具鏈版本: 建議將 rust-toolchain 版本升級至 1.82 或更高。這個問題已經在 1.82 中被修復了(下一小節會介紹修復方法)。
自定義 Panic Hook: Rust 支援透過註冊自定義的 panic hook 函式來替代預設行為。若無法升級 Rust 版本,可利用 backtrace-rs 庫設定自定義 panic hook 函式。
Rust 預設的 panic hook 函式可能無法滿足特定環境下的需求,例如,在 Android 平臺上,可能傾向於將 panic 資訊輸出到檔案或 logcat, 而預設的 panic hook 函式只是把 panic 資訊輸出到標準錯誤中。因此,很多場景下都需要我們自定義 panic hook 函式。
下面提供一個實現示例:
pub fn set_panic_hook() {
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";
cqgn.scffy.cn,cqgn.cnjiasi.cn,cqgn.xintiao78.com
cqgn.62nsfs.com,cqgn.jinduoceramics.com
std::panic::set_hook(Box::new(move |panic| {
let backtrace = backtrace::Backtrace::new();
let Some(l) = panic.location() else {
log::error!(
"Panic: {:?}, backtrace: {}{:#?}",
panic, LINE_ENDING, backtrace
);
return;
};
log::error!(
"Panic: {:?}, file: {}, line: {}, col: {}, backtrace: {}{:#?}",
panic,
l.file(),
l.line(),
l.column(),
LINE_ENDING,
backtrace,
);
}));
}
輸出的堆疊資訊如下所示(在編譯選項中去掉了 debug info, 且保留了符號表):
Panic: PanicHookInfo { payload: Any { .. }, location: Location { file: "src/main.rs", line: 3, col: 5 }, can_unwind: true, force_no_backtrace: false }, file: src/main.rs, line: 3, col: 5, backtrace:
0: 0x5a58805bdf63 - cross_compile_on_android::set_panic_hook::{{closure}}::h8ff538cfa624b522
1: 0x5a58805fb0f3 - std::panicking::rust_panic_with_hook::h1f4c9072872fa4b1
2: 0x5a58805fadb3 - std::panicking::begin_panic_handler::{{closure}}::h73465221de2f2f04
3: 0x5a58805f9689 - std::sys::backtrace::__rust_end_short_backtrace::h67f67f7cadadf1c3
4: 0x5a58805faa74 - rust_begin_unwind
5: 0x5a5880605b63 - core::panicking::panic_fmt::h394cd2a8b9d0c24d
6: 0x5a58805bdf11 - cross_compile_on_android::main::h477274cf7246f129
7: 0x5a58805bdb53 - std::sys::backtrace::__rust_begin_short_backtrace::hb593986c2bdf2ffe
8: 0x5a58805bdb49 - std::rt::lang_start::{{closure}}::hdba3573990c4d5eb
9: 0x5a58805f4519 - std::rt::lang_start_internal::h50565391ca281790
10: 0x5a58805be1f5 - main
11: 0x7e5b4020278d - __libc_init
cqgn.mxy998.com,cqgn.gjkds.com,cqgn.mbslzp.com
cqgn.sh-soyun.com,cqgn.clzyfc.com
補充說明:輸出的堆疊資訊與編譯選項也有關係。如果把二進位制中的符號表和 debug info 都去掉,會生成 unknown 的堆疊。如果保留 debug info,堆疊資訊將更詳細,但二進位制的體積會增加很多。
問題原因
接下來,我們將基於 Rust 1.81,來探究一下之前提出的問題。
前置知識
Rust 標準庫的 backtrace 依賴了 backtrace-rs 庫,並以 git submodule 的形式整合到了 Rust 標準庫中,詳見這裡。
backtrace-rs 在編譯構建時,會判斷 Android 的 API 版本。如果大於等於 21,則會啟用 dl_iterate_phdr 特性。詳見這裡(注: backtrace-rs 的版本是 Rust 1.81 依賴的版本,並不是最新版本)。
綜合以上兩點,Rust 標準庫以 git submodule 的形式引入了 backtrace-rs,但是並沒有執行 backtrace-rs 中的 build.rs 的構建邏輯,導致 dl_iterate_phdr 特性未能啟用。那麼標準庫的 backtrace 就無法在 Android 上正常工作了。
破案了!
解決方法
實際上,我們只需在標準庫中啟用 backtrace-rs 的 dl_iterate_phdr 特性即可。但是從 #120593 開始,Rust 對 Android 的最小支援 API 版本從 19 提升至 21,並且 從 21 開始,Android 就支援了 dl_iterate_phdr,具體資訊可以檢視這裡。所以我們可以在 backtrace-rs 庫中直接預設開啟 dl_iterate_phdr 特性,無需檢測 Android 的 API 版本(Rust 1.82 也是這麼修復的)。
相關 PR 連結
https://github.com/rust-lang/backtrace-rs/pull/656
https://github.com/rust-lang/rust/pull/129305
總結
交叉編譯一直是非常棘手的,可能會碰到各種各樣的問題,並沒有什麼固定的解決方案,我們總是要針對特定的問題進行處理。幸運的是,Cargo NDK 和 Android NDK 提供了一套便捷的解決方案,幫助我們有效地應對了大部分的編譯問題。
透過本文的探討,我們認識到交叉編譯在 Android 環境中的重要性,以及 Rust 編譯機制的優勢。雖然理想的編譯過程在實踐中會遇到諸多挑戰,但希望我們的經驗能為後續的開發提供一些實用的參考和啟發。
關於 Greptime
Greptime 格睿科技專注於為可觀測、物聯網及車聯網等領域提供實時、高效的資料儲存和分析服務,幫助客戶挖掘資料的深層價值。目前基於雲原生的時序資料庫 GreptimeDB 已經衍生出多款適合不同使用者的解決方案,更多資訊或 demo 展示請聯絡下方小助手(微訊號:greptime)。
歡迎對開源感興趣的朋友們參與貢獻和討論,從帶有 good first issue 標籤的 issue 開始你的開源之旅吧~期待在開源社群裡遇見你!新增小助手微信即可加入“技術交流群”與志同道合的朋友們面對面交流哦~
Star us on GitHub Now: https://github.com/GreptimeTeam/greptimedb
官網:https://greptime.cn/
文件:https://docs.greptime.cn/
Twitter: https://twitter.com/Greptime
Slack: https://greptime.com/slack
LinkedIn: https://www.linkedin.com/company/greptime/