Nginx, reverse proxy i rozkładanie ruchu
Po tej lekcji: kursant stawia stronę na Nginxie, sprawdza konfigurację nginx -t, ustawia Nginxa jako reverse proxy przed aplikacją, rozkłada ruch na kilka serwerów przez upstream i robi to samo w HAProxy z kontrolą zdrowia serwerów.
Wstęp
Piątek, wieczór, sklep z kapciami ma promocję. Ruch rośnie pięć razy. Jeden serwer dyszy, procesor na stu procentach, klienci widzą kręcące się kółko. Szef pyta: „Nie da się dostawić drugiego serwera?”
Da się. Tylko klient wpisuje jeden adres. Ktoś musi stać w drzwiach i mówić: „pan do okienka pierwszego, pani do drugiego”. Dziś postawimy takiego kogoś. Dwa razy. Raz w Nginxie, raz w HAProxy.
Nginx, czyli druga recepcja
Pamiętasz, że serwer WWW to recepcja hotelu? Apache to recepcja w starym, porządnym hotelu. Dla każdego gościa osobny boy hotelowy. Nginx to recepcja na lotnisku. Kilku pracowników obsługuje tysiące ludzi naraz, bo nikt nie stoi i nie czeka, aż gość skończy mówić. Pracownik przyjmuje sprawę, odchodzi do następnego, wraca, jak coś jest gotowe.
Instalacja:
sudo apt install nginx # [Ubuntu], startuje sam
sudo dnf install nginx # [RHEL]
sudo systemctl enable --now nginx # [RHEL] trzeba włączyć ręcznie
sudo ufw allow 'Nginx Full' # [Ubuntu] 80 i 443
sudo firewall-cmd --permanent --add-service={http,https} # [RHEL]
sudo firewall-cmd --reloadSprawdźmy, kto pracuje:
ps -o pid,user,cmd -C nginx PID USER CMD
20202 root nginx: master process nginx
20203 www-data nginx: worker process
20204 www-data nginx: worker processJeden kierownik jako root, bo tylko root może otworzyć port 80. I pracownicy jako zwykły użytkownik, którzy robią całą robotę. [RHEL] użytkownik nazywa się nginx, nie www-data.
Gdzie Nginx trzyma konfigurację
Główny plik to zawsze /etc/nginx/nginx.conf. Dalej rodziny się rozjeżdżają.
[Ubuntu] Ten sam układ, co w Apache: sites-available to szafa ze wszystkimi stronami, sites-enabled to dowiązania do tych, które działają. Tylko nie ma a2ensite. Dowiązanie robisz sam przez ln -s.
[RHEL] Nie ma sites-available. Wrzucasz plik z końcówką .conf do /etc/nginx/conf.d/ i tyle. Nie chcesz strony? Zmieniasz końcówkę na .conf.off.
W konfiguracji Nginxa strona to blok server. To odpowiednik VirtualHosta z Apache. Zróbmy pierwszy:
sudo mkdir -p /var/www/kapcie
echo '<h1>Kapcie z Nginxa</h1>' | sudo tee /var/www/kapcie/index.html
sudo nano /etc/nginx/sites-available/kapcieserver {
listen 80;
server_name kapcie.test;
root /var/www/kapcie;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Czytamy po ludzku. listen to numer okienka. server_name to nazwa, na którą ta recepcja reaguje. root to folder z plikami. location / mówi, co robić z adresami zaczynającymi się od ukośnika, czyli ze wszystkimi. try_files to instrukcja dla recepcjonisty: „poszukaj takiego pliku, potem takiego folderu, a jak nie ma, powiedz 404”.
Zwróć uwagę na średniki. Każda linijka ze słowem kluczowym kończy się średnikiem. Zapamiętaj to. Za trzy minuty średnik nas ugryzie.
sudo ln -s /etc/nginx/sites-available/kapcie /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -i http://kapcie.testnginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)nginx -t to odpowiednik apachectl configtest. Sprawdza konfigurację bez dotykania działającego serwera. Zasada jak w samolocie: najpierw lista kontrolna, potem start. Zawsze nginx -t przed reload.
A nginx -T z wielką literą wypisuje całą konfigurację sklejoną ze wszystkich plików. Bezcenne, kiedy nie wiesz, który plik co ustawia.
Reverse proxy, czyli recepcjonista, który nic nie gotuje
Wyobraź sobie restaurację, w której kelner nie gotuje. Przyjmuje zamówienie, zanosi do kuchni, przynosi talerz. Gość nigdy nie widzi kuchni. Nie wie, czy kucharz jest jeden, czy pięciu, i czy kuchnia jest w tym budynku.
To jest reverse proxy. Klient rozmawia z Nginxem. Nginx przekazuje prośbę do prawdziwej aplikacji, która siedzi gdzieś z tyłu, i oddaje odpowiedź. Po co? Bo aplikacje w Node, Pythonie czy Javie zwykle słuchają na porcie typu 3000 albo 8080, nie znają się na HTTPS i nie powinny wystawiać się prosto do internetu. Nginx bierze na siebie certyfikat, kompresję i pliki statyczne. Aplikacja robi tylko swoje.
Zróbmy dwie udawane aplikacje. Python ma wbudowany malutki serwer WWW, w sam raz do testów:
mkdir app1 app2
echo 'Odpowiada serwer 1' > app1/index.html
echo 'Odpowiada serwer 2' > app2/index.html
python3 -m http.server 9001 --bind 127.0.0.1 --directory app1 &
python3 -m http.server 9002 --bind 127.0.0.1 --directory app2 &Słuchają tylko na 127.0.0.1, czyli z zewnątrz nikt się do nich nie dobije. Jedyne drzwi to Nginx. Teraz kluczowe słówko, proxy_pass:
server {
listen 80;
server_name app.test;
location / {
proxy_pass http://127.0.0.1:9001;
include proxy_params;
}
}include proxy_params dokleja gotowy zestaw nagłówków z pliku /etc/nginx/proxy_params. [RHEL] tego pliku nie ma, więc wpisujesz linijki sam:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;Po co? Bez nich aplikacja myśli, że każdy klient to 127.0.0.1. Bo z jej punktu widzenia przychodzi do niej tylko Nginx. Kelner musi przekazać kuchni nie tylko zamówienie, ale i numer stolika. Podsłuchałem, co dokładnie dostaje aplikacja:
GET /koszyk HTTP/1.0
Host: app.test
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: http
Connection: close[RHEL] Tu SELinux zatrzyma Cię pierwszy raz. Domyślnie serwer WWW nie może sam łączyć się z innymi portami. Dostaniesz 502, a w logu Permission denied. Lekarstwo z dokumentacji Red Hata:
sudo setsebool -P httpd_can_network_connect 1To samo pole, co dla Apache, bo dla SELinuksa oba to „serwer WWW”.
Upstream, czyli kolejka do kilku okienek
Wracamy do piątkowej promocji. Mamy dwie aplikacje. Chcemy, żeby Nginx dzielił ruch między obie. Zamiast jednego adresu w proxy_pass dajemy nazwę grupy. Grupę definiuje blok upstream:
upstream aplikacja {
server 127.0.0.1:9001;
server 127.0.0.1:9002;
}
server {
listen 80;
server_name app.test;
location / {
proxy_pass http://aplikacja;
include proxy_params;
}
}sudo nginx -t && sudo systemctl reload nginx
for i in $(seq 1 10); do curl -s http://app.test; done | sort | uniq -c 5 Odpowiada serwer 1
5 Odpowiada serwer 2Pół na pół. Rura i uniq -c z modułu pierwszego właśnie policzyły nam rozkład ruchu. Mówiłem, że się przyda.
Metody rozkładania
Ochroniarz w Biedronce może kierować ludzi na kilka sposobów.
Po kolei (round robin). Ty do pierwszej, ty do drugiej, ty do pierwszej. To domyślne. Nic nie wpisujesz.
Z wagami. Kasa pierwsza ma doświadczoną kasjerkę, druga stażystę. Pierwsza dostaje trzy razy więcej klientów:
server 127.0.0.1:9001 weight=3;
server 127.0.0.1:9002; 9 Odpowiada serwer 1
3 Odpowiada serwer 2Dwanaście zapytań, dziewięć do trzy. Matematyka się zgadza, stażysta odetchnął.
Do najmniej zajętego, least_conn;. Ochroniarz patrzy, przy której kasie stoi najkrótsza kolejka. Dobre, kiedy zapytania trwają bardzo różnie długo.
Zawsze do tego samego, ip_hash;. Stały klient zawsze idzie do tej samej kasjerki, bo ona go zna. Ten sam adres IP trafia do tego samego serwera. Przydaje się, kiedy aplikacja trzyma koszyk w pamięci jednego serwera.
upstream aplikacja {
ip_hash;
server 127.0.0.1:9001;
server 127.0.0.1:9002;
}Cztery zapytania z mojego komputera, cztery razy „serwer 1”. Działa.
Zapasowy, backup. Kasa otwierana tylko wtedy, kiedy wszystkie inne padły:
server 127.0.0.1:9001;
server 127.0.0.1:9002 backup;I jeszcze jedno: co, jak serwer padnie? Wyłączyłem serwer 2 i puściłem sześć zapytań:
200 200 200 200 200 200Klient nic nie zauważył. Nginx spróbował drugiego, dostał odmowę, po cichu przerzucił zapytanie na pierwszy i zapisał w logu:
[error] connect() failed (111: Connection refused) while connecting to upstream, ... upstream: "http://127.0.0.1:9002/"Potem przez 10 sekund nie zawraca sobie nim głowy. Tak działają domyślne max_fails=1 i fail_timeout=10s. Nginx w wersji darmowej sprawdza zdrowie serwerów biernie: dowiaduje się, że kasa zamknięta, dopiero jak wyśle do niej klienta. Wyłączyłem oba serwery:
HTTP/1.1 502 Bad Gateway502 to „kelner wrócił z kuchni, a kuchni nie ma”. Nginx działa, zaplecze nie. Kiedy zobaczysz 502, nie grzeb w Nginxie. Sprawdź aplikację za nim.
HAProxy, czyli zawodowy ochroniarz
Nginx to recepcja, która przy okazji umie kierować ruchem. HAProxy to człowiek, który robi tylko to. Za to robi to świetnie. Sam chodzi i co dwie sekundy zagląda do każdej kasy, czy jest otwarta. To aktywne sprawdzanie zdrowia.
sudo apt install haproxy # [Ubuntu]
sudo dnf install haproxy # [RHEL]Konfiguracja w obu rodzinach to jeden plik, /etc/haproxy/haproxy.cfg. Na górze jest global i defaults, to zostawiamy. Dopisujemy na końcu:
frontend wejscie
bind *:8085
default_backend aplikacja
backend aplikacja
balance roundrobin
option httpchk GET /
server app1 127.0.0.1:9001 check
server app2 127.0.0.1:9002 check
listen statystyki
bind 127.0.0.1:8086
stats enable
stats uri /statsDwa nowe słowa. frontend to drzwi wejściowe, gdzie przychodzą klienci. backend to zaplecze, czyli lista serwerów i sposób dzielenia. check przy serwerze włącza zaglądanie do kasy. option httpchk mówi, żeby zaglądać zapytaniem HTTP, a nie tylko pukaniem w port.
Sprawdzenie konfiguracji, tym razem flagą -c:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxy
for i in 1 2 3 4; do curl -s http://localhost:8085; doneConfiguration file is valid
Odpowiada serwer 1
Odpowiada serwer 2
Odpowiada serwer 1
Odpowiada serwer 2Teraz wyłączam serwer 2 i patrzę w statystyki. Strona /stats jest w przeglądarce, ale ma też wersję CSV dla skryptów:
curl -s 'http://127.0.0.1:8086/stats;csv' | cut -d, -f1,2,18# pxname,svname,status
wejscie,FRONTEND,OPEN
aplikacja,app1,UP
aplikacja,app2,DOWNHAProxy sam zauważył, że serwer 2 padł, zanim wysłał tam jakiegokolwiek klienta. Wszystkie zapytania idą teraz do pierwszego. Jak serwer 2 wstanie, wróci do gry sam.
Metody w HAProxy nazywają się trochę inaczej: roundrobin, leastconn i source, czyli ten sam pomysł, co ip_hash w Nginxie.
[RHEL] Znowu SELinux. HAProxy domyślnie nie może łączyć się z dowolnym portem. Jeśli zaplecze słucha na niestandardowym porcie, włączasz setsebool -P haproxy_connect_any 1. Według strony man haproxy_selinux ta opcja jest domyślnie wyłączona.
Jak to działa pod spodem
Reverse proxy to tak naprawdę dwa osobne połączenia TCP. Klient łączy się z Nginxem albo HAProxy. Proxy otwiera drugie, własne połączenie do aplikacji. Aplikacja nigdy nie widzi klienta. Dlatego potrzebne są nagłówki X-Forwarded-For: to jedyny ślad, kto naprawdę pukał.
Nginx ma jeden proces główny jako root. Ten proces czyta konfigurację i otwiera porty, a potem odpala procesy robocze jako zwykły użytkownik. Przy reload kierownik czyta nową konfigurację, uruchamia nowych pracowników, a starzy dokańczają obsługę swoich klientów i dopiero wtedy wychodzą. Dlatego reload nie zrywa połączeń. A jak nowa konfiguracja jest zła, kierownik zostaje przy starej.
HAProxy pracuje na dwóch poziomach. W mode http czyta zapytania HTTP i może kierować według adresu czy nagłówka. W mode tcp przekazuje surowe bajty, nie wiedząc, co w nich jest. Tak można rozkładać ruch do baz danych czy serwerów pocztowych.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
nginx | -t | sprawdza konfigurację |
nginx | -T | sprawdza i wypisuje całą sklejoną konfigurację |
nginx | -s reload | przeładowuje konfigurację bez systemd |
nginx | -v / -V | wersja / wersja z opcjami kompilacji |
haproxy | -c -f plik | sprawdza plik konfiguracji |
haproxy | -v | wersja |
upstream | weight=N | waga serwera |
upstream | least_conn; / ip_hash; | do najmniej zajętego / zawsze ten sam dla danego IP |
upstream | backup / down | serwer zapasowy / chwilowo wyłączony |
HAProxy backend | balance roundrobin / leastconn / source | metoda rozkładania |
HAProxy server | check | aktywne sprawdzanie zdrowia |
setsebool -P | httpd_can_network_connect 1 | [RHEL] pozwala Nginxowi łączyć się z aplikacją |
⚠ Typowy błąd
Masz już Apache na tym serwerze. Instalujesz Nginxa, startujesz i:
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)nginx -t nic nie wykrył, bo konfiguracja jest poprawna. Problem w tym, że okienko numer 80 jest już zajęte przez Apache. Dwóch recepcjonistów nie usiądzie na jednym krześle. Sprawdzasz, kto siedzi:
sudo ss -ltnp 'sport = :80'I decydujesz: albo Nginx słucha na innym porcie, albo Apache przechodzi na 8080 i Nginx staje przed nim jako reverse proxy. To drugie to zresztą bardzo częsty układ.
Drugi klasyk: zapomniany średnik.
nginx: [emerg] invalid number of arguments in "proxy_pass" directive in /etc/nginx/sites-enabled/proxy:12
nginx: configuration file /etc/nginx/nginx.conf test failedZgubiłeś średnik po proxy_pass, więc Nginx skleił tę linijkę z następną i wyszło mu za dużo słów. Komunikat wskazuje linijkę 12, a błąd jest na jej końcu. Zawsze patrz też linijkę wyżej.
✎ Zadanie
- Postaw dwie aplikacje testowe
python3 -m http.serverna portach 9001 i 9002, każda z innymindex.html. - Ustaw przed nimi Nginxa z blokiem
upstream, sprawdźnginx -ti policz rozkład dziesięciu zapytań przezsort | uniq -c. - Daj pierwszemu serwerowi
weight=4i policz jeszcze raz. - Wyłącz jedną aplikację i sprawdź, co widzi klient, a co jest w
/var/log/nginx/error.log.
Pokaż rozwiązanie
Dwie aplikacje na 9001 i 9002, Nginx z upstream przed nimi. Najciekawsza część to waga:
upstream aplikacja {
server 127.0.0.1:9001 weight=4;
server 127.0.0.1:9002;
}sudo nginx -t && sudo systemctl reload nginx
for i in $(seq 1 10); do curl -s http://app.test; done | sort | uniq -c 8 Odpowiada serwer 1
2 Odpowiada serwer 2Waga 4 do 1, czyli na pięć zapytań cztery idą do pierwszego. Na dziesięć osiem. Matematyka z podstawówki działa nawet w Nginxie.
Potem wyłączasz serwer 2. Klient dostaje same 200, a w /var/log/nginx/error.log ląduje connect() failed (111: Connection refused) while connecting to upstream. Czyli: klient zadowolony, admin ma ślad w logu. Tak powinno być. Gorzej, jakby było odwrotnie.
Co dalej
Aplikacja, która tylko wyświetla „Odpowiada serwer 1”, to żadna aplikacja. Prawdziwe gdzieś trzymają dane. W następnej lekcji bazy danych od strony admina: MariaDB i PostgreSQL, instalacja, zabezpieczenie, użytkownicy i kopie zapasowe, które ratują posadę.