Hijrah Backend (7): Dari Paket TCP ke Time-Series Database



Setiap detik, armada mengirim kabar. Puluhan ribu tracker GPS mengirim paket TCP berisi posisi, status mesin, dan kekuatan sinyal. Sistem harus menerima semuanya sepanjang hari, tanpa kampanye “ayo hemat kirim data” hahaha.

Di episode pertama saya sempat cerita bahwa sistem lama menerima paket dan menulisnya ke database dalam layanan yang sama. Sekarang tugasnya dipisah: listener menerima dan menerjemahkan paket, antrean menampungnya, lalu consumer menulis ke database. Di episode ini saya ceritakan alurnya, termasuk kesalahan yang baru kelihatan setelah jalan hehe~

Kenapa Ada Antrean di Tengah Jalan #

Mungkin kamu bertanya, bukankah menambah satu hop berarti menambah waktu?

Menerima paket dan menulis ke database itu kecepatannya beda. Tracker tetap mengirim meski database sedang sibuk. Kalau penerimaan dan penulisan disatukan, database yang lambat ikut memperlambat listener. Paket yang gagal masuk tinggal berharap device-nya mencoba lagi.

Antrean jadi tempat menitipkan paket sementara. Listener bisa lanjut menerima, sementara consumer mengambil data secepat database sanggup menulis. Buffer membantu saat ada lonjakan siang hari. Tapi komponen baru berarti ada cara baru untuk gagal juga. Saya baru sadar setelah ketemu masalahnya di produksi hahaha.

Saat Broker Mati, Pesan Ikut Hilang #

Sistem lama tidak punya urusan dengan antrean mati, karena ia langsung melakukan insert. Kalau database-nya ngambek, device bisa mencoba lagi nanti. Sistem baru punya masalah sendiri. Solusi pertama saya? Kalau koneksi ke broker putus, pesannya dibuang. Sampai sekarang saya masih heran kenapa dulu rasanya itu masuk akal.

Koneksi broker putus tanpa peringatan, lalu data posisi kendaraan pelanggan ikut hilang begitu saja. Infrastrukturnya bersin, datanya yang tumbang.

Saya memperbaikinya bertahap. Koneksi yang cuma tersendat diberi retry selama beberapa detik. Kalau broker tetap mati, masalahnya harus kelihatan di log error, bukan diam-diam menunggu ada yang menemukan peringatan berminggu-minggu kemudian. Lalu saya pasang backpressure. Antrean menampung sekitar 8 ribu paket;1 kalau penuh, Send() ikut menunggu. Listener melambat dan tracker mencoba lagi lewat TCP. Jadi, data tidak hilang, cuma telat datang. Membuat sumber menunggu ternyata lebih murah daripada kehilangan paket hehe.

Komentar di kode producer sekarang mencatat alurnya: antrean penuh → Send() blok → listener melambat → device mencoba lagi. Dulu terlihat seperti bug, ternyata bisa jadi pengaman.

Disiplin yang Salah Tempat #

Di tahap berikutnya, consumer menulis ribuan baris lokasi sekaligus (bulk insert). Saya pernah membuat kesalahan yang agak ironis: kalau satu baris gagal di dalam loop, fungsi langsung menjalankan return err. Baris-baris lokasi setelahnya ikut dibuang.

Sistem PHP lama malah memakai continue untuk kasus seperti ini. Versi Go saya yang katanya “lebih disiplin soal error” justru membuang lebih banyak data. Perbaikan pertamanya sederhana: ganti return dengan continue, lalu catat baris yang gagal di log. Saya lakukan hal yang sama di sender lain, menaikkan buffer konsumen sepuluh kali lipat, dan menambah worker. Angka-angkanya saya tulis di komentar config supaya tidak jadi angka misterius hahaha.2

Dari kejadian ini saya belajar bahwa pilihan continue atau return di loop bukan soal gaya saja. Satu error bisa menghentikan pemrosesan ribuan baris. Jadi saya berusaha memikirkan dampaknya dulu, bukan sekadar memilih yang kelihatan paling disiplin.

Database yang Mengerti Waktu #

Tahap terakhir menyimpan data lokasi, yang selalu terkait waktu dan tempat. Untuk itu saya memakai PostgreSQL dengan ekstensi time-series. Tabel lokasi menjadi hypertable yang dipartisi otomatis berdasarkan waktu.

Ada dua hal yang tidak saya dapatkan di sistem lama. Pertama, kompresi: chunk lama dikompresi otomatis. Kini lebih dari seratus chunk disimpan dengan cara itu, dan index tertentu bisa dibangun langsung di atas data terkompresi tanpa membongkarnya. Kedua, retensi otomatis: kebijakan mingguan membuang data yang melewati batas usia. Dulu, data lama dibersihkan secara manual, sambil berdoa. Sekarang database-nya yang mengurus jadwal hehe~

Sekarang kalau ada yang bertanya kapan data lama dibersihkan, saya tinggal menunjuk policy-nya hehe.

Catatan Saya #

Yang paling terasa dari perubahan ini bukan karena antrean atau database baru terdengar keren. Saya harus memutuskan dulu apa yang dilakukan sistem saat ada bagian yang gagal, dan memastikan kegagalannya kelihatan. continue dan return juga sama-sama punya tempatnya. Salah pilih, banyak baris bisa ikut hilang.

Di sisi penyimpanan, backpressure bikin sumber menunggu daripada membuang data, sementara kompresi dan retensi otomatis mengurangi pekerjaan beres-beres manual. Ternyata beberapa urusan yang dulu bikin saya berdoa sekarang tinggal jadi aturan database hehe.

Berikutnya episode terakhir seri ini. Saya akan menengok perjalanan hampir dua tahun, hal yang akan saya ubah kalau mengulang, dan bagian yang paling saya syukuri.

Versi tutorial dari dua babak episode ini, dari TCP listener sampai time-series database, ada di GPS Backend Part 2 dan Part 4.


  1. Kapasitas infrastruktur (ukuran antrean, jumlah worker, retensi) ditampilkan sebagai orde besaran; yang penting untuk cerita adalah arah dan alasannya. ↩︎

  2. Potongan kode dan angka konfigurasi disederhanakan, bentuk aslinya lebih panjang dan membawa detail internal. ↩︎