Код-ревью: что блокирует 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));
});
Коротко
Что должен найти ревьюер
- Производительностьстрока 2
readFileSyncв обработчике запроса блокирует event loop на время чтения диска. Асинхронныйfs.promises.readFileили кеш в памяти, раз файл один. - Производительностьстрока 3
JSON.parseбольшого файла на каждый запрос синхронно грузит CPU; парсить один раз и кешировать или считать отчёт заранее. - Производительностьстрока 4Фильтрация и сортировка большого массива на каждый запрос: это работа для БД (WHERE + ORDER BY + LIMIT) или для заранее посчитанного результата. Плюс нет пагинации.
- Производительностьстрока 10
pbkdf2Syncс 600 000 итераций занимает сотни миллисекунд основного потока на каждую регистрацию. Асинхронныйcrypto.pbkdf2или argon2/bcrypt с async API выполняются в пуле libuv. - Безопасностьстрока 16Пользовательский ввод в RegExp: ReDoS. Шаблон вида
(a+)+$на длинной строке вызывает катастрофический бэктрекинг и вешает процесс. Экранировать ввод или искать без regex. - Производительностьстрока 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 ошибок.
Термины по теме
Ещё по теме «Event loop и libuv»
- Объясни event loop в Node.js
- setImmediate и setTimeout внутри I/O
- setTimeout(0) против setImmediate на верхнем уровне
- Что такое libuv
- Рекурсивный process.nextTick
- process.nextTick и setImmediate
- Event loop Node и браузера: в чём разница
- Порядок nextTick, промиса и setTimeout