Dalam 15 menit ke depan, kita akan merakit sebuah sistem notifikasi real-time berstandar industri dari nol sampai berjalan mulus. Pengalaman di lapangan membuktikan bahwa menjaga 100.000 koneksi WebSocket konkuren agar tetap stabil bukanlah perkara sihir, melainkan soal bagaimana kita mengelola memori goroutine dan mencegah lonjakan garbage collection yang brutal. Sambil menyeruput kopi, bayangkan kita sedang berada di ruang kontrol menghadapi trafik flash sale e-commerce: ribuan client terhubung serentak, dan satu saja memory leak kecil di level handler akan membuat server kita bertekuk lutut dalam hitungan menit.
Mari kita bedah peta jalannya secara taktis. Kita tidak akan membuang waktu membahas teori klise tentang apa itu WebSocket. Sebaliknya, kita langsung fokus pada tiga pilar utama yang menentukan hidup-matinya arsitektur real-time: bagaimana kita mendistribusikan tugas melalui Goroutine Pool Management agar CPU tidak jebol, bagaimana kita membersihkan koneksi zombie menggunakan Heartbeat & Connection Cleanup, serta bagaimana kita menyambungkan Redis Pub/Sub Integration for Multi-Node Scaling saat aplikasi kita harus diskalakan secara horizontal di balik load balancer Nginx.
# Mari siapkan direktori kerja kita dan inisialisasi modul Go
mkdir -p real-time-engine && cd real-time-engine
go mod init notification-engine
# Unduh pustaka gorilla/websocket dan driver redis
go get github.com/gorilla/websocket
go get github.com/redis/go-redis/v9
Perhatikan respons terminal Anda setelah perintah di atas tuntas. Jika tidak ada error merah yang muncul, artinya fondasi Go environment kita sudah siap menerima beban kerja berat.
Sekarang kita masuk ke jantung permainan: Goroutine Pool Management dan penanganan koneksi. Setiap client WebSocket yang masuk akan memakan sebuah goroutine jika tidak diatur dengan bijak. Di sinilah kita meracik kode Go yang bersih, efisien, dan menggunakan struktur data channel untuk membatasi konkurensi serta mencegah OOM (Out of Memory) killer menyapa kernel Linux kita.
package main
import (
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
func NewHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
[!IMPORTANT] Jangan pernah membiarkan penulisan ke socket dilakukan secara konkuren tanpa mutex atau channel buffer. Pola
selectdi atas menjamin bahwa jika satu client lambat merespons, goroutine pengirim tidak akan mengalami hang (goroutine leak).
Agar koneksi yang terputus di tengah jalan tidak meninggalkan 'mayat' di memori RAM server, kita wajib menerapkan Heartbeat & Connection Cleanup. Koneksi TCP yang terputus karena kabel cabut atau HP kehilangan sinyal sering kali tidak langsung terdeteksi oleh OS. Di sinilah mekanisme ping-pong berkala menyelamatkan server kita dari kebocoran file descriptor.
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
maxMessageSize = 512
)
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, _, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.hub.unregister <- c
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
n := len(c.send)
for i := 0; i < n; i++ {
w.Write([]byte{'
'})
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
[!TIP] Set nilai
ulimit -ndi OS Linux Anda menjadi minimal 200.000 agar kernel tidak menolak file descriptor baru saat ribuan client mulai handshaking bersamaan.
Ketika aplikasi Go kita sukses menampung 100.000 koneksi pada satu instance server, tantangan berikutnya muncul: bagaimana jika instance server kita bertambah menjadi tiga karena beban trafik melonjak? Di sinilah Redis Pub/Sub Integration for Multi-Node Scaling berperan sebagai jembatan komunikasi antar-node. Ketika Node A menerima event notifikasi, Redis akan menyiarkannya ke Node B dan Node C secara instan.
package main
import (
"context"
"github.com/redis/go-redis/v9"
)
var ctx = context.Background()
func InitRedisPubSub(hub *Hub, redisClient *redis.Client) {
pubsub := redisClient.Subscribe(ctx, "notifications")
go func() {
ch := pubsub.Channel()
for msg := range ch {
hub.broadcast <- []byte(msg.Payload)
}
}()
}
[!WARNING] Pastikan koneksi Redis menggunakan pool yang optimal dan aktifkan mekanisme reconnect otomatis. Jangan sampai kegabutan jaringan sesaat pada Redis memutus seluruh alur broadcast sistem notifikasi Anda.
Sentuhan akhir yang tidak boleh dilewatkan adalah optimasi sistem operasi dan Nginx sebagai reverse proxy. Tambahkan konfigurasi berikut pada blok server Nginx Anda agar koneksi WebSocket tidak terputus secara prematur oleh timeout bawaan:
server {
listen 80;
server_name notify.domain.com;
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Mari kita jalankan verifikasi kilat. Nyalakan server Go Anda dengan perintah go run main.go, lalu buka terminal terpisah dan gunakan perkakas websocat atau script kecil berbasis JavaScript di browser untuk melakukan koneksi uji coba.
# Uji koneksi cepat menggunakan websocat
websocat ws://localhost:8080/ws
Kirimkan sebuah teks dari terminal atau melalui publish Redis, lalu lihat apakah pesan tersebut langsung terpantul ke layar client tanpa jeda. Jika pesan muncul seketika, itu tanda bahwa arsitektur event-driven kita sudah berjalan sempurna di level produksi.
Kendala kecil yang sering ditemui di lapangan biasanya berupa error too many open files. Jangan panik, cukup sesuaikan parameter sistem limit di /etc/security/limits.conf dengan menambahkan baris * soft nofile 200000 dan * hard nofile 200000, lalu restart layanan Anda. Selamat mencoba di environment staging Anda hari ini!