Алгоритм bcrypt относится к классу вычислительно дорогих хеш-функций. Его основная цель — намеренно замедлить вычисление хеша, чтобы усложнить подбор паролей методом перебора. В JavaScript-реализации bcrypt.js это достигается полностью в рамках интерпретируемого кода, что делает операции ещё более ресурсоёмкими по сравнению с нативными биндингами.
Каждый вызов генерации соли и вычисления хеша выполняется синхронно внутри одного потока выполнения JavaScript. Это означает прямое влияние на event loop: пока выполняется bcrypt-операция, цикл обработки событий блокируется, а сервер перестаёт обрабатывать другие запросы.
Ключевая особенность:
Зависания в контексте bcrypt.js не являются ошибкой в классическом смысле — это предсказуемое поведение CPU-bound задач в однопоточном рантайме Node.js.
Основные факторы:
1. Высокий cost factor (rounds) Каждое увеличение параметра rounds увеличивает количество итераций алгоритма. Разница между 10 и 14 может быть кратной по времени выполнения.
2. Пиковая нагрузка на API При одновременной регистрации или авторизации большого числа пользователей event loop блокируется множеством параллельных хеширований.
3. Синхронное использование API bcrypt.js предоставляет синхронные методы, которые полностью блокируют поток до завершения операции.
Важно учитывать, что тайм-ауты на уровне JavaScript не могут прервать выполнение уже запущенной CPU-bound операции.
Пример распространённой ошибки:
setTimeout(() => {
// попытка "остановить" bcrypt
}, 1000);
Если bcrypt уже выполняется, этот таймер не имеет возможности его прервать. Event loop занят, и callback выполнится только после завершения вычислений.
Даже конструкции вида Promise.race() не обеспечивают
реальной остановки вычисления:
Promise.race([
bcrypt.hash(password, 12),
timeoutPromise(1000)
]);
Хеширование продолжит выполняться в фоне текущего потока.
Использование Promise.race часто применяется как попытка
реализовать тайм-аут операций. Однако в случае bcrypt.js это создаёт
лишь логическую отмену результата, но не останавливает сам процесс.
Побочные эффекты:
Фактически это механизм контроля ответа, но не управления выполнением.
Наиболее корректный способ защиты от зависаний — вынесение bcrypt-операций в отдельные потоки.
Node.js предоставляет worker_threads, позволяющие
выполнять CPU-heavy задачи вне основного event loop.
Пример архитектуры:
// worker.js
const { parentPort } = require('worker_threads');
const bcrypt = require('bcryptjs');
parentPort.on('message', async (data) => {
const hash = await bcrypt.hash(data.password, data.rounds);
parentPort.postMessage(hash);
});
Основной поток:
const { Worker } = require('worker_threads');
function hashPassword(password) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
worker.postMessage({ password, rounds: 12 });
worker.on('message', resolve);
worker.on('error', reject);
setTimeout(() => {
worker.terminate();
reject(new Error('bcrypt timeout'));
}, 2000);
});
}
Преимущества:
При высокой нагрузке запуск множества bcrypt-задач одновременно приводит к деградации производительности. Решением становится очередь задач с ограничением параллельных вычислений.
Типовая модель:
Пример логики:
const PQueue = require('p-queue');
const bcrypt = require('bcryptjs');
const queue = new PQueue({ concurrency: 2 });
function safeHash(password) {
return queue.add(() => bcrypt.hash(password, 12));
}
Эта модель предотвращает:
Дополнительный уровень защиты реализуется через тайм-ауты самого сервера.
app.use((req, res, next) => {
req.setTimeout(2000);
res.setTimeout(2000);
next();
});
server.setTimeout(2000);
Ограничение на уровне HTTP не прерывает bcrypt напрямую, но позволяет:
Даже если клиент отключился или истёк HTTP timeout, bcrypt продолжает выполнение, если он запущен в основном потоке.
Это ключевая проблема:
Именно поэтому одного HTTP timeout недостаточно.
Выбор параметра rounds напрямую влияет на вероятность зависаний.
Практические наблюдения:
Оптимизация зависит от:
При работе с bcrypt.js важна комплексная защита системы:
1. Rate limiting Ограничение количества запросов на регистрацию/логин.
2. Backpressure Принудительное замедление входящего потока запросов при перегрузке очереди.
3. Изоляция CPU-bound задач bcrypt должен быть отделён от основного request pipeline.
4. Мониторинг event loop lag Рост задержек event loop указывает на блокировки:
Типовая устойчивая архитектура включает несколько уровней:
Такой подход позволяет устранить основную проблему bcrypt.js — блокировку event loop при интенсивных вычислениях — без отказа от самого алгоритма хеширования.