Как масштабировать приложение Node.js на Heroku для обработки более 1000 пользователей с помощью Socket.IO и Puppeteer?

Вопрос или проблема

У меня есть приложение на Node.js, использующее Express, Socket.IO и Puppeteer, которое интегрируется с WhatsApp через библиотеку whatsapp-web.js. Приложение предназначено для обработки связи в реальном времени для пользователей, которые регистрируют свои телефонные номера. Однако меня беспокоит его способность эффективно масштабироваться на Heroku, особенно при обработке более 1000 пользователей одновременно. Вот некоторые ключевые моменты моей текущей реализации:
Управление сессиями: Я использую express-session с хранилищем в памяти для управления сессиями. Это работает нормально для небольшого числа пользователей, но я понимаю, что это не сохранится при использовании нескольких динозавров в Heroku Dyno Info (Performance-M 12x Compute 2.5 GB RAM). Я не могу использовать Redis или MongoDB для хранения сессий.
Подключения WebSocket: Приложение использует Socket.IO для управления соединениями в реальном времени. Каждый пользователь поддерживает соединение WebSocket, что потребляет ресурсы. Я беспокоюсь о снижении производительности по мере увеличения числа одновременных подключений.
Кластер Puppeteer: Я использую Puppeteer с настроенным кластером для управления клиентами WhatsApp. Когда я использую Puppeteer самостоятельно, у меня нет этой проблемы, но когда 4 или более пользователей входят в систему, приложение вылетает. Моя текущая конфигурация позволяет максимальную одновременную работу 10 клиентов, но проблема, с которой я сталкиваюсь, при использовании этого puppeteer-cluster, возникает следующая ошибка:

node_modules\puppeteer-cluster\dist\Worker.js:41
                        throw new Error('Unable to get browser page');
                              ^

Ошибка: Невозможно получить страницу браузера

require("dotenv").config();
const express = require("express");
const http = require("http");
const socketIO = require("socket.io");
const session = require("express-session");
const { Client, LocalAuth } = require("whatsapp-web.js");
const axios = require("axios");
const qr = require("qr-image");
const { Cluster } = require("puppeteer-cluster");
const puppeteerConfig = require("./.puppeteerrc.cjs");

/**
 * Express Приложение - Настройка
 */
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
  transports: ["websocket", "polling"], // Включить как WebSocket, так и опрос
});

// Хранилище сессий в памяти (заметьте: в памяти не будет сохранено между динозаврами в сценариях масштабирования)
const sessionMiddleware = session({
  secret: process.env.SESSION_SECRET || "67Vx4cuhcx",
  resave: false,
  saveUninitialized: false,
  cookie: { secure: process.env.NODE_ENV === "production" },
});

app.use(sessionMiddleware);
app.use(express.static("public"));
app.use(express.json());

const clients = {};  // Хранить клиентов WhatsApp по номеру телефона
const users = {};    // Хранить подключенных пользователей

// Настройка кластера Puppeteer
const clusterOptions = {
  concurrency: Cluster.CONCURRENCY_CONTEXT,
  maxConcurrency: Math.min(10, Math.floor(process.env.MAX_CLIENTS || 10)), // Установить максимальную одновременность на основе переменной окружения
  puppeteerOptions: {
    ...puppeteerConfig,
    headless: true,
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      "--disable-dev-shm-usage",
      "--disable-gpu",
      "--single-process",
    ],
    executablePath: process.env.EDGE_BIN || "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", // Используйте путь Edge на Heroku
  },
};

// Запуск кластера Puppeteer
let cluster;
(async () => {
  cluster = await Cluster.launch(clusterOptions);
})();

// Middleware для привязки сессии к сокету
io.use((socket, next) => {
  sessionMiddleware(socket.request, {}, next);
});

// Управление сокет-соединениями
io.on("connection", (socket) => {
  console.log("Новое соединение установлено");

  const sessionId = socket.request.sessionID;

  // Ограничить повторные подключения, проверяя, существует ли пользователь уже
  if (users[sessionId]) {
    console.log(`Пользователь с идентификатором сессии ${sessionId} уже подключен`);
    socket.disconnect();
    return;
  }

  users[sessionId] = socket; // Хранить сокет пользователя по идентификатору сеанса

  socket.on("registerPhone", async (phoneNumber) => {
    console.log(`Клиент подключен с номером телефона: ${phoneNumber}`);

    // Проверка, существует ли клиент, чтобы избежать множественной инициализации
    if (!clients[phoneNumber]) {
      clients[phoneNumber] = await cluster.execute(async ({ page }) => {
        const client = new Client({
          authStrategy: new LocalAuth({ clientId: phoneNumber }),
        });

        // Событие: Генерация QR-кода
        client.on("qr", (qrCode) => {
          console.log(`QR-код получен для номера телефона ${phoneNumber}`);
          const qrImage = qr.imageSync(qrCode, { type: "png" });
          const qrData = "data:image/png;base64," + qrImage.toString("base64");
          socket.emit("qr", qrData);
        });

        // Событие: Клиент отключен
        client.on("disconnected", (reason) => {
          console.log(`WhatsApp отключен для номера телефона ${phoneNumber}: ${reason}`);
          socket.emit("disconnected", "WhatsApp был отключен. Пожалуйста, обновите страницу.");
          delete clients[phoneNumber]; // Очистка
        });

        await client.initialize();
        return client;
      });
    }
  });

  // Обработка отключения сокета
  socket.on("disconnect", () => {
    console.log(`Клиент отключен с идентификатором сеанса ${sessionId}`);
    delete users[sessionId]; // Удалить сессию пользователя
  });
});

// Маршрут для получения QR-кода для конкретного пользователя
app.get("/qr-code/:userId", async (req, res) => {
  const phoneNumber = req.params.userId;
  io.on("connection", (socket) => {
    socket.emit("passNumber", phoneNumber);
  });
  res.redirect("/qr_code.html");
});
// Запустите сервер на указанном порту или по умолчанию на порту 3000
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Сервер работает на порту ${PORT}`);
});

package.json
{
  "main": "main.js",
  "scripts": {
    "start": "node main.js",
  },
  "dependencies": {
    "express": "^4.21.0",
    "express-session": "^1.18.0",
    "puppeteer": "^23.4.0",
    "puppeteer-cluster": "^0.24.0",
    "qr-image": "^3.2.0",
    "socket.io": "^4.8.0",
    "whatsapp-web.js": "^1.26.0"
  }
}

Ответ или решение

Для обеспечения масштабируемости вашего Node.js приложения на платформе Heroku, способного обрабатывать более 1000 пользователей одновременно с использованием Socket.IO и Puppeteer, необходимо внести несколько важных изменений в архитектуру и реализацию вашего приложения. Давайте разберем ключевые аспекты:

1. Управление сессиями

Использование express-session с in-memory хранилищем не будет работать для масштабируемости на нескольких динозавров Heroku. Вам необходимо использовать внешнее хранилище для сессий. Рассмотрите возможность использования Heroku Redis, который предоставляет бесплатный тарифный план с ограничениями, но может быть достаточно для начала. Если Redis использовать не получится, стоит подумать о других подходах, таких как JWT (JSON Web Tokens) для аутентификации, которые не требуют хранения сессий на сервере.

Пример использования JWT:

const jwt = require('jsonwebtoken');

// При аутентификации
const token = jwt.sign({ phoneNumber }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });

// При проверке
const decoded = jwt.verify(token, process.env.JWT_SECRET);

2. WebSocket Подключения

Socket.IO использует WebSocket для двустороннего взаимодействия, и каждому пользователю требуется активное соединение. Вам нужно следить за количеством подключений и при необходимости добавлять логику для управления подключениями. Это может включать в себя ограничение на количество одновременно подключенных пользователей, аналогично реализации connect и disconnect на вашем сервере:

const MAX_CONNECTIONS = 1000; // Максимальное количество подключений
let activeConnections = 0;

io.on("connection", (socket) => {
  if (activeConnections >= MAX_CONNECTIONS) {
    socket.disconnect();
    return;
  }
  activeConnections++;

  socket.on("disconnect", () => {
    activeConnections--;
  });
});

3. Распределение нагрузки и Puppeteer

Puppeteer потребляет много ресурсов, и при высоком уровне параллелизма (как у вас) могут возникать ошибки. Один из способов решения этой проблемы — использование очередей на основе задач для управления количеством одновременно работающих экземпляров Puppeteer. Возможные решения включают использование bull или bee-queue для распределения нагрузки по экземплярам:

const Queue = require('bull');
const puppeteerQueue = new Queue('puppeteerQueue');

puppeteerQueue.process(async (job) => {
  const client = new Client({ authStrategy: new LocalAuth({ clientId: job.data.phoneNumber }) });
  await client.initialize();
  // добавьте вашу логику здесь
});

// При регистрации пользователя
puppeteerQueue.add({ phoneNumber });

4. Устойчивость к ошибкам

Нужно добавить обработку ошибок по всему коду, особенно при работе с Puppeteer. Вы можете использовать механизмы повторных попыток и внутренние проверки:

cluster.on('disconnected', (client, reason) => {
  console.log(`Client for ${client.id} disconnected: ${reason}`);
  // ... логика повторного подключения или обработки ошибок
});

5. Нагрузочное тестирование

Перед тем как развернуть приложение на Heroku, проведите нагрузочное тестирование с помощью инструментов, таких как Artillery или JMeter. Это поможет выявить узкие места в архитектуре и позволит вам оптимизировать приложение перед выходом в продакшн.

6. Настройка Heroku

Убедитесь, что вы правильно настроили количество динамиков, необходимых вашему приложению. Для обработки более высокой нагрузки, возможно, потребуется переключиться на более производительные типы динамиков, такие как Performance-M, которые обеспечивают больше ресурсов.

Заключение

С масштабированием приложения, особенно при использовании WebSocket и Puppeteer, потребуются продуманные архитектурные изменения, которые помогут вам поддерживать производительность и устойчивость. Применяя предложенные подходы, вы значительно повысите возможность вашего приложения обрабатывать 1000+ пользователей одновременно.

Оцените материал
Добавить комментарий

Капча загружается...