Избегайте длительных операций в цикле событий Node.js

Если вы когда-нибудь использовали регулярные выражения, то наверняка слышали такой каштан:

Некоторые люди, столкнувшись с проблемой, думают: «Я знаю, я буду использовать регулярные выражения». - Теперь у них две проблемы.

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

Один за другим все экземпляры нашей службы перестали отвечать на проверки работоспособности и больше не отправляли журналы или показатели. Нам пришлось остановить эти контейнеры (поскольку мы запускаем Node.js в Docker) и запустить новые.

Пора заняться этим.

С самого начала было ясно, что цикл обработки событий Node.js заблокирован какой-то длительной операцией при обработке клиентского запроса. Балансировщик нагрузки определил, что экземпляр неисправен, и начал отправлять трафик другим пользователям. В итоге все экземпляры пострадали и погибли.

Это было странно, поскольку мы не выполняли никаких синхронных задач или блокирующих операций во время обработки запроса - по крайней мере, мы думали, что не выполняли.

Согласно журналам, последней операцией перед тем, как процесс перестал отвечать, была проверка адреса электронной почты ... с использованием регулярного выражения
- Я знаю, что это неправильно, но все это делают, верно? 😉

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

Время выполнения очень быстро увеличивалось по мере увеличения длины электронного адреса. Для строки из ~ 40 символов мы не могли дождаться завершения.

А вот пример неудачного регулярного выражения:

const email = '[email protected]';
const regex = /^[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(regex)) {
  throw new Error('Invalid email');
}

Поскольку String.prototype.match() является блокирующей операцией, весь цикл событий был заблокирован до его завершения, и служба не обрабатывала другие запросы.

Такое поведение не характерно для Node.js - то же самое регулярное выражение в Java также демонстрирует экспоненциальный рост времени выполнения. Но из-за многопоточности служба Java не умерла полностью, поэтому последствия для Java были не такими драматичными, как в Node.js.

Решение было тривиальным: заменить плохое регулярное выражение на другое, которое широко используется в других библиотеках (например, email-validator или validator). Затем мы убедились, что время выполнения больше не зависит от длины проверенных адресов.

В результате я сделал следующие выводы:

  • По возможности избегайте использования регулярных выражений, поскольку они блокируют цикл обработки событий. Трудно сказать, какая у них сложность алгоритмов и как они будут вести себя с большим набором данных или с данными определенной формы.
  • Если не получается избавиться от регулярного выражения - проверьте время выполнения.

И, что наиболее важно, отслеживайте состояние цикла событий Node.js.

Если вы используете для этого разные инструменты или у вас есть другие идеи, поделитесь в комментариях.