
В этом посте я собираюсь показать вам, как потенциально утроить производительность вашего приложения Node за счет управления несколькими потоками. Это важный учебник, в котором показанные методы и примеры дадут вам все необходимое для настройки эффективного управления потоками.
Дочерние процессы, кластеризация и Worker Threads
В течение долгого времени Node обладал способностью быть многопоточным, используя дочерние процессы, кластеризацию, или более предпочтительный в последнее время метод модуля под названием Worker Threads.
Дочерние процессы были первоначальным средством создания нескольких потоков для вашего приложения и были доступны с версии 0.10. Это достигалось путем создания узлового процесса для каждого дополнительного потока, который вы хотели создать.
Кластеризация, которая стала стабильной примерно с версии 4, позволяет нам упростить создание и управление дочерними процессами. Она великолепно работает в сочетании с PM2.
Теперь, прежде чем мы перейдем к многопоточности нашего приложения, есть несколько моментов, которые должны быть полностью поняты:
1. Многопоточность уже существует для задач ввода/вывода
В Node есть слой, который уже является многопоточным, и это пул потоков libuv. Задачи ввода-вывода, такие как управление файлами и папками, транзакции TCP/UDP, сжатие и шифрование, передаются libuv, и если они не являются асинхронными по своей природе, то обрабатываются в пуле потоков libuv.
2. Child Processes (Дочерние процессы)/Worker Threads работают только для синхронной логики JavaScript.
Имплементация многопоточности с помощью дочерних процессов или Worker Threads будет эффективна только для синхронного кода JavaScript, выполняющего трудоемкие операции, такие как циклы, вычисления и т.д. Если вы в качестве примера попытаетесь переложить задачи ввода-вывода на Worker Threads, то не увидите улучшения производительности.
3. Создать один поток легко. Динамично управлять несколькими потоками сложно
Создать один дополнительный поток в вашем приложении достаточно просто, поскольку существует масса руководств о том, как это сделать. Однако создание потоков, эквивалентных количеству логических ядер вашей или виртуальной машины, и управление распределением работы между этими потоками является более сложной задачей, разработка такой логики достаточно трудозатратна.
Хорошо, что мы живем в мире открытого исходного кода и блестящего вклада сообщества Node. Это значит, что уже существует модуль, который даст нам полную возможность динамически создавать и управлять потоками в зависимости от доступности ЦП нашей или виртуальной машины.
Worker Pool
Модуль, с которым мы будем работать сегодня, называется Worker Pool. Созданный Джосом де Йонгом, Worker Pool предлагает простой способ создания пула воркеров для динамической разгрузки вычислений, а также для управления пулом выделенных воркеров. По сути, это менеджер пула потоков для Node JS, поддерживающий Worker Threads, дочерние процессы и Web Workers для браузерных имплементаций.
Чтобы использовать модуль Worker Pool в нашем приложении, необходимо выполнить следующие задачи:
-
Установить Worker Pool
Сначала нам нужно установить модуль Worker Pool — npm install workerpool
-
Инициализировать Worker Pool
Далее нам нужно будет инициализировать Worker Pool при запуске нашего приложения.
-
Создать Middleware Layer
Затем нам нужно будет создать Middleware Layer (промежуточный слой) между нашей сложной JavaScript-логикой и Worker Pool, который будет управлять ею.
-
Обновить существующую логику
Наконец, нам нужно обновить наше приложение, чтобы при необходимости передавать трудоемкие задачи Worker Pool.
Управление несколькими потоками с помощью Worker Pool
На данном этапе у вас есть 2 варианта: Использовать свое собственное приложение NodeJS (и установить модули workerpool и bcryptjs), или загрузить исходный код с GitHub по этому руководству и моей серии видео об оптимизации производительности NodeJS.
Если вы выберете последний вариант, файлы по данному руководству будут находиться в папке 06-multithreading. После загрузки войдите в корневую папку проекта и запустите npm install. Затем войдите в папку 06-multithreading, чтобы продолжить работу.

В папке worker-pool у нас есть 2 файла: один — это логика контроллера для Worker Pool (controller.js). Другой (файл) содержит функции, которые будут запускаться потоками… он же так называемый промежуточный слой, о котором я говорил ранее (thread-functions.js).
worker-pool/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. У нас также есть 2 экспортируемые функции, которые называются init и get. Функция init будет выполняться один раз во время загрузки нашего приложения. Она инстанцирует Worker Pool с опциями, которые мы предоставим, и ссылкой на thread-functions.js. Она также создает прокси, который будет храниться в памяти до тех пор, пока работает наше приложение. Функция get просто возвращает прокси в памяти.
worker-pool/thread-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, устанавливая 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. Затем настроим сервер Express и создадим маршрут под названием /bcrypt. При запуске этого маршрута проверим, включен ли Worker Pool. Если да, то получим управление прокси Worker Pool и выполним функцию bcryptHash, которую мы объявили в файле thread-functions.js. Она, в свою очередь, выполнит функцию bcryptHash в Utilities и вернет нам результат. Если Worker Pool отключен, тогда просто выполним функцию bcryptHash непосредственно в Utilities.
В конце нашего файла 1-app.js находится функция, вызывающая саму себя. Это сделано для поддержки async/await, которую мы используем при взаимодействии с Worker Pool. Далее инициализируем Worker Pool, если он включен. Единственная конфигурация, которую надо переопределить — установка minWorkers на «max». Это гарантирует, что Worker Pool породит столько потоков, сколько логических ядер есть на нашей машине, за исключением 1 логического ядра, которое используется для главного потока. В моем случае имеется 6 физических ядер с гиперпоточностью, это означает, что у меня в распоряжении 12 логических ядер. Поэтому при значении minWorkers равном «max», Worker Pool будет создавать и управлять 11 потоками. Наконец, последний фрагмент кода — это запуск нашего сервера и прослушивание порта 6000.
Тестирование Worker Pool
Тестировать Worker Pool очень просто: запустите приложение и во время его работы выполните запрос get на http://localhost:6000/bcrypt. Если у вас есть инструментарий нагрузочного тестирования, такой как AutoCannon, вы можете посмотреть разницу в производительности при включении/выключении Worker Pool. AutoCannon очень прост в использовании.
Заключение
Я надеюсь, что это руководство дало вам представление об управлении несколькими потоками в вашем приложении Node. Вложенное видео в начале статьи наглядно демонстрирует процесс тестирования приложения Node.
Перевод подготовлен в рамках курса «Node.js Developer». Если интересно узнать о курсе больше, регистрируйтесь на день открытых дверей.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/563920/
Добавить комментарий