Dalam 15 menit ke depan, kita akan merakit sebuah pipa notifikasi real-time berlatensi rendah dari nol sampai berjalan mulus, berbasis pengalaman di lapangan menangani lonjakan traffic jutaan pengguna. Lupakan teori textbook yang kaku; kita akan langsung menyelami bagaimana Go menangani ratusan ribu koneksi WebSocket secara bersamaan tanpa membuat RAM server Anda menjerit.
1. Menu Kita Hari Ini & Alat yang Perlu Disiapkan
Pernahkah Anda mendapati aplikasi web mendadak lumpuh saat ribuan pengguna terkoneksi secara bersamaan karena proses garbage collection Go tersendat atau manajemen goroutine yang buruk? Di dapur backend kali ini, kita tidak akan mengulang kesalahan itu. Kita akan menyusun sebuah sistem notifikasi tangguh menggunakan Go sebagai mesin utama WebSocket, Redis Pub/Sub sebagai lem perekat untuk horizontal scaling lintas server, dan sedikit trik pengaturan memori agar koneksi lama yang mangkir tidak menghabiskan sumber daya sistem.
[!IMPORTANT] Pastikan Anda sudah menginstal Go versi 1.21 atau yang lebih baru, serta Redis server yang aktif di mesin lokal atau container Docker Anda sebelum kita mulai menyalakan kompor.
2. Peta Jalan Singkat: Apa yang Sedang Kita Rakit
Bayangkan kita sedang membangun dapur restoran cepat saji. Go bertindak sebagai koki utama yang melayani pelanggan secara langsung di meja lewat jalur WebSocket, sementara Redis Pub/Sub adalah pelayan pembawa pesan dari dapur pusat ke setiap cabang restoran agar tidak ada pesanan yang terlewat. Alur kerjanya sederhana: klien melakukan handshake WebSocket ke server Go, server mendaftarkan klien ke dalam hub lokal, dan ketika ada event baru, Redis akan menyiarkannya ke seluruh instance server Go yang sedang berjalan secara serentak.
Mari kita siapkan bahan-bahannya secara bertahap, mulai dari inisialisasi lingkungan kerja hingga pengujian beban secara langsung.
3. Langkah 1: Mempersiapkan Fondasi & Environment
Pertama-tama, buat direktori kerja baru di terminal Anda dan inisialisasi modul Go baru. Kita juga akan langsung menarik pustaka gorilla/websocket yang menjadi standar industri untuk menangani protokol WebSocket di Go.
mkdir real-time-pipeline && cd real-time-pipeline
go mod init notification-pipeline
go get github.com/gorilla/websocket
go get github.com/redis/go-redis/v9
Perhatikan respons dari perintah go get tersebut. Jika unduhan berjalan lancar tanpa error resolusi paket, itu tanda bahwa fondasi lingkungan kerja kita sudah siap dan stabil.
4. Langkah 2: Meracik Konfigurasi Inti Tanpa Basa-basi
Sekarang kita masuk ke bagian inti dapur kita. Buat sebuah file bernama main.go. Di sinilah kita merangkai logika Hub untuk mengelola klien WebSocket, serta integrasi Redis Pub/Sub agar arsitektur kita siap melakukan scaling secara horizontal di balik load balancer.
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/redis/go-redis/v9"
)
var ctx = context.Background()
// Upgrader standar untuk mengubah koneksi HTTP menjadi WebSocket
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Sesuaikan untuk production agar membatasi origin
},
}
// Client merepresentasikan satu koneksi aktif
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
// Hub mengelola sekumpulan client aktif
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)
}
}
}
}
}
func main() {
hub := newHub()
go hub.run()
// Koneksi ke Redis untuk Pub/Sub Scaling
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// Subscribe ke channel Redis
pubsub := rdb.Subscribe(ctx, "notifications")
defer pubsub.Close()
go func() {
ch := pubsub.Channel()
for msg := range ch {
hub.broadcast <- []byte(msg.Payload)
}
}()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
hub.register <- client
go client.writePump()
go client.readPump()
})
log.Println("Server WebSocket berjalan di port :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, _, err := c.conn.ReadMessage()
if err != nil {
break
}
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{}))
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
Perhatikan fungsi writePump di atas. Penggunaan ticker untuk mengirim PingMessage secara berkala adalah kunci utama agar koneksi tidak terputus di tengah jalan oleh proxy atau load balancer seperti Nginx yang agresif menutup koneksi idle.
5. Langkah 3: Sentuhan Akhir Keamanan & Optimasi
Di lingkungan produksi, Anda tidak bisa membiarkan koneksi terbuka tanpa batas. Pengaturan batas waktu (timeouts) dan ukuran buffer sangat menentukan performa jangka panjang. Pastikan juga Nginx di depan server Go Anda dikonfigurasi dengan benar untuk mendukung Upgrade header WebSocket.
server {
listen 80;
server_name notif.domain.com;
location /ws {
proxy_pass http://localhost: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 86400s;
proxy_send_timeout 86400s;
}
}
[!TIP] Set nilai
proxy_read_timeoutmenjadi86400s(24 jam) pada blok Nginx Anda agar koneksi WebSocket jangka panjang tidak terputus secara sepihak oleh timeout bawaan reverse proxy.
6. Tes Rasa: Memastikan Semuanya Berjalan Sempurna
Waktunya mencicipi hasil masakan kita! Jalankan server Go di terminal Anda:
go run main.go
Buka tab terminal lain, lalu gunakan CLI Redis untuk mempublikasikan pesan uji coba ke channel notifications:
redis-cli PUBLISH notifications "Halo dari Redis Pub/Sub!"
Jika Anda melihat pesan tersebut langsung mendarat di konsol klien WebSocket yang terhubung, selamat! Pipa real-time Anda telah beroperasi dengan sempurna dan siap mendistribusikan jutaan event.
7. Pertolongan Pertama: Jika Terjadi Kendala Kecil
[!WARNING] Menghadapi error
websocket: close 1006 (abnormal closure)? Ini biasanya terjadi karena jaringan klien terputus mendadak atau idle timeout di level load balancer terlalu singkat. Selalu pastikan mekanisme ping/pong berjalan aktif di level aplikasi dan naikkan batas timeout pada layer reverse proxy Anda.