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

Посмотрите видеоверсию этой статьи ниже:

Дочерние процессы, кластеризация и рабочие потоки

Долгое время Node мог быть многопоточным, используя Дочерние процессы, Кластеризацию или более поздний предпочтительный метод модуля под названием Рабочие потоки.

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

Кластеризация, которая является стабильным выпуском примерно с версии 4, позволяет нам упростить создание дочерних процессов и управление ими. Прекрасно работает в сочетании с PM2.

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

1. Многопоточность уже существует для задач ввода-вывода

Существует слой Node, который уже является многопоточным, и это пул потоков libuv. Задачи ввода-вывода, такие как управление файлами и папками, транзакции TCP / UDP, сжатие и шифрование, передаются libuv и, если они не асинхронны по своей природе, обрабатываются в пуле потоков libuv.

2. Дочерние процессы / рабочие потоки работают только для синхронной логики JavaScript.

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

3. Создать одну ветку очень просто. Сложно динамически управлять несколькими потоками

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

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

Рабочий пул

Модуль, с которым мы будем работать сегодня, называется Worker Pool. Worker Pool, созданный Jos de Jong, предлагает простой способ создать пул рабочих как для динамической разгрузки вычислений, так и для управления пулом выделенных рабочих. По сути, это менеджер пула потоков для Node JS, поддерживающий рабочие потоки, дочерние процессы и веб-рабочие процессы для реализаций на основе браузера.

Чтобы использовать модуль Worker Pool в нашем приложении, необходимо выполнить следующие задачи:

  • Установить пул рабочих

Сначала нам нужно установить модуль Worker Pool - npm install workerpool

  • Инициализация пула рабочих

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

  • Создать уровень ПО промежуточного слоя

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

  • Обновить существующую логику

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

Управление несколькими потоками с помощью рабочего пула

На данный момент у вас есть 2 варианта: использовать собственное приложение NodeJS (и установить модули workerpool и bcryptjs) или загрузить исходный код с GitHub для этого руководства и моей серии видеороликов Оптимизация производительности NodeJS.

В последнем случае файлы для этого руководства будут находиться в папке 06-многопоточность. После загрузки войдите в корневую папку проекта и запустите npm install. После этого войдите в папку 06-многопоточность и следуйте инструкциям.

В папке worker-pool у нас есть 2 файла: один - логика контроллера для Worker Pool (controller.js). Другой содержит функции, которые будут запускаться потоками… он же промежуточный уровень, о котором я упоминал ранее (thread-functions.js).

рабочий пул / controller.js

'use strict'

const WorkerPool = require('workerpool')
const Path = require('path')

let poolProxy = null

// FUNCTIONS
const init = async (options) => {
  const pool = WorkerPool.pool(Path.join(__dirname, './thread-functions.js'), options)
  poolProxy = await pool.proxy()
  console.log(`Worker Threads Enabled - Min Workers: ${pool.minWorkers} - Max Workers: ${pool.maxWorkers} - Worker Type: ${pool.workerType}`)
}

const get = () => {
  return poolProxy
}

// EXPORTS
exports.init = init
exports.get = get

В controller.js нам нужен модуль workerpool. У нас также есть две экспортируемые функции: init и get. Функция init будет выполнена один раз во время загрузки нашего приложения. Он создает экземпляр Worker Pool с параметрами, которые мы предоставим, и ссылкой на thread-functions.js. Он также создает прокси-сервер, который будет храниться в памяти, пока работает наше приложение. Функция get просто возвращает прокси в памяти.

рабочий пул / поток-functions.js

'use strict'

const WorkerPool = require('workerpool')
const Utilities = require('../2-utilities')

// MIDDLEWARE FUNCTIONS
const bcryptHash = (password) => {
  return Utilities.bcryptHash(password)
}

// CREATE WORKERS
WorkerPool.worker({
  bcryptHash
})

В файле thread-functions.js мы создаем рабочие функции, которыми будет управлять Worker Pool. В нашем примере мы будем использовать BcryptJS для хеширования паролей. Обычно это занимает около 10 миллисекунд, в зависимости от скорости машины, и является хорошим вариантом, когда речь идет о тяжелых задачах. В файле utilities.js есть функция и логика, которые хешируют пароль. Все, что мы делаем в потоковых функциях, - это выполняем этот bcryptHash через функцию workerpool. Это позволяет нам сохранять код централизованным и избегать дублирования или путаницы при выполнении определенных операций.

2-utilities.js

'use strict'

const BCrypt = require('bcryptjs')

const bcryptHash = async (password) => {
  return await BCrypt.hash(password, 8)
}

exports.bcryptHash = bcryptHash

.env

NODE_ENV="production"
PORT=6000
WORKER_POOL_ENABLED="1"

Файл .env содержит номер порта и устанавливает для переменной NODE_ENV значение «production». Здесь также мы указываем, хотим ли мы включить или отключить рабочий пул, установив для WORKER_POOL_ENABLED значение «1» или «0».

1-app.js

'use strict'

require('dotenv').config()

const Express = require('express')
const App = Express()
const HTTP = require('http')
const Utilities = require('./2-utilities')
const WorkerCon = require('./worker-pool/controller')

// Router Setup
App.get('/bcrypt', async (req, res) => {
  const password = 'This is a long password'
  let result = null
  let workerPool = null

  if (process.env.WORKER_POOL_ENABLED === '1') {
    workerPool = WorkerCon.get()
    result = await workerPool.bcryptHash(password)
  } else {
    result = await Utilities.bcryptHash(password)
  }

  res.send(result)
})

// Server Setup
const port = process.env.PORT
const server = HTTP.createServer(App)

;(async () => {
  // Init Worker Pool
  if (process.env.WORKER_POOL_ENABLED === '1') {
    const options = { minWorkers: 'max' }
    await WorkerCon.init(options)
  }

  // Start Server
  server.listen(port, () => {
    console.log('NodeJS Performance Optimizations listening on: ', port)
  })
})()

Наконец, наш 1-app.js содержит код, который будет выполняться при запуске нашего приложения. Сначала мы инициализируем переменные в файле .env. Затем мы настраиваем Экспресс-сервер и создаем маршрут под названием / bcrypt. Когда этот маршрут запускается, мы проверим, включен ли рабочий пул. Если да, мы получаем дескриптор прокси-сервера Worker Pool и выполняем функцию bcryptHash, которую мы объявили в файле thread-functions.js. Это, в свою очередь, выполнит функцию bcryptHash в Утилитах и вернет нам результат. Если рабочий пул отключен, мы просто выполняем функцию bcryptHash непосредственно в служебных программах.

Внизу нашего 1-app.js вы увидите, что у нас есть функция самовызова. Мы делаем это для поддержки async / await, которые мы используем при взаимодействии с Worker Pool. Здесь мы инициализируем пул рабочих, если он включен. Единственная конфигурация, которую мы хотим переопределить, - это установить для minWorkers значение «max». Это гарантирует, что рабочий пул будет порождать столько потоков, сколько логических ядер на нашей машине, за исключением одного логического ядра, которое используется для нашего основного потока. В моем случае у меня 6 физических ядер с гиперпоточностью, то есть у меня 12 логических ядер. Таким образом, если для параметра minWorkers установлено значение «max», рабочий пул будет создавать 11 потоков и управлять ими. Наконец, последний фрагмент кода - это то место, где мы запускаем наш сервер и слушаем порт 6000.

Тестирование рабочего пула

Тестировать рабочий пул так же просто, как запустить приложение и, пока оно работает, предварительно сформировать запрос на получение для http://localhost:6000/bcrypt. Если у вас есть инструмент для нагрузочного тестирования, такой как AutoCannon, вы можете немного повеселиться, увидев разницу в производительности при включении / отключении Worker Pool. AutoCannon очень проста в использовании.

Вывод

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

До следующего раза, ура :)