Наверное каждый слышал выражение, что если ты решаешь проблему с помощью регулярных выражение, то у тебя становится две проблемы.
Недавно и сам столкнулся с проблемой производительности регулярок на Node.js, и к чему это может привести.
В один прекрасный момент все инстансы сервиса на Node.js один за одним перестали отвечать на health-check, слать логи и метрики. Пришлось остановить эти контейнеры (мы запускаем Node.js в Docker) и запустить новые.
Начали разбираться.
С самого начала было понятно, что был заблокирован event loop Node.j какой-то очень длительной операцией в обработке запроса пользователя. Load balancer определял что этот инстанс нездоров и направлял трафик на другие. Туда приходил следующий злополучный запрос от пользователя и они умирали.
Что было странно, что никаких синхронных или блокирующих операций мы в обработке запроса не делали — ну, по крайней мере, нам так казалось. Логгер показывал, что последней операцией перед тем как процесс переставал отвечать была валидация email… с помощью регулярного выражения (да-да я знаю, что так делать нельзя, но все так делают :).
Запустили этот-же скрипт валидации емейла на локальной машине и прифигели — регулярка имела экспоненциальную сложность, время выполнения росло очень быстро при увеличении длины емейла. На строке длиной в ~40 символов нам не хватило терпения дождаться его окончания.
const email = 'some_very_long_invalid_email_that_slows_down_regexp';
const regexp = /^[a-zA-Z0-9][a-zA-Z0-9_\.\-\+&]*@([a-zA-Z0-9]([a-zA-Z0-9]*[\-]?[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,10}$/;
if (!email.match(regexp)) {
throw new Error('Invalid email')
}
Так как String.prototype.match() это блокирующая операция, то весь event loop ждал пока она завершится, и сервис не обрабатывал другие запросы.
Причем это не было особенность Node.js, эта же регулярка запущенная на Java давала такое же время выполнения. Но из-за мультипочности сервис на Java не умирал полностью, и поэтому последствия были не настолько драматичными, как в случае с Node.js.
Решение было простое: заменить регулярное выражение на другое, которое широко используется в других библиотеках (таких как email-validator
или validator
) и проверить, что время выполнения больше не зависит от длины емейла.
Для себя сделал следующие выводы:
- Избегать использования регулярок, поскольку они блокируют event loop, а по их внешнему виду невозможно сказать, какая сложность у алгоритма. И как он себя поведет при больших входящих данных или данных специфической формы.
- Если избавиться нельзя — то проверять время выполнения.
И самое важное — крайне желательно мониторить состояние event loop в Node.js. Подскажите в комментариях удобные инструменты для этого.
Автор: Леонид Якубович