Задать вопрос

Как правильно масштабировать nodejs-приложение с помощью cluster?

Для начала, вот мой кривой стартовый скрипт (кривой потому-что не работает правильно), я постарался прокомментировать в нем все чтобы вам было понятнее:
#!/usr/bin/env node
const log = require("lib/log")(module);  
require('globals');   //подключаем глобальные переменные global.****
const DatabaseController = require("lib/mysql");   //конструктор mysql запросов
const LocalizationController = require("localizationController"); //Загружает данные с локализацией из mysql в глобальные переменные
const CronJob = require('cron').CronJob;
const http = require('http');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

var app = require('config/application');  //подключаем express со всеми его настройками (мидлвары, view-engine, роуты и др)
app.set('port', _G.cfg.port);  //_G - глобальная переменная с конфигами и проч
var server = http.createServer(app);
var io = require('base/socket')(server); //подключаем socket.io, в 'base/socket' находятся основные события connect/disconnect, авторизация сокета и др.
var redis = require('socket.io-redis');
io.adapter(redis({ host: 'localhost', port: 6379 }));

if (cluster.isMaster) {
  var i = 0;  //поскольку мы в мастере, стартуем тестовую кронджобу, предпологаем что они будут выполняться только в мастере
  let job = new CronJob('* * * * * *', function() {
    log.info(`You will see this message every second: ${i}`);
    i++;
  }, null);
  job.start();

  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    log.info(`worker ${worker.process.pid} died`);
  });
} else {
  // для воркеров
  server.listen(_G.cfg.port,()=>{  //запускаем сервер на нашем порту из конфига
    LocalizationController.refreshLocales(() =>{  //загружаем локализации из бд в глоб переменную
      let cc = DatabaseController.cacheController;  // DatabaseController.cacheController - экземпляр для работы с redis
      cc.getByMask(`${_G.cfg.session.matchingID}:*`,(err,data) =>{ //из redis загружаем все сессии
        for(let i in data){
          _G.sess[i.split(':').pop()] = data[i]; //кладем в глобальную переменную соответствие id-юзера и session-id
                                                  //их мы потом будем использовать для отправки данных нужному сокету
        }
        app.set('io', io);
        log.info(`App listen port: "${server.address().port}"`);
      });
    });
  });

  server.on('error', (error) =>{
    //error handler
  });
}


Здесь, у меня есть две наглядные проблемы, первая - это проблема сокетов, они ведут себя безобразно, например, сокет с одним и тем-же id при одном запросе может проходить авторизацию несколько раз, я не понял как с этим разобраться, чтобы заставить из работать правильно - так же как и без cluster.
Вторая, и очень важная проблема, это глобальные переменные. Я там храню локализацию и соответствие id-сессий - id авторизованного пользователя, и возможно для чего-либо еще они будут использоваться. Здесь проблема в том, что для всех процессов свои глобальные переменные, а нужно их как-то обобщить, если один процесс что-то изменяет в глобальных переменных - то это изменение должно продублироваться для всех процессов. Я не понимаю как это сделать, единственно что приходит в голову - хранить эти данные в redis и оттуда брать при необходимости, но насколько это отразиться на производительности приложения, ведь взять из глобальной переменной проще чем делать запрос к redis.

Для меня это очень запутанная задача (особенно с сокетами), я не пойму где я делаю ошибки или что я вообще не так делаю. Надеюсь на вашу помощь...
  • Вопрос задан
  • 2146 просмотров
Подписаться 6 Оценить Комментировать
Решения вопроса 1
@xfg
Здесь проблема в том, что для всех процессов свои глобальные переменные, а нужно их как-то обобщить, если один процесс что-то изменяет в глобальных переменных - то это изменение должно продублироваться для всех процессов.

Горизонтальное масштабирование в первую очередь предполагает возможность развернуть приложение на N количестве физически удаленных компьютеров. Соответственно вы не можете изменить что-то локально таким образом чтобы при этом оно продублировалось для всех процессов на всех удаленных машинах. Увы, но магии не существует. Чтобы это было возможным, требуется реализовать какой-нибудь механизм межпроцессного взаимодействия. Один из них вы уже назвали - использовать для этих целей redis.

Но ваша проблема в другом. В архитектуре. Её нет. Код выше - это спагетти-код. Поэтому вы думаете, что вам нужно это
если один процесс что-то изменяет в глобальных переменных - то это изменение должно продублироваться
В действительности если заняться рефакторингом кода, очень скоро может выясниться, что вам это было не так уж и нужно да и вроде как глобальные переменные были не особо нужны.

Вообще наличие большого количества глобальных переменных - это один из показателей плохого кода.

Вы можете продолжать городить костыли и накапливать технический долг сохраняя глобальное состояние в redis или прокидывать данные из дочерних процессов в мастер-процесс и наоборот (благо в node.js есть для этого инструменты), но все же лучше начать что-то изучать из области архитектурных решений в вебе (и в javascript в частности) и смотреть код других крупных проектов и желательно не только в javascript.

Думайте над архитектурой. Начните с того, как сделать так, чтобы отпала нужда в этих диких манипуляциях с глобальными переменными.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
Всё можно гораздо проще сделать, посмотрите в сторону PM2
pm2.keymetrics.io/docs/usage/cluster-mode
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы