L
Node.js • Literally Web ID

Scaling Redis Pub/Sub untuk Real-time WebSockets di Skala Jutaan Pengguna

⏱️ 15 menit baca • 📅 12 Oct 2026 • 👁️ 1 views
Scaling Redis Pub/Sub untuk Real-time WebSockets di Skala Jutaan Pengguna

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap aplikasi web modern dewasa ini, kebutuhan akan komunikasi real-time dua arah (bidirectional) melalui protokol WebSocket bukan lagi sekadar fitur tambahan (nice-to-have), melainkan standar fondasional. Platform seperti layanan trading finansial, aplikasi kolaborasi dokumen secara langsung, dashboard monitoring infrastruktur, hingga sistem perpesanan instan (chat) menuntut latensi sub-milidetik dan skalabilitas hingga jutaan pengguna aktif secara bersamaan (concurrent connections).

Namun, ketika sebuah aplikasi tumbuh melampaui kapasitas satu instansi server (single node) dan harus di-scale out ke dalam arsitektur multi-node di balik Load Balancer (seperti Nginx, HAProxy, atau Cloudflare Load Balancing), sebuah masalah fundamental langsung muncul: Isolasi Koneksi Memori (Memory Siloing).

Masalah Utama: Terputusnya Konteks Real-Time di Multi-Node

Bayangkan sebuah skenario di mana pengguna A terhubung ke WebSocket Node 1, sementara pengguna B terhubung ke WebSocket Node 2. Ketika pengguna A mengirimkan pesan chat, Node 1 menerima data tersebut melalui socket-nya. Namun, Node 2 sama sekali tidak mengetahui keberadaan pesan tersebut karena proses Node.js atau server Go pada Node 2 hanya menyimpan state koneksi klien yang terhubung secara fisik kepadanya di dalam RAM lokal (in-memory connection pool).

[Client A] ---> (WebSocket) ---> [Nginx LB] ---> [Node 1 (WebSocket Server)]
                                            
[Client B] ---> (WebSocket) ---> [Nginx LB] ---> [Node 2 (WebSocket Server)]

Jika tidak ada lapisan perantara (message broker) yang menyambungkan kedua node ini, pesan dari Client A tidak akan pernah sampai ke Client B. Pendekatan primitif seperti melakukan polling database atau menggunakan mekanisme shared file system jelas tidak memadai karena memperkenalkan I/O overhead yang masif dan latensi yang tidak dapat diterima.

Solusi Industri: Redis Pub/Sub dengan Redis Adapter

Untuk mengatasi isolasi ini, industri mengandalkan Redis Pub/Sub yang dikombinasikan dengan arsitektur Redis Adapter (seperti Socket.io Redis Adapter). Redis bertindak sebagai centralized message bus berkinerja tinggi berbasis in-memory. Setiap kali sebuah node menerima pesan dari klien WebSocket, node tersebut mempublikasikannya (publish) ke channel Redis. Semua node WebSocket lain yang melakukan subscribe ke channel tersebut akan menerima pesan secara instan dan meneruskannya ke klien lokal masing-masing.

Kondisi Sebelum vs Sesudah Penerapan

Aspek Sebelum (Single Node / Tanpa Adapter) Sesudah (Multi-Node + Redis Pub/Sub Adapter)
Skalabilitas Terbatas pada kapasitas RAM & CPU satu server fisik/VM. Horisontal tak terbatas (bisa menambah node WebSocket sesuai beban).
Fault Tolerance Rendah. Jika server mati, seluruh koneksi terputus permanen. Tinggi. Load balancer dapat mengalihkan trafik ke node lain yang sehat.
Sinkronisasi Pesan Gagal total antar node berbeda. Pesan hilang atau tidak terkirim. Real-time broadcast mulus ke seluruh klien tanpa peduli di node mana mereka mendarat.
Kompleksitas Operasional Rendah di awal, namun bottleneck parah saat traffic melonjak. Menengah ke atas (membutuhkan manajemen Redis Cluster & Sticky Sessions).

2. Konsep Inti & Deep Dive Arsitektur

Untuk merancang sistem yang mampu menangani jutaan pengguna, kita harus memahami bagaimana arsitektur jaringan, load balancing, dan message broker berinteraksi di balik layar (under the hood).

Alur Eksekusi Data (Data Flow Architecture)

+----------+     WebSocket     +----------------+     Redis Protocol     +-------------------+
| Client A | ----------------> | Nginx LB       | ---------------------> | Redis Pub/Sub     |
+----------+                   | (Sticky Sess)  |                        | Master/Replica    |
                               +----------------+                        +-------------------+
                                        |                                          |
                                        | WebSocket                                | Pub/Sub Broadcast
                                        v                                          v
                               +----------------+                        +-------------------+
                               | Node 1 (App)   | <--------------------- | Node 2 (App)      |
                               +----------------+   Internal Sync        +-------------------+
  1. Inisialisasi Handshake & Sticky Sessions: Klien melakukan HTTP Handshake untuk upgrade ke protokol WebSocket. Nginx Load Balancer mengarahkan klien ke Node 1. Berkat konfigurasi Sticky Sessions (biasanya berdasarkan cookie route atau IP hashing), klien yang sama akan terus terhubung ke Node 1 selama sesi aktif.
  2. Pengiriman Pesan (Publish): Klien A mengirimkan payload JSON melalui WebSocket ke Node 1. Server aplikasi di Node 1 memproses pesan tersebut dan memanggil perintah PUBLISH chat_room_1 "Hello World".
  3. Distribusi Pesan oleh Redis: Redis Server (atau Redis Cluster) menerima perintah publish dan mendistribusikannya secara asinkron ke seluruh subscriber yang terdaftar pada channel chat_room_1.
  4. Broadcasting Lokal: Node 1 dan Node 2 sama-sama melakukan subscribe ke channel tersebut. Node 2 menerima pesan dari Redis, mendecode payload-nya, lalu melakukan iterasi pada seluruh socket klien lokal yang bergabung dalam chat_room_1 di RAM Node 2, lalu mengirimkan data tersebut ke Client B.

3. Perbandingan Komparatif / Analisis Pendekatan

Memilih infrastruktur real-time memerlukan analisis trade-off yang cermat antara penggunaan sumber daya (CPU, RAM, Network I/O, Latency) dan kompleksitas pemeliharaan.

Pendekatan / Teknologi Latency Rata-rata Konsumsi RAM Kompleksitas Scaling Kegagalan Titik Tunggal (SPOF) Keamanan & Enkripsi
In-Memory Polling (HTTP) Tinggi (>500ms) Rendah Sangat Sulit (Database beban tinggi) Ya (Database Bottleneck) Standar HTTPS/TLS
RabbitMQ AMQP Pub/Sub Sedang (15-50ms) Tinggi (Erlang VM) Tinggi Tergantung Cluster RabbitMQ AMQPS / SSL
Redis Pub/Sub (Standalone) Sangat Rendah (<5ms) Sangat Efisien Sedang Ya (Jika tidak pakai Redis Sentinel/Cluster) Redis AUTH + TLS / Stunnel
Kafka (Event Streaming) Rendah-Sedang Sangat Tinggi Kompleks (JVM tuning) Rendah (Distributed Log) SASL / SSL

[!TIP] Untuk kasus penggunaan chat interaktif dan live notification berskala jutaan pengguna, Redis Pub/Sub adalah pilihan optimal karena kecepatan eksekusi in-memory murni, footprint memori yang kecil, dan dukungan adapter bawaan yang matang di berbagai framework backend (Node.js, Go, Python).


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum melangkah ke implementasi kode, pastikan infrastruktur sistem operasi dan dependensi dasar telah dikonfigurasi dengan benar untuk menahan beban tinggi (high concurrency).

1. Konfigurasi Kernel Linux (Sysctl & File Descriptors)

Secara default, Linux membatasi jumlah file terbuka (file descriptors) per proses. Koneksi WebSocket dihitung sebagai file terbuka. Ubah batasan sistem operasi dengan mengedit /etc/security/limits.conf:

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

Terapkan parameter kernel untuk jaringan berkecepatan tinggi di /etc/sysctl.conf:

# Memperbesar antrean koneksi jaringan masuk
net.core.somaxconn = 65535
# Mengoptimalkan port ephemeral
net.ipv4.ip_local_port_range = 1024 65535
# Mengaktifkan reuse TCP sockets
net.ipv4.tcp_tw_reuse = 1
# Memperbesar ukuran buffer TCP
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

Jalankan perintah berikut untuk menerapkan perubahan:

sysctl -p

2. Instalasi Node.js & Redis Server

Pastikan menggunakan LTS versi terbaru (Node.js 20.x atau lebih tinggi) dan Redis Server 7.x.

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs redis-server build-essential

5. Panduan Implementasi Langkah demi Langkah

Berikut adalah panduan lengkap membangun arsitektur WebSocket multi-node menggunakan Node.js, Socket.io, Redis Adapter, dan Nginx sebagai Load Balancer dengan Sticky Sessions.

Langkah 1: Konfigurasi Nginx sebagai Load Balancer & Sticky Sessions

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

upstream websocket_backend {
    # Menggunakan IP Hash untuk Sticky Sessions sederhana
    ip_hash;
    
    server 192.168.1.101:3000 max_fails=3 fail_timeout=10s;
    server 192.168.1.102:3000 max_fails=3 fail_timeout=10s;
}

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

    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        
        # Header krusial untuk upgrade koneksi 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 untuk koneksi WebSocket jangka panjang
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Langkah 2: Inisialisasi Proyek Node.js

Buat direktori proyek baru dan instal dependensi yang diperlukan:

mkdir enterprise-ws-cluster
cd enterprise-ws-cluster
npm init -y
npm install express socket.io @socket.io/redis-adapter redis dotenv

Langkah 3: Implementasi Kode Server WebSocket (server.js)

Buat file server.js yang akan dijalankan pada setiap node aplikasi:

require('dotenv').config();
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');

const app = express();
const server = http.createServer(app);

const PORT = process.env.PORT || 3000;
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';

async function bootstrap() {
    // 1. Inisialisasi Redis Client Utama dan Client Subscriber khusus
    const pubClient = createClient({ url: REDIS_URL });
    const subClient = pubClient.duplicate();

    pubClient.on('error', (err) => console.error('Redis Pub Error:', err));
    subClient.on('error', (err) => console.error('Redis Sub Error:', err));

    await Promise.all([
        pubClient.connect(),
        subClient.connect()
    ]);

    console.log('Terhubung ke Redis Cluster/Server.');

    // 2. Inisialisasi Socket.io dengan Redis Adapter
    const io = new Server(server, {
        cors: {
            origin: "*",
            methods: ["GET", "POST"]
        },
        pingInterval: 25000,
        pingTimeout: 20000,
    });

    io.adapter(createAdapter(pubClient, subClient));

    // 3. Manajemen Koneksi WebSocket
    io.on('connection', (socket) => {
        console.log(`Klien terhubung ke Node [${PORT}]: ${socket.id}`);

        // Bergabung ke room tertentu
        socket.on('join_room', (room) => {
            socket.join(room);
            console.log(`Socket ${socket.id} bergabung ke room: ${room}`);
            
            // Kirim notifikasi ke seluruh node bahwa ada user baru
            io.to(room).emit('system_message', {
                sender: 'SYSTEM',
                text: `Pengguna ${socket.id} bergabung ke ${room}`,
                timestamp: new Date()
            });
        });

        // Menerima pesan chat dan menyebarkannya
        socket.on('send_message', (data) => {
            const { room, message } = data;
            console.log(`Pesan diterima dari ${socket.id} di room ${room}: ${message}`);
            
            // Broadcast otomatis dikoordinasikan melalui Redis Adapter ke semua node
            io.to(room).emit('receive_message', {
                sender: socket.id,
                message: message,
                nodePort: PORT,
                timestamp: new Date()
            });
        });

        socket.on('disconnect', (reason) => {
            console.log(`Klien terputus [${PORT}] ${socket.id}: ${reason}`);
        });
    });

    server.listen(PORT, () => {
        console.log(`Enterprise WebSocket Server berjalan pada port ${PORT}`);
    });
}

bootstrap().catch((err) => {
    console.error('Gagal menjalankan server:', err);
    process.exit(1);
});

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. Pemisahan pubClient dan subClient:

    const pubClient = createClient({ url: RED_URL });
    const subClient = pubClient.duplicate();
    

    Mengapa harus dua klien terpisah? Berdasarkan spesifikasi protokol Redis, koneksi yang masuk ke mode subscribe (SUBSCRIBE command) terkunci dan hanya dapat menerima pesan pub/sub, tidak dapat digunakan untuk menjalankan perintah lain seperti GET, SET, atau PUBLISH. Oleh karena itu, Socket.io Redis Adapter membutuhkan dua koneksi terpisah secara wajib.

  2. Penggunaan createAdapter(pubClient, subClient): Fungsi ini menjembatani event internal Socket.io dengan channel Redis. Ketika io.to(room).emit() dipanggil, adapter membungkus payload ke dalam format biner standar dan mempublikasikannya ke Redis. Node lain yang mendengar channel tersebut akan mereplikasi event emitter secara lokal pada instans Socket.io mereka.

  3. Konfigurasi pingInterval dan pingTimeout:

pingInterval: 25000, pingTimeout: 20000,

Pengaturan ini krusial di lingkungan enterprise untuk mendeteksi koneksi mati (*dead connections*) akibat terputusnya jaringan atau client yang menutup aplikasi secara paksa. Server mengirimkan ping setiap 25 detik; jika klien tidak merespons dalam 20 detik, koneksi diputus secara sepihak dan memori dibebaskan.

---

## 7. Skenario Produksi & Studi Kasus Dunia Nyata

### Studi Kasus: Platform Flash Sale & Live Chat E-commerce dengan 500.000 Konkurensi
Sebuah platform e-commerce besar mengalami lonjakan trafik masif saat *flash sale*. Arsitektur yang diterapkan menggunakan:
* **4 Unit Node WebSocket** (Masing-masing berjalan di server 8 vCPU, 32GB RAM).
* **1 Cluster Redis** (3 Master, 3 Replica) yang dikelola secara terpusat.
* **Nginx Load Balancer** di depan klaster dengan penyeimbangan beban berbasis `ip_hash`.

**Hasil Optimasi:**
* Sistem mampu menangani hingga 500.000 koneksi WebSocket aktif secara bersamaan tanpa lonjakan latensi (latency tetap di bawah 12ms).
* Penggunaan Redis Memory terjaga di angka ~4.5GB untuk total manajemen *pub/sub channels* dan room mapping.
* Ketika salah satu node WebSocket mengalami kegagalan hardware (*kernel panic*), Nginx otomatis mengalihkan koneksi baru ke node yang sehat dalam waktu kurang dari 3 detik.

---

## 8. Common Pitfalls, Anti-Patterns & Solusi

### 1. Anti-Pattern: Menggunakan Satu Koneksi Redis untuk `pub` dan `sub` Sekaligus
* **Kesalahan:** Menggunakan objek klien Redis yang sama untuk perintah publikasi dan langganan.
* **Dampak:** Aplikasi melempar error `ERR Can't execute command while subscribed` dan membuat adapter Socket.io gagal melakukan sinkronisasi antar node.
* **Solusi:** Selalu gunakan `.duplicate()` seperti yang ditunjukkan pada panduan implementasi.

### 2. Pitfall: Memory Leak pada Socket.io Rooms
* **Kesalahan:** Lupa mengeluarkan socket dari *room* atau tidak membersihkan *event listener* saat klien terputus (`disconnect`).
* **Dampak:** RAM server aplikasi terus meningkat secara perlahan (*memory leak*) hingga server mengalami *Out Of Memory (OOM) Killer*.
* **Solusi:** Socket.io secara otomatis menghapus socket dari seluruh *room* saat event `disconnect` terjadi, namun pastikan custom data structures atau *global maps* di dalam kode Anda dibersihkan secara eksplisit:
```javascript
socket.on('disconnect', () => {
   // Hapus referensi kustom
   activeUserSessions.delete(socket.id);
});

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji ketahanan arsitektur WebSocket yang telah dibangun, gunakan tool pengujian beban modern seperti Artillery.io atau K6 (dengan ekstensi WebSocket k6/x/websockets).

Contoh Skrip Pengujian K6 (load-test.js):

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

export const options = {
    stages: [
        { duration: '1m', target: 10000 }, // Ramp-up hingga 10k pengguna
        { duration: '3m', target: 50000 }, // Pertahankan 50k pengguna
        { duration: '1m', target: 0 },     // Ramp-down
    ],
};

export default function () {
    const url = 'ws://ws.enterprise.internal/socket.io/?EIO=4&transport=websocket';
    
    const res = ws.connect(url, {}, function (socket) {
        socket.on('open', function () {
            console.log('Connected to WebSocket server');
            socket.send(JSON.stringify({ event: 'join_room', data: 'flash_sale_room' }));
        });

        socket.on('message', function (message) {
            check(message, { 'pesan diterima': (msg) => msg.length > 0 });
        });

        socket.sleep(30);
    });

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

Metrik Utama yang Harus Dipantau (Prometheus & Grafana):

  1. redis_connected_clients: Pastikan jumlah koneksi ke Redis tidak mendekati batas maksimum konfigurasi maxclients.
  2. nodejs_heap_used_bytes: Memantau penggunaan memori heap V8 untuk mencegah kebocoran memori.
  3. process_cpu_seconds_total: Memastikan penggunaan CPU merata di seluruh node WebSocket.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan sistem ke lingkungan produksi (live production), verifikasi seluruh poin dalam checklist berikut:

  • Konfigurasi kernel Linux (somaxconn, nofile) telah ditingkatkan pada semua node server.
  • Redis Server dikonfigurasi dengan autentikasi sandi yang kuat (requirepass) dan kebijakan eviksi memori (maxmemory-policy noeviction atau volatile-lru).
  • Nginx Load Balancer telah mengaktifkan ip_hash atau mekanisme Sticky Sessions berbasis cookie.
  • Pemisahan pubClient dan subClient pada kode Redis Adapter telah diuji dengan benar.
  • Monitoring metrik (Prometheus, Grafana, Node Exporter) aktif dan diatur dengan alerting otomatis untuk penggunaan RAM > 85%.
  • Enkripsi SSL/TLS (WSS - WebSocket Secure) aktif pada Nginx menggunakan sertifikat dari Let's Encrypt.

Kesimpulan

Scaling Redis Pub/Sub untuk arsitektur WebSocket multi-node adalah solusi industri yang teruji dan sangat tangguh. Dengan memadukan manajemen Sticky Sessions di load balancer, pemisahan klien pub/sub di level kode aplikasi, serta tuning parameter kernel sistem operasi, aplikasi Anda siap melayani jutaan pengguna secara real-time, aman, dan berkinerja tinggi.

🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server