Предположим, у вас есть служба 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, как находить проблемы локально и в работающей производственной среде без перезапуска. ИМХО исправления производительности и оптимизация - самая интересная часть в работе разработчика. Если у вас есть подходящие инструменты, найти и исправить любую проблему не так уж и сложно.

Увидимся в следующий раз! Сервус!