Nginx : tips pour booster le service

Bonjour à tous et Bonne Année à ceux qui ont raté la première salve ! Je vous parlais ici de comment aborder calmement NGinx, cet outil russe jouant serveurs web et reverse proxy. Je vous propose donc cette fois-ci quelques idées qui pourront améliorer d'un peu les performances de votre service.

Factorisation, segmentation

Avant de parler fonctionnalité, mettons l'accent sur les grands fondamentaux : un code clair et surtout bien organisé. Bien sûr, il ne s'agit pas de faire la leçon en terme de lisibilité ou autre perche tendue à un utilisateur inexpérimenté, mais bien de réorganisation d'instructions, afin que NGinx ne perde pas son temps à évaluer et réévaluer une expression qui n'a pas forcément à l'être -du moins, pas autant de fois.

Factorisation

Un exemple est très frappant, qui est donné par la Communauté :
http {
  index index.php index.htm index.html;
  server {
    server_name www.domain.com;
    location / {
      index index.php index.htm index.html;
      [...]
    }
  }
  server {
    server_name domain.com;
    location / {
      index index.php index.htm index.html;
      [...]
    }
    location /foo {
      index index.php;
      [...]
    }
  }
}
On constate la répétition inutile -mais ici, elle coûte pas en performances, c'est déjà ça- des fichiers à rechercher, par ordre de préférence. Le copier-coller est facile, mais pas très élégant, surtout lorsque l'on sait que NGinx utilise le fichier de conf -donc les blocs et sous-blocs- comme une hiérarchie. D'autant plus que si l'on définie un nouveau serveur, mais qu'on oublie notre joli paste, on peut s'attendre à ce que NGinx botte en touche. Pourquoi dans ce cas, ne pas profiter de l'héritage ?
http {
  index index.php index.htm index.html;
  server {
    server_name www.domain.com;
    location / {
      [...]
    }
  }
  server {
    server_name domain.com;
    location / {
      [...]
    }
    location /foo {
      [...]
    }
  }
}
Comme je le souligne plus haut, cet exemple n'illustre pas une perte de performance sur l'évaluation d'une instruction.

En revanche, prenons dans le cas inverse : que se passerait-il si justement on stipulait très tôt un traitement déclenché par une regex ?

Segmentation

N'oublions pas que NGinx fonctionne non pas sur la base de session mais bien de requête. Une regex placée un peu trop haut impliquerait son évaluation pour toutes les requêtes devant passer par le bloc, et donc impacterait plus significativement les performances. Il en va de même pour n'importe quel type de test, y compris le fameux If. Exemple :
server {
  server_name domain.com *.domain.com;
  if ($host ~* ^www\.(.+)) {
    set $raw_domain $1;
    rewrite ^/(.*)$ $raw_domain/$1 permanent;
    [...]
  }
}
Ici, à chaque requête, on doit analyser le Host Header pour voir s'il colle à notre if regexpé. Autant dire qu'on fait travailler le serveur pour pas grand' chose... A ce moment, et comme on opère tout de même un rewrite si le if est validé, tant qu'à faire, autant fractionner au plus près des besoins ! En mieux, on tombe sur un truc du genre :
server {
  server_name www.domain.com;
  rewrite ^ $scheme://domain.com$request_uri? permanent;
}
server {
  server_name domain.com;
  [...]
}
Le premier bloc s'occupera donc d'opérer le rewrite pour toute requête s'adressant à www.domain.com, et uniquement celles-là, et les renverra vers domain.com. Une fois renvoyées, elles ne passeront plus systématiquement par l'évaluation du Host Header. Ca, c'est fait. Même principe pour ceux qui poussent tout sur PHP. Plutôt que d'envoyer tout et absolument tout au proxy, autant segmenter pour coller au plus près de ce que l'on doit fournir à PHP, quitte à passer par un try_file.
server {
    server_name _;
    root /var/www/site;
    location / {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/tmp/phpcgi.socket;
    }
}
En mieux :
server {
    server_name _;
    root /var/www/site;
    location / {
        try_files $uri $uri/ @proxy;
    }
    location @proxy {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/tmp/phpcgi.socket;
    }
}
Le try_file permet de tester d'abord la présence de la donnée en locale, et de la servir. Sinon, on passe par une location factice appelée @proxy qui s'appliquera à pousser tout ce petit monde à PHP. Bref vous l'avez compris, la décomposition des éléments joue un rôle très appréciable ! Non seulement votre configuration est claire, mais en plus ça a l'avantage d'améliorer de manière drastique la façon dont sont servies les données et donc les performances de la bête.

Compression

Cette fois-ci, penchons-nous non pas sur le temps de process d'une requête mais sur le goulot d'étranglement classique : la bande passante. En effet, la richesse et la forte sollicitation d'un site peut facilement peser sur un serveur, surtout si celui-ci est tout seul et donc ne dispose pas de moyen de gérer la charge. NGinx dispose d'un ensemble de modules qui prennent en charge la compression et la réutilisation des éléments déjà compressés qui seront poussés vers le client. Il s'agit notamment des modules suivants :
  • HttpGzipModule, module mettant à disposition une fonctionnalité de compression à la volée,
  • HttpGzipStaticModule, le module qui permet de rechercher et d'utiliser une version compressée d'un élément, plutôt que de recompresser une donnée déjà traitée. Par contre, pour disposer de ce module, il faut compiler NGinx avec le mot magique : ./configure --with-http_gzip_static_module
Concrètement, à la première requête, le délai de réponse souffrira un peu de la compression de l'élément, mais grâce à une gestion plus intelligente, l'opération n'est faite que si nécessaire. Il suffit ensuite de mettre NGinx au courant, au niveau du contexte (du super-bloc) http :
  gzip            on;
  gzip_http_version 1.0;
  gzip_comp_level 2;
  gzip_vary           on;
  gzip_proxied     any;
  gzip_types      text/plain text/html text/css application/x-javascript text/xml application/xml
application/xml+rss text/javascript;
D'ailleurs, on remarque avec un intérêt le paramètre gzip_proxied : il s'agit de compresser la réponse faite par le proxy.

Plusieurs paramétrages sont possibles, et en plus cela permet d'introduire une autre fonctionnalité permettant de soulager votre serveur : le cache !

Cache

Par location

Il arrive que certaines -beaucoup parfois- des données qui sont sollicitées sont ce qu'on appelle des données statiques : elles ne changent jamais. En jouant sur la gestion de ce type de source, on peut donc facilement améliorer les performances de notre service. D'abord en isolant bien proprement le bloc associé, afin de ne pas rentrer dans des moulinets gourmands alors que ces données n'ont besoin d'aucun traitement. Mais surtout il y a le cache. Si elles ne changent que très rarement, pas besoin d'aller systématiquement les chercher, autant les mettre en cache et forcer une expiration relativement raisonnable. Bon, 10 ans, c'est quand même un peu beaucoup...
location ~ ^/(images|javascripts|stylesheets)/ {
  expires 10y;
}

En configuration Reverse Proxy

Possible également d'utiliser le cache dans une configuration de reverse proxy, comme en témoigne cet exemple sorti du wiki :
http {
    proxy_cache_path  /data/nginx/cache  levels=1:2    keys_zone=STATIC:10m
                                         inactive=24h  max_size=1g;
    server {
        location / {
            proxy_pass             http://proxy.domain.com;
            proxy_set_header       Host $host;
            proxy_cache            STATIC;
            proxy_cache_valid      200  1d;
            proxy_cache_use_stale  error timeout invalid_header updating
                                   http_500 http_502 http_503 http_504;
        }
    }
}
Pour expliquer un peu :
  • le proxy_cache_path stipule les paramètres de stockage et d'utilisation du cache : chemin(/data/nginx/cache), niveau de sous-répertoire à mettre en cache(1:2), le nom et la taille de la zone (STATIC:10m), le temps durant lequel le cache est gardé et à partir duquel il est flushé (24h)...
  • proxy_pass explicite le serveur qui va jouer le proxy. Sa définition est à votre convenance : IP, IP:port, FQDN, socket...
  • proxy_cache reprend la zone définie par proxy_cache_path, quant à proxy_cache_valid, il paramètre la durée du cache en fonction des réponses retournées : ici 1 jour pour un code 200 renvoyé. Il est d'ailleurs possible d'avoir de multiples instructions proxy_cache_valid, pour chaque code retour intéressant, afin de pouvoir stipuler les durées selon les besoins.
  • proxy_cache_use_stale enfin spécifie quand servir les caches plutôt que de requêter les éléments, mais aussi de faire en sorte (option updating) que si plusieurs requêtes demandent un update de l'élement mis en cache, seule une traitera cet update tandis que les autres continueront à se servir du cache, le temps que l'update soit fait.
Au final, on verra que le cache a écrit ses données dans un fichier nommé et référencé grâce au hash MD5 de l'URL qui est passée au proxy. Pour ceux qui veulent approfondir, c'est par ici ! Évidemment, NGinx n'est pas le seul à savoir gérer les caches, et certaines solutions très pertinentes peuvent très bien se marier avec notre produit russe. Citons par exemple MemCached, dont le support est intégré de manière standard. L'exemple pour la forme est celui du wiki :
server {
  location / {
    set $memcached_key $uri;
    memcached_pass     name:11211;
    default_type       text/html;
    error_page         404 @fallback;
  }

  location @fallback {
    proxy_pass backend;
  }
}
NGinx intègre également un module memc, qui est une version étendue de memcached. Memc offre des options de gestion et de manipulation de commande Memcached. La configuration est nettement moins sympa, mais reste claire tout de même :
    # GET /bar?cmd=get&key=cat
    # GET /bar?cmd=set&key=dog&val=animal&flags=1234&exptime=2
    # GET /bar?cmd=delete&key=dog
    # GET /bar?cmd=flush_all
    location /bar {
        set $memc_cmd $arg_cmd;
        set $memc_key $arg_key;
        set $memc_value $arg_val;
        set $memc_flags $arg_flags; # defaults to 0
        set $memc_exptime $arg_exptime; # defaults to 0

        memc_cmds_allowed get set add delete flush_all;

        memc_pass 127.0.0.1:11211;
    }
Je vous renvoie à la page du wiki pour plus explicite.

Remaniement des requêtes

Server name vs Host Header

Dans le cas d'une configuration simple, c'est-à-dire que tout les serveurs écoutent sur la même adresse et sur le même port, de configuration identique, il est possible de bypasser l'évaluation du Host Header des requêtes soumises et de les rediriger automatiquement sur le premier server_name donné. C'est le rôle de l'instruction optimize_server_names [on|off]. Par défaut, ce paramètre est à on. Cela permet notamment d'éviter l'analyse systématique des Host Header de chacune des requêtes arrivant au serveur.

Création de module

Enfin, si tout cela ne couvre pas vos besoins et que vous êtes développeur dans l'âme, vous pouvez très bien vous pencher sur le développement de module. Jetez un coup d'oeil par ici si ça vous tente :)

Ouverture

En bref, NGinx est vraiment très riche, et je suis sûrement encore loin du compte ! En revanche, si vous avez pu expérimenter d'autres optimisations, n'hésitez pas à faire un petit retour de vos expériences, bonnes ou mauvaises ! Sur ce, Enjoy ! [Edit] : N'hésitez pas à jeter un coup d'œil sur le blog de NicoLargo, il y a pas mal de choses qui valent le détour :)
Vus : 2985
Publié par K-Tux : 59