Перейти к содержанию

Кластеризация

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

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

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

Создание Node.js кластера

Рассмотрим создание Node.js кластера на примере.

app.js

const express = require('express'),
  app = express(),
  os = require('os'),
  cluster = require('cluster')

const host = '127.0.0.1'
const port = 7000

app.use((req, res, next) => {
  if (cluster.isWorker) console.log(`Worker ${cluster.worker.id} handle request`)

  next()
})

app.get('/', (req, res) => res.send('Cluster mode.'))

if (cluster.isMaster) {
  let cpus = os.cpus().length

  for (let i = 0; i < cpus; i++) cluster.fork()

  cluster.on('exit', (worker, code) => {
    console.log(`Worker ${worker.id} finished. Exit code: ${code}`)

    app.listen(port, host, () => console.log(`Worker ${cluster.worker.id} launched`))
  })
} else app.listen(port, host, () => console.log(`Worker ${cluster.worker.id} launched`))

Для кластеризации используется встроенный в Node.js модуль cluster. При запуске сервера проверяется, является ли текущий процесс основным. Если да, то на основании данных о процессоре, полученных с помощью модуля os, запускаются дополнительные экземпляры, которые при запуске попадают в ветку else.

Каждому дополнительному экземпляру назначается уникальный идентификационный номер.

В итоге при запуске сервера Node.js в консоли должно быть приблизительно следующее (в зависимости от процессора).

Worker 2 launched
Worker 3 launched
Worker 1 launched
Worker 4 launched

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

cluster.on('exit', (worker, code) => {
  console.log(`Worker ${worker.id} finished. Exit code: ${code}`)

  app.listen(port, host, () => console.log(`Worker ${cluster.worker.id} launched`))
})

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

app.use((req, res, next) => {
  if (cluster.isWorker) console.log(`Worker ${cluster.worker.id} handle request`)

  next()
})

При выполнении GET-запроса по адресу http://127.0.0.1:7000 в консоли вы должны увидеть следующее.

Worker 4 handle request

Cluster API

Для работы с кластером Node.js модуль cluster реализовывает ряд методов, свойств и событий.

Методы и свойства модуля cluster:

  • fork() - создает новый экземпляр приложения;
  • disconnect() - отключает от кластера все созданные дополнительно экземпляры, параметром принимает callback-функцию, которая будет вызвана, когда все экземпляры будут отключены;
  • isMaster - булевое значение, если true - значит текущий процесс является основным;
  • isWorker - булевое значение, если true - значит текущий процесс является дочерним.

События модуля cluster:

fork - инициируется при создании нового экземпляра приложения, в качестве аргумента callback-функции передается созданный экземпляр;

cluster.on('fork', worker => {...});

listening - возникает в момент запуска экземпляра сервера при вызове функции listen(), параметры callback-функции - экземпляр и объект с данными адреса, на котором он запущен;

cluster.on('listening', (worker, addr) => {
  console.log(`Host: ${addr.address}, Port: ${addr.port}`)
})

online - возникает после того, как новый экземпляр посылает основному процессу сообщение в том, что он появился в кластере;

cluster.on('online', worker => {...});

message - инициируется при отправке одним из дочерних экземпляров сообщения основному процессу (сообщение посылается вызовом у дочернего процесса метода send());

cluster.on('message', (worker, message) => {...});

disconnect - инициируется при разрыве IPC канала, предназначенного для обмена данными между процессами;

cluster.on('disconnect', worker => {...});

exit - возникает, когда завершается один из дочерних процессов.

cluster.on('exit', (worker, code, signal) => {...});

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

  • disconnect() - разрывает IPC канал между основным и текущим дочерним процессами, но сам дочерний процесс не завершается, пока у него будут активные подключения, новых подключений он уже не принимает;
  • kill() - завершает дочерний экземпляр, начиная с разрыва канала IPC и заканчивая присвоением статуса завершения процесса, который передается методу kill() аргументом (по умолчанию 0);
  • send() - посылает экземпляру данные и инициирует у него событие message;
if (cluster.isMaster) {
  let worker = cluster.fork()
  worker.send({ message: 'Data' })
}
  • isConnected() - возвращает true, если дочерний процесс связан с главным процессом IPC каналом;
  • isDead() - возвращает true, если экземпляр уже завершен
  • id - уникальный идентификатор процесса;
  • process - экземпляр созданного дочернего процесса;
  • exitedAfterDisconnect - булевое значение, если true, то экземпляр был завершен вызовом kill() или disconnect().

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