Главная / Статьи / Определение настоящего узкого места в медленном конце точки входа Node.js

Определение настоящего узкого места в медленном конце точки входа Node.js

Изучите систематический метод отслеживания задержек на стороне сервера вдоль пути запроса — от кода Node.js до запросов к базе данных — с использованием функций измерения времени и команды EXPLAIN ANALYZE.

1671 слов

Фреймворк на стороне сервера работает медленно, и реакция людей почти всегда одинакова:

«Node.js медленный».

Раньше это тоже было стандартным объяснением.

Однако после того, как я потратил время на поиск реальных проблем с производительностью, я пришёл к другому выводу:

Место, где проявляется медлительность, не обязательно является причиной её возникновения.

Ваш сервис на Node.js может работать именно так, как задумано, в то время как настоящая задержка может происходить из-за базы данных, стороннего API, сети или одного плохо оптимизированного запроса, спрятанного где-то в пути запроса.

Ниже приведён метод, который стоит использовать каждый раз, когда фреймворк на стороне сервера кажется чрезмерно медленным.

1. Начните с самой проблемы

Представьте маршрут вроде этого:

GET /api/users?email=user@example.com

Ответ верный.

Но это обычно занимает примерно 2–3 секунды.

Первоначальная реакция обычно включает такие идеи:

  • Настроить код Node.js
  • Ввести слой кэширования
  • Увеличить мощность сервера
  • Запустить дополнительные инстансы
  • Переписать фрагменты JavaScript

Ничто из этого пока не подкреплено доказательствами — это лишь предположения.

Сначала следует задать вопрос:

Куда на самом деле уходит время?

2. Измеряйте перед тем, как что-либо менять

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

Например:

console.time("getUsers");
const users = await getUsers();console.timeEnd("getUsers");

Если выводимый результат будет таким:

getUsers: 2720ms

это единственное число уже даёт вам полезную информацию.

Вероятно, узким местом не является сам слой HTTP.

Это происходит где-то внутри функции getUsers().

Пора заглянуть на ещё один уровень глубже.

3. Измерение времени выполнения запроса к базе данных

Допустим, основная функция выглядит так:

console.time("db-query");
const users = await prisma.user.findMany({
  where: {
    email: email
  }
});console.timeEnd("db-query");

А результаты измерения времени выглядят следующим образом:

db-query: 2680ms

Это значительно сужает круг поиска.

Проблема не в Node.js, который тратит 2,6 секунды на обработку запроса.

Проблема в вызове базы данных.

Именно поэтому сразу переход к настройкам на уровне приложения может привести к потере времени, которое уже невозможно вернуть.

4. Теперь спросим PostgreSQL, что он делает

Именно сейчас следует воспользоваться командой EXPLAIN ANALYZE.

Например:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'user@example.com';

Результаты могут показать что-то вроде этого:

Seq Scan on users
(actual time=0.025..2720.532 rows=1)

Важнейшим моментом, на который следует обратить внимание, является:

Seq Scan

PostgreSQL просматривает всю таблицу строчка за строчкой, вместо того чтобы сразу перейти к соответствующей записи через индекс.

Когда таблица становится достаточно большой, такой последовательный поиск становится действительно дорогостоящим.

5. Решение не в «оптимизации Node.js»

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

Например:

CREATE INDEX idx_users_email
ON users(email);

После этого снова выполните тот же запрос:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'user@example.com';

Теперь план выполнения должен выглядеть примерно так:

Index Scan using idx_users_email

вместо того чтобы:

Seq Scan

Точное значение в миллисекундах здесь не так уж и важно.

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

6. Более важный урок

Всё это по сути не связано непосредственно с PostgreSQL.

Это урок о том, как систематически отлаживать.

Когда обработка запроса затягивается, не спешите сразу винить фреймворк.

Представьте полный путь, по которому проходит запрос:

Client
  ↓
HTTP
  ↓
Node.js
  ↓
Business Logic
  ↓
Redis / Database / External API
  ↓
Response

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

Ваша задача — определить точно какой именно.

7. Простой процесс отладки

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

Шаг 1 — Измерьте весь запрос

Request: 2.8

Шаг 2 — Разделите запрос на компоненты

Authentication: 20ms
Business logic: 50ms
Database: 2.6s
Response serialization: 15ms

После такого разбиения найти причину становится гораздо проще.

Шаг 3 — Изучите самый медленный компонент

Если на обработку сегмента базы данных уходит 2,6 секунды:

Не тратите час на доработку JavaScript.

Сразу переходите к базе данных.

Шаг 4 — Проверка запроса

Внимательно рассмотрите:

EXPLAIN ANALYZE

Также проверьте:

  • Последовательные сканирования
  • Используются ли на самом деле индексы
  • Соединения
  • Операции сортировки
  • Условия фильтрации
  • Количество сканируемых строк
  • Количество строк, фактически возвращаемых

Шаг 5 — Исправление одной проблемы

Некоторые возможные решения:

  • Добавление подходящего индекса
  • Переписывание запроса с неэффективной структурой
  • Удаление ненужного соединения
  • Устранение паттерна запроса N+1
  • Сокращение объема загружаемых ненужных данных

Шаг 6 — Повторная оценка

Никогда не полагайтесь на уверенность в том, что ваше исправление сработало.

Подтвердите это с помощью новых измерений.

8. Не забывайте об внешних API

Проблема с задержкой не всегда кроется в базе данных.

Рассмотрим следующую ситуацию:

const user = await getUser();
const payment = await getPaymentDetails();const orders = await getOrders();return {
  user,
  payment,
  orders
};

Если каждый отдельный вызов занимает:

getUser()          → 100ms
getPaymentDetails() → 900ms
getOrders()         → 700ms

то конечная точка работы становится гораздо медленнее, чем это необходимо.

И здесь ответом не является фраза «сделать Node.js быстрее».

Возможно, речь идет просто о изменении способа выполнения независимых вызовов.

Например:

const [user, payment, orders] = await Promise.all([
  getUser(),
  getPaymentDetails(),
  getOrders()
]);

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

Однако здесь существует важное ограничение:

Не используйтеPromise.all(), не тщательно все обдумав.

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

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

9. Остерегайтесь запросов типа N+1

Существует ещё одна ловушка производительности, которая на первый взгляд кажется безвредной.

Возьмём такой пример:

const users = await getUsers();
for (const user of users) {
  user.orders = await getOrders(user.id);
}

Если у вас 100 пользователей, этот паттерн тихо генерирует:

1 query → get users
100 queries → get orders

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

Это хорошо известная проблема запросов типа N+1.

В зависимости от ситуации более эффективные стратегии могут включать:

  • Прямое объединение таблиц
  • Использование функций загрузки связей ORM
  • Загрузка записей пакетами, а не по одной
  • Использование условия WHERE IN
  • Пересмотр структуры ответа
  • Внедрение кэширования там, где это целесообразно
  • Как и всегда, выбор подхода полностью зависит от объема работы.

    10. Не увеличивайте размер сервера слишком рано

    Частой импульсивной реакцией на медленный API является:

    «Давайте добавим больше процессорных ядер и оперативной памяти».

    В некоторых случаях это может помочь.

    Но часто вы просто тратите больше денег, не решая самой проблемы.

    Если запрос к базе данных написан плохо, усиление сервера Node.js не сделает его выполнение быстрее.

    Прежде чем увеличивать мощность оборудования, спросите себя:

    Действительно ли приложение ограничено процессором или памятью?

    Если нет, увеличение мощности сервера вряд ли решит настоящую проблему.

    11. Менталитет отладки, которого я стараюсь придерживаться

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

    Is the API slow?
           ↓
    Measure it
           ↓
    Which layer is slow?
           ↓
    Measure that layer
           ↓
    Find the actual bottleneck
           ↓
    Make one change
           ↓
    Measure again
    

    Вместо того чтобы:

    API slow
       ↓
    Optimize Node.js
       ↓
    Add Redis
       ↓
    Increase server
       ↓
    Hope it gets faster
    

    Второй путь основан на догадках.

    Первый путь — это реальная инженерная практика.

    12. Проверочный список производительности бэкенда

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

    • Каково реальное время отклика от начала до конца?
    • Зависит ли нагрузка от производительности CPU?
    • Медленен ли сам запрос к базе данных?
    • Не скрывается ли где-то паттерн N+1?
    • Установлены ли необходимые индексы?
    • Что показывает команда EXPLAIN ANALYZE?
    • Не увеличивает ли задержку API сторонних поставщиков?
    • Выполняются ли независимые задачи последовательно, хотя это не нужно?
    • Не загружает ли код больше данных, чем требуется?
    • Используется ли Redis там, где это целесообразно?
    • Правильно ли настроен пул соединений?
    • Действительно ли внесенные изменения повлияли на полученные показатели?

    Одним из самых ценных уроков работы с бэкендом является следующий:

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

    PostgreSQL
    Redis
    External APIs
    Network
    N+1 queries
    Poor indexes
    Serialization
    Connection pools
    Application logic
    

    Знание того, как настраивать каждую из этих технологий по отдельности, — это не главное умение.

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

    Сначала измеряйте. Найдите узкое место. Исправьте узкое место. Снова измеряйте.

  • Стратегия использования токенов обновления для систем аутентификации Node.js — Узнайте, как проектировать, обновлять, аннулировать и безопасно хранить токены обновления в Node.js, чтобы кража токенов и выход из системы действительно работали так, как ожидается.
  • Структурированное логирование в Node.js: превращение хаоса при отладке в продакшене в порядок — Узнайте, почему функция console.log не работает в приложениях Node.js в продакшене, и как структурированное логирование, уровни логов и идентификаторы корреляции помогают быстро устранять сложные ошибки.
  • Как справиться с конкурентностью в Node.js: как избежать сбоев API с помощью p-map и Bottleneck — Узнайте, как сочетание плагинов p-map и Bottleneck в Node.js помогает предотвратить ошибки ограничения скорости и перегрузку системы путем контроля конкурентности и времени обработки запросов.