Jak ogólnie Node.js obsługuje 10 000 jednoczesnych żądań?


394

Rozumiem, że Node.js używa pojedynczego wątku i pętli zdarzeń do przetwarzania żądań przetwarzających tylko jeden naraz (co nie blokuje). Ale jak to działa, powiedzmy 10 000 równoczesnych żądań. Pętla zdarzeń przetworzy wszystkie żądania? Czy to nie potrwa zbyt długo?

Nie mogę (jeszcze) zrozumieć, jak może być szybszy niż wielowątkowy serwer WWW. Rozumiem, że wielowątkowy serwer WWW będzie droższy w zasobach (pamięci, procesorze), ale czy nie będzie jeszcze szybszy? Prawdopodobnie się mylę; proszę wyjaśnić, w jaki sposób ten wątek jest szybszy przy wielu żądaniach i co zwykle robi (na wysokim poziomie) podczas obsługi wielu żądań, np. 10 000.

A także, czy ta jednowątkowa skala dobrze pasuje do tak dużej ilości? Pamiętaj, że dopiero zaczynam się uczyć Node.js.


4
Ponieważ większość pracy (przenoszenie danych) nie wymaga procesora.
OrangeDog

4
Zauważ też, że tylko dlatego, że jest tylko jeden wątek wykonujący Javascript, nie oznacza to, że wiele innych wątków działa.
OrangeDog

To pytanie jest albo zbyt szerokie, albo jest duplikatem różnych innych pytań.
OrangeDog,


Wraz z pojedynczym wątkiem, Node.js robi coś, co nazywa się „nieblokującym We / Wy”. Oto, gdzie cała magia została wykonana
Anand

Odpowiedzi:


762

Jeśli musisz zadać to pytanie, prawdopodobnie nie wiesz, co robi większość aplikacji / usług internetowych. Prawdopodobnie myślisz, że całe oprogramowanie to robi:

user do an action
       
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Jednak nie tak działają aplikacje internetowe, a nawet każda aplikacja z bazą danych jako zapleczem. Aplikacje internetowe to robią:

user do an action
       
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

W tym scenariuszu oprogramowanie spędza większość czasu działania, używając 0% czasu procesora, czekając na powrót bazy danych.

Wielowątkowa aplikacja sieciowa:

Wielowątkowe aplikacje sieciowe obsługują powyższe obciążenie w następujący sposób:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

Wątek spędza więc większość czasu, używając 0% CPU, czekając, aż baza danych zwróci dane. W tym czasie musieli przydzielić pamięć wymaganą dla wątku, który zawiera całkowicie osobny stos programów dla każdego wątku itp. Ponadto musieliby uruchomić wątek, który, choć nie jest tak drogi jak rozpoczęcie pełnego procesu, wciąż nie jest dokładnie tani.

Pętla zdarzeń singlethreaded

Ponieważ spędzamy większość czasu na wykorzystaniu 0% procesora, dlaczego nie uruchomić kodu, gdy nie korzystamy z procesora? W ten sposób każde żądanie będzie nadal otrzymywało tyle samo czasu procesora, co aplikacje wielowątkowe, ale nie musimy zaczynać wątku. Robimy to:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

W praktyce oba podejścia zwracają dane z mniej więcej takim samym opóźnieniem, ponieważ czas odpowiedzi bazy danych dominuje w przetwarzaniu.

Główną zaletą jest to, że nie musimy odradzać nowego wątku, więc nie musimy robić wielu malloców, które by nas spowolniły.

Magiczne, niewidoczne nawlekanie

Z pozoru tajemniczą rzeczą jest to, jak oba powyższe podejścia radzą sobie z obciążeniem w trybie „równoległym”? Odpowiedź jest taka, że ​​baza danych jest podzielona na wątki. Nasza aplikacja jednowątkowa wykorzystuje tak naprawdę wielowątkowe zachowanie innego procesu: bazy danych.

Tam, gdzie podejście z pojedynczym wątkiem zawodzi

Aplikacja z pojedynczym wątkiem zawiedzie, jeśli musisz zwrócić dużo obliczeń procesora przed zwróceniem danych. Nie mam na myśli pętli for przetwarzającej wynik bazy danych. To wciąż głównie O (n). Chodzi mi o rzeczy takie jak transformacja Fouriera (na przykład kodowanie mp3), ray tracing (renderowanie 3D) itp.

Kolejną pułapką pojedynczych aplikacji jest to, że będą one wykorzystywać tylko jeden rdzeń procesora. Więc jeśli masz serwer czterordzeniowy (co nie jest dzisiaj rzadkością), nie używasz pozostałych 3 rdzeni.

W przypadku niepowodzenia podejścia wielowątkowego

Aplikacja wielowątkowa zawiedzie, jeśli trzeba przydzielić dużo pamięci RAM na wątek. Po pierwsze, użycie pamięci RAM oznacza, że ​​nie można obsłużyć tylu żądań, co aplikacja z pojedynczym wątkiem. Gorzej, Malloc jest powolny. Przydzielanie wielu obiektów (co jest typowe dla współczesnych frameworków internetowych) oznacza, że ​​możemy potencjalnie być wolniejsi niż aplikacje z jednym wątkiem. To tutaj zwykle wygrywa node.js.

Jednym z przypadków użycia, które mogą pogorszyć wielowątkowość, jest potrzeba uruchomienia innego języka skryptowego w wątku. Najpierw zazwyczaj musisz wykonać malloc cały środowisko wykonawcze dla tego języka, a następnie malloc zmienne używane przez skrypt.

Więc jeśli piszesz aplikacje sieciowe w C, go lub java, to narzuty związane z wątkami zwykle nie będą takie złe. Jeśli piszesz serwer WWW C do obsługi PHP lub Ruby, bardzo łatwo jest napisać szybszy serwer w javascript, Ruby lub Python.

Podejście hybrydowe

Niektóre serwery WWW stosują podejście hybrydowe. Nginx i Apache2 na przykład implementują kod przetwarzania sieci jako pulę wątków pętli zdarzeń. Każdy wątek uruchamia pętlę zdarzeń jednocześnie przetwarzając żądania jednowątkowe, ale żądania są równoważone obciążeniem między wieloma wątkami.

Niektóre architektury jednowątkowe również wykorzystują podejście hybrydowe. Zamiast uruchamiania wielu wątków z jednego procesu można uruchomić wiele aplikacji - na przykład 4 serwery node.js na czterordzeniowym komputerze. Następnie używasz modułu równoważenia obciążenia, aby rozłożyć obciążenie między procesami.

W efekcie oba podejścia są technicznie identycznymi odbiciami lustrzanymi.


104
To zdecydowanie najlepsze wyjaśnienie dla węzła, jakie do tej pory przeczytałem. Ta „jednowątkowa aplikacja faktycznie wykorzystuje wielowątkowe zachowanie innego procesu: bazy danych”.
Zadziałało

co jeśli klient wysyła wiele żądań w węźle, na przykład uzyskując nazwę i modyfikując ją, i powiedz, że te operacje wypychają serwer do bardzo szybkiej obsługi przez wielu klientów. jak sobie poradzić z takim scenariuszem?
Remario,

3
@CaspainCaldion To zależy od tego, co rozumiesz przez bardzo szybką i dużą liczbę klientów. W tej chwili node.js może przetwarzać w górę o 1000 żądań na sekundę, a prędkość jest ograniczona tylko do prędkości karty sieciowej. Pamiętaj, że to 1000 żądań na sekundę, a nie klienci połączeni jednocześnie. Może obsłużyć 10000 klientów jednocześnie bez problemu. Prawdziwym wąskim gardłem jest karta sieciowa.
slebetman

1
@slebetman, najlepsze wyjaśnienie w historii. jedno, jeśli mam algorytm uczenia maszynowego, który przetwarza niektóre informacje i odpowiednio dostarcza wyniki, powinienem zastosować podejście wielowątkowe lub jednowątkowe
Ganesh Karewad

5
@GaneshKarewad Algorytmy używają procesora, usługi (baza danych, REST API itp.) Używają We / Wy. Jeśli AI jest algorytmem zapisanym w js, powinieneś uruchomić go w innym wątku lub procesie. Jeśli AI jest usługą działającą na innym komputerze (takim jak Amazon, Google lub usługi IBM AI), użyj architektury z jednym wątkiem.
slebetman

46

Wydaje ci się, że większość przetwarzania odbywa się w pętli zdarzeń węzła. Węzeł faktycznie farmuje poza pracą we / wy do wątków. Operacje we / wy zwykle zajmują rzędy wielkości dłuższe niż operacje na procesorze, więc dlaczego procesor na to czekał? Poza tym system operacyjny bardzo dobrze radzi sobie z zadaniami we / wy. W rzeczywistości, ponieważ Węzeł nie czeka, osiąga znacznie wyższe wykorzystanie procesora.

Analogicznie, myśl o NodeJS jako kelnerze przyjmującym zamówienia od klientów, podczas gdy szefowie we / wy przygotowują je w kuchni. Inne systemy mają wielu szefów kuchni, którzy przyjmują zamówienia od klientów, przygotowują posiłek, sprzątają ze stołu, a dopiero potem obsługują kolejnego klienta.


5
Dzięki za analogię do restauracji! Znalazłem analogie i przykłady ze świata rzeczywistego o wiele łatwiejsze do nauczenia się.
LaVache,

13

Rozumiem, że Node.js używa pojedynczego wątku i pętli zdarzeń do przetwarzania żądań przetwarzających tylko jeden naraz (co nie blokuje).

Mogę nie rozumieć, co tu powiedziałeś, ale „pojedynczo” brzmi, jakbyś nie rozumiał w pełni architektury opartej na zdarzeniach.

W „konwencjonalnej” (nieopartej na zdarzeniach) architekturze aplikacji proces spędza dużo czasu, czekając, aż coś się wydarzy. W architekturze opartej na zdarzeniach, takiej jak Node.js, proces nie tylko czeka, może zająć się inną pracą.

Na przykład: otrzymujesz połączenie od klienta, akceptujesz je, czytasz nagłówki żądania (w przypadku http), a następnie zaczynasz reagować na żądanie. Możesz przeczytać treść żądania, zazwyczaj kończy się to wysyłaniem niektórych danych z powrotem do klienta (jest to celowe uproszczenie procedury, tylko w celu wykazania sensu).

Na każdym z tych etapów większość czasu spędza się na oczekiwaniu na dane z drugiego końca - faktyczny czas przetwarzania w głównym wątku JS jest zwykle dość minimalny.

Gdy stan obiektu we / wy (takiego jak połączenie sieciowe) zmienia się i wymaga przetworzenia (np. Dane są odbierane w gnieździe, gniazdo staje się zapisywalne itp.), Główny wątek JS Node.js zostaje obudzony z listą elementów wymagających przetworzenia.

Znajduje odpowiednią strukturę danych i emituje pewne zdarzenie w tej strukturze, które powoduje uruchomienie wywołań zwrotnych, które przetwarzają przychodzące dane lub zapisują więcej danych w gnieździe itp. Gdy wszystkie obiekty we / wy wymagające przetworzenia zostały już przetworzone przetworzony, główny wątek JS Node.js będzie czekał ponownie, dopóki nie zostanie powiedziane, że dostępnych jest więcej danych (lub inna operacja została zakończona lub upłynął limit czasu).

Następnym razem po przebudzeniu może to być spowodowane tym, że inny obiekt I / O musi zostać przetworzony - na przykład inne połączenie sieciowe. Za każdym razem uruchamiane są odpowiednie połączenia zwrotne, a następnie wracają one do trybu uśpienia, czekając na coś innego.

Ważne jest to, że przetwarzanie różnych żądań jest przeplatane, nie przetwarza jednego żądania od początku do końca, a następnie przechodzi do następnego.

Moim zdaniem, główną zaletą tego jest to, że powolne żądanie (np. Próbujesz wysłać 1 MB danych odpowiedzi do urządzenia telefonu komórkowego przez połączenie danych 2G lub robisz naprawdę wolne zapytanie do bazy danych) blokuj szybsze.

W tradycyjnym wielowątkowym serwerze internetowym zwykle masz wątek dla każdego obsługiwanego żądania i będzie on przetwarzał TYLKO to żądanie, dopóki nie zostanie zakończone. Co się stanie, jeśli masz dużo powolnych wniosków? W rezultacie wiele wątków kręci się wokół przetwarzania tych żądań, a inne żądania (które mogą być bardzo proste, które można obsłużyć bardzo szybko) stają w kolejce za nimi.

Istnieje wiele innych systemów opartych na zdarzeniach oprócz Node.js i mają one zwykle podobne zalety i wady w porównaniu z modelem konwencjonalnym.

Nie twierdzę, że systemy oparte na zdarzeniach są szybsze w każdej sytuacji lub przy każdym obciążeniu - zwykle działają dobrze w przypadku obciążeń związanych z operacjami we / wy, a nie tak dobrze w przypadku obciążeń związanych z procesorem.


12

Kroki przetwarzania modelu pętli zdarzeń jednowątkowych:

  • Klienci Wyślij zapytanie do serwera WWW.

  • Węzeł JS Web Server wewnętrznie utrzymuje pulę ograniczonych wątków, aby świadczyć usługi na żądanie klienta.

  • Węzeł Serwer WWW JS odbiera te żądania i umieszcza je w kolejce. Jest znany jako „Kolejka zdarzeń”.

  • Węzeł Serwer WWW JS ma wewnętrznie komponent, znany jako „Pętla zdarzeń”. Dlaczego ma tę nazwę, ponieważ korzysta z nieokreślonej pętli do odbierania żądań i ich przetwarzania.

  • Pętla zdarzeń używa tylko jednego wątku. Jest to główne serce modelu przetwarzania platformy Node JS.

  • Pętla zdarzeń sprawdza, czy każde żądanie klienta jest umieszczane w kolejce zdarzeń. Jeśli nie, poczekaj na przychodzące żądania na czas nieokreślony.

  • Jeśli tak, wybierz jedno żądanie klienta z kolejki zdarzeń

    1. Rozpoczyna proces żądania klienta
    2. Jeśli to żądanie klienta nie wymaga żadnych blokujących operacji we / wy, przetwórz wszystko, przygotuj odpowiedź i odeślij ją do klienta.
    3. Jeśli to żądanie klienta wymaga pewnych blokujących operacji we / wy, takich jak interakcja z bazą danych, systemem plików, usługami zewnętrznymi, wówczas zastosowane zostanie inne podejście
  • Sprawdza dostępność wątków z wewnętrznej puli wątków
  • Pobiera jeden wątek i przypisuje to żądanie klienta do tego wątku.
  • Wątek jest odpowiedzialny za przyjęcie tego żądania, przetworzenie go, wykonanie blokujących operacji we / wy, przygotowanie odpowiedzi i odesłanie jej do pętli zdarzeń

    bardzo ładnie wyjaśnione przez @Rambabu Posa, aby uzyskać więcej wyjaśnień, wrzuć ten link


schemat podany w tym poście na blogu wydaje się błędny, to, o czym wspomnieli w tym artykule, nie jest całkowicie poprawne.
rranj

11

Dodanie do odpowiedzi slebetman: Kiedy mówisz Node.JS możesz obsłużyć 10 000 równoczesnych żądań, są to zasadniczo żądania nieblokujące, tj. Żądania te dotyczą głównie zapytań do bazy danych.

Wewnętrznie event loopod Node.JSprzenoszących thread pool, gdzie każdy wątek obsługuje non-blocking requesti pętla zdarzenie nadal słuchać więcej zamówienie po przekazaniu pracy do jednego z gwintem thread pool. Gdy jeden z wątków zakończy pracę, wysyła sygnał event loop, że zakończył aka callback.Event loopnastępnie przetworz to wywołanie zwrotne i odeślij odpowiedź.

Jako, że dopiero zaczynasz korzystać z NodeJS, przeczytaj więcej o nextTicktym, jak zrozumieć wewnętrzną pętlę zdarzeń. Czytając blogi na stronie http://javascriptissexy.com , były one dla mnie bardzo pomocne, kiedy zaczynałem od JavaScript / NodeJS.


2

Dodawanie do Slebetman dla większej przejrzystości tego, co dzieje się podczas wykonywania kodu.

Wewnętrzna pula wątków w nodeJs ma domyślnie tylko 4 wątki. i to nie tak, że całe żądanie jest dołączone do nowego wątku z puli wątków, całe wykonanie żądania odbywa się tak jak każde normalne żądanie (bez żadnego zadania blokującego), tyle że za każdym razem, gdy żądanie ma długi czas działania lub ciężką operację, taką jak db wywołanie, operacja na pliku lub żądanie HTTP zadanie jest umieszczone w kolejce do wewnętrznej puli wątków dostarczanej przez libuv. A ponieważ nodeJs domyślnie udostępnia 4 wątki w wewnętrznej puli wątków, co 5. lub kolejne współbieżne żądanie czeka, aż wątek będzie wolny, a po zakończeniu tych operacji połączenie zwrotne zostanie wypchnięte do kolejki wywołań zwrotnych. i jest odbierany przez pętlę zdarzeń i odsyła odpowiedź.

Teraz pojawia się kolejna informacja, że ​​nie jest to jedna kolejka oddzwaniania, istnieje wiele kolejek.

  1. NextTick kolejki
  2. Kolejka mikro zadań
  3. Kolejka czasowa
  4. Kolejka oddzwaniania we / wy (żądania, operacje na plikach, operacje na dbach)
  5. Kolejka ankiet IO
  6. Sprawdź kolejkę faz lub SetImmediate
  7. zamknij kolejkę obsługi

Za każdym razem, gdy pojawia się żądanie, kod jest wykonywany w tej kolejności wywołań zwrotnych w kolejce.

To nie jest tak, że gdy jest żądanie blokowania, jest dołączane do nowego wątku. Domyślnie są tylko 4 wątki. Tak więc dzieje się tam kolejka.

Ilekroć w kodzie występuje proces blokowania, taki jak odczyt pliku, wywołuje funkcję, która wykorzystuje wątek z puli wątków, a następnie po zakończeniu operacji wywołanie zwrotne jest przekazywane do odpowiedniej kolejki, a następnie wykonywane w kolejności.

Wszystko jest ustawiane w kolejce na podstawie rodzaju wywołania zwrotnego i przetwarzane w wyżej wymienionej kolejności.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.