Node.js и Балансировщик Нагрузки: Полное Руководство для Новичков
-
Представьте ситуацию: вы запустили Node.js приложение на боевом сервере, оно работает хорошо при 100 пользователях в день. Но вот приложение набирает популярность, и уже 10,000 пользователей одновременно пытаются получить доступ. Единственный экземпляр вашего приложения начинает медленнее отвечать, потом вообще падает. Запросы теряются, пользователи уходят. Вот здесь и приходит на помощь балансировщик нагрузки.
Балансировщик нагрузки — это промежуточный слой между вашими пользователями и приложением, который распределяет входящие запросы между несколькими копиями вашего приложения, работающими одновременно.
Часть 1: Основные Концепции
Что такое горизонтальное масштабирование?
Есть два способа справиться с растущей нагрузкой:
Вертикальное масштабирование (upgrade сервера)
- Вы просто покупаете более мощный сервер (больше RAM, быстрее CPU)
- Простой способ, но дорогой и имеет физический предел
- Если сервер падает — всё падает
Горизонтальное масштабирование (добавить серверы)
- Вы запускаете копии приложения на разных машинах или портах
- Балансировщик распределяет запросы между ними
- Дешевле в долгосроке, нет единой точки отказа, можно масштабировать бесконечно
Вывод: для highload приложений используйте горизонтальное масштабирование с балансировщиком нагрузки.
Зачем нужен балансировщик нагрузки?
- Улучшение производительности — запросы распределяются, ни один сервер не перегружается
- Высокая доступность — если один сервер упадет, остальные продолжат работать
- Масштабируемость — просто добавляете новые серверы при растущей нагрузке
- Равномерная нагрузка — алгоритмы балансировки гарантируют справедливое распределение
Часть 2: Алгоритмы Балансировки Нагрузки
Балансировщик должен знать, на какой сервер отправить запрос. Это делают алгоритмы:
1. Round Robin (Круговой Метод)
Как работает: Балансировщик отправляет запросы по очереди: первый на сервер 1, второй на сервер 2, третий на сервер 3, потом снова на сервер 1.
Запрос 1 → Сервер 1 Запрос 2 → Сервер 2 Запрос 3 → Сервер 3 Запрос 4 → Сервер 1 (цикл повторяется)Когда использовать: Когда все серверы одинаковой мощности и все запросы примерно одинакового времени обработки.
Преимущества: Простой, быстрый, справедливый.
Недостатки: Если один запрос длится 10 секунд, а другой 1 секунду, сервер с длинным запросом будет перегружен.2. Least Connections (Минимум Соединений)
Как работает: Балансировщик следит, сколько активных соединений на каждом сервере, и отправляет новый запрос на сервер с наименьшим количеством активных соединений.
Сервер 1: 5 активных запросов Сервер 2: 2 активных запроса ← новый запрос пойдет сюда Сервер 3: 8 активных запросовКогда использовать: Когда запросы имеют разное время обработки, или когда есть долгоживущие соединения (например, WebSocket).
Преимущества: Более справедливое распределение при нестабильной нагрузке.
Недостатки: Требует отслеживания состояния, немного медленнее.3. IP Hash (Хеширование IP)
Как работает: Балансировщик берет IP-адрес клиента, применяет хеш-функцию и на основе результата всегда отправляет этого клиента на одинаковый сервер.
Клиент с IP 192.168.1.100 → всегда на Сервер 2 Клиент с IP 192.168.1.101 → всегда на Сервер 1Когда использовать: Когда нужны sticky sessions — когда данные сессии хранятся в памяти сервера, и клиент должен всегда идти на один и тот же сервер.
Преимущества: Гарантирует, что один пользователь всегда на одном сервере.
Недостатки: Может привести к неравномерной нагрузке, если одни IP адреса требуют больше ресурсов.
Часть 3: Решение 1 — Балансировка через Nginx
Nginx — это надежный, быстрый web-сервер, часто используемый как балансировщик нагрузки.
Как это работает
Интернет ↓ Nginx (порт 80) ↓ (распределяет) ┌─────────────────┬─────────────────┬─────────────────┐ │ Node App │ Node App │ Node App │ │ (порт 3000) │ (порт 3001) │ (порт 3002) │ └─────────────────┴─────────────────┴─────────────────┘Практический пример
Шаг 1: Запустите несколько копий вашего приложения
Создайте простой Node.js app (
server.js
const express = require('express'); const app = express(); // Получаем порт из переменной окружения или используем 3000 const PORT = process.env.PORT || 3000; app.get('/', (req, res) => { res.json({ message: 'Hello from Node.js', port: PORT, timestamp: new Date().toISOString() }); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });Запустите три копии с разными портами:
# Терминал 1 PORT=3000 node server.js # Терминал 2 PORT=3001 node server.js # Терминал 3 PORT=3002 node server.jsШаг 2: Настройте Nginx
Отредактируйте
/etc/nginx/nginx.confили создайте файл в/etc/nginx/sites-available/myapp:# Определяем группу upstream-серверов upstream nodejs_servers { # Round Robin по умолчанию server localhost:3000; server localhost:3001; server localhost:3002; } # HTTP сервер (балансировщик) server { listen 80; server_name localhost; # Все запросы проксируем на upstream группу location / { proxy_pass http://nodejs_servers; # Важные заголовки для проксирования 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; } }Шаг 3: Проверьте конфигурацию и перезагрузите Nginx
# Проверка синтаксиса sudo nginx -t # Если всё OK: sudo systemctl reload nginxШаг 4: Тестируйте
Откройте браузер и несколько раз зайдите на
http://localhost/. Обратите внимание на полеportв ответе — оно меняется между 3000, 3001 и 3002, подтверждая работу балансировки.Продвинутые параметры Nginx
Least Connections вместо Round Robin:
upstream nodejs_servers { least_conn; # Используем алгоритм least connections server localhost:3000; server localhost:3001; server localhost:3002; }IP Hash для sticky sessions:
upstream nodejs_servers { ip_hash; # Один IP — один сервер server localhost:3000; server localhost:3001; server localhost:3002; }Взвешенное распределение (если серверы разной мощности):
upstream nodejs_servers { server localhost:3000 weight=3; # Получает в 3 раза больше запросов server localhost:3001 weight=1; server localhost:3002 weight=1; }Health checks (только в Nginx Plus, но полезно знать):
upstream nodejs_servers { server localhost:3000; server localhost:3001; server localhost:3002; # Проверка здоровья: если сервер не ответит за 3 сек — исключить check interval=3000 rise=2 fall=5 timeout=1000 type=http; check_http_send "GET / HTTP/1.0\r\n\r\n"; check_http_expect_alive http_2xx; }
Часть 4: Решение 2 — Собственный Балансировщик на Node.js
Если вам нужна большая гибкость, можно написать балансировщик на самом Node.js используя Express и
http-proxy-middleware.Как это работает
Интернет ↓ Node.js Балансировщик (порт 8080) ↓ (проксирует запросы) ┌─────────────────┬─────────────────┬─────────────────┐ │ Node App │ Node App │ Node App │ │ (порт 3000) │ (порт 3001) │ (порт 3002) │ └─────────────────┴─────────────────┴─────────────────┘Пример 1: Простой балансировщик с Round Robin
Установите зависимости:
npm install express axiosСоздайте файл
load-balancer.js:const express = require('express'); const axios = require('axios'); const app = express(); // Список backend серверов const servers = [ 'http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002' ]; // Счетчик для round-robin let currentServer = 0; // Обработчик всех запросов app.use(async (req, res) => { try { // Выбираем сервер по очереди (round-robin) const server = servers[currentServer]; currentServer = (currentServer + 1) % servers.length; // Формируем URL для backend сервера const backendUrl = `${server}${req.originalUrl}`; // Делаем запрос к backend серверу const response = await axios({ method: req.method, url: backendUrl, data: req.body, headers: { ...req.headers, 'X-Forwarded-For': req.ip, 'X-Real-IP': req.ip, } }); // Возвращаем ответ клиенту res.status(response.status).send(response.data); } catch (error) { console.error('Error proxying request:', error.message); res.status(503).json({ error: 'Service unavailable' }); } }); app.listen(8080, () => { console.log('Load balancer running on port 8080'); });Запустите:
node load-balancer.jsТеперь обращайтесь к
http://localhost:8080вместо отдельных серверов.Пример 2: Балансировщик с Least Connections
const express = require('express'); const axios = require('axios'); const app = express(); // Backend серверы с отслеживанием соединений const servers = [ { url: 'http://localhost:3000', activeConnections: 0 }, { url: 'http://localhost:3001', activeConnections: 0 }, { url: 'http://localhost:3002', activeConnections: 0 } ]; // Выбираем сервер с наименьшим количеством активных соединений function selectServer() { return servers.reduce((prev, current) => prev.activeConnections < current.activeConnections ? prev : current ); } app.use(async (req, res) => { const selectedServer = selectServer(); selectedServer.activeConnections++; try { const backendUrl = `${selectedServer.url}${req.originalUrl}`; const response = await axios({ method: req.method, url: backendUrl, data: req.body, headers: { ...req.headers, 'X-Forwarded-For': req.ip, } }); res.status(response.status).send(response.data); } catch (error) { console.error('Error proxying request:', error.message); res.status(503).json({ error: 'Service unavailable' }); } finally { // Уменьшаем счетчик соединений selectedServer.activeConnections--; } }); app.listen(8080, () => { console.log('Load balancer (least connections) running on port 8080'); });Пример 3: Балансировщик с Sticky Sessions
Когда нужно, чтобы один пользователь всегда ходил на один сервер:
const express = require('express'); const axios = require('axios'); const crypto = require('crypto'); const app = express(); const servers = [ 'http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002' ]; // Хранилище сессий: IP адрес -> индекс сервера const sessions = new Map(); function selectServer(clientIP) { if (sessions.has(clientIP)) { // Клиент уже был ранее return sessions.get(clientIP); } else { // Новый клиент — распределяем по IP hash const hash = crypto .createHash('md5') .update(clientIP) .digest('hex'); const serverIndex = parseInt(hash, 16) % servers.length; sessions.set(clientIP, serverIndex); return serverIndex; } } app.use(async (req, res) => { const clientIP = req.ip; const serverIndex = selectServer(clientIP); const selectedServer = servers[serverIndex]; try { const backendUrl = `${selectedServer}${req.originalUrl}`; const response = await axios({ method: req.method, url: backendUrl, data: req.body, headers: { ...req.headers, 'X-Forwarded-For': clientIP, } }); res.status(response.status).send(response.data); } catch (error) { console.error('Error proxying request:', error.message); res.status(503).json({ error: 'Service unavailable' }); } }); app.listen(8080, () => { console.log('Load balancer (sticky sessions) running on port 8080'); });
Часть 5: Решение 3 — PM2 для Управления Кластером
PM2 — это процесс-менеджер для Node.js, который встроил управление кластером и балансировкой нагрузки.
Как это работает
PM2 автоматически создает несколько копий вашего приложения (воркеры) и распределяет между ними запросы используя встроенный балансировщик.
Практический пример
Установите PM2:
npm install -g pm2Создайте конфиг файл
ecosystem.config.js:module.exports = { apps: [ { name: 'nodejs-app', // Имя приложения script: './server.js', // Файл приложения instances: 4, // Количество копий (или 'max' для всех CPU ядер) exec_mode: 'cluster', // Режим кластера watch: false, // Перезагружать при изменении файлов (false для production) max_memory_restart: '500M', // Перезагружать если память > 500MB env: { NODE_ENV: 'development' }, env_production: { NODE_ENV: 'production' }, // Graceful shutdown timeout listen_timeout: 3000, // Автоматический перезапуск упавшего процесса autorestart: true, // Минимальное время жизни процесса перед перезапуском min_uptime: '10s', // Максимальное количество перезапусков за час max_restarts: 10 } ] };Запустите приложение:
pm2 start ecosystem.config.jsПолезные команды:
# Просмотр статуса всех процессов pm2 status # Просмотр логов pm2 logs nodejs-app # Перезагрузка с нулевым downtime pm2 reload ecosystem.config.js # Остановка pm2 stop ecosystem.config.js # Удаление pm2 delete ecosystem.config.js # Автозагрузка при перезагрузке сервера pm2 startup pm2 saveКак PM2 распределяет запросы?
PM2 использует алгоритм Round Robin по умолчанию. Главный процесс (master) принимает входящие соединения и передает их воркерам по очереди:
Входящий запрос ↓ Master процесс PM2 ↓ Воркер 1, затем Воркер 2, затем Воркер 3, затем Воркер 4, затем снова Воркер 1...Отличие PM2 от Nginx
- PM2: Управляет несколькими копиями одного приложения на одной машине
- Nginx: Распределяет нагрузку между разными машинами (или портами)
Часто используют вместе: Nginx распределяет между машинами, PM2 на каждой машине управляет кластером.
Часть 6: Обработка Сессий при Горизонтальном Масштабировании
Проблема
Когда у вас есть несколько копий приложения, встает вопрос: где хранить данные сессии пользователя?
Пользователь логинится на Сервер 1 → сессия сохраняется на Сервер 1 Пользователь (через балансировщик) попадает на Сервер 2 → где найти сессию?Решение 1: Sticky Sessions (IP Hash)
Используем IP Hash балансировщика, чтобы один пользователь всегда был на одном сервере:
Nginx:
upstream nodejs_servers { ip_hash; server localhost:3000; server localhost:3001; server localhost:3002; }Минусы: Неравномерная нагрузка, если один сервер упадет — пользователь потеряет сессию.
Решение 2: Централизованное Хранилище (Рекомендуется)
Храните сессии в отдельной БД или кэше (Redis), доступной всем серверам.
Пример с Redis:
const express = require('express'); const session = require('express-session'); const RedisStore = require('connect-redis').default; const redis = require('redis'); const app = express(); // Создаем Redis клиент const redisClient = redis.createClient({ host: 'localhost', port: 6379 }); redisClient.connect(); // Настраиваем сессии с хранилищем в Redis app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret-key', resave: false, saveUninitialized: false, cookie: { secure: false, // true для HTTPS httpOnly: true, maxAge: 24 * 60 * 60 * 1000 // 24 часа } })); app.get('/login', (req, res) => { req.session.userId = 123; req.session.username = 'john'; res.json({ message: 'Logged in', session: req.session }); }); app.get('/profile', (req, res) => { if (req.session.userId) { res.json({ message: 'Welcome back', username: req.session.username }); } else { res.status(401).json({ error: 'Not logged in' }); } }); app.listen(3000, () => { console.log('Server with Redis sessions on port 3000'); });Установите зависимости:
npm install express-session redis connect-redisПреимущества:
- Один пользователь может обращаться к разным серверам
- Если сервер упадет, сессия не потеряется
- Масштабируется на множество машин
Минусы:
- Нужно поддерживать отдельное хранилище (Redis, PostgreSQL)
- Небольшое замедление из-за обращения к БД/кэшу
Решение 3: Stateless приложение (Токены)
Вообще не хранить сессии на сервере. Используйте JWT токены:
const express = require('express'); const jwt = require('jsonwebtoken'); const app = express(); const SECRET = 'your-secret-key'; app.get('/login', (req, res) => { const token = jwt.sign( { userId: 123, username: 'john' }, SECRET, { expiresIn: '24h' } ); res.json({ token }); }); app.get('/profile', (req, res) => { const token = req.headers.authorization?.replace('Bearer ', ''); try { const decoded = jwt.verify(token, SECRET); res.json({ message: 'Welcome', user: decoded }); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } }); app.listen(3000);Преимущества:
- Совсем не нужно хранить состояние на сервере
- Легко масштабируется на любое количество машин
- Работает отлично для микросервисов
Минусы:
- Клиент должен отправлять токен в каждом запросе
- Токен нельзя аннулировать до истечения (или нужен отдельный список отозванных)
Часть 7: Health Checks (Проверки Здоровья Серверов)
Балансировщик должен знать, какие серверы работают, а какие упали.
Health Check в Nginx
upstream nodejs_servers { server localhost:3000; server localhost:3001; server localhost:3002; # Проверка: отправить GET запрос на /health # Если не ответит за 1 сек или вернет ошибку — сервер мертв } server { listen 80; location / { proxy_pass http://nodejs_servers; # Таймауты proxy_connect_timeout 3s; proxy_send_timeout 5s; proxy_read_timeout 5s; } # Endpint здоровья (для балансировщика проверить) location /health { proxy_pass http://nodejs_servers; } }Endpoint здоровья в приложении
const express = require('express'); const app = express(); // Endpoint для проверок здоровья app.get('/health', (req, res) => { // Проверяем, что БД доступна, нет критических ошибок и т.д. const healthcheck = { uptime: process.uptime(), timestamp: Date.now(), status: 'OK' }; res.status(200).json(healthcheck); }); // Используйте этот endpoint в тестах app.listen(3000);Health Check в PM2
PM2 встроенно мониторит состояние процессов и перезагружает упавшие.
Используйте параметры в
ecosystem.config.js:module.exports = { apps: [{ name: 'app', script: './server.js', instances: 4, exec_mode: 'cluster', watch: false, max_memory_restart: '500M', autorestart: true, min_uptime: '10s', max_restarts: 10, listen_timeout: 3000 // Даем 3 сек на инициализацию }] };
Часть 8: Полный Пример — Реальная Setup
Объединим всё вместе: Nginx балансировщик + несколько Node.js серверов + PM2 кластер.
Архитектура
Интернет (пользователи) ↓ Nginx (порт 80) на машине 1 ↓ ┌────────────────────────────────────────────────┐ │ Машина 2: PM2 кластер (4 воркера) │ │ ├─ Воркер 1 (порт 3000) │ │ ├─ Воркер 2 (порт 3001) │ │ ├─ Воркер 3 (порт 3002) │ │ └─ Воркер 4 (порт 3003) │ └────────────────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────┐ │ Машина 3: PM2 кластер (4 воркера) │ │ ├─ Воркер 1 (порт 3000) │ │ ├─ Воркер 2 (порт 3001) │ │ ├─ Воркер 3 (порт 3002) │ │ └─ Воркер 4 (порт 3003) │ └────────────────────────────────────────────────┘ ↓ Централизованное хранилище сессий (Redis / PostgreSQL)Шаг 1: Приложение Node.js
server.js:const express = require('express'); const session = require('express-session'); const redis = require('redis'); const RedisStore = require('connect-redis').default; const app = express(); const PORT = process.env.PORT || 3000; // Redis для сессий const redisClient = redis.createClient({ host: 'redis.example.com', // Централизованный Redis port: 6379 }); redisClient.connect(); // Session middleware app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'production-secret-key', resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 } })); // Health check app.get('/health', (req, res) => { res.json({ status: 'OK', uptime: process.uptime(), port: PORT, pid: process.pid }); }); // API endpoint app.get('/api/data', (req, res) => { req.session.lastAccess = new Date(); res.json({ data: 'Hello from Node.js', port: PORT, sessionId: req.sessionID, sessionData: req.session }); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully...'); // PM2 даст нам 3 сек (listen_timeout) на завершение redisClient.quit(); process.exit(0); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}, PID: ${process.pid}`); });Шаг 2: PM2 конфиг на каждой машине
ecosystem.config.js(на машине 2 и машине 3):module.exports = { apps: [ { name: 'nodejs-app', script: './server.js', instances: 4, exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, watch: false, max_memory_restart: '500M', autorestart: true, min_uptime: '10s', max_restarts: 10, listen_timeout: 3000, error_file: './logs/err.log', out_file: './logs/out.log', log_date_format: 'YYYY-MM-DD HH:mm:ss Z' } ] };Запустите:
pm2 start ecosystem.config.js pm2 startup pm2 saveШаг 3: Nginx конфиг
На машине 1, файл
/etc/nginx/sites-available/nodeapp:upstream nodejs_servers { least_conn; # Используем least connections для равномерной нагрузки # Серверы на машине 2 server 192.168.1.10:3000; server 192.168.1.10:3001; server 192.168.1.10:3002; server 192.168.1.10:3003; # Серверы на машине 3 server 192.168.1.11:3000; server 192.168.1.11:3001; server 192.168.1.11:3002; server 192.168.1.11:3003; } server { listen 80; server_name api.example.com; # Логи access_log /var/log/nginx/nodejs_access.log; error_log /var/log/nginx/nodejs_error.log; location / { proxy_pass http://nodejs_servers; # Заголовки 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; # Таймауты proxy_connect_timeout 3s; proxy_send_timeout 5s; proxy_read_timeout 5s; # Буферизация proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; } # Health check endpoint location /health { proxy_pass http://nodejs_servers; access_log off; } # SSL (если нужно) # listen 443 ssl http2; # ssl_certificate /etc/ssl/certs/cert.pem; # ssl_certificate_key /etc/ssl/private/key.pem; } # Перенаправление HTTP на HTTPS server { listen 80; server_name api.example.com; return 301 https://$server_name$request_uri; }Включите конфиг:
sudo ln -s /etc/nginx/sites-available/nodeapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginxШаг 4: Мониторинг
Проверяем статус:
# На машинах 2 и 3 pm2 status pm2 logs nodejs-app # На машине 1 (Nginx) sudo systemctl status nginx sudo tail -f /var/log/nginx/nodejs_access.logТестируем:
# На локальной машине for i in {1..10}; do curl http://api.example.com/api/data; echo ""; doneВидите меняющиеся значения
portиpid— балансировка работает!
Часть 9: Распространённые Проблемы и Решения
Проблема 1: Sticky Sessions не работают
Симптом: Пользователь теряет данные между запросами.
Причина: Используются разные серверы для одного пользователя.
Решение:
- Либо используйте IP Hash в балансировщике
- Либо сохраняйте сессию в Redis/БД (рекомендуется)
Проблема 2: Высокая задержка при проксировании
Симптом: Приложение через балансировщик работает медленнее.
Причина: Сетевые задержки, неправильная буферизация.
Решение:
# Optimize buffering proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; proxy_busy_buffers_size 8k; # TCP optimization proxy_tcp_nodelay on; proxy_tcp_nopush on;Проблема 3: Соединение разрывается при reload сервера
Симптом: Клиенты получают ошибку при перезагрузке.
Причина: Сервер не обрабатывает graceful shutdown.
Решение: Добавьте обработку SIGTERM:
process.on('SIGTERM', () => { server.close(() => { console.log('Server shut down gracefully'); process.exit(0); }); });Проблема 4: Памятью утекает на одном сервере
Симптом: Один воркер занимает всё больше памяти.
Причина: Memory leak в коде.
Решение: Настройте автоматический перезапуск:
max_memory_restart: '500M' // PM2 перезагрузит если превышено 500MBПроблема 5: WebSocket не работает через балансировщик
Симптом: WebSocket соединение разрывается.
Причина: Нужен sticky session для WebSocket.
Решение:
upstream nodejs_servers { ip_hash; # Обязательно для WebSocket! server localhost:3000; server localhost:3001; } location / { proxy_pass http://nodejs_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
Часть 10: Best Practices и Оптимизация
1. Количество воркеров
// Оптимально: количество CPU ядер на машине const numCPUs = require('os').cpus().length; // PM2: instances: numCPUs // или 'max' в ecosystem.config.js2. Graceful Shutdown
const server = app.listen(PORT, () => { console.log(`Server on ${PORT}`); }); process.on('SIGTERM', () => { console.log('SIGTERM received'); server.close(() => { console.log('HTTP server closed'); process.exit(0); }); // Если не закрылось за 10 сек — просто выходим setTimeout(() => { console.error('Could not close connections, forcing shutdown'); process.exit(1); }, 10000); });3. Логирование и Мониторинг
const morgan = require('morgan'); const app = express(); // Логируем все запросы app.use(morgan('combined')); // Логируем неперехваченные ошибки process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); });4. Rate Limiting
const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 минут max: 100 // Максимум 100 запросов за окно }); app.use('/api', limiter);5. Кэширование ответов
upstream nodejs_servers { server localhost:3000; server localhost:3001; } proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m; server { location /api/public { proxy_pass http://nodejs_servers; proxy_cache my_cache; proxy_cache_valid 200 10m; // Кэшировать на 10 минут } }
Итоги
Что мы изучили:
- Зачем нужен балансировщик нагрузки — распределение запросов, высокая доступность, масштабируемость
- Алгоритмы — Round Robin, Least Connections, IP Hash
- Nginx — надежный, быстрый, конфигурируется просто
- Node.js Балансировщик — гибкий, если нужна кастомная логика
- PM2 — управление кластером на одной машине с нулевым downtime
- Сессии — Redis/БД для масштабирования, либо JWT токены
- Health Checks — автоматическое исключение мертвых серверов
- Полная архитектура — Nginx + PM2 кластеры на нескольких машинах
Рекомендуемый стек для production:
Интернет ↓ Nginx (балансировщик на порт 80/443) ↓ Несколько машин (2+) с PM2 кластером (4+ воркера на машину) ↓ Централизованное хранилище сессий (Redis) ↓ База данныхГлавное правило: Ваше приложение должно быть stateless — не хранить пользовательские данные локально. Тогда балансировка будет работать идеально.
Начните с Nginx + 2 простых серверов, потом добавляйте PM2 кластеры по мере роста нагрузки.
-
Node.js Load Balancer: Быстрый Старт - Готовые Примеры
Вариант 1: Nginx + 3 Node.js сервера (самый простой способ)
Шаг 1. Создайте простой Node.js app
Файл
app.js:const http = require('http'); const PORT = process.env.PORT || 3000; const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'Hello from Node.js', port: PORT, timestamp: new Date().toISOString() })); }); server.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });Шаг 2. Запустите 3 копии
# Терминал 1 PORT=3000 node app.js # Терминал 2 PORT=3001 node app.js # Терминал 3 PORT=3002 node app.jsШаг 3. Установите Nginx
# macOS brew install nginx # Ubuntu sudo apt-get update && sudo apt-get install nginxШаг 4. Настройте Nginx
Отредактируйте
/usr/local/etc/nginx/nginx.conf(macOS) или/etc/nginx/nginx.conf(Ubuntu):http { upstream backend { server localhost:3000; server localhost:3001; server localhost:3002; } server { listen 8080; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } }Шаг 5. Запустите Nginx
# macOS nginx # или перезагрузить nginx -s reload # Ubuntu sudo systemctl start nginxШаг 6. Тестируйте
# Откройте браузер и несколько раз перейдите на # http://localhost:8080 # Или используйте curl for i in {1..9}; do curl http://localhost:8080 echo "" doneВидите меняющиеся порты (3000, 3001, 3002)? Балансировка работает!

Вариант 2: Собственный балансировщик на Node.js
Файл:
load-balancer.jsconst express = require('express'); const axios = require('axios'); const app = express(); // Backend серверы const servers = [ 'http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002' ]; let currentIndex = 0; // Middleware для балансировки app.use(async (req, res) => { try { // Round-robin: выбираем сервер по порядку const server = servers[currentIndex]; currentIndex = (currentIndex + 1) % servers.length; // Отправляем запрос на backend сервер const response = await axios.get(server); res.json(response.data); } catch (error) { console.error('Backend error:', error.message); res.status(503).json({ error: 'Service unavailable' }); } }); app.listen(8080, () => { console.log('Load balancer running on port 8080'); });Установите зависимости:
npm init -y npm install express axiosЗапустите:
node load-balancer.jsТестируйте:
curl http://localhost:8080
Вариант 3: PM2 Cluster Mode (для одной машины)
Файл:
server.jsconst express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; app.get('/', (req, res) => { res.json({ message: 'Hello from Node.js', port: PORT, processId: process.pid }); }); app.listen(PORT, () => { console.log(`Server running on port ${PORT}, PID: ${process.pid}`); });Файл:
ecosystem.config.jsmodule.exports = { apps: [{ name: 'myapp', script: './server.js', instances: 4, // 4 копии приложения exec_mode: 'cluster', env: { NODE_ENV: 'development' } }] };Установите PM2:
npm install -g pm2 npm install expressЗапустите:
pm2 start ecosystem.config.jsПолезные команды:
pm2 status # Статус всех процессов pm2 logs # Логи в реальном времени pm2 stop all # Остановить всё pm2 delete all # Удалить из PM2Тестируйте на порту 3000:
for i in {1..8}; do curl http://localhost:3000 echo "" doneВидите разные PID? PM2 распределяет между 4 воркерами!

Вариант 4: Sticky Sessions (если нужны сессии)
Backend сервер с Express-session:
const express = require('express'); const session = require('express-session'); const app = express(); const PORT = process.env.PORT || 3000; // Сессии в памяти (для локальной разработки) app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: true, cookie: { maxAge: 60000 } })); app.get('/login', (req, res) => { req.session.userId = 123; req.session.username = 'john_doe'; res.json({ message: 'Logged in', port: PORT }); }); app.get('/profile', (req, res) => { if (req.session.userId) { res.json({ username: req.session.username, port: PORT }); } else { res.status(401).json({ error: 'Not logged in' }); } }); app.listen(PORT);Nginx config с sticky sessions:
upstream backend { ip_hash; # Один IP адрес — один сервер server localhost:3000; server localhost:3001; server localhost:3002; } server { listen 8080; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }Тестируйте:
# Логинитесь curl -c cookies.txt http://localhost:8080/login # Проверяете профиль (должен вернуть ваше имя) curl -b cookies.txt http://localhost:8080/profile # Повторяйте несколько раз - всегда один и тот же port! curl -b cookies.txt http://localhost:8080/profile
Вариант 5: Least Connections для лучшей балансировки
Node.js балансировщик с Least Connections:
const express = require('express'); const axios = require('axios'); const app = express(); // Серверы с отслеживанием соединений const servers = [ { url: 'http://localhost:3000', connections: 0 }, { url: 'http://localhost:3001', connections: 0 }, { url: 'http://localhost:3002', connections: 0 } ]; function selectServer() { // Выбираем сервер с наименьшим количеством активных соединений return servers.reduce((prev, curr) => prev.connections < curr.connections ? prev : curr ); } app.use(async (req, res) => { const server = selectServer(); server.connections++; try { const response = await axios.get(server.url); res.json(response.data); } catch (error) { res.status(503).json({ error: 'Service unavailable' }); } finally { server.connections--; } }); app.listen(8080, () => { console.log('Load balancer (least connections) on port 8080'); });
Вариант 6: Health Checks (проверка живых серверов)
Backend сервер с health endpoint:
const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'OK', uptime: process.uptime(), timestamp: new Date().toISOString() }); }); app.get('/', (req, res) => { // Иногда симулируем ошибку if (Math.random() > 0.8) { res.status(500).json({ error: 'Internal error' }); } else { res.json({ port: PORT }); } }); app.listen(PORT);Балансировщик с health checks:
const express = require('express'); const axios = require('axios'); const app = express(); const servers = [ { url: 'http://localhost:3000', healthy: true }, { url: 'http://localhost:3001', healthy: true }, { url: 'http://localhost:3002', healthy: true } ]; // Периодически проверяем здоровье серверов setInterval(async () => { for (const server of servers) { try { await axios.get(server.url + '/health', { timeout: 2000 }); server.healthy = true; } catch (error) { server.healthy = false; } } }, 5000); let currentIndex = 0; app.use(async (req, res) => { // Находим живой сервер const healthyServers = servers.filter(s => s.healthy); if (healthyServers.length === 0) { return res.status(503).json({ error: 'All backends down' }); } // Round-robin только среди живых const server = healthyServers[currentIndex % healthyServers.length]; currentIndex++; try { const response = await axios.get(server.url); res.json(response.data); } catch (error) { res.status(503).json({ error: 'Service unavailable' }); } }); app.listen(8080);
Вариант 7: Docker Compose (полная локальная setup)
docker-compose.yml:
version: '3.8' services: app1: image: node:18 working_dir: /app volumes: - ./app.js:/app/app.js environment: PORT: 3000 command: node app.js ports: - "3000:3000" app2: image: node:18 working_dir: /app volumes: - ./app.js:/app/app.js environment: PORT: 3000 command: node app.js ports: - "3001:3000" app3: image: node:18 working_dir: /app volumes: - ./app.js:/app/app.js environment: PORT: 3000 command: node app.js ports: - "3002:3000" nginx: image: nginx:latest volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - "8080:80" depends_on: - app1 - app2 - app3nginx.conf:
events { worker_connections 1024; } http { upstream backend { server app1:3000; server app2:3000; server app3:3000; } server { listen 80; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } }Запустите:
docker-compose upТестируйте:
curl http://localhost:8080
Сравнение вариантов
Вариант Сложность Масштабируемость Когда использовать Nginx Низкая На 1 машину Для простых случаев, production Node.js балансировщик Средняя На 1 машину Когда нужна кастомная логика PM2 Низкая На 1 машину Быстрый старт, development Docker Средняя На несколько машин Микросервисы, облако
Частые ошибки и как их избежать
Ошибка 1: Забыли установить зависимости
npm install express axios # Для Node.js балансировщикаОшибка 2: Порты уже заняты
# Найти процесс на порту 8080 lsof -i :8080 # Убить процесс kill -9 <PID>Ошибка 3: Nginx ошибка при перезагрузке
# Проверить конфиг nginx -t # Если ошибка - смотреть её и исправлятьОшибка 4: Соединение отказано (Connection refused)
# Убедитесь что все backend серверы работают! curl http://localhost:3000 curl http://localhost:3001 curl http://localhost:3002
Команды для тестирования нагрузки
Простый тест через curl:
for i in {1..20}; do curl http://localhost:8080 echo "" doneApache Bench (если установлен):
ab -n 1000 -c 10 http://localhost:8080/Установка Apache Bench:
# macOS brew install httpd # Ubuntu sudo apt-get install apache2-utilswrk (продвинутый инструмент):
# Установка git clone https://github.com/wg/wrk.git cd wrk && make # Тест: 4 потока, 100 соединений, 30 секунд ./wrk -t4 -c100 -d30s http://localhost:8080/
Логирование и отладка
PM2 логи:
pm2 logs myapp --lines 100 pm2 logs myapp --err # Только ошибкиNginx логи:
# Все запросы tail -f /var/log/nginx/access.log # Ошибки tail -f /var/log/nginx/error.logNode.js логирование:
// Добавьте в app.js const fs = require('fs'); app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); });
Дополнительные ресурсы
- Документация Nginx: https://nginx.org/en/docs/
- PM2 документация: https://pm2.io/docs/runtime/
- Express.js: https://expressjs.com/
- Axios: https://axios-http.com/
Готово! Выбирайте вариант, который вам нравится, и начинайте масштабировать!
© 2024 - 2025 ExLends, Inc. Все права защищены.