...

Optymalna konfiguracja procesów roboczych NGINX w celu uzyskania maksymalnej wydajności

Konfiguruję NGINX Worker tak, aby wartości worker_processes, worker_connections i worker_rlimit_nofile były dokładnie dopasowane, a epoll działał w pętli zdarzeń. Dzięki temu korzystam z Rdzenie CPU zapewnij wydajność, umożliw planowanie skalowania liczby jednoczesnych połączeń oraz utrzymuj niskie opóźnienia w okresach szczytowego obciążenia.

Punkty centralne

Poniższe kluczowe aspekty pozwolą Ci od razu zorientować się, jak skonfigurować niezawodną instancję roboczą NGINX.

  • worker_processes powiązać z liczbą rdzeni logicznych, najlepiej z opcją „auto“.
  • worker_connections ustawić tak, aby z łatwością pokryć rzeczywiste szczyty.
  • rlimit_nofile oraz zwiększyć limity systemu operacyjnego odpowiednio do przepustowości łącza.
  • epoll oraz włączyć opcję multi_accept, aby efektywnie wykorzystać pętlę zdarzeń.
  • Testy obciążeniowe postępować i precyzyjnie dostosowywać, wykonując małe kroki.

Architektura NGINX: co to są serwery główne i pomocnicze

Rozdzielam zadania od Mistrz Rola serwera głównego i serwerów roboczych jest jasna: serwer główny ładuje konfiguracje, otwiera gniazda i uruchamia procesy, podczas gdy serwery robocze przetwarzają żądania w pętli zdarzeń. Każdy serwer roboczy działa niezależnie, reaguje na zdarzenia i może zarządzać tysiącami połączeń bez powodowania blokad. Model ten sprawdza się znakomicie, gdy odpowiednio wykorzystuję rdzenie procesora i optymalnie obsługuję pętlę zdarzeń za pomocą epoll. Pamiętam przy tym, że każdy dodatkowy przeskok przez serwer proxy zużywa zasoby połączeń, co znajduje odzwierciedlenie w limitach. Kto rozumie te role, ten świadomie podejmuje decyzje dotyczące Zasoby i zapobiega wczesnym wąskim gardłom.

Właściwe połączenie trzech kluczowych wytycznych

Rozważam worker_processes, worker_connections i worker_rlimit_nofile nigdy nie rozpatruję osobno, lecz jako całość. Łączna liczba możliwych połączeń wynika z iloczynu liczby procesów roboczych i liczby połączeń na jeden proces roboczy; na tej podstawie ustalam limity dla deskryptorów plików. Jeśli te parametry nie są ze sobą zgrane, napotykam błąd „too many open files“ lub występują twarde limity czasu. W przypadku dużego obciążenia potrzebuję spójnego łańcucha: wystarczającej liczby procesów, dużej liczby połączeń, odpowiednio zwiększonej wartości rlimit_nofile oraz odpowiednich parametrów systemu operacyjnego. W ten sposób zapobiegam sytuacji, w której zbyt mała Limit cała wydajność została ograniczona.

worker_processes: Wybierz konkretną liczbę

Ustawiłem worker_processes Zazwyczaj ustawia się tę opcję na „auto“, aby NGINX mógł rozpoznać liczbę logicznych rdzeni procesora i wykorzystać każdy z nich. Jeden proces roboczy na rdzeń pozwala uniknąć niepotrzebnych zmian kontekstu i zapewnia równomierny rozkład obciążenia, dzięki czemu czas reakcji pozostaje przewidywalny. Na maszynach z bardzo dużą liczbą rdzeni celowo testuję również mniejszą liczbę procesów roboczych, aby porównać trafienia w pamięci podręcznej i obciążenie rdzeni. Jeśli wskaźniki pokazują, że rdzenie są przeciążone lub rośnie liczba nieudanych odwołań do TLB, stopniowo dostosowuję liczbę procesów roboczych. Najpierw pomiar, potem zmiana – w ten sposób zapewniam sobie wiarygodne Wyniki.

worker_connections: planowane zwiększenie liczby połączeń

Wybieram worker_connections w zależności od ruchu docelowego i zestawu protokołów, często zaczynając od 2048 lub 4096. W przypadku intensywnie wykorzystywanych interfejsów API rozważam wartość 8192, o ile pozwalają na to ograniczenia systemu operacyjnego i pamięć RAM. Każde zwiększenie tej wartości sprawdzam za pomocą testów obciążeniowych, ponieważ otwarte połączenia zajmują pamięć i wpływają na wydajność sieci upstream. Jeśli dominują uzgodnienia SSL lub duże przesyłania danych, kieruję się raczej profilami obciążenia procesora i operacji wejścia/wyjścia, a nie tylko samą liczbą połączeń. W ten sposób zapewniam, że zdefiniowana na pracownika Pojemność pozostaje również przydatny w praktyce.

Synchronizacja parametrów `worker_rlimit_nofile` i limitów systemu operacyjnego

Dopilnuję, aby rlimit_nofile pokrywa co najmniej całkowitą pojemność obliczeniową i często jest skonfigurowany z rezerwą. W scenariuszach z odwrotnym proxy uwzględniam drugi deskryptor do serwera upstream na każde połączenie klienta. W związku z tym chętnie ustawiam rlimit_nofile na wartość dwukrotnie wyższą od oczekiwanej liczby jednoczesnych połączeń. Limity jądra i użytkownika (ulimit -n, fs.file-max) zwiększam tak, aby NGINX mógł faktycznie wykorzystać te wartości. Jeśli w dzienniku błędów pojawiają się komunikaty dotyczące otwartych plików, szybko zwiększam limity i obserwuję Opóźnienie ponownie pod obciążeniem.

Blok wydarzeń: efektywne wykorzystanie epoll i multi_accept

W bloku „Wydarzenia” aktywuję epoll i ustawiam multi_accept na „on“, aby procesy robocze przyjmowały oczekujące połączenia w jednym przebiegu. Epoll zmniejsza obciążenie przy dużej liczbie jednoczesnych gniazd i współgra z nieblokującą architekturą NGINX. Te opcje sprawdzają się podczas szczytów ruchu, ponieważ przyspieszają fazę przyjmowania połączeń i pozwalają szybciej przejść do właściwego przetwarzania. W systemie Linux jest to moje standardowe ustawienie, które zmieniam tylko w rzadkich, szczególnych przypadkach. Osoby pragnące zgłębić ten temat mogą porównać model pętli zdarzeń z Pula wątków a pętla zdarzeń i wyciąga z tego wniosek, że wnioski dla własnego otoczenia.

Afinność procesora: przypisywanie procesów roboczych do rdzeni

Ustawiłem affinność procesora dla procesu stosuję to celowo, gdy obciążenia są stałe i zależne od procesora. Schemat przypisywania rozdzielam za pomocą masek bitowych, aby uniknąć zmian kontekstu i zwiększyć lokalność pamięci podręcznej. W przypadku czterech rdzeni przypisuję maski w taki sposób, aby każdy proces pracujący otrzymał własny rdzeń. Następnie sprawdzam wskaźniki braków w pamięci podręcznej, mediany opóźnień oraz 99. percentyl, aby wyraźnie dostrzec efekt. Bardziej szczegółowe wyjaśnienia dotyczące powinowactwa i NUMA znajdziesz w skróconej formie na stronie Afinity procesora w praktyce, co przy precyzyjnym dostrajaniu Pracownik-układy są pomocne.

Planowanie wydajności: rezerwa wydajności i testy obciążeniowe

W przypadku połączeń planuję Bufor który znacznie przewyższa obserwowane wartości szczytowe, aby krótkotrwałe skoki nie powodowały bezpośredniego przekroczenia limitów. Jeśli jako punkt wyjścia podwoję obciążenie szczytowe, w wielu scenariuszach zyskuję solidny margines bezpieczeństwa. W przypadku silnie zmiennego ruchu zwiększam bufor jeszcze bardziej, aż 99. percentyle będą przebiegać płynnie. Następnie sprawdzam wąskie gardła za pomocą narzędzi takich jak wrk lub k6, obserwuję wskaźniki błędów i sprawdzam status otwartych połączeń. Dopiero gdy wskaźniki są spójne, celowo zwiększam lub zmniejszam poszczególne Wartości.

Konfiguracja i przykłady obliczeń

Obliczam przepustowość połączeń, mnożąc liczbę procesów roboczych przez liczbę połączeń na jeden proces roboczy, a następnie ustalam limity na wyższym poziomie, wynikającym z tych obliczeń. Przy czterech rdzeniach procesora z opcją „auto” i 4096 połączeń na proces roboczy, obliczeniowo dochodzę do 16 384 jednoczesnych połączeń. W scenariuszach z serwerem proxy ustawiam rlimit_nofile raczej na 32 768 lub więcej, aby uwzględnić gniazda upstream. W przypadku małych maszyn z dwoma rdzeniami często wystarcza 2048 połączeń na pracownika, o ile udział transferu wychodzącego i połączeń TLS pozostaje umiarkowany. Poniższa tabela pomaga w klasyfikacji Wartości początkowe:

Rdzenie CPU worker_processes worker_connections (Początek) Min. rlimit_nofile (wartość orientacyjna) Wskazówka
2 auto (≈2) 2048 ≥ 4096 Rezerwa zaplanować dla TLS/proxy
4 auto (≈4) 4096 ≥ 16 384 W przypadku serwerów proxy często współczynnik 2 dla FD
8 auto (≈8) 4096-8192 ≥ 32768 Test obciążenia podejmuje decyzję o podwyżce
16+ auto, ewentualnie mniej 8192+ ≥ 65535 Testowanie z wyczuciem i powściągliwością

NGINX Worker i Upstreams: właściwe wyważenie scenariuszy

Rozróżniam dostarczanie statyczne, tryb odwrotnego proxy oraz obciążenie bramy API, ponieważ mają one Pracownik-Różne wymagania konfiguracyjne. Treści statyczne zużywają mniej zasobów, podczas gdy TLS, kompresja i połączenia upstream obciążają w większym stopniu procesor i deskryptory plików (FD). Im większe klucze SSL i im więcej procedur uzgadniania połączenia, tym większe korzyści przynosi ustawienie „jeden worker na rdzeń“. Duże przesyłanki powodują przesunięcie profilu w kierunku operacji wejścia/wyjścia, co sprawia, że zwracam większą uwagę na rlimit_nofile i bufory sieciowe. W przypadku odczuwalnych opóźnień w przyjmowaniu danych lub odpowiedzi z backendu pomocny jest mi przegląd Kolejki i opóźnienia, aby uniknąć wąskich miejsc ukierunkowany rozwiązać.

Przebieg pracy w praktyce: krok po kroku do szybszego serwera

Zacznę od sporządzenia aktualnego zestawienia wszystkich istotnych Wartości W pliku nginx.conf sprawdzam liczbę rdzeni procesora, ustawienia ulimit oraz parametry jądra. Następnie ustawiam worker_processes na auto, ustalam worker_connections na przykład na 4096 i znacznie zwiększam wartość rlimit_nofile. W bloku Events włączam epoll oraz multi_accept, a następnie po przeładowaniu sprawdzam logi. Następnie przeprowadzam testy obciążeniowe w powtarzalnych warunkach, podczas których obserwuję czasy odpowiedzi, wskaźniki błędów i liczbę otwartych połączeń. Podczas dopracowywania ustawień zmieniam zawsze tylko jedną zmienną, dokumentuję każdy krok i sprawdzam skutki w Metryki.

Środowisko hostingowe: zasoby, jądro, sieć

Dbam o to, by było wystarczająco CPU-rdzenie, wystarczająca ilość pamięci RAM, szybkie dyski SSD lub NVMe oraz aktualne jądro systemu Linux. Tylko w ten sposób epoll, nowoczesne stosy TCP i przydatne funkcje odciążania działają niezawodnie. Parametry sieciowe, takie jak somaxconn i tcp_max_syn_backlog, dostosowuję do docelowej liczby połączeń, aby kolejki przyjmowania były krótkie. Warto zdecydować się na dostawcę oferującego wysoką wydajność operacji wejścia/wyjścia oraz swobodny dostęp do konfiguracji systemu. Z porównań wynika, że usługi o stałej Zasoby Znacznie zwiększyć możliwości NGINX.

Strategia keepalive: połączenia z klientem i połączenia upstream

Celowo wykorzystuję Keepalive jako narzędzie do regulacji przepustowości i opóźnień. Po stronie klienta ustawiam keepalive_timeout nie za wysoka, aby nieaktywne gniazda nie były niepotrzebnie worker_connections zablokować. Wartości z przedziału 10–30 s często stanowią dla mnie dobry kompromis między ponownym wykorzystaniem a zajęciem zasobów. Z keepalive_requests Ograniczam liczbę żądań na połączenie, aby ograniczyć długotrwałe połączenia i uniknąć obciążenia pamięci. Po stronie upstream (proxy odwrotne) utrzymuję trwałe połączenia z keepalive w bloku upstream, dzięki czemu nie są wymagane procedury uzgadniania połączenia ani konfiguracja TCP. Przy tym ostrożnie skaluję liczbę na backend w zależności od jego wydajności (max_conns), w przeciwnym razie sam generuję kolejki w upstreemie. Ważne: każde gniazdo keepalive liczy się jako otwarte połączenie i wymaga numerów FD; uwzględniam to w rlimit_nofile oraz moje plany dotyczące rezerwy mocy.

Optymalizacja list: reuseport, backlog i strategia akceptacji

Równomiernie rozkładam obciążenie, poprzez SO_REUSEPORT aktywuj (listen … reuseport). Dzięki temu każdy worker posiada własną kolejkę Accept, co ogranicza zjawisko „thundering herds“ i pozwala uniknąć punktów przeciążenia. W połączeniu z multi_accept znacznie przyspieszam fazę przyjmowania. Lista-zaległości (listen … backlog=) oraz odpowiadające im parametry jądra (somaxconn, tcp_max_syn_backlog) ustawiam na dość wysokie wartości, aby szczytowe obciążenia nie rozpraszały się na wejściu gniazda. Opcja odroczony odkłada akceptację do momentu otrzymania danych – w przypadku wielu żądań o krótkim czasie trwania może to pomóc; w pozostałych przypadkach sprawdzam to w testach. Czy accept_mutex Czy jest mi to potrzebne, sprawdzam w teście porównawczym: przy użyciu reuseportu zazwyczaj można się bez niego obejść; bez reuseportu może on poprawić sprawiedliwość, ale wymaga koordynacji. Tutaj podejmuję decyzję w oparciu o dane, nigdy na podstawie przeczucia.

Stabilne ustawienie limitów czasu i kolejek

Ustawiłem Limity czasu tak, aby wolno działające klienty nie przeciążały procesów roboczych: client_header_timeout oraz client_body_timeout Ustalam je na tyle wąskie, by uniknąć zawieszeń, ale na tyle szerokie, by były wygodne dla prawdziwych użytkowników. send_timeout zapobiega blokowaniu odpowiedzi wysyłanych do klienta. W kontekście serwera proxy definiuję proxy_connect_timeout, proxy_read_timeout oraz proxy_send_timeout rygorystycznie, aby zawieszone moduły zaplecza nie paraliżowały interfejsu użytkownika. W przypadku modułów zaplecza o ograniczonej równoległości korzystam z kolejka w bloku upstream z limitem czasu, aby złagodzić szczyty obciążenia i w kontrolowany sposób sygnalizować błąd 503, zamiast przypisywania wszystkich procesów roboczych do oczekujących gniazd upstream. Dodatkowo stabilizuję działanie za pomocą limit_req (Burst/Delay) oraz limit_conn optymalizacja ścieżek, tak aby poszczególne klienty lub boty nie zużywały nieproporcjonalnie dużej ilości zasobów.

Buforowanie, sendfile i AIO: świadomy wybór metod operacji wejścia/wyjścia

Ustawiłem sendfile dla plików statycznych i połącz to z tcp_nopush/tcp_nodelay w zależności od obciążenia, aby efektywnie grupować pakiety lub zmniejszyć opóźnienia interakcji. W przypadku dużych plików korzystam z kierunek powyżej pewnego progu, aby uniknąć zanieczyszczenia pamięci podręcznej i zapobiec wypchnięciu zawartości pamięci podręcznej stron. W trybie proxy sam decyduję, czy buforowanie proxy pomaga (szybkie przekazywanie danych do klienta, oddzielny odczyt z źródła) czy też w przypadku obciążeń związanych ze strumieniowaniem lepiej proxy_request_buffering zmniejszam, aby wcześnie zainicjować przesyłanie plików. Rozmiary proxy_buffers, proxy_buffer_size oraz duże bufory nagłówków klientów kontroluję to świadomie, aby zużycie pamięci na jedno połączenie nie wzrosło gwałtownie. Aby dostęp do plików nie obciążał nadmiernie procesora, rozważam aio (natywne lub wątki), ale należy przeprowadzić dokładne testy, ponieważ pętla zdarzeń i charakterystyka operacji wejścia/wyjścia wzajemnie na siebie oddziałują.

HTTP/2/HTTP/3 i TLS: wpływ na wydajność procesów roboczych

Biorę pod uwagę, że HTTP/2 oraz HTTP/3 Zmiana dynamiki połączeń: Wiele żądań jest przetwarzanych jako Strumienie poprzez niewielką liczbę połączeń TCP lub QUIC. Zmniejsza to liczbę połączeń, ale zwiększa zapotrzebowanie na moc procesora i pamięć na jedno połączenie (multipleksowanie, kompresja nagłówków, TLS/QUIC). Moje worker_connections Nie interpretuję tego zatem bezkrytycznie jako „taką samą liczbę żądań“. Zauważam, że strumienie równoległe na każde połączenie i pasuje keepalive_timeout i w razie potrzeby. http2_max_concurrent_streams . Po stronie TLS zyskuję dzięki funkcji wznowienia sesji (bilety/pamięć podręczna) oraz OCSP-Stapling; w ten sposób oszczędzam na kosztownych procedurach nawiązywania połączenia i utrzymuję niskie opóźnienia. Minus: dłuższe keepalive’y zajmują FD i pamięć RAM – dlatego planuję rlimit_nofile oraz limity pamięci z realistycznymi rezerwami. W przypadku algorytmów szyfrujących obciążających procesor warto przetestować opcję powinowactwa oraz nowoczesne akceleratory kryptograficzne.

Monitorowalność: stan, logi i wskaźniki

Zapewniam przejrzystość dzięki uproszczonemu Status-Endpoint (np. stub_status), aby sprawdzić aktywne połączenia, operacje odczytu/zapisu/oczekiwania oraz przyjęte żądania. W logach staram się ograniczać zbędne informacje: zwięzły log_format Z danymi dotyczącymi czasu, statusu, czasu przesyłu w górę i liczby bajtów wystarcza to do większości analiz. Przy bardzo wysokim QPS wyłączam dziennik dostępu selektywnie (na podstawie lokalizacji) lub buforuję logi asynchronicznie, aby operacje wejścia/wyjścia nie spowalniały systemu. Dziennik błędów ustawiam na ostrzeżenie lub błąd i przełącz się na chwilę na ten tryb wyłącznie w celu przeprowadzenia konkretnych analiz debugowanie. Na bieżąco koreluję opóźnienia (mediana/95. i 99. percentyl), otwarte połączenia, wskaźniki błędów backendu oraz obciążenie procesora na pracownika – na tej podstawie ustalam korekty trzech kluczowych dyrektyw i wcześnie wykrywam efekty nasycenia.

Kontenery i środowiska wirtualne: płynne przekazywanie limitów

Sprawdzam w kontenerach, czy cgroup-Ustal limity dla procesora, pamięci RAM i identyfikatorów PID, a następnie dostosuj je do ustawień serwera NGINX. ulimit -n musi być wystarczająco wysoka w obrębie kontenera, w przeciwnym razie moje ustawienia rlimit_nofile nie będą miały znaczenia. W przypadku limitów procesora (np. 2 vCPU) ustawiam worker_processes odpowiednio, aby harmonogramowanie nie powodowało sztucznego spieszenia. W przypadku sieci lokalnych korzystam z trybów sieciowych „host“, które charakteryzują się mniejszym opóźnieniem związanym z obciążeniem, podczas gdy sieci nakładkowe oznaczają dodatkowe przeskoki. Na hostach Multi-NUMA zwracam uwagę na powinowactwo i gniazda pamięci, aby procesy robocze nie działały w różnych węzłach. To samo dotyczy powinowactwa IRQ oraz RPS/XPS: jeśli ścieżki od karty sieciowej przez IRQ aż do jądra procesu roboczego są spójne, szczytowe opóźnienia zmniejszają się w wymierny sposób.

Cykl życia połączeń: porty efemeryczne, stan TIME_WAIT i rezerwy

Planuję wystarczającą ilość porty tymczasowe (ip_local_port_range), gdy NGINX działa jako aktywny klient w stosunku do serwerów upstream. Przy bardzo wysokiej przepustowości połączeń unikam nadmiernych zmian portów dzięki funkcji Upstream-Keepalive, co pozwala zmniejszyć stosy TIME_WAIT. Z opcji jądra dotyczących „ponownego wykorzystania“ korzystam tylko ostrożnie; nowoczesne stosy sieciowe już teraz optymalizują wiele procesów wewnętrznie. Bardziej stabilnym rozwiązaniem jest kontrolowanie czasu trwania połączeń za pomocą rozsądnych wartości keepalive i timeout oraz reuseport wykorzystać to do sprawiedliwego podziału. Przy obliczaniu przepustowości zawsze biorę pod uwagę nie tylko klientów, ale także stronę upstream – często to właśnie tam FD stanowią rzeczywisty czynnik ograniczający, a nie frontdoor.

Płynne przeładowywanie i wdrażanie bez przerw w działaniu

Korzystam z modelu master/worker do płynne przeładowania: Master ładuje nowe konfiguracje, stare worker’y wyłączają się, a nowe płynnie przejmują ich zadania. Dzięki worker_shutdown_timeout daję żądaniom czas na prawidłowe zakończenie, bez blokowania zasobów. Wdrożenia bez przestojów w środowisku upstream łączę z kontrolami stanu i proxy_next_upstream-Zasady, dzięki którym pojedyncze, wadliwe komponenty zaplecza nie powodują wzrostu ogólnego opóźnienia. Wprowadzając zmiany w konfiguracji, zawsze modyfikuję tylko jeden parametr i weryfikuję efekty w logach oraz metrykach – w ten sposób unikam błędów wynikających z niejasności i zapewniam powtarzalność wydajności.

Zwięzłe podsumowanie

Łączę worker_processes ustaw liczbę rdzeni (najlepiej automatycznie), dostosuj liczbę połączeń worker_connections do szczytowego obciążenia i znacznie zwiększ wartość rlimit_nofile wraz z limitami systemu operacyjnego. W bloku zdarzeń korzystam z epoll i multi_accept, sprawdzam wszystko za pomocą powtarzalnych testów obciążeniowych, a następnie dostosowuję ustawienia małymi krokami. W przypadku obciążeń proxy uwzględniam dodatkowe deskryptory i testuję powinowactwo procesora, gdy obciążenia są stałe. Różnicę stanowi prawidłowo skonfigurowany stos z odpowiednim jądrem, szybkim wejściem/wyjściem oraz sensownymi parametrami sieciowymi. W ten sposób osiągam NGINX niezawodnie w zakresie wydajności, jakiego wymagają zaawansowane strony internetowe i interfejsy API.

Artykuły bieżące