Код-ревью Middle Event loop и libuv спрашивают часто обновлено

Код-ревью: что блокирует event loop

Вопрос

Под нагрузкой сервис отвечает рывками, p99 латентности в секундах, хотя CPU загружен не полностью. Найди, что блокирует event loop.

app.get('/report', (req, res) => {
  const raw = fs.readFileSync('/data/report.json', 'utf8');
  const data = JSON.parse(raw);
  const rows = data.items.filter((x) => x.active).sort((a, b) => a.date - b.date);
  res.json(rows);
});

app.post('/signup', async (req, res) => {
  const salt = crypto.randomBytes(16);
  const hash = crypto.pbkdf2Sync(req.body.password, salt, 600000, 64, 'sha512');
  await db.users.insert({ email: req.body.email, hash, salt });
  res.status(201).end();
});

app.get('/search', (req, res) => {
  const re = new RegExp('^(' + req.query.q + ')+$');
  res.json(products.filter((p) => re.test(p.name)));
});

app.get('/export', async (req, res) => {
  const all = await db.orders.findAll();
  res.send(JSON.stringify(all));
});

Коротко

Всё, что выполняется синхронно в JS-потоке, задерживает все запросы процесса. Типичные источники: Sync API файловой системы и crypto в обработчиках, JSON.parse/JSON.stringify мегабайтных данных, сортировки больших массивов, катастрофические регулярные выражения.

Что должен найти ревьюер

  1. Производительностьстрока 2
    readFileSync в обработчике запроса блокирует event loop на время чтения диска. Асинхронный fs.promises.readFile или кеш в памяти, раз файл один.
  2. Производительностьстрока 3
    JSON.parse большого файла на каждый запрос синхронно грузит CPU; парсить один раз и кешировать или считать отчёт заранее.
  3. Производительностьстрока 4
    Фильтрация и сортировка большого массива на каждый запрос: это работа для БД (WHERE + ORDER BY + LIMIT) или для заранее посчитанного результата. Плюс нет пагинации.
  4. Производительностьстрока 10
    pbkdf2Sync с 600 000 итераций занимает сотни миллисекунд основного потока на каждую регистрацию. Асинхронный crypto.pbkdf2 или argon2/bcrypt с async API выполняются в пуле libuv.
  5. Безопасностьстрока 16
    Пользовательский ввод в RegExp: ReDoS. Шаблон вида (a+)+$ на длинной строке вызывает катастрофический бэктрекинг и вешает процесс. Экранировать ввод или искать без regex.
  6. Производительностьстрока 21
    Все заказы грузятся в память и сериализуются одним JSON.stringify: огромная синхронная операция и пик памяти. Нужен поток (курсор БД + стриминг ответа) и ограничение объёма.

Разбор

Всё, что выполняется синхронно в JS-потоке, задерживает все запросы процесса. Типичные источники: *Sync API файловой системы и crypto в обработчиках, JSON.parse/JSON.stringify мегабайтных данных, сортировки больших массивов, катастрофические регулярные выражения.

Как диагностировать: метрика задержки event loop (perf_hooks.monitorEventLoopDelay, она есть в prom-client по умолчанию), профиль CPU через --cpu-prof или clinic.js, flame graph покажет «широкие» синхронные функции.

Асинхронные crypto.pbkdf2, scrypt, zlib и fs уходят в пул потоков libuv (по умолчанию 4 потока, UV_THREADPOOL_SIZE), поэтому основной поток остаётся свободным.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.

Термины по теме