L
Vue.js • Literally Web ID

Scaling Real-time WebSocket Clusters dengan Redis Pub/Sub

⏱️ 18 menit baca • 📅 15 Oct 2026 • 👁️ 1 views
Scaling Real-time WebSocket Clusters dengan Redis Pub/Sub

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan aplikasi web modern, kebutuhan akan komunikasi real-time—mulai dari live chat, pembaruan dasbor finansial, kolaborasi dokumen secara langsung, hingga sistem pelacakan logistik—telah menjadi standar industri yang tidak bisa ditawar. Pengguna mengharapkan latensi di bawah 100 milidetik dan ketersediaan sistem 99.99%. Namun, ketika basis pengguna berkembang dari ratusan menjadi ratusan ribu atau bahkan jutaan koneksi simultan (concurrent connections), arsitektur tradisional berbasis Single-Server WebSocket runtuh seketika.

Bottleneck Performa pada Arsitektur Tunggal

Sebuah server aplikasi tunggal (baik itu dibangun menggunakan Node.js, Go, maupun PHP dengan Swoole) memiliki batas fisik pada jumlah deskriptor file (file descriptors) yang dapat dibuka oleh sistem operasi Linux per proses, serta kapasitas memori RAM untuk mempertahankan koneksi TCP persistent tetap terbuka. Lebih krusial lagi, tantangan terbesar muncul ketika aplikasi harus diskalakan secara horizontal (horizontal scaling).

Bayangkan sebuah skenario di mana arsitektur di-deploy di belakang sebuah Load Balancer (seperti Nginx) yang mendistribusikan traffic ke tiga instance server backend yang berbeda: Instance A, Instance B, dan Instance C. Klien X terhubung ke Instance A, sementara Klien Y terhubung ke Instance B. Ketika Klien X mengirimkan pesan yang ditujukan untuk Klien Y, Instance A tidak memiliki pengetahuan kontekstual, memori, atau jalur komunikasi langsung ke Klien Y karena koneksi TCP Klien Y di-maintain oleh Instance B. Pesan tersebut akan hilang di dalam silo memori Instance A.

Sebelum vs Sesudah Penerapan Redis Pub/Sub

  • Sebelum (Silo/Monolitik): Koneksi WebSocket terisolasi per server. Pesan yang dikirimkan oleh pengguna pada satu instance server tidak dapat disiarkan ke pengguna yang terhubung ke instance server yang berbeda. Penambahan server backend justru merusak integritas fungsionalitas real-time.
  • Sesudah (Terdistribusi dengan Redis Pub/Sub): Seluruh instance server backend terhubung ke sebuah Message Broker terpusat menggunakan pola Publish/Subscribe (Pub/Sub) yang disediakan oleh Redis. Ketika Instance A menerima pesan dari Klien X yang harus disiarkan ke seluruh ruangan, Instance A mempublikasikan (publish) pesan tersebut ke sebuah channel Redis. Seluruh instance server (A, B, dan C) yang berlangganan (subscribe) ke channel tersebut akan menerima pesan secara instan dan meneruskannya ke klien masing-masing yang terhubung melalui WebSocket. Arsitektur ini memungkinkan penskalaan horizontal tanpa batas (infinite horizontal scaling).

2. Konsep Inti & Deep Dive Arsitektur

Redis Pub/Sub adalah mekanisme pengiriman pesan di mana pengirim (publisher) tidak memprogram pesan untuk dikirim ke penerima tertentu secara spesifik. Sebaliknya, pesan dipublikasikan ke dalam kategori atau channel tertentu tanpa mengetahui siapa (jika ada) yang akan mendengarkannya. Demikian pula, pelanggan (subscribers) menyatakan minat pada satu atau lebih channel dan hanya menerima pesan yang menarik bagi mereka, tanpa mengetahui siapa pengirimnya.

Alur Eksekusi Data (Data Flow Under the Hood)

  1. Handshake WebSocket: Klien melakukan HTTP request dengan header Upgrade: websocket ke Nginx.
  2. Load Balancing: Nginx meneruskan request tersebut ke salah satu backend server (misal: Node.js Instance 1) berdasarkan algoritma least_conn atau ip_hash.
  3. Persistent Connection: Koneksi TCP dipertahankan terbuka. Instance 1 mendaftarkan koneksi klien ke dalam in-memory map lokal.
  4. Publish Event: Klien mengirimkan payload JSON melalui WebSocket. Backend Instance 1 menerima payload, memproses logika bisnis, lalu mengeksekusi perintah Redis: PUBLISH chat_room_123 '{"user":"Alice","message":"Halo Dunia"}'.
  5. Broker Distribution: Redis menerima perintah PUBLISH dan secara instan mendistribusikan pesan tersebut ke semua koneksi klien Redis yang berada dalam mode subscriber untuk channel chat_room_123.
  6. Broadcast to Clients: Backend Instance 1, Instance 2, dan Instance 3 yang semuanya melakukan subscribe ke chat_room_123 menerima pesan tersebut dari Redis, lalu melakukan iterasi pada in-memory map lokal mereka dan mengirimkan pesan ke seluruh klien yang terhubung ke instance masing-masing.

Visualisasi Arsitektur

+-------------------------------------------------------------------------+
|                              DNS & CDN                                  |
+-------------------------------------------------------------------------+
                                     |
                                     v
+-------------------------------------------------------------------------+
|                        Nginx Load Balancer                              |
|                  (Reverse Proxy & WebSocket Proxying)                   |
+-------------------------------------------------------------------------+
           /                         |                         \
          v                          v                          v
+--------------------+     +--------------------+     +--------------------+
| Backend Node.js #1 |     | Backend Node.js #2 |     | Backend Node.js #3 |
| (WebSocket Server) |     | (WebSocket Server) |     | (WebSocket Server) |
+--------------------+     +--------------------+     +--------------------+
          |                          |                          |
          +--------------------------+--------------------------+
                                     |
                                     v
+-------------------------------------------------------------------------+
|                           Redis Pub/Sub Cluster                         |
|                     (In-Memory Message Broker)                          |
+-------------------------------------------------------------------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Untuk memilih strategi sinkronisasi kluster real-time, kita harus membandingkan Redis Pub/Sub dengan alternatif lain seperti Sticky Sessions murni, RabbitMQ (AMQP), dan Apache Kafka.

Parameter Komparasi Redis Pub/Sub Sticky Sessions Murni RabbitMQ / AMQP Apache Kafka
Kompleksitas Setup Rendah Sangat Rendah Sedang-Tinggi Tinggi
Latensi Pesan Ultra Rendah (< 2ms) N/A (Tidak ada sinkronisasi) Rendah (2-5ms) Sedang (5-15ms)
Persistensi Pesan Tidak Ada (Fire-and-Forget) N/A Ada (Durable queues) Ada (Log-based storage)
Penggunaan RAM/CPU Sangat Efisien Bergantung Server Sedang Tinggi
Skalabilitas Kluster Tinggi Terbatas (Single Instance) Sangat Tinggi Sangat Tinggi
Ideal Use Case Chat, Live Ticker, Notifikasi Aplikasi Stateful Sederhana Enterprise Message Queuing Big Data Stream Processing

[!WARNING] Redis Pub/Sub bersifat fire-and-forget. Jika sebuah server backend terputus dari Redis saat pesan dipublikasikan, pesan tersebut akan hilang selamanya dan tidak disimpan dalam antrean (queue). Jika aplikasi Anda menuntut jaminan pengiriman (guaranteed delivery), gunakan Redis Streams atau beralihlah ke RabbitMQ/Kafka.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum melangkah ke implementasi kode, pastikan infrastruktur dasar telah disiapkan dengan spesifikasi minimum berikut:

  • Sistem Operasi: Ubuntu Server 22.04 LTS (x86_64) dengan kernel Linux tuning (peningkatan batas file descriptors).
  • Web Server / Proxy: Nginx versi 1.18+ (mendukung upgrade header WebSocket).
  • Runtime Backend: Node.js v18.x LTS (menggunakan pustaka ws dan ioredis).
  • In-Memory Store: Redis Server v7.0+.

Konfigurasi Sistem Operasi (Kernel Tuning)

Edit file /etc/security/limits.conf untuk memastikan sistem dapat menangani ratusan ribu koneksi jaringan:

* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535

Terapkan perubahan parameter kernel pada /etc/sysctl.conf:

fs.file-max = 2097152
net.ipv4.tcp_max_syn_backlog = 65535
net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1

Jalankan perintah berikut untuk menerapkan perubahan kernel:

sudo sysctl -p

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Instalasi dan Konfigurasi Redis

Instal Redis di server lokal atau node terpusat:

sudo apt update
sudo apt install redis-server -y

Sesuaikan konfigurasi pada /etc/redis/redis.conf untuk performa tinggi:

bind 0.0.0.0
protected-mode yes
port 6379
timeout 300
tcp-keepalive 60
daemonize no
supervised systemd
loglevel notice
databases 16
maxmemory 2gb
maxmemory-policy allkeys-lru

Restart layanan Redis:

sudo systemctl restart redis-server
sudo systemctl enable redis-server

Langkah 2: Setup Proyek Backend Node.js

Inisialisasi direktori proyek dan instal dependensi yang diperlukan:

mkdir websocket-cluster-app
cd websocket-cluster-app
npm init -y
npm install ws ioredis dotenv

Langkah 3: Kode Aplikasi Backend (server.js)

Buat file server.js dengan implementasi lengkap yang menghubungkan WebSocket dengan Redis Pub/Sub.

const http = require('http');
const { WebSocketServer } = require('ws');
const Redis = require('ioredis');

// Konfigurasi port dari environment variable atau default ke 3000
const PORT = process.env.PORT || 3000;
const INSTANCE_ID = process.env.INSTANCE_ID || `node-${Math.random().toString(36).substring(7)}`;

// HTTP Server dasar
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`WebSocket Cluster Node ID: ${INSTANCE_ID}\n`);
});

// WebSocket Server instance
const wss = new WebSocketServer({ server });

// Redis Client untuk Publishing
const pub = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: process.env.REDIS_PORT || 6379,
});

// Redis Client untuk Subscribing (Harus instance terpisah)
const sub = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: process.env.REDIS_PORT || 6379,
});

const CHANNEL_NAME = 'global_chat_channel';

// Subscribe ke channel Redis
sub.subscribe(CHANNEL_NAME, (err, count) => {
  if (err) {
    console.error(`[${INSTANCE_ID}] Gagal melakukan subscribe ke Redis:`, err);
    process.exit(1);
  }
  console.log(`[${INSTANCE_ID}] Berhasil subscribe ke channel Redis: ${CHANNEL_NAME}. Total subscriptions: ${count}`);
});

// Menangani pesan yang masuk dari Redis Pub/Sub
sub.on('message', (channel, message) => {
  if (channel === CHANNEL_NAME) {
    console.log(`[${INSTANCE_ID}] Menerima pesan dari Redis Pub/Sub:`, message);
    // Broadcast pesan ke seluruh klien WebSocket yang terhubung ke instance ini
    wss.clients.forEach((client) => {
      if (client.readyState === client.OPEN) {
        client.send(message);
      }
    });
  }
});

// Manajemen koneksi WebSocket klien
wss.on('connection', (ws, req) => {
  const clientIp = req.socket.remoteAddress;
  console.log(`[${INSTANCE_ID}] Klien baru terhubung dari IP: ${clientIp}`);

  ws.on('message', async (data) => {
    try {
      const parsedMessage = JSON.parse(data.toString());
      console.log(`[${INSTANCE_ID}] Menerima pesan dari klien WebSocket:`, parsedMessage);

      // Tambahkan metadata instance pengirim jika diperlukan
      const payload = JSON.stringify({
        senderInstance: INSTANCE_ID,
        user: parsedMessage.user || 'Anonymous',
        text: parsedMessage.text,
        timestamp: Date.now(),
      });

      // Publikasikan pesan ke Redis, bukan langsung broadcast lokal
      await pub.publish(CHANNEL_NAME, payload);
    } catch (error) {
      console.error(`[${INSTANCE_ID}] Error memproses pesan masuk:`, error);
      ws.send(JSON.stringify({ error: 'Format pesan tidak valid. Gunakan JSON.' }));
    }
  });

  ws.on('close', () => {
    console.log(`[${INSTANCE_ID}] Klien terputus dari server.`);
  });

  ws.on('error', (error) => {
    console.error(`[${INSTANCE_ID}] Error pada koneksi WebSocket klien:`, error);
  });
});

server.listen(PORT, () => {
  console.log(`[${INSTANCE_ID}] Server berjalan dan mendengarkan pada port ${PORT}`);
});

Langkah 4: Konfigurasi Nginx sebagai Load Balancer & WebSocket Reverse Proxy

Buat file konfigurasi Nginx pada /etc/nginx/sites-available/websocket_cluster:

upstream websocket_backend {
    # Menggunakan algoritma least_conn untuk distribusi beban optimal
    least_conn;
    
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;

    # Konfigurasi Keepalive untuk koneksi backend
    keepalive 64;
}

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

    access_log /var/log/nginx/ws_access.log;
    error_log /var/log/nginx/ws_error.log;

    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        
        # Header krusial untuk proses Upgrade HTTP ke WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header 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 persistent jangka panjang
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_connect_timeout 60s;

        # Nonaktifkan buffering agar pesan real-time tidak tertahan
        proxy_buffering off;
    }
}

Aktifkan konfigurasi Nginx dan lakukan validasi sintaks:

sudo ln -s /etc/nginx/sites-available/websocket_cluster /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah beberapa baris kode paling krusial dalam arsitektur di atas:

  1. Dua Koneksi Redis Terpisah (pub dan sub):

    const pub = new Redis({...});
    const sub = new Redis({...});
    

    Mengapa harus dua? Di dalam protokol Redis, ketika sebuah koneksi klien memasuki mode subscriber (SUBSCRIBE), klien tersebut dilarang mengirimkan perintah lain selain perintah manajemen subscription (seperti SUBSCRIBE, UNSUBSCRIBE, PING). Jika Anda mencoba menjalankan perintah PUBLISH atau GET menggunakan koneksi yang sama yang sedang aktif berlangganan, Redis akan mengembalikan error. Oleh karena itu, arsitektur publisher/subscriber wajib memisahkan koneksi fisik menjadi dua instance terpisah.

  2. Pemisahan Logika Publish dan Broadcast Lokal:

    await pub.publish(CHANNEL_NAME, payload);
    

    Ketika klien mengirim pesan, backend tidak langsung melakukan looping ke seluruh klien lokal. Pesan dikirim ke Redis terlebih dahulu. Ini memastikan bahwa jika Anda memiliki 5 instance server backend yang berjalan, semua klien di ke-5 server tersebut (termasuk klien di server pengirim sendiri) akan menerima pesan secara seragam melalui iven sub.on('message', ...).

  3. Pengaturan Header Nginx Upgrade dan Connection:

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    

    Secara default, Nginx menutup koneksi setelah siklus request-response HTTP selesai. Header ini memberitahu Nginx untuk menginstruksikan upstream server agar mengubah protokol dari HTTP/1.1 menjadi WebSocket, serta menjaga agar terowongan TCP tetap terbuka (persistent).


7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Platform Trading Finansial Real-Time

Sebuah perusahaan teknologi finansial (FinTech) memerlukan infrastruktur untuk menyiarkan perubahan harga saham tick-by-tick kepada 250.000 pengguna aktif bersamaan.

  • Topologi Deploy: Menggunakan 5 node server aplikasi berbasis Node.js (masing-masing menghandle ~50.000 koneksi WebSocket) di belakang Nginx Load Balancer dengan load distribution least_conn.
  • Redis Sentinel / Redis Cluster: Untuk menghindari kegagalan titik tunggal (Single Point of Failure), Redis dikonfigurasi dalam mode Redis Sentinel dengan 1 Master dan 2 Replica. Jika node Redis Master mengalami gangguan (failover), Sentinel secara otomatis mempromosikan salah satu Replica menjadi Master dalam waktu kurang dari 3 detik, sementara klien Node.js secara otomatis merebut kembali koneksi melalui pustaka ioredis.
  • Manajemen Memori Redis: Dengan alokasi RAM 16GB pada server Redis, eviction policy diset ke noeviction untuk data kritikal, dan menggunakan Pub/Sub channels murni yang tidak mengonsumsi memori persisten jangka panjang karena pesan langsung dibuang setelah dikirim ke seluruh subscribers yang aktif.

8. Common Pitfalls, Anti-Patterns & Solusi

  1. Menggunakan Satu Koneksi Redis untuk Publish dan Subscribe:

    • Anti-Pattern: Mencoba memanggil redisClient.publish() menggunakan objek klien Redis yang sama yang sedang memanggil redisClient.subscribe().
    • Solusi: Selalu instansiasi dua koneksi independen, satu khusus untuk Publisher dan satu khusus untuk Subscriber.
  2. Memory Leak pada In-Memory Client Map:

    • Anti-Pattern: Menyimpan referensi objek WebSocket ke dalam struktur data global kustom tanpa menghapusnya saat event close atau error terpanggil.
    • Solusi: Selalu gunakan iterasi bawaan pustaka wss.clients atau pastikan blok event ws.on('close', ...) membersihkan referensi memori dengan benar.
  3. Timeout Koneksi oleh Nginx atau Firewall Tengah:

    • Anti-Pattern: Membiarkan konfigurasi proxy_read_timeout default Nginx (60 detik). Klien akan terputus secara sepihak setiap 60 detik jika tidak ada aktivitas.
    • Solusi: Set proxy_read_timeout 3600s; danimplementasikan Heartbeat Ping/Pong aplikasi setiap 30 detik untuk mendeteksi koneksi mati (dead sockets).

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji ketahanan kluster WebSocket yang telah dibangun, Anda dapat menggunakan alat pengujian beban (load testing tool) profesional seperti autocannon atau artillery.

Menjalankan Load Testing dengan Artillery

Buat file konfigurasi uji beban load-test.yml:

config:
  target: "ws://chat.enterprise.internal"
  phases:
    - duration: 60
      arrivalRate: 100
      rampTo: 500
      name: "Simulasi lonjakan traffic bertahap"
  processor: "./test-script.js"
scenarios:
  - engine: "ws"
    flow:
      - send: '{"user":"LoadTestBot","text":"Halo dari pengujian performa!"}'
      - think: 5

Jalankan pengujian:

npx artillery run load-test.yml

Metrik Kinerja Utama yang Harus Dipantau:

  • CPU Utilization: Pastikan penggunaan CPU pada server Node.js dan Redis tidak melebihi 80% pada beban puncak.
  • Resident Set Size (RSS) Memory: Pantau konsumsi memori heap Node.js untuk mencegah memory leaks akibat pertumbuhan map koneksi.
  • Redis Connected Clients: Pantau metrik connected_clients menggunakan perintah INFO clients di CLI Redis.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan arsitektur WebSocket kluster berbasis Redis Pub/Sub ke lingkungan produksi (production), pastikan Anda telah memverifikasi seluruh item pada checklist berikut:

  • Batas file descriptors sistem operasi Linux telah ditingkatkan (sysctl.conf dan limits.conf).
  • Nginx dikonfigurasi dengan header Upgrade dan Connection yang benar, serta penonaktifkan buffering (proxy_buffering off).
  • Koneksi Redis dipisahkan secara tegas antara Publisher dan Subscriber.
  • Implementasi mekanisme Heartbeat / Ping-Pong di sisi klien dan server untuk mendeteksi koneksi terputus secara dini.
  • Penerapan Redis Sentinel atau Redis Cluster untuk memastikan ketersediaan tinggi (High Availability) pada lapisan Message Broker.
  • Pengaturan SSL/TLS (WSS - WebSocket Secure) aktif menggunakan sertifikat valid (Let's Encrypt) pada Nginx.

Dengan menerapkan arsitektur terdistribusi ini, aplikasi web Anda kini memiliki fondasi yang kokoh, tangguh, dan dapat diskalakan secara horizontal untuk melayani ratusan ribu pengguna secara real-time tanpa mengorbankan latensi maupun stabilitas sistem.

🏷️ Tags: #Nginx #Redis #Websocket #System Architecture #Node.js