Aplikasi backend berbasis Rust umumnya andal dalam menangani memori dan runtime safety. Namun, crash yang terjadi sebelum fungsi main() dieksekusi memunculkan tantangan diagnostik tersendiri. Layanan langsung berhenti tanpa meninggalkan log kesalahan atau jejak stack di terminal, menyebabkan kontainer berada dalam siklus CrashLoopBackOff.
Gejala: CrashLoopBackOff Tanpa Log Aplikasi
Pada lingkungan Kubernetes atau Docker, service mati mendadak dengan exit code 134 (SIGABRT) atau 139 (SIGSEGV). Perintah kubectl logs atau docker logs tidak menampilkan keluaran apapun. Kondisi ini terjadi karena subsistem logging seperti tracing-subscriber atau env_logger umumnya diinisialisasi pada baris pertama fungsi main(). Jika proses terhenti di tahap C Runtime (CRT) bootstrap atau inisialisasi static linker, runtime tracing belum aktif sehingga tidak ada buffer log yang terkirim ke stdout maupun stderr.
Root Cause: Eksekusi Kode pada Tahap CRT Initialization
Sebelum sistem operasi mengeksekusi entry point Rust (main), dynamic linker (ld.so di Linux) memuat dependensi pustaka bersama dan menjalankan fungsi-fungsi yang terdaftar pada seksi .init_array pada binary ELF. Di ekosistem Rust, kode dapat dijalankan pada tahap ini melalui:
- Penggunaan crate pihak ketiga seperti
ctoryang menandai fungsi dengan atribut konstruktor global. - Pustaka FFI C/C++ dependen yang mengeksekusi
__attribute__((constructor)). - Inisialisasi static global yang melakukan operasi rentan kegagalan (misalnya alokasi dinamis kompleks, pembacaan environment variable, atau I/O jaringan).
Jika terjadi panic di dalam blok konstruktor global, mekanisme unwinding standar Rust mungkin belum sepenuhnya siap, atau panic hook kustom (std::panic::set_hook) belum terpasang. Akibatnya, runtime Rust langsung memanggil instruksi abort (SIGABRT) tanpa format error dump standar.
Langkah Diagnosis: Menangkap Crash Menggunakan strace dan LLDB
Karena log aplikasi tidak tersedia, pelacakan harus dilakukan pada level system call dan dynamic loader.
1. Deteksi System Call dengan strace
Jalankan binary secara lokal atau di dalam container debug menggunakan strace untuk memantau system call sebelum terminasi:
strace -f -e trace=process,write,rt_sigaction,rt_sigprocmask,tgkill,abort ./target/release/my-serviceOutput tipikal akan menunjukkan proses memuat file ELF, mendaftarkan sinyal, lalu langsung mengeksekusi rt_sigprocmask dan tgkill(..., SIGABRT) tanpa pernah ada system call write ke file descriptor 1 atau 2:
...
rt_sigaction(SIGABRT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x...}, NULL, 8) = 0
tgkill(12345, 12345, SIGABRT) = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=12345, si_uid=1000} ---
+++ killed by SIGABRT +++2. Pelacakan Stack Trace dengan LLDB atau rust-gdb
Gunakan debugger untuk menangkap lokasi pemanggilan abort() secara presisi. Buat build debug terlebih dahulu agar simbol tidak di-strip:
cargo build
rust-lldb ./target/debug/my-serviceDi dalam shell debugger, jalankan aplikasi:
(lldb) run
Process 12345 stopped
* thread #1, name = 'my-service', stop reason = signal SIGABRT
frame #0: 0x00007ffff7dc9884 libc.so.6`__pthread_kill_implementation + 260
(lldb) bt
* frame #0: 0x00007ffff7dc9884 libc.so.6`__pthread_kill_implementation + 260
frame #1: 0x00007ffff7d7a8ee libc.so.6`raise + 26
frame #2: 0x00007ffff7d648ff libc.so.6`abort + 211
frame #3: 0x000055555557a123 my-service`rust_panic + 147
frame #4: 0x0000555555562d5a my-service`my_service::init_config::{{closure}} + 218
frame #5: 0x0000555555562b0e my-service`ctor::ctor_function + 14
frame #6: 0x00007ffff7fe1632 ld-linux-x86-64.so.2`call_init + 146
frame #7: 0x00007ffff7fe1724 ld-linux-x86-64.so.2`_dl_init + 132
frame #8: 0x00007ffff7fcd0aa ld-linux-x86-64.so.2`_dl_start_user + 50Backtrace di atas membuktikan pemanggilan panic berasal dari call_init milik dynamic linker yang mengeksekusi ctor::ctor_function, jauh sebelum main sempat dipanggil.
Solusi: Refaktor ke Explicit Lazy Initialization dengan OnceLock
Pendekatan yang benar adalah menghapus seluruh side-effect (pembacaan konfigurasi, parsing file, atau inisialisasi pool koneksi) dari fase static allocation/konstruktor global, dan memindahkannya ke dalam main().
Contoh Masalah (Anti-pattern dengan ctor):
use ctor::ctor;
use std::env;
static mut DB_URL: Option<String> = None;
#[ctor]
fn init_config() {
// Panic jika ENV tidak ditemukan sebelum main() berjalan
let url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
unsafe {
DB_URL = Some(url);
}
}
fn main() {
println!("App started");
}Jika variabel DATABASE_URL tidak ada pada environment, program mengalami panic di level CRT, tidak mengeluarkan teks log, dan mati seketika via SIGABRT.
Solusi: Safe Startup Sequence Menggunakan std::sync::OnceLock
Gunakan std::sync::OnceLock (tersedia di Rust standar sejak 1.70) untuk state global yang thread-safe, dan kendalikan inisialisasinya secara eksplisit setelah logger aktif.
use std::env;
use std::sync::OnceLock;
static DB_URL: OnceLock<String> = OnceLock::new();
fn get_db_url() -> &'static str {
DB_URL.get().expect("DB_URL must be initialized")
}
fn init_runtime() -> Result<(), Box<dyn std::error::Error>> {
// 1. Baca konfigurasi tanpa panic implisit
let url = env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL environment variable is missing")?;
// 2. Set static state secara aman
DB_URL.set(url).map_err(|_| "Failed to set DB_URL: already initialized")?;
Ok(())
}
fn main() {
// 1. Daftarkan panic hook eksplisit agar selalu tercetak ke stderr
std::panic::set_hook(Box::new(|info| {
eprintln!("FATAL UNCAUGHT PANIC: {}", info);
}));
// 2. Inisialisasi logging/tracing runtime
eprintln!("Bootstrapping application runtime...");
// 3. Eksekusi startup sequence berurut dengan penanganan error terstruktur
if let Err(err) = init_runtime() {
eprintln!("Application startup failed: {err}");
std::process::exit(1);
}
println!("Service ready. DB URL configured: {}", get_db_url());
}Prinsip Mencegah Crash Pre-main
- Hindari crate konstruktor global: Jangan gunakan
#[ctor]atau atribut.init_arraykecuali saat membangun dynamic shared library (C ABI) yang benar-benar mewajibkannya. - Tolak I/O di static lifetime: Konstruksi static global hanya boleh berupa operasi konstanta murni (
const fn). Operasi file, network, atau pemanggilanstd::env::varharus terjadi di dalam aliran eksekusi fungsimain(). - Pasang panic hook di baris pertama: Selalu posisikan
std::panic::set_hookdan logging framework di urutan teratasmain()agar setiap kegagalan berikutnya dapat tercatat pada monitoring pod.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!