Balancejador HTTP/S econòmic: Nginx+Haproxy+Pacemaker

Destacades

Seguidament us explicaré com es pot montar un balancejador httpS molt econòmic i molt eficient, el qual s’ha implementat tan en entorns productius com no productius, la motivació inicial del qual va ser centralitzar el balanceig dels diferents entorns d’integració continua d’una aplicació web. En entorns productius també l’he implementat després de fer varies probes de rendiment amb el “ab” i el “siege” .

L’únic requeriment que hem d’asumir és allotjar tot tràfic dels diferents entorns en subdominis d’un domini (p.e.example.com), on podem tenir int01.example.com, int02.example.com, test01.example.com, test02.example.com, pre.example.com, etc..

Anàlogament, en els entorns de producció, podem fer la mateixa discriminació diferenciant el tràfic pels paisos(es.example.com,fr.example.com), plataformes(m.example.com), tipus de contingut(static.example.com).

Els objectius que em vaig marcar van ser:

  • Baix cost econòmic: utilitzarem programari de codi obert des dels operatius fins als dimonis: CentOS, Nginx, Haproxy, Pacemaker, Corosync.
  • Alta disponibilitat de maquinari i programari: a nivell de maquinari ho montarem en una infraestructura virtualitzada amb varis nodes físics i implemtarem el pacemaker+corosync per a tenir alta disponibilitat en els dimonis.
  • Centralitzar la negociació SSL de tota la plataforma en un punt inicial i alliberar càrrega SSL del reste de la plataforma.
  • Permetre més flexibilitat i control en els balancejos de cadascún dels entorns: haproxy permet fa possible controlar en un pool de balanceig si un frontal hi està activat segons el seu nivell de càrrega o si te connexió contra la base de dades, per exemple.
  • Tenir una plataforma securitzada, requeriment essencial sobretot en entorns productius.
  • Els dimonis que utilitzarem serà Nginx, Haproxy, Pacemaker i Corosync, tot programari lliure on abarata el cost considerablement.

    En el següent esquema es veu clarament l’arquitectura de tot el montatge.

    fisicalLB

    El comento breument, no entraré en detall en tot, em centro en lo important d’aquest post. Els navegadors dels usuaris fan les peticions a les empreses que fan de CDN, després aquestes peticions van a parar en la nostra plataforma on filtrarem el tràfic amb un firewall. Un cop les peticions són filtrades van a parar un nginx. La finalitat del nginx es discriminar el tràfic segons el entorn i centralitzar totes les negociacions SSL del tràfic HTTPS. Seguidament, les peticions passen a un haproxy on aquest balanceja entre els diferents frontals de cadascun dels entorns.

    Per tal de securitzar-ho tot, definirem 3 VLAN’s, dmz, frontend i backend. Tot el tràfic és filtrat pel firewall, el tràfic sortint, el entrant i el tràfic entre zones. En la DMZ posarem 2 maquines virtuals CentOs on tindrem els serveis de Nginx i Haproxy en actiu-passiu utlitzant pacemaker-corosync. En el frontend tenim els frontals de cadascun dels entorns i en el backend tenim les dades, mysql, cassandras, mongos, nfs, cifs, etc….

    Per exemplificar-ho millor, ens centrarem només en un entorn, int01.example.com.

    Primerament en la DMZ montarem una 2 màquines virtuals on hi tindrem 2 ip flotants, una per el nginx i l’altre pel haproxy. Jo ho he implemetat en una infraestructura virtualitzada vmware però també es poden montar solucions més econòmiques com XenServer, KVM, etc… Evidentment, sigui quina sigui la solució de virtualització, hem d’assegurar-nos que les màquines virtuales sempre estàn corrent en màquines físiques diferents.

    Les peticions entraran en la ip flotant del nginx, el nginx té dues funcions, centralitzar la negociació SSL i discriminar el tràfic per entorns. En el tràfic HTTPS afegirem una capçalera HTTP “X-forwarded-proto: https”, on l’apache la recollirà i activarà la variable HTTPS emmascarant-li a la aplicació. El tipus de certificat que afegirem en el nginx serà un certificat signat per una entitat certificadora oficial on el Common-Name que utilitzarem serà de tipus wildcard, es a dir, per aquest exemple, “*.example.com”, això ens facilitza molt la gestió del certificats i ens dona molta facilitat de gestió. En entorns de producció ho dimensionem assignant els valors correctes de worker_processes i worker_connections tenint en compte la formula: max clients = worker_processes * worker_connections/4.

    Seguidament les peticions passen al haproxy on aquest balanceja entre els diferents frontals de cadascun del entorn. El haproxy ens permet estriar l’algorisme de balanceig, jo acostumo a utilitzar Round Robin. A més, podem prefixar els rangs de IPs dels frontals que hi hauran en cadascun dels entorns, així podem desplegar més frontals en el cas que sigui necessari sense haber de reinicilitzar el dimoni.
    Haproxy fara una petició a un php que hi ha en cadascun dels frontals on retorna un “OK” si no està swapejant, no té molta càrrega i té conectivitat en les bases de dades. Cal remarcar que haproxy també ens permet realitzar balancejos “sticky” tan en aplicacions JAVA com PHP, depenentment d’una capçalara HTTP, habitualment les capçaleres JSESSIONID i PHPSESSID respectivament. (en apps ASP teniem ASPSESSIONID). En entorns de producció fixarem la variable maxconn tenim en compte que cada petició consumeix 17kb.

    Finalment les peticions arriben als frontals on primerament passen pel varnish i les peticions que no siguin cachejables arribaran finalment al apache, crec que aquests dos dimonis es mereixen un post especial per ells dos, que ja tinc en el backlog del blog.

    En el següent diagrama clarifica tota l’arquitectura:

    Diagrama Lògic

    Diagrama Lògic

    Seguidament posaré les configuracions més importants, Nginx, Haproxy, Pacemaker:

    Parametres del Nginx

    ....
    upstream http-example-int01 {
        server lb2-vip.example.com:8080;
        keepalive 16;
    }
    server {
            listen lb1-vip.example.com:80;
        server_name int01.example.com ~^.*-int01\.example\.com$;
    
            location / {
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header Host $host;
                    proxy_pass http://http-example-int01/;
                    proxy_redirect off;
            }
    }
    server {
            listen lb1-vip.example.com:443 ssl;
        server_name int01.example.com ~^.*-int01\.example\.com$;
    
            ssl on;
            ssl_certificate /etc/nginx/ssl/crt/concat.pem;
            ssl_certificate_key /etc/nginx/ssl/key/example.key;
    
            location / {
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header X-Forwarded-Proto https;
                    proxy_set_header Host $host;
                    proxy_pass http://http-example-int01/;
                    proxy_redirect off;
            }
    }
    ....
    

    Parametres del Haproxy

    ...
    frontend example-int01 lb2-vip.grpprod.com:8080
        default_backend example-int01
    backend  example-int01
            option forwardfor
            option httpchk GET /healthcheck.php
            http-check expect string OK
            server  web01 x.y.z.w:80 check inter 2000 fall 3
            server  web02 x.y.z.w:80 check inter 2000 fall 3
            server  web03 x.y.z.w:80 check inter 2000 fall 3
            server  web04 x.y.z.w:80 check inter 2000 fall 3
            server  web05 x.y.z.w:80 check inter 2000 fall 3
    ...
    

    Parametres del Apache

    
     ServerName int01.example.com
        DocumentRoot "/srv/www/example/fa-front/public"
    
       <Directory "/srv/www/example/fa-front/public">
          Options -Indexes FollowSymLinks
          AllowOverride None
          Allow from All
          Order Allow,Deny
          RewriteEngine On
          RewriteCond %{HTTP:X-Forwarded-Proto} https
          RewriteRule .* - [E=HTTPS:on]
    
          RewriteCond %{REQUEST_FILENAME} -s [OR]
          RewriteCond %{REQUEST_FILENAME} -l [OR]
          RewriteCond %{REQUEST_FILENAME} -d
          RewriteRule ^.*$ - [NC,L]
          RewriteRule ^.*$ index.php [NC,L]
    
       SetEnv APPLICATION_ENV int01
       DirectoryIndex index.php
    
       LogFormat "%v %{Host}i %h %l %u %t \"%r\" %>s %b %{User-agent}i" marc.int01
       CustomLog /var/log/httpd/cloud-example-front.log example
    
    

    Parametres del Pacemaker

    node balance01
    node balance02
    primitive nginx lsb:nginx \
            op monitor interval="1s" \
            meta target-role="Started
    primitive haproxy lsb:haproxy \
            op monitor interval="1s" \
            meta target-role="Started"
    primitive lb1-vip ocf:heartbeat:IPaddr2 \
            params ip="x.x.x.x" iflabel="nginx-vip" cidr_netmask="32" \
            op monitor interval="1s"
    primitive lb2-vip ocf:heartbeat:IPaddr2 \
            params ip="y.y.y.y" iflabel="haproxy-vip" cidr_netmask="32" \
            op monitor interval="1s"
    group haproxy_cluster lb2-vip haproxy \
            meta target-role="Started"
    group nginx_cluster lb1-vip  nginx \
            meta target-role="Started"
    property $id="cib-bootstrap-options" \
            dc-version="1.1.7-6.el6-148fccfd5985c5590cc601123c6c16e966b85d14" \
            cluster-infrastructure="openais" \
            expected-quorum-votes="2" \
            stonith-enabled="false" \
            last-lrm-refresh="1355137974" \
            no-quorum-policy="ignore"
    rsc_defaults $id="rsc-options" \
            resource-stickiness="100"
    

    Afinem RHEL/CentOs per a l’NGINX+HAPROXY en entorns de producció

    Per una banda, l’Nginx es un servidor WEB molt lleuger capaç de gestionar gran quantitat de connexions concurrents amb una eficaç gestió dels recursos, jo l’he utilitzat com a servidor proxy on he balancejat el tràfic de diferents aplicacions, tan en entorns productius com en entorns de desenvolupament.

    D’una altra banda utilitzarem el HAPROXY com a balancejador, ja que és un dels programaris més potents, lleugers i flexibles a l’hora de balancejar tràfic, poden implementar-ho com a balancejador HTTP o TCP, en el meu cas, HTTP amb balanceig enganxós. Per a aquest dimoni només he hagut d’afinar valors com tune.bufsize i tune.maxrewrite

    En un entorn productiu cal afinar la plataforma per a tindre’ls preparats per a que ens permeti un bon rendiment, l’objectiu del post es tenir ben recopilada tota la informació que he utilitzat. Per la xarxa hi han molta documentació sobre aquest propòsit, aquí únicament els recopilarem per a tenir-ho ben recol·lectat, al final del post hi llisto les fonts.

    CentOs/RHEL es la plataforma on ho he muntat, on el nginx únicament fa la funció de “reverse proxy” de diferents sites, descartant les funcionalitats de balanceig i de cache, on les he implementat amb HAProxy i Varnish-cache que ja he explicat en un altre post, aquí amb vull centrar en afinar-ho i llistar els petits problemes que m’he trobat/corregit.

    Per tal d’afinar-ho, segmentem 3 configuracions diferents:

    • Paràmetres del nucli del sistema operatiu
    • Límits del sistema Operatiu
    • Configuració del nginx

    Paràmetres del nucli del sistema operatiu

    Valor per defecte i aplicat:

    sysctl -w net.ipv4.tcp_congestion_control = cubic
    

    Algorisme de gestió dels trafic TCP, en el paràmetre net.ipv4.tcp_available_congestion_control podem saber quines opcions tenim carregades en el kernel de linux
    En CentOs6 tenim aquestes opcions: cubic reno, en el següent enllaç ho explica
    http://kaivanov.blogspot.com.es/2010/09/linux-tcp-tuning.html

    • reno: Traditional TCP used by almost all other OSes. (default)
    • cubic: CUBIC-TCP (NOTE: There is a cubic bug in the Linux 2.6.18 kernel used by Redhat Enterprise Linux 5.3 and Scientific Linux 5.3. Use 2.6.18.2 or higher!)
    • bic: BIC-TCP
    • htcp: Hamilton TCP
    • vegas: TCP Vegas
    • westwood: optimized for lossy networks

    Valor per defecte i aplicat:

    sysctl -w net.ipv4.tcp_window_scaling = 1
    

    Auto-escalat de la mida de la finestra de recepció TCP utilitzat, la finestra més gran pot ocupar 65,535 bytes. 1 ho activem i 0 ho desactivem.

    Valor per defecte:

    sysctl -w net.ipv4.ip_local_port_range="32768 61000"
    

    Valor per aplicat:

    sysctl -w net.ipv4.ip_local_port_range="2000 65000"
    

    Defineix el conjunt de ports locals que fa servir el SO per connexions les connexions TCP i UDP

    Valor per defecte:

    sysctl -w net.ipv4.tcp_max_syn_backlog="2048"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_max_syn_backlog="204800"
    

    Numero màxim de peticions per les connexions rebudes que encara no ha rebut el ACK del emissor, per defecte és 1024 i en sistemes de menys de 128Mb li assignem 128. Si el servidor pateix sobrecàrrega intenta incrementar aquest valor.

    Valor per defecte:

    sysctl -w net.core.somaxconn="128""
    

    Valor per aplicat:

    sysctl -w net.core.somaxconn="12800"
    

    Numero màxim de connexions que estan passant de LISTEN a ESTABLISHED, si es supera aquest número de connexions establertes el sistema les rebutja.

    Valor per defecte:

    sysctl -w net.ipv4.tcp_max_tw_buckets="262144"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_max_tw_buckets="524284"
    

    Numero màxim de connexions en estat TIME_WAIT, quant el superem, el sistema elimina els sockets i envia un avís. Aquest limit només s’utilitza per AVISAR de atacs DDOS. Aquest valor no s’acostuma a baixar sinó a incrementar proporcionalment amb la memòria, si les condicions de xarxa aixi ho requereixen.

    Valor per defecte:

    sysctl -w net.core.rmem_default="229376"
    

    Valor per aplicat:

    sysctl -w net.core.rmem_default="262142"
    

    Mida de la memòria asignada per defecte en la recepció pels socket

    Valor per defecte:

    sysctl -w net.core.rmem_max="131071"
    

    Valor per aplicat:

    sysctl -w net.core.rmem_max="524284"
    

    Mida màxima de memòria assignada per defecte en la recepció pels socket, aquest valor ha de ser superior al net.core.rmem_default
    Alerta! En RHEL compleix aquesta ultima recomanació però en CentOS6 no ho segueix

    Valor per defecte:

    sysctl -w net.core.wmem_default="229376"
    

    Valor per aplicat:

    sysctl -w net.core.wmem_default="262142
    

    Mida de memòria asignada per defecte en l’enviament pels socket

    Valor per defecte:

    sysctl -w net.core.wmem_max="131071"
    

    Valor per aplicat:

    sysctl -w net.core.wmem_max="524284"
    

    Mida màxima de memòria assignada per defecte en la emissió pels socket, aquest valor ha de ser superior al net.core.rmem_default
    Alerta! En RHEL compleix aquesta ultima recomanació però en CentOS6 no ho segueix

    Valor per defecte i aplicat:

    sysctl -w net.ipv4.tcp_rmem="4096 87380 4194304"
    

    Paràmetres de auto configuració TCP en la recepció de dades: El primer valor es la mida mínima de memòria utilitzat per a cada connexió TCP. El segon valor especifica la mida PER DEFECTE de buffer en la recepció per a cadascun dels socket. Aquest valor sobreescriu el /proc/sys/net/core/rmem_default utilitzat en altres protocols(!=TCP). El tercer i ultim valor especifica la mida màxima de memòria per la recepció de dades per a cadascun de les connexions TCP.

    Valor per defecte i aplicat:

    sysctl -w net.ipv4.tcp_wmem="4096 65536 4194304"
    

    Parametres de auto configuració TCP en la emissió: Aquests tres valors indiquen quin es l’espai de memòria assignat a cada socket en l’enviament de dades. El primer es la mida mínima, el segon la mida per defecte i el tercer la mida màxim del buffer reservat per a l’enviament de dades.

    Valor per defecte:

    sysctl -w net.ipv4.tcp_mem="753888 1005184 1507776"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_mem="753888 1005184 4194304"
    

    Paràmetres de auto configuració TCP: Defineix com el sistema operatiu gestion l’ús de la memòria per a les connexions TCP. El primer valor especifica el umbral mínim. Per sota d’aquest valor el SO no va cap canvi de gestió de la memòria pels diferents socket TCP. El segons valor indica la mida per defecte permès per a 1a connexió TCP, el tercer valor ens indica la mida màxima de memòria utilitzat per les connexions TCP, si el SO consumeix més memòria per a les connexions TCP comença a fer DROPS de les noves connexions.

    Valor per defecte i aplicat:

    sysctl -w fs.file-max="793779"
    

    Defineix el número màxim de descriptors de fitxers que gestiona el SO (Recordem que tant els sockets com els fitxers son descriptors de fitxers en entorns Linux/Unix)

    Valor per defecte:

    sysctl -w net.ipv4.tcp_tw_reuse="0"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_tw_reuse="1"
    

    Permetre reutilitzar els sockets que estan en TIME_WAIT quant es segur des del punt de vista del protocol. Non hauria de ser modificat sense la supervisió d’un expert 😉

    Valor per defecte:

    sysctl -w net.ipv4.tcp_tw_recycle="0"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_tw_recycle="1"
    

    Permetre la rapida reutilització de sockets en TIME_WAIT. Activant aquesta opcio no es recomenada si estas fent NAT. En canvi amb el HAPROXY es molt recomenable per baixar CONSIDERABLEMENT el numero de connexions en TIME_WAIT. Adjunto una captura de graphite on podem veure el canvi només aplicant aquesta variable.
    haproxy

    Valor per defecte:

    sysctl -w net.ipv4.tcp_max_orphans="262144"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_max_orphans="30000"
    

    Número màxim de TCP sockets orfans que no estan connectats a cap proces. Quant aquest numero s’excedeix, les connexions orfanes es resetejant i es notifica amb una alerta. Aquest limit existeix nomes per prevenir atacs de denegament de servei. Decrementar aquest valor no es aconsellable. Les condicions de xarxa poden requerir que incrementis el numero de orfans permesos, cadascun dels orfans poden ocupar aproximadament 64k de memoria NO-SWAPEJABLE. El valor per defecte es igual que el parametre NR_FILE del kernel, en 262144 en RHEL6/CentOS6, ajustat a la memoria del sistema.

    Valor per defecte:

    sysctl -w net.ipv4.tcp_synack_retries="5"
    

    Valor per aplicat:

    sysctl -w net.ipv4.tcp_synack_retries="3"
    

    Número màxim de vegades que un segment SYN/ACK per a una connexio TCP passiva sera retransmitit. Aquest numero no pot excedir de 255.

    Límits del sistema Operatiu
    Amb concordança amb el últim paràmetre explicat en l’apartat anterior, hem de ampliar el numero de descriptors de fitxers que permetem realitzar al usuari que instancia el nginx, afegint les línies següents al fitxer /etc/security/limits.conf

    nginx       soft    nofile   10000
    nginx       hard    nofile   30000
    

    Configuració del nginx
    No menys important que les configuracions de sistema són la configuració del nginx.

    Primerament cal dimensionar la memòria i la CPU del sistema. Cal dimensionar assignant els valors correctes de worker_processes i worker_connections tenint en compte la formula: max clients = worker_processes * worker_connections/4. En el meu cas tenim més de 1200 connexions concurrents on tinc una maquina amb 8 vCPUs i 8GB de RAM, on he assignat:

    worker_processes  8;
    events {
        worker_connections  1024;
    }
    

    Seguidament segons el tipus d’aplicacions que estiguem balancejant amb l’nginx, tenint en compte el número i el tipus d’aplicacions, les connexions concurrents que requereixen, la mida tant de les capçaleres com del cos, tant de les peticions com de les respostes, caldrà afinar els següents paràmetres.

    ## Size Limits
      client_body_buffer_size     128K;
      client_header_buffer_size   8k;
      client_max_body_size          1M;
      large_client_header_buffers 8 32k;
    ## Timeouts
      client_body_timeout   60;
      client_header_timeout 60;
      expires               24h;
      keepalive_timeout     60 60;
      send_timeout          60;
      proxy_connect_timeout 60s; 
      proxy_read_timeout 120s;
      proxy_send_timeout 120s
    ## TCP options
      tcp_nodelay on;
      tcp_nopush  on;
    ## Proxy options
      proxy_buffering           on;
      proxy_buffers 16 16k;
      proxy_buffer_size 32k;
    ##Fix header too big http://forum.nginx.org/read.php?2,188352
      fastcgi_buffers 16 16k;
      fastcgi_buffer_size 32k;
    

    Aquests paràmetres els utilitzo per a publicar aplicacions PHP i contingut estàtic on es connecten tot tipus de navegadors tan estacions de treball, ordinadors portàtils, dispositius mòbils com tabletes des de diferents proveïdors ADSL, 2G, 3G, 4G, WiMax, FTH, etc…. Remarco que no utilitzo la funció de cache del nginx.
    Seguidament faig esmena de 2 problemes que m’he trobat amb les nostres aplicacions.

    Per part del Nginx, si volem augmentar les mides de la capçalera que reben del frontal a més de 16kb, també s’ha de parametritzar el parametres fastcgi_buffers i fastcgi_buffer_size tal com ho reporten en el foro aquest link, no li vaig trobar logica però va ser aplicar-ho i desapareixer.

    Per part del Haproxy també hi han limitacions del tamany del buffsize, en Centos/RHEL els paquets compilats del repositori yum estan tots a 16kb, en el foro de HAPROXY trobem com recompilar-ho
    Molt útil per corregir aquest tipus de errors:

    echo show errors | socat stdio unix-connect:<path-to-socket>
    

    Les principals fonts que he seguit són:

    http://linux.die.net/man/7/tcp
    http://dak1n1.com/blog/12-nginx-performance-tuning
    http://www.cyberciti.biz/faq/rhel-linux-install-nginx-as-reverse-proxy-load-balancer/
    http://www.cyberciti.biz/faq/linux-unix-nginx-too-many-open-files
    http://www.exceliance.fr/sites/default/files/biblio/art-2006-making_applications_scalable_with_lb.pdf
    http://comments.gmane.org/gmane.comp.web.haproxy/11145