WebSocket Proxying #
Protokol HTTP standar didesain menggunakan paradigma permintaan-respons (request-response) yang bersifat searah (klien mengirim permintaan, server membalas, dan koneksi ditutup). Untuk aplikasi interaktif modern yang membutuhkan transfer data secara real-time—seperti aplikasi obrolan (chatting), grafik harga saham langsung, notifikasi instan, atau permainan multi-pemain online—protokol HTTP biasa menjadi sangat tidak efisien karena memicu beban overhead polling berulang yang tinggi.
WebSocket memecahkan masalah ini dengan menyediakan saluran komunikasi dua arah (full-duplex) yang persisten di atas satu koneksi TCP tunggal. Karena Nginx berada di depan server aplikasi kita sebagai reverse proxy, Nginx wajib dikonfigurasi secara khusus agar dapat mendeteksi jabat tangan (handshake) awal WebSocket, mengubah protokol koneksi HTTP menjadi terowongan TCP persisten, dan mencegah pemutusan koneksi sepihak. Di artikel ini, kita akan membahas cara kerja handshake upgrade, memetakan header secara dinamis, melakukan tuning timeout, serta menyusun skema load balancing upstream untuk kluster server WebSocket.
Alur Kerja Jabat Tangan (Handshake) WebSocket Upgrade #
WebSocket tidak langsung membuat koneksi socket murni dari awal. Sebaliknya, WebSocket memanfaatkan port HTTP standar (port 80 atau 443) untuk melakukan jabat tangan awal menggunakan mekanisme yang disebut HTTP Upgrade.
Berikut adalah visualisasi urutan jabat tangan protokol WebSocket melewati proxy Nginx:
sequenceDiagram
autonumber
actor Browser as Klien Browser
participant Nginx as Nginx Proxy
participant Backend as Backend WS Server (Node.js/Go)
Browser->>Nginx: GET /socket.io/ HTTP/1.1<br/>Upgrade: websocket<br/>Connection: Upgrade
Note over Nginx: Mendeteksi Upgrade header<br/>Memetakan variabel $connection_upgrade
Nginx->>Backend: GET /socket.io/ HTTP/1.1<br/>Upgrade: websocket<br/>Connection: upgrade
Note over Backend: Validasi Key & Handshake
Backend-->>Nginx: HTTP/1.1 101 Switching Protocols<br/>Upgrade: websocket<br/>Connection: Upgrade
Nginx-->>Browser: HTTP/1.1 101 Switching Protocols<br/>Upgrade: websocket<br/>Connection: Upgrade
Note over Browser, Backend: Terowongan TCP Terbentuk (Koneksi Persisten Dua Arah)
Browser->>Backend: Kirim Data Frame (Klien ke Server)
Backend->>Browser: Kirim Data Frame (Server ke Klien)Proses di atas terjadi dalam langkah-langkah berikut:
- Browser klien mengirimkan permintaan HTTP GET ke Nginx dengan menyertakan dua header khusus:
Upgrade: websocketdanConnection: Upgrade. Langkah ini memberi tahu server bahwa klien ingin mengubah protokol komunikasi. - Nginx mendeteksi header tersebut, menerjemahkannya, dan meneruskannya ke server backend WebSocket (seperti server Node.js Socket.io atau server Go gorilla/websocket).
- Server backend memvalidasi kunci keamanan jabat tangan, lalu membalas dengan status respons HTTP
101 Switching Protocolsbeserta header konfirmasi Upgrade. - Nginx meneruskan respons HTTP 101 tersebut kembali ke browser klien.
- Koneksi TCP di tingkat bawah dipertahankan tetap terbuka di memori. Protokol komunikasi kini resmi berubah menjadi WebSocket persisten dua arah yang efisien.
Masalah Hardcoded Connection Header (Anti-Pattern) #
Untuk meneruskan jabat tangan WebSocket, Nginx membutuhkan tiga direktif proxy penting:
proxy_http_version 1.1; # WebSockets membutuhkan HTTP/1.1
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # Anti-pattern jika di-hardcode global!
Menyisipkan direktif proxy_set_header Connection "upgrade" secara langsung (hardcoded) di level global http atau server block adalah tindakan yang sangat tidak direkomendasikan.
Jika ada klien yang mengirimkan permintaan HTTP biasa (non-WebSocket) ke server kita, Nginx akan tetap menyisipkan header Connection: upgrade ke backend aplikasi kita. Beberapa server backend yang ketat akan bingung karena menerima header upgrade pada request biasa, lalu menolak memproses request tersebut atau mengembalikan galat HTTP 400 Bad Request.
Solusi: Pemetaan Connection Upgrade Secara Dinamis #
Praktik terbaik untuk menangani WebSocket di Nginx adalah dengan menggunakan modul map di dalam konteks http (di luar level server block). Kita mendefinisikan pemetaan dinamis untuk Connection header berdasarkan nilai Upgrade header yang dikirim oleh browser klien:
http {
# Jika klien mengirim header 'Upgrade: websocket',
# Nginx akan menyetel nilai variabel $connection_upgrade menjadi 'upgrade'.
# Jika klien tidak mengirim header Upgrade (HTTP biasa),
# Nginx akan menyetel nilai variabel $connection_upgrade menjadi 'close'.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name app.unisbadri.com;
location /socket.io/ {
proxy_pass http://websocket_backend;
# 1. Gunakan HTTP/1.1 wajib
proxy_http_version 1.1;
# 2. Teruskan Upgrade header asli klien
proxy_set_header Upgrade $http_upgrade;
# 3. Gunakan variabel dinamis hasil mapping di atas
proxy_set_header Connection $connection_upgrade;
# 4. Standard proxy headers
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;
}
}
}
Dengan pemetaan di atas, satu location block yang sama dapat melayani lalu lintas HTTP biasa sekaligus lalu lintas WebSocket secara cerdas dan aman tanpa memicu kesalahan interpretasi protokol di backend.
Menghindari Koneksi Terputus: Tuning Timeout WebSockets #
Tantangan operasional terbesar setelah WebSocket berhasil terhubung di Nginx adalah pemutusan koneksi secara tiba-tiba. Klien sering kali mendapati socket mereka terputus setiap 60 detik secara presisi saat halaman dalam kondisi idle (tidak ada transfer data aktif).
Hal ini dipengaruhi secara langsung oleh dua direktif timeout bawaan Nginx:
proxy_read_timeout(default 60s): Batas waktu Nginx menunggu backend mengirimkan data baru.proxy_send_timeout(default 60s): Batas waktu Nginx mengirimkan data ke backend.
Jika dalam 60 detik tidak ada aktivitas kirim-terima pesan antara klien dan server (misalnya pengguna sedang membaca artikel obrolan tanpa mengetik pesan), Nginx akan menganggap koneksi TCP tersebut telah mati atau gantung (stale), lalu menutup soket secara sepihak.
Solusi 1: Menaikkan Nilai Timeout di Nginx #
Kita bisa menaikkan nilai timeout di location block WebSocket menjadi sangat panjang (misalnya 24 jam atau 86.400 detik) agar Nginx membiarkan koneksi idle tetap terbuka:
location /ws/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Setel batas waktu timeout ke 24 jam
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
Solusi 2: Implementasi Ping-Pong (Heartbeat) di Sisi Aplikasi #
Menaikkan timeout hingga 24 jam memiliki efek samping buruk: server kita akan dipenuhi oleh koneksi socket “mati” (dead connections) yang sebenarnya sudah ditinggalkan oleh pengguna luar tetapi memorinya masih ditahan oleh Nginx karena belum melewati batas timeout.
Langkah terbaik adalah menurunkan timeout Nginx ke batas wajar (misalnya 120s atau 180s), lalu mengonfigurasi aplikasi backend kita (seperti Socket.io) untuk secara periodik mengirimkan paket data kecil penyelamat (heartbeat ping-pong frames) setiap 25 atau 50 detik. Paket data berkala ini mendeteksi koneksi yang benar-benar mati sekaligus memberi tahu Nginx bahwa koneksi tersebut masih aktif dan tidak boleh ditutup.
Skalabilitas WebSocket Upstream (Load Balancing) #
Jika server WebSocket kita melayani puluhan ribu koneksi simultan, satu instansi server backend di belakang Nginx tidak akan mampu menampung beban kerja tersebut. Kita harus membuat kluster server backend (upstream pool).
Namun, ada perbedaan mendasar dalam load balancing WebSocket dibandingkan HTTP biasa: WebSocket bersifat stateful.
Setelah koneksi terowongan TCP terbentuk di server A, seluruh pertukaran data berikutnya wajib terus dikirim ke server A. Jika Nginx memindahkan koneksi secara acak (seperti menggunakan Round-Robin standar) di tengah transaksi, koneksi socket akan langsung terputus dan rusak.
Kita mengatasi masalah persistensi koneksi ini menggunakan metode ip_hash pada blok upstream:
upstream websocket_servers {
# ip_hash menjamin klien dengan IP yang sama selalu diarahkan ke backend server yang sama
ip_hash;
server 10.0.3.5:8000;
server 10.0.3.6:8000;
server 10.0.3.7:8000;
# Pertahankan beberapa koneksi keepalive aktif ke backend untuk efisiensi transfer
keepalive 64;
}
server {
listen 443 ssl http2;
server_name chat.unisbadri.com;
location /socket.io/ {
proxy_pass http://websocket_servers;
proxy_http_version 1.1;
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;
# Setel timeout keepalive WebSocket
proxy_read_timeout 180s;
proxy_send_timeout 180s;
}
}
Contoh Konfigurasi Server Block Produksi Lengkap (WebSocket Secure - WSS) #
Berikut adalah berkas konfigurasi HTTPS tingkat produksi lengkap untuk mendeploy server WebSocket terenkripsi tepercaya (WSS - WebSocket Secure) yang aman, cepat, dan terproteksi dari kelangkaan resource:
# 1. Pemetaan Header Upgrade Dinamis di http context
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# 2. Definisikan upstream pool server backend WebSocket
upstream wss_production_pool {
ip_hash; # Gunakan persistensi IP Hash wajib untuk stateful WebSocket
server 127.0.0.1:4000 max_fails=3 fail_timeout=10s;
server 127.0.0.1:4001 max_fails=3 fail_timeout=10s;
server 127.0.0.1:4002 max_fails=3 fail_timeout=10s;
keepalive 32;
}
# Redireksi otomatis HTTP ke HTTPS
server {
listen 80;
server_name wss.unisbadri.com;
return 301 https://$host$request_uri;
}
# Server HTTPS Utama untuk WSS
server {
listen 443 ssl http2;
server_name wss.unisbadri.com;
# Pengaturan Sertifikat SSL Let's Encrypt
ssl_certificate /etc/letsencrypt/live/wss.unisbadri.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/wss.unisbadri.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Penyetelan optimasi buffer network
client_max_body_size 5m;
client_body_buffer_size 128k;
# Batasi koneksi simultan dari satu IP untuk mencegah spam socket attack
# (Membutuhkan limit_conn_zone yang sudah didefinisikan sebelumnya)
# limit_conn addr 50; # Maksimal 50 koneksi socket simultan per IP
# Rute utama untuk Proxying WebSocket
location / {
proxy_pass http://wss_production_pool;
# Konfigurasi protokol HTTP/1.1 & WebSocket handshake
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Meneruskan detail informasi identitas klien asli
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;
# Tuning Timeout untuk mencegah disconnect tiba-tiba saat idle
# Kita setel 3 menit (180s) dikombinasikan dengan heartbeat aplikasi 30s
proxy_read_timeout 180s;
proxy_send_timeout 180s;
# Optimasi transfer data streaming
proxy_buffering off; # Matikan buffering untuk lalu lintas real-time interaktif
proxy_cache_bypass $http_upgrade;
}
# Logging konfigurasi
access_log /var/log/nginx/wss-access.log;
error_log /var/log/nginx/wss-error.log warn;
}
Tuning Sistem Operasi & Nginx untuk Skalabilitas WebSocket (100k+ Koneksi) #
WebSocket bersifat persisten dan berumur panjang, yang berarti koneksi TCP antara klien-Nginx dan Nginx-backend akan tetap terbuka secara bersamaan dalam jangka waktu lama. Pada trafik tinggi, server kita dengan cepat dapat kehabisan kapasitas berkas (file descriptors) jika batasan default sistem operasi Linux dan Nginx tidak disesuaikan.
Untuk mendukung ratusan ribu koneksi WebSocket secara simultan, kita harus melakukan penalaan (tuning) pada konfigurasi sistem operasi Linux dan berkas Nginx kita:
1. Meningkatkan Batas File Descriptor di Linux (limits.conf)
#
Secara default, Linux membatasi jumlah file descriptor per user sebanyak 1024. Karena setiap koneksi TCP aktif direpresentasikan sebagai satu file descriptor di sistem operasi, server kita tidak akan bisa melayani lebih dari 1024 koneksi bersamaan sebelum memicu error “Too many open files”.
Edit berkas /etc/security/limits.conf untuk menaikkan batas bagi user nginx (atau user sistem yang menjalankan Nginx):
nginx soft nofile 100000
nginx hard nofile 100000
2. Penalaan Kernel Linux (sysctl.conf)
#
Kita juga perlu menyesuaikan setelan kernel di /etc/sysctl.conf agar sistem operasi mampu mengelola memori koneksi TCP secara efisien:
# Meningkatkan jumlah maksimal antrean koneksi TCP backlog
net.core.somaxconn = 65535
# Meningkatkan batas maksimum file descriptor sistem secara keseluruhan
fs.file-max = 2097152
# Memperluas jangkauan port lokal untuk outbound connection ke backend
net.ipv4.ip_local_port_range = 1024 65535
# Mengaktifkan penggunaan kembali (reuse) socket TIME_WAIT untuk koneksi cepat
net.ipv4.tcp_tw_reuse = 1
Jalankan perintah sudo sysctl -p untuk langsung menerapkan perubahan parameter kernel di atas tanpa perlu melakukan reboot server.
3. Konfigurasi Penyesuaian di nginx.conf
#
Di dalam berkas konfigurasi utama nginx.conf, kita wajib meningkatkan kapasitas worker connections agar sejalan dengan limit file descriptor di sistem operasi:
# Konfigurasi utama di nginx.conf (global context)
worker_processes auto;
worker_rlimit_nofile 100000; # Harus selaras dengan limits.conf
events {
worker_connections 50000; # Jumlah maksimal koneksi per worker process
use epoll; # Metode I/O multiplexing berkinerja tinggi untuk Linux
multi_accept on; # Menerima semua koneksi baru sekaligus jika memungkinkan
}
Dengan penyesuaian di tiga lapisan tersebut (OS Limits, Kernel parameter, dan Nginx event model), server kita siap menangani puluhan ribu koneksi WebSocket aktif secara simultan tanpa mengalami kendala overload soket atau kebocoran memori.
Ringkasan dan Praktik Terbaik #
- Gunakan map connection_upgrade: Hindari menulis
Connection "upgrade"secara permanen di tingkat global. Gunakan blok pemetaanmapuntuk mendeteksi tipe permintaan klien secara fleksibel.- Matikan proxy_buffering: Untuk lalu lintas real-time WebSocket, selalu setel
proxy_buffering off;agar Nginx tidak menahan frame data di memori buffer dan langsung meneruskannya ke klien seketika.- Terapkan ip_hash untuk Upstream Stateful: WebSocket bersifat persisten pada satu server fisik. Gunakan metode load balancing
ip_hashagar koneksi socket klien tidak rusak akibat berpindah-pindah instansi server di backend.- Padukan Timeout Nginx dengan Heartbeat Aplikasi: Menyetel timeout Nginx terlalu lama memboroskan memori server. Gunakan batas wajar (misal 180 detik) dikombinasikan dengan pengiriman data heartbeat di sisi kode aplikasi klien-server kita.
← Sebelumnya: Single Page Application (React/Vue) Berikutnya: Error Umum →