Reverse Proxy
La configurazione di Nginx come reverse proxy avviene semplicemente specificando una Location
con la direttiva proxy_pass. Ad esempio:
location /mia/location/ {
proxy_pass http://www.example.com/link/;
}
Quando i client richiederanno al server l'indirizzo /mia/location
, Nginx effettuerà una richiesta al server www.example.com
, richiedendo la risorsa /link
e restituendo il risultato della richiesta al client.
L'intera location viene mappata sul server esterno. Ad esempio, se si richiede al server l'indirizzo /mia/location/sub/pagina.html
il server Nginx richiederà al server esterno la risorsa /link/sub/pagina.html
.
E' possibile specificare un numero di porta:
location /mia/location/ {
proxy_pass http://www.example.com:3000/link/;
}
E' possibile inserire headers:
location /mia/location/ {
proxy_set_header Accept-Encoding "";
proxy_pass http://www.example.com/link/;
}
Si può usare un buffer per le richieste:
location /mia/location/ {
proxy_buffers 16 4k;
proxy_buffer_size 2k;
proxy_pass http://www.example.com/link/;
}
Si può esplicitamente disabilitare il buffering:
location /mia/location/ {
proxy_buffering off;
proxy_pass http://www.example.com/link/;
}
Load Balancer
Nginx può essere configurato per svolgere il ruolo di load balancer in modo molto efficiente. In particolare, nginx supporta tre differenti modalità di load balancing:
- round-robin - Le richieste vengono distribuite in sequenza ciclica, in base all'ordine di arrivo;
- least-connected - Ogni richiesta è assegnata al server con meno connessioni attive in quel momento;
- ip-hash - Una funzione di hash è utilizzata come funzione di mappatura tra l'indirizzo IP del client e l'identificativo del server che gestirà la richiesta.
La direttiva upstream viene utilizzata per indicare gli indirizzi degli application server utilizzati dal reverse proxy.
Ad esempio, supponendo di voler gestire le richieste in arrivo su www.miaapp.com
attraverso i server srv1.miaapp.com
, srv2.miaapp.com
, srv3.miaapp.com
, nginx andrebbe configurato come segue:
http {
upstream myapp1 {
server srv1.miaapp.com;
server srv2.miaapp.com;
server srv3.miaapp.com;
}
server {
listen 80;
location / {
proxy_pass http://miaapp.com;
}
}
}
Quando la modalità di load balancing non è specificata, nginx utilizza round-robin per default.
Per attivare la modalità least-connected è sufficiente aggiungere la direttiva leat_conn
nel gruppo upstream. Ad esempio:
...
upstream myapp1 {
least_conn;
server srv1.miaapp.com;
server srv2.miaapp.com;
server srv3.miaapp.com;
}
...
Persistenza delle Sessioni
Sia nella modalità round-robin che in quella least-connected non vi è garanzia della persistenza delle sessioni. Ricordiamo infatti che HTTP è per sua natura stateless, e pertanto la gestione delle sessioni è sempre a carico del server. In uno scenario in cui è presente un load balancer, è possibile che le richieste in arrivo da uno stesso client e che logicamente appartengono ad una stessa sessione siano distribuite su application server differenti e che pertanto non saranno in grado di riconoscere gli ID o i cookie di sessione generati dagli altri.
Occorre configurare il load balancer in modo tale da assicurarsi che le richieste in arrivo da uno stesso client vengano sempre servite dallo stesso application server. Tale schema è implementato in nginx con la modalità ip-hash.
In ip-hash, l'indirizzo IP del client è utilizzato come chiave di hashing per la determinazione dell'application server da selezionare. Per attivare questa modalità è sufficiente specificare la direttiva ip_hash
nel gruppo upstream. Ad esempio:
...
upstream myapp1 {
ip_hash;
server srv1.miaapp.com;
server srv2.miaapp.com;
server srv3.miaapp.com;
}
...
Specifica dei Pesi
Nginx permette di attribuire un peso numerico a ciascun application server indicato nella direttiva upstream
. Server con un peso maggiore verranno selezionati più frequentemente di quelli con un peso inferiore. Per default, in assenza di una esplicita indicazione, si assume un peso pari ad 1.
Questo permette di distribuire il carico in maniera più consona rispetto alle reali capacitè di elaborazione degli application server.
Si supponga che l'application server srv1.miaapp.com
sia in grado di gestire più richieste rispetto ai server srv2
e srv3
. In tal caso, si può assegnare ad srv1 un peso più alto per assicurarsi che esso venga selezionato più spesso dal load balancer. Ad esempio, specificando:
http {
upstream myapp1 {
server srv1.miaapp.com weight=3;
server srv2.miaapp.com;
server srv3.miaapp.com;
}
server {
listen 80;
location / {
proxy_pass http://miaapp.com;
}
}
}
Resilienza
Nginx include anche un meccanismo di monitoraggio passivo degli application server. Ciò significa che se un application server non risponde correttamente alle richieste, esso verrà marcato come non funzionante ed nginx non lo selezionerà per le richieste successive.
È possibile controllare il funzionamento di questo schema. In particolare, è possibile specificare il numero di tentativi da effettuare prima di considerare l'application server non funzionante tramite la direttiva max_fails
. Per default, questo parametro è impostato ad uno, e ciò comporta che un server venga immediatamente riconosciuto come non funzionante al verificarsi del primo errore.
È inoltre possibile configurare nginx affinchè monitori lo stato dei server non funzionanti, effettuando dei controlli periodici. Il tempo che intercorre tra un controllo ed il successivo viene impostato con la direttiva fail_timeout
. Per default, l'intervallo di fail_timeout
à impostato a 10 secondi.
Esercizio: Nginx Reverse Proxy e Load Balancer
Preparazione
Creare la directory per l'esercizio:
cd ~/ex
mkdir -p 04net1-ccli-net2-cproxy-2cweb
cd 04net1-ccli-net2-cproxy-2cweb
Preparare lo scaffolding:
mkdir ccli cproxynx cweb
touch ccli/dockerfile docker-compose.yml
tree
Il client ccli
rimane quello del precedente esercizio:
vim ccli/Dockerfile
FROM alpine:3.7
MAINTAINER John Smith <john@stormforce.ac>
RUN apk --update add --no-cache openssh tcpdump curl
CMD ["/bin/sleep","1000000"]
Il cproxy
sarà un'immagine nginx:alpine
. Il server web target rimane un'immagine httpd:2.4-alpine
, come in precedenza.
Creaiamo un volume docker per la persistenza della configurazione:
docker volume create nginx-conf
Il docker-compose.yml
è:
vim docker-compose.yml
version: '3.6'
services:
one:
build: ccli
image: ccli
container_name: one
hostname: one
cap_add:
- ALL
networks:
net1:
ipv4_address: 192.168.101.11
two:
image: nginx:alpine
container_name: two
hostname: two
cap_add:
- ALL
volumes:
- nginx-conf:/etc/nginx
networks:
net1:
ipv4_address: 192.168.101.10
net2:
ipv4_address: 192.168.102.10
three:
image: httpd:2.4-alpine
container_name: three
hostname: three
cap_add:
- ALL
volumes:
- three-html:/usr/local/apache2/htdocs/
networks:
net2:
ipv4_address: 192.168.102.12
four:
image: httpd:2.4-alpine
container_name: four
hostname: four
cap_add:
- ALL
volumes:
- four-html:/usr/local/apache2/htdocs/
networks:
net2:
ipv4_address: 192.168.102.13
networks:
net1:
name: net1
ipam:
driver: default
config:
- subnet: 192.168.101.0/24
net2:
name: net2
ipam:
driver: default
config:
- subnet: 192.168.102.0/24
volumes:
nginx-conf:
external: true
three-html:
external: true
four-html:
external: true
Partenza del progetto:
docker compose up -d
Collegamento al server nginx:
docker exec -ti two sh
Nella directory di configurazione:
cd /etc/nginx
Disabilitiamo il file di configurazione del sito di default semplicemente cambiandogli nome:
mv conf.d/default.conf conf.d/default.conf.old
Modifichiamo il file principale di configurazione:
vi nginx.conf
worker_processes 5;
user nobody nobody;
error_log /dev/stdout;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream bongo {
#ip_hash;
server 192.168.102.12;
server 192.168.102.13;
}
server {
listen 8080;
root /usr/share/nginx/html;
access_log /dev/stdout;
error_log /dev/stdout;
location / {
proxy_pass http://bongo/;
}
}
}
Testare la configurazione col comando:
nginx -t
Restartare il server col comando:
nginx -s reload
Uscire:
exit
Connettersi a one e provare se il proxy funziona:
docker exec -ti one sh
Ripetere più volte:
curl two:8080
Si vede l'effetto round robin.
Per provare la persistenza di connessione, tornare nella configurazione di two e scommentare il parametro:
ip_hash;
Riprovando, si nota che il server upstream connesso è sempre lo stesso.