NGINX: Logika przetwarzania żądań

18 Jan 2020

best-practices  http  nginx  requests 

Share on:

Proces przetwarzania żądań przez serwer NGINX na pierwszy rzut oka może wydawać się skomplikowany. Cała logika jest jednak prosta i dobrze przemyślana.

W dużym skrócie wyszukiwanie rozpoczyna się od bloku http, następnie przechodzi przez jeden lub więcej bloków server, a następnie przez bloki location. Blok http zawiera dyrektywy do obsługi ruchu w sieci (do obsługi protokołów HTTP/HTTPS), które są przekazywane do wszystkich konfiguracji domen obsługiwanych przez NGINX.

Podczas obsługi żądań NGINX wykorzystuje bloki server (ich działanie jest analogiczne jak wirtualne hosty w Apache), które zawierają dwie kluczowe dyrektywy:

W trakcie oceny żądania NGINX sprawdza nagłówek Host, którego wartość zawiera domenę lub adres IP, do którego klient faktycznie próbuje dotrzeć. Co więcej, NGINX próbuje znaleźć najlepsze dopasowanie do wartości, którą znajdzie w tym nagłówku, patrząc na dyrektywę server_name w każdym z bloków serwera.

Obsługa połączeń przychodzących #

NGINX używa następującej logiki do określenia, który serwer wirtualny (blok serwera) powinien zostać użyty:

1) Dopasowuje parę <adres:port> do dyrektywy listen — może istnieć wiele bloków z dyrektywami listen o tej samej specyfice, które mogą obsłużyć żądanie

NGINX używa kombinacji <adres:port> do obsługi połączeń przychodzących. Ta para jest przypisana do dyrektywy listen.

Wartość dyrektywy listen można ustawić na kilka sposobów:

Jeśli dyrektywa listen nie jest ustawiona, wówczas używana jest konstrukcja *:80 (działa z uprawnieniami superużytkownika), albo *:8000.

Przetwarzanie w obrębie dyrektywy listen zaczyna się od następujących kroków:

Spójrz na poniższy przykład:

# From client side:
GET / HTTP/1.0
Host: api.random.com

# From server side:
server {

  # This block will be processed:
  listen 192.168.252.10;  # --> 192.168.252.10:80

  ...

}

server {

  listen 80;  # --> *:80 --> 0.0.0.0:80
  server_name api.random.com;

  ...

}

2) Dopasowuje pole nagłówka Host do dyrektywy server_name jako ciąg znaków z wykorzystaniem tablicy skrótów z dokładnymi nazwami

3) Dopasowuje pole nagłówka Host do dyrektywy server_name z symbolem wieloznacznym na początku łańcucha oraz z wykorzystaniem tablicy skrótów z nazwami symboli wieloznacznych rozpoczynającymi się gwiazdką

Jeśli na tym etapie dopasowanie będzie poprawne, blok, w którym występuje dyrektywa server_name zostanie wykorzystany do obsługi żądania. Jeśli znaleziono wiele dopasowań, do wykonania żądania zostanie użyty blok serwera z najdłuższym dopasowaniem.

4) Dopasowuje pole nagłówka Host do dyrektywy server_name ze znakiem wieloznacznym na końcu łańcucha oraz z wykorzystaniem tablicy skrótów z nazwami symboli wieloznacznych kończącymi się gwiazdką

Jeśli na tym etapie dopasowanie będzie poprawne, blok, w którym występuje taka dyrektywa server_name zostanie wykorzystany do obsługi żądania. Jeśli znaleziono wiele dopasowań, do wykonania żądania zostanie użyty blok serwera z najdłuższym dopasowaniem.

5) Dopasowuje pole nagłówka Host do dyrektywy server_name jako wyrażenie regularne

Pierwsze wystąpienie dyrektywy server_name (z wyrażeniem regularnym) pasującej do nagłówka Host zostanie użyte do obsługi żądania.

6) Jeśli nagłówek Host nie pasuje do nazwy serwera, NGINX przechodzi się do dyrektywy listen oznaczonej jako default_server (parametr ten powoduje, że blok serwera odpowiada na wszystkie żądania, które nie pasują do żadnego bloku serwera)

7) Jeśli nagłówek Host nie pasuje do nazwy serwera i nie ma domyślnego serwera, NGINX przechodzi bezpośrednio do pierwszego bloku serwera z dyrektywą listen

Wynika z tego, że domyślny serwer występuje zawsze. Jeżeli nie wskażemy go jawnie za pomocą dyrektywy default_server będzie nim pierwszy blok server w konfiguracji. Może rodzić to niepożądane problemy dlatego zalecane jest aby zawsze wskacać serwer domyślny w konfiguracji.

8) Następnie NGINX przechodzi do kontekstu location

Dopasowanie lokalizacji #

Blok lokalizacji umożliwia obsługę kilku typów identyfikatorów URI/tras (routing w warstwie 7 na podstawie adresu URL) w obrębie bloku serwera. Składnia wygląda następująco:

location optional_modifier location_match { ... }

location_match określa sprawdzenie identyfikatora URI żądania. Argument optional_modifier spowoduje, że skojarzony blok lokalizacji zostanie zinterpretowany w następujący sposób (w tej chwili kolejność nie ma znaczenia):

A teraz krótkie wprowadzenie wyjaśniające priorytet lokalizacji:

Spójrz na poniższy przykład:

location = / {
  # Matches the query / only.
  [ configuration A ]
}
location / {
  # Matches any query, since all queries begin with /, but regular
  # expressions and any longer conventional blocks will be
  # matched first.
  [ configuration B ]
}
location /documents/ {
  # Matches any query beginning with /documents/ and continues searching,
  # so regular expressions will be checked. This will be matched only if
  # regular expressions don't find a match.
  [ configuration C ]
}
location ^~ /images/ {
  # Matches any query beginning with /images/ and halts searching,
  # so regular expressions will not be checked.
  [ configuration D ]
}
location ~* \.(gif|jpg|jpeg)$ {
  # Matches any request ending in gif, jpg, or jpeg. However, all
  # requests to the /images/ directory will be handled by
  # Configuration D.
  [ configuration E ]
}

W celu lepszego zrozumienia przetwarzania lokalizacji polecam następujące narzędzia:

Proces wyboru bloku lokalizacji NGINX jest następujący (szczegółowe wyjaśnienie):

1) NGINX szuka dokładnego dopasowania. Jeśli modyfikator =, np. location = foo {...}, dokładnie pasuje do identyfikatora URI żądania, ten konkretny blok lokalizacji jest wybierany od razu

2) Następnie wykonywane jest dopasowanie lokalizacji oparte na prefiksach (bez wyrażeń regularnych). Każda lokalizacja zostanie sprawdzona pod kątem identyfikatora URI żądania. Jeśli nie zostanie znaleziony dokładny (tzn. bez modyfikatora =) blok lokalizacji, NGINX będzie kontynuował wyszukiwanie z tzw. nieprecyzyjnymi prefiksami. Zaczyna od najdłuższego pasującego prefiksu dla tego identyfikatora URI, z następującym podejściem:

3) Gdy tylko zostanie wybrany i zapisany najdłuższy pasujący prefiks, NGINX kontynuuje ocenę rozróżniania wielkości liter (ang. case-sensitive regular expression), np. location ~ foo {...}, lub pomija ich rozróżnianie (ang. insensitive regular expression), np. location ~* foo {.. .}. Pierwsze wyrażenie regularne, które pasuje do identyfikatora URI, jest wybierane od razu do przetworzenia żądania

4) Jeśli nie zostaną znalezione odpowiednie wyrażenia regularne pasujące do identyfikatora URI żądania, poprzednio zapisana lokalizacja prefiksu (np. location foo {...}) zostanie wybrana do obsługi żądania

Powinieneś także wiedzieć, że typy dopasowania inne niż wyrażenia regularne są w pełni deklaratywne — kolejność definicji w konfiguracji nie ma znaczenia, jednak „zwycięskie” dopasowanie wyrażeń regularnych (jeśli przetwarzanie nawet zajdzie tak daleko) jest całkowicie oparte na kolejności wprowadzenia ich w pliku konfiguracyjnym.

Aby lepiej zrozumieć, jak działa ten proces, zapoznaj się z poniższą tabelką, która pozwoli Ci zaprojektować bloki lokalizacji w przewidywalny sposób:

Na koniec, przykład trochę bardziej skomplikowanej konfiguracji:

server {

 listen 80;
 server_name xyz.com www.xyz.com;

 location ~ ^/(media|static)/ {
  root /var/www/xyz.com/static;
  expires 10d;
 }

 location ~* ^/(media2|static2) {
  root /var/www/xyz.com/static2;
  expires 20d;
 }

 location /static3 {
  root /var/www/xyz.com/static3;
 }

 location ^~ /static4 {
  root /var/www/xyz.com/static4;
 }

 location = /api {
  proxy_pass http://127.0.0.1:8080;
 }

 location / {
  proxy_pass http://127.0.0.1:8080;
 }

 location /backend {
  proxy_pass http://127.0.0.1:8080;
 }

 location ~ logo.xcf$ {
  root /var/www/logo;
  expires 48h;
 }

 location ~* .(png|ico|gif|xcf)$ {
  root /var/www/img;
  expires 24h;
 }

 location ~ logo.ico$ {
  root /var/www/logo;
  expires 96h;
 }

 location ~ logo.jpg$ {
  root /var/www/logo;
  expires 48h;
 }

}

A oto niektóre z rezultatów:

URL LOCATIONS FOUND FINAL MATCH
/ 1) prefix match for / /
/css 1) prefix match for / /
/api 1) exact match for /api /api
/api/ 1) prefix match for / /
/backend 1) prefix match for /
2) prefix match for /backend
/backend
/static 1) prefix match for / /
/static/header.png 1) prefix match for /
2) case sensitive regex match for ^/(media\|static)/
^/(media\|static)/
/static/logo.jpg 1) prefix match for /
2) case sensitive regex match for ^/(media\|static)/
^/(media\|static)/
/media2 1) prefix match for /
2) case insensitive regex match for ^/(media2\|static2)
^/(media2\|static2)
/media2/ 1) prefix match for /
2) case insensitive regex match for ^/(media2\|static2)
^/(media2\|static2)
/static2/logo.jpg 1) prefix match for /
2) case insensitive regex match for ^/(media2\|static2)
^/(media2\|static2)
/static2/logo.png 1) prefix match for /
2) case insensitive regex match for ^/(media2\|static2)
^/(media2\|static2)
/static3/logo.jpg 1) prefix match for /static3
2) prefix match for /
3) case sensitive regex match for logo.jpg$
logo.jpg$
/static3/logo.png 1) prefix match for /static3
2) prefix match for /
3) case insensitive regex match for .(png\|ico\|gif\|xcf)$
.(png\|ico\|gif\|xcf)$
/static4/logo.jpg 1) priority prefix match for /static4
2) prefix match for /
/static4
/static4/logo.png 1) priority prefix match for /static4
2) prefix match for /
/static4
/static5/logo.jpg 1) prefix match for /
2) case sensitive regex match for logo.jpg$
logo.jpg$
/static5/logo.png 1) prefix match for /
2) case insensitive regex match for .(png\|ico\|gif\|xcf)$
.(png\|ico\|gif\|xcf)$
/static5/logo.xcf 1) prefix match for /
2) case sensitive regex match for logo.xcf$
logo.xcf$
/static5/logo.ico 1) prefix match for /
2) case insensitive regex match for .(png\|ico\|gif\|xcf)$
.(png\|ico\|gif\|xcf)$

Fazy przetwarzania żądań #

Na tym temat moglibyśmy zakończyć jednak jest jeszcze jedna niezwykle istotna rzecz warta wspomnienia — fazy przetwarzania żądań HTTP.

Otóż idąc za oficjalną dokumentacją, każde żądanie HTTP przechodzi przez sekwencję faz gdzie w każdej fazie wykonywany jest inny rodzaj przetwarzania żądania. Fazy są przetwarzane jedna po drugiej, a odpowiednie metody obsługi faz są wywoływane, gdy żądanie dotrze do danej fazy. Poniżej znajduje się lista faz HTTP:

Zrozumienie ich jest niezwykle istotne, ponieważ w języku NGINX kolejność pisania w pliku konfiguracyjnym może znacznie różnić się od kolejności wykonywania na ogólnej osi czasu przetwarzania, co zwykle dezorientuje wielu administratorów.

Zwykle moduły i ich polecenia rejestrują swoje wykonanie tylko w jednej z trzech faz: rewrite, access i content. Na przykład dyrektywa set działa w fazie przepisywania, a polecenie echo działa w fazie treści. Ponieważ pierwsza z wymienionych występuje zawsze przed fazą content, polecenia i dyrektywy w niej zawarte są również wykonywane wcześniej. Dlatego polecenie set zawsze jest wykonywane przed poleceniem „podłączonym” do fazy treści w ramach jednej dyrektywy location, niezależnie od kolejności ich wystąpienia w konfiguracji.

Co istotne, polecenia w różnych fazach nie mogą być wykonywane w tę i z powrotem a dwa, nie każde polecenie ma odpowiednią fazę. Przykładami są dyrektywy geo i map. Te polecenia, które nie mają wyraźnie stosowanej fazy, są deklaratywne i niezwiązane z koncepcją kolejności wykonywania. Inną ciekawą rzeczą jest to, że polecenia różnych modułów są wykonywane niezależnie od siebie, nawet jeśli wszystkie są zarejestrowane w tej samej fazie (wyjątkiem jest moduł ngx_set_misc, którego polecenia są specjalnie dostrojone za pomocą modułu ngx_rewrite, tak, aby były wykonane na samym końcu). Innymi słowy, każda faza przetwarzania jest dalej dzielona na mniejsze fazy przez moduły serwera NGINX.

Aby podejrzeć, w jakiej fazie wykonywane są konkretne polecenia, możesz wykorzystać tryb debug (należy go włączyć podczas kompilacji).

Przygotowałem również proste wyjaśnienie, które pomoże ci zrozumieć, jakie moduły oraz dyrektywy są używane na każdym etapie:

Dodatkowo każda z faz ma listę powiązanych z nią procedur obsługi. Co więcej, na każdej fazie można zarejestrować dowolną liczbę handlerów. Na przykład pisząc własny moduł w Lua możesz umieścić go w różnych fazach działania serwera, aby spełnić różne wymagania.

Polecam zapoznać się ze świetnym wyjaśnieniem dotyczącym faz przetwarzania żądań. Dodatkowo, w tym oficjalnym przewodniku także dość dokładnie opisano cały proces przejścia żądania przez każdą z faz.

Wracając jeszcze do wspomnianego przed chwilą kontekstu lokalizacji, to wszystkie polecenia ustawione w tym kontekscie są wykonywane w fazie przepisywania. W rzeczywistości prawie wszystkie polecenia implementowane przez przepisywanie są wykonywane w fazie przepisywania w określonym kontekście. Należy jednak mieć świadomość, że gdy niektóre polecenia zostaną znalezione w dyrektywie server, zostaną wykonane we wcześniejszej fazie, tj. w fazie przepisywania serwera.

Poniżej znajduje się znacznie prostszy podgląd, który pomoże zrozumieć omawiany temat:

Polecam przeczytać świetne wyjaśnienie na temat faz przetwarzania żądań HTTP w NGINX i oczywiście oficjalny przewodnik dla developerów. Na koniec, koniecznie zapoznaj się z artykułem agentzh’s Nginx Tutorials (version 2020.03.19), który w świetny sposób wyjaśnia jak działają fazy przetwarzania serwera NGINX podająć przy okazji wiele pomocnych przykładów.