Медленный regexp, умирающий Node.js

в 19:07, , рубрики: javascript, node.js, Разработка веб-сайтов

Медленный regexp, умирающий Node.js - 1

Наверное каждый слышал выражение, что если ты решаешь проблему с помощью регулярных выражение, то у тебя становится две проблемы.

Недавно и сам столкнулся с проблемой производительности регулярок на 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. Подскажите в комментариях удобные инструменты для этого.

Автор: Леонид Якубович

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js