WEBSOCKETS REAL-TIME SCALABILITY REDIS PUB-SUB BACKEND DISTRIBUTED-SYSTEMS NODE.JS JAVASCRIPT SYSTEM-DESIGN MESSAGING

Membangun Aplikasi Real-time Skalabel: Kombinasi WebSockets dan Redis Pub/Sub

⏱️ 13 menit baca
👨‍💻

Membangun Aplikasi Real-time Skalabel: Kombinasi WebSockets dan Redis Pub/Sub

1. Pendahuluan

Di era digital yang serba cepat ini, ekspektasi pengguna terhadap aplikasi web semakin tinggi. Mereka menginginkan pengalaman yang instan dan interaktif: notifikasi yang muncul seketika, obrolan yang lancar tanpa refresh, atau dashboard yang menampilkan data real-time. Untuk memenuhi kebutuhan ini, komunikasi real-time menjadi sangat krusial.

WebSockets adalah teknologi yang menjadi tulang punggung banyak fitur real-time. Dengan kemampuan untuk menjaga koneksi dua arah yang persisten antara klien (browser) dan server, WebSockets memungkinkan pertukaran data secara efisien tanpa overhead HTTP tradisional. Namun, seiring pertumbuhan aplikasi, tantangan skalabilitas WebSockets mulai muncul. Bagaimana jika Anda memiliki ribuan, bahkan jutaan, koneksi WebSocket yang tersebar di banyak server? Bagaimana cara memastikan pesan dari satu server bisa sampai ke klien yang terhubung ke server lain?

Di sinilah Redis Pub/Sub (Publish/Subscribe) masuk sebagai solusi elegan. Dengan mengombinasikan kekuatan WebSockets untuk komunikasi client-server langsung dan Redis Pub/Sub sebagai backbone messaging yang skalabel, kita bisa membangun aplikasi real-time yang tangguh dan siap menghadapi beban tinggi.

Artikel ini akan memandu Anda memahami arsitektur ini, cara kerjanya, dan memberikan contoh implementasi praktis menggunakan Node.js. Mari kita selami!

2. Memahami Fondasi: WebSockets dan Pub/Sub

Sebelum kita menggabungkan keduanya, mari kita pahami terlebih dahulu masing-masing teknologi.

2.1. WebSockets: Komunikasi Dua Arah yang Efisien

📌 WebSockets menyediakan saluran komunikasi dua arah, full-duplex, dan persisten (tetap terbuka) melalui satu koneksi TCP. Ini berbeda dengan HTTP yang bersifat request-response dan stateless.

Bagaimana cara kerjanya?

  1. Handshake: Klien (browser) mengirimkan permintaan upgrade HTTP ke server.
  2. Upgrade: Jika server mendukung WebSockets, ia akan merespons dengan handshake yang meng-upgrade koneksi dari HTTP ke WebSocket.
  3. Persistent Connection: Setelah handshake berhasil, koneksi TCP tetap terbuka, memungkinkan klien dan server saling mengirim pesan kapan saja tanpa perlu membangun koneksi baru.

Contoh Kasus Penggunaan:

Batasan Saat Skala Besar: Meskipun powerful, server WebSocket tunggal memiliki batasan. Jika server crash, semua koneksi akan terputus. Lebih penting lagi, jika aplikasi Anda membutuhkan banyak server WebSocket di belakang load balancer (untuk high availability atau scalability), bagaimana cara server A tahu bahwa klien B yang harus menerima pesan X terhubung ke server C? Inilah masalah yang akan dipecahkan oleh Redis Pub/Sub.

2.2. Redis Pub/Sub: Pola Pesan Fleksibel

💡 Redis Pub/Sub adalah fitur dari Redis yang mengimplementasikan pola pesan Publish/Subscribe. Dalam pola ini, pengirim pesan (publisher) tidak secara langsung mengirim pesan ke penerima tertentu (subscriber). Sebaliknya, publisher mempublikasikan pesan ke “channel” tertentu, dan semua subscriber yang berlangganan channel tersebut akan menerima pesan.

Komponen Utama:

Keuntungan Redis Pub/Sub untuk Real-time Messaging:

3. Arsitektur Skalabel dengan WebSockets dan Redis Pub/Sub

🎯 Mari kita bayangkan arsitektur aplikasi chat sederhana yang skalabel.

  1. Klien (Browser): Terhubung ke salah satu server WebSocket yang tersedia melalui load balancer.
  2. Load Balancer: Mendistribusikan koneksi klien ke beberapa instance server WebSocket. Penting untuk menggunakan sticky sessions atau layer 7 load balancing yang mendukung WebSockets agar klien tetap terhubung ke server yang sama selama sesi mereka.
  3. Server WebSocket (misalnya, Node.js):
    • Menerima koneksi WebSocket dari klien.
    • Ketika klien mengirim pesan (misalnya, “Halo dari Klien A”), server ini tidak langsung mengirim ke klien lain.
    • Sebaliknya, server ini mempublikasikan (publish) pesan tersebut ke channel Redis tertentu (misalnya, chat:general).
    • Setiap server WebSocket juga berlangganan (subscribe) ke channel Redis yang sama (chat:general).
    • Ketika ada pesan baru di channel Redis, semua server WebSocket yang berlangganan akan menerimanya.
    • Setiap server kemudian menyebarkan (broadcast) pesan tersebut ke semua klien WebSocket yang terhubung langsung dengannya.

```mermaid graph TD subgraph Clients C1[Browser Klien 1] C2[Browser Klien 2] end
subgraph Load Balancer
    LB[Load Balancer]
end

subgraph WebSocket Servers
    WS1[WebSocket Server 1]
    WS2[WebSocket Server 2]
end

subgraph Redis
    R[Redis Server (Pub/Sub)]
end

C1 --> LB
C2 --> LB
LB --> WS1
LB --> WS2

WS1 -- Publish/Subscribe --> R
WS2 -- Publish/Subscribe --> R

C1 --- "Kirim Pesan 'Halo'" --> WS1
WS1 --- "PUBLISH chat:general 'Halo'" --> R
R --- "Pesan 'Halo' diterima" --> WS1
R --- "Pesan 'Halo' diterima" --> WS2
WS1 --- "Broadcast 'Halo'" --> C1
WS2 --- "Broadcast 'Halo'" --> C2
<br>

Dalam arsitektur ini, Redis bertindak sebagai "jembatan" atau "bus pesan" antara semua server WebSocket. Pesan yang dipublikasikan oleh satu server akan diterima oleh semua server lainnya melalui Redis, yang kemudian memungkinkan mereka untuk meneruskan pesan ke klien yang relevan. Ini memastikan bahwa semua klien, tidak peduli ke server mana mereka terhubung, akan menerima semua pesan yang dipublikasikan ke channel yang sama.

## 4. Implementasi Praktis: Membangun Chat Sederhana

Mari kita bangun contoh aplikasi chat sederhana menggunakan Node.js, Express, library `ws` untuk WebSocket, dan `ioredis` untuk Redis.

### 4.1. Setup Proyek

Pertama, buat proyek baru dan instal dependensi yang dibutuhkan:

```bash
mkdir websocket-redis-chat
cd websocket-redis-chat
npm init -y
npm install express ws ioredis

Buat file index.js dan public/index.html.

4.2. Server WebSocket Dasar

Kita akan membuat server Express yang juga menghosting server WebSocket.

// index.js
const express = require("express");
const http = require("http");
const WebSocket = require("ws");
const Redis = require("ioredis");

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";

// Koneksi Redis untuk publisher
const publisher = new Redis(REDIS_URL);
// Koneksi Redis untuk subscriber (harus terpisah dari publisher)
const subscriber = new Redis(REDIS_URL);

// Set untuk menyimpan semua koneksi WebSocket aktif
const clients = new Set();

app.use(express.static("public")); // Untuk menyajikan file HTML klien

wss.on("connection", (ws) => {
  console.log("Klien terhubung");
  clients.add(ws); // Tambahkan koneksi baru ke set

  ws.on("message", (message) => {
    const messageString = message.toString();
    console.log(`Pesan diterima dari klien: ${messageString}`);
    // Ketika server menerima pesan dari klien, publish ke Redis
    publisher.publish("chat:general", messageString);
  });

  ws.on("close", () => {
    console.log("Klien terputus");
    clients.delete(ws); // Hapus koneksi dari set
  });

  ws.on("error", (error) => {
    console.error("WebSocket error:", error);
  });
});

// Berlangganan ke channel Redis
subscriber.subscribe("chat:general", (err, count) => {
  if (err) {
    console.error("Gagal berlangganan Redis channel:", err);
    return;
  }
  console.log(
    `Berhasil berlangganan ke channel 'chat:general'. Jumlah channel: ${count}`,
  );
});

// Ketika ada pesan baru di channel Redis
subscriber.on("message", (channel, message) => {
  console.log(`Pesan diterima dari Redis channel '${channel}': ${message}`);
  // Sebarkan pesan ke semua klien WebSocket yang terhubung ke server ini
  clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server berjalan di http://localhost:${PORT}`);
});

4.3. Kode Klien (HTML & JavaScript)

Buat file public/index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket Redis Chat</title>
    <style>
      body {
        font-family: sans-serif;
        margin: 20px;
      }
      #messages {
        border: 1px solid #ccc;
        padding: 10px;
        height: 300px;
        overflow-y: scroll;
        margin-bottom: 10px;
      }
      #messageInput {
        width: calc(100% - 70px);
        padding: 8px;
      }
      #sendButton {
        padding: 8px 15px;
      }
    </style>
  </head>
  <body>
    <h1>Obrolan Real-time</h1>
    <div id="messages"></div>
    <input type="text" id="messageInput" placeholder="Ketik pesan Anda..." />
    <button id="sendButton">Kirim</button>

    <script>
      const messagesDiv = document.getElementById("messages");
      const messageInput = document.getElementById("messageInput");
      const sendButton = document.getElementById("sendButton");

      // Gunakan wss:// jika di produksi dengan HTTPS
      const socket = new WebSocket("ws://localhost:3000");

      socket.onopen = (event) => {
        console.log("Terhubung ke server WebSocket");
        messagesDiv.innerHTML += `<p><em>Anda terhubung!</em></p>`;
      };

      socket.onmessage = (event) => {
        console.log("Pesan diterima:", event.data);
        messagesDiv.innerHTML += `<p>${event.data}</p>`;
        messagesDiv.scrollTop = messagesDiv.scrollHeight; // Scroll ke bawah
      };

      socket.onclose = (event) => {
        console.log("Koneksi WebSocket terputus");
        messagesDiv.innerHTML += `<p><em>Koneksi terputus!</em></p>`;
      };

      socket.onerror = (error) => {
        console.error("WebSocket error:", error);
        messagesDiv.innerHTML += `<p style="color: red;"><em>Terjadi kesalahan!</em></p>`;
      };

      sendButton.addEventListener("click", () => {
        sendMessage();
      });

      messageInput.addEventListener("keypress", (event) => {
        if (event.key === "Enter") {
          sendMessage();
        }
      });

      function sendMessage() {
        const message = messageInput.value;
        if (message.trim() !== "") {
          socket.send(message);
          messageInput.value = "";
        }
      }
    </script>
  </body>
</html>

Cara Menjalankan:

  1. Pastikan Anda memiliki Redis server yang berjalan (misalnya, redis-server di terminal).
  2. Jalankan aplikasi Node.js Anda: node index.js.
  3. Buka beberapa tab browser di http://localhost:3000.
  4. Ketik pesan di salah satu tab, dan Anda akan melihatnya muncul di semua tab lainnya!

Uji Skalabilitas: Untuk benar-benar menguji skalabilitasnya, Anda bisa mencoba menjalankan beberapa instance index.js di port yang berbeda (misalnya, PORT=3001 node index.js, PORT=3002 node index.js). Kemudian, Anda bisa mengarahkan klien ke port yang berbeda atau menggunakan reverse proxy (misalnya Nginx) untuk mendistribusikan koneksi. Dengan Redis Pub/Sub, pesan akan tetap terkirim ke semua klien, tidak peduli ke server WebSocket mana mereka terhubung!

5. Tips dan Best Practices untuk Produksi

Membangun aplikasi real-time skalabel membutuhkan lebih dari sekadar kode dasar. Berikut adalah beberapa tips dan praktik terbaik untuk lingkungan produksi:

Kesimpulan

Membangun aplikasi real-time yang skalabel adalah salah satu tantangan menarik dalam pengembangan web modern. Dengan memahami kekuatan WebSockets untuk komunikasi dua arah yang efisien dan mengintegrasikannya dengan Redis Pub/Sub sebagai backbone messaging yang tangguh, Anda dapat menciptakan sistem yang tidak hanya interaktif tetapi juga mampu menangani jutaan koneksi secara bersamaan.

Arsitektur ini memungkinkan Anda untuk mendistribusikan beban koneksi WebSocket ke banyak server tanpa kehilangan kemampuan untuk menyebarkan pesan secara global. Ini adalah pola desain yang powerful dan banyak digunakan di industri untuk aplikasi chat, notifikasi, live dashboard, dan banyak lagi. Mulailah bereksperimen, dan rasakan kekuatan komunikasi real-time yang sebenarnya!

🔗 Baca Juga