L
GraphQL API Development • Literally Web ID

Arsitektur Real-Time Data Streaming Menggunakan GraphQL Subscriptions

⏱️ 18 menit baca • 📅 16 Oct 2026 • 👁️ 1 views
Arsitektur Real-Time Data Streaming Menggunakan GraphQL Subscriptions

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan web modern, ekspektasi pengguna terhadap kecepatan dan ketersediaan data secara instan telah mencapai tingkat yang belum pernah terjadi sebelumnya. Dasbor finansial real-time, aplikasi kolaborasi dokumen secara langsung, sistem pemantauan infrastruktur (monitoring), hingga platform e-commerce dengan inventaris yang bergerak cepat tidak lagi dapat diandalkan menggunakan pola komunikasi tradisional berbasis HTTP Request-Response atau teknik polling berkala (short-polling dan long-polling).

Secara historis, arsitektur berbasis REST API sangat efisien untuk operasi CRUD (Create, Read, Update, Delete) diskrit. Namun, ketika diterapkan pada skenario real-time, REST mengalami kemacetan struktural yang parah. Polling secara terus-menerus membebani web server (seperti Nginx, Apache, atau Caddy) dan database (MySQL/MariaDB) dengan jutaan koneksi TCP baru serta query yang sebagian besar menghasilkan data redundan atau kosong (ketika tidak ada perubahan data). Sebaliknya, migrasi langsung ke protokol mentah seperti WebSocket sering kali mengorbankan fleksibilitas skema data, manajemen tipe yang aman (type safety), serta kemudahan pengelolaan di sisi klien yang biasanya dinikmati pengembang melalui ekosistem GraphQL.

GraphQL Subscriptions hadir sebagai jembatan elegan yang menggabungkan kekuatan manajemen query deklaratif GraphQL dengan kemampuan komunikasi dua arah berbasis event-driven dari WebSocket. Meskipun demikian, transisi dari stateless queries ke stateful persistent connections (koneksi persisten berstatus) membuka serangkaian tantangan infrastruktur yang masif. Bottleneck utama di dunia nyata meliputi:

  • Kelelahan Sumber Daya Server (Resource Exhaustion): Setiap koneksi WebSocket memelihara satu file descriptor dan alokasi memori di RAM. Ratusan ribu koneksi konkuren dapat dengan cepat menghabiskan memori server aplikasi jika tidak dikelola dengan benar.
  • Masalah Bottleneck Pub/Sub Terdistribusi: Ketika aplikasi diskalakan secara horizontal (multi-instance Node.js atau Go di belakang load balancer Nginx/Cloudflare), sebuah event yang dipicu pada Server A tidak akan sampai ke klien yang terhubung ke Server B kecuali ada lapisan perantara Pub/Sub terdistribusi seperti Redis Cluster.
  • N+1 Query Problem yang Berlipat Ganda: Pada skenario subscription, lonjakan data masuk yang sering dapat memicu query database berulang secara eksponensial jika resolver tidak dioptimalkan dengan dataloader atau teknik caching yang agresif.

Sebelum vs Sesudah Penerapan

Metrik / Parameter Sebelum (HTTP Polling / REST) Sesudah (GraphQL Subscriptions + Redis Cluster)
Latensi Jaringan Tinggi (tergantung interval polling, 1-5 detik) Ultra-rendah (mendekati real-time, < 50ms)
Beban Server & DB Sangat Tinggi (95% request mengembalikan 304/data sama) Sangat Rendah (koneksi persisten, push data hanya saat event terjadi)
Penggunaan Bandwidth Boros (header HTTP berulang pada setiap polling) Sangat Efisien (payload biner/JSON minimal via WebSocket)
Skalabilitas Horizontal Mudah dengan stateless LB, namun membebani DB Memerlukan infrastruktur stateful dan Redis Pub/Sub backplane

2. Konsep Inti & Deep Dive Arsitektur

GraphQL Subscriptions beroperasi di atas protokol transport yang umumnya menggunakan WebSocket (meskipun protokol lain seperti SSE / Server-Sent Events dapat digunakan). Alur kerja dimulai ketika klien melakukan handshake HTTP awal yang kemudian ditingkatkan (upgrade) menjadi koneksi WebSocket abadi (persistent connection).

Alur Eksekusi Data (Under the Hood)

+--------+      1. WebSocket Connection       +---------+      3. Publish Event      +-------+
| Client | ---------------------------------> | Backend | -------------------------> | Redis |
|  (App) | <--------------------------------- | Server  | <------------------------- | Pub/S |
+--------+      4. Push Payload (JSON)        +---------+      2. Subscribe          +-------+
  1. Inisiasi Koneksi: Klien mengirimkan permintaan WebSocket handshake ke endpoint GraphQL (misalnya wss://api.domain.com/graphql).
  2. Registrasi Subscription: Klien mengirimkan pesan connection_init diikuti dengan operation GraphQL bertipe subscription. Server mendaftarkan klien ini ke dalam memori internal atau event emitter lokal.
  3. Pemicu Perubahan (Event Trigger): Ketika terjadi mutasi data (misalnya createOrder) di salah satu instance backend:
    • Data disimpan ke database utama (MySQL/MariaDB).
    • Backend memanggil fungsi pubsub.publish('ORDER_CREATED', { orderCreated: data }).
  4. Distribusi Cluster: Karena backend berjalan dalam arsitektur multi-node, perintah publish dikirim ke Redis Pub/Sub Channel.
  5. Broadcasting ke Klien: Semua node backend yang mendengarkan Redis Channel tersebut menerima pesan, lalu meneruskannya melalui koneksi WebSocket aktif ke klien masing-masing yang berlangganan.

3. Perbandingan Komparatif / Analisis Pendekatan

Memilih mekanisme real-time yang tepat sangat menentukan umur panjang dan stabilitas arsitektur sistem Anda. Berikut adalah analisis komparatif antara GraphQL Subscriptions, REST Short-Polling, dan Server-Sent Events (SSE).

Fitur Arsitektur GraphQL Subscriptions REST Short-Polling Server-Sent Events (SSE)
Arah Komunikasi Dua Arah (Full-Duplex via WS) Satu Arah (Client ke Server terus) Satu Arah (Server ke Client)
Overhead Protokol Rendah (setelah handshake WS) Tinggi (Header HTTP berulang) Rendah (Koneksi HTTP persisten)
Dukungan Tipe Data Sangat Kuat (Schema-driven) Bergantung pada implementasi Fleksibel (biasanya teks/JSON)
Kompleksitas Setup Tinggi (Membutuhkan WS & Pub/Sub) Sangat Rendah Sedang (Membutuhkan HTTP/2)
Konsumsi RAM Server Tinggi (Menjaga state koneksi) Rendah (Stateless) Sedang (Menjaga koneksi HTTP)
Kinerja di Balik NAT/Proxy Kadang diblokir oleh proxy lama Berjalan mulus di semua jaringan Memerlukan konfigurasi timeout proxy

[!WARNING] Jangan pernah menggunakan GraphQL Subscriptions untuk data yang jarang berubah atau tidak memerlukan kecepatan respons milidetik. Gunakan query standar atau caching CDN (Cloudflare) untuk mengurangi beban infrastruktur secara drastis.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum masuk keimplementasi teknis, pastikan infrastruktur dan lingkungan pengembangan Anda memenuhi spesifikasi berikut:

  • Sistem Operasi: Ubuntu Server 22.04 LTS / Debian 12 Bookworm.
  • Runtime & Bahasa: Node.js v20.x LTS (atau Go 1.22+ untuk performa maksimal).
  • Web Server / Reverse Proxy: Nginx (versi terbaru dengan dukungan modul WebSocket proxy_set_header Upgrade $http_upgrade;).
  • In-Memory Datastore: Redis Server / Redis Cluster v7.2+.
  • Database Relasional: MySQL 8.0 / MariaDB 10.11+.

Verifikasi Port dan Firewall (UFW)

Pastikan port yang diperlukan terbuka dan dikonfigurasi dengan benar:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 4000/tcp
sudo ufw enable

5. Panduan Implementasi Langkah demi Langkah

Kita akan membangun implementasi menggunakan Node.js (TypeScript) dengan Apollo Server, graphql-ws (protokol WebSocket modern), dan ioredis sebagai jembatan ke Redis Pub/Sub.

Langkah 1: Inisialisasi Proyek dan Instalasi Dependensi

mkdir graphql-realtime-engine
cd graphql-realtime-engine
npm init -y
npm install express graphql apollo-server-express graphql-ws ws ioredis reflect-metadata
npm install -D typescript @types/node ts-node nodemon
npx tsc --init

Langkah 2: Konfigurasi TypeScript (tsconfig.json)

Buat file tsconfig.json dengan konfigurasi dasar yang mendukung dekorator dan modul modern:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Langkah 3: Implementasi Skema dan Server (src/index.ts)

Berikut adalah kode lengkap server Node.js yang mengintegrasikan Apollo Server, WebSocket Server, dan Redis Pub/Sub:

import express from 'express';
import http from 'http';
import { ApolloServer } from 'apollo-server-express';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import Redis from 'ioredis';
import { RedisPubSub } from 'graphql-redis-subscriptions';

// Konfigurasi koneksi Redis untuk Pub/Sub
const options = {
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  retryStrategy: (times: number) => Math.min(times * 50, 2000),
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

const COMMENT_ADDED = 'COMMENT_ADDED';

// Definisi Skema GraphQL
const typeDefs = `
  type Comment {
    id: ID!
    content: String!
    postId: ID!
    createdAt: String!
  }

  type Query {
    comments(postId: ID!): [Comment]
  }

  type Mutation {
    addComment(postId: ID!, content: String!): Comment!
  }

  type Subscription {
    commentAdded(postId: ID!): Comment!
  }
`;

// Mock Database sementara (Simulasi MySQL/MariaDB)
const commentsDb: any[] = [];

const resolvers = {
  Query: {
    comments: (_: any, { postId }: { postId: string }) => {
      return commentsDb.filter(c => c.postId === postId);
    },
  },
  Mutation: {
    addComment: (_: any, { postId, content }: { postId: string; content: string }) => {
      const newComment = {
        id: String(commentsDb.length + 1),
        content,
        postId,
        createdAt: new Date().toISOString(),
      };
      
      commentsDb.push(newComment);

      // Publikasikan event ke Redis Pub/Sub
      pubsub.publish(COMMENT_ADDED, { commentAdded: newComment });

      return newComment;
    },
  },
  Subscription: {
    commentAdded: {
      // Menggunakan asyncIterator dari Redis Pub/Sub untuk menangani multi-node scaling
      subscribe: (_: any, { postId }: { postId: string }) => {
        return pubsub.asyncIterator([COMMENT_ADDED]);
      },
      resolve: (payload: any, { postId }: { postId: string }) => {
        // Filter event berdasarkan postId di tingkat resolver subscription
        if (payload.commentAdded.postId === postId) {
          return payload.commentAdded;
        }
      },
    },
  },
};

async function startServer() {
  const app = express();
  const httpServer = http.createServer(app);

  const schema = makeExecutableSchema({ typeDefs, resolvers });

  // Konfigurasi WebSocket Server
  const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql',
  });

  const serverCleanup = useServer({ schema }, wsServer);

  // Konfigurasi Apollo Server
  const server = new ApolloServer({
    schema,
    plugins: [
      {
        async serverWillStart() {
          return {
            async drainServer() {
              await serverCleanup.dispose();
            },
          };
        },
      },
    ],
  });

  await server.start();
  server.applyMiddleware({ app: app as any });

  const PORT = process.env.PORT || 4000;
  httpServer.listen(PORT, () => {
    console.log(`🚀 Server siap di http://localhost:${PORT}${server.graphqlPath}`);
    console.log(`🚀 Subscriptions siap di ws://localhost:${PORT}/graphql`);
  });
}

startServer().catch((err) => {
  console.error('Gagal menjalankan server:', err);
});

Langkah 4: Konfigurasi Nginx sebagai Reverse Proxy

Agar aplikasi dapat diakses di produksi melalui domain dengan enkripsi SSL/TLS, konfigurasikan Nginx (/etc/nginx/sites-available/graphql-api) agar mendukung koneksi Upgrade WebSocket:

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

server {
    listen 80;
    server_name api.domain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.domain.com;

    ssl_certificate /etc/letsencrypt/live/api.domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.domain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location /graphql {
        proxy_pass http://127.0.0.1:4000/graphql;
        proxy_http_version 1.1;
        
        # Header penting untuk mendukung WebSocket Upgrade
        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 untuk koneksi WebSocket yang panjang
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

  • new RedisPubSub({ publisher, subscriber }): Baris ini menggantikan in-memory event emitter standar. Di lingkungan kluster, instance Redis bertindak sebagai message broker pusat. Tanpa ini, instance Node.js di Server A tidak akan pernah tahu ada event baru yang harus dikirim ke klien yang menempel di Server B.
  • proxy_set_header Upgrade $http_upgrade; dan proxy_set_header Connection $connection_upgrade;: Dalam konfigurasi Nginx, secara default koneksi HTTP standar akan ditutup setelah respons dikirim. Baris ini memberi tahu Nginx untuk meneruskan header Upgrade agar klien dan server backend dapat menegosiasikan transisi protokol dari HTTP ke WebSocket secara mulus.
  • proxy_read_timeout 86400s;: Koneksi WebSocket dibiarkan terbuka dalam waktu yang sangat lama. Tanpa menaikkan nilai timeout Nginx ini, koneksi akan terputus secara otomatis setiap 60 detik (default timeout Nginx untuk pembacaan data).

7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Sistem Notifikasi Finansial Real-Time (Trading Platform)

Sebuah platform perdagangan kripto memerlukan pengiriman data harga instan kepada 50.000 pengguna aktif secara bersamaan.

  1. Arsitektur Skala Horizontal:
    • 4 unit Application Server (Node.js/PM2 cluster mode) berjalan di balik AWS Application Load Balancer (ALB).
    • Redis Cluster dikonfigurasi dengan 3 Master dan 3 Replica untuk memastikan ketersediaan tinggi (High Availability).
  2. Manajemen Beban Database (MySQL):
    • Setiap kali pesanan (order) masuk, data dicatat ke database master MySQL.
    • Karena pembacaan data historis dipisahkan dari subscription streaming (yang langsung memancarkan objek dari memori/payload mutasi), database relational tidak mengalami kelonjakan beban CPU akibat polling berulang.
  3. Manajemen Kegagalan Jaringan (Reconnection Strategy):
    • Di sisi klien, pustaka graphql-ws dikonfigurasi dengan exponential backoff agar jika terjadi gangguan jaringan singkat, klien otomatis mencoba menyambung kembali tanpa membebani server secara serentak (thundering herd problem).

8. Common Pitfalls, Anti-Patterns & Solusi

[!CAUTION] Anti-Pattern 1: Menyimpan State Koneksi di Memory Lokal Tanpa Backplane Redis Kesalahan: Menggunakan PubSub bawaan standar (graphql-subscriptions) saat menjalankan aplikasi di multi-node server. Akibat: Klien yang terhubung ke server berbeda tidak akan menerima pembaruan data. Solusi: Selalu gunakan graphql-redis-subscriptions atau Kafka/RabbitMQ adapter yang terhubung ke message broker eksternal.

[!CAUTION] Anti-Pattern 2: Mengabaikan Auth/Authorization pada Handshake WebSocket Kesalahan: Membiarkan endpoint /graphql terbuka untuk koneksi WebSocket tanpa memvalidasi token JWT saat inisialisasi. Akibat: Siapa pun dapat membuka koneksi dan mendengarkan data sensitif melalui subscription. Solusi: Lakukan verifikasi token JWT di dalam fungsi context atau parameter connectionParams pada modul useServer sebelum koneksi diterima.


9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji ketahanan server WebSocket dalam menangani koneksi konkuren dalam jumlah besar, kita dapat menggunakan tool pengujian beban seperti Artillery atau skrip kustom berbasis ws di Node.js.

Contoh Skrip Pengujian Beban (Load Test Script)

// load-test.js
const { WebSocket } = require('ws');

const CLIENTS = 1000;
let connectedCount = 0;

for (let i = 0; i < CLIENTS; i++) {
  const ws = new WebSocket('ws://localhost:4000/graphql', 'graphql-transport-ws');
  
  ws.on('open', () => {
    connectedCount++;
    if (connectedCount % 100 === 0) {
      console.log(`Berhasil menghubungkan ${connectedCount} klien.`);
    }
    // Kirim inisiasi koneksi graphql-ws
    ws.send(JSON.stringify({ type: 'connection_init' }));
  });

  ws.on('error', (err) => {
    console.error('Error WebSocket:', err.message);
  });
}

Tuning Kernel Linux (/etc/sysctl.conf)

Untuk menangani puluhan ribu koneksi TCP konkuren (C10K problem), sesuaikan parameter kernel sistem operasi Linux Anda:

# Tingkatkan batas maksimal file descriptor
fs.file-max = 2097152

# Optimalkan port ephemeral TCP
net.ipv4.ip_local_port_range = 1024 65535

# Aktifkan reuse koneksi TIME_WAIT
net.ipv4.tcp_tw_reuse = 1

# Tingkatkan ukuran backlog maksimum socket
net.core.somaxconn = 65535

Terapkan perubahan dengan perintah:

sudo sysctl -p

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan arsitektur GraphQL Subscriptions ke lingkungan produksi, pastikan seluruh item dalam checklist berikut telah terpenuhi:

  • Enskripsi Transport (TLS/SSL): Pastikan menggunakan protokol wss:// melalui sertifikat SSL yang valid (Let's Encrypt / Wildcard Certificate).
  • Autentikasi & Otorisasi: Token JWT divalidasi pada fase connection_init WebSocket.
  • Redis Cluster / High Availability: Redis Pub/Sub dikonfigurasi dengan failover otomatis.
  • Tuning Nginx / Load Balancer: Timeout proxy diatur cukup panjang (86400s) dan header Upgrade diaktifkan.
  • Resource Limits (ulimit): Batas file descriptor sistem operasi telah ditingkatkan untuk mencegah error Too many open files.
  • Monitoring & Logging: Integrasikan pemantauan metrik memori RAM, jumlah koneksi WebSocket aktif, dan latensi Redis menggunakan Prometheus & Grafana.

Dengan mengikuti panduan komprehensif ini, Anda kini memiliki fondasi arsitektur web real-time yang tidak hanya responsif bagi pengguna akhir, tetapi juga tangguh, skalabel, dan siap menghadapi beban tinggi di tingkat enterprise.

🏷️ Tags: #Redis #Websocket #Backend Architecture #Node.js #Graphql