Di episode sebelumnya saya menyinggung soal satu endpoint yang bisa makan 15 detik sekali panggil. Episode ini cerita lengkapnya, karena perjalanan mencari penyebabnya adalah salah satu debugging paling mengedukasi yang pernah saya alami. Bukan karena rumit, tapi karena saya salah berulang kali dengan penuh percaya diri hehe~
Sebuah Klik, ±17 Detik #
Korbannya: endpoint estimasi bahan bakar per grup kendaraan. Satu klik di dashboard, lalu menunggu. ±17 detik lebih sedikit, setiap kali. Untuk ukuran dashboard yang diakses tiap hari, itu terasa seperti loading jaman dial-up hahaha.
Dan selama berminggu-minggu penyelidikan, ada soundtrack-nya: “Faiq, kok error?” “Faiq, kok lemot?” Tenang, Mas, lagi saya telusuri hehe.
Sebelum lanjut: endpoint ini menjalankan satu query ber-CTE1 ke tabel ringkasan perangkat yang isinya puluhan juta baris. Dari sini, teori pertama langsung muncul dengan wajah paling percaya diri.
Tersangka Pertama: Query-nya Tentu Saja #
Kalau lambat, ya query-nya dong. Naluri semua developer, termasuk saya.
Jadi saya buka EXPLAIN. Dan EXPLAIN-nya menjawab: query-mu sehat-sehat saja, Mas. Rencana eksekusinya memakai index dengan benar, langkah termahalnya pun masih di ordo puluhan milidetik. Untuk memastikan, query yang sama saya jalankan langsung ke database lewat CLI: 78 milidetik, termasuk round-trip jarak ribuan kilometer.
Tujuh puluh delapan milidetik. Endpoint-nya: 17.000 milidetik. Tersangka pertama gugur. Ups hehe.
Tersangka Kedua: Geografi #
Nah, ini bagian yang memalukan sekaligus penting: arsitektur yang saya terima waktu itu menempatkan aplikasi di region cloud Eropa, sementara database utamanya di Indonesia. Jaraknya ribuan kilometer, dan setiap query MySQL membayar tol sekitar 190 milidetik satu arah. User di Indonesia, database di Indonesia, tapi datanya muter dulu ke Eropa. Best practice mana yang bilang begitu? hahaha.
Dengan kondisi seperti itu, teori kedua terdengar sangat masuk akal: pool koneksi yang kecil bikin request ngantri tiket round-trip mahal. Saya besarkan pool-nya, tambah timeout, rapikan idle connection. Deploy dengan harapan.
Hasilnya: 16,57 detik menjadi 15,64 detik. Itu pun saya curigai cuma noise. Tersangka kedua… tidak tertangkap. Hadeh.
Tersangka Ketiga: Umur Koneksi #
Masih di jalur yang sama, muncul teori ketiga: server database membunuh koneksi idle di menit kesepuluh, sementara aplikasi mengira koneksinya berumur 30 menit. Ketimpangan umur = koneksi zombie = gagal di tengah jalan.
Teori ini tidak sepenuhnya salah, ketimpangannya nyata, dan perbaikannya memang patut dilakukan. Tapi juga tidak menjawab misteri utama: angkanya tetap di atas 15 detik. Tiga kali menuduh, tiga kali meleset. Sudah waktunya berhenti ber teori dan mulai mendengarkan.
Log Bicara, Semua Teori Bubar #
Malam itu saya buka log aplikasi. Dan di sana, jawabannya sudah lama duduk manis:
GetGasEstimationByGroups query failed: invalid connection
Bukan 200 yang lambat. 500 yang gagal. Endpoint-nya tidak pelit waktu, dia error, lalu database driver mencoba ulang otomatis, tiap percobaan membayar biaya sambungan ulang di jalur transbenua, gagal lagi, dan begitu terus sampai menyerah. Frontend ikut mencoba ulang. User melihat semuanya sebagai… “lambat”.
Begitu sudut pandangnya berubah, petunjuk yang tadinya menganga jadi terbaca:
- Query ber-CTE selalu gagal, query sederhana di pool yang sama selalu sukses.
- Query gas yang sama, lewat CLI, sukses mulus 0,7 detik.
Apa bedanya aplikasi dengan CLI? Protokol. Aplikasi saya waktu itu pakai prepared statement (protokol biner MySQL, tiga round-trip per query), sedangkan CLI pakai protokol teks (satu round-trip). Dan pada jalur biner itulah server me-reset koneksi di tengah eksekusi. Jalurnya pun bukan internet telanjang: koneksi ke server database di Indonesia melewati tunnel VPN, dan di lorong itulah query CTE ber-protokol biner berulang kali terbunuh di tengah jalan, sementara query teks yang ramping lolos tanpa lecet.
Satu Baris yang Mengakhiri Semua #
Perbaikannya akhirnya cuma satu baris konfigurasi di DSN:
InterpolateParams: true,
Artinya: kirim query lewat protokol teks, parameter di-interpolate di sisi driver (dengan escaping yang benar, jadi aman terhadap SQL injection asalkan tetap pakai placeholder, bukan concat manual), persis seperti yang dilakukan CLI yang selalu sukses itu.
Deploy. Klik dashboard. Dan invalid connection tidak pernah muncul lagi. Alhamdulillah. Yang namanya perjuangan berminggu-minggu, titiknya bisa semencit di satu baris hehe.
Yang Terbukti, yang Masih Dugaan #
Sekarang bagian yang jarang ditulis orang: memisahkan mana yang terbukti dan mana yang masih dugaan.
Terbukti: pada jalur prepared statement lewat tunnel VPN ke database, query CTE berulang kali terbunuh di tengah jalan; jalur protokol teks menghindarinya; perbaikan satu baris itu menghentikan error sepenuhnya di produksi.
Masih dugaan sampai sekarang: kenapa persisnya tunnel itu menghukum jalur biner pada query CTE yang kompleks (dan hanya mereka). Ada beberapa kandidat penjelasan, tapi saya belum punya bukti definitifnya, dan menurut saya itu boleh-boleh saja diakui di tulisan publik hahaha. Debugging yang jujur tidak harus berakhir di kepastian total; cukup berakhir di masalah yang hilang.
Oh iya, dua “perbaikan salah” di tengah jalan (ukuran pool dan umur koneksi) tetap saya pertahankan, keduanya kebetulan best practice yang memang harusnya ada. Jadi bukan kerugian total lah ya, kan? hehe~
Pelajaran #
- Ukur dulu, baru menuduh. Satu EXPLAIN menggugurkan teori yang hampir membuat saya rewrite query yang ternyata tidak bersalah.
- Baca log sebelum bikin teori. Jawabannya sudah duduk manis di log berminggu-minggu, sementara saya sibuk menghakimi query, geografi, dan umur koneksi.
- Asimetri adalah petunjuk emas. Mana yang selalu gagal vs mana yang selalu sukses, begitu digarisbawahi, penyebabnya ketahuan sendiri.
- “Lambat” kadang sesungguhnya “gagal berkali-kali”. Latency yang aneh selalu layak dicurigai sebagai retry yang menumpuk.
Satu catatan penutup: sampai episode ini ditulis, endpoint-endpoint ringan di sistem itu masih punya floor sekitar 2-3 detik karena arsitektur transbenua yang sama. Itu cerita arsitektur yang saya simpan untuk episode penutup. Stay tuned lagi deh hehe.
Episode berikutnya kita pindah panggung: dari database ke paket-paket biner yang dikirim tracker GPS, dan bagaimana saya mem-port puluhan parser protokol tanpa merusak data armada yang sedang jalan. Sampai ketemu di sana.
Sekian. Salam.
-
CTE (Common Table Expression), cara menulis query bertingkat dengan sub-hasil yang diberi nama, diawali kata
WITH. Enak dibaca, kadang bikin database mikir keras. ↩︎