W jaki sposób Node.js jest z natury szybszy, gdy nadal opiera się na wątkach wewnętrznie?


281

Właśnie obejrzałem następujący film: Wprowadzenie do Node.js i nadal nie rozumiem, w jaki sposób zyskujesz szybkość.

Głównie w pewnym momencie Ryan Dahl (twórca Node.js) mówi, że Node.js jest oparty na pętli zdarzeń zamiast na wątku. Wątki są drogie i należy je pozostawić wyłącznie ekspertom w dziedzinie programowania równoległego.

Później pokazuje stos architektury Node.js, który ma podstawową implementację C, która ma wewnętrznie własną pulę wątków. Oczywiście programiści Node.js nigdy nie wykopaliby własnych wątków ani nie korzystali bezpośrednio z puli wątków ... używają asynchronicznych wywołań zwrotnych. Tyle rozumiem.

Nie rozumiem tylko tego, że Node.js nadal używa wątków ... po prostu ukrywa implementację, więc jak to jest szybsze, jeśli 50 osób żąda 50 plików (obecnie nie w pamięci), więc nie jest wymaganych 50 wątków ?

Jedyna różnica polega na tym, że ponieważ jest on zarządzany wewnętrznie, programista Node.js nie musi kodować szczegółów wątków, ale pod nimi nadal używa wątków do przetwarzania żądań plików IO (blokujących).

Czy naprawdę nie bierzesz tylko jednego problemu (wątków) i ukrywasz go, dopóki ten problem nadal istnieje: głównie wiele wątków, przełączanie kontekstu, martwe blokady ... itd.?

Muszą być pewne szczegóły, których wciąż nie rozumiem.


14
Jestem skłonny zgodzić się z tobą, że roszczenie jest nieco zbyt uproszczone. Uważam, że przewaga wydajności węzła sprowadza się do dwóch rzeczy: 1) wszystkie rzeczywiste wątki są zawarte na dość niskim poziomie, a zatem pozostają ograniczone pod względem wielkości i liczby, a zatem synchronizacja wątków jest uproszczona; 2) „Przełączanie” na poziomie systemu operacyjnego select()jest szybsze niż zamiana kontekstu wątku.
Pointy

Odpowiedzi:


140

W rzeczywistości jest tu kilka różnych rzeczy. Ale zaczyna się od memu, że wątki są po prostu bardzo trudne. Więc jeśli są twarde, bardziej prawdopodobne jest, że używając wątków do 1) zerwania z powodu błędów i 2) nie wykorzystania ich tak skutecznie, jak to możliwe. (2) to ten, o który pytasz.

Pomyśl o jednym z podanych przez niego przykładów, w którym pojawia się żądanie, a następnie uruchom zapytanie, a następnie zrób coś z jego wynikami. Jeśli napiszesz go w standardowy sposób, kod może wyglądać następująco:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Jeśli nadchodzące żądanie spowodowało utworzenie nowego wątku, który uruchomił powyższy kod, wątek będzie tam siedział, nic nie robiąc podczas query()działania. (Według Ryana Apache używa pojedynczego wątku do spełnienia pierwotnego żądania, podczas gdy nginx przewyższa go w przypadkach, o których mówi, ponieważ tak nie jest.)

Teraz, jeśli jesteś naprawdę sprytny, możesz wyrazić powyższy kod w taki sposób, aby środowisko mogło się wyłączyć i zrobić coś innego podczas uruchamiania zapytania:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

To jest właśnie to, co robi node.js. Zasadniczo dekorujesz - w sposób dogodny ze względu na język i środowisko, stąd uwagi na temat zamknięć - swój kod w taki sposób, aby środowisko mogło sprytnie wiedzieć, co się uruchamia i kiedy. W ten sposób node.js nie jest nowy w tym sensie, że wynalazł asynchroniczne operacje we / wy (nie dlatego, że ktoś tak twierdził), ale jest nowy, ponieważ sposób jego wyrażania jest nieco inny.

Uwaga: kiedy mówię, że środowisko może być sprytne w kwestii tego, co działa i kiedy, w szczególności mam na myśli to, że wątek użyty do uruchomienia niektórych operacji we / wy może być teraz używany do obsługi innych żądań lub obliczeń, które można wykonać równolegle lub uruchom inne równoległe wejścia / wyjścia. (Nie jestem pewien, czy węzeł jest wystarczająco zaawansowany, aby rozpocząć więcej pracy dla tego samego żądania, ale masz pomysł).


6
Dobra, zdecydowanie mogę zobaczyć, jak to może zwiększyć wydajność, ponieważ wydaje mi się, że jesteś w stanie zmaksymalizować swój procesor, ponieważ nie ma żadnych wątków ani stosów wykonawczych, które tylko czekają na powrót IO, więc to, co zrobił Ryan, zostało skutecznie znalezione sposób na zlikwidowanie wszystkich luk.
Ralph Caraveo

34
Tak, jedyne, co powiem, to to, że nie znalazł sposobu na uzupełnienie luk: nie jest to nowy wzór. Różni się tym, że używa Javascript, aby pozwolić programiście wyrazić swój program w sposób, który jest znacznie wygodniejszy dla tego rodzaju asynchronii. Być może drobiazgowy szczegół, ale wciąż ...
jrtipton

16
Warto również zauważyć, że w przypadku wielu zadań we / wy Node używa dowolnego dostępnego interfejsu API we / wy na poziomie jądra (epoll, kqueue, / dev / poll, cokolwiek)
Paul

7
Nadal nie jestem pewien, czy w pełni to rozumiem. Jeśli weźmiemy pod uwagę, że w żądaniu internetowym operacje IO zajmują większość czasu potrzebnego do przetworzenia żądania i jeśli dla każdej operacji IO tworzony jest nowy wątek, to dla 50 żądań, które przychodzą bardzo szybko, prawdopodobnie mają równolegle 50 wątków i wykonują swoją część IO. Różnica w stosunku do standardowych serwerów WWW polega na tym, że tam całe żądanie jest wykonywane w wątku, podczas gdy w node.js tylko jego część IO, ale jest to część, która zajmuje większość czasu i powoduje, że wątek musi czekać.
Florin Dumitrescu,

13
@SystemParadox dziękuję za zwrócenie na to uwagi. Ostatnio faktycznie przeprowadziłem kilka badań na ten temat i rzeczywiście, chwytem jest to, że asynchroniczne operacje we / wy, gdy są właściwie implementowane na poziomie jądra, nie używają wątków podczas wykonywania operacji we / wy asynchronicznych. Zamiast tego wątek wywołujący zostaje zwolniony, gdy tylko operacja I / O zostanie uruchomiona, a wywołanie zwrotne zostanie wykonane, gdy operacja I / O zostanie zakończona, a wątek będzie dla niej dostępny. Węzeł.js może więc uruchamiać 50 współbieżnych żądań z 50 operacjami we / wy w (prawie) równoległym przy użyciu tylko jednego wątku, jeśli obsługa asynchroniczna operacji we / wy jest poprawnie zaimplementowana.
Florin Dumitrescu

32

Uwaga! To stara odpowiedź. Choć w ogólnym zarysie jest to nadal prawdą, niektóre szczegóły mogły ulec zmianie ze względu na szybki rozwój Node w ciągu ostatnich kilku lat.

Używa wątków, ponieważ:

  1. Opcja O_NONBLOCK funkcji open () nie działa na plikach .
  2. Istnieją biblioteki innych firm, które nie oferują nieblokującego We / Wy.

Aby sfałszować nie blokujące IO, niezbędne są wątki: wykonaj blokujące IO w osobnym wątku. Jest to brzydkie rozwiązanie i powoduje znaczne koszty.

Jest jeszcze gorzej na poziomie sprzętowym:

  • Dzięki DMA procesor asynchronicznie odciąża IO.
  • Dane są przesyłane bezpośrednio między urządzeniem IO a pamięcią.
  • Jądro otacza to synchroniczne, blokujące wywołanie systemowe.
  • Node.js otacza blokujące wywołanie systemowe wątkiem.

To jest po prostu głupie i nieefektywne. Ale to działa przynajmniej! Możemy cieszyć się Node.js, ponieważ ukrywa brzydkie i nieporęczne szczegóły za architekturą asynchroniczną sterowaną zdarzeniami.

Może ktoś zaimplementuje O_NONBLOCK dla plików w przyszłości? ...

Edycja: Dyskutowałem o tym z przyjacielem, który powiedział mi, że alternatywą dla wątków jest odpytywanie za pomocą select : określ limit czasu 0 i wykonaj IO na zwróconych deskryptorach plików (teraz, gdy gwarantuje się, że nie będą blokować).


Co z Windows?
Pacerier

Przepraszam, nie mam pojęcia. Wiem tylko, że libuv jest warstwą neutralną dla platformy do wykonywania pracy asynchronicznej. Na początku Node nie było libuv. Następnie postanowiono oddzielić libuv, co ułatwiło specyficzny dla platformy kod. Innymi słowy, Windows ma swoją własną historię asynchroniczną, która może być zupełnie inna niż Linux, ale dla nas to nie ma znaczenia, ponieważ libuv wykonuje dla nas ciężką pracę.
dokładnie

28

Obawiam się, że „robię coś złego” tutaj, jeśli tak, usuń mnie i przepraszam. W szczególności nie widzę, jak tworzę zgrabne małe adnotacje, które stworzyli niektórzy ludzie. Mam jednak wiele obaw / spostrzeżeń dotyczących tego wątku.

1) Skomentowany element w pseudokodzie w jednej z popularnych odpowiedzi

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

jest zasadniczo fałszywy. Jeśli wątek oblicza, to nie kręci kciukami, wykonuje niezbędną pracę. Z drugiej strony, jeśli po prostu czeka na zakończenie IO, to nie jest wykorzystuje czasu procesora, cały punkt infrastruktury kontroli wątków w jądrze polega na tym, że CPU znajdzie coś pożytecznego do zrobienia. Jedynym sposobem na „poruszenie kciukami”, jak sugerowano tutaj, byłoby utworzenie pętli odpytywania, a nikt, kto zakodował prawdziwy serwer sieciowy, nie jest na to wystarczająco przygotowany.

2) „Wątki są trudne”, ma sens tylko w kontekście udostępniania danych. Jeśli masz zasadniczo niezależne wątki, tak jak ma to miejsce w przypadku obsługi niezależnych żądań internetowych, to wątkowanie jest banalnie proste, po prostu kodujesz liniowy przepływ obsługi jednego zadania i siedzisz całkiem wiedząc, że obsłuży on wiele żądań i każdego będzie skutecznie niezależny. Osobiście zaryzykowałbym to, że dla większości programistów nauka mechanizmu zamykania / oddzwaniania jest bardziej złożona niż zwykłe kodowanie wersji wątku od góry do dołu. (Ale tak, jeśli musisz komunikować się między wątkami, życie staje się naprawdę trudne naprawdę szybko, ale nie jestem przekonany, że mechanizm zamykania / oddzwaniania naprawdę to zmienia, po prostu ogranicza twoje opcje, ponieważ takie podejście jest wciąż możliwe do osiągnięcia dzięki wątkom W każdym razie, że ”

3) Jak dotąd nikt nie przedstawił żadnych prawdziwych dowodów na to, dlaczego jeden konkretny typ zmiany kontekstu byłby mniej lub bardziej czasochłonny niż jakikolwiek inny typ. Moje doświadczenie w tworzeniu wielozadaniowych jąder (na małą skalę dla wbudowanych kontrolerów, nic tak wymyślnego jak „prawdziwy” system operacyjny) sugeruje, że tak nie byłoby.

4) Wszystkie ilustracje, które do tej pory widziałem, które pokazują, jak szybszy jest Węzeł niż inne serwery WWW, są strasznie wadliwe, jednak są one wadliwe w sposób, który pośrednio ilustruje jedną korzyść, którą zdecydowanie zaakceptowałbym dla Węzła (i to nie jest wcale nieistotne). Węzeł nie wygląda tak, jakby wymagał (a nawet nie zezwala) na dostrojenie. Jeśli masz model gwintowany, musisz utworzyć wystarczającą liczbę wątków, aby obsłużyć oczekiwane obciążenie. Zrób to źle, a skończysz na niskiej wydajności. Jeśli jest za mało wątków, procesor jest bezczynny, ale nie jest w stanie zaakceptować większej liczby żądań, utworzyć zbyt wiele wątków, a ty zmarnujesz pamięć jądra, aw przypadku środowiska Java również zmarnujesz pamięć główną sterty . W przypadku Javy marnowanie sterty jest pierwszym, najlepszym sposobem na zwiększenie wydajności systemu, ponieważ wydajne zbieranie śmieci (obecnie może się to zmienić w przypadku G1, ale wydaje się, że jury wciąż nie jest w tym punkcie co najmniej na początku 2013 r.) zależy od dużej ilości zapasów. Więc jest problem, dostrój go za mało wątków, masz bezczynne procesory i słabą przepustowość, dostrój go za dużo, i zapada się na inne sposoby.

5) Jest inny sposób, w jaki akceptuję logikę twierdzenia, że ​​podejście Węzła „jest z założenia szybsze” i to jest to. Większość modeli wątków wykorzystuje model przełącznika kontekstu podzielony na przedziały czasowe, nałożony na bardziej odpowiedni (alert oceny wartości :) i bardziej wydajny (a nie ocena wartości) modelu zapobiegawczego. Dzieje się tak z dwóch powodów: po pierwsze, większość programistów wydaje się nie rozumieć pierwszeństwa z wyprzedzeniem, a po drugie, jeśli nauczysz się wątkowania w środowisku Windows, tworzenie czasów jest takie, czy ci się to podoba, czy nie (oczywiście, to wzmacnia pierwszy punkt ; przede wszystkim w pierwszych wersjach Javy zastosowano priorytetowe zapobieganie implementacjom Solaris i systemowi timeslicing w Windows. Ponieważ większość programistów nie rozumiała i narzekała, że ​​„wątkowanie nie działa w Solaris” wszędzie zmienili model na przedziały czasu). W każdym razie najważniejsze jest to, że tworzenie czasów powoduje dodatkowe (i potencjalnie niepotrzebne) przełączniki kontekstu. Każdy przełącznik kontekstu zajmuje czas procesora i ten czas jest skutecznie usuwany z pracy, którą można wykonać na rzeczywistym zadaniu. Jednak ilość czasu zainwestowanego w zmianę kontekstu z powodu tworzenia przedziału czasu nie powinna przekraczać bardzo małego odsetka całkowitego czasu, chyba że dzieje się coś dziwacznego i nie widzę powodu, dla którego mogę oczekiwać, że tak będzie w przypadku prosty serwer WWW). Tak, tak, przełączniki nadmiaru kontekstu biorące udział w tworzeniu fragmentów są nieefektywne (i nie zdarzają się one w i czas ten jest skutecznie usuwany z pracy, którą można wykonać na prawdziwej pracy pod ręką. Jednak ilość czasu zainwestowanego w zmianę kontekstu z powodu tworzenia przedziału czasu nie powinna przekraczać bardzo małego odsetka całkowitego czasu, chyba że dzieje się coś dziwacznego i nie widzę powodu, dla którego mogę oczekiwać, że tak będzie w przypadku prosty serwer WWW). Tak, tak, przełączniki nadmiaru kontekstu biorące udział w tworzeniu fragmentów są nieefektywne (i nie zdarzają się one w i czas ten jest skutecznie usuwany z pracy, którą można wykonać na prawdziwej pracy pod ręką. Jednak ilość czasu zainwestowanego w zmianę kontekstu z powodu tworzenia przedziału czasu nie powinna przekraczać bardzo małego odsetka całkowitego czasu, chyba że dzieje się coś dziwacznego i nie widzę powodu, dla którego mogę oczekiwać, że tak będzie w przypadku prosty serwer WWW). Tak, tak, przełączniki nadmiaru kontekstu biorące udział w tworzeniu fragmentów są nieefektywne (i nie zdarzają się one wwątki jądra z reguły, btw), ale różnica będzie wynosić kilka procent przepustowości, a nie rodzaj liczby całkowitej, która jest sugerowana w oświadczeniach dotyczących wydajności, które są często sugerowane dla Węzła.

W każdym razie przepraszam, że to wszystko jest długie i chaotyczne, ale naprawdę czuję, że do tej pory dyskusja niczego nie udowodniła i chętnie usłyszę od kogoś w którejkolwiek z tych sytuacji:

a) prawdziwe wyjaśnienie, dlaczego Node powinien być lepszy (poza dwoma scenariuszami, które przedstawiłem powyżej, z których pierwszy (słabe dostrojenie) uważam za prawdziwe wyjaśnienie wszystkich testów, które do tej pory widziałem. [[edytuj ], im więcej o tym myślę, tym bardziej zastanawiam się, czy pamięć używana przez ogromną liczbę stosów może być tutaj znacząca. Domyślne rozmiary stosów dla współczesnych wątków bywają dość duże, ale pamięć przydzielana przez system zdarzeń oparty na zamknięciu byłby tylko tym, czego potrzeba)

b) prawdziwy test porównawczy, który faktycznie daje uczciwą szansę wybranemu serwerowi wątkowemu. Przynajmniej w ten sposób musiałbym przestać wierzyć, że twierdzenia są zasadniczo fałszywe;> ([edytuj] jest to prawdopodobnie silniejsze niż zamierzałem, ale wydaje mi się, że wyjaśnienia dotyczące korzyści w zakresie wydajności są w najlepszym razie niepełne, a przedstawione testy porównawcze są nieuzasadnione).

Na zdrowie, Toby


2
Problem z wątkami: potrzebują pamięci RAM. Bardzo zajęty serwer może mieć do kilku tysięcy wątków. Node.js unika wątków, dzięki czemu jest bardziej wydajny. Wydajność nie polega na szybszym uruchamianiu kodu. Nie ma znaczenia, czy kod jest uruchamiany w wątkach, czy w pętli zdarzeń. W przypadku procesora jest tak samo. Ale usuwając wątki oszczędzamy pamięć RAM: tylko jeden stos zamiast kilku tysięcy stosów. Zapisujemy również przełączniki kontekstu.
dokładnie

3
Ale węzeł nie pozbywa się wątków. Nadal używa ich wewnętrznie do zadań IO, czego wymaga większość żądań internetowych.
levi

1
Węzeł przechowuje również zamknięcia oddzwaniania w pamięci RAM, więc nie widzę, gdzie wygrywa.
Oleksandr Papchenko

@levi Ale nodejs nie używa rzeczy typu „jeden wątek na żądanie”. Używa puli wątków we / wy, prawdopodobnie w celu uniknięcia komplikacji przy użyciu asynchronicznych interfejsów API we / wy (a być może POSIX open()nie może zostać zablokowany?). W ten sposób amortyzuje każde uderzenie wydajności, w którym tradycyjny model fork()/ na pthread_create()żądanie musiałby tworzyć i niszczyć wątki. I, jak wspomniano w PostScript a), amortyzuje to również problem z miejscem na stosie. Prawdopodobnie możesz obsłużyć tysiące żądań z, powiedzmy, 16 wątkami We / Wy w porządku.
binki,

„Domyślne rozmiary stosów dla współczesnych wątków są zwykle bardzo duże, ale pamięć przydzielona przez system zdarzeń oparty na zamknięciu byłaby tylko tym, czego potrzeba”. Mam wrażenie, że powinny one być tego samego rzędu. Zamknięcia nie są tanie, środowisko wykonawcze będzie musiało zachować całe drzewo wywołań aplikacji jednowątkowej w pamięci („emulować stosy”, że tak powiem) i będzie w stanie wyczyścić, gdy liść drzewa zostanie zwolniony jako powiązane zamknięcie zostaje „rozwiązany”. Będzie to zawierało wiele odniesień do rzeczy na stosie, których nie można wyrzucać do śmieci, i wpłyną na wydajność w czasie czyszczenia.
David Tonhofer,

14

Nie rozumiem tylko tego, że Node.js nadal używa wątków.

Ryan używa wątków dla części, które blokują (większość pliku node.js korzysta z nieblokującego IO), ponieważ niektóre części są szalenie trudne do napisania bez blokowania. Ale wierzę, że Ryan marzy o tym, aby wszystko było nieblokujące. Na slajdzie 63 (projekt wewnętrzny) widać, że Ryan używa biblioteki libev (biblioteka, która wyodrębnia asynchroniczne powiadamianie o zdarzeniach) dla nieblokującej pętli zdarzeń . Z powodu pętli zdarzeń node.js potrzebuje mniej wątków, co zmniejsza przełączanie kontekstu, zużycie pamięci itp.


11

Wątki są używane tylko do obsługi funkcji niemających funkcji asynchronicznej, takich jak stat().

stat()Funkcja blokująca jest zawsze tak node.js musi wykorzystać, aby wykonać gwint rzeczywiste połączenia, bez blokowania głównego wątku (pętla zdarzenia). Potencjalnie żaden wątek z puli wątków nigdy nie zostanie użyty, jeśli nie trzeba wywoływać tego rodzaju funkcji.


7

Nic nie wiem o wewnętrznych działaniach node.js, ale widzę, jak użycie pętli zdarzeń może przewyższyć obsługę wątkowych operacji we / wy. Wyobraź sobie żądanie płyty, podaj mi staticFile.x, zrób 100 żądań dla tego pliku. Każde żądanie zwykle zajmuje wątek pobierający ten plik, czyli 100 wątków.

Teraz wyobraź sobie, że pierwsze żądanie tworzy jeden wątek, który staje się obiektem wydawcy, wszystkie 99 innych żądań najpierw sprawdza, czy istnieje obiekt wydawcy dla staticFile.x, jeśli tak, wysłuchaj go podczas pracy, w przeciwnym razie rozpocznij nowy wątek, a tym samym nowy obiekt wydawcy.

Po zakończeniu pojedynczego wątku przekazuje staticFile.x do wszystkich 100 detektorów i sam się niszczy, więc następne żądanie tworzy nowy nowy wątek i obiekt wydawcy.

Tak więc w powyższym przykładzie jest to 100 wątków vs 1 wątek, ale także wyszukiwanie 1 dysku zamiast 100 wyszukiwania dysku, zysk może być dość fenomenalny. Ryan jest mądrym facetem!

Innym sposobem na spojrzenie jest jeden z jego przykładów na początku filmu. Zamiast:

pseudo code:
result = query('select * from ...');

Ponownie 100 oddzielnych zapytań do bazy danych w porównaniu do ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Jeśli zapytanie już trwa, inne równe zapytania po prostu wskoczą na modę, dzięki czemu możesz mieć 100 zapytań w jednej rundzie bazy danych.


3
W przypadku bazy danych chodzi raczej o to, by nie czekać na odpowiedź przy wstrzymywaniu innych żądań (które mogą, ale nie muszą korzystać z bazy danych), ale raczej poprosić o coś, a potem pozwolić ci zadzwonić, kiedy wróci. Nie sądzę, aby łączyło to je ze sobą, ponieważ trudno byłoby śledzić reakcję. Nie sądzę też, aby istniał jakikolwiek interfejs MySQL, który pozwala przechowywać wiele niebuforowanych odpowiedzi na jednym połączeniu (??)
Tor Valamo

To tylko abstrakcyjny przykład wyjaśniający, w jaki sposób pętle zdarzeń mogą oferować większą wydajność, nodejs nic nie robi z DB bez dodatkowych modułów;)
BGerrissen

1
Tak, mój komentarz był bardziej zbliżony do 100 zapytań w jednej rundzie bazy danych. : p
Tor Valamo

2
Cześć BGerrissen: fajny post. Tak więc, gdy zapytanie jest wykonywane, inne podobne zapytania będą „nasłuchiwać”, tak jak przykład staticFile.X powyżej? na przykład 100 użytkowników pobierających to samo zapytanie, tylko jedno zapytanie zostanie wykonane, a pozostali 99 będą nasłuchiwać pierwszego? dzięki !
CHAPa

1
Wygląda na to, że nodejs automatycznie zapamiętuje wywołania funkcji lub coś takiego. Ponieważ nie musisz się martwić synchronizacją pamięci współużytkowanej w modelu pętli zdarzeń JavaScript, łatwiej jest bezpiecznie buforować rzeczy w pamięci. Ale to nie znaczy, że nodejs robi to magicznie dla ciebie lub że jest to rodzaj poprawy wydajności, o którą pytasz.
binki,
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.