Bayangin ada satu endpoint yang grafik latency-nya nggak rata. Kebanyakan waktu dia adem, tapi tiba-tiba naik lancip, turun lagi, lalu naik lagi. Kamu mandangin grafik itu sambil ngopi, dan yang muncul di kepala bukan profiler. Yang muncul adalah kalimat, "kayaknya pindah bahasa aja deh." 😅
Dorongan itu wajar banget, kok. Ada perusahaan besar yang beneran pindah bahasa dan hasilnya bagus. Tapi ada yang jarang kita perhatiin: dua cerita paling terkenal itu nggak dimulai dari bahasa baru. Mereka mulai dari angka, dari ngukur sampai kelihatan persis di mana yang mentok. Joel Spolsky pernah bilang bahwa menulis ulang dari nol adalah "the single worst strategic mistake that any software company can make", dan kita bakal lihat kenapa kalimat itu masih relevan.
Yuk kita bedah tiga kasus nyata, plus satu kontra-contoh. Tujuannya bukan bilang pindah bahasa itu salah. Tujuannya biar kamu tahu kapan solusi murah udah habis, dan kapan baru boleh mikir rewrite.
Poin Penting
Pindah bahasa baru punya dasar kalau bottleneck-nya terbukti berasal dari runtime atau dari arsitektur yang nggak bisa berbagi resource, dan komponennya cukup terisolasi untuk digarap sendirian.
- Yang sebenarnya kamu beli dari pindah bahasa bukan "kode yang lebih cepat", tapi hilangnya GC di kasus Discord dan kemampuan berbagi resource lintas thread di kasus Cloudflare.
- Discord baru pindah ke Rust setelah dua usaha tuning-nya gagal, dan Cloudflare mengevaluasi pilihannya tiap kuartal selama beberapa tahun sebelum akhirnya membangun Pingora.
- Instagram justru cukup mematikan GC di Python-nya dan langsung jadi 10% lebih efisien, jadi bahasa yang dianggap lambat pun bukan masalah selama dipakai dengan benar.
- Selama masih ada query, indeks, atau konfigurasi yang belum dibereskan, masalah yang sama cuma pindah ke bahasa baru berikut biaya belajarnya.
Kenapa Discord akhirnya pindah dari Go ke Rust?
Yang mereka pindahkan namanya Read States. Tugasnya cuma nyatet channel dan pesan mana yang udah kamu baca. Sederhana, tapi layanan ini dipanggil tiap kali kita connect, tiap kali kirim pesan, dan tiap kali buka pesan. Jadi dia nempel di hampir semua hal yang kita lakuin di sana, lho.
Skalanya gede, nih. Ada miliaran Read State, satu per user per channel. Tiap instance cache memegang jutaan user dan puluhan juta Read State. Per detiknya ada ratusan ribu update cache dan puluhan ribu write database.
Gejalanya emang aneh tapi konsisten: spike latency dan CPU tiap sekitar 2 menit. Penyebabnya, Go memaksa GC jalan minimal tiap 2 menit, terlepas dari seberapa besar heap-nya tumbuh. Dari grafik di blog mereka, CPU naik dari sekitar 20% ke 30–40%. Average response time naik dari sekitar 0–2 ms ke sekitar 25 ms. Yang p95 melonjak dari sekitar 0–10 ms ke sekitar 300 ms. Catat ya, itu p95, bukan p99, dan semua angka ini hasil baca grafik, jadi sifatnya perkiraan.
Nah, bagian pentingnya: mereka nggak langsung pindah. Mereka nyoba tuning dulu, dan dua-duanya gagal. Pertama, mengubah GC percent dari endpoint. Hasilnya nggak ngaruh, soalnya alokasi mereka nggak cukup cepat untuk memicu GC lebih sering. Kedua, memperkecil LRU cache. Spike-nya memang mengecil, tapi p99-nya naik karena cache jadi lebih sering miss dan bebannya pindah ke database.
Jadi mereka udah ketemu tembok, tuh. Bukan tembok malas, tapi tembok yang diukur dan dicoba dobrak dua kali. Baru setelah itu mereka porting ke Rust, dan kata mereka latency-nya sama bagusnya dengan versi Go, dan nggak ada spike sama sekali. Dengan optimasi dasar aja versi Rust sudah mengalahkan versi Go yang di-tuning mati-matian. Setelah diprofil lagi, Rust menang di latency, CPU, dan memori.
Beberapa hari kemudian kapasitas cache dinaikkan ke 8 juta Read State, hal yang nggak mungkin di versi Go karena GC-nya makin lama makin berat. Hasilnya, average response time jadi satuan mikrodetik, dan Max @mention jadi satuan milidetik. Tapi lihat catatan kaki mereka sendiri: mereka nggak berpikir kamu harus menulis ulang semuanya di Rust cuma karena bisa.
Cloudflare: kodenya lebih cepat, atau arsitekturnya?
Cerita kedua datang dari Cloudflare, yang mengganti NGINX dengan Pingora. Menurut tulisan mereka soal Pingora, sistem barunya melayani lebih dari 1 triliun request per hari. Dia cuma butuh sepertiga CPU dan memori dibanding infrastruktur proxy sebelumnya. Di produksi, hematnya 70% CPU dan 67% memori di beban yang sama. TTFB juga turun 5 ms di median dan 80 ms di p95.
Sekarang bagian yang paling sering kelewat. Hemat itu bukan karena kodenya lebih cepat, lho. Layanan lama mereka juga udah sub-milidetik. Yang lebih cepat itu cara dia berbagi koneksi. 🤯
Masalahnya ada di arsitektur NGINX, sih. Satu request cuma dilayani satu worker, yaitu satu proses OS. Load-nya nggak seimbang antar core, dan connection pool-nya per worker. Akibatnya makin banyak worker ditambah, makin jelek rasio reuse koneksinya. Ini kayak kalau tiap orang punya galon air sendiri di mejanya. Kamu butuh minum, galon sebelah masih penuh, tapi nggak bisa diambil, jadi kamu beli galon baru lagi. Tambah orang bukan bikin galon yang ada lebih kepakai, malah nambah galon baru terus.
Pingora dibangun dengan multithreading, runtime async Tokio, dan work stealing. Mereka juga bikin HTTP library sendiri karena traffic di internet banyak yang nggak patuh RFC. Hasilnya, koneksi baru yang dibuat per detik tinggal sepertiga. Untuk satu pelanggan besar, rasio reuse koneksi naik dari 87,1% ke 99,92%, dan koneksi baru ke origin turun 160x. Terjemahan blog mereka: "kami menghemat 434 tahun waktu handshake setiap hari."
Ada alasan lain mereka pindah. C nggak memory safe, Lua kurang performan dan nggak ada static typing, dan komunitas NGINX bergerak lambat. Setelah melayani beberapa ratus triliun request, Pingora belum pernah crash karena kode service-nya. Sistem ini lalu di-open source-kan pada 28 Februari 2024.
Jadi sebenarnya apa yang kamu beli dengan pindah bahasa?
Dua kemenangan tadi datang dari dua mekanisme yang beda. Di Discord, yang hilang adalah GC. Di Cloudflare, yang didapat adalah kemampuan berbagi resource lintas thread. Itu aja, sih. Bukan "Rust lebih cepat" secara magis, tapi ada satu batasan spesifik yang akhirnya lenyap.
Kalau masalahmu bukan salah satu dari dua itu, ganti bahasa nggak akan mengubah apa pun. Query lambat tetap lambat di Rust. N+1 tetap N+1 di Go. Kamu cuma bakal punya masalah yang sama dengan sintaks yang berbeda, plus biaya belajar yang nggak kecil, lho.
Kutipan Knuth sering dipotong jadi slogan. Versi lengkapnya begini: "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." Slogan itu sering dipakai buat menghindari ngukur, padahal Knuth justru minta kita tahu kode mana yang masuk 3% itu. Dan satu-satunya cara tahu adalah dengan profiler.
Gimana kalau masalahmu memang GC, tapi bahasanya Python?
Di sini ada kabar yang menenangkan. Kalau masalahmu memang GC, kamu nggak selalu butuh bahasa baru. Pada Januari 2017, Instagram mematikan GC di Python-nya dan langsung jadi 10% lebih efisien. Nggak pindah bahasa, nggak ganti framework, cuma mematikan satu fitur. 🙌
Konteksnya gini. Instagram jalan dengan Django multi-proses, pakai uWSGI model pre-fork. Satu master mem-fork dirinya jadi puluhan worker yang melayani request. Idealnya, memori yang dibagi antar worker itu tetap dibagi.
Tapi temuannya lain. Shared memory tiap worker turun dari sekitar 250 MB ke sekitar 140 MB, kira-kira sepertiga, cuma dalam beberapa detik setelah worker lahir. Sebabnya, Python pakai reference counting. Setiap kali sebuah objek dibaca, counter-nya ditulis. Tulisan itu memicu copy-on-write di kernel, jadi halaman memori yang tadinya dibagi berubah jadi milik masing-masing proses.
Coba pikirin sebentar, deh. Gejala "GC bikin latency jelek" muncul di tiga tempat berbeda: di Go (Discord), di JVM Cassandra (Discord lagi), dan di Python (Instagram). Jawabannya beda-beda. Discord pindah bahasa, Instagram cuma ganti konfigurasi. Yang salah bukan bahasanya, tapi keputusan yang diambil di sekitar bahasanya.
Kadang bottleneck-nya bahkan bukan bahasa
Ini kontra-contohnya, dan lagi-lagi dari Discord. Pada 2023, mereka pindah dari Cassandra ke ScyllaDB, dan 177 node jadi 72 node. P99 untuk mengambil pesan historis turun dari 40–125 ms jadi 15 ms. Insert pesan turun dari 5–70 ms jadi 5 ms.
Akar masalah aslinya adalah hot partition, konkurensi yang nggak terkendali, dan GC di JVM Cassandra. Yang menyelesaikannya adalah request coalescing plus consistent hash routing (ditulis di Rust), serta database yang bebas GC. Jadi bahkan di cerita "pindah ke Rust", sebagian besar kemenangannya datang dari desain dan pilihan komponen yang tepat.
Bonusnya, migrator-nya ditulis di Rust dan estimasi migrasinya berubah dari 3 bulan jadi 9 hari. Puncaknya jalan di 3,2 juta pesan per detik. Jadi Rust tetap punya tempat yang jelas, tapi tempatnya sempit dan spesifik.
Kapan rewrite boleh dipertimbangkan?
Alasan Spolsky masuk akal: kode lama itu sudah dipakai, sudah diuji, dan ratusan bug sudah ditemukan dan diperbaiki. Menulis ulang berarti membuang semua pengetahuan itu. Dia juga bilang soal performa, "1% of the work gets you 99% of the bang." Kalau 1% itu belum kamu cari, jangan loncat ke 100%.
Jadi rewrite itu keputusan terakhir, bukan langkah pertama. Ada tiga pertanyaan yang harus kamu punya datanya.
- Udah habis belum usaha yang lebih murah, seperti query, indeks, caching, N+1, dan connection pool? Ingat, Cloudflare mengevaluasi pilihannya tiap kuartal selama beberapa tahun.
- Bottleneck-nya kebukti berasal dari runtime atau arsitektur, bukan dari kode kita sendiri?
- Komponennya cukup terisolasi? Discord cuma memindahkan satu service, dan Cloudflare cuma mengganti layer proxy.
Kalau belum bisa jawab ketiganya dengan data, jawabannya sekarang adalah belum. Dan itu jawaban yang sehat, kok, bukan tanda kamu pengecut.
Kalau tetap harus pindah, pilih yang mana?
Ini cuma pendapat, ya. Pilih Go kalau bebanmu didominasi I/O dan kamu butuh cepat produktif. Pilih Rust kalau bebanmu CPU-bound, kamu memegang cache besar di RAM, kamu bangun infrastruktur jaringan atau storage, atau kamu butuh p99 yang datar tanpa toleransi jeda GC.
Dan kalau Python atau Node-mu masih jalan baik setelah semua usaha murah dicoba, nggak ada yang memaksa kamu pindah. Bahasa yang dianggap lambat pun bukan masalah kalau dipakai dengan benar.
Minggu ini, buka profiler dulu
Jadi balik ke endpoint yang spike tadi. Sebelum nulis baris Rust pertama, buka profiler dulu minggu ini. Cari tahu apakah spike-nya datang dari GC, dari query, atau dari pool koneksi yang nggak dibagi. Kalau jawabannya satu dari dua yang pertama dan cuma bisa dibereskan dengan pindah runtime, kamu punya data buat meyakinkan tim. Kalau bukan, selamat, kamu baru aja hemat berbulan-bulan kerja. 😄