Strona główna / Artykuły / Znalezienie prawdziwego wąskiego gardła w wolnym punkcie końcowym Node.js

Znalezienie prawdziwego wąskiego gardła w wolnym punkcie końcowym Node.js

Naucz się systematycznego sposobu na śledzenie opóźnień w warstwie backendowej wzdłuż ścieżki żądania – od kodu Node.js po zapytania do bazy danych – przy użyciu pomiarów czasu i polecenia EXPLAIN ANALYZE.

1671 słów

Koniec punktu końcowego backendu działa powoli, a reakcja jest niemal zawsze taka sama:

"Node.js jest wolny."

Kiedyś to też było twoje standardowe wyjaśnienie.

Następnie, po poświęceniu czasu na badanie rzeczywistych problemów wydajności, pojawiła się inna lekcja:

Miejsce, w którym pojawia się wolność, nie jest automatycznie miejscem, które ją powoduje.

Serwis Node.js może działać dokładnie tak, jak zaplanowano, podczas gdy rzeczywiste opóźnienie pochodzi z bazy danych, API firmy trzeciej, sieci lub pojedynczego słabo zoptymalizowanego zapytania ukrytego gdzieś w ścieżce żądania.

Poniżej znajduje się metoda, którą warto zastosować za każdym razem, gdy koniec punktu końcowego backendu wydaje się nieuzasadnionie wolny.

1. Zacznij od rzeczywistego problemu

Wyobraź sobie taką ścieżkę:

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

Odpowiedź jest poprawna.

Ale zazwyczaj zajmuje to około 2 do 3 sekund.

Natychmiastowa reakcja polega zwykle na takich pomysłach jak:

  • Dostosowanie kodu Node.js
  • Wprowadzenie warstwy cache’owania
  • Zwiększenie mocy serwera
  • Uruchomienie dodatkowych instancji
  • Ponowne napisanie fragmentów kodu JavaScript

Żaden z tych rozwiązań nie opiera się jeszcze na dowodach — to jedynie spekulacje.

To, o co należy najpierw zapytać, to:

Gdzie właściwie znika czas?

2. Mierz przed wprowadzaniem jakichkolwiek zmian

Zamiast od razu przechodzić do modyfikacji kodu, najpierw zmierz czas trwania każdego poszczególnego kroku.

Naprzимер:

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

Jeśli wyświetlony wynik to:

getUsers: 2720ms

ten jeden numer już podaje ci coś przydatnego.

Prawdopodobnie wąskim gardłem nie jest sama warstwa HTTP.

Dzieje się to gdzieś wewnątrz getUsers().

Czas zagłębić się o jeden poziom głębiej.

3. Pomiar zapytania do bazy danych

Załóżmy, że funkcja, która to obsługuje, wygląda tak:

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

A wynik pomiaru czasu wygląda następująco:

db-query: 2680ms

To znacznie zawęża zakres poszukiwań.

To nie Node.js zużywa 2,6 sekundy na obsługę żądania.

To jest wywołanie do bazy danych.

Dlatego właśnie natychmiastowe dostosowywanie na poziomie aplikacji może być stratą czasu, której nigdy nie odzyskasz.

4. Zapytaj teraz PostgreSQL, co robi

To właśnie ten moment, gdy należy użyć polecenia EXPLAIN ANALYZE.

Naprzимер:

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

Wynik może ujawnić coś w tym rodzaju:

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

Kluczowym szczegółem, na który należy zwrócić uwagę, jest:

Seq Scan

PostgreSQL przegląda całą tabelę wiersz po wierszu, zamiast przechodzić bezpośrednio do odpowiedniej rekordu za pomocą indeksu.

Gdy tabela staje się wystarczająco duża, taki sekwencyjny przegląd staje się naprawdę kosztowny.

5. Rozwiązanie to nie „optymalizacja Node.js”

Jeśli wyszukiwania według email odbywają się często, dodanie odpowiedniego indeksu może znacząco obniżyć koszt zapytania.

Naprzимер:

CREATE INDEX idx_users_email
ON users(email);

Zainstaluj ponownie to samo zapytanie później:

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

Plan wykonywania powinien teraz pokazywać coś bliższego:

Index Scan using idx_users_email

zamiast:

Seq Scan

Dokładna wartość w milisekundach nie jest tu tak naprawdę istotna.

Ważne jest to, że strategia wykonywania została zmieniona na podstawie rzeczywistych pomiarów, a nie domysłów.

6. Większa lekcja

Wszystko to nie dotyczy w istocie PostgreSQL.

To lekcja na temat jak debugować w sposób systematyczny.

Gdy żądanie trwa zbyt długo, unikaj natychmiastowego obwiniania frameworka.

Wyobraź sobie pełną ścieżkę, którą przemierza żądanie:

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

Każdy pojedynczy element w tej łańcuchu może być prawdziwym wąskim gardłem.

dokładnie którego.

7. Prosty proces debugowania

Zawsze, gdy punkt końcowy okazuje się wolny, należy zastosować mniej więcej tę sekwencję działań.

Krok 1 — Pomiary całego żądania

Request: 2.8

Krok 2 — Podział żądania na komponenty

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

Gdy już masz taką strukturę, znalezienie przyczyny staje się o wiele prostsze.

Krok 3 — Zbadanie najwolniejszego komponentu

Jeśli segment bazy danych pochłania 2,6 sekundy:

Nie marnuj godziny na dopracowywanie JavaScriptu.

Idź prosto do bazy danych.

Krok 4 — Sprawdzenie zapytania

Przyjrzyj się uważnie:

EXPLAIN ANALYZE

Sprawdź również:

  • Skany sekwencyjne
  • To, czy rzeczywiście używane są indeksy
  • Łączenia
  • Operacje sortowania
  • Warunki filtrowania
  • Ile wierszy jest skanowanych
  • Ile wierszy faktycznie zostaje zwróconych

Krok 5 — Popraw jedną rzecz

Niektóre możliwe rozwiązania:

  • Dodaj odpowiedni indeks
  • Ponownie napisz zapytanie o nieefektywnej strukturze
  • Usuń niepotrzebne łączenie
  • Rozwiąż problem wzorca zapytania N+1
  • Zmniejsz ilość pobieranych niepotrzebnych danych

Krok 6 — Pomiierz ponownie

Nigdy nie ufaj bezpodstawnie temu, że twoja poprawka zadziałała.

Potwierdź to nowym pomiarem.

8. Nie zapominaj o zewnętrznych API

Baza danych nie zawsze jest przyczyną opóźnienia.

Rozważmy następujący scenariusz:

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

Jeśli każde pojedyncze wezwanie zajmuje:

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

wtedy punkt końcowy działa znacznie wolniej, niż powinien.

A tutaj odpowiedzią nie jest „sprawić, by Node.js działał szybciej”.

Często chodzi po prostu o zmianę sposobu wykonywania niezależnych wezwań.

Naprzимер:

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

W ten sposób operacje, które nie zależą od siebie, są wykonywane równocześnie, a nie po kolei.

Niemniej jednak istnieje tu ważna uwaga:

Nie używajPromise.all()bez wcześniejszego przemyślenia.

Gdy operacje zależą od siebie, wymagają ograniczeń dotyczących równoczesności lub mogą obciążyć usługę poniżej w łańcuchu, wykonywanie wszystkiego równolegle może w rzeczywistości pogorszyć sytuację zamiast ją poprawić.

Odpowiednia korekta wydajności zawsze zależy od konkretnego obciążenia, z którym się mierzysz.

9. Uważaj na zapytania typu N+1

Istnieje jeszcze jedna pułapka wydajnościowa, która na pierwszy rzut oka wydaje się nieszkodliwa.

Weźmy takie przykład:

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

Jeśli masz do czynienia z 100 użytkownikami, ten wzorzec po cichu generuje:

1 query → get users
100 queries → get orders

To oznacza aż 101 oddzielnych zapytań do bazy danych potrzebnych do obsługi jednego wywołania API.

To jest dobrze znany problem zapytań typu N+1.

w zależności od twojej sytuacji lepsze strategie mogą obejmować:

  • Bezpośrednie łączenie tabel
  • Wykorzystanie funkcji ładowania relacji w ORM
  • Ładowanie rekordów partiami zamiast pojedynczo
  • Użycie klauzuli WHERE IN
  • Ponowne przemyślenie struktury odpowiedzi
  • Wprowadzanie buforowania tam, gdzie ma to sens
  • Jak zawsze, wybór odpowiedniego podejścia zależy w całości od obciążenia pracy.

    10. Nie zwiększaj zbyt wcześnie rozmiaru serwera

    Częstą, instynktowną reakcją na wolną API jest:

    "Dodajmy więcej procesorów i pamięci RAM.

    To może pomóc w niektórych przypadkach.

    Jednak często po prostu wydajemy więcej pieniędzy, nie rozwiązując rzeczywistego problemu.

    Jeśli zapytanie do bazy danych jest źle napisane, wzmocnienie serwera Node.js nie sprawi, że to zapytanie będzie wykonywane szybciej.

    Zanim zwiększysz moc sprzętową, zadaj sobie pytanie:

    Czy aplikacja rzeczywiście jest ograniczona przez procesor lub pamięć?

    Jeśli nie, dodanie mocy serwera prawdopodobnie nie rozwiąże rzeczywistego wąskiego gardła.

    11. Przyzwyczajenia do debugowania, których staram się przestrzegać

    Pożyteczny model myślowy do radzenia sobie z wolnością backendu wygląda następująco:

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

    Zamiast:

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

    Druga ścieżka to domysły.

    Pierwsza ścieżka to prawdziwa inżynieria.

    12. Lista kontrolna wydajności mojego backendu

    Zanim przystąpisz do jakiejkolwiek optymalizacji, warto potwierdzić:

    • Jaki jest rzeczywisty czas odpowiedzi od początku do końca?
    • Czy obciążenie jest ograniczone przez CPU?
    • Czy sama zapytanie do bazy danych jest wolne?
    • Czy gdzieś nie kryje się wzorzec N+1?
    • Czy istnieją odpowiednie indeksy?
    • Co ujawnia polecenie EXPLAIN ANALYZE?
    • Czy API od strony trzeciej dodaje opóźnienie?
    • Czy zadania niezależne są wykonywane sekwencyjnie, mimo że nie muszą?
    • Czy kod pobiera więcej danych, niż faktycznie potrzebuje?
    • Czy w odpowiednich miejscach wykorzystywany jest Redis?
    • Czy zbiór połączeń jest poprawnie skonfigurowany?
    • Czy wprowadzona zmiana rzeczywiście wpłynęła na zmierzone wartości?

    Ostatnia myśl

    Jedną z najcenniejszych lekcji w pracy nad backendem jest następująca:

    Rzadko udaje się rozwiązać problemy wydajnościowe poprzez domysły.

    Słaba wydajność API Node.js nie oznacza automatycznie, że winny jest sam Node.js.

    Prawdziwym winowajcą może być:

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

    Znajomość sposobu dostosowywania poszczególnych technologii nie jest kluczową umiejętnością.

    Prawdziwą umiejętnością jest potrafienie określić dokładnie, gdzie znajduje się wąskie gardło.

    Gdy już wiemy dokładnie, gdzie znika czas, rozwiązanie zazwyczaj przychodzi samo.

    Najpierw mierz. Znajdź wąskie gardło. Popraw je. Zmierz ponownie.

    To właśnie taka metoda kieruje obecnie rozwiązywaniem problemów z wydajnością backendu.

    Literatura pokrewna

  • Strategia tokenów odnowienia dla systemów autoryzacji Node.js — Dowiedz się, jak projektować, rotować, cofać i bezpiecznie przechowywać tokeny odnowienia w Node.js, aby kradzież tokenów i wylogowanie działały zgodnie z oczekiwaniami.
  • Logowanie strukturyzowane w Node.js: Przekształcanie chaosu podczas debugowania w produkcji w jasność — Dowiedz się, dlaczego console.log zawodzi w aplikacjach Node.js w produkcji oraz jak logowanie strukturyzowane, poziomy logów i identyfikatory korelacji przekształcają trudne błędy w szybkie naprawy.
  • Opanowanie konkurencji w Node.js: Unikanie zawieszeń API za pomocą p-map i Bottleneck — Dowiedz się, jak połączenie p-map i Bottleneck w Node.js zapobiega błędom związanym z ograniczeniami szybkości i przeciążeniem systemu poprzez kontrolę konkurencji oraz czasu obsługi żądań.