Rust di Produksi: Mengapa Kami Menulis Ulang Signal Processor Kami
Kami menulis ulang mesin pemrosesan sinyal kami dari Go ke Rust. Inilah alasannya, apa yang kami pelajari, dan peningkatan throughput 4x yang kami capai.
Enam bulan lalu, kami mengambil keputusan untuk menulis ulang mesin pemrosesan sinyal kami — komponen yang mengubah sinyal browser mentah menjadi vektor fitur yang ternormalisasi dan dapat di-hash — dari Go ke Rust. Ini bukan keputusan yang kami ambil dengan enteng. Implementasi Go kami berfungsi. Sudah teruji. Sudah ter-deploy. Namun ia telah menjadi bottleneck dalam pipeline kami, dan kami membutuhkan peningkatan throughput yang bersifat step-function. Inilah yang terjadi.
Mengapa Kami Melampaui Kapasitas Go
Signal processor kami melakukan pekerjaan yang intensif secara komputasi: mem-parsing payload JSON, menerapkan fungsi normalisasi pada 130+ sinyal, menghitung hash proprietari, dan membangun vektor hash identifikasi. Di Go, pekerjaan ini bersifat CPU-bound, dan garbage collector Go menjadi masalah pada skala besar. Setiap siklus pemrosesan sinyal mengalokasikan objek antara — node JSON yang di-parsing, nilai string yang dinormalisasi, buffer hash — yang menciptakan tekanan GC.
Pada 30K event/detik, signal processor Go kami menunjukkan jeda GC sebesar 2-5ms setiap beberapa detik. Jeda ini masih dapat diterima. Pada 50K event/detik, jeda GC tumbuh menjadi 8-15ms dan terjadi lebih sering. Pada 80K event/detik — beban proyeksi kami untuk Q3 — jeda GC akan menyebabkan latensi p99 melampaui SLA kami. Kami membutuhkan entah lebih banyak server (mahal) atau implementasi yang lebih efisien.
Mengapa Rust
Kami mengevaluasi tiga opsi: mengoptimasi implementasi Go (sync.Pool, arena allocation, tuning GOGC), menulis ulang dalam C++, dan menulis ulang dalam Rust. Optimasi Go menghasilkan peningkatan 30% tetapi tidak secara fundamental menyelesaikan masalah GC. C++ ditolak karena kekhawatiran memory safety dalam sistem yang kritis terhadap keamanan. Rust menawarkan zero-cost abstraction, tanpa garbage collector, dan jaminan memory safety yang ditegakkan di waktu kompilasi.
Ekosistem Rust juga memiliki library yang matang untuk semua yang kami butuhkan: serde untuk parsing JSON, crate hashing berkinerja tinggi, dan tokio untuk I/O asinkron. Kurva pembelajarannya nyata — tim kami memiliki pengalaman Go yang mendalam tetapi pengalaman Rust yang terbatas — tetapi karakteristik kinerjanya persis seperti yang kami butuhkan.
Proses Penulisan Ulang
Kami menulis ulang signal processor sebagai layanan mandiri yang berkomunikasi dengan bagian lain pipeline kami melalui gRPC. Ini memungkinkan kami men-deploy-nya berdampingan dengan implementasi Go dan secara bertahap mengalihkan trafik. Penulisan ulang membutuhkan tiga engineer selama empat minggu — dua minggu untuk implementasi inti dan dua minggu untuk pengujian, benchmarking, dan penanganan edge case.
Aspek yang paling menantang bukanlah bahasanya itu sendiri, melainkan memastikan paritas perilaku dengan implementasi Go. Kami membangun sebuah comparison harness yang menjalankan kedua implementasi terhadap input yang sama dan memverifikasi bahwa keduanya menghasilkan output yang identik. Kami menemukan 14 perbedaan halus selama proses ini — sebagian besar terkait penanganan floating-point, normalisasi Unicode, dan edge case parsing JSON.
Hasil Kinerja
Implementasi Rust memproses sinyal dalam rata-rata 0,8ms dibandingkan 3,2ms untuk Go — peningkatan 4x. Penggunaan memori turun dari 2,1GB menjadi 340MB untuk beban kerja yang sama. Tidak ada jeda GC karena tidak ada garbage collector. Utilisasi CPU menurun sebesar 60% pada throughput yang sama, yang berarti setiap server menangani trafik 4x lebih banyak.
Pada 80K event/detik, implementasi Rust mempertahankan waktu pemrosesan p99 sebesar 1,4ms dengan nol jeda. Headroom ini berarti kami tidak perlu meninjau kembali kinerja pemrosesan sinyal untuk waktu yang lama ke depan. Berkurangnya penggunaan CPU dan memori juga langsung berujung pada biaya infrastruktur yang lebih rendah — kami memensiunkan 8 dari 12 server pemrosesan sinyal.
Pelajaran yang Dipetik
Menulis ulang dalam Rust sepadan untuk kasus spesifik kami — beban kerja yang CPU-bound, banyak alokasi, dan sensitif terhadap latensi. Kami tidak akan menulis ulang lapisan ingesti HTTP atau layanan query ClickHouse kami dalam Rust, karena komponen-komponen tersebut bersifat I/O-bound dan Go menanganinya secara efisien. Pelajarannya bukan "tulis ulang semuanya dalam Rust" melainkan "gunakan Rust di tempat zero-cost abstraction dan kinerja deterministiknya paling penting."
Kejutan terbesar adalah betapa banyaknya yang ditangkap compiler Rust selama penulisan ulang. Beberapa bug laten dalam implementasi Go kami — race condition pada buffer bersama, integer overflow dalam komputasi hash, dan akses out-of-bounds pada input yang cacat — tertangkap sebagai error di waktu kompilasi dalam Rust. Compiler-nya menuntut, tetapi ia membayar dirinya sendiri dalam hal kebenaran.
Sejujurnya, minggu pertama terasa menyakitkan. Sarah menyimpan hitungan "pertarungan dengan borrow checker" di papan tulis — kami mencapai 47 sebelum tim berhenti menghitung. Namun pada minggu ketiga, kode yang berhasil dikompilasi langsung berjalan mulus. Tidak ada panic misterius di produksi, tidak ada data race di bawah beban. Trade-off itu sepadan untuk apa pun yang berada di hot path.