СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Тонкости node.js. ч.3 Отказоустойчивые приложения в черновиках

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

И так, чаще всего при разработке масштабируемых отказоустойчивых приложений используется модуль cluster. Так разработчику не приходится заботиться о конкурентности и писать свою систему балансировки, легко управлять количеством процессов и в случае падения одного из них, в целом приложение продолжает работать. Но решает ли это проблему отказоустойчивости? Не совсем! Потому что, если у вас в каждом из процессов есть сбойный модуль, он будет валить весь процесс вне зависимости работают ли исправно другие модули. Как известно способов отловить ошибку в nodejs почти нет: домены не в счет, а промисы еще только осваиваются как инструмент.



Решить проблему поможет изоляция отдельных компонентов приложения. В этом нам поможет метод child_process.fork, который, как и cluster.fork является частным случаем child_process.spawn, но с нюансами! Метод child_process.fork позволяет создать процесс с указанным модулем и при этом создает канал для общения между процессами. Этот канал позволяет передавать JSON-объекты с помощью process.send. Вот так:

// ---- main.js ----
var fork = require('child_process').fork;
// Создаем дочерний процесс
var submodule = fork('./submodule.js');

// Добавляем обработчик событий для обмена сообщениями
submodule.on('message', function(msg) {
    console.log('main', msg);
    submodule.send(msg);
});



// ---- submodule.js ----
process.on('message', function(msg){
    console.log('submodule', msg);
});

// Отправляем сообщение родительскому процессу
process.send('hi');


Это пример простого echo-сервиса не делающего ничего, кроме пересылки сообщения обратно отправителю. Очевидно, что этого не достаточно и вам понадобится некая система запрос-ответ, чтобы наладить полноценное взаимодействие. Давайте рассмотрим один из вариантов такой системы:

// ---- main.js ----
var fork = require('child_process').fork;

var submodule = fork('./submodule.js');

var stack = {};
var ts = Date.now();
var id = 0;

submodule.on('message', function(msg) {
    if (msg.event !== 'result') return;

    if (msg.id in stack == false) {
        throw new Error('Message id not found');
    }

    var cb = stack[msg.id];
    delete stack[msg.id];
    cb.call(msg.args);
});

function sendCall(method, args, callback) {
    // Обновляем счетчик с привязкой ко времени, чтобы избежать переполнения целочисленного id.
    var ts1 = Date.now();
    if (ts1 < ts) {
        ts = ts1;
        id = 0;
    }

    // Генерируем сам id
    ++id;
    var messageId = ts + '.' + id;

    // Отправляем запрос
    submodule.send({
        id : messageId,
        event : 'method',
        method : method,
        args : args
    });
}



// ---- submodule.js ----
var api = {
    hello : function(name, callback) {
        callback(null, 'Hello, ' + name + '!');
    }
};

process.on('message', function(msg){
    if (msg.event !== 'method') retrun;
    if (event.method in api === false) {
        process.send({
            id : msg.id,
            args : [new Error('Method not found')]
        });
    } else {
        // Вызваем метод с колбеком
        var cb = function(){
            process.send({
                id : msg.id,
                event : 'result',
                args : arguments
            });
        };

        try {
            api[msg.method].apply(api, msg.args.concat(cb));
        } catch (err) {
            cb(err);
        }
    }
});


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

submodule.hello('%username%', function(err, msg) {
    console.log(msg) //-> Hello, %username%!
})


Для реализации механизма взаимодействия, так же можно использовать готовые решения, такие как node-jsonrpc-tcp или zerorpc-node.

Чем еще может нам помочь изоляция модулей? Мы можем:
  • Отправить в главный модуль сообщение об ошибке полученное с событием uncaughtException дочернего модуля. Это поможет вам лучше понимать из-за чего приложение перестало работать.
  • Отлавливать вывод модуля для обработки и раздельного логирования (см. options.stdio).
  • Перезагружать приложение по частям для hot-reload
  • Собирать статистику для мониторинга нагрузки.
  • А самое главное – создать приложение с распределенной архитектурой, когда каждый модуль может работать не просто в отдельном процессе, но вообще на другом сервере.


Cтоит упомянуть, что использование метода process.send не желательно для больших данных, так как является блокирующим. В замен рекомендуется передать tcp-соединение в дочерний процесс вторым аргументом через тот же метод process.send. Так можно будет отправлять большие сообщения и файлы.

Как видите, мы получили мощный и гибкий инструмент для создания отказоустойчивых приложений, написав всего пару десятков строк! Советую самостоятельно изучить все возможности модуля child_process, чтобы понять все на собственном опыте.

комментарии (2)

+1
rozhik ,  
Это всё конечно хорошо, в некоторых случаях (когда у нас не много тяжелых серверов) даже достаточно эффективно. Но:
1. Если нужен fork — это тонкий намёк на то, что возможно nodejs не лучшее решение.
2. В ноде, как указано у Вас, есть и другие средства IPC.

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

fork не уникальная особенность. Не совсем понятно определение «отказоустойчивые приложения», ибо если рассматривать классические определения — то решается лишь часть задач. Если важно «в несколько строк» — так есть более короткие решения. Если Вы о удобстве управления — так опять это вариант, не всегда оптимальный.

Я совершенно не хочу сказать что статья «отстой». Я просто попросил бы указать в каких именно случаях предлагаемые решения эффективны, и особенно был бы полезен живой пример задачи.
–2
rumkin ,  
> Если нужен fork — это тонкий намёк на то, что возможно nodejs не лучшее решение.
Все зависит от цели, для создания отказоустойчивой работы fork – одно из лучших решений.

> есть и другие средства IPC.
Если вы про cluster, то он имеет ряд ораничений, о которых я написал выше.

> fork не уникальная особенность.
Уникальным является сочетание fork+send и наличие подобного инструментария в стандартной библиотеке.