Хеширование паролей является одной из самых ресурсоёмких операций в серверных приложениях на Node.js, особенно при использовании алгоритмов с высокой стоимостью вычислений, таких как bcrypt, scrypt или Argon2. При увеличении количества одновременных запросов выполнение этих операций в основном потоке приводит к деградации производительности: блокируется event loop, увеличивается время отклика API и снижается пропускная способность системы.
Архитектурно правильным решением становится вынос криптографических вычислений в отдельные потоки исполнения с использованием Worker Threads. Такой подход позволяет изолировать CPU-bound операции и сохранить отзывчивость основного потока.
Алгоритмы хеширования паролей специально проектируются как вычислительно дорогие. Их цель — усложнить подбор пароля методом перебора. Это достигается за счёт:
Например, bcrypt с cost 12 выполняет тысячи итераций на каждый вызов, а Argon2 может дополнительно использовать сотни мегабайт памяти.
В Node.js такие операции выполняются синхронно или через libuv thread pool. Однако стандартный пул потоков ограничен (по умолчанию 4 потока), и при высокой нагрузке он становится узким местом. В результате запросы на регистрацию и аутентификацию начинают конкурировать за ресурсы.
Многие криптографические функции Node.js (например,
crypto.pbkdf2, crypto.scrypt) используют libuv
thread pool. Однако:
UV_THREADPOOL_SIZE,При росте нагрузки хеширование начинает блокировать другие подсистемы, включая файловые операции и сетевые запросы.
Модуль worker_threads предоставляет возможность
создавать полноценные потоки исполнения JavaScript, работающие
параллельно с основным процессом.
Каждый worker:
Это делает его подходящим для CPU-intensive задач, таких как хеширование паролей.
Типичная архитектура включает:
Схематически:
HTTP Request → Main Thread → Worker Pool → Hash Function → Worker Response → Main Thread → HTTP Response
Файл worker-а, отвечающий за вычисление хеша, изолирует криптографическую логику:
// hash.worker.js
const { parentPort } = require('worker_threads');
const bcrypt = require('bcrypt');
parentPort.on('message', async (data) => {
const { password, cost } = data;
try {
const hash = await bcrypt.hash(password, cost);
parentPort.postMessage({ hash });
} catch (err) {
parentPort.postMessage({ error: err.message });
}
});
Worker получает данные, выполняет вычисление и возвращает результат
через postMessage.
Создание одного worker на каждый запрос неэффективно. Используется пул потоков, переиспользующий уже созданные экземпляры.
Простейшая реализация менеджера:
const { Worker } = require('worker_threads');
class HashWorkerPool {
constructor(size, workerFile) {
this.size = size;
this.workerFile = workerFile;
this.workers = [];
this.queue = [];
this.active = new Map();
for (let i = 0; i < size; i++) {
this.workers.push(this.createWorker());
}
}
createWorker() {
const worker = new Worker(this.workerFile);
worker.on('message', (result) => {
const callback = this.active.get(worker);
this.active.delete(worker);
callback.resolve(result);
this.release(worker);
});
worker.on('error', (err) => {
const callback = this.active.get(worker);
this.active.delete(worker);
callback.reject(err);
this.release(worker);
});
return worker;
}
execute(data) {
return new Promise((resolve, reject) => {
const worker = this.workers.pop();
if (!worker) {
this.queue.push({ data, resolve, reject });
return;
}
this.active.set(worker, { resolve, reject });
worker.postMessage(data);
});
}
release(worker) {
if (this.queue.length > 0) {
const task = this.queue.shift();
this.active.set(worker, task);
worker.postMessage(task.data);
} else {
this.workers.push(worker);
}
}
}
module.exports = HashWorkerPool;
Такая модель обеспечивает переиспользование потоков и контроль над конкурентным выполнением задач.
При использовании библиотеки Password-hash (или аналогичного слоя абстракции над bcrypt/argon2) основная задача заключается в делегировании вычисления хеша worker-у.
Сервисный слой:
const HashWorkerPool = require('./hashPool');
const path = require('path');
const pool = new HashWorkerPool(
4,
path.resolve(__dirname, 'hash.worker.js')
);
async function hashPassword(password) {
const result = await pool.execute({
password,
cost: 12
});
if (result.error) {
throw new Error(result.error);
}
return result.hash;
}
При высокой нагрузке worker pool начинает аккумулировать задачи. Важным аспектом становится управление очередью:
Пример ограничения:
if (this.queue.length > 1000) {
throw new Error('Queue overflow');
}
Передача данных в Worker Threads осуществляется через структурированное клонирование. Это означает:
Для паролей нагрузка минимальна, но при расширении системы (например, добавлении соли, метаданных пользователя) стоит учитывать стоимость сериализации.
Использование worker threads требует балансировки:
Дополнительно можно использовать:
Worker может завершиться аварийно. Поэтому необходим контроль:
exit,Пример восстановления:
worker.on('exit', (code) => {
if (code !== 0) {
this.workers.push(this.createWorker());
}
});
Вынос хеширования в Worker Threads изменяет поведение системы:
При этом система становится предсказуемой под нагрузкой, так как CPU-bound операции перестают блокировать основной поток.
Такая модель легко расширяется:
Worker Threads становятся промежуточным уровнем между синхронной криптографией и распределённой системой обработки задач.