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