L
PHP • Literally Web ID

Arsitektur Real-Time WebSockets Menggunakan Go dan Redis Pub/Sub

⏱️ 18 menit baca • 📅 14 Oct 2026 • 👁️ 1 views
Arsitektur Real-Time WebSockets Menggunakan Go dan Redis Pub/Sub

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan aplikasi web modern, kebutuhan akan komunikasi real-time dua arah telah bergeser dari sekadar fitur pelengkap menjadi fondasi mutlak bagi platform enterprise. Layanan seperti trading platform, aplikasi chatting skala masif, sistem pemantauan infrastruktur (infrastructure monitoring), hingga papan skor olahraga langsung menuntut latensi milidetik dan efisiensi sumber daya jaringan yang maksimal.

Secara historis, arsitektur web berbasis HTTP tradisional menghadapi hambatan struktural yang signifikan ketika dipaksa menangani pembaruan data secara instan. HTTP didesain dengan pola request-response yang stateless. Sebelum protokol WebSocket distandarisasi oleh IETF melalui RFC 6455 pada tahun 2011, para insinyur menggunakan teknik seperti HTTP Short Polling dan HTTP Long Polling untuk mensimulasikan pembaruan data real-time.

Analisis Botleneck Performa

  • HTTP Short Polling: Klien secara periodik (misalnya setiap 2 detik) mengirimkan permintaan HTTP baru ke server. Pendekatan ini menghasilkan overhead jaringan yang masif akibat pengiriman header HTTP berukuran besar (seringkali 500 hingga 1000 byte) pada setiap permintaan, meskipun tidak ada data baru yang tersedia. Hal ini membanjiri CPU server, memenuhi antrean TCP stack, dan menghabiskan sumber daya bandwidth.
  • HTTP Long Polling: Klien mengirimkan permintaan, dan server menahan koneksi tetap terbuka (pending) hingga data tersedia atau waktu habis (timeout). Meskipun lebih efisien daripada short polling, metode ini tetap memerlukan proses pembentukan koneksi HTTP (TCP handshake dan TLS handshake jika menggunakan HTTPS) berulang kali setiap kali siklus poll selesai dan terhubung kembali.

Kondisi Sebelum vs Sesudah Penerapan Arsitektur WebSocket

  • Sebelum (HTTP Polling / Tanpa Skala):
    • Setiap instance aplikasi web mandiri dan mempertahankan state di memori lokal (in-memory).
    • Ketika arsitektur diskalakan secara horizontal (menjadi 3 node server di belakang Load Balancer), klien A yang terhubung ke Node 1 tidak dapat menerima pesan dari klien B yang terhubung ke Node 2 karena tidak ada mekanisme sinkronisasi memori antar-node.
    • Konsumsi CPU dan memori melonjak tinggi akibat tingginya frekuensi pembukaan dan penutupan soket TCP.
  • Sesudah (Go + WebSocket + Redis Pub/Sub):
    • Koneksi TCP tunggal dipertahankan secara persisten antara klien dan server, memungkinkan komunikasi dupleks penuh (full-duplex) dengan overhead header minimal (hanya 2-10 byte per frame setelah handshake awal).
    • Penskalaan horizontal menjadi sangat mulus (seamless) berkat lapisan messaging broker eksternal menggunakan Redis Pub/Sub, yang mendistribusikan pesan antar-node secara instan.

2. Konsep Inti & Deep Dive Arsitektur

Arsitektur real-time tangguh memerlukan pemahaman mendalam tentang siklus hidup koneksi WebSocket dan bagaimana sistem mendistribusikan beban di antara banyak instance server backend.

Alur Kerja di Balik Layar (Under the Hood)

  1. Handshake Pembuka: Klien mengirimkan permintaan HTTP GET ke server dengan header khusus Upgrade: websocket dan Connection: Upgrade. Server memvalidasi header ini dan merespons dengan kode status 101 Switching Protocols.
  2. Koneksi Persisten: Setelah handshake berhasil, protokol dasar berubah dari HTTP murni menjadi protokol WebSocket melalui koneksi TCP yang sama.
  3. Distribusi Pesan Lintas Node (Redis Pub/Sub): Ketika Server A ingin menyiarkan pesan ke semua klien, Server A mempublikasikan pesan tersebut ke sebuah channel di Redis. Seluruh server backend (termasuk Server B, C, dan D) yang berlangganan (subscribe) ke channel tersebut akan menerima pesan secara instan dan meneruskannya ke klien lokal masing-masing yang terhubung.
+----------+       WebSocket       +----------+      Redis Publish      +-------------+
| Client A | <-------------------> | Server A | ---------------------> |             |
+----------+                       +----------+                        |             |
                                                                       | Redis Broker|
+----------+       WebSocket       +----------+      Redis Subscribe   | (Pub/Sub)   |
| Client B | <-------------------> | Server B | <--------------------- |             |
+----------+                       +----------+                        +-------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Berikut adalah analisis mendalam mengenai pilihan teknologi untuk membangun sistem real-time:

Parameter Komparasi HTTP Long Polling Go (Gorilla/Fiber) + WebSocket Node.js + Socket.io Python (FastAPI) + WebSockets
Konsumsi Memori (RAM) Tinggi (Banyak proses/thread aktif) Sangat Rendah (~10-20 KB per koneksi) Sedang (~40-50 KB per koneksi) Sedang (~30-40 KB per koneksi)
Penggunaan CPU Boros (Siklus request-response masif) Sangat Efisien (Goroutines ringan) Efisien (Event loop tunggal) Efisien (Asynchronous I/O)
Latensi Jaringan Tinggi (> 100ms hingga detik) Sangat Rendah (< 10ms) Rendah (< 15ms) Rendah (< 15ms)
Kompleksitas Skala Horizontal Sulit (Memerlukan sticky session ketat) Sangat Mudah (Dipadu Redis Pub/Sub) Mudah (Redis adapter) Mudah (Redis broadcast)
Dukungan Ekosistem Universal Sangat Kuat untuk Backend Enterprise Sangat Luas di Frontend/Fullstack Bagus untuk API, kurang untuk skala masif

[!TIP] Gunakan bahasa Go untuk menangani koneksi WebSocket dalam jumlah besar (ratusan ribu hingga jutaan koneksi bersamaan) karena pemanfaatan memori per goroutine yang sangat kecil (hanya 2KB pada alokasi awal) dibandingkan model thread tradisional.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum memulai implementasi teknis, pastikan sistem operasi dan perangkat lunak pendukung telah terinstal dengan versi yang memadai.

  • Sistem Operasi: Linux Ubuntu 22.04 LTS / Debian 12 / AlmaLinux 9 (Direkomendasikan untuk production environment).
  • Go Runtime: Versi 1.21 atau lebih baru.
  • Redis Server: Versi 7.0 atau lebih baru.
  • Nginx: Versi 1.18+ (Berfungsi sebagai Reverse Proxy dan SSL Termination).

Penyesuaian Kernel Linux untuk High Concurrency (/etc/security/limits.conf)|

* soft nofile 65535
* hard nofile 65535
root soft nofile 65535
root hard nofile 65535

Penyesuaian Parameter Jaringan (/etc/sysctl.conf)

fs.file-max = 2097152
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.core.somaxconn = 65535

Terapkan perubahan dengan perintah:

sysctl -p

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Inisialisasi Proyek Go

Buat direktori proyek baru dan inisialisasi modul Go:

mkdir -p go-websocket-cluster
cd go-websocket-cluster
go mod init github.com/username/go-websocket-cluster

Instal dependensi yang diperlukan (Gorilla WebSocket dan Redis Go Client):

go get github.com/gorilla/websocket
go get github.com/redis/go-redis/v9

Langkah 2: Membuat Struktur Kode Backend

Buat file main.go dengan kode lengkap di bawah ini:

package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"os"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

// Client merepresentasikan koneksi klien WebSocket yang aktif.
type Client struct {
	conn *websocket.Conn
	send chan []byte
}

// Hub mengelola seluruh klien yang terhubung ke instance server ini.
type Hub struct {
	clients    map[*Client]bool
	broadcast  chan []byte
	register   chan *Client
	unregister chan *Client
	mutex      sync.Mutex
}

// Message mendefinisikan struktur payload JSON
type Message struct {
	Sender  string `json:"sender"`
	Content string `json:"content"`
}

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true // Sesuaikan untuk production
	},
}

func NewHub() *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
	}
}

func (h *Hub) Run(redisClient *redis.Client) {
	// Goroutine untuk mendengarkan pesan dari Redis Pub/Sub
	go func() {
		pubsub := redisClient.Subscribe(ctx, "chat_room")
		defer pubsub.Close()

		ch := pubsub.Channel()
		for msg := range ch {
			h.broadcast <- []byte(msg.Payload)
		}
	}()

	for {
		select {
		case client := <-h.register:
			h.mutex.Lock()
			h.clients[client] = true
			h.mutex.Unlock()

		case client := <-h.unregister:
			h.mutex.Lock()
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
			h.mutex.Unlock()

		case message := <-h.broadcast:
			h.mutex.Lock()
			for client := range h.clients {
				select {
				case client.send <- message:
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
			h.mutex.Unlock()
		}
	}
}

func (c *Client) writePump() {
	ticker := time.NewTicker(30 * time.Second)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()

	for {
		select {
		case message, ok := <-c.send:
			c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if !ok {
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})+
					return
			}

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)

			// Kirim pesan antrean tambahan dalam batch yang sama
			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write([]byte{'
'})
				w.Write(<-c.send)
			}

			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

func (c *Client) readPump(hub *Hub, rClient *redis.Client) {
	defer func() {
		hub.unregister <- c
		c.conn.Close()
	}()

	c.conn.SetReadLimit(512 * 1024) // Batas 512KB per pesan
	c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
	c.conn.SetPongHandler(func(string) error {
		c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
		return nil
	})

	for {
		_, messageBytes, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("error: %v", err)
			}
			break
		}

		var msg Message
		if err := json.Unmarshal(messageBytes, &msg); err != nil {
			log.Printf("invalid json: %v", err)
			continue
		}

		// Publikasikan pesan ke Redis Pub/Sub alih-alih broadcast lokal langsung
		payload, _ := json.Marshal(msg)
		err = rClient.Publish(ctx, "chat_room", payload).Err()
		if err != nil {
			log.Printf("failed to publish to redis: %v", err)
		}
	}
}

func serveWs(hub *Hub, rClient *redis.Client, w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	client := &Client{conn: conn, send: make(chan []byte, 256)}
	hub.register <- client

	go client.writePump()
	go client.readPump(hub, rClient)
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	redisAddr := os.Getenv("REDIS_ADDR")
	if redisAddr == "" {
		redisAddr = "localhost:6379"
	}

	rClient := redis.NewClient(&redis.Options{
		Addr:     redisAddr,
		Password: "",
		DB:       0,
	})

	hub := NewHub()
	go hub.Run(rClient)

	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		serveWs(hub, rClient, w, r)
	})

	log.Printf("Server started on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal("ListenAndServe: ", err)
		}
}

Langkah 3: Konfigurasi Nginx sebagai Reverse Proxy

Agar aplikasi Go dapat diakses melalui port standar 80/443 dengan dukungan WebSocket upgrade yang benar, gunakan konfigurasi Nginx berikut pada /etc/nginx/sites-available/websocket-app:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream websocket_backend {
    server 127.0.0.1:8080;
    # Tambahkan server backend lain untuk load balancing horizontal
    # server 127.0.0.1:8081;
}

server {
    listen 80;
    server_name chat.enterprise.internal;

    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeout penting untuk koneksi WebSocket jangka panjang
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

Aktifkan konfigurasi dengan membuat symlink:

ln -s /etc/nginx/sites-available/websocket-app /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. upgrader.CheckOrigin: Pada baris konfigurasi upgrader, fungsi CheckOrigin mengembalikan true. Di lingkungan produksi, Anda wajib memvalidasi header Origin untuk mencegah serangan Cross-Site WebSocket Hijacking (CSWSH).
  2. readPump dan writePump: Setiap koneksi klien dilayani oleh sepasang goroutine independen. readPump membaca aliran data masuk dari soket, sedangkan writePump mengirimkan data keluar serta paket detak jantung (heartbeat). Pemisahan ini mencegah kebuntuan (deadlock) pada operasi I/O.
  3. Mekanisme Ping/Pong (Heartbeat): writePump mengirimkan websocket.PingMessage setiap 30 detik. Jika klien tidak membalas dengan Pong dalam batas waktu pembacaan (60 detik pada SetReadDeadline), koneksi otomatis diputus untuk membersihkan sumber daya dari klien yang terputus secara tidak normal (dead connections).
  4. Redis Pub/Sub Integration: Alih-alih melakukan broadcast langsung ke memori lokal server, metode readPump mempublikasikan pesan ke Redis (rClient.Publish). Goroutine Hub mendengarkan saluran Redis tersebut dan mendistribusikannya ke seluruh klien yang terhubung ke instance server lokal. Dengan cara ini, ribuan instance backend dapat disebar di balik Load Balancer tanpa kehilangan sinkronisasi pesan.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Menghadapi Lonjakan Trafik (Flash Sale / Live Event)

Sebuah platform perdagangan kripto mengalami lonjakan trafik hingga 500.000 koneksi bersamaan saat pengumuman pasar penting.

  • Solusi Infrastruktur: Insinyur menerapkan pola auto-scaling pada instance backend Go di Kubernetes dengan pembagian 10 pod mandiri. Lapisan Redis menggunakan klaster Redis terkelola (Redis Cluster) dengan replikasi master-replica untuk menangani operasi penulisan dan pembacaan Pub/Sub yang masif.
  • Optimasi File Descriptor: Setiap koneksi WebSocket mengonsumsi satu file descriptor Linux. Batas bawaan OS (1024) ditingkatkan menjadi 1.000.000 melalui direktori systemd service file /etc/systemd/system/go-app.service:
[Service]
LimitNOFILE=1000000
ExecStart=/usr/local/bin/go-websocket-cluster
Restart=always

[Install]
WantedBy=multi-user.target

8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern 1: Mengabaikan Timeout Koneksi (Idle Connection Leak)
    • Masalah: Klien menutup aplikasi seluler atau laptop tanpa mengirimkan frame penutupan WebSocket (CloseFrame). Server tetap menganggap koneksi aktif, mengakibatkan kebocoran memori dan kehabisan file descriptor.
    • Solusi: Selalu implementasikan mekanisme Ping/Pong berkala disertai read deadline dan write deadline yang ketat.
  • Anti-Pattern 2: Sinkronisasi Tanpa Mutex pada Map Klien
    • Masalah: Mengakses peta klien (h.clients) dari banyak goroutine secara bersamaan tanpa proteksi sync.Mutex akan memicu fatal error panic: concurrent map write di Go.
    • Solusi: Gunakan sync.Mutex atau sync.RWMutex setiap kali melakukan operasi tambah, hapus, atau iterasi pada peta klien.
  • Anti-Pattern 3: Tidak Menangani Reconnection di Sisi Klien (Frontend)
    • Masalah: Koneksi terputus sesaat akibat gangguan jaringan seluler, namun antarmuka pengguna membeku dan tidak mencoba menyambung kembali.
    • Solusi: Implementasikan algoritma Exponential Backoff pada klien JavaScript untuk mencoba ulang koneksi secara otomatis.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji ketahanan aplikasi WebSocket yang dibangun, gunakan alat pengujian beban khusus WebSocket seperti tsung, artillery, atau utilitas berbasis Go seperti vegeta / skrip kustom.

Contoh Pengujian Beban Menggunakan Artillery

Buat file konfigurasi load-test.yml:

config:
  target: "ws://127.0.0.1:8080/ws"
  phases:
    - duration: 60
      arrivalRate: 50
      rampTo: 500
      name: "Ramp up load"
scenarios:
  - engine: "ws"
    flow:
      - send: '{"sender":"LoadTester","content":"Hello WebSocket Cluster"}'
      - think: 5

Jalankan pengujian:

artillery run load-test.yml

Metrik Kinerja Utama yang Harus Dipantau:

  1. Active Connections: Jumlah koneksi TCP/WebSocket terbuka secara bersamaan.
  2. Goroutine Count (runtime.NumGoroutine()): Memastikan tidak terjadi lonjakan goroutine yang tidak terkendali (goroutine leak).
  3. Memory Heap Allocation (runtime.MemStats): Memantau efisiensi pengumpulan sampah (Garbage Collection) di Go.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum merilis sistem arsitektur WebSocket berbasis Go dan Redis Pub/Sub ini ke lingkungan produksi (Production), pastikan Anda telah melewati daftar pemeriksaan berikut:

  • Validasi header Origin pada fungsi pembentukan WebSocket (CheckOrigin) untuk mencegah serangan CSWSH.
  • Konfigurasi parameter sistem operasi (nofile, somaxconn) untuk mendukung ribuan koneksi konkuren.
  • Implementasi mekanisme Heartbeat (Ping/Pong) dengan timeout yang teruji.
  • Penggunaan Redis Pub/Sub terenkripsi dengan otentikasi kata sandi / ACL yang aman.
  • Penyiapan Nginx reverse proxy dengan dukungan timeout panjang (proxy_read_timeout & proxy_send_timeout).
  • Pemantauan metrik (monitoring) menggunakan Prometheus dan Grafana untuk melacak penggunaan memori dan CPU.

Dengan memadukan efisiensi bahasa Go, fleksibilitas protokol WebSocket, dan kecepatan distribusi pesan dari Redis Pub/Sub, Anda kini memiliki fondasi arsitektur infrastruktur real-time tingkat enterprise yang sangat cepat, handal, dan siap diskalakan tanpa batas.

🏷️ Tags: #Redis #Golang #Websocket #System Architecture #Backend Development