L
Event-Driven Architecture 📚 Definitive Comprehensive Guide • Literally Web ID

Real-Time Event-Driven Architecture dengan NATS dan WebSockets

⏱️ 12 menit baca • 📅 20 Oct 2026 • 👁️ 5 views
Real-Time Event-Driven Architecture dengan NATS dan WebSockets
🔊 Dengarkan Artikel Ini

Real-Time Event-Driven Architecture dengan NATS dan WebSockets

1. Titik Awal Masalah: Mengapa Arsitektur Lama Sering Ambruk di Produksi

Ingatkah Anda malam ketika traffic melonjak tiga kali lipat karena flash sale mendadak, dan dasbor pemantauan tiba-tiba berubah menjadi lautan warna merah menyala? Di ruang server, CPU pada instance backend Go kita melompat ke 95%, bukan karena payload datanya besar, melainkan karena ratusan ribu koneksi WebSocket yang terbuka secara bersamaan saling berebut memori dan membuat file descriptor meluber. Tim frontend mengeluh data harga tidak sinkron, sementara tim infrastruktur sibuk mematikan server karena kehabisan RAM.

Masalah klasik ini sering kali berakar dari kesalahan mendesain komunikasi real-time. Mengandalkan long polling HTTP atau membiarkan setiap instance server web mengelola koneksi WebSocket secara terisolasi tanpa message broker yang handal adalah resep bencana. Ketika aplikasi diskalakan secara horizontal menjadi lima node di belakang load balancer Nginx, Node A tidak tahu apa yang sedang didengar oleh Node B. Pesan yang dikirimkan oleh pengguna di Node A tidak pernah sampai ke klien yang terhubung melalui Node B. Di sinilah kita membutuhkan arsitektur terdistribusi berbasis event yang mampu menyatukan jutaan kabel virtual tanpa membuat peladen kolaps.

Transisi dari arsitektur monolitik yang reaktif menuju sistem event-driven yang tangguh menuntut kita untuk tidak hanya memikirkan kode yang berjalan di mesin lokal, tetapi juga bagaimana data mengalir melintasi batas-batas klaster server yang terdistribusi secara geografis. Mari kita bedah bagaimana fondasi ini dibangun dari nol.

2. Di Balik Layar: Bagaimana Sistem Ini Bekerja Tanpa Sihir

Untuk memahami cara kerja sistem real-time berlatensi rendah, bayangkan NATS sebagai sistem pos kilat super cepat di dalam sebuah pabrik raksasa, sementara WebSocket adalah saluran pipa transparan yang langsung menghubungkan tangan kurir ke meja pelanggan. Klien web membuka koneksi TCP abadi melalui WebSocket ke sebuah Gateway—dalam hal ini, aplikasi Go ringan yang bertindak sebagai jembatan.

[!TIP] Jangan pernah membiarkan klien WebSocket berbicara langsung ke database atau memproses logika berat. Gunakan Gateway yang hanya bertugas melakukan enkapsulasi bingkai (framing) dan meneruskannya ke message broker.

Ketika sebuah event terjadi di sistem—misalnya tombol "beli" ditekan—backend mengirimkan pesan JSON ke subjek NATS tertentu, katakanlah orders.checkout.success. NATS Server, yang ditulis dalam bahasa Go dan terkenal karena efisiensinya yang mendekati logam (bare metal), secara instan merutekan pesan tersebut ke seluruh Gateway yang berlangganan pada subjek tersebut. Gateway kemudian menerjemahkan pesan NATS tadi menjadi bingkai teks WebSocket dan mendorongnya ke ribuan browser dalam hitungan milidetik. Tanpa polling, tanpa beban I/O disk, murni operasi memori asinkron.

Namun, memilih komponen yang tepat di atas kertas sering kali berbeda jauh dengan realita pengujian beban di lingkungan staging. Mari kita lihat kompromi apa saja yang harus kita ambil.

3. Pilihan Pendekatan & Kompromi yang Harus Diambil

Sebelum mengetikkan baris kode pertama, kita dihadapkan pada persimpangan jalan arsitektur. Apakah kita harus menggunakan Redis Pub/Sub, Apache Kafka, atau NATS Server? Mari kita buang teori pemasaran dan lihat faktanya di lapangan.

Redis Pub/Sub sangat cepat, tetapi ia memiliki kelemahan fatal: fire-and-forget murni tanpa jaminan pengiriman (at-most-once). Jika klien terputus sedetik saja, pesan hilang selamanya. Di sisi lain, Apache Kafka adalah raksasa penyimpanan persisten yang luar biasa untuk event sourcing skala enterprise, namun kompleksitas operasionalnya—membutuhkan ZooKeeper (atau KRaft baru-baru ini) dan pengelolaan partisi disk yang ketat—terlalu berlebihan jika tujuan kita hanya mengalirkan notifikasi UI instan.

NATS hadir sebagai jalan tengah yang sempurna. Ia menawarkan kecepatan memori Redis dengan keandalan perutean yang dirancang khusus untuk komunikasi mikroservis dan edge computing. NATS JetStream, fitur persistensi bawaannya, bahkan memberikan kita kemampuan jaminan pengiriman pesan (at-least-once) tanpa overhead manajemen klaster yang menyiksa.

[!IMPORTANT] Pilih NATS Core untuk komunikasi ephemeral (data transien seperti posisi kursor atau indikator mengetik) dan aktifkan JetStream hanya untuk data kritikal finansial atau status pesanan yang tidak boleh hilang.

Dengan keputusan arsitektur yang sudah bulat, saatnya kita menyiapkan tangan untuk meracik lingkungan pengembangan lokal.

4. Meracik Lingkungan: Persiapan & Prasyarat Lapangan

Kita tidak akan menggunakan Docker Compose yang membengkak dengan puluhan layanan tak dikenal. Kita butuh setup yang bersih, deterministik, dan mendekati produksi. Kita akan menjalankan NATS Server mandiri dan aplikasi Gateway berbasis Go.

Pastikan Anda memiliki Go (versi 1.21 atau lebih baru) dan NATS CLI terinstal di mesin Anda. Mari kita mulai dengan menginisialisasi direktori proyek:

mkdir -p nats-ws-gateway/cmd/gateway
mkdir -p nats-ws-gateway/internal
cd nats-ws-gateway
go mod init github.com/username/nats-ws-gateway

Selanjutnya, unduh dependensi esensial kita: pustaka klien NATS resmi dan gorilla/websocket untuk menangani protokol WebSocket di sisi Go.

go get github.com/nats-io/nats.go
go get github.com/gorilla/websocket

Untuk menjalankan NATS Server secara lokal untuk keperluan pengembangan, kita bisa memanfaatkan Docker dengan konfigurasi port standar:

version: '3.8'
services:
  nats:
    image: nats:2.10-alpine
    container_name: nats-server
    ports:
      - "4222:4222"
      - "8222:8222"
    command: ["--jetstream", "-m", "8222"]

Jalankan kontainer tersebut dengan docker compose up -d. Sekarang server NATS dengan fitur JetStream aktif di port 4222, dan dasbor pemantauan HTTP siap di port 8222. Mari kita tulis kode jembatannya.

5. Langkah Implementasi: Membangun Solusi yang Tangguh

Inti dari sistem ini adalah Gateway yang bertindak ganda: sebagai klien NATS yang mendengarkan topik internal, dan sebagai server WebSocket yang melayani koneksi dari browser pengguna.

Mari buat berkas cmd/gateway/main.go dengan struktur yang bersih dan penanganan konkurensi yang aman:

package main

import (
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	"github.com/gorilla/websocket"
	"github.com/nats-io/nats.go"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		// Di produksi, validasi origin dengan ketat!
		return true
	},
}

func main മുഖ്യമന്ത്രി() {}

func main() {
	// Koneksi ke klaster NATS
	natsURL := os.Getenv("NATS_URL")
	if natsURL == "" {
		natsURL = nats.DefaultURL
	}

	nc, err := nats.Connect(natsURL)
	if err != nil {
		log.Fatalf("Gagal terhubung ke NATS: %v", err)
	}
	defer nc.Close()
	log.Println("Berhasil terhubung ke NATS broker")

	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			log.Printf("Gagal melakukan upgrade WebSocket: %v", err)
			return
		}
		defer conn.Close()

		// Setiap klien berlangganan ke subjek universal atau spesifik pengguna
		sub, err := nc.Subscribe("events.broadcast", func(msg *nats.Msg) {
			err := conn.WriteMessage(websocket.TextMessage, msg.Data)
			if err != nil {
				log.Printf("Gagal mengirim pesan ke klien WebSocket: %v", err)
			}
		})
		if err != nil {
			log.Printf("Gagal membuat langganan NATS untuk klien: %v", err)
			return
		}
		defer sub.Unsubscribe()

		// Blok eksekusi agar koneksi tetap hidup sampai klien putus
		for {
			_, _, err := conn.ReadMessage()
			if err != nil {
				break
			}
		}
	})

	server := &http.Server{Addr: ":8080"}
	go func() {
		log.Println("Gateway WebSocket berjalan pada port :8080")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("HTTP server error: %v", err)
		}
	}()

	// Graceful shutdown
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
	<-stop
	log.Println("Mematikan gateway secara elegan...")
}

Kode di atas terlihat sederhana, namun ada beberapa detail implementasi mendalam yang kerap menjebak para insinyur di lapangan. Mari kita bedah bagian tersebut.

6. Membedah Baris Kode Krusial yang Kerap Menjebak

Perhatikan bagaimana langganan NATS (nc.Subscribe) dibuat di dalam penanganan HTTP handler WebSocket. Setiap kali klien membuka satu koneksi baru, satu langganan NATS baru dibuat di dalam broker. Apakah ini buruk?

Untuk skala di bawah 10.000 klien per node, NATS menangani langganan subjek liar (wildcard subscriptions) dengan sangat efisien. Namun, jika Anda melayani 100.000 klien per instans Gateway, membuat langganan NATS per koneksi akan membebani tabel perutean memori NATS.

[!WARNING] Jangan membuat dedicated NATS subscription per koneksi WebSocket jika Anda mengantisipasi ratusan ribu klien konkuren pada satu instance Gateway. Gunakan Hub Pattern terpusat di dalam memori aplikasi Go, di mana Gateway hanya memiliki satu langganan NATS global, lalu menyebarkan pesan masuk ke seluruh klien WebSocket yang aktif melalui peta (map) aman-benang (thread-safe).

Selain manajemen memori, tantangan terbesar berikutnya muncul ketika jaringan internet pengguna berkedip—situasi yang sangat lumrah di dunia nyata.

7. Bertahan di Bawah Tekanan: Skenario Traffic Nyata

Di dunia nyata, kabel jaringan tidak pernah sempurna. Pengguna seluler melewati terowongan kereta bawah tanah, beralih dari Wi-Fi kantor ke data seluler 5G, atau mengalami gangguan transmisi sesaat. Jika Gateway tidak menangani siklus hidup koneksi putus-sambung (reconnection) dengan benar, memori server akan mengalami kebocoran (memory leak) secara perlahan karena goroutine yang menggantung menunggu tulis jaringan yang macet.

Untuk mengatasinya, implementasikan mekanisme Ping/Pong heartbeat secara rutin pada level WebSocket:

conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
	conn.SetReadDeadline(time.Now().Add(60 * time.Second))
	return nil
})

Jika klien tidak merespons ping dalam waktu 60 detik, koneksi diputus secara paksa oleh server, sumber daya memori dibersihkan, dan langganan NATS yang terkait langsung dibatalkan (unsubscribed). Pendekatan defensif ini memastikan server Anda tetap bernapas meskipun diguyur badai koneksi yatim piatu.

Namun, bahkan dengan kode yang paling kokoh sekalipun, ada beberapa jebakan klasik yang sering kali lolos dari pengujian lokal.

8. Jebakan Klasik & Cara Menghindarinya Sebelum Meledak

Kesalahan paling sering yang dilakukan pengembang pemula pada arsitektur berbasis event adalah mengabaikan urutan pesan (message ordering) dan penanganan duplikasi data.

Ketika jaringan terputus dan klien melakukan reconnect, klien tersebut mungkin melewatkan sepuluh event penting yang terjadi saat mereka offline. Jika Anda menggunakan NATS Core biasa, pesan-pesan tersebut hilang selamanya bagi klien tersebut. Solusinya adalah memadukan NATS JetStream dengan mekanisme Consumer Sequence ID di sisi klien. Ketika klien terhubung kembali, mereka mengirimkan last_seen_id terakhir mereka ke Gateway, dan Gateway meminta replay pesan dari JetStream KV store atau JetStream Consumer sebelum melanjutkan aliran real-time biasa.

Jebakan kedua adalah kurangnya pembatasan laju (rate limiting). Seorang pengguna usil dapat memicu ratusan event dalam sedetik dari sisi klien, membanjiri NATS, dan melumpuhkan seluruh klaster. Selalu pasang token bucket rate limiter pada titik masuk Gateway sebelum pesan dipublikasikan ke NATS.

Sebelum meluncurkan sistem ini ke produksi, mari kita uji seberapa jauh batas kemampuan arsitektur ini.

9. Uji Nyali: Mengukur Performa di Titik Batas

Jangan pernah percaya bahwa sistem Anda siap sebelum diuji di bawah tekanan ekstrem. Kita dapat menggunakan alat pengujian beban modern seperti k6 atau utilitas khusus WebSocket untuk mensimulasikan ribuan klien virtual.

Buat skrip pengujian k6 sederhana (load-test.js):

import ws from 'k6/ws';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 1000 }, // Naikkan ke 1000 klien dalam 30 detik
    { duration: '1m', target: 1000 },  // Tahan beban selama 1 menit
    { duration: '10s', target: 0 },    // Turunkan perlahan
  ],
};

export default function () {
  const url = 'ws://localhost:8080/ws';
  const res = ws.connect(url, {}, function (socket) {
    socket.on('open', function () {
      console.log('Klien terhubung');
    });
    socket.on('message', function (message) {
      check(message, { 'pesan diterima': (msg) => msg.length > 0 });
    });
    socket.setTimeout(function () {
      socket.close();
    }, 45000);
  });

  check(res, { 'status koneksi 101': (r) => r && r.status === 101 });
}

Jalankan pengujian menggunakan perintah k6 run load-test.js sambil memantau penggunaan CPU dan memori melalui dasbor NATS di port 8222. Anda akan takjub melihat bagaimana satu instans Go kecil mampu menangani ribuan koneksi dengan konsumsi memori yang tidak lebih dari beberapa puluh megabita.

10. Catatan Lapangan & Checklist Sebelum Rilis

Sebelum Anda menekan tombol merge terakhir dan mendorong kode ini ke lingkungan produksi, pastikan checklist operasional berikut sudah terpenuhi:

  • Konfigurasi NATS Cluster: Jalankan minimal 3 node NATS untuk mencapai kuorum tinggi (fault tolerance).
  • File Descriptor Limits: Tingkatkan batas sistem operasi (ulimit -n) pada server Gateway Anda agar tidak mengalami galat too many open files saat trafik melonjak.
  • Load Balancer Configuration: Pastikan Nginx atau AWS ALB di depan Gateway Anda dikonfigurasi dengan timeout WebSocket yang memadai (misalnya proxy_read_timeout 3600s) dan mengaktifkan dukungan connection upgrade.
  • TLS Enkripsi: Gunakan WSS (wss://) alih-alih WS biasa di produksi menggunakan sertifikat Let's Encrypt yang dikelola secara otomatis oleh Caddy atau Certbot.
  • Observabilitas: Hubungkan metrik NATS Prometheus ke Grafana untuk memantau inbound/outbound message rate secara real-time.

Arsitektur real-time bukan tentang seberapa pintar kode yang Anda tulis, melainkan seberapa tangguh sistem tersebut berdiri ketika kekacauan terjadi di dunia nyata. Selamat membangun sistem yang tidak hanya cepat, tetapi juga tahan banting!

🏷️ Tags: #Websocket #Go #Architecture #Nats #Real-Time

Apakah panduan tutorial ini membantu Anda?

Masukan Anda sangat berharga untuk terus memperbarui dan menyempurnakan kualitas materi teknis kami.

🎉

Terima kasih banyak atas apresiasi Anda!

Tim kami terus menyempurnakan tutorial agar selalu akurat dengan versi teknologi terbaru.

Bantu Kami Menyempurnakan Materi Ini

⚡ Literally Tech Digest

Update Tutorial & Developer Tools Terbaru Langsung ke Email Anda

Dapatkan kurasi artikel arsitektur software, tips DevOps, panduan database, serta rilis fitur developer utility gratis setiap minggu. Bebas spam, bisa berhenti kapan saja.

⏳ Mencari artikel...
Tutorial & Artikel Terkait
Developer Tools
Tidak ditemukan tutorial atau tool dengan kata kunci "".
Ketik minimal 2 karakter untuk mencari artikel atau nama tools.
Tekan ESC untuk menutup Cari di Semua Artikel →