Masalah utamanya bukan di antena, RTL-SDR, atau decoder ADS-B. Pada kasus ini, data mentah masuk terus dengan laju normal, tetapi sebagian update posisi pesawat hilang atau tiba terlambat saat melewati pipeline backend. Setelah ditelusuri, akar masalahnya adalah backpressure yang tidak ditangani dan buffer event yang tumbuh tanpa batas.
Artikel ini membahas studi kasus debugging stream ADS-B pada backend TypeScript, terinspirasi dari arsitektur proyek pemantauan lalu lintas udara seperti Skylight. Fokusnya bukan pada decoding sinyal radio, melainkan pada jalur data setelah pesan ADS-B diterima: ingest, parsing, enrichment ringan, dan broadcast ke klien. Kita akan lihat gejala yang muncul, indikator di log dan metrik, hipotesis awal yang salah, akar masalah sesungguhnya, lalu langkah perbaikan yang realistis untuk sistem event stream real-time.
Alur data dan konteks masalah
Secara sederhana, pipeline backend-nya terlihat seperti ini:
RTL-SDR -> decoder ADS-B -> proses ingest Node/TypeScript -> queue internal
-> normalisasi/update state pesawat -> broadcast WebSocket/APIPada arsitektur seperti ini, sumber data biasanya push-based: decoder terus mengirim pesan baru, dan backend harus mengikutinya. Ketika jumlah pesawat meningkat atau ada lonjakan pesan posisi, jalur hilir seperti serialisasi JSON, update state, dan pengiriman ke WebSocket bisa menjadi lebih lambat daripada laju event masuk.
Jika sistem tidak punya mekanisme backpressure yang jelas, event akan menumpuk di memori. Akibatnya:
- update posisi lama masih menunggu di antrean ketika data yang lebih baru sudah datang,
- latensi meningkat secara bertahap,
- memori bertambah,
- garbage collection makin sering,
- klien menerima posisi pesawat yang terlihat “meloncat”, “hilang”, atau telat beberapa detik.
Gejala di produksi: data masuk normal, tetapi posisi tampak hilang
Gejala awal terlihat membingungkan karena sisi ingest tampak sehat. Proses decoder tetap aktif, koneksi ke sumber data stabil, dan jumlah pesan masuk per detik tidak turun signifikan. Namun, pada UI atau consumer downstream, sebagian pesawat seperti berhenti bergerak sesaat, lalu lompat ke posisi baru.
Indikator yang biasanya terlihat
- Ingress rate stabil: pesan dari decoder tetap masuk dengan volume normal.
- Broadcast latency naik: selisih waktu antara event diterima dan event dikirim ke klien terus membesar.
- Queue depth meningkat: buffer internal atau array event bertambah tanpa turun kembali.
- Heap usage naik perlahan: memori tampak “bocor”, padahal sebenarnya antrean membesar.
- CPU tidak selalu 100%: ini sering menyesatkan, karena bottleneck bisa ada di scheduling, serialisasi, atau banyak operasi async kecil.
- Event loop lag sesekali naik: terutama saat flush besar atau GC.
Contoh log yang mengarah ke masalah
[ingest] messages_in=1250/s
[state] aircraft_updated=820/s
[broadcast] sent=410/s avg_latency_ms=1800 p95_latency_ms=5200
[queue] pending_events=18400
[mem] heap_used_mb=512
Log seperti ini menunjukkan ketidakseimbangan: laju masuk lebih tinggi daripada laju proses keluar. Jika dibiarkan, sistem tidak benar-benar kehilangan event di awal, tetapi kehilangan nilai event karena update lama dikirim saat sudah tidak relevan lagi.
Hipotesis awal yang sering salah
Dalam kasus stream ADS-B, beberapa dugaan awal terdengar masuk akal tetapi ternyata bukan akar masalah.
1. Decoder ADS-B menjatuhkan paket
Dugaan ini wajar, apalagi jika sumber datanya dari RTL-SDR. Namun jika hitungan pesan mentah stabil dan tidak ada indikasi reconnect atau error di jalur input, kemungkinan besar masalah ada setelah ingest.
2. WebSocket klien lambat, jadi posisi tampak hilang
Klien lambat memang bisa memperburuk situasi, tetapi jika seluruh sistem mengalami keterlambatan, masalah utamanya biasanya ada di jalur broadcast server-side atau antrian bersama sebelum fan-out ke klien.
3. Database atau storage menjadi bottleneck
Pada pipeline real-time, database sering dicurigai pertama. Padahal jika update posisi diproses di memori dan broadcast duluan, bottleneck yang lebih umum justru berasal dari buffer internal yang tak terbatas, serialisasi payload berulang, atau satu tahap async yang tidak mampu mengimbangi beban.
4. Node.js “single-threaded” jadi penyebab utama
Ini terlalu umum dan kurang membantu. Masalah sebenarnya bukan semata karena satu thread, melainkan karena tidak ada kontrol laju antara produsen event dan konsumen event. Bahasa atau runtime lain pun bisa mengalami hal yang sama jika antrian dibiarkan tumbuh bebas.
Akar masalah: backpressure dan buffer event yang tidak terkendali
Backpressure adalah kondisi saat bagian hilir pipeline memproses lebih lambat daripada bagian hulu yang menghasilkan data. Dalam sistem stream real-time, ini bukan kondisi aneh; yang berbahaya adalah ketika pipeline tidak punya kebijakan untuk menanganinya.
Pada studi kasus ini, implementasi awal menggunakan pola yang tampak sederhana: setiap event posisi yang datang langsung dimasukkan ke array/queue internal, lalu worker async akan memproses dan menyiarkannya. Masalahnya:
- queue tidak dibatasi ukurannya,
- setiap update posisi dianggap sama pentingnya,
- tidak ada strategi overwrite untuk data yang cepat basi,
- ingest dan broadcast berada pada jalur eksekusi yang terlalu saling bergantung.
Untuk data ADS-B, setiap update posisi tidak selalu harus dipertahankan satu per satu. Jika pesawat yang sama mengirim 10 update dalam waktu sangat singkat, tetapi sistem sedang tertinggal, menyimpan semua 10 update justru memperbesar latensi dan menurunkan kegunaan data. Dalam banyak kasus, yang dibutuhkan adalah state terbaru, bukan riwayat lengkap tiap langkah mikro.
Pola kode yang memicu masalah
type PositionEvent = {
icao: string;
lat: number;
lon: number;
alt?: number;
ts: number;
};
const queue: PositionEvent[] = [];
let processing = false;
export function onAdsbMessage(evt: PositionEvent) {
queue.push(evt);
void drainQueue();
}
async function drainQueue() {
if (processing) return;
processing = true;
try {
while (queue.length > 0) {
const evt = queue.shift()!;
await updateAircraftState(evt);
await broadcastToClients(evt);
}
} finally {
processing = false;
}
}
Masalah utama dari pola ini:
queuebisa tumbuh tanpa batas,shift()pada array besar tidak efisien,- setiap event diproses satu per satu secara serial,
broadcastToClientsikut memperlambat tahap ingest/state update.
Saat beban meningkat, event baru terus masuk. Queue makin panjang. Sistem tidak kehilangan data secara eksplisit, tetapi tertinggal jauh sehingga event yang sampai ke klien sudah usang.
Perbaikan: bounded queue, drop policy, batching, dan pemisahan jalur
Perbaikan yang efektif biasanya bukan satu perubahan tunggal, melainkan kombinasi beberapa kebijakan yang membuat sistem tetap berguna saat overload.
1. Gunakan bounded queue
Queue internal harus punya kapasitas maksimum. Tujuannya bukan sekadar menghemat memori, tetapi memaksa sistem memiliki perilaku yang terdefinisi saat beban melebihi kapasitas.
Jika queue penuh, Anda harus memilih salah satu:
- menolak event baru,
- membuang event lama,
- menggabungkan event serupa,
- menyimpan hanya event terbaru per pesawat.
Untuk update posisi ADS-B, kebijakan yang paling masuk akal sering kali adalah drop intermediate updates dan pertahankan state terbaru.
2. Terapkan drop policy yang sesuai sifat data
Tidak semua stream cocok diperlakukan sama. Pada sistem audit, drop event bisa berbahaya. Pada posisi pesawat real-time untuk visualisasi, event lama cepat kehilangan nilai. Karena itu, pendekatan seperti latest-wins biasanya lebih tepat daripada memaksa pemrosesan semua event.
Contoh kebijakan:
- jika pesawat yang sama sudah punya event tertunda, timpa event lama dengan yang terbaru,
- jika queue global penuh, buang event posisi paling tua yang belum diproses,
- jangan buang event penting lain seperti aircraft seen/unseen atau perubahan identitas jika memang dibutuhkan.
3. Batch processing untuk mengurangi overhead
Memproses event satu per satu sering mahal karena tiap event memicu overhead async, serialisasi, locking logis, atau fan-out. Mengambil event dalam batch kecil memberi peluang untuk:
- memperbarui banyak state sekaligus,
- menggabungkan update per pesawat,
- mengirim payload broadcast yang lebih efisien.
Batching bukan berarti menunggu terlalu lama. Pada sistem real-time, batch bisa dibentuk berdasarkan ukuran kecil atau interval sangat pendek, selama trade-off latensinya diterima.
4. Pisahkan ingest dari broadcast
Ini perubahan arsitektur yang sangat penting. Ingest harus sesederhana mungkin: baca event, validasi ringan, normalisasi, lalu simpan ke jalur internal yang cepat. Broadcast ke WebSocket atau consumer lain sebaiknya tidak menghambat jalur masuk utama.
Pemisahan ini bisa dilakukan dengan:
- queue terpisah untuk state update dan queue terpisah untuk outbound broadcast,
- snapshot state periodik untuk klien, bukan mengirim semua event mentah,
- worker independen untuk fan-out ke klien.
5. Tambahkan alerting berbasis gejala, bukan hanya error
Backpressure sering tidak menghasilkan exception yang jelas. Karena itu alerting harus memantau sinyal operasional seperti:
- queue depth,
- drop count,
- end-to-end latency,
- event loop lag,
- heap usage,
- jumlah klien broadcast aktif.
Contoh desain perbaikan di TypeScript
Berikut pseudocode yang lebih cocok untuk stream ADS-B posisi real-time. Tujuannya bukan menjadi implementasi final, tetapi menunjukkan prinsip penting: queue dibatasi, event untuk pesawat yang sama bisa di-overwrite, dan broadcast dipisah dari ingest.
type PositionEvent = {
icao: string;
lat: number;
lon: number;
alt?: number;
ts: number;
};
type AircraftState = PositionEvent;
const latestByAircraft = new Map<string, PositionEvent>();
const pendingAircraft = new Set<string>();
const processOrder: string[] = [];
const MAX_PENDING_AIRCRAFT = 5000;
const aircraftState = new Map<string, AircraftState>();
const outboundBatch: AircraftState[] = [];
let draining = false;
let droppedEvents = 0;
export function onAdsbMessage(evt: PositionEvent) {
// Jika aircraft baru dan kapasitas penuh, drop event paling tidak penting.
if (!pendingAircraft.has(evt.icao) && pendingAircraft.size >= MAX_PENDING_AIRCRAFT) {
droppedEvents++;
return;
}
const alreadyPending = pendingAircraft.has(evt.icao);
latestByAircraft.set(evt.icao, evt);
if (!alreadyPending) {
pendingAircraft.add(evt.icao);
processOrder.push(evt.icao);
}
void scheduleDrain();
}
async function scheduleDrain() {
if (draining) return;
draining = true;
try {
while (processOrder.length > 0) {
const batchKeys = processOrder.splice(0, 200);
const toBroadcast: AircraftState[] = [];
for (const icao of batchKeys) {
pendingAircraft.delete(icao);
const evt = latestByAircraft.get(icao);
if (!evt) continue;
latestByAircraft.delete(icao);
const nextState: AircraftState = evt;
aircraftState.set(icao, nextState);
toBroadcast.push(nextState);
}
enqueueBroadcastBatch(toBroadcast);
// Beri kesempatan event loop memproses I/O lain.
await new Promise(resolve => setImmediate(resolve));
}
} finally {
draining = false;
}
}
function enqueueBroadcastBatch(states: AircraftState[]) {
if (states.length === 0) return;
outboundBatch.push(...states);
}
Poin penting dari pendekatan ini:
- Yang diantrikan bukan semua event, melainkan identitas pesawat yang butuh diproses.
- Update terbaru menimpa update lama untuk pesawat yang sama.
- Ukuran pending dibatasi agar memori tidak tumbuh liar.
- Batch processing mengurangi overhead dibanding pemrosesan satu per satu.
Worker broadcast terpisah
let broadcastLoopRunning = false;
async function broadcastLoop() {
if (broadcastLoopRunning) return;
broadcastLoopRunning = true;
try {
while (true) {
if (outboundBatch.length === 0) {
await sleep(50);
continue;
}
const batch = outboundBatch.splice(0, 300);
const payload = JSON.stringify({ type: "aircraft_positions", data: batch });
await broadcastPayloadToAllClients(payload);
}
} finally {
broadcastLoopRunning = false;
}
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Dengan pemisahan ini, jalur ingest tidak perlu menunggu pengiriman ke klien selesai. Jika broadcast melambat, dampaknya tetap harus diawasi, tetapi tidak langsung menahan seluruh jalur masuk.
Kenapa pendekatan ini bekerja
Alasan teknisnya cukup jelas:
- Membatasi queue menghentikan pertumbuhan memori yang tidak terkendali.
- Meng-overwrite update lama menjaga relevansi data real-time. Untuk visualisasi posisi, state terbaru lebih penting daripada seluruh riwayat per detik.
- Batching menurunkan biaya per event karena serialisasi dan fan-out dilakukan dalam kelompok.
- Memisahkan ingest dan broadcast mencegah bottleneck hilir menyumbat sumber input.
- Alerting berbasis latency dan queue depth membantu mendeteksi overload sebelum pengguna melihat posisi pesawat “hilang”.
Trade-off-nya tentu ada. Anda memang akan kehilangan sebagian update intermediate saat overload. Namun dalam sistem real-time observability seperti peta pesawat, itu sering kali lebih baik daripada mengirim semua update dengan keterlambatan besar. Data yang telat beberapa detik bisa lebih menyesatkan daripada data yang diringkas menjadi state terbaru.
Checklist verifikasi setelah perbaikan
Setelah implementasi fix, jangan berhenti di “terasa lebih baik”. Verifikasi harus dilakukan dengan indikator yang bisa diamati.
Checklist teknis
- Queue depth stabil
Pastikan pending queue tidak terus naik saat beban normal, dan kembali turun setelah lonjakan. - Heap usage tidak bertumbuh tanpa batas
Naik-turun wajar boleh terjadi, tetapi tidak boleh ada tren pertumbuhan panjang akibat antrean. - End-to-end latency turun
Ukur selisih antara timestamp event masuk dan timestamp saat dibroadcast ke klien. - Drop count terukur dan dapat diterima
Saat overload, drop boleh terjadi, tetapi harus terpantau dan sesuai ekspektasi desain. - Posisi terbaru lebih cepat muncul di klien
Uji dengan membandingkan event terbaru dari sumber dengan posisi yang tampil di UI atau consumer. - Tidak ada starvation
Pastikan pesawat yang sangat aktif tidak membuat pesawat lain jarang terproses. - Broadcast worker tidak menjadi antrean tak terbatas baru
Pemisahan jalur bukan alasan membiarkan queue outbound tumbuh liar.
Contoh metrik yang layak dipasang
adsb_ingest_messages_totaladsb_pending_aircraftadsb_position_dropped_totaladsb_processing_latency_msadsb_broadcast_latency_msnode_event_loop_lag_msnode_heap_used_bytes
Jika Anda hanya memantau error rate, masalah seperti ini bisa lolos lama. Sistem tetap “berjalan”, tetapi kualitas datanya menurun drastis.
Kesalahan umum saat memperbaiki backpressure
- Menambah buffer tanpa batas lebih besar
Ini hanya menunda masalah dan menambah latensi. - Menganggap semua event harus diproses lengkap
Untuk data yang cepat basi, strategi latest-state sering lebih tepat. - Memindahkan bottleneck ke worker lain tanpa metrik
Pemisahan service/worker tidak cukup jika antrean baru tetap tidak dibatasi. - Tidak membedakan event kritis dan non-kritis
Drop policy harus sadar jenis data. - Batch terlalu besar
Batch memang efisien, tetapi jika terlalu besar dapat meningkatkan burst latency dan membuat event loop terasa tersendat.
Pelajaran umum untuk sistem event stream real-time
Kasus debug stream ADS-B ini memberi pelajaran yang lebih luas daripada domain aviasi.
- Setiap pipeline real-time butuh strategi overload. Jika tidak, sistem akan gagal secara diam-diam melalui latensi dan antrean.
- Queue adalah alat penghalus lonjakan, bukan tempat parkir permanen. Jika queue terus tumbuh, itu sinyal ketidakseimbangan laju.
- Data real-time harus dinilai berdasarkan kegunaannya saat diterima. Event yang lengkap tetapi telat belum tentu lebih baik.
- Observability harus mencakup freshness, bukan hanya throughput dan error.
- Pemisahan jalur ingest, state, dan broadcast membuat sistem lebih tahan terhadap beban dan lebih mudah di-debug.
Jika Anda menangani stream posisi, telemetri, sensor, atau event observability lain, pola yang sama sangat mungkin muncul. Begitu produsen lebih cepat daripada konsumen, keputusan desain tentang backpressure akan menentukan apakah sistem tetap relevan atau hanya sibuk memproses masa lalu.
Penutup
Pada studi kasus ini, posisi pesawat yang hilang ternyata bukan karena RTL-SDR atau decoder ADS-B gagal menangkap data. Sumber data tetap sehat; masalahnya ada di backend yang membiarkan event menumpuk tanpa kontrol. Dengan menerapkan bounded queue, drop policy yang sesuai, batching, pemisahan ingest dan broadcast, serta alerting berbasis latency dan queue depth, pipeline kembali mengutamakan freshness data dan tetap stabil saat terjadi lonjakan trafik.
Untuk sistem event stream real-time, pertanyaan pentingnya bukan hanya “berapa banyak event yang bisa diterima”, tetapi juga apa yang harus dilakukan ketika tidak mungkin memproses semuanya tepat waktu. Jawaban atas pertanyaan itu biasanya menentukan kualitas sistem di dunia nyata.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!