Серия 3|В многопроцессной модели Node.js

Node.js внешний интерфейс сервер JavaScript
Серия 3|В многопроцессной модели Node.js

Текст: Zhenglong (веб-интерфейс-инженер Shanghai Internet School)

Оригинальная статья, воспроизведенная, пожалуйста, укажите автор и источник

Предыдущая статья "Реализация анализа в HTTP Node.js

const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
} else {
  require('http').createServer((req, res) => {
    res.end('hello world');
  }).listen(3333);
}

Обычно модель master-slave включает в себя главный процесс (master) и несколько подчиненных процессов (worker).Главный процесс отвечает за получение запросов на соединение и распределение отдельных задач запросов подчиненным процессам для обработки; ответственностью подчиненного процесса является постоянно отвечать на запросы клиентов, пока не перейдет в состояние ожидания. Как показано на рис. 3-1:

В этой статье мы надеемся прояснить несколько ключевых вопросов, связанных с этим кодом:

  1. Из процесса создания;

В Node.jscluster.forkс POSIXforkНемного отличается: хотя подчиненный процесс все же создается форком, он не использует напрямую образ процесса главного процесса, а вызывает системные функцииexecvpПусть подчиненный процесс использует новый образ процесса. Кроме того, каждый подчиненный процесс соответствует объекту Worker, который имеет следующие состояния: нет, онлайн, прослушивание, мертвый и отключенный.

Объект ChildProcess в основном обеспечивает создание процесса (spawn), уничтожение (kill) и управление счетчиком ссылок дескриптора процесса (ref и unref). Помимо инкапсуляции объекта Process (process_wrap.cc), он также сам обрабатывает некоторые детали. Например, в методе порождения, если необходимо установить конвейер IPC между ведущим и подчиненным процессами, переменная среды NODE_CHANNEL_FD используется для информирования подчиненного процесса о связанном с IPC файловом дескрипторе (fd), который должен быть связан. позже снова будет использована специальная переменная окружения.

Три отношения ссылки на объект, упомянутые выше, следующие:

Основной поток выполнения cluster.fork:

  1. Вызовите Child_Process.spawn;

  2. Создание объекта ChildProcess и инициализация его свойств _handle для объекта Process; process_wrap.cc Процесс публикуется в объект JavaScript, который инкапсулирует рабочие функции libuv процесса. Прикрепите определения объекта Process C++:

interface Process {
  construtor(const FunctionCallbackInfo<Value>& args);
  void close(const FunctionCallbackInfo<Value>& args);
  void spawn(const FunctionCallbackInfo<Value>& args);
  void kill(const FunctionCallbackInfo<Value>& args);
  void ref(const FunctionCallbackInfo<Value>& args);
  void unref(const FunctionCallbackInfo<Value>& args);
  void hasRef(const FunctionCallbackInfo<Value>& args);
}
  1. Вызвать порождение метода ChildProcess._handle и, в конечном итоге, вызвать библиотеку libuv.uv_spawn.

Когда главный процесс выполняет cluster.fork, он указывает две специальные переменные среды NODE_CHANNEL_FD и NODE_UNIQUE_ID, поэтому процесс инициализации подчиненного процесса немного отличается от общего процесса Node.js:

  1. bootstrap_node.js — это входной файл JavaScript, включенный во время выполнения, который вызывает internal\process.setupChannel;

  2. Если переменная окружения содержит NODE_CHANNEL_FD, вызовите child_process._forkChild, затем удалите это значение;

  3. Вызовите internal\child_process.setupChannel, прослушайте сообщения internalMessage о глобальном процессе дочерних объектов и добавьте методы send и _send. При этом пакет для отправки только _send, как правило, только канал записи _send JSON-сериализации после сообщения и, наконец, доставляется получателю.

  4. Если переменная среды содержит Node_unique_ID, текущий процесс представляет собой модельный работник, выполняет DECORYINIT при загрузке модуля кластера; Кроме того, он также повлияет на метод прослушивания Net.server, метод прослушивания вызовов Cluster._getserver под рабочим режимом, который по существу Инициированное сообщение к основным процессам {«ACT»: «QueryServer»}, а не настоящий порт прослушивания.

Детали реализации IPC

Как упоминалось выше, процесс Node.js master-slave поддерживает связь только через IPC, поэтому в этом разделе подробно анализируются детали реализации IPC. Во-первых, давайте посмотрим на пример кода:

1-master.js

const {spawn} = require('child_process');
let child = spawn(process.execPath, [`${__dirname}/1-slave.js`], {
  stdio: [0, 1, 2, 'ipc']
});

child.on('message', function(data) {
  console.log('received in master:');
  console.log(data);
});

child.send({
  msg: 'msg from master'
});

1-slave.js

process.on('message', function(data) {
  console.log('received in slave:');
  console.log(data);
});
process.send({
  'msg': 'message from slave'
});
node 1-master.js

Результаты приведены ниже:

socketpair

На самом деле можно использоватьsocketpairСоздайте пару полнодуплексных анонимных розетков для отправки сообщений между процессами; подпись функции выглядит следующим образом:

int socketpair(int domain, int type, int protocol, int sv[2]);

При нормальных обстоятельствах мы не можем пропустить дескрипторы файлов через розетки; когда основной процесс устанавливает соединение с клиентом, необходимо проинформировать рабский процесс дескриптора соединения для обработки. Что мне делать? На самом деле, указав первый параметр Socketpair в качестве AF_UNIX, это означает создание анонимного сокета домена Unix (сокет домена Unix), чтобы можно было использовать системные функцииsendmsgа такжеrecvmsgдля передачи/получения файловых дескрипторов.

Когда основной процесс вызывает cluster.fork, соответствующий процесс выглядит следующим образом:

  1. Создайте объект Pipe (pipe_wrap.cc) и укажите для параметра ipc значение true;
  2. Вызов uv_spawn, параметры параметров структуры uv_process_options_s, объект хранится в структурированном теле атрибута Pipe stdio;
  3. Вызовите uv__process_init_stdio, чтобы создать полнодуплексный сокет через socketpair;
  4. Вызовите uv__process_open_stream и установите значение iowatcher.fd объекта Pipe на один из полнодуплексных сокетов.

В этот момент процесс ведущий-ведомый может обмениваться данными в обоих направлениях. Блок-схема выглядит следующим образом:

Взглянем на переменную окружения node_channel_fd, что сомнительно, ее значение всегда равно 3. В описании документа уровня процесса 0-2 — это стандартный ввод stdin, стандартный вывод stdout и стандартный вывод ошибок stderr, затем первый доступный файловый дескриптор 3, SocketPair, очевидно, занимает первый доступный файл из дескриптора процесса. Таким образом, при записи данных из процесса в fd = 3, основной процесс может получить сообщение, наоборот, тоже аналогично.

Чтение сообщений от IPC в основном является потоковой операцией, которая будет подробно объяснена позже.Основные процессы перечислены ниже:

  1. Обратный вызов StreamBase::EditData при чтении;

  2. Streamprap :: onreadimpl вызов Streamwrap :: editdata;

  3. StreamWrap::ReadStart вызывает uv_read_start Pass StreamWrap::OnRead в качестве третьего параметра:

int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)

Используемые отношения диаграммы классов следующие:

Модель master-slave сервера

Выше приведен грубый анализ процесса создания и его особенностей подчиненного процесса; если мы хотим реализовать модель обслуживания ведущий-подчиненный, нам необходимо решить основную проблему: как подчиненный процесс получает дескриптор соединения с клиентом? Начнем с сигнатуры функции process.send (метод send доступен только для глобального объекта процесса подчиненного устройства, и мастер может получить к нему доступ через worker.process или worker):

void send(message, sendHandle, callback)

Параметры сообщения и его значение могут быть явными обратными вызовами, функции обратного вызова относятся соответственно после окончания сообщения объектов и операций, подлежащих передаче. Что его второй параметр sendHandle для каких целей?

Как было сказано выше, системная функция socketpair может создавать пару двусторонних сокетов, которые можно использовать для отправки сообщений JSON, в этом блоке в основном задействованы потоковые операции, кроме того, когда sendHandle имеет значение, они также могут использоваться для передачи файловые дескрипторы.Этот процесс относительно немного сложнее, но в конечном итоге он вызовет системные функции sendmsg и recvmsg.

Передать дескриптор соединения клиенту

В сервисной модели master-slave главный процесс отвечает за установление соединения с клиентом, а затем передачу дескриптора соединения черезsendmsgпередан подчиненному процессу. Давайте посмотрим на процесс:

подчиненный процесс

  1. Вызов метода http.server.listen (унаследован до net.server);

  2. Вызов cluster._getServer, инициируйте сообщение основному процессу:

{
  "cmd": "NODE_HANDLE",
  "msg": {
    "act": "queryServer"
  }
}

основной процесс

  1. При получении и обработке этого сообщения будет создан новый объект RoundRobinHandle, который является дескриптором переменной. Каждый дескриптор соответствует конечной точке соединения и соответствует нескольким экземплярам подчиненного процесса; в то же время он открывает сервисный сокет TCP, соответствующий конечной точке соединения.
class RoundRobinHandle {
  construtor(key, address, port, addressType, fd) {
    // 监听同一端点的从进程集合
    this.all = [];

    // 可用的从进程集合
    this.free = [];

    // 当前等待处理的客户端连接描述符集合
    this.handles = [];

    // 指定端点的TCP服务socket
    this.server = null;
  }
  add(worker, send) {
    // 把从进程实例加入this.all
  }
  remove(worker) {
    // 移除指定从进程
  }
  distribute(err, handle) {
    // 把连接描述符handle存入this.handles,并指派一个可用的从进程实例开始处理连接请求
  }
  handoff(worker) {
    // 从this.handles中取出一个待处理的连接描述符,并向从进程发起消息
    // {
    //  "type": "NODE_HANDLE",
    //  "msg": {
    //    "act": "newconn",
    //  }
    // }
  }
}
  1. Вызовите метод handle.add, чтобы добавить рабочий объект в коллекцию handle.all;

  2. Когда handle.server начнет прослушивать клиентские запросы, сбросьте его функцию обратного вызова onconnection на RoundRobinHandle.distribute, чтобы главному процессу не нужно было фактически обрабатывать клиентские соединения, а нужно было только распределять соединения подчиненным процессам для обработки. Он сохраняет дескриптор соединения в коллекции handle.handles и, когда рабочий процесс доступен, отправляет ему сообщение { "act": "newconn" }. Если назначенный рабочий процесс не отвечает подтверждающим сообщением { "ack": message.seq, accept: true }, он попытается назначить соединение другому рабочему процессу.

Блок-схема выглядит следующим образом:

Позвоните, чтобы послушать из процесса

Обработка клиентских соединений

Как ведомый процесс прослушивает тот же порт, что и главный процесс?

Есть две основные причины:

** I. Инициализация среды выполнения Node.js немного отличается от внутрипроцессной**

  1. Поскольку имеется переменная среды Node_unique_ID из процесса, метод DETORYINIT выполняется, когда модуль кластера загружен в bootstrap_node.js. Метод Masterinit, реализованный этим местом, и основным процессом, является: 1. В процессе нельзя создать метод Cluster. Объект Отключение и уничтожение реализации также различны.: Мы используем вызывающую рабочую. Процесс из процесса завершающий процесс затем выходит из-за того, что его третий модуль кластера добавляется из процесса. Монитор указанного адреса порта; затем сам использует аналоговый дескриптор TCP для продолжения выполнения;

  2. internalMessage

  3. Поскольку в переменной окружения процесса есть NODE_CHANNEL_FD, при вызове internal\process.setupChannel он будет подключаться к двунаправленному сокету, созданному системной функцией socketpair, и отслеживатьinternalMessage, обрабатывать типы сообщений: NODE_HANDLE_ACK и NODE_HANDLE.

** II.Метод listen выполняет немного отличающийся код в главном и подчиненном процессах. **

В методе listen для net.Server (net.js), если это ведущий процесс, выполняется стандартный процесс привязки портов, если подчиненный процесс, то будет вызываться cluster._getServer, описание метода см. выше .

Наконец, прилагается сервисная модель Master-Slave версии C, основанная на libuv,Адрес GitHub.

После запуска сервера доступаhttp://localhost:3333Результаты приведены ниже:

Я считаю, что после знакомства с этой статьей у всех появилось полное представление о кластере Node.js. В следующий раз автор поделится с вами подробным анализом доступности управления процессами Node.js в производственной среде, так что следите за обновлениями.

Серия 1 | Node.js в начало процесса анализа

Рекомендация: Самоотчет мастера переводческого проекта:

1. Галантерея|Каждый является мастером переводческих проектов

2. Обучение апплету WeChat от iKcamp состоит из 5 глав и 16 подразделов (включая видео).

3. Начать сериализацию бесплатно ~ 11 уроков iKcamp обновляются два раза в неделю | Создание практического проекта Node.js на основе Koa2 (включая видео) | Введение в план курса


В 2019 году оригинальная новая книга iKcamp «Практика разработки Koa и Node.js» была продана на JD.com, Tmall, Amazon и Dangdang!