Mengapa Network Latency Mematikan Throughput Database

Saat aplikasi dan database berada di node, subnet, atau data center terpisah, performa I/O database sering kali tidak dibatasi oleh disk IOPS atau CPU, melainkan oleh Network Round-Trip Time (RTT). Eksekusi 1.000 operasi INSERT individual pada koneksi dengan latensi 5 ms membutuhkan waktu minimal 5 detik hanya untuk transfer paket data bolak-balik, terlepas dari seberapa cepat engine database memproses query tersebut.

Masalah ini diperparah oleh biaya pembaruan index (B-Tree maintenance) synchronous pada setiap write. Untuk mencapai throughput tinggi pada arsitektur high-latency, strategi utama berpusat pada dua mekanisme: mengelompokkan payload via batching dan menunda pembaruan indeks via deferred indexing.

Strategi 1: Batch Write Mengurangi Network Round-Trip

Eksekusi query satu per satu (single-row operation) memaksa protokol jaringan melakukan TCP handshake/ack dan database query protocol lifecycle untuk tiap baris data. Pendekatan multi-row batching menggabungkan data ke dalam satu frame jaringan tunggal.

Contoh Query Multi-Row Insert dengan Resolusi Konflik

Daripada mengirim ratusan query terpisah, gunakan multi-row VALUES statement. Jika target data memiliki potensi duplikasi, manfaatkan ON CONFLICT untuk mencegah komputasi balik ke sisi aplikasi:

INSERT INTO sensor_logs (device_id, metric_value, recorded_at)
VALUES 
  ('dev-01', 42.5, '2025-05-10 10:00:00'),
  ('dev-02', 18.2, '2025-05-10 10:00:00'),
  ('dev-03', 99.1, '2025-05-10 10:00:01')
ON CONFLICT (device_id, recorded_at) 
DO UPDATE SET metric_value = EXCLUDED.metric_value;

Teknik ini menurunkan $N$ network round-trips menjadi $1$ round-trip. Batasan batching biasanya ditentukan oleh ukuran buffer paket jaringan database (seperti max_allowed_packet di MySQL) dan batasan alokasi parameter query engine.

Strategi 2: In-Memory Buffering Menggunakan Background Worker

Menumpuk batch langsung di request path API akan membebani memory handler HTTP. Solusi standarnya adalah menaruh antrean penampung sementara (in-memory buffer) yang dibaca oleh background worker secara periodik menggunakan strategi size-or-time flush.

// Contoh worker Go untuk batch flushing
type LogBuffer struct {
    buffer  []LogEntry
    maxSize int
    ticker  *time.Ticker
    lock    sync.Mutex
}

func (b *LogBuffer) Run(db *sql.DB) {
    for {
        select {
        case <-b.ticker.C:
            b.flush(db)
        }
    }
}

func (b *LogBuffer) Add(entry LogEntry, db *sql.DB) {
    b.lock.Lock()
    b.buffer = append(b.buffer, entry)
    ready := len(b.buffer) >= b.maxSize
    b.lock.Unlock()

    if ready {
        b.flush(db)
    }
}

func (b *LogBuffer) flush(db *sql.DB) {
    b.lock.Lock()
    if len(b.buffer) == 0 {
        b.lock.Unlock()
        return
    }
    toWrite := b.buffer
    b.buffer = make([]LogEntry, 0, b.maxSize)
    b.lock.Unlock()

    // Eksekusi multi-row INSERT ke DB via pool koneksi
    executeBulkInsert(db, toWrite)
}
Peringatan Risiko: Pola buffer berbasis memori rentan data loss jika proses crash sebelum flush selesai. Untuk critical data, gunakan write-ahead log sederhana atau persistent queue (seperti Redis stream atau Kafka) sebelum memproses batch ke database target.

Strategi 3: Deferred Indexing untuk High-Volume Ingestion

Setiap penambahan baris baru ke tabel memaksa database memperbarui node pada struktur B-Tree indeks terkait. Dalam skenario migrasi, bulk load batch reguler, atau data feed berkapasitas masif, beban komputasi write amplification ini memperlambat waktu eksekusi secara signifikan.

1. Pola Temp/Staging Table

Muat data mentah ke tabel staging unindexed, lalu lakukan merge atomic ke tabel produksi secara berkala:

-- 1. Buat staging table tanpa index non-essential
CREATE TEMP TABLE staging_events (
    event_id UUID,
    payload JSONB,
    created_at TIMESTAMP
);

-- 2. Bulk load data via fast channel (misal COPY protocol)
-- COPY staging_events FROM STDIN;

-- 3. Merge data ke target table yang memiliki index terstruktur
INSERT INTO events (event_id, payload, created_at)
SELECT event_id, payload, created_at 
FROM staging_events
ON CONFLICT (event_id) DO NOTHING;

2. Maintenance Indeks Manual pada Batch Skala Besar

Untuk proses batching masif terjadwal (misal ETL harian jutaan baris), dropping index sekunder sebelum ingest dan melakukan CREATE INDEX CONCURRENTLY setelah ingest selesai secara komputasi jauh lebih cepat daripada membiarkan database menyeimbangkan B-Tree per baris selama proses write.

Trade-off dan Limitasi Arsitektur

  • Read Your Own Writes: Penundaan tulis via buffer atau deferred indexing menyebabkan data tidak langsung terlihat oleh consumer (eventual consistency). Aplikasi pemanggil harus mentolerir data lag ini.
  • Ukuran Batch: Batch terlalu kecil tidak mengatasi bottleneck network RTT; batch terlalu besar dapat memicu lock table berkepanjangan dan memory spikes di server DB. Ukuran ideal umumnya berada pada rentang 500 hingga 2.000 baris per batch tergantung ukuran row payload.
  • Deadlock Hazard: Saat melakukan bulk insert secara paralel di beberapa worker, selalu urutkan data berdasarkan primary key sebelum query dikirim untuk menghindari deadlock antar worker.