Lesson 0034 · System Design · Modul Studi kasus · ±20 menit

Studi kasus: chat (B): koneksi, presence, ordering

Langkah 5: koneksi WebSocket stateful di banyak server, routing pesan, heartbeat dan presence, urutan dengan seq, dan pesan offline

Sebelumnya, lesson 0033 mengerjakan langkah 1–4 dan berhenti di satu angka: ±3 juta koneksi WebSocket bersamaan, jauh di atas kapasitas satu chat server. Lesson ini adalah langkah 5. Setiap komponen baru menjawab satu masalah yang muncul begitu ada lebih dari satu server.

Target lesson ini: Anda bisa menjawab "Desain v1 chat Anda satu chat server WebSocket. Bagaimana Anda mengembangkannya untuk 3 juta koneksi bersamaan, dengan presence, urutan pesan, dan pesan offline?" tanpa catatan, dalam 90 detik: kenapa koneksi stateful butuh session registry, routing pesan antar server dan trade-off-nya, heartbeat dan presence dengan TTL, seq per percakapan, dan push notification untuk user offline.

Ringkasan dari lesson 0033

User A User B Chat server (1) DB messages simpan + seq peta: user_id → koneksi target: ±3 juta koneksi, ±12.000 pesan/detik pada puncak
v1 (satu chat server) dari lesson 0033. Angka yang dibawa: ±4.000 pesan/detik (puncak ±12.000), ±3 juta koneksi bersamaan, ±80 GB pesan/hari, tabel messages dengan key (conversation_id, seq).

Langkah 5 — Deep dive, bottleneck, trade-off

Langkah ini adalah evolusi dari v1 ke v2, satu perubahan per masalah:

1. Banyak chat server; koneksi bersifat stateful. Misalkan satu chat server sanggup ±50.000 koneksi (asumsi; ukur memori dan CPU per koneksi dengan load test). 3 juta ÷ 50.000 = 60 server, ditambah cadangan. Berbeda dengan app server stateless di news feed, chat server stateful: koneksi user B hidup di satu server tertentu, dan hanya server itu yang bisa mendorong pesan ke B. Load balancer dan proxy harus meneruskan koneksi panjang ini. Di nginx, header Upgrade tidak otomatis diteruskan, dan "By default, the connection will be closed if the proxied server does not transmit any data within 60 seconds." [1] Koneksi juga pasti putus karena hal lain: deploy, server mati, atau batas platform. AWS API Gateway, misalnya, memberi batas durasi koneksi WebSocket 2 jam dan idle timeout 10 menit. [2] Jadi reconnect adalah jalur normal, bukan pengecualian: client menyambung ulang dengan backoff + jitter supaya 50.000 user dari satu server yang mati tidak menyerbu bersamaan.

2. Routing pesan antar server. A terhubung ke server 3, B ke server 7. Simpan session registry user_id → server di Redis, ditulis saat connect dan dihapus saat disconnect. Server 3 menyimpan pesan, membalas ack ke A, membaca registry, lalu mem-publish pesan ke channel milik server 7. Trade-off yang perlu Anda ucapkan: Redis Pub/Sub "exhibits at-most-once message delivery semantics", dan "If the subscriber is unable to handle the message (for example, due to an error or a network disconnect) the message is forever lost." [3] Itu bisa diterima di sini karena pub/sub hanya jalur cepat: pesan sudah tersimpan sebelum ack, dan bagian 4 di bawah menutup celahnya.

3. Heartbeat dan presence. Supaya proxy tidak menutup koneksi yang diam dan server tahu client masih hidup, kirim heartbeat lebih sering daripada timeout terpendek (mis. setiap 30 detik). RFC 6455 menyediakan frame kontrol untuk ini: "A Ping frame may serve either as a keepalive or as a means to verify that the remote endpoint is still responsive." [4] Heartbeat yang sama memberi presence: setiap heartbeat menulis SET presence:42 online EX 60. Redis EXPIRE: "After the timeout has expired, the key will automatically be deleted." [5] Ponsel yang kehilangan sinyal tidak pernah mengirim "logout", tetapi key-nya hilang sendiri dalam 60 detik. Angkanya: 3 juta ÷ 30 detik ≈ ±100.000 penulisan per detik, alasan presence ada di Redis yang di-shard, bukan di tabel SQL. Status online cukup dikirim ke teman yang sedang membuka aplikasi, bukan ke semua kontak, dan boleh terlambat beberapa detik.

4. Ordering dengan seq per percakapan. Jam ponsel tidak bisa dipercaya, dan dua penerima bisa menerima pesan dalam urutan tiba yang berbeda. Server memberi nomor urut (sequence number, seq) yang naik per conversation_id saat menyimpan (mis. counter atomic per percakapan). Urutan hanya perlu benar di dalam satu percakapan; urutan global tidak dibutuhkan dan mahal. Kalau pesan lewat log seperti Kafka, pakai conversation_id sebagai key: "Events with the same event key (e.g., a customer or vehicle ID) are written to the same partition, and Kafka guarantees that any consumer of a given topic-partition will always read that partition's events in exactly the same order as they were written." [6] seq juga mendeteksi pesan yang terlewat: client yang menerima 5012 lalu 5014 tahu 5013 hilang (misalnya karena pub/sub at-most-once) dan mengambilnya lewat REST riwayat. clientMsgId dari lesson 0033 membuat kirim ulang setelah reconnect idempotent: server mengenali pesan yang sama dan mengembalikan seq yang sudah ada.

5. Pesan offline dan multi-device. Kalau B tidak ada di registry, pesan tetap tersimpan; server lalu meminta push notification lewat FCM (Android/web) atau APNs (iOS), layanan platform yang "lets you reliably send messages" ke perangkat yang sedang tidak membuka aplikasi. [7] Saat B membuka aplikasi, client terhubung, mengirim seq terakhir per percakapan (dari user_conversations), dan menerima sisanya. Setiap perangkat menyimpan seq-nya sendiri, jadi laptop dan ponsel sinkron dengan cara yang sama. Untuk grup sampai 100 anggota, server pengirim cukup mengulang langkah 2 per anggota online; ±12.000 pesan/detik × beberapa penerima masih dalam jangkauan pub/sub.

Latihan 1 — Gambar diagram v2 sendiri dulu

Gambar diagram v2 Anda sendiri dulu, mulai dari v1 di ringkasan: di mana session registry, jalur pub/sub antar server, presence, dan push notification berada. Setelah selesai, buka jawabannya.

Lihat jawaban
User A User B Load balancer Chat server 3 Chat server 7 Redis: registry, presence, pub/sub DB messages Push service FCM/APNs WebSocket (stateful) publish ke server 7 subscribe B offline heartbeat 30 dtk → presence TTL 60 dtk
v2: banyak chat server stateful; registry di Redis memberi tahu server mana yang memegang koneksi B; pub/sub at-most-once ditutup oleh pesan tersimpan + seq; B offline mendapat push notification lalu sinkron dari seq terakhir.

Alur kirim: A → server 3: simpan dengan seq berikutnya → ack ke A → baca registry B → B di server 7: publish ke channel server 7 → server 7 mendorong ke B. Kalau B tidak ada di registry: push notification lewat FCM/APNs. Alur reconnect: B terhubung (registry diperbarui) → kirim seq terakhir per percakapan → server mengirim pesan setelahnya.

Latihan 2 — Koneksi, presence, dan ordering

Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review 0035.

Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil review 0035.

Latihan lisan — Ucapkan dengan suara keras

Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

"Desain v1 chat Anda satu chat server WebSocket. Bagaimana Anda mengembangkannya untuk 3 juta koneksi bersamaan, dengan presence, urutan pesan, dan pesan offline?" (90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"3 juta koneksi tidak muat di satu server, jadi saya jalankan banyak chat server di belakang load balancer yang mendukung WebSocket. Koneksinya stateful: B hanya bisa dicapai lewat server yang memegang koneksinya, jadi saya simpan session registry user ke server di Redis, dan server pengirim mem-publish pesan ke channel server penerima. Redis Pub/Sub at-most-once, tapi pesan sudah disimpan sebelum ack, jadi yang terlewat diambil ulang lewat seq. Heartbeat 30 detik menjaga koneksi tetap hidup, karena proxy seperti nginx menutup koneksi diam setelah 60 detik; heartbeat yang sama memperbarui key presence di Redis dengan TTL 60 detik, sekitar 100.000 penulisan per detik. Urutan memakai seq naik per percakapan yang diberi server, bukan jam client. Penerima offline mendapat push notification lewat FCM atau APNs, lalu saat terhubung mengirim seq terakhir per percakapan dan mengambil sisanya. Client juga harus siap reconnect dengan jitter, karena deploy atau batas durasi koneksi memutus koneksi."

Checklist: apakah Anda menyebut…

  • banyak chat server + load balancer; koneksi stateful dan session registry user → server?
  • routing antar server lewat pub/sub, dengan trade-off at-most-once yang ditutup oleh pesan tersimpan + seq?
  • heartbeat (Ping/Pong) karena timeout koneksi diam, dan presence sebagai key dengan TTL?
  • ordering: seq naik per percakapan dari server, bukan timestamp client; gap seq memicu pengambilan ulang?
  • pesan offline: simpan dulu, push notification FCM/APNs, sinkron dari seq terakhir saat reconnect?
  • membenarkan komponen dengan angka (banyak server karena 3 juta koneksi; Redis presence karena ±100.000 heartbeat/detik)?

Lulus jika minimal 4 dari 6.

Bacaan lanjutan (opsional, tidak dihitung dalam durasi) Redis docs: Pub/Sub, ±15 menit. Bagian "Delivery semantics" menjelaskan kenapa pub/sub tidak boleh menjadi satu-satunya jalur pesan, dan kapan Redis Streams lebih cocok.

Modul Studi kasus selesai. Berikutnya, lesson 0035: Review 4, studi kasus + topik lemah dari review sebelumnya.

Istilah: GLOSSARY.md (bagian "Studi kasus") · Indeks: semua lesson System Design

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "bagaimana tanda 'sudah dibaca' bekerja di grup?", "apa yang terjadi pada registry kalau chat server mati mendadak?", atau "kapan pub/sub perlu diganti Redis Streams atau Kafka?"

Sumber: [1] nginx docs: WebSocket proxying · [2] AWS docs: Quotas for configuring and running a WebSocket in API Gateway · [3] Redis docs: Pub/Sub (delivery semantics) · [4] RFC 6455: The WebSocket Protocol, 5.5.2 Ping · [5] Redis docs: EXPIRE · [6] Apache Kafka 4.4 docs: Introduction, main concepts · [7] Firebase docs: Firebase Cloud Messaging