...

Optymalizacja żądań Keepalive w NGINX: maksymalna wydajność serwera WWW dzięki ukierunkowanemu dostrajaniu

Z nginx keepalive Obniżam koszty nawiązywania połączeń, ograniczam liczbę procedur uzgadniania i wyraźnie skracam czas odpowiedzi. Precyzyjnie dostosowane limity czasu, ograniczenia liczby żądań na połączenie oraz ponowne wykorzystywanie gniazd upstream zapewniają wymierny wzrost wydajności bez konieczności zakupu nowego sprzętu.

Punkty centralne

  • Limity czasu Warto wybrać odpowiednie ustawienia: czas bezczynności powinien być tak krótki, jak to konieczne, i tak długi, jak to przydatne.
  • Żądania Ograniczanie liczby połączeń: trwałe gniazda, brak zawieszania się.
  • Pule upstreamowe Włącz: Stałe połączenia z backendem dla każdego pracownika.
  • Pracownik oraz dostosować połączenia: wystarczająca liczba slotów dla klientów w trybie bezczynności i aktywnych.
  • Monitoring Skonfigurować: monitorować szybkość połączenia, opóźnienie i liczbę błędów.

NGINX Keepalive: działanie i koszty

Celowo utrzymuję otwarte połączenia TCP, ponieważ Uściski dłoni są kosztowne i dominują w przypadku wielu małych żądań. Trwałe gniazda nie tylko oszczędzają RTT, ale także wyrównują obciążenie procesora, ponieważ kryptografia dla protokołu TLS uruchamia się rzadziej. Każde otwarte połączenie zajmuje jednak Zasoby, na przykład deskryptory plików i bufory, na które muszę zwracać uwagę. Sztuka polega na znalezieniu odpowiedniej równowagi: wystarczająca ilość ponownego wykorzystania, by zapewnić szybkość, oraz wystarczający budżet na nowe połączenia w okresach szczytowego obciążenia. Kto osiągnie tę równowagę, uzyska stale niskie wartości TTFB i zapewni użytkownikom wrażenie szybkości działania.

HTTP/2 i HTTP/3: multipleksowanie spotyka się z keepalive

Z HTTP/2 oraz HTTP/3 zmniejsza się liczba niezbędnych połączeń na klienta, ponieważ przez jedno łącze przepływa kilka strumieni. Funkcja Keepalive pozostaje jednak istotna: jedno połączenie powinno pozostawać niezawodnie otwarte, w przeciwnym razie korzyść wynikająca z multipleksowania zostanie zniwelowana przez częste ponowne nawiązywanie połączeń.

Zwracam uwagę na dedykowane parametry stanu bezczynności dla nowoczesnych protokołów i dbam o to, by wartości te były dostosowane do limitów czasu moich klientów. Podczas testów zaczynam od umiarkowanych wartości i zwiększam je przy stabilnym obciążeniu, aż częstotliwość ponownego nawiązywania połączeń spadnie, a opóźnienia pozostaną na stałym poziomie.

http {
    # HTTP/2: czas bezczynności dla nieużywanych, ale otwartych strumieni
    http2_idle_timeout 60s;

    # HTTP/3/QUIC: podobna logika dla połączeń opartych na protokole UDP
    http3_idle_timeout 60s;

    # Wznowienie połączenia TLS zmniejsza koszty uzgadniania połączenia przy ponownym nawiązywaniu połączeń
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
}

Multipleksowanie zmniejsza wymaganą liczbę równoległych połączeń TCP/QUIC, ale nie zmniejsza znaczenia prawidłowe przerwy na żądanie. Użytkownicy protokołów HTTP/2/3 często mogą ustawić nieco dłuższe limity czasu po stronie klienta, ponieważ wiele niewielkich zasobów przepływa przez ten sam kanał. Ważne: należy zachować możliwość pomiaru Czas do pierwszego bajtu, wskaźniki błędów i otwarte strumienie dla każdego połączenia.

Prawidłowe skonfigurowanie funkcji Client-Keepalive

W przypadku klientów przeglądarkowych zarządzam ponownym wykorzystaniem za pomocą keepalive_timeout oraz keepalive_requests, aby gniazda działały wystarczająco długo, nie blokując się w nieskończoność. Na początek ustawiam limit czasu na 30–60 sekund i 100–300 żądań na połączenie, a następnie dostosowuję te wartości na podstawie wskaźników. Szczegółowe wyjaśnienie można znaleźć w tym Przewodnik po limitach czasu keepalive, który wyjaśnia wpływ na opóźnienia i zasoby serwera. Krótsze limity czasu sprawdzają się w przypadku bardzo dużej liczby krótkich wywołań, natomiast dłuższe pomagają przy okresowych dostępach do API. Na początek ustalam jasne wartości domyślne i mierzę wpływ na liczbę otwartych połączeń oraz rodzaje błędów.

http {
    # Połączenia w stanie bezczynności z klientem
    keepalive_timeout 60s;
    # Maksymalna liczba żądań na połączenie TCP
    keepalive_requests 200;

    # Opcjonalnie: wyłączenie funkcji Keep-Alive dla określonych klientów (błędy starszych wersji)
    # keepalive_disable msie6;
}

Funkcja Upstream-Keepalive w serwerze proxy odwrotnym

Pomiędzy NGINX a aplikacjami backendowymi korzystam z trwałych gniazd upstream, ponieważ nawiązanie połączenia z PHP-FPM, Node.js lub usługami w języku Python również Opóźnienie kosztuje. W tym celu aktywuję w puli upstream odpowiednią liczbę połączeń wielokrotnego użytku na każdego pracownika. Ważne jest, aby używać protokołu HTTP/1.1 w kierunku serwera oraz pustego nagłówka Connection, w przeciwnym razie żądanie klienta „close“ przerwie trwałość połączenia z backendem. Kieruję się liczbą jednoczesnych żądań i konfiguruję pulę tak, aby prawie nie powstawały nowe połączenia. W ten sposób skraca się czas nawiązywania połączenia z backendem, a cały łańcuch zapewnia szybsze Odpowiedzi.

upstream backend {
    server 127.0.0.1:9000;
    keepalive 64; # Liczba trwałych połączeń upstream na każdy worker
}

server {
    location / {
 proxy_pass http://backend;
        proxy_http_version 1.1;
 proxy_set_header Connection "";
 # Utrzymywanie połączeń TCP dla gniazd upstream na poziomie systemu operacyjnego
 proxy_socket_keepalive on;
    }
}

Wymiarowanie basenu i budżet na połączenia

Obliczam puli w sposób realistyczny: liczba trwałych połączeń upstream wynika z worker_processes × keepalive na każdy upstream. Jeśli używa się 8 procesów roboczych i wartości keepalive wynoszącej 64, otwarte pozostaje do 512 gniazd na każdy upstream – na każdą instancję. W przypadku korzystania z modułu równoważenia obciążenia lub przy większej liczbie celów upstream liczba ta może szybko wzrosnąć.

Moja docelowa wartość: wystarczająca liczba otwartych gniazd, aby większość żądań bez nowego Connect jest obsługiwany, ale pozostaje jeszcze margines na obsługę szczytów. Monitoruję wskaźnik „nowe połączenia wychodzące na sekundę“ i zmniejszam go, aż dalsze zwiększanie rozmiaru puli nie przyniesie już znaczącej poprawy opóźnienia.

Biorę również pod uwagę Sprawiedliwość: Zbyt duże pule mogą stawiać nowo przybyłych klientów w niekorzystnej sytuacji, ponieważ sloty pracowników zajmują nieaktywne połączenia. Umiarkowany limit połączony z aktywnym monitorowaniem zazwyczaj działa szybciej niż wartości maksymalne ustalane na chybił trafił.

Dostrajanie: limity czasu i limity żądań

Łączę limit czasu i limit żądań w taki sposób, aby połączenia były efektywnie ponownie wykorzystywane, bez Narciarz biegowy . Wysokie wartości na obu osiach minimalizują liczbę połączeń typu „Connect”, ale zwiększają ryzyko zawieszania się gniazd w przypadku problemów z siecią. Niskie wartości zapewniają świeże połączenia, ale wymagają dodatkowych procedur uzgadniania. Postępuję małymi krokami, obserwuję błędy i dostosowuję ustawienia w określonych odstępach czasu. Poniższa tabela przedstawia sensowne przedziały wartości początkowych dla różnych wzorców użytkowania i dostarcza zwięzłą Orientacja.

Scenariusz keepalive_timeout keepalive_requests Wskazówka
Wiele krótkich odsłon stron 10–30 s 100-300 Szybkie ponowne wykorzystanie, niewielkie obciążenie w stanie bezczynności
Typowa strona internetowa 60–120 s 200–400 Dobry poziom średni dla plików Assets i HTML
API z okresowymi wywołaniami 60–120 s 300–1000 Wyższy wskaźnik ponownego wykorzystania wśród klientów
Usługi wewnętrzne / bramy 30–90 s 500–1000+ Stałość jest ważniejsza niż minimalna liczba połączeń

Optymalizacja pracowników i powiązania

Włożyłem worker_processes na „auto” lub na liczbę rdzeni procesora i zaplanuj wystarczającą ilość worker_connections ponieważ gniazda w stanie bezczynności zajmują sloty. Zbyt niskie limity uniemożliwiają przyjmowanie nowych połączeń, mimo że procesor dysponuje jeszcze wolną mocą obliczeniową. Kto korzysta z dużych pul keepalive, potrzebuje wystarczającej liczby deskryptorów i slotów zdarzeń na każdego pracownika. Dobrym wprowadzeniem do tematu jest „Skalowanie połączeń pracowników“, która wyjaśnia powiązania między zdarzeniami, połączeniami i obciążeniem. Przemyślane wartości gwarantują, że ponowne wykorzystanie w stanie bezczynności i nowe połączenia mogą funkcjonować równolegle.

worker_processes auto;

events {
    worker_connections 4096;
    # Opcjonalnie: reuseport może poprawić rozkład obciążenia na poziomie jądra
    # multi_accept on;
}

http {
    keepalive_timeout 60s;
    keepalive_requests 200;

 upstream backend {
 server 127.0.0.1:9000;
 keepalive 64;
    }
}

Optymalizacja systemu operacyjnego i gniazd

Sprawdzam ograniczenia systemowe, aby funkcja Keepalive mogła w pełni wykorzystać swój potencjał. Zbyt mała liczba deskryptorów lub przepełnione kolejki gniazd powodują sztuczne wąskie gardła. Oprócz ulimit i worker_rlimit_nofile decydujące znaczenie mają ograniczenia jądra.

# Przykładowe wartości sysctl (należy dostosowywać je ostrożnie i po przeprowadzeniu testów)
fs.file-max = 1000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 262144

Dostosowuję te wartości do warunków otoczenia: wiele połączeń krótkotrwałych zyskuje na szerszym zakresie portów i krótszych czasach trwania stanów FIN i TIME_WAIT. W przypadku keepalive’u w kierunku upstream zmniejszam Neuconnects, co powoduje zmniejszenie obciążeń związanych z fazą TIME_WAIT. Ponadto biorę pod uwagę NAT-Urządzenia pomiędzy serwerem proxy a serwerem zaplecza: Zbyt agresywne limity czasu bezczynności w sieci powodują nieprzewidywalne rozłączanie połączeń. Umiarkowany limit żądań na gniazdo oraz mechanizmy utrzymywania połączeń TCP (proxy_socket_keepalive on;) zapobiegają „starym“ połączeniom.

Prawidłowe ustawienie nagłówka i wersji HTTP

Zwracam uwagę na HTTP/1.1 do backendu, ponieważ funkcja Upstream-Keepalive działa tylko w ten sposób. Ponadto usuwam aktywne sterowanie połączeniami za pomocą nagłówków, aby NGINX samodzielnie zarządzał trwałością połączeń. Po stronie klienta pozwalam, aby funkcja Keep-Alive działała zgodnie ze standardem i ograniczam czas życia połączeń za pomocą limitu czasu (timeout) oraz limitu żądań (request-limit). Dodatkowo sprawdzam limity czasu bezczynności serwerów backendowych (backend-idle-timeouts) i ustawiam je na wartość minimalnie wyższą niż w NGINX, aby uniknąć błędów resetowania. Poprawnie skonfigurowane nagłówki zapewniają Ponowne użycie bez niepożądanych zamknięć.

# Przykład: Lokalizacja proxy z poprawnymi nagłówkami
location /api/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

Różnica: HTTP Keep-Alive a TCP Keep-Alive

Dokonuję ścisłego rozróżnienia między HTTP Keep-Alive (kilka żądań HTTP na jedno połączenie) oraz TCP-Keepalive (Testy na poziomie systemu operacyjnego w celu wykrycia nieaktywnych punktów końcowych). Funkcję HTTP Keep-Alive kontroluję za pomocą keepalive_timeout oraz keepalive_requests, podczas gdy komunikaty TCP Keepalive, w zależności od stosu, wynoszą proxy_socket_keepalive on; oraz parametry systemowe. W przypadku serwerów działających w niestabilnych sieciach włączam funkcję TCP-Keepalive, aby szybciej usuwać zawieszone gniazda.

Procesy długotrwałe i przypadki szczególne: WebSockets, SSE, gRPC

WebSockets i zdarzenia wysyłane przez serwer są Narciarz biegowy, które utrzymują połączenie przez długi czas – w tym przypadku klasyczna metoda Reuse odgrywa drugorzędną rolę. Dbam o odpowiednie proxy_read_timeout i chroń mnie za pomocą send_timeout przeciwko Slowloris-efekty. W przypadku gRPC (opartego na HTTP/2) należy uwzględnić kwestie związane z multipleksowaniem; limity czasu bezczynności ustalam tak, aby strumienie nie były niepotrzebnie przerywane.

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
    send_timeout 30s;
}

Monitorowanie i wskaźniki

Sukces oceniam na podstawie wskaźników, takich jak odsetek nowych połączeń typu upstream, upstream_connect_time oraz odsetek otwartych połączeń na jednego pracownika. Spadające wskaźniki połączeń przy stałej lub rosnącej liczbie żądań wskazują na skuteczne ponowne wykorzystanie połączeń. Nietypowe przekroczenia limitów czasu lub resetowanie połączeń sygnalizują rozbieżności w limitach czasu między NGINX a zapleczem. Ponadto obserwuję pamięć, deskryptory plików i kolejki zdarzeń pod obciążeniem. Regularne sprawdzanie pozwala wcześnie wykrywać trendy i zapobiega kosztownym Awarie.

Ulepszenia w zakresie rejestrowania w celu zapewnienia przejrzystości ponownego wykorzystania

Aby uzyskać lepszy wgląd w dane, rozszerzam dziennik dostępu o szczegóły połączeń. Dzięki temu mogę sprawdzić, jak często ponownie wykorzystywane jest dane połączenie TCP oraz jak zmieniają się czasy nawiązywania połączeń.

log_format keepalive_fmt
  '$remote_addr $host "$request" $status $body_bytes_sent '
  '$request_time $upstream_connect_time '
  'conn:$connection reqs:$connection_requests';

access_log /var/log/nginx/access_keepalive.log keepalive_fmt;

Obserwuję wartości mediany oraz P95/P99 wynoszące upstream_connect_time oraz podział $_żądania_połączeń. Rosnące liczby ponownych prób przy stabilnym opóźnieniu oznaczają, że pule i limity czasu zostały odpowiednio dobrane.

Typowe przeszkody i rozwiązania

Zbyt duże pule zajmują gniazda połączeń, podczas gdy nowi klienci czekają, dlatego utrzymuję ich rozmiary na umiarkowanym poziomie i monitoruję je. Różnice w czasach wygaśnięcia bezczynności między serwerem proxy a serwerem zaplecza powodują resetowanie, więc ustawiam czas wygaśnięcia na serwerze zaplecza na minimalnie wyższy niż NGINX. Zapomniane „Connection: close“ w nagłówku proxy przerywa trwałość połączenia, dlatego konsekwentnie czyszczę ten nagłówek. Negocjacja TLS może obciążać procesor przy dużej liczbie nowych połączeń, co łagodzę poprzez zwiększenie udziału ponownego wykorzystania. W przypadku sporadycznych błędów sieciowych pomocne jest umiarkowane ograniczenie liczby żądań na gniazdo, dzięki czemu stare Sesje nie żyć wiecznie.

Konfiguracje dostosowane do praktyki

W przypadku stron internetowych o dużym natężeniu ruchu wybieram krótki czas oczekiwania i średnio wysoki limit żądań, aby zasoby działały wydajnie. W przypadku interfejsów API z powtarzającymi się wywołaniami zwiększam ten limit, aby jeszcze bardziej skrócić czas uzgadniania połączeń TCP i TLS. Wielkość pul upstreamowych dobieram na podstawie przewidywanej równoległości i testuję przy użyciu realistycznego ruchu. Każde środowisko zachowuje się inaczej, dlatego po wprowadzeniu zmian sprawdzam opóźnienia i profile błędów. Oto dwa przykłady: Wartości początkowe, które następnie dopracowuję za pomocą wskaźników.

# Scenariusz 1: Strona internetowa o dużym natężeniu ruchu
http {
    keepalive_timeout 30s;
    keepalive_requests 300;

 upstream app {
 server 127.0.0.1:8080;
        keepalive 32;
    }

 server {
 listen 443 ssl http2;
 Monitorowanie czasu bezczynności HTTP/2 w trybie #
 http2_idle_timeout 45s;
    }
}
# Scenariusz 2: API z okresowymi wywołaniami
http {
    keepalive_timeout 75s;
    keepalive_requests 1000;

    upstream api_backend {
    server 127.0.0.1:9001;
    keepalive 64;
    }

    server {
 listen 443 ssl http2;
 # Nieco dłuższe okno bezczynności dla powtarzających się wywołań
 http2_idle_timeout 75s;
    }
}

Lista kontrolna dotycząca optymalizacji iteracyjnej

Zacznę od analizy obecnej sytuacji: rytm wyznaczają wzorce ruchu, czasy odpowiedzi i wskaźnik błędów. Następnie ustawiam limit czasu klienta i limit żądań na rozsądne wartości początkowe oraz aktywuję pule upstream. Limity czasu bezczynności backendu ustawiam nieco wyżej niż w NGINX, aby uniknąć nieoczekiwanych Resety występują. Następnie monitoruję liczbę połączeń, czas połączenia oraz liczbę otwartych gniazd na każdego pracownika. Osoby, które chcą zgłębić temat stopnia ponownego wykorzystania, znajdą wskazówki dotyczące Ponowne użycie połączenia oraz rozsądne pułapy.

Dodatkowa diagnoza: niedopasowania i zachowanie czasowe

Gdy połączenia zrywają się pozornie „bez powodu“, szukam Niedopasowania w łańcuchu: bezczynność klienta vs. limit czasu NGINX vs. bezczynność serwera zaplecza oraz pośredniczące urządzenia NAT/bramy. Nieznacznie zwiększam limit czasu serwera zaplecza powyżej wartości NGINX, sprawdzam kody resetowania w dzienniku błędów i obserwuję, czy upstream_connect_time Wykazuje skoki. Często wystarczy niewielki bufor (np. +10–20%) w przypadku przekroczenia limitu czasu po stronie serwera, aby wyeliminować resetowanie.

Zwracam również uwagę na „ujęcie zbliżające się powoli“Fazy -: Podczas zamykania NGINX na krótko przepuszcza przychodzące dane, co angażuje zasoby procesów roboczych. Bardzo duża liczba jednoczesnych zamknięć może blokować zdarzenia. W takich przypadkach dostosowuję przedziały czasowe zamykania i utrzymuję w równowadze całkowitą liczbę otwartych połączeń poprzez odpowiednie wartości keepalive.

Podsumowanie: Keepalive jako czynnik wpływający na wydajność

Celowo stosuję Keepalive, ponieważ obniża to koszty nawiązywania połączeń, zmniejsza opóźnienia i odciąża procesor. Połączenie odpowiedniego limitu czasu, właściwego limitu żądań i odpowiednich pul upstream zapewnia zauważalne Prędkość. Bez monitorowania potencjał pozostaje niewykorzystany, dlatego na bieżąco sprawdzam wskaźniki i stopniowo dostosowuję wartości. Kto potrzebuje dodatkowych rezerw, powinien zwrócić uwagę na liczbę workerów, sloty połączeń i prawidłowe przetwarzanie nagłówków. Profesjonalne konfiguracje, na przykład w przypadku webhoster.de, w pełni wykorzystują te możliwości i zapewniają szybkie, niezawodne usługi.

Artykuły bieżące