Нагрузочное тестирование и влияние rounds на throughput

Модель работы bcrypt.js и стоимость вычислений

bcrypt.js реализует адаптивную криптографическую функцию хеширования, основанную на Blowfish. Ключевая характеристика алгоритма — параметр cost factor, чаще называемый rounds или saltRounds. Он определяет количество итераций экспоненциального расширения ключа.

Каждое увеличение rounds на единицу увеличивает вычислительную сложность примерно в 2 раза. Это означает нелинейное падение производительности при росте значения параметра.

Формально зависимость времени вычисления можно описать как:

T ^{r}

где r — значение cost factor (rounds), T — время хеширования.

Эта экспоненциальная зависимость является ключевым фактором при проектировании систем аутентификации под нагрузкой.


Архитектурные особенности bcrypt.js в Node.js

bcrypt.js является чистой JavaScript-реализацией алгоритма, без использования нативных C++ биндингов. Это приводит к следующим характеристикам:

  • полная совместимость с любыми JS-окружениями
  • отсутствие зависимости от нативных сборок
  • существенно более низкая производительность по сравнению с bcrypt (native)

В контексте нагрузочного тестирования это критично: CPU-bound операции становятся основным ограничением throughput.

Хеширование выполняется синхронно или асинхронно через event loop с использованием очереди задач, что влияет на latency под высокой нагрузкой.


Throughput и его деградация при росте rounds

Throughput системы аутентификации определяется количеством операций хеширования в единицу времени:

=

где:

  • N — количество операций bcrypt.hash
  • t — время выполнения

При увеличении rounds время t растёт экспоненциально, что приводит к резкому снижению throughput.

Типичная зависимость:

rounds относительное время относительный throughput
8 100%
10 25%
12 16× 6.25%
14 64× ~1.5%

Эти значения демонстрируют не линейное, а степенное ухудшение производительности.


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

При увеличении параллельных запросов к bcrypt.hash в Node.js наблюдаются следующие эффекты:

1. Рост latency

Каждая операция блокирует CPU на длительное время. Даже при использовании асинхронного API event loop получает значительную нагрузку.

2. Очереди задач

Асинхронные вызовы bcrypt.js формируют очередь, в которой операции выполняются последовательно на уровне потоков libuv.

3. Saturation CPU

При высоких rounds (12–14) CPU быстро достигает 100% загрузки даже при умеренном количестве запросов.

4. Эффект деградации API

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


Методика нагрузочного тестирования bcrypt.js

Нагрузочное тестирование должно учитывать следующие параметры:

  • rounds (cost factor)
  • количество параллельных запросов
  • тип API (sync/async)
  • размер входных данных (password length)
  • распределение нагрузки во времени

Базовый подход — фиксирование throughput при разных значениях rounds.

Пример тестового сценария на Node.js:

import bcrypt fr om "bcryptjs";

async function runBenchmark(rounds, iterations) {
  const password = "test_password_123";

  const start = process.hrtime.bigint();

  for (let i = 0; i < iterations; i++) {
    await bcrypt.hash(password, rounds);
  }

  const end = process.hrtime.bigint();

  const durationMs = Number(end - start) / 1e6;

  return {
    rounds,
    iterations,
    durationMs,
    opsPerSecond: (iterations / durationMs) * 1000
  };
}

(async () => {
  console.log(await runBenchmark(8, 50));
  console.log(await runBenchmark(10, 50));
  console.log(await runBenchmark(12, 50));
})();

Синхронный и асинхронный режим под нагрузкой

bcrypt.js предоставляет два основных режима:

Асинхронный режим

  • использует event loop
  • не блокирует поток исполнения
  • под высокой нагрузкой создаёт очередь задач

Синхронный режим

  • полностью блокирует event loop
  • приводит к деградации всего приложения
  • непригоден для серверных нагрузок

При нагрузочном тестировании синхронный режим используется только для измерения worst-case latency.


Влияние rounds на масштабируемость системы

С увеличением rounds масштабирование системы требует не только увеличения CPU, но и изменения архитектуры:

  • горизонтальное масштабирование (cluster, multiple instances)
  • вынесение bcrypt операций в отдельные worker threads
  • использование очередей задач (RabbitMQ, Redis Queue)
  • ограничение concurrency на уровне API gateway

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


Ограничение concurrency как фактор стабилизации

Один из ключевых методов стабилизации throughput — ограничение числа одновременных bcrypt операций:

import pLimit fr om "p-lim it";
import bcrypt fr om "bcryptjs";

const lim it = pLimit(5);

async function safeHash(password, rounds) {
  return lim it(() => bcrypt.hash(password, rounds));
}

Это предотвращает полное насыщение CPU и снижает вероятность collapse event loop.


Наблюдение деградации через метрики

В реальных системах используются следующие метрики:

  • average hash time (ms)
  • p95 / p99 latency
  • event loop lag
  • CPU utilization
  • queue length (если используется очередь)

При увеличении rounds наблюдается характерный профиль:

  • линейный рост latency до порога CPU saturation
  • затем экспоненциальный рост latency
  • резкое падение throughput после насыщения

Подбор rounds с точки зрения производительности

Зависимость между безопасностью и производительностью является компромиссом:

  • низкие значения (8–10) — высокая скорость, меньшая устойчивость к brute-force
  • средние (10–12) — баланс
  • высокие (12–14+) — значительное падение throughput

Оценка времени вычисления может быть представлена как:

T_{new} = T_{base} ^{(r - r_{base})}

где увеличение r даже на 1 приводит к удвоению стоимости операции.


Поведение в распределённых системах

В микросервисной архитектуре bcrypt становится узким местом:

  • auth-service становится CPU-bound
  • autoscaling реагирует с задержкой
  • увеличивается cost per request

При высокой нагрузке часто применяется стратегия:

  • предварительная валидация пароля (rate limiting)
  • кеширование неудачных попыток
  • распределение нагрузки на worker pool

Сравнение производительности bcrypt.js и native bcrypt

bcrypt.js проигрывает нативной реализации по throughput:

  • отсутствие SIMD и native оптимизаций
  • интерпретируемый характер выполнения
  • высокая нагрузка на GC при больших rounds

Это усиливает эффект деградации под нагрузочным тестированием.


Наблюдаемые закономерности при стресс-тестах

При увеличении нагрузки фиксируются следующие закономерности:

  • throughput стабилизируется на уровне CPU saturation
  • latency растёт экспоненциально после порога нагрузки
  • event loop lag становится основным ограничением
  • увеличение rounds смещает точку насыщения влево

Поведение системы приближается к модели очереди M/M/1 с ограниченной обслуживающей способностью, где service time определяется экспоненциальной функцией rounds.