"Nie blokuj pętli zdarzeń..." - zapewne słyszałeś to zdanie wiele razy... Nie dziwię się, ponieważ jest to jedno z najważniejszych założeń podczas pracy z Node. Ale jest też druga "rzecz", której nie należy blokować - Worker Pool. Jej zaniedbanie może mieć znaczący wpływ na wydajność aplikacji, a nawet jej bezpieczeństwo.
Nici
Najważniejsza rzecz do zapamiętania: istnieją dwa rodzaje wątków w Node.js: Główny wątek - który jest obsługiwany przez Pętla zdarzeńoraz Pula pracowników (pula wątków) - która jest pulą wątków -
dzięki libuv. Każdy z nich ma inne zadanie. Celem pierwszego z nich jest obsługa nieblokujących operacji wejścia/wyjścia, a drugi jest odpowiedzialny za intensywną pracę procesora, a także blokowanie operacji wejścia/wyjścia.
Ale czym jest wątek i czym różni się od procesu? Istnieje kilka różnic, ale najważniejszą dla nas jest sposób, w jaki przydzielana jest im pamięć. O procesie można myśleć jak o aplikacji. Wewnątrz każdego procesu znajduje się kawałek pamięci przeznaczony tylko dla niego. Tak więc jeden proces nie ma dostępu do pamięci drugiego, a ta właściwość zapewnia wysokie bezpieczeństwo. Aby ustanowić komunikację między nimi, musimy wykonać pewną pracę. Wątki są inne. Wątki działają wewnątrz procesu i współdzielą tę samą pamięć, więc nie ma żadnego problemu z wątkami współdzielącymi dane.
Jednak jedna kwestia powoduje problem. Jest to tzw. warunek wyścigu. Wątki mogą działać w tym samym czasie, więc skąd mamy wiedzieć, który kończy się pierwszy? Może się zdarzyć, że przy pierwszym uruchomieniu pierwsza operacja zakończy się jako pierwsza, a następnym razem może być odwrotnie i druga operacja zakończy się przed pierwszą. Wyobraź sobie pracę z operacjami zapisu/odczytu w takich warunkach! Koszmar! Czasami bardzo trudno jest napisać poprawne kod w środowisku wielowątkowym.
Ponadto języki wielowątkowe mają duży narzut pamięciowy, ponieważ tworzą osobny wątek dla każdego żądania; więc jeśli chcesz wywołać 1000 żądań, tworzą 1000 wątków.
Jak poradzić sobie z takim problemem? Zamiast tego użyć pojedynczego wątku! I to jest właśnie to Węzeł oferuje.
Jako JavaScript deweloper Zachęcam do obejrzenia film
w którym Bart Belder jasno wyjaśnia koncepcję pętli zdarzeń. Powyższy diagram pochodzi z jego prezentacji. A jeśli w ogóle nie znasz tych terminów, zarówno Węzeł Libuv i Libuv mają doskonałą dokumentację 🙂
O blokowaniu
W Rozwój JavaScript mówią, że ponieważ Węzeł jest jednowątkowa i nieblokująca, można osiągnąć wyższą współbieżność przy tych samych zasobach niż w przypadku rozwiązań wielowątkowych. To prawda, ale nie jest to tak piękne i łatwe, jak mogłoby się wydawać.
Od Node.js jest jednowątkowa (część JS), zadania intensywnie wykorzystujące procesor będą blokować wszystkie żądania w toku, dopóki dane zadanie nie zostanie zakończone. Tak więc prawdą jest, że w Node.js można zablokować każde żądanie tylko dlatego, że jedno z nich zawierało instrukcję blokującą. Kod blokujący oznacza, że jego zakończenie zajmuje więcej niż kilka milisekund. Nie należy jednak mylić długiego czasu odpowiedzi z blokowaniem. Odpowiedź z bazy danych może trwać bardzo długo, ale nie blokuje procesu (aplikacji).
Metody blokujące są wykonywane synchronicznie, a metody nieblokujące są wykonywane asynchronicznie.
Jak spowolnić (lub zablokować) pętlę zdarzeń?
- podatne wyrażenie regularne - podatne wyrażenie regularne to takie, na którym silnik wyrażeń regularnych może zająć wykładniczy czas; możesz przeczytać o nich więcej tutaj,
- Operacje JSON na dużych obiektach,
- przy użyciu synchronicznych interfejsów API z Węzeł zamiast wersji asynchronicznych; wszystkie metody I/O w bibliotece standardowej Node.js zapewniają również ich wersje asynchroniczne,
- inne błędy programistyczne, takie jak synchroniczne nieskończone pętle.
W takim przypadku, skoro Worker Pool korzysta z puli wątków, czy możliwe jest ich zablokowanie? Niestety tak 🙁 Węzeł opiera się na filozofii jeden wątek dla wielu klientów.
Załóżmy, że dane zadanie wykonywane przez konkretnego Workera jest bardzo złożone i wymaga więcej czasu na jego ukończenie. W rezultacie Worker zostaje zablokowany i nie można go użyć do wykonania żadnego z innych oczekujących zadań, dopóki jego instrukcje nie zostaną wykonane. Jak już zapewne się domyśliłeś, może to mieć wpływ na wydajność. Możesz zapobiec takim problemom, minimalizując różnice w czasach zadań za pomocą partycjonowania zadań.
Wnioski
Unikaj blokowania, to na pewno. Jeśli tylko możesz, zawsze wybieraj asynchroniczne wersje standardowych API bibliotek. W przeciwnym razie po uruchomieniu aplikacji klient może doświadczyć kilku problemów, zaczynając od obniżonej przepustowości, a kończąc na całkowitym wycofaniu, co jest fatalne z punktu widzenia użytkownika.
Czytaj więcej:
Dlaczego (prawdopodobnie) powinieneś używać Typescript
Jak nie zabić projektu złymi praktykami kodowania?
Strategie pobierania danych w NextJS