Перейти к содержанию
  • Лента
  • Категории
  • Последние
  • Метки
  • Популярные
  • Пользователи
  • Группы
Свернуть
exlends
Категории
  1. Главная
  2. Категории
  3. Бекенд, разработка серверов
  4. Node JS
  5. Node.js и Балансировщик Нагрузки: Полное Руководство для Новичков

Node.js и Балансировщик Нагрузки: Полное Руководство для Новичков

Запланировано Прикреплена Закрыта Перенесена Node JS
2 Сообщения 1 Постеры 13 Просмотры
  • Сначала старые
  • Сначала новые
  • По количеству голосов
Ответить
  • Ответить, создав новую тему
Авторизуйтесь, чтобы ответить
Эта тема была удалена. Только пользователи с правом управления темами могут её видеть.
  • AladdinA Не в сети
    AladdinA Не в сети
    Aladdin
    js
    написал отредактировано
    #1

    Представьте ситуацию: вы запустили Node.js приложение на боевом сервере, оно работает хорошо при 100 пользователях в день. Но вот приложение набирает популярность, и уже 10,000 пользователей одновременно пытаются получить доступ. Единственный экземпляр вашего приложения начинает медленнее отвечать, потом вообще падает. Запросы теряются, пользователи уходят. Вот здесь и приходит на помощь балансировщик нагрузки.

    Балансировщик нагрузки — это промежуточный слой между вашими пользователями и приложением, который распределяет входящие запросы между несколькими копиями вашего приложения, работающими одновременно.


    Часть 1: Основные Концепции

    Что такое горизонтальное масштабирование?

    Есть два способа справиться с растущей нагрузкой:

    Вертикальное масштабирование (upgrade сервера)

    • Вы просто покупаете более мощный сервер (больше RAM, быстрее CPU)
    • Простой способ, но дорогой и имеет физический предел
    • Если сервер падает — всё падает

    Горизонтальное масштабирование (добавить серверы)

    • Вы запускаете копии приложения на разных машинах или портах
    • Балансировщик распределяет запросы между ними
    • Дешевле в долгосроке, нет единой точки отказа, можно масштабировать бесконечно

    Вывод: для highload приложений используйте горизонтальное масштабирование с балансировщиком нагрузки.

    Зачем нужен балансировщик нагрузки?

    1. Улучшение производительности — запросы распределяются, ни один сервер не перегружается
    2. Высокая доступность — если один сервер упадет, остальные продолжат работать
    3. Масштабируемость — просто добавляете новые серверы при растущей нагрузке
    4. Равномерная нагрузка — алгоритмы балансировки гарантируют справедливое распределение

    Часть 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.js
    

    2. 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 минут
      }
    }
    

    Итоги

    Что мы изучили:

    1. Зачем нужен балансировщик нагрузки — распределение запросов, высокая доступность, масштабируемость
    2. Алгоритмы — Round Robin, Least Connections, IP Hash
    3. Nginx — надежный, быстрый, конфигурируется просто
    4. Node.js Балансировщик — гибкий, если нужна кастомная логика
    5. PM2 — управление кластером на одной машине с нулевым downtime
    6. Сессии — Redis/БД для масштабирования, либо JWT токены
    7. Health Checks — автоматическое исключение мертвых серверов
    8. Полная архитектура — Nginx + PM2 кластеры на нескольких машинах

    Рекомендуемый стек для production:

    Интернет
      ↓
    Nginx (балансировщик на порт 80/443)
      ↓
    Несколько машин (2+) с PM2 кластером (4+ воркера на машину)
      ↓
    Централизованное хранилище сессий (Redis)
      ↓
    База данных
    

    Главное правило: Ваше приложение должно быть stateless — не хранить пользовательские данные локально. Тогда балансировка будет работать идеально.

    Начните с Nginx + 2 простых серверов, потом добавляйте PM2 кластеры по мере роста нагрузки.

    1 ответ Последний ответ
    1
    • AladdinA Не в сети
      AladdinA Не в сети
      Aladdin
      js
      написал отредактировано
      #2

      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.js

      const 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.js

      const 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.js

      module.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
            - app3
      

      nginx.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 ""
      done
      

      Apache Bench (если установлен):

      ab -n 1000 -c 10 http://localhost:8080/
      

      Установка Apache Bench:

      # macOS
      brew install httpd
      
      # Ubuntu
      sudo apt-get install apache2-utils
      

      wrk (продвинутый инструмент):

      # Установка
      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.log
      

      Node.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/

      Готово! Выбирайте вариант, который вам нравится, и начинайте масштабировать!

      1 ответ Последний ответ
      0

      Категории

      • Главная
      • Новости
      • Фронтенд
      • Бекенд
      • Языки программирования

      Контакты

      • Сотрудничество
      • info@exlends.com
      • Наш чат
      • Наш ТГ канал

      © 2024 - 2025 ExLends, Inc. Все права защищены.

      Политика конфиденциальности
      • Войти

      • Нет учётной записи? Зарегистрироваться

      • Войдите или зарегистрируйтесь для поиска.
      • Первое сообщение
        Последнее сообщение
      0
      • Лента
      • Категории
      • Последние
      • Метки
      • Популярные
      • Пользователи
      • Группы