L
REST API Design • Literally Web ID

Arsitektur Real-Time WebSocket Skala Masif Menggunakan Redis Pub/Sub dan Node.js Cluster

⏱️ 15 menit baca • 📅 19 Oct 2026 • 👁️ 4 views
Arsitektur Real-Time WebSocket Skala Masif Menggunakan Redis Pub/Sub dan Node.js Cluster

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan aplikasi web modern, ekspektasi pengguna terhadap kecepatan dan interaktivitas telah bergeser dari request-response tradisional (HTTP REST) menuju komunikasi dua arah yang instan (real-time). Fitur seperti live chat, papan skor olahraga langsung, collaborative document editing, hingga notification center memerlukan koneksi persisten yang dapat mengirimkan data dari server ke klien tanpa jeda yang berarti. Protokol standar untuk kebutuhan ini adalah WebSocket (RFC 6455).

Namun, ketika sebuah aplikasi web bertumbuh dan trafiknya meledak hingga jutaan pengguna aktif bersamaan (concurrent users), menjalankan satu instance server backend Node.js tunggal menjadi tidak rasional dan rentan terhadap titik kegagalan tunggal (Single Point of Failure / SPOF). Solusi standar industri adalah mendistribusikan beban menggunakan infrastruktur Load Balancer (seperti Nginx, HAProxy, atau AWS ALB) di depan cluster server aplikasi Node.js.

Di sinilah akar masalah arsitektural muncul: Stateful Connection pada Load Balancer. Berbeda dengan protokol HTTP stateless di mana setiap permintaan dapat diarahkan ke server berbeda secara acak, koneksi WebSocket bersifat stateful dan long-lived. Klien (browser) membuka satu koneksi TCP yang tetap terbuka dengan satu instance server tertentu. Jika Anda memiliki dua instance server (Node-A dan Node-UserB), dan seorang pengguna yang terhubung ke Node-A ingin mengirim pesan ke pengguna lain yang terhubung ke Node-B, Node-A secara default tidak memiliki jalur komunikasi langsung ke memori proses Node-B. Pesan tersebut akan hilang di dalam ruang isolasi memori lokal masing-masing proses Node.js.

Sebelum vs Sesudah Penerapan Arsitektur Terdistribusi

  • Sebelum (Single Node / Naive Approach):

    • Arsitektur: Satu server Node.js menangani semua koneksi WebSocket.
    • Kelemahan: Keterbatasan sumber daya (koneksi maksimum dibatasi oleh batas file descriptor OS dan RAM per mesin), tidak ada redundansi, dan aplikasi akan crash total jika server mengalami lonjakan CPU atau restart.
  • Sesudah (Cluster + Redis Pub/Sub Adapter):

    • Arsitektur: Multi-server Node.js di belakang Load Balancer, dengan Redis bertindak sebagai message broker terpusat menggunakan pola Publish/Subscribe.
    • Keuntungan: Skalabilitas horizontal tanpa batas (horizontal scaling), fault tolerance tinggi, dan sinkronisasi pesan instan lintas node dengan latensi di bawah milidetik.

2. Konsep Inti & Deep Dive Arsitektur

Untuk mengatasi isolasi memori antar proses dalam Node.js Cluster, kita memerlukan sistem perantara pesan (Message Broker) yang beroperasi di luar proses aplikasi utama. Redis dipilih karena kecepatan in-memory-nya yang luar biasa dan dukungan natif terhadap pola komunikasi Pub/Sub (Publish/Subscriber).

Alur Kerja di Balik Layar (Under the Hood)

  1. Handshake & Connection: Klien melakukan inisiasi koneksi WebSocket. Load Balancer (misalnya Nginx) meneruskan (proxy) koneksi tersebut ke Node-A berdasarkan algoritma IP Hash atau Least Connections.
  2. Room / Channel Registration: Klien bergabung ke sebuah channel atau room tertentu (misalnya room:chat_global). Node-A mencatat pemetaan koneksi klien tersebut di dalam struktur data memori lokalnya.
  3. Publishing Message: Klien di Node-A mengirimkan pesan chat. Node-A menerima pesan tersebut, memprosesnya, lalu mempublikasikannya (publish) ke channel Redis menggunakan perintah PUBLISH room:chat_global <payload>.
  4. Broadcasting Across Cluster: Redis menerima pesan tersebut dan secara asinkron mendistribusikannya ke semua instance server Node.js (Node-A, Node-B, Node-C) yang telah berlangganan (subscribe) ke channel room:chat_global.
  5. Delivery to Clients: Node-B dan Node-C menerima pesan dari Redis. Setiap node memeriksa daftar klien lokal yang terhubung ke room tersebut, lalu meneruskan pesan ke browser masing-masing melalui soket TCP yang aktif.

Visualisasi Arsitektur (ASCII Flow Chart)

+-------------------------------------------------------------+
|                         CLIENTS                             |
|         (Browser A)                 (Browser B)             |
+--------------+---------------------------+------------------+
               |                           |
         [WebSocket]                 [WebSocket]
               |                           |
+--------------v---------------------------v------------------+
|                     LOAD BALANCER (Nginx)                   |
|                 (Sticky Sessions / IP Hash)                 |
+--------------+---------------------------+------------------+
               |                           |
        (Proxy Pass)                (Proxy Pass)
               |                           |
+--------------v--------------+     +------v------------------+
|     NODE.JS CLUSTER A       |     |     NODE.JS CLUSTER B   |
|  +-----------------------+  |     |  +-------------------+  |
|  | Local Socket Storage  |  |     |  | Local Socket Store|  |
|  +-----------+-----------+  |     |  +---------+---------+  |
+--------------|--------------+     +------------|------------+
               |                                 |
               +--------+                 +------+
                        |                 |
+-----------------------v-----------------v-------------------+
|                     REDIS SERVER                            |
|                     (Pub/Sub Broker)                        |
+-------------------------------------------------------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Memilih infrastruktur real-time menuntut pemahaman mendalam mengenai trade-off antara kompleksitas operasional, penggunaan memori, dan latensi jaringan.

Parameter Perbandingan Pendekatan Monolitik (Single Node) Redis Pub/Sub + Node.js Cluster Apache Kafka / RabbitMQ + Node.js
Kompleksitas Setup Sangat Rendah Sedang Tinggi
Latensi Pesan < 1 ms (In-memory lokal) 1 - 3 ms (Network loopback ke Redis) 5 - 15 ms (Disk I/O & partition overhead)
Skalabilitas Terbatas pada kapasitas 1 mesin Tinggi (Skala jutaan koneksi) Sangat Tinggi (Enterprise grade streaming)
Konsumsi RAM Rendah per server Sedang (Node.js + Redis overhead) Tinggi (Memerlukan JVM & cluster broker terpisah)
Persistence Pesan Tidak ada (Volatile) Tidak ada (Pub/Sub fire-and-forget) Kuat (Disk persistence & message replay)
Kasus Penggunaan Ideal Aplikasi skala kecil / MVP Chat, Live Dashboard, Notification System Financial ticker, IoT telemetry, Event-Sourcing

[!TIP] Gunakan Redis Pub/Sub ketika prioritas utama Anda adalah latensi rendah dan throughput tinggi untuk pesan yang sifatnya ephemeral (tidak wajib disimpan di antrean jika klien sedang offline). Jika pesan wajib dijamin sampai meskipun klien terputus, pertimbangkan untuk mengombinasikan Redis dengan database persisten atau beralih ke arsitektur berbasis Queue.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum mulai menulis kode, pastikan lingkungan server produksi atau staging Anda memenuhi spesifikasi minimum dan dikonfigurasi dengan benar untuk menangani ribuan koneksi jaringan paralel.

Spesifikasi Lingkungan:

  • OS: Ubuntu Server 22.04 LTS / Debian 12 (Direkomendasikan untuk tuning kernel network).
  • Runtime: Node.js Versi 20.x LTS.
  • Datastore: Redis Server Versi 7.x.
  • Web Server / Proxy: Nginx Versi 1.18+.

Penyesuaian Limit Sistem Operasi (File Descriptors)

Secara default, Linux membatasi jumlah file terbuka per proses (nofile). Karena setiap koneksi WebSocket merepresentasikan satu socket file descriptor, batas ini harus dinaikkan.

Edit file konfigurasi /etc/security/limits.conf:

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

Perbarui juga parameter kernel di /etc/sysctl.conf untuk mengoptimalkan penggunaan memori jaringan:

net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535

Terapkan perubahan dengan perintah:

sysctl -p

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Inisialisasi Proyek Node.js

Buat direktori baru dan instal dependensi yang diperlukan (ws untuk WebSocket server, ioredis untuk klien Redis yang handal, dan dotenv untuk manajemen konfigurasi).

mkdir ws-cluster-demo
cd ws-cluster-demo
npm init -y
npm install ws ioredis dotenv

Langkah 2: Konfigurasi Nginx sebagai Load Balancer & Reverse Proxy

Buat file konfigurasi Nginx /etc/nginx/sites-available/ws_cluster:

upstream websocket_cluster {
    # Menggunakan IP Hash untuk memastikan session stickiness tingkat transport
    ip_hash;
    
    server 127.0.0.1:4001 weight=1 max_fails=3 fail_timeout=10s;
    server 127.0.0.1:4002 weight=1 max_fails=3 fail_timeout=10s;
}

server {
    listen 80;
    server_name ws.example.com;

    location / {
        proxy_pass http://websocket_cluster;
        proxy_http_version 1.1;
        
        # Header krusial untuk upgrade protokol 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;
        
        # Timeout koneksi WebSocket yang panjang (24 jam)
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Aktifkan konfigurasi:

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

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

Buat file server.js dengan kode lengkap produksi yang mengintegrasikan Node.js Cluster, ws, dan Redis Pub/Sub.

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

// Ambil port dari argumen CLI atau environment variable
const PORT = process.env.PORT || 4001;
const INSTANCE_ID = `node-server-${PORT}`;

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

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

// Inisialisasi Redis Publisher & Subscriber
// Redis Pub/Sub memerlukan koneksi terpisah untuk subscribe dan publish
const redisPub = new Redis({ host: '127.0.0.1', port: 6379 });
const redisSub = new Redis({ host: '127.0.0.1', port: 6379 });

const REDIS_CHANNEL = 'global_chat_channel';

// Set untuk menyimpan koneksi klien lokal pada instance ini
const localClients = new Set();

// Berlangganan ke Redis Channel
redisSub.subscribe(REDIS_CHANNEL, (err, count) => {
    if (err) {
        console.error(`[${INSTANCE_ID}] Gagal subscribe ke Redis channel:`, err);
    } else {
        console.log(`[${INSTANCE_ID}] Berhasil subscribe ke ${REDIS_CHANNEL}. Total subs: ${count}`);
    }
});

// Tangkap pesan yang datang dari Redis (dikirim oleh instance server lain atau diri sendiri)
redisSub.on('message', (channel, messageString) => {
    if (channel === REDIS_CHANNEL) {
        try {
            const parsedMessage = JSON.parse(messageString);
            
            // Jangan kirim kembali pesan ke pengirim asli jika berasal dari instance ini
            // (Opsional, tergantung kebutuhan logika aplikasi)
            
            // Broadcast ke semua klien yang terhubung secara lokal ke instance ini
            for (const client of localClients) {
                if (client.readyState === client.OPEN) {
                    client.send(JSON.stringify(parsedMessage));
                }
            }
        } catch (e) {
            console.error(`[${INSTANCE_ID}] Error parsing pesan Redis:`, e);
        }
    }
});

// Manajemen Koneksi WebSocket
wss.on('connection', (ws, req) => {
    localClients.add(ws);
    console.log(`[${INSTANCE_ID}] Klien baru terhubung. Total klien lokal: ${localClients.size}`);

    // Kirim pesan sambutan
    ws.send(JSON.stringify({
        type: 'SYSTEM',
        message: `Terhubung ke instance ${INSTANCE_ID}`
    }));

    // Tangkap pesan dari Klien
    ws.on('message', async (data) => {
        try {
            const payload = JSON.parse(data.toString());
            
            // Tambahkan metadata pengirim
            const enrichedMessage = {
                senderInstance: INSTANCE_ID,
                timestamp: Date.now(),
                data: payload
            };

            // Publikasikan pesan ke Redis Pub/Sub
            await redisPub.publish(REDIS_CHANNEL, JSON.stringify(enrichedMessage));
        } catch (err) {
            console.error(`[${INSTANCE_ID}] Error memproses pesan masuk dari klien:`, err);
        }
    });

    // Tangkap penutupan koneksi
    ws.on('close', () => {
        localClients.delete(ws);
        console.log(`[${INSTANCE_ID}] Klien terputus. Sisa klien lokal: ${localClients.size}`);
    });

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

// Jalankan Server HTTP & WebSocket
server.listen(PORT, () => {
    console.log(`[${INSTANCE_ID}] Server aktif mendengarkan pada port ${PORT}`);
});

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. Dual Redis Connection (redisPub & redisSub):

    const redisPub = new Redis();
    const redisSub = new Redis();
    

    Penjelasan: Dalam klien Redis Node.js (seperti ioredis), ketika sebuah koneksi memasuki mode subscriber (SUBSCRIBE), koneksi tersebut terkunci dan hanya dapat menerima pesan dari channel yang di-subscribe. Koneksi tersebut tidak dapat menjalankan perintah lain seperti PUBLISH atau SET. Oleh karena itu, kita wajib mendeklarasikan dua instansiasi klien Redis terpisah.

  2. Manajemen localClients dengan Set:

    const localClients = new Set();
    

    Penjelasan: Struktur data Set di JavaScript menawarkan performa $O(1)$ untuk penambahan (add), penghapusan (delete), dan iterasi saat melakukan broadcasting. Ini jauh lebih efisien dibandingkan menggunakan Array biasa yang memerlukan pencarian indeks linier $O(N)$.

  3. Pemisahan Tanggung Jawab Pesan: Ketika klien mengirim pesan, pesan tersebut tidak langsung disebarkan ke klien lokal lain. Pesan dikirim ke Redis via redisPub.publish(). Redis kemudian menyiarkan balik pesan tersebut ke semua subscriber (termasuk server pengirim itu sendiri). Desain ini memastikan konsistensi mutlak dan arsitektur loosely coupled lintas node.


7. Skenario Produksi & Studi Kasus Dunia Nyata

Menjalankan Multi-Instance dengan PM2 Process Manager

Di lingkungan produksi, mengelola beberapa proses Node.js secara manual sangatlah rentan. Kita menggunakan PM2 untuk menjalankan beberapa instance cluster pada port berbeda.

Buat file konfigurasi PM2 ecosystem.config.js:

module.exports = {
  apps: [
    {
      name: 'ws-node-4001',
      script: './server.js',
      env: {
        PORT: 4001,
        NODE_ENV: 'production',
      },
      instances: 1,
      exec_mode: 'fork',
    },
    {
      name: 'ws-node-4002',
      script: './server.js',
      env: {
        PORT: 4002,
        NODE_ENV: 'production',
      },
      instances: 1,
      exec_mode: 'fork',
    },
  ],
};

Jalankan aplikasi dengan perintah:

pm2 start ecosystem.config.js
pm2 save
pm2 startup

8. Common Pitfalls, Anti-Patterns & Solusi

  1. Anti-Pattern: Menggunakan Redis Pub/Sub untuk Menyimpan State / History Chat.

    • Masalah: Redis Pub/Sub bersifat fire-and-forget. Jika sebuah instance server Node.js sedang down atau mengalami gangguan jaringan sesaat ketika pesan dipublikasikan, pesan tersebut hilang selamanya dan tidak akan pernah sampai ke klien.
    • Solusi: Untuk riwayat pesan (chat history), gunakan database persisten (seperti PostgreSQL atau MongoDB) yang mencatat pesan secara asinkron setiap kali pesan diterima di server WebSocket sebelum atau sesudah dipublikasikan ke Redis.
  2. Pitfall: Memory Leak pada localClients Set.

    • Masalah: Jika event close atau error pada objek WebSocket gagal dipicu karena kegagalan jaringan yang tidak biasa (misalnya abrupt client disconnection), referensi objek soket akan tetap tersimpan di dalam memori localClients Set, mengakibatkan kebocoran memori secara perlahan.
    • Solusi: Implementasikan mekanisme Heartbeat / Ping-Pong secara berkala.

Implementasi Heartbeat (Ping-Pong Strategy)

Tambahkan kode berikut di dalam blok koneksi wss.on('connection', ...) pada server.js untuk mendeteksi klien "zombie":

ws.isAlive = true;

ws.on('pong', () => {
    ws.isAlive = true;
});

// Interval pengecekan setiap 30 detik
const interval = setInterval(() => {
    wss.clients.forEach((client) => {
        if (client.isAlive === false) {
            localClients.delete(client);
            return client.terminate();
        }
        client.isAlive = false;
        client.ping();
    });
}, 30000);

wss.on('close', () => {
    clearInterval(interval);
});

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji ketahanan arsitektur WebSocket yang telah dibangun, Anda dapat menggunakan alat pengujian beban (load testing tool) khusus WebSocket seperti Artillery.io atau Autocannon.

Contoh Skenario Pengujian dengan Artillery

Buat file konfigurasi load-test.yml:

config:
  target: "http://ws.example.com"
  phases:
    - duration: 60
      arrivalRate: 50
      rampTo: 500
      name: "Ramp up load hingga 500 koneksi per detik"
  engines:
    ws: {}
scenarios:
  - engine: ws
    flow:
      - send:
          json:
            type: "CHAT"
            text: "Halo dari load tester!"
      - think: 5

Jalankan pengujian:

artillery run load-test.yml

Metrik Utama yang Harus Dipantau:

  • CPU Utilization: Pastikan penggunaan CPU merata di semua instance Node.js berkat Load Balancer.
  • Redis Memory & CPU: Pantau latensi perintah Redis (INFO stats / slowlog) untuk memastikan Redis tidak menjadi bottleneck saat throughput pesan melonjak.
  • Active File Descriptors: Pastikan sistem operasi tidak kehabisan alokasi deskriptor file (ulimit -n).

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan sistem ini ke lingkungan production, pastikan seluruh poin dalam checklist berikut telah terpenuhi:

  • Batasan sistem operasi (nofile dan somaxconn) telah ditingkatkan pada level kernel.
  • Load Balancer (Nginx) dikonfigurasi dengan Sticky Sessions (ip_hash) dan parameter proxy_read_timeout yang diperpanjang.
  • Mekanisme Heartbeat (Ping-Pong) aktif untuk membersihkan koneksi zombie.
  • Redis dikonfigurasi dengan kebijakan persistence yang memadai atau dijalankan dengan replikasi Master-Replica untuk ketersediaan tinggi.
  • Monitoring sumber daya (Prometheus + Grafana atau Datadog) terpasang untuk memantau metrik koneksi aktif secara real-time.

Dengan mengintegrasikan Node.js Cluster, Nginx Load Balancer, dan Redis Pub/Sub, Anda kini memiliki fondasi infrastruktur real-time yang tangguh, skalabel, dan siap menangani beban trafik enterprise berskala masif tanpa rasa khawatir.

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