10. Масштабирование real-time
Один сервер — тысячи соединений. Несколько серверов — нужна синхронизация. Разберём как это работает.
Проблема горизонтального масштабирования
Заголовок раздела «Проблема горизонтального масштабирования»БЕЗ масштабирования (один сервер): Клиент A → Сервер 1 Клиент B → Сервер 1 A пишет → Сервер 1 знает про B → B получает ✅
С масштабированием (несколько серверов): Клиент A → Сервер 1 Клиент B → Сервер 2 ← Другой сервер! A пишет → Сервер 1 не знает про B на Сервере 2 → B НЕ получает ❌Решение: Общая шина сообщений между серверами.
Redis Pub/Sub адаптер для Socket.io
Заголовок раздела «Redis Pub/Sub адаптер для Socket.io»npm install @socket.io/redis-adapter ioredisconst { 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 получает ✅Nginx балансировка нагрузки
Заголовок раздела «Nginx балансировка нагрузки»WebSocket требует sticky sessions (один клиент → один бекенд):
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;}Redis для SSE
Заголовок раздела «Redis для SSE»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.jsconst server = httpServer.listen(3000);server.maxConnections = 50000;
// Следить за количеством соединенийsetInterval(() => { console.log('Соединений:', io.sockets.sockets.size); console.log('Память:', process.memoryUsage().heapUsed / 1024 / 1024, 'MB');}, 60000);Горизонтальное масштабирование с Kubernetes
Заголовок раздела «Горизонтальное масштабирование с Kubernetes»apiVersion: apps/v1kind: Deploymentmetadata: name: websocket-serverspec: 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 через sessionAffinityapiVersion: v1kind: Servicemetadata: name: websocket-servicespec: sessionAffinity: ClientIP # ← sticky sessions sessionAffinityConfig: clientIP: timeoutSeconds: 3600 selector: app: websocket ports: - port: 80 targetPort: 3000Мониторинг real-time системы
Заголовок раздела «Мониторинг real-time системы»// Метрики для мониторинга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 endpointapp.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 connectionsws_connections_total ${metrics.connections}
# HELP ws_rooms_total Active roomsws_rooms_total ${metrics.rooms}
# HELP ws_errors_total WebSocket errorsws_errors_total ${metrics.errors} `);});
// Grafana Dashboard: мониторинг в реальном времениТестирование нагрузки
Заголовок раздела «Тестирование нагрузки»# artillery для WebSocket нагрузкиnpm install -g artillery artillery-engine-socketio
# load-test.ymlconfig: 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Задания
Заголовок раздела «Задания»- Запусти два экземпляра Socket.io сервера с Redis адаптером. Убедись, что сообщения между серверами работают
- Настрой Nginx со sticky sessions для WebSocket
- Напиши нагрузочный тест и найди максимальное число соединений на свою машину
- Добавь Prometheus метрики к своему Socket.io серверу
Масштабирование real-time = Redis Pub/Sub (синхронизация серверов) + sticky sessions (балансировщик). Socket.io Redis адаптер делает это прозрачно. Следующий урок — WebRTC для прямой связи между клиентами.