[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-10 22:08:15 +09:00
parent 21ac3ed255
commit 504fd5fb42
3011 changed files with 380280 additions and 206977 deletions
@@ -0,0 +1,350 @@
---
id: backend-websocket-production
title: WebSocket Production — auth / scale / pub/sub
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [backend, websocket, vibe-coding]
tech_stack: { language: "TS", applicable_to: ["Backend"] }
applied_in: []
aliases: [WebSocket production, WebSocket auth, sticky session, Redis pub/sub, Socket.io, Pusher, Ably]
---
# 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)
```ts
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)
```ts
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)
```nginx
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)
```ts
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
```ts
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
```ts
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
```ts
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
```ts
ws.send(data, (err) => {
if (err) console.error(err);
});
// 또는 bufferedAmount check
if (ws.bufferedAmount > 1_000_000) {
// Slow client — drop / skip.
}
```
### Compression
```ts
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: true,
});
```
→ Bandwidth ↓. CPU ↑.
### Socket.io (high-level)
```ts
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)
```ts
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
```ts
// 1 chat room = 1 DO instance.
// Stateful + global.
```
→ [[Backend_Edge_Runtime_Deep]].
### 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)
```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)
```ts
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)
```ts
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
```ts
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.
```
→ [[Web_WebTransport_HID_USB]].
## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 작은 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.
## 🔗 관련 문서
- [[Backend_WebSocket_Scaling]]
- [[Web_WebSocket_Reconnect]]
- [[Backend_NATS_JetStream]]