Визначення справжньої проблеми у повільному кінцевому пункті Node.js
Дізнайтеся про систематичний метод відстеження затримки бекенду по шляху запиту — від коду Node.js до запитів до бази даних — за допомогою вимірювання часу та команд EXPLAIN ANALYZE.
Кінцева точка бекенду працює повільно, і реакція майже завжди однакова:
"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, проектувати власну ієрархію помилок, централізувати асинхронну обробку помилок та зберігати стек-трейси для підвищеної стійкості додатків у промислових умовах.
- 20 шаблонів Node.js, які запобігають зупинці серверів у промислових умовах — Дізнайтеся про 20 практичних шаблонів Node.js — від обробки помилок до плавного завершення роботи та кеширування з’єднань — які запобігають збоям, перш ніж стане необхідним перезапуск.