bookworm-smart-assistant/skills/websocket-engineer/references/scaling.md

334 lines
7.7 KiB
Markdown
Raw Permalink Normal View History

# Horizontal Scaling Reference
## Architecture Overview
```
┌─────────────┐
│Load Balancer│ (nginx/HAProxy with sticky sessions)
└──────┬──────┘
┌───┴───┐
│ │
┌──▼──┐ ┌──▼──┐
│WS #1│ │WS #2│ ... (Socket.IO servers)
└──┬──┘ └──┬──┘
│ │
└───┬───┘
┌───▼───┐
│ Redis │ (Pub/Sub adapter)
└───────┘
```
## Redis Adapter Configuration
### Socket.IO with Redis
```javascript
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const httpServer = createServer();
const io = new Server(httpServer, {
cors: { origin: '*' }
});
// Redis pub/sub client setup
const pubClient = createClient({
host: 'localhost',
port: 6379
});
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis adapter connected');
});
// Now broadcasts work across all servers
io.emit('news', { hello: 'world' });
httpServer.listen(3000);
```
### Redis Streams for Reliable Delivery
```javascript
const { createAdapter } = require('@socket.io/redis-streams-adapter');
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect().then(() => {
io.adapter(createAdapter(redisClient, {
streamName: 'socket.io-stream',
maxLen: 10000, // Keep last 10k messages
readCount: 100 // Process 100 messages at a time
}));
});
```
## Sticky Sessions
### Nginx Configuration
```nginx
upstream websocket_backend {
ip_hash; # Sticky sessions based on IP
server ws1.example.com:3000;
server ws2.example.com:3000;
server ws3.example.com:3000;
}
server {
listen 80;
server_name example.com;
location /socket.io/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header 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;
# Timeouts
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
}
```
### HAProxy Configuration
```haproxyconf
frontend websocket_frontend
bind *:80
mode http
option httplog
use_backend websocket_backend
backend websocket_backend
mode http
balance source # Sticky sessions by source IP
hash-type consistent # Consistent hashing
# Health checks
option httpchk GET /health
http-check expect status 200
server ws1 10.0.1.1:3000 check
server ws2 10.0.1.2:3000 check
server ws3 10.0.1.3:3000 check
```
### Cookie-based Sticky Sessions
```javascript
// Server-side: Set affinity cookie
io.engine.on('connection', (rawSocket) => {
const serverID = process.env.SERVER_ID || 'server1';
rawSocket.request.res.setHeader(
'Set-Cookie',
`io=${serverID}; Path=/; HttpOnly; SameSite=Lax`
);
});
```
```nginx
# Nginx: Use cookie for routing
upstream websocket_backend {
server ws1.example.com:3000;
server ws2.example.com:3000;
}
map $cookie_io $backend_server {
"server1" ws1.example.com:3000;
"server2" ws2.example.com:3000;
default websocket_backend;
}
location /socket.io/ {
proxy_pass http://$backend_server;
# ... other proxy settings
}
```
## State Management
### Shared State in Redis
```javascript
const Redis = require('ioredis');
const redis = new Redis();
// Store user connection info
io.on('connection', async (socket) => {
const userId = socket.handshake.auth.userId;
// Track which server has this user
await redis.hset('user:connections', userId, process.env.SERVER_ID);
// Store user presence
await redis.hset(`user:${userId}`, {
socketId: socket.id,
serverId: process.env.SERVER_ID,
connectedAt: Date.now(),
status: 'online'
});
socket.on('disconnect', async () => {
await redis.hdel('user:connections', userId);
await redis.del(`user:${userId}`);
});
});
// Send message to specific user across cluster
async function sendToUser(userId, event, data) {
const serverId = await redis.hget('user:connections', userId);
if (serverId === process.env.SERVER_ID) {
// User is on this server
const sockets = await io.in(`user:${userId}`).fetchSockets();
sockets.forEach(socket => socket.emit(event, data));
} else {
// User is on another server - use Redis to route
io.to(`user:${userId}`).emit(event, data);
}
}
```
## Connection Limits
### Per-Server Limits
```javascript
const MAX_CONNECTIONS = 50000;
io.engine.on('connection', (socket) => {
const currentConnections = io.engine.clientsCount;
if (currentConnections > MAX_CONNECTIONS) {
socket.close(1008, 'Server at capacity');
return;
}
});
```
### Kubernetes Horizontal Pod Autoscaling
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: websocket-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: websocket-server
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: websocket_connections
target:
type: AverageValue
averageValue: "40000" # Scale when avg > 40k connections/pod
```
## Graceful Shutdown
```javascript
const gracefulShutdown = () => {
console.log('Shutting down gracefully...');
// Stop accepting new connections
io.close(() => {
console.log('All connections closed');
process.exit(0);
});
// Force close after 30 seconds
setTimeout(() => {
console.error('Forcing shutdown after timeout');
process.exit(1);
}, 30000);
};
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
```
## Performance Optimization
### Node.js Clustering
```javascript
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numWorkers = os.cpus().length;
console.log(`Master ${process.pid} starting ${numWorkers} workers`);
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died, spawning new`);
cluster.fork();
});
} else {
// Worker process runs Socket.IO server
const io = require('./socket-server');
io.listen(3000);
console.log(`Worker ${process.pid} started`);
}
```
### uWebSockets.js for Maximum Performance
```javascript
const uWS = require('uWebSockets.js');
const app = uWS.App()
.ws('/*', {
compression: uWS.SHARED_COMPRESSOR,
maxPayloadLength: 16 * 1024,
idleTimeout: 60,
open: (ws) => {
console.log('Client connected');
},
message: (ws, message, isBinary) => {
// Echo message
ws.send(message, isBinary);
},
close: (ws, code, message) => {
console.log('Client disconnected');
}
})
.listen(9001, (token) => {
if (token) {
console.log('Listening on port 9001');
}
});
```