Предположим, у вас есть служба Node.js, развернутая в производственной среде, каждая строка кода проверена и покрыта тестами. Но при 10 запросах в секунду процесс Node.js начинает потреблять 100% ЦП, или на графике ЦП появляются случайные всплески, в результате время отклика увеличивается и влияет на всех потребителей. Конечно, вы можете просто увеличить количество запущенных экземпляров, но это не решение проблемы, служба будет вести себя так же.
Основные причины высокой загрузки ЦП
- Циклы и итерации. Любой вызов
.map
,.reduce
,.forEach
и других методов итерации может вызвать проблему, если вы не ограничите размер итерируемой коллекции. Та же потенциальная проблема с петлямиfor
иwhile
. Если вам нужно иметь дело с большими коллекциями, используйте потоки или разбейте коллекции на части и обрабатывайте их асинхронно. Он распределяет нагрузку между разными итерациями EventLoop, и эффект блокировки будет уменьшен. - Рекурсивные функции. Тот же принцип здесь, вы должны учитывать глубину рекурсии, особенно когда функция синхронна. Пример из моего опыта: один из моих товарищей по команде добавил функцию для решения Задачи торгового продавца, она работала нормально, пока не была вызвана с 28 баллами. В результате каждый вызов блокировал весь процесс Node.js на 2 секунды при 100% ЦП.
- Огромные полезные нагрузки. Node.js создан для обработки огромного количества асинхронных операций, таких как выполнение запросов к базам данных или внешние вызовы API. И он отлично работает до тех пор, пока полезные нагрузки из внешних источников не станут небольшими. Не забывайте, что Node.js необходимо сначала прочитать полезную нагрузку и сохранить ее в памяти, а затем преобразовать JSON в объект (добавляется больше памяти), выполнить некоторые операции с объектом. Огромные полезные данные от служб Node.js также могут быть проблемой, потому что Node.js сначала преобразует объекты в JSON, а затем отправляет их клиенту. Все эти операции могут вызвать загрузку ЦП, убедитесь, что размер полезной нагрузки невелик, используйте разбиение на страницы и не заполняйте ненужные данные заранее. Для сервисов GraphQL используйте сложность, чтобы ограничить полезную нагрузку ответа.
Promise.all
. Не поймите меня неправильно,Promise.all
сам по себе в порядке. Но это может вызвать проблемы, если вы вызовете его с большим количеством операций. Например, у вас есть массив идентификаторов, и вам нужно прочитать сущности из базы данных. Если в списке 10 идентификаторов, это не проблема, но если их 1000… Попробуйте выполнить такие массовые операции и прочитать данные из базы данных с помощью курсора.- Утечки памяти. Node.js имеет встроенный сборщик мусора, в зависимости от различных условий сборщик мусора удаляет неиспользуемые объекты из памяти. Поиск и удаление ненужных предметов - операция не из дешевых. И если в вашей службе Node.js есть утечка памяти, сборщик мусора будет пытаться освободить память снова и снова, но безуспешно, просто тратя впустую ЦП.
Итак, как найти первопричину высокой загрузки ЦП?
Очевидное решение - попытаться воспроизвести проблему локально. Попробуйте запустить свой сервис локально и сделать к нему несколько запросов. Вы можете создавать скрипты нагрузочного тестирования на Node.js или использовать фреймворки для нагрузочного тестирования, такие как Artillery. Просто помните, что локальная конфигурация должна быть максимально приближена к производственной. Откройте монитор ресурсов, запустите нагрузочные тесты и смотрите. Если вам удастся воспроизвести, перезапустите приложение с флагом --inspect
, снова выполните нагрузочные тесты, откройте chrome: // inspect в браузере Chrome:
Щелкните inspect
под своим приложением, а затем запустите профилирование ЦП:
Подождите некоторое время, обычно достаточно 10–15 секунд, и у вас есть профиль процессора:
И теперь вы можете определить, что не так с вашим кодом, в профиле ЦП есть все необходимое для этого.
Кроме того, может быть полезно взять профиль кучи, чтобы определить, есть ли утечка памяти. Просто перейдите на вкладку Память и нажмите Сделать снимок:
В результате вы получите что-то вроде этого:
Игнорируйте строки с системными типами (compiled code)
, (string)
, (array)
, Object
, (closure)
, system / Context
, (system)
, Array
, WeakMap
и т. Д., В большинстве случаев они не помогут обнаружить память в вашем коде. Попробуйте сделать несколько снимков кучи, чтобы увидеть, как изменяется количество объектов каждого типа. Если он только растет, держу пари, у вас утечка памяти :)
А как насчет производства? Как снять профиль процессора на запущенном экземпляре?
В большинстве случаев очень сложно воспроизвести проблемы с производительностью, потому что вам нужна та же конфигурация среды, одни и те же данные в базах данных, кешах и т. Д. Проблема с производительностью может быть специфичной только для некоторых категорий пользователей, поскольку у них есть определенные данные.
А как насчет режима отладки в производственной среде? Что ж, не рекомендуется включать режим отладки в производственной среде, потому что в режиме отладки процессы Node.js потребляют больше ресурсов, и это небезопасно.
Но есть лучший подход - создавать профили по запросу с inspector
модулем https://nodejs.org/api/inspector.html. Это встроенный модуль Node.js, вам не нужно устанавливать никаких дополнительных зависимостей, но я рекомендую вам использовать inspector-api
https://www.npmjs.com/package/inspector-api. Это простая оболочка с поддержкой обещаний. Давайте создадим конечную точку, которая записывает профиль ЦП, я создам пример для NestJS, для других фреймворков это выглядит примерно так же:
Весь код заключен в setImmediate
, потому что нам не нужно ждать окончания записи. Не забудьте защитить эту конечную точку, она должна быть доступна только суперадминистраторам или пользователям системы. Давайте проверим это с помощью curl:
curl -X POST https://127.0.0.1/profile/cpu
И через 10 секунд мы получили профиль во временном каталоге:
Давайте добавим аналогичную конечную точку для профилирования кучи:
Теперь вы можете использовать профили ЦП и кучи, когда захотите, просто скопируйте их локально после записи.
Если вы не хотите добавлять эту функцию в качестве конечных точек HTTP, вы можете вместо этого заключить их в обработчики сигналов процесса, например:
И используйте его, посылая сигналы с помощью команды kill
:
kill -USR1 ${pid} // for CPU kill -USR2 ${pid} // for Heap
Если вы используете Kubernetes, может быть сложно скопировать файлы из модулей, для этого случая inspector-api
автор добавил замечательную функцию: загрузку файлов профиля в AWS S3. Чтобы включить его, передайте необходимые параметры конструктору Inspector
и задайте переменные учетных данных AWS для среды:
Заключение
Сегодня мы обсудили, что может вызвать проблемы с производительностью в приложениях Node.js, как находить проблемы локально и в работающей производственной среде без перезапуска. ИМХО исправления производительности и оптимизация - самая интересная часть в работе разработчика. Если у вас есть подходящие инструменты, найти и исправить любую проблему не так уж и сложно.
Увидимся в следующий раз! Сервус!