Menaklukkan keterbatasan satu node: arsitektur multi-node dengan load balancer, sticky sessions, broadcast WebSocket lintas node via Redis pub/sub, serta graceful restart tanpa downtime.

Setelah di episode 21 kita membangun server game dan IoT — pada episode kali ini kita menaikkan skala: dari satu node menjadi banyak node. Semua yang kita pelajari — worker, coroutine, shared memory — bekerja di satu mesin. Begitu traffic melebihi satu mesin, kalian butuh arsitektur terdistribusi.
Mengapa episode ini penting? Karena inilah batas yang sering membuat aplikasi Swoole "berhasil di lab, gagal di produksi". Satu server Swoole bisa melayani ratusan ribu koneksi — tetapi WebSocket cluster, sticky session, dan restart tanpa downtime adalah masalah yang sama sekali berbeda level. Episode ini menjembatani keduanya.
Arsitektur dasar multi-node:
Tiga node Swoole di belakang load balancer. Sekarang timbul masalah yang tidak ada di satu node:
| Masalah | Di satu node | Di multi-node |
|---|---|---|
| Broadcast WebSocket | $server->connections | Tidak cukup — koneksi tersebar di node lain |
| State shared | Swoole\Table | Harus pindah ke Redis/database |
| Koneksi user | Satu node tahu semua | Node lain tidak tahu |
| Restart node | Cepat | Berisiko memutus semua koneksi |
Solusi untuk masing-masing ada di bawah.
Koneksi WebSocket bersifat stateful: user terhubung ke satu node dan berharap pesannya datang dari sana. Bila load balancer mengarahkan user ke node lain, koneksi putus.
Dua pendekatan:
ip_hash/hash di Nginx/HAProxy.Tip
Untuk HTTP biasa, kalian bisa memakai dispatch_mode => 4 (IP hash) di satu node. Tapi untuk multi-node, atur sticky di load balancer — jangan di aplikasi. WebSocket wajib sticky: browser tidak punya mekanisme failover otomatis antar node.
Masalah inti broadcast lintas node: user terhubung ke Node 1, user lain di Node 3. Node 1 tidak punya koneksi Node 3. Solusinya: Redis pub/sub sebagai bus pesan — setiap node mendaftarkan dirinya, dan pesan broadcast diteruskan lewat Redis.
$server->on('Message', function (Server $server, Frame $frame) use ($redis) {
// 1. Broadcast lokal (node ini)
foreach ($server->connections as $fd) {
if ($fd !== $frame->fd && $server->isEstablished($fd)) {
$server->push($fd, $frame->data);
}
}
// 2. Teruskan ke node lain via Redis
$redis->publish('chat-broadcast', json_encode([
'data' => $frame->data,
'sender' => $frame->fd,
'origin' => getNodeId(),
]));
});Setiap node menjalankan subscriber yang mendengarkan channel dan meneruskan ke koneksi lokalnya:
use Swoole\Process;
$subscriber = new Process(function (Process $proc) use ($server, $redisSub) {
$redisSub->subscribe(['chat-broadcast'], function ($redis, $channel, $msg) use ($server) {
$payload = json_decode($msg, true);
if ($payload['origin'] === getNodeId()) {
return; // pesan dari node sendiri — sudah di-broadcast lokal
}
foreach ($server->connections as $fd) {
if ($fd !== $payload['sender'] && $server->isEstablished($fd)) {
$server->push($fd, $payload['data']);
}
}
});
});
$server->addProcess($subscriber);Pola ini membuat broadcast global: satu pesan dari Node 1 sampai ke semua user di Node 2 dan 3. origin mencegah pesan berputar balik ke node asal (loop broadcast).
Warning
Tanpa pengecekan origin, satu pesan bisa dipancarkan dua kali oleh node asal (sekali lokal, sekali hasil subscribe). Simpan identitas node unik (misal env NODE_ID), dan lewati pesan yang berasal dari diri sendiri. Ini bug klasik cluster broadcast yang sulit dideteksi karena hanya muncul di multi-node.
Swoole\Table (episode 12) berhenti bekerja lintas node — memory-nya lokal. Ganti dengan:
| Kebutuhan | Pengganti multi-node |
|---|---|
| Online counter | Redis SADD/SCARD atau INCR |
| Rate limiter | Redis INCR + EXPIRE (sliding window) |
| Cache kecil | Redis/KV shared |
| Lock | Redis SET NX EX (Redlock untuk kritis) |
Contoh online counter global:
$server->on('Open', function (Server $server, Request $req) use ($redis) {
$redis->sAdd('ws:online', $req->fd);
$server->push($req->fd, 'Online: ' . $redis->sCard('ws:online'));
});
$server->on('Close', function (Server $server, int $fd) use ($redis) {
$redis->sRem('ws:online', $fd);
});Restart server Swoole tidak boleh memutus semua koneksi sekaligus. Swoole menyediakan dua mekanisme:
| Signal | Fungsi |
|---|---|
SIGUSR1 | Reload worker — proses lama dilanjutkan sampai request selesai, lalu diganti proses baru |
SIGTERM | Graceful shutdown — berhenti menangani request baru, tunggu yang sedang berjalan selesai |
kill -USR1 $(pgrep -f server.php)Dalam kode, setelan penting agar reload aman:
$server->set([
'max_request' => 100000, // worker direcycle setelah N request
'reload_async' => true, // tunggu handler selesai sebelum mati
]);Untuk deployment multi-node, lakukan rolling restart: reload node satu per satu, pastikan node kembali sehat (health check /health), baru lanjut node berikutnya. User tidak akan merasakan apa pun.
| Masalah | Penyebab | Solusi |
|---|---|---|
| Pesan broadcast ganda | Tidak cek origin di subscriber | Skip pesan dari node sendiri |
| User terputus saat restart | Reload tidak async / semua node direstart bersamaan | reload_async, rolling restart per node |
| Counter online beda antar node | Table lokal dipakai di multi-node | Pindah ke Redis |
| Broadcast tidak sampai ke node lain | Subscriber tidak jalan / Redis down | Health-check subscriber, HA Redis |
| Sticky session rusak | LB tidak di-set ip_hash | Atur sticky di level load balancer |
Pada episode 22 ini, kalian telah menaikkan skala ke multi-node.
Inti yang harus dibawa pulang:
origin agar tidak ganda.Swoole\Table → Redis saat lintas node.SIGUSR1 reload async + rolling restart per node.Di episode 23 selanjutnya, kita menyebarkan aplikasi tanpa PHP terinstall: Swoole CLI & Static Runtime — swoole-cli, build-static-php, dan TypePHP/AOT. Sampai jumpa di episode 23!