Параллельное хеширование нескольких паролей

Алгоритм bcrypt относится к классу адаптивных функций хеширования, специально разработанных для защиты паролей. Его ключевая характеристика — высокая вычислительная стоимость, которая делает перебор паролей (brute force) крайне медленным. В библиотеке bcrypt.js, реализованной на чистом JavaScript, эта стоимость дополнительно усиливается тем, что вычисления выполняются в рамках JavaScript-движка, без нативных оптимизаций.

Параллельное хеширование нескольких паролей в таких условиях требует понимания ограничений event loop, нагрузки на CPU и особенностей асинхронной модели Node.js.

Влияние нагрузки на event loop

bcrypt.js выполняет вычисления синхронно внутри асинхронного API через внутреннюю имитацию неблокирующего поведения. Однако фактически каждый процесс хеширования остаётся CPU-интенсивной операцией.

При запуске множества хеширований одновременно возникают следующие эффекты:

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

Особенно это заметно при высокой стоимости раундов (salt rounds), например 12–15 и выше.

Базовый параллелизм через Promise.all

Самый прямолинейный способ выполнить несколько хеширований одновременно — использовать Promise.all:

import bcrypt fr om 'bcryptjs';

const passwords = [
  'password1',
  'password2',
  'password3',
  'password4'
];

const saltRounds = 12;

const hashAll = async (list) => {
  const tasks = list.map(pwd => bcrypt.hash(pwd, saltRounds));
  return await Promise.all(tasks);
};

const hashes = await hashAll(passwords);

Такой подход запускает все операции одновременно, что в малых объёмах выглядит эффективно. Однако при увеличении количества паролей возникает перегрузка CPU.

Основной недостаток:

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

Ограничение параллелизма

Более устойчивый подход — ограничение числа одновременно выполняемых хеширований. Это позволяет балансировать между скоростью и стабильностью системы.

Пример с ручным контролем очереди:

import bcrypt fr om 'bcryptjs';

const saltRounds = 12;

async function hashWithLimit(passwords, lim it = 2) {
  const results = [];
  let index = 0;

  const workers = new Array(lim it).fill(null).map(async () => {
    while (index < passwords.length) {
      const current = index++;
      const hash = await bcrypt.hash(passwords[current], saltRounds);
      results[current] = hash;
    }
  });

  await Promise.all(workers);
  return results;
}

Такой подход обеспечивает:

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

Использование готовых очередей задач

Для production-решений часто применяется библиотека p-limit, которая упрощает управление конкурентностью:

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

const lim it = pLimit(2);
const saltRounds = 12;

const passwords = ['a', 'b', 'c', 'd', 'e'];

const tasks = passwords.map(pwd =>
  limit(() => bcrypt.hash(pwd, saltRounds))
);

const results = await Promise.all(tasks);

Преимущества такого подхода:

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

Масштабирование при больших объёмах данных

При хешировании сотен или тысяч паролей важно учитывать, что bcrypt.js не предназначен для массовых batch-операций в одном потоке без ограничений.

Типичная стратегия масштабирования включает:

  • ограничение concurrency (обычно 1–4 потока)
  • разбиение входных данных на чанки
  • обработку очередями

Пример чанкинга:

function chunkArray(arr, size) {
  const chunks = [];
  for (let i = 0; i < arr.length; i += size) {
    chunks.push(arr.slice(i, i + size));
  }
  return chunks;
}

Далее обработка по блокам:

const chunks = chunkArray(passwords, 10);

for (const chunk of chunks) {
  await Promise.all(chunk.map(p => bcrypt.hash(p, saltRounds)));
}

Такой подход снижает пиковую нагрузку и делает систему устойчивой при больших объёмах данных.

Ограничения bcrypt.js в многопоточности

В отличие от нативной реализации bcrypt (например, bcrypt на C++), bcrypt.js не использует потоковую модель вычислений. Это означает:

  • нет автоматического распределения нагрузки по ядрам CPU
  • параллелизм достигается только через JavaScript-уровень
  • увеличение concurrency не всегда ускоряет выполнение

При слишком большом числе параллельных задач возникает обратный эффект: замедление из-за конкуренции за CPU и garbage collection.

Worker Threads как альтернатива

Для более тяжёлых сценариев можно вынести хеширование в отдельные потоки:

import { Worker } from 'worker_threads';

function hashInWorker(password) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./hash-worker.js', {
      workerData: password
    });

    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

Внутри worker:

import bcrypt from 'bcryptjs';
import { parentPort, workerData } from 'worker_threads';

const hash = bcrypt.hashSync(workerData, 12);
parentPort.postMessage(hash);

Такой подход:

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

Но требует дополнительных ресурсов и усложняет архитектуру.

Практическая стратегия выбора подхода

Выбор метода параллельного хеширования зависит от объёма данных:

  • до 10 паролей: Promise.all
  • 10–100: ограниченный concurrency (2–4 потока)
  • 100–1000: очереди + chunking
  • 1000+: worker threads или переход на нативный bcrypt

Баланс между безопасностью и производительностью

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

Практически важно учитывать:

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

Оптимальный режим работы bcrypt.js достигается не максимальным распараллеливанием, а контролируемой очередью задач с ограниченной конкуренцией.