Di episode sebelumnya saya sempat cerita soal endpoint yang bisa makan 15 detik sekali panggil. Bagian yang paling saya ingat justru bukan teknisnya, tapi betapa yakinnya saya pada jawaban-jawaban yang ternyata salah. Ehem.
Satu Klik, Lalu Menunggu #
Satu klik pada endpoint estimasi bahan bakar per grup kendaraan, lalu menunggu sekitar 17 detik. Kalau dashboard-nya dibuka tiap hari, menunggu segitu lama rasanya seperti balik ke zaman dial-up.
Dan selama berminggu-minggu penyelidikan, ada soundtrack-nya: “Faiq, kok error?” “Faiq, kok lemot?” Tenang, Mas, lagi saya telusuri hehe.
Endpoint ini menjalankan satu query ber-CTE1 ke tabel ringkasan perangkat yang berisi puluhan juta baris. Saya langsung punya tersangka.
Tersangka Pertama, Query #
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, Jarak #
Bagian ini agak memalukan, tapi penting: aplikasi yang saya terima berjalan di region cloud Eropa, sementara database utamanya berada di Indonesia. Jaraknya ribuan kilometer; setiap query MySQL membayar tol sekitar 190 milidetik satu arah. User dan database sama-sama di Indonesia, tetapi datanya harus memutar lewat Eropa.
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. Saya curiga penurunan itu cuma noise. Perubahan pool belum menjelaskan masalahnya.
Tersangka Ketiga, Koneksi Tua #
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 berteori dan mulai mendengarkan.
Lalu Saya Baca Log-nya #
Malam itu saya buka log aplikasi:
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”.
Setelah itu, dua petunjuk yang sebelumnya saya lewatkan jadi jelas:
- 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 Konfigurasi #
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 #
Nah, saya mau pisahkan mana yang terbukti dan mana yang sampai sekarang 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~
Yang Saya Bawa Pulang #
- Ukur dulu sebelum menuduh. Satu EXPLAIN menyelamatkan query yang hampir saya rewrite, padahal dia tidak bersalah hehe.
- Baca log-nya. Jawabannya sudah ada di sana berminggu-minggu, sementara saya sibuk menyalahkan query, jarak, dan umur koneksi.
- Bandingkan yang gagal dan yang berhasil. Begitu saya lihat query CTE selalu gagal sementara yang sederhana lolos, petunjuknya mulai kelihatan.
- “Lambat” kadang artinya gagal berkali-kali. Kalau latency-nya aneh, saya sekarang lebih curiga ada retry yang menumpuk.
Sampai tulisan ini dibuat, endpoint-endpoint ringan di sistem itu masih punya floor sekitar 2-3 detik karena arsitektur transbenua yang sama. Penyebab latensi sisanya masih menjadi cerita tersendiri.
-
CTE (Common Table Expression), cara menulis query bertingkat dengan sub-hasil yang diberi nama, diawali kata
WITH. Enak dibaca, kadang bikin database mikir keras. ↩︎