Перейти к содержимому

10. Масштабирование real-time

Один сервер — тысячи соединений. Несколько серверов — нужна синхронизация. Разберём как это работает.

Проблема горизонтального масштабирования

Заголовок раздела «Проблема горизонтального масштабирования»
БЕЗ масштабирования (один сервер):
Клиент A → Сервер 1
Клиент B → Сервер 1
A пишет → Сервер 1 знает про B → B получает ✅
С масштабированием (несколько серверов):
Клиент A → Сервер 1
Клиент B → Сервер 2 ← Другой сервер!
A пишет → Сервер 1 не знает про B на Сервере 2 → B НЕ получает ❌

Решение: Общая шина сообщений между серверами.

Окно терминала
npm install @socket.io/redis-adapter ioredis
server.js
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
async function main() {
const httpServer = createServer();
const io = new Server(httpServer);
// Два клиента: publisher и subscriber
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
// Подключить адаптер
io.adapter(createAdapter(pubClient, subClient));
io.on('connection', (socket) => {
socket.on('send-message', ({ roomId, text }) => {
// Теперь это работает на ВСЕХ серверах!
io.to(roomId).emit('new-message', { text });
});
});
const port = process.env.PORT || 3000;
httpServer.listen(port, () => {
console.log(`Сервер ${port} запущен`);
});
}
main();
Сервер 1 (порт 3001) Redis Сервер 2 (порт 3002)
│ │ │
Клиент A │ Клиент B
│ │ │
A → "hello" ──────────→ │ PUBLISH │
│ ─────────────→ io.to('room').emit
│ → Клиент B получает ✅

WebSocket требует sticky sessions (один клиент → один бекенд):

nginx.conf
upstream socket_servers {
ip_hash; # ← sticky sessions по IP!
server backend1:3001;
server backend2:3002;
server backend3:3003;
}
server {
listen 80;
location /socket.io/ {
proxy_pass http://socket_servers;
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_read_timeout 86400s; # Долгие соединения
proxy_send_timeout 86400s;
}
location /api/ {
proxy_pass http://socket_servers;
# Без sticky sessions для обычного HTTP
}
}

Или использовать cookie для sticky sessions (надёжнее IP):

upstream socket_servers {
hash $cookie_serverid consistent;
server backend1:3001;
server backend2:3002;
}
const redis = require('redis');
const sub = redis.createClient({ url: process.env.REDIS_URL });
const pub = redis.createClient({ url: process.env.REDIS_URL });
await Promise.all([sub.connect(), pub.connect()]);
const clients = new Map(); // clientId → res
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.flushHeaders();
const userId = req.user.id;
const clientId = `${userId}-${Date.now()}`;
clients.set(clientId, { res, userId });
req.on('close', () => clients.delete(clientId));
});
// Подписаться на Redis канал (получать события от других серверов)
await sub.subscribe('sse-events', (message) => {
const { targetUserId, event, data } = JSON.parse(message);
// Найти клиентов этого пользователя на ЭТОМ сервере
clients.forEach(({ res, userId }) => {
if (userId === targetUserId) {
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
}
});
});
// Отправить событие пользователю (из любого сервера)
async function sendToUser(userId, event, data) {
await pub.publish('sse-events', JSON.stringify({ targetUserId: userId, event, data }));
}
// Сколько соединений может держать Node.js?
// По умолчанию: ~65,535 (системный лимит)
// Реально: 10,000-100,000 (зависит от памяти и CPU)
// Настройка системных лимитов (Linux)
// /etc/sysctl.conf:
// net.core.somaxconn = 65535
// net.ipv4.tcp_max_syn_backlog = 65535
// Настройка Node.js
const server = httpServer.listen(3000);
server.maxConnections = 50000;
// Следить за количеством соединений
setInterval(() => {
console.log('Соединений:', io.sockets.sockets.size);
console.log('Память:', process.memoryUsage().heapUsed / 1024 / 1024, 'MB');
}, 60000);
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: websocket-server
spec:
replicas: 3 # 3 сервера
selector:
matchLabels:
app: websocket
template:
spec:
containers:
- name: server
image: myapp:latest
env:
- name: REDIS_URL
value: "redis://redis-service:6379"
ports:
- containerPort: 3000
---
# service.yaml — sticky sessions через sessionAffinity
apiVersion: v1
kind: Service
metadata:
name: websocket-service
spec:
sessionAffinity: ClientIP # ← sticky sessions
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600
selector:
app: websocket
ports:
- port: 80
targetPort: 3000
// Метрики для мониторинга
const metrics = {
connections: 0,
messagesPerSecond: 0,
errors: 0,
rooms: 0,
};
io.on('connection', (socket) => {
metrics.connections++;
socket.on('disconnect', () => metrics.connections--);
socket.on('error', () => metrics.errors++);
});
// Prometheus endpoint
app.get('/metrics', (req, res) => {
const rooms = io.sockets.adapter.rooms;
metrics.rooms = rooms.size;
res.set('Content-Type', 'text/plain');
res.send(`
# HELP ws_connections_total Active WebSocket connections
ws_connections_total ${metrics.connections}
# HELP ws_rooms_total Active rooms
ws_rooms_total ${metrics.rooms}
# HELP ws_errors_total WebSocket errors
ws_errors_total ${metrics.errors}
`);
});
// Grafana Dashboard: мониторинг в реальном времени
Окно терминала
# artillery для WebSocket нагрузки
npm install -g artillery artillery-engine-socketio
# load-test.yml
config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 10 # 10 новых соединений в секунду
engines:
socketio: {}
scenarios:
- engine: socketio
flow:
- emit:
channel: "join-room"
data: { roomId: "room-1" }
- think: 5
- emit:
channel: "send-message"
data: { text: "Test message" }
- think: 55
# Запуск
artillery run load-test.yml
  1. Запусти два экземпляра Socket.io сервера с Redis адаптером. Убедись, что сообщения между серверами работают
  2. Настрой Nginx со sticky sessions для WebSocket
  3. Напиши нагрузочный тест и найди максимальное число соединений на свою машину
  4. Добавь Prometheus метрики к своему Socket.io серверу

Масштабирование real-time = Redis Pub/Sub (синхронизация серверов) + sticky sessions (балансировщик). Socket.io Redis адаптер делает это прозрачно. Следующий урок — WebRTC для прямой связи между клиентами.