7.1 KiB
7.1 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-websocket-production | WebSocket Production — auth / scale / pub/sub | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
WebSocket Production
Demo WS = simple. Production = auth / scale / pub-sub / heartbeat / reconnect. 1 server 가 ~10k connection.
📖 핵심 개념
- WebSocket = persistent connection.
- Sticky session 필요 (load balancer).
- Pub/sub 가 multi-instance.
- Heartbeat = idle timeout 방지.
💻 코드 패턴
Basic (Node + ws)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
ws.on('message', (data) => {
ws.send('echo: ' + data);
});
});
Auth (JWT)
wss.on('connection', (ws, req) => {
const url = new URL(req.url!, `http://${req.headers.host}`);
const token = url.searchParams.get('token');
try {
const payload = jwt.verify(token!, secret);
ws.userId = payload.sub;
} catch {
ws.close(1008, 'unauthorized');
return;
}
// ... handle
});
→ Cookie / header / query param.
Sticky session (LB)
upstream ws {
ip_hash;
server ws1:8080;
server ws2:8080;
}
server {
location /ws {
proxy_pass http://ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
→ 같은 client = 같은 server.
Pub/sub (multi-instance)
import Redis from 'ioredis';
const sub = new Redis();
const pub = new Redis();
// 매 server 가 subscribe
sub.subscribe('chat:room:1');
sub.on('message', (channel, msg) => {
// 모든 connected client 에 broadcast
wss.clients.forEach(c => c.send(msg));
});
// Send (다른 server 가 broadcast)
pub.publish('chat:room:1', JSON.stringify({ text: 'hi' }));
→ Cross-instance broadcast.
Heartbeat / ping
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
const interval = setInterval(() => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
}, 30000);
ws.on('close', () => clearInterval(interval));
});
→ 30s 마다 ping. Pong 안 = terminate.
Client reconnect
class WSClient {
private ws?: WebSocket;
private retries = 0;
connect() {
this.ws = new WebSocket(url);
this.ws.onopen = () => { this.retries = 0; };
this.ws.onclose = () => this.reconnect();
this.ws.onerror = () => this.ws?.close();
}
reconnect() {
const delay = Math.min(30000, 1000 * Math.pow(2, this.retries++));
setTimeout(() => this.connect(), delay);
}
}
→ Exponential backoff.
Subscribe / room
const rooms = new Map<string, Set<WebSocket>>();
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'join') {
if (!rooms.has(msg.room)) rooms.set(msg.room, new Set());
rooms.get(msg.room)!.add(ws);
} else if (msg.type === 'leave') {
rooms.get(msg.room)?.delete(ws);
} else if (msg.type === 'broadcast') {
rooms.get(msg.room)?.forEach(c => c.send(JSON.stringify(msg)));
}
});
Backpressure
ws.send(data, (err) => {
if (err) console.error(err);
});
// 또는 bufferedAmount check
if (ws.bufferedAmount > 1_000_000) {
// Slow client — drop / skip.
}
Compression
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: true,
});
→ Bandwidth ↓. CPU ↑.
Socket.io (high-level)
import { Server } from 'socket.io';
const io = new Server(server, {
cors: { origin: '*' },
adapter: createAdapter(redis, pub),
});
io.on('connection', (socket) => {
socket.join('room1');
socket.to('room1').emit('hello');
});
→ Built-in: rooms, fallback, reconnect, pub/sub.
Pusher / Ably (managed)
import Pusher from 'pusher-js';
const pusher = new Pusher(key, { cluster: 'mt1' });
const channel = pusher.subscribe('chat-1');
channel.bind('message', (data) => console.log(data));
→ Self-host 안 하고 싶으면.
Cloudflare Durable Objects
// 1 chat room = 1 DO instance.
// Stateful + global.
Capacity per instance
Node + ws: ~10k connection (4 GB RAM).
uWebSockets.js: ~100k+ (C++ binding).
Go gorilla: ~100k.
Erlang / Elixir Phoenix: 매우 크.
→ 큰 = sticky LB + N instance.
Phoenix Channels (Elixir)
defmodule MyApp.RoomChannel do
use Phoenix.Channel
def join('room:lobby', _, socket) do
{:ok, socket}
end
end
→ 2M concurrent (single server). Best-in-class.
Authorization (per message)
ws.on('message', async (data) => {
const msg = JSON.parse(data);
// Check permission per action
if (msg.type === 'admin-action' && !ws.user.isAdmin) {
ws.send(JSON.stringify({ error: 'forbidden' }));
return;
}
// ... handle
});
→ Connection auth 가 끝 X. Per-action 도.
Rate limit (per connection)
const limits = new Map<WebSocket, { count: number; reset: number }>();
ws.on('message', () => {
const now = Date.now();
const limit = limits.get(ws) ?? { count: 0, reset: now + 60000 };
if (now > limit.reset) {
limit.count = 0;
limit.reset = now + 60000;
}
limit.count++;
if (limit.count > 100) {
ws.close(1008, 'rate limit');
return;
}
});
Monitor
- Concurrent connection.
- Messages per sec.
- Disconnect rate.
- Heartbeat fail rate.
- Memory / CPU.
→ Datadog / Prometheus.
Graceful shutdown
process.on('SIGTERM', async () => {
// Notify all
wss.clients.forEach(ws => ws.send(JSON.stringify({ type: 'reconnect-soon' })));
// Wait
await new Promise(r => setTimeout(r, 5000));
// Close
wss.close();
});
→ Client 가 다른 instance 로 reconnect.
vs SSE
WebSocket: bidirectional.
SSE: server → client only.
→ Chat = WS.
Live update = SSE 또는 WS.
vs HTTP/3 streams
HTTP/3 (WebTransport): UDP + multiple stream.
WebSocket: TCP single.
→ Future = WebTransport.
Now = WebSocket.
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 작은 chat | ws (Node) |
| Production scale | Socket.io + Redis adapter |
| Managed | Pusher / Ably |
| Edge | Cloudflare DO |
| 매우 큰 | Phoenix / uWebSockets |
| Streaming server → client | SSE |
❌ 안티패턴
- No sticky LB: round-robin 가 connection 깨짐.
- No pub/sub: cross-instance broadcast X.
- No heartbeat: idle timeout.
- No rate limit: spam.
- Connection auth 만: per-action 도.
- No reconnect: bad UX.
- Compression always: CPU.
🤖 LLM 활용 힌트
- Sticky LB + Redis pub/sub 가 multi-instance.
- Heartbeat 30s.
- Client reconnect (exponential).
- Phoenix / uWebSockets 가 큰 scale.