Strona główna / Artykuły / 12 zasad przybliżonych dla niezawodnych API w Node.js

12 zasad przybliżonych dla niezawodnych API w Node.js

Praktyczne zasady niezawodności API Node.js: timeouty, idempotencja, transakcje, unieważnianie pamięci cache, logowanie oraz opóźnienia w zależnościach — przed stroną o godzinie 3 nad ranem.

906 słów

Część lekcji dotyczących niezawodności utrwala się dopiero po awarii. Te dwanaście z nich występuje na tyle często w różnych usługach Node.js, że ich jasne sformułowanie jest tańsze niż ponowne odkrywanie ich pod presją sygnałów alarmowych. Traktuj je jako standardy przy projektowaniu obsługujących skryptów, a nie jako opcjonalne ulepszenia po uruchomieniu.

1. Prośby, które można ponowić, zostaną spróbowane ponownie.

Nie „może być”. Klienci wygaszają po upływie czasu, połączenia zostają przerwane w trakcie odpowiedzi, a ludzie naciskają przycisk wysłania zbyt szybko. Balansery obciążenia i sieci mobilne dodatkowo powodują duplikację dostaw. Każdy obsługujący skrypt, który tworzy lub modyfikuje stan, potrzebuje wyraźnej odpowiedzi na pytanie „a co jeśli ten identyczny pakiet danych przyjdzie ponownie”, ponieważ w wystarczająco długim okresie czasu nie jest to przypadek szczególny – jest to nieuniknione. Projektuj zarówno standardową ścieżkę działania, jak i ścieżkę z duplikatami jednocześnie.

2. Ustaw czas wygaśnięcia dla każdego wyjściowego połączenia.

Zapytania do bazy danych, połączenia HTTP z usługami third-party, bramy płatnicze – jeśli pominiesz termin realizacji, niesprawny partner może utrzymywać otwarty socket w nieskończoność. Proste wywołanie fetch(url) nie oznacza „cierpliwego czekania przez chwilę”; jest to nieograniczone oczekiwanie, a takie sytuacje mogą sprawić, że jedna wolna zależność zamrozi całą infrastrukturę. Lepiej korzystać z domyślnych ustawień bibliotek, które nakładają ograniczenia, oraz w razie błędu wywoływać powtórki lub pokazywać błędy widoczne dla użytkownika, zamiast milczącego zawieszenia.

3. Sprawdzania w przeglądarce dotyczą doświadczenia użytkownika, a nie bezpieczeństwa.

Użytkownicy mogą pominąć twoją aplikację typu SPA i bezpośrednio skorzystać z API, wysyłając dowolny JSON za pomocą curl, Postman lub skompromitowanego skryptu. Zasady istotne dla poprawnego funkcjonowania – prawidłowo sformatowana adresa e-mail, nieujemna cena, obowiązkowe pola – muszą być ponownie sprawdzane na serwerze z taką samą surowością, jak w przypadku niechcianego ruchu. Walidacja po stronie klienta pomaga jedynie użytkownikom współpracującym, którzy i tak nie złamaliby zasad.

4. Pamięć cache bez mechanizmu unieważniania to błąd odroczony.

Caching tworzy drugą kopię prawdy, a te kopie mogą się różnić. Gdy wprowadzasz pamięć cache, zdecyduj, który z zapisów, webhooka lub zdarzenia TTL ma ją usunąć, i udokumentuj tę decyzję obok klucza cache. Poleganie na założeniu, że „kiedyś się wygaśnie”, często oznacza „dłużej, niż klienci są w stanie znieść błędne salda”, szczególnie gdy TTL jest mierzony w godzinach dla „lepszej wydajności”.

5. Zapisy powiązane wymagają transakcji, a nie optymizmu.

await update1(); await update2(); wygląda na operację atomową, ale taką nie jest. Pomiędzy tymi wywołaniami awaria, zakłócenie lub odrzucenie mogą spowodować, że zostanie zastosowany tylko jeden zapis, co prowadzi do rozbieżności między stanem zapasów a fakturacją. Tylko rzeczywista transakcja bazodanowa – lub wyraźna sekwencja działań z kompensacjami – zapewnia gwarancje, na których można polegać.

6. Narzędzie do incydentów, których jeszcze nie widziałeś.

Gdy coś się zepsuje, jest już za późno na dodanie linii z dziennika, która mogłaby to wyjaśnić. Ustrukturyzowane dzienniki, identyfikatory korelacji przekazywane między usługami oraz wystarczający kontekst do odtworzenia żądania wysłanego przed awarią przewyższają skutecznością najzdolniejsze sesje debugowania po incydencie. Dane z dzienników, które można bezpiecznie przechowywać, wyniki działania oraz opóźnienia w zależnościach między komponentami, gdy system jest sprawny, są niezwykle cenne.

7. Pamięć procesu to pamięć cache, a nie system przechowywania danych.

Wartość istniejąca tylko w procesie Node znika po ponownym uruchomieniu, jest niewidoczna dla innych instancji tego samego procesu i nigdy nie jest trwała. Flagi funkcjonalne przechowywane w RAM, liczniki czy „tymczasowe” tablice Map mają ten sam los. Jeśli dane muszą przetrwać aktualizację, awarię lub równoważenie obciążenia bez użycia sesji trwałych, należy je przechowywać w bazie danych lub innym trwałym magazynie, a nie w przypadkowym sektorze pamięci RAM.

8. Planuj tak, aby każde połączenie sieciowe ostatecznie zawiodło.

Cennym pytaniem nie jest „czy to zwykle działa?”, lecz „co się dzieje przy rzadkich awariach?”. Czas wygaśnięcia, częściowe odpowiedzi oraz nagłe wzrosty kodów błędów 5xx również mają znaczenie. Jeśli szczera odpowiedź brzmi „nie jest jasne”, zamknij tę lukę poprzez bezpieczne próby ponowne, mechanizmy ochronne lub tryby obniżonej wydajności, zanim produkcja odkryje ten problem.

9. Kolejka tylko przesuwa zadania w czasie; nie usuwa ich.

Dodawanie zadania do kolejki sprawia wrażenie jego zakończenia. Pracownik nadal wykona te same czynności później, z tymi samymi możliwościami awarii, tylko w innym momencie. Jeśli pracownicy spóźniają się, zaległości rosną, mimo że API pozostaje responsywne – to lepsze niż blokowanie żądań, ale nie oznacza tańszej pracy. Dobieraj rozmiar konsumatorów i ścieżek dla wiadomości błędnych tak samo starannie, jak rozmiar warstwy HTTP.

10. Nadawaj nazwy z myślą o czytelnikach za sześć miesięcy, a nie dla dzisiejszych uproszczeń.

data, temp oraz handleStuff pozostają dokładne tylko do chwili, gdy trzy inne koncepcje również zostaną zaklasyfikowane jako „data”. Nazwy związane z koncepcją biznesową są zrozumiałe nawet dla użytkowników bez kontekstu — włączając przyszłego ciebie korzystającego z nieznanej usługi. Wolno preferować rzeczowniki odnoszące się do domeny nad czasownikami opisującymi szczegóły pierwszej implementacji.

11. Twórcy będą wywoływani dwa razy.

Siatki niestabilne, agresywne klienty próbujące ponownie wysłać żądanie, niecierpliwi użytkownicy wykonujący podwójne kliknięcia — cokolwiek z tego. Operacje POST służące do tworzenia muszą umożliwiać sprawną obsługę duplikatów, zwykle poprzez klucz idempotencji przechowywany razem z utworzoną zasobem; w przeciwnym razie podwójne opłaty i duplikatowe zamówienia staną się kwestią timingu. Funkcja „już utworzone” powinna zwracać pierwotny wynik, a nie nowy efekt uboczny.

12. Opóźnienie końcowa punktu dostępu zależy od najwolniejszej zależności.

Elegancki mechanizm obsługi, który wysyła zapytanie nieindeksowane lub żąda dane z odległej API bez ustawienia limitu czasowego, nadal działa powoli. Analiza własnego kodu przy jednoczesnym ignorowaniu funkcji wywoływanych odpowiada na niewłaściwe pytanie. Należy zmierzyć wartości p95 i p99 w całym grafie wywołań, a następnie poprawić indeksy, zgrupowanie operacji oraz limity czasowe w tych miejscach, gdzie faktycznie marnuje się czas.

Żaden z tych wskazówek nie jest genialny, i to właśnie chodzi: strony tworzone o trzeciej nad ranem rzadko kiedy są genialne. Zazwyczaj należą do jednej z tych dwunastu kategorii i pozostają bez opieki, dopóki nie przestaną wydawać się pilne. Należy wdrażać te nudne środki bezpieczeństwa, póki usługa jest jeszcze na tyle mała, by można ją było zmienić.

Literatura pokrewna