CPU-bound природа bcrypt и её последствия для сервера

bcrypt — это алгоритм хеширования паролей, построенный вокруг вычислительно тяжёлой функции Blowfish. В JavaScript-реализации bcrypt.js эта тяжесть становится особенно заметной, поскольку вся криптографическая работа выполняется в пользовательском пространстве и полностью ложится на CPU процесса Node.js.

Ключевая особенность bcrypt заключается в его намеренно высокой вычислительной стоимости. Алгоритм включает многократное расширение ключа (key expansion) и повторяющиеся раунды шифрования, причём количество итераций регулируется параметром cost factor (work factor). Рост этого параметра экспоненциально увеличивает время вычисления хеша.

T = 2^{cost} t_0

где (T) — время вычисления, а (t_0) — базовая стоимость одного цикла.

Именно эта экспоненциальная зависимость и делает bcrypt CPU-bound задачей: время работы определяется не вводом/выводом, а чистой вычислительной нагрузкой на процессор.

В случае bcrypt.js ситуация усиливается тем, что реализация написана на чистом JavaScript без нативных криптографических расширений. Это означает:

  • отсутствие SIMD-оптимизаций уровня C/C++
  • отсутствие аппаратного ускорения
  • выполнение всех раундов шифрования в одном потоке Node.js

Влияние на event loop Node.js

Node.js построен вокруг однопоточного event loop. Любая CPU-bound операция блокирует этот цикл до завершения вычислений. bcrypt-хеширование — типичный пример такой операции.

При вызове:

bcrypt.hash(password, 12, (err, hash) => {
  // callback
});

даже асинхронный интерфейс не избавляет от вычислительной нагрузки. Асинхронность здесь означает лишь неблокирующий API с точки зрения JavaScript-кода, но сама операция выполняется в пуле потоков libuv или в event loop-подобной модели исполнения, конкурируя за CPU.

При высокой нагрузке это приводит к:

  • увеличению latency всех запросов
  • росту очереди событий (event loop lag)
  • деградации throughput сервера

Особенно критично это в API-сервисах с частыми операциями логина или регистрации.

Почему bcrypt.js тяжелее нативных реализаций

Существует нативный пакет bcrypt (C++ binding), который использует оптимизированные реализации OpenBSD bcrypt. В отличие от него bcrypt.js:

  • полностью интерпретируется JavaScript-движком V8
  • не использует системные криптографические библиотеки
  • не имеет прямого доступа к низкоуровневым инструкциям CPU

Это приводит к тому, что при одинаковом cost factor:

  • нативная реализация выполняется существенно быстрее
  • bcrypt.js потребляет больше CPU-времени на ту же операцию
  • нагрузка на event loop выше

Последствия для серверной архитектуры

1. Деградация отклика API

Каждый запрос на хеширование пароля конкурирует за CPU. При высокой частоте регистраций или логинов сервер начинает «проседать» по задержкам даже в unrelated endpoints.

Типичная картина:

  • /login → 200–400 ms при нормальной нагрузке
  • при росте трафика → 1–3 секунды и выше

2. Уязвимость к CPU exhaustion

bcrypt часто становится точкой атаки. Массовые запросы на:

  • регистрацию
  • сброс пароля
  • логин с неверными данными

создают искусственную нагрузку на CPU. Поскольку операция дорогая, даже небольшой поток запросов способен:

  • загрузить CPU на 100%
  • заблокировать обработку остальных запросов

3. Неравномерное распределение нагрузки

В многопользовательских системах bcrypt создаёт дисбаланс:

  • лёгкие запросы (GET /profile) обрабатываются быстро
  • тяжёлые (POST /login) вытесняют их из очереди CPU

Это приводит к эффекту «голодания» менее ресурсоёмких операций.

4. Ограничение масштабирования через event loop

Вертикальное масштабирование (увеличение CPU) помогает лишь частично, поскольку:

  • рост cost factor быстро съедает дополнительные ресурсы
  • масштабирование становится линейно-дорогим

Горизонтальное масштабирование требует балансировки, но не решает проблему CPU-bound характера самой операции.

Влияние cost factor на производительность

Cost factor — главный рычаг безопасности и одновременно главный источник нагрузки.

T ^{c}

где (c) — cost factor.

Практическая интерпретация:

  • cost 8 → быстро, слабая защита
  • cost 10–12 → баланс
  • cost 14+ → высокая нагрузка на CPU

Разница между соседними значениями не линейная, а экспоненциальная, поэтому увеличение на +1 может удвоить или утроить время вычисления.

Поведение под нагрузкой в реальных системах

В production-среде с bcrypt.js наблюдаются типичные эффекты:

  • рост очереди задач в libuv thread pool
  • увеличение GC pressure из-за долгоживущих задач
  • деградация WebSocket и real-time соединений
  • скачкообразный рост latency при пиках логинов

Особенно выражено это в системах без выделенного worker pool для криптографических операций.

Архитектурные подходы к снижению ущерба

Использование bcrypt в Node.js требует компенсации CPU-bound природы:

  • вынос хеширования в worker threads или отдельный сервис
  • ограничение rate of login/register endpoints
  • кэширование результатов проверки (где допустимо)
  • использование очередей задач (job queue) для контроля нагрузки
  • разделение API и auth-сервиса по разным процессам

В кластерах Node.js (cluster module) проблема не исчезает полностью, но распределяется между процессами, уменьшая давление на один event loop.

Сравнение с альтернативами с точки зрения CPU нагрузки

bcrypt не является единственным алгоритмом хеширования, но он один из самых дорогих по CPU:

  • bcrypt — высокая CPU стоимость, регулируемая
  • scrypt — ещё более тяжёлый и memory-hard
  • argon2 — адаптивный, требует CPU и памяти

bcrypt.js при этом остаётся наиболее «чисто CPU-bound» в рамках JavaScript, так как полностью лишён нативной оптимизации.

Практические последствия выбора bcrypt.js

Использование bcrypt.js в серверных приложениях напрямую влияет на:

  • максимальную пропускную способность auth-сервиса
  • устойчивость к пиковым нагрузкам
  • задержки в пользовательских сценариях входа
  • стоимость масштабирования инфраструктуры

При высоконагруженных системах именно CPU-bound природа bcrypt становится ограничивающим фактором архитектуры, а не второстепенной деталью реализации.