Статьи

Приложение Multi-Instance Node.js в PaaS с использованием Redis Pub / Sub

Если вы выбрали PaaS в качестве хостинга для своего приложения, у вас, вероятно, возникла или возникнет такая проблема: ваше приложение развернуто в небольших «контейнерах» (известных как dynos в Heroku или шестеренках в OpenShift), и вы хотите масштабировать их.

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

В этом уроке я покажу вам, как преодолеть это небольшое неудобство.

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

Имейте в виду, что в этой статье я предполагаю, что у вас уже есть приложение Node.js, написанное и работающее.


Сначала вы должны подготовить базу данных Redis. Мне нравится использовать Redis To Go , потому что установка очень быстрая, и если вы используете Heroku, есть дополнение (хотя вашей учетной записи должна быть назначена кредитная карта). Существует также Redis Cloud , который включает больше хранилища и резервных копий.

Оттуда настройка Heroku довольно проста: выберите надстройку на странице дополнений Heroku и выберите Redis Cloud или Redis To Go или используйте одну из следующих команд (обратите внимание, что первая для Redis To Go и второй для Redis Cloud):

1
2
$ heroku addons:add redistogo
$ heroku addons:add rediscloud

На этом этапе мы должны добавить требуемый модуль Node в файл package.json . Мы будем использовать рекомендуемый модуль node_redis . Добавьте эту строку в ваш файл package.json , в разделе зависимостей:

1
«node_redis»: «0.11.x»

Если вы хотите, вы также можете включить hiredis , высокопроизводительную библиотеку, написанную на C, которую node_redis будет использовать, если она доступна:

1
«hiredis»: «0.1.x»

В зависимости от того, как вы создали свою базу данных Redis и какой провайдер PaaS вы используете, настройки подключения могут выглядеть немного иначе. Вам нужен host , port , username и password для вашего соединения.

Heroku хранит все в переменных конфигурации в виде URL. Вы должны извлечь необходимую информацию из них с помощью модуля url узла (config var для Redis To Go — это process.env.REDISTOGO_URL а для Redis Cloud — process.env.REDISCLOUD_URL ). Этот код идет вверху вашего основного файла приложения:

1
2
3
4
5
6
7
var redis = require(‘redis’);
var url = require(‘url’);
 
var redisURL = url.parse(YOUR_CONFIG_VAR_HERE);
var client = redis.createClient(redisURL.host, redisURL.port);
 
client.auth(redisURL.auth.split(‘:’)[1]);

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

1
2
3
var redis = require(‘redis’);
var client = redis.createClient(YOUR_HOST, YOUR_PORT);
client.auth(YOUR_PASSWORD);

После этого мы можем начать работать над взаимодействием между экземплярами.


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

Прежде чем что-то делать, создайте другое соединение с именем client2 . Я объясню, зачем нам это нужно позже.

Давайте начнем с того, что просто отправим сообщение, которое мы начали. Это делается с помощью метода publish() клиента. Он принимает два аргумента: канал, на который мы хотим отправить сообщение, и текст сообщения:

1
client.publish(‘instances’, ‘start’);

Это все, что вам нужно, чтобы отправить сообщение. Мы можем прослушивать сообщения в обработчике событий message (обратите внимание, что мы вызываем это на нашем втором клиенте):

1
client2.on(‘message’, function (channel, message) {

Обратному вызову передаются те же аргументы, которые мы передаем методу publish() . Теперь давайте отобразим эту информацию в консоли:

1
2
3
if ((channel == ‘instances’) and (message == ‘start’))
    console.log(‘New instance started!’);
});

Последнее, что нужно сделать, это подписаться на канал, который мы будем использовать:

1
client2.subscribe(‘instances’);

Для этого мы использовали два клиента, потому что когда вы вызываете на нем subscribe() , его соединение переключается в режим подписчика . С этого момента единственными методами, которые вы можете вызывать на сервере Redis, являются SUBSCRIBE и UNSUBSCRIBE . Поэтому, если мы находимся в режиме подписчика, мы можем publish() сообщения.

Если вы хотите, вы также можете отправить сообщение при закрытии экземпляра — вы можете прослушать событие SIGTERM и отправить сообщение на тот же канал:

1
2
3
4
process.on(‘SIGTERM’, function () {
    client.publish(‘instances’, ‘stop’);
    process.exit();
});

Для обработки этого случая в обработчике message добавьте это else if там:

1
2
else if ((channel == ‘instances’) and (message == ‘stop’))
   console.log(‘Instance stopped!’);

Так это выглядит потом:

1
2
3
4
5
6
7
8
client2.on(‘message’, function (channel, message) {
 
    if ((channel == ‘instances’) and (message == ‘start’))
        console.log(‘New instance started!’);
    else if ((channel == ‘instances’) and (message == ‘stop’))
        console.log(‘Instance stopped!’);
 
});

Обратите внимание, что если вы тестируете в Windows, он не поддерживает сигнал SIGTERM .

Чтобы проверить его локально, запустите приложение несколько раз и посмотрите, что происходит в консоли. Если вы хотите проверить сообщение о завершении, не вводите команду Ctrl+C в терминале — вместо этого используйте команду kill . Обратите внимание, что это не поддерживается в Windows, поэтому вы не можете проверить это.

Во-первых, используйте команду ps чтобы проверить, какой идентификатор у вашего процесса, и направьте его в grep чтобы упростить его:

1
$ ps -aux |

Второй столбец вывода — это идентификатор, который вы ищете. Имейте в виду, что также будет строка для команды, которую вы только что выполнили. Теперь выполните команду kill используя 15 для сигнала — это SIGTERM :

1
$ kill -15 PID

PID — это идентификатор вашего процесса.


Теперь, когда вы знаете, как использовать протокол Redis Pub / Sub, вы можете выйти за рамки простого примера, представленного ранее. Вот несколько вариантов использования, которые могут быть полезны.

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

Несколько вещей для запоминания:

  • Свободных экземпляров Redis будет недостаточно: вам нужно больше памяти, чем 5 МБ / 25 МБ, которые они предоставляют.
  • Вам понадобится другое соединение для этого.

Нам понадобится модуль connect-redis . Версия зависит от версии Express, которую вы используете. Это для Express 3.x:

1
«connect-redis»: «1.4.7»

И это для Express 4.x:

1
«connect-redis»: «2.x»

Теперь создайте еще одно соединение Redis с именем client_sessions . Использование модуля снова зависит от версии Express. Для 3.x вы создаете RedisStore следующим образом:

1
var RedisStore = require(‘connect-redis’)(express)

А в 4.x вы должны передать express-session в качестве параметра:

1
2
var session = require(‘express-session’);
var RedisStore = require(‘connect-redis’)(session);

После этого настройка одинакова в обеих версиях:

1
app.use(session({ store: new RedisStore({ client: client_sessions }), secret: ‘your secret string’ }));

Как вы можете видеть, мы передаем наш клиент Redis как свойство client объекта, переданного конструктору RedisStore , а затем передаем хранилище конструктору session .

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

Допустим, у вас есть полностью отделенный экземпляр ( рабочий динамо на Heroku) для выполнения более ресурсоемкой работы, такой как сложные вычисления, обработка данных в базе данных или обмен большим количеством данных с внешней службой. Вам нужно, чтобы «нормальные» экземпляры (и, следовательно, пользователи) знали результат этой работы после ее завершения.

В зависимости от того, хотите ли вы, чтобы веб-экземпляры отправляли какие-либо данные работнику, вам потребуется одно или два соединения ( client_sub их также client_sub и client_pub на работнике). Вы также можете повторно использовать любое соединение, которое ни на что не подписывается (например, соединение, которое вы используете для сеансов Express), вместо client_pub .

Теперь, когда пользователь хочет выполнить действие, вы публикуете сообщение на канале, который зарезервирован только для этого пользователя и для этого конкретного задания:

1
2
3
// this goes into your request handler
client_pub.publish(‘JOB:USERID:JOBNAME:START’, JSON.stringify(THEDATAYOUWANTTOSEND));
client_sub.subscribe(‘JOB:USERID:JOBNAME:PROGRESS’);

Конечно, вам придется заменить USERID и JOBNAME соответствующими значениями. Вы также должны подготовить обработчик message для соединения client_sub :

01
02
03
04
05
06
07
08
09
10
client_sub.on(‘message’, function (channel, message) {
 
    var USERID = channel.split(‘:’)[1];
     
    if (message == ‘DONE’)
        client_sub.unsubscribe(channel);
     
    sockets[USERID].emit(channel, message);
 
});

Это извлекает USERID из названия канала (поэтому убедитесь, что вы не подписываетесь на каналы, не связанные с заданиями пользователя в этом соединении), и отправляет сообщение соответствующему клиенту. В зависимости от того, какую библиотеку WebSocket вы используете, будет несколько способов получить доступ к сокету по его идентификатору.

Вы можете задаться вопросом, как рабочий экземпляр может подписаться на все эти каналы. Конечно, вы не просто хотите сделать несколько циклов для всех возможных USERID и JOBNAME . Метод psubscribe() принимает шаблон в качестве аргумента, поэтому он может подписаться на все каналы JOB:* :

1
2
3
// this code goes to the worker instance
// and you call it ONCE
client_sub.psubscribe(‘JOB:*’)

Есть несколько проблем, с которыми вы можете столкнуться при использовании Pub / Sub:

  • В вашем соединении с сервером Redis отказано. Если это произойдет, убедитесь, что вы предоставили правильные параметры подключения и учетные данные, и что максимальное количество подключений не было достигнуто.
  • Ваши сообщения не доставляются. Если это происходит, проверьте, что вы подписаны на тот же канал, на который отправляете сообщения (кажется глупым, но иногда случается). Также убедитесь, что вы подключаете обработчик message перед вызовом subscribe() , и что вы вызываете метод subscribe() в одном экземпляре, прежде чем вызывать publish() в другом.