Strona główna / Artykuły

Wewnątrz libuv: silnik w języku C stojący za asynchronicznym I/O w Node.js

Wewnątrz libuv, biblioteki w języku C używanej w Node.js: jak pętla zdarzeń, zbiór wątków, obsługa zdarzeń i żądania umożliwiają pracę bez blokowania wejścia/wyjścia oraz gdzie faktycznie znajdują się wątki.

Wydawca: REACTAPP.TOP6 min czytania1352 słów

Źródła: Inside libuv: The C Engine That Powers Node.js Asynchronous I/O

To przewodnik odbudowuje funkcjonalną ścieżkę dla tematu: Wewnątrz libuv: silnik w języku C napędzający asynchroniczną obsługę wejścia/wyjścia w Node.js. Skupia się na umowach, sprawdzaniach oraz kodzie, który można bez problemu dodać do repozytorium, nie musząc zgadywać jego intencji. Aby uzyskać ogólny obraz, należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Lepiej używać małych, testowalnych jednostek niż rozbudowanych skryptów. Gdy dany krok zawiedzie, powód awarii powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną sekwencję operacji.

Bazowe elementy: obsługi i żądania

Dla podstawowych elementów: Handle i Requests – zdefiniuj wprowadzane dane, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy wprowadzanymi danymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadania. Ustal wersje zależności i zapisz hash obrazu, który został użyty do uruchomienia demonstracji.

1. Handle

1. W przypadku elementów sterujących należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania i koszty obok wyników funkcjonalnych. Wczesna widoczność zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Należy również ustalić wersje zależności i zapisać hash obrazu, który uruchomił demonstrację.

2. Zapytania

Dla zapytań typu 2 należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Należy ustalić konkretne wersje zależności i zapisać hash obrazu, który został użyty do uruchomienia demonstracji. Dla zapytań typu 2 należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, łatwe do przetestowania jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinno to wskazywać na konkretną przyczynę, a nie na skomplikowaną strukturę procesów.

uv_loop_t – struktura: serce całego systemu

Dla struktury uv_loop_t – serca maszyny – należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania. Zrozum granice pętli zdarzeń: co blokuje wątek, a co czeka na interwencję jądra.

Problem pollingu wejścia/wyjścia: gotowość versus ukończenie

W przypadku problemu z pollingiem wejść/wyjść: gotowość versus zakończenie, należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną czynność oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić tę czynność od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy trwania i koszty obok wyników funkcjonalnych. Wczesna widoczność zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Trzeba zrozumieć granice pętli wydarzeń: co blokuje wątek, a co czeka na działanie jądra.

Pool wątków: symulowanie asynchroniczności tam, gdzie to niemożliwe

Dla puli wątków: przy symulowaniu asynchroniczności w przypadkach, gdzie to niemożliwe, należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Należy zrozumieć granice pętli wydarzeń: co blokuje wątek, a co czeka na interakcję z jądrem systemu. Dla puli wątków: przy symulowaniu asynchroniczności w przypadkach, gdzie to niemożliwe, należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, łatwe do testowania jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powód awarii powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną strukturę przepływu zadań.

Czasy wykonywania: implementacja min-heapa

Dla czasów wykonywania: implementacja min-heapa należy najpierw zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia, zanim zostanie zmieniony kod. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć przypadkowe, częściowe ukończenie zadań. Zmierz opóźnienie pod obciążeniem przed zmianą czasu wykonywania. Mikropomiary rzadko odpowiadają warunkom w środowisku produkcyjnym.

Kolejność wykonywania faz: bliższy rzut oka na uv__run_idle

Dla kolejności wykonywania faz: bliższe przyjrzenie się uv__run_idle – należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanej punktacji kontrolnej, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania i koszty obok wyników funkcjonalnych. Wczesna widoczność zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Należy zmierzyć opóźnienia przy obciążeniu przed modyfikacją czasów wykonywania. Mikrotesty rzadko odpowiadają warunkom w środowisku produkcyjnym.

// Simplified libuv uv_run internal logic
while (true) {
    // 1. Run timers phase (drain min-heap)
    uv__run_timers(loop);

    // 2. Run pending callbacks (thread pool completions)
    uv__run_pending(loop);

    // 3. Run idle handles (internal use)
    uv__run_idle(loop);

    // 4. Run prepare handles (before I/O polling)
    uv__run_prepare(loop);

    // 5. Calculate poll timeout and block for I/O
    // (Timeout is based on the next timer in the min-heap)
    uv__io_poll(loop, timeout);

    // 6. Run check handles (setImmediate in Node.js)
    uv__run_check(loop);

    // 7. Run close callbacks
    uv__run_closing_handles(loop);

    // Check if loop should exit (no more active handles)
    if (loop->active_handles == 0) break;
}

Wniosek: Nieznany bohater JavaScriptu po stronie serwera

Podsumowując: Nieznany bohater JavaScriptu serwerowego – zdefiniuj dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, skrypty przechowujące dane poufne oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Zmierz opóźnienia pod obciążeniem przed zmianą czasu wykonywania. Mikrotesty rzadko odzwierciedlają warunki produkcji. Podsumowując: Nieznany bohater JavaScriptu serwerowego – zdefiniuj dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić dany krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Wolno preferować małe, testowalne jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na ogólny problem.

Przeanalizuj pipeline.

Lista kontrolna operacyjna

Dla listy kontrolnej operacyjnej należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu.

Zdokumentuj zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później.

Zrozum granice pętli zdarzeń: co blokuje wątek, a co czeka na jądro.

Napisz krótki podręcznik obsługi: jak rotować klucze, jak opróżnić kolejkę, jak cofnąć ostatnią zmianę.

Niech lepsze będą małe, testowalne jednostki niż rozbudowane skrypty. Gdy dany krok zawiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany pipeline.

Zrozum granice pętli zdarzeń: co blokuje wątek, a co czeka na jądro.

Zanim zaczniesz promować tę architekturę, zamroź wersje oprogramowania, stwórz dokładny zapis procesu dla kluczowych ścieżek działania oraz potwierdź kroki odwracające zmiany. Środowiska współdzielone wymagają ograniczeń szybkości działania, weryfikacji uprawnień użytkowników oraz wyraźnego odpowiedzialnego za rotację haseł. Wolimy nudną niezawodność od pomysłowych, jednorazowych demonstracji.

Literatura pokrewna

  • JavaScript event loop: microtasks, macrotasks i stack wywołań — Prześledź, jak obietnice (promises), timerzy oraz funkcje zwrotne fetch przemieszczają się w kolejkach microtask i task, dzięki czemu logi asynchroniczne nie będą wyglądać losowo podczas rozmów kwalifikacyjnych ani przy błędach w produkcji.
  • Node.js vs Bun vs Deno: trzy filozofie runtime’ów dla JavaScript — Node optymalizuje ciągłość działania, Deno poprawność oraz nowoczesne standardy, a Bun przyspiesza pracę programistów — wybór powinien opierać się na ograniczeniach organizacyjnych, a nie tylko na wynikach testów.
  • Scraping w środowisku produkcyjnym poza zabezpieczeniami anty-botów: praktyczne wzorce — limity szybkości, higiena identyfikatorów oraz odpornie działające ścieżki pobierania danych dla aplikacji, które muszą w odpowiedzialny sposób zbierać dane publiczne.