Strona główna / Artykuły

Ile żądań może faktycznie obsłużyć serwer Node.js?

Ile żądań może naprawdę obsłużyć serwer Node.js? Testy obciążeniowe Express na Heroku i AWS w porównaniu z Django, szacunkowa przepustowość 40 tys. żądań na sekundę oraz czynniki spowalniające API.

Wydawca: REACTAPP.TOP6 min czytania1358 słów

Źródła: How Many Requests Can a Real-World Node.js Server-Side App Handle?

Każdy, kto dopiero zaczyna pracę z Node.js, w końcu zadaje to samo praktyczne pytanie: ile żądań może obsłużyć jedna aplikacja Node.js w środowisku produkcyjnym i ile instancji serwera jest potrzebnych przy określonym natężeniu ruchu? Odpowiedź zależy od znacznie więcej czynników niż tylko od frameworka.

Cel, do którego warto dążyć

Platforma do edycji zdjęć PicsArt, jak donoszono, obsługuje około 40 000 żądań na sekundę dzięki backendowi opartemu głównie na Node.js.

To ogromna liczba, a ona zależy od wielu czynników poza samym serwerem backendowym. W tym celu pozostałe czynniki są pomijane, pozostawiając jedynie sprzęt i oprogramowanie backendu, a w szczególności Node.js.

Co pokazują syntetyczne testy wydajności

Zespół stojący za Fastify opublikował test wydajności, w którym Express obsługuje około 15 000 żądań na sekundę, podczas gdy sam moduł HTTP w Node osiąga około 70 000.

MacBook Pro 15″ z 2015 roku pokazał niemal identyczne wyniki. Nawet 15 tysięcy zapytań na sekundę w Expressie wygląda imponująco, ale takie testy przeprowadzane są z użyciem małego obsługiwacza, który jest daleki od prawdziwej API. To, co czyni API „praktycznym”, to dane, sposób ich przesyłania oraz interakcja komponentów, dlatego realistyczny obsługiwacz musi komunikować się z bazą danych lub innymi usługami.

Różnica pomiędzy wersją surową a Expressem i tak nie jest zbyt istotna. Ustalone frameworki takie jak Express zazwyczaj są lepszym wyborem, ponieważ w praktyce żadna pojedyncza instancja nie będzie w stanie obsłużyć 100 tysięcy zapytań na sekundę. Dlatego do tych testów zainstalowano Express, przy czym mały obsługiwacz wykonywał różnorodne operacje.

Obsługiwacz testowy

Obsługiwacz wykonywał sześć kroków:

  1. Wstawienie nowego użytkownika do bazy danych.
  2. Pobranie 20 użytkowników z bazy danych.
  3. Przekształcenie tablicy 20 użytkowników w ciąg znaków oraz jej ponowna analiza.
  4. Przekształć rekurencyjnie te 20 obiektów użytkowników na format camelCase.
  5. Uporządkuj tablicę.
  6. Wykonaj proste obliczenia i operacje na stdout, takie jak Date.now i console.log, które również zużywają zasoby obliczeniowe.

Odpowiedź ważyła około 12 KB. Każdy obiekt użytkownika miał rozmiar 609 bajtów i wyglądał w ten sposób:

Wynik 1: Node.js na Heroku z AWS RDS

Aplikacja została wdrożona na Heroku na sprzęcie kosztującym 25 dolarów miesięcznie, a żądania były wysyłane z maksymalną częstotliwością, która nie powodowała wzrostu średniej latencji. Instancja obsłużyła 31 000 żądań w ciągu 60 sekund, co daje średnio 515 żądań na sekundę.

Wynik 2: Django na Heroku z AWS RDS

Aby zobaczyć, co dałaby technologia niewykorzystująca pojedynczego wątku, w Django stworzono równoważny mechanizm obsługi w identycznych warunkach i umieszczono go na tym samym serwerze. Wyniki okazały się nieco gorsze niż w przypadku Node.js – udało się obsłużyć 23 000 żądań w ciągu 60 sekund, co daje 389 żądań na sekundę.

Wynik 3: trzy instancje Node.js na AWS Elastic Beanstalk

Trzecia konfiguracja odzwierciedlała rzeczywiste środowisko produkcyjne na AWS. Baza danych oraz warstwa backendowa znajdowały się w Frankfurtu (eu-central-1), najbliższym regionie, oddalonym o około 3 000 km od osoby przeprowadzającej testy. Trzy instancje Node.js pracowały za pośrednictwem mechanizmu równoważenia obciążenia AWS. Udało im się obsłużyć 19 000 żądań w ciągu 10 sekund, co odpowiada 1 858 żądań na sekundę. Następnie przeprowadzono ciągły test obciążeniowy, w którym w ciągu 13 minut zrealizowano 1,5 miliona udanych żądań.

Skalowanie do 40 000 żądań na sekundę

Jedna instancja radzi sobie z około 600 zapytaniami na sekundę, więc ile będzie potrzebnych do obsługi 40 000 zapytań? Załóżmy, że średni punkt końcowy na dużej platformie jest dwa razy bardziej obciążający niż mechanizm obsługi testów. To daje 40 000 / (600 / 2) = 133 instancje rozmieszczone na całym świecie dla aplikacji na poziomie globalnego „unikorna”. To nie jest wielki problem.

Większe punkty końcowe mogą być trzy razy bardziej wymagające, ale cache i komunikacja oparta na zdarzeniach mogą przywrócić wydajność, więc dwukrotność to rozsądna średnia ocena.

Co spowalnia API

Niezliczone czynniki mogą obniżyć szybkość działania API. Są to kwestie związane z wydajnością, które występują w większości aplikacji internetowych działających po stronie serwera.

Rozmiar danych

Ilość danych przechowywanych w bazie danych lub pobieranych z niej ma znaczenie, szczególnie w bazach relacyjnych, gdzie duże obciążenia często oznaczają złożone zapytania lub intensywną pracę pomiędzy systemem zarządzania bazą danych a jej pamięcią dyskową. W każdym przypadku większa liczba bajtów oznacza dłuższy czas transmisji. Przechowywanie plików w bazie danych jest złym rozwiązaniem, natomiast lepszym wyborem jest przechowywanie obiektów. W idealnym przypadku rekord mieści się w granicach kilkudziesięciu kilobajtów, zazwyczaj od 1 do 20 KB.

Odległość od bazy danych

Sprzęt bazy danych

Sprzęt również ma duży wpływ. Mała aplikacja powinna zakupić lepszy sprzęt odpowiadający jej obciążeniu, w przeciwnym razie znów będzie konieczna skomplikowana inżynieria.

Konfiguracja bazy danych

Bazy danych dostarczane są z ustawieniami domyślnymi, dzięki czemu nie stanowią dodatkowego problemu do zarządzania, ale w skali większej wymagają uwagi. Liczba maksymalnych połączeń to jedna z ustawień, na które należy zwrócić uwagę – może ona stać się wąskim gardłem dla aplikacji lub przeładować serwer bazy danych, a biblioteki klienta, takie jak ORM, często je zmieniają. Czas timeout połączenia to kolejna kwestia do rozważenia.

Liczba wywołań bazy danych

Prawdziwa API może wykonać dziesiątki oddzielnych wywołań bazy danych, aby dokończyć zadanie, a także może korzystać z innych usług, z których każda wykonuje swoje własne dziesiątki wywołań. Pamiętnym przykładem jest API monolitu bankowego, który w środowisku rozwoju chmurowego miał czas odpowiedzi wynoszący 9 sekund. Jego obsługa wykonywała 300 linijek kodu, obejmujących około 30 wywołań do bazy danych oraz około 10 wywołań do usług wewnętrznych. Taka sytuacja ma tendencję do występowania przy podejściu „kod najpierw”, a nie „projekt najpierw”.

Rozwiązania zależą od logiki biznesowej. Kolejki i inne techniki asynchroniczne mogą przenieść część zadań bazodanowych w tło, co wymaga podziału dużych usług na mniejsze. Zapytania łączące mogą również zmniejszyć liczbę wywołań.

Haki

Popularne narzędzia ODM i ORM oferują haki lub middleware, które ułatwiają obsługę określonej logiki. Podczas operacji masowych mogą stanowić ukryty wąskie gardło, ponieważ pliki rozszerzalne takie jak konwertery camelCase lub pascal_case również zużywają zasoby obliczeniowe i czas. Haki pomagają, ale to, co w nich jest napisane i gdzie, wymaga dokładniejszej analizy.

Zadania wymagające dużej mocy obliczeniowej CPU

Zadania wymagające dużej mocy obliczeniowej CPU spowalniają serwer i zmuszają użytkownika końcowego do oczekiwania. W Node.js wiele z nich polega na użyciu puli wątków. Choć nie blokuje to operacji wejścia/wyjścia asynchronicznego, to nadal spowalnia API. Na przykład kliknięcie w opcję rejestracji wymaga, aż pula wątków zaszyfruje hasło, co trwa ponad 100 milisekund.

Wąskie gardła w pętli zdarzeń

Node.js wykonywa kod w ramach pętli zdarzeń, na pojedynczym wątku, więc wydajność kodu wpływa na całe aplikację. Funkcje JSON.parse i JSON.stringify mogą spowalniać API, a pętle nawarstwione oraz funkcje rekurencyjne stanowią zagrożenie. Ignorowanie sposobu działania Node.js może sprawić, że aplikacja stanie się niedostępna, w zależności od rozmiaru danych i ruchu.

Słabe projektowanie infrastruktury

Wielka infrastruktura nie tylko ma więcej problemów niż monolit, ale także inne problemy, które często wymagają złożonych rozwiązań architektonicznych. Przy dużej liczbie komponentów zła architektura poważnie wpływa na szybkość API i sprawia, że system jest nieskuteczny pod względem wykorzystania zasobów, więc trzeba płacić więcej za coś, co mogłoby kosztować mniej. Projektując złożone aplikacje, należy zastanowić się nad tym, jakie problemy rozwiązuje dana technologia, oraz znać dostępne alternatywy. Należy również wziąć pod uwagę, jak wymagania mogą ulec zmianie w ciągu dwóch lub sześciu miesięcy, i sprawdzić każdą decyzję w odniesieniu do tych przypuszczeń. Trzeba też pomyśleć o umowie SLA oraz o wymaganiach niefunkcjonalnych, w tym o tym, co się stanie, jeśli ruch wzrośnie.

Skala i konfiguracja komponentów

Komponenty dodawane do infrastruktury mogą spowolnić całą platformę. Weźmy na przykład RabbitMQ – podobnie jak każda inna technologia, posiada wiele ustawień. Domyślne wartości nadają się na początek, ale dzięki badaniom i bliższej analizie konkretnego komponentu, przegląd konfiguracji lub jej przeredagowanie mogą znacznie poprawić funkcjonowanie infrastruktury. Komponenty muszą również skalować się wraz z intensywnością użytkowania, aby nie stały się wąskim gardłem.

Literatura pokrewna