Головна / Статті / Визначення справжньої проблеми у повільному кінцевому пункті 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

Це призводить до створення аж 101 окремого запиту до бази даних для обробки одного виклику API.

Це є добре відомою проблемою запитів типу 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 допомагає запобігати помилкам обмеження частоти та перевантаженню системи шляхом контролю конкуруентності та часу обробки запитів.