20 процентов ваших действий будут составлять 80 процентов ваших результатов.

Я начал с принципа Парето, потому что считаю, что он полностью соответствует идее статьи. На самом деле исправление типичных проблем - это около 20 процентов усилий, которые разработчики могут потратить на исправление производительности и в итоге получить неплохие результаты.

Также я хотел бы упомянуть, что в статье не будет ничего о цикле событий, потому что:

  1. есть масса статей о одиночном потоке, цикле событий и т. д.
  2. нетипично писать тяжелые вычисления или задачи с интенсивным использованием ЦП с помощью NodeJS. Но если так и было решено, то, думаю, сделано намеренно. Таким образом, разработчик знает, как справляться с задачами, интенсивно использующими процессор (ссылка на статью).

Мониторинг и оповещение

Прежде чем перейти к убийцам производительности, я хочу кратко пройти через мониторинг и оповещения, потому что это важная часть современных систем. Они позволяют быстро получить обратную связь о технических условиях системы, даже не открывая приложение, по наиболее удобным для вас и вашей команды каналам (например, почта, Slack и т. Д.). Кроме того, это дает возможность отслеживать, улучшают ли ваши исправления производительность и насколько хороши исправления. Кроме того, настроенная распределенная трассировка может пролить свет на недостаточную межсервисную связь и связь с постоянными хранилищами.

Представим, что в системе нет мониторинга. Итак, в этом случае есть следующие способы узнать, что система имеет проблемы с производительностью:

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

Что нужно хотя бы контролировать?

  • Использование процессора. Высокая загрузка ЦП приводит к общему замедлению и может быть признаком задержек цикла событий. Оповещать, когда загрузка ЦП превышает пороговое значение (например, limitCpu * 0.5 <= currentCpu).
  • Использование памяти. Большое использование памяти может привести к завершению процесса из-за нехватки памяти. Оповещать, когда использование памяти превышает пороговое значение (например, limitMemory * 0.5 <= currentMemory)
  • Запросы критического пути выполняются медленно. У каждого приложения есть свой критический путь со списком запросов, которые сильно на него влияют. Отследить время запроса можно с помощью логгера или любых инструментов APM (кстати, у меня есть статья про эластичный APM). Настройте срабатывание предупреждений, когда время запроса превышает пороговое значение. Лично я использую 75 процентилей за последние 12 часов.

Убийцы производительности

1. Связанный асинхронный поток

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

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

Эту проблему легко решить, запустив независимые асинхронные команды параллельно.

В этом случае getReportMetadata и getUser выполняются параллельно и сокращают общее время запроса.

2. Вызов асинхронных функций в foreach / map / reduce и т. Д.

Довольно часто код уже содержит простые асинхронные функции, которые хорошо работают с одним объектом. Например. функция, которая извлекает данные, затем выполняет некоторые преобразования и сохраняет их.

Функция выглядит неплохо, но давайте представим, что поступило новое требование для поддержки массовых обновлений. Первая идея - сделать что-то вроде этого:

Но цена может быть довольно высокой. В данном конкретном случае сделано 2*n запросов к базе данных. Просто чтобы понять, что существует 100 одновременных запросов для 50 сущностей.

По крайней мере, было бы намного проще переработать существующий код с точки зрения производительности следующим образом:

Например, при таком подходе данные могут быть получены с помощью предложения in where и обновлены с помощью массовой записи (например, массовая запись mongo, обновление из значений в postgre)

3. Использование слепой цепочки промежуточного программного обеспечения

Существуют разные подходы (DDD, Луковая архитектура, Многоуровневая архитектура и т. Д.), В которых говорится, что код бизнес-логики должен быть отделен от кода, связанного с фреймворком. Но все же есть много проектов, которые все еще используют промежуточное ПО (например, на основе express и koa) для бизнес-логики, выборки данных и связи с внешними службами и т. Д.

Обычно фрагмент кода:

Код выглядит отлично. Это просто, легко и чисто. Но есть две основные возможные проблемы с производительностью:

  • Цепной асинхронный поток.

Это происходит потому, что каждое промежуточное ПО изолировано и не знает друг о друге. Вот пример такого промежуточного программного обеспечения:

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

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

  • Выполнение избыточных действий

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

Давайте пересмотрим наш пример выше. Приложение выполняет выборку пользователя, выборку метаданных отчета и выборку схемы перед вычислением данных. Предположим, что после рефакторинга получение схемы больше не требуется. Но промежуточное ПО не удалили. Он не был удален, потому что между fetchSchema и calculateData промежуточным программным обеспечением нет явных отношений. В результате возникает избыточный внешний вызов.

Проблема может быть решена так же, как и предыдущая. Код следует реорганизовать путем извлечения выделенного сервиса или функции. В этом случае взаимосвязь между получением данных и обработкой данных будет явной, и избыточные действия можно будет легко удалить.

4. Игнорировать обновления NodeJS.

Команда разработчиков NodeJS регулярно выпускает новые версии. Они постоянно добавляют новые функции (рабочие, новые функции v8), исправляют проблемы (производительность, безопасность, ошибки). Игнорируя обновления, вы создаете блокировку для улучшений, которые, как правило, имеют низкую цену.

Если вы даже не обновляете версию NodeJS, настоятельно рекомендую вести журнал изменений. Наверное, найдешь что-нибудь полезное.

Мои предложения:

  • используйте последнюю версию NodeJS для новых сервисов
  • регулярное обновление версии NodeJS во время активной разработки сервиса

5. Ошибки ORM / ODM

ORM / ODM - отличный инструмент, который может ускорить разработку приложений. Он абстрагирует ваш код от кода базы данных. Но мощь ORM имеет и свои недостатки.

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

Типичные проблемы:

  1. Неэффективные запросы. Фреймворк ORM может генерировать subselect вместо join. Или, что еще хуже, он может генерировать N + 1 запросов для довольно простого сценария.
  2. Возбуждение нетерпеливых отношений. Активная выборка означает, что для каждого выбранного ORM будет выбирать подресурсы, даже если вашему коду они не нужны. В результате код тратит драгоценное время на ненужные вещи.

Мои предложения:

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

6. Забудьте об индексах БД

Говорят, что преждевременная оптимизация - корень всех зол. Но на самом деле это не совсем верно для индексов БД. При разработке персистентной модели следует учитывать наиболее распространенные запросы. В результате становится ясно, как будут использоваться данные и какие столбцы следует проиндексировать.

Вот пара очевидных сценариев, когда индексы можно добавлять, не дожидаясь проблем с производительностью:

  1. Существует мультитенантное приложение со стратегией на основе таблиц. В этом случае каждая таблица содержит идентификатор арендатора. Таким образом, в этом случае столбец идентификатора арендатора должен быть проиндексирован в каждой таблице.
  2. Есть приложение, которое содержит orders. Обычно заказы содержат line items. Таким образом, во время проектирования базы данных line item будет иметь ссылку на order, которая будет широко использоваться при запросах. Так что order id - отличный кандидат для индексации в line items таблице.

Я предлагаю с самого начала индексировать внешние ключи, которые должны широко использоваться при запросах и, скорее всего, редко или никогда не изменяться.

Заключение

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