World Wide Web

La quasi intégralité des articles sur l’optimisation des performances Nginx recommandent l’utilisation des options sendfile, tcp_nodelay et tcp_nopush, mais aucun d’entre eux n’explique réellement quels effets elles ont sur le serveur Web et comment elles fonctionnent.

Aiguillonné par Greg à l’occasion d’une peer review en mode prove it mothahfuckah, je me suis plongé dans les tréfonds de la pile TCP histoire d’en comprendre les tenants et les aboutissants, d’autant qu’à première vue, combiner les deux dernières a autant de sens qu’un dictionnaire chez Franck Ribery (ou alors pour caler un meuble).

tcp_nodelay

Comment forcer une socket à envoyer toutes les données qu’elle a dans sa file d’attente ? Une des solutions se trouve dans l’option TCP_NODELAY de la pile TCP(7). Activer TCP_NODELAY force l’envoi immédiat des données contenues dans la socket, quelle que soit la taille du paquet, et c’est ce que fait l’option tcp_nodelay de Nginx.

Afin d’éviter de congestionner le réseau, la pile TCP dispose d’un mécanisme permettant d’attendre jusque’à 0,2 secondes l’arrivée de données avant d’envoyer un paquet trop petit. Ce mécanisme est assuré par l’algorithme de Nagle, et les 200ms correspondent à la valeur choisie pour son implémentations sur la pile réseau UNIX.

Pour comprendre l’utilité de Nagle, il faut se rappeler qu’Internet n’a pas toujours servi à transférer des pages Web et de gros fichiers. Imaginez-vous connecté en telnet sur une machine. Quand vous appuyez sur ctrl+c, vous envoyez un message d’un octet à la machine distante ; à cela, il vous faut rajouter les en-têtes IP (20 octets en IPv4, 40 en IPv6), plus les en-têtes TCP (20 octets). On arrive donc facilement à 60 octets d’en-têtes pour 1 octet de données. Nagle permet d’attendre que vous ayez tapé d’autres caractères au clavier avant d’envoyer votre message.

Quoi Nagle, mais qu’est-ce qu’elle a Nagle ?

Le problème de Nagle, c’est qu’il n’est plus adapté à Internet tel que nous le connaissons, et notamment à la transmission de flux de données. Il y a très peu de chances pour qu’un fichier fasse très exactement un nombre entier de paquets. Nagle oblige donc le fichier à attendre 0,2 secondes pour envoyer le dernier paquet, créant une latence côté client.

L’option TCP_NODELAY permet de désactiver Nagle, et donc d’envoyer les données dès qu’elles sont disponibles.

Nginx utilise l’option TCP_NODELAY sur les connexions keepalive HTTP, c’est à dire des sockets qui restent ouvertes un certain temps après avoir terminé d’envoyer du contenu. keepalive évite d’ouvrir une nouvelle connexions et de rejouer un 3 ways handshake chaque fois qu’une requête HTTP est terminée. Cela permet de gagner du temps et d’économiser des sockets, puisqu’elles ne passent pas en FIN_WAIT à la fin d’un transfert. Connection: Keep-alive est une option en HTTP 1.0, et le comportement par défaut d’HTTP 1.1.

Sur l’affichage d’une page Web, TCP_NODELAY peut faire économiser jusqu’à 0,2 secondes sur chaque requête, ce qui est déjà bien, pour du jeu en réseaux ou des activités comme le trading haute fréquence, cela devient plus que critique, au prix d’une “saturation” relative du réseau.

tcp_nopush

La directive de configuration tcp_nopush de Nginx fait exactement le contraire de tcp_nodelay : au lieu d’optimiser les délais d’envoi des informations, elle permet d’optimiser la quantité d’informations envoyée en une seule fois.

Histoire d’être parfaitement logiques, sous Linux, tcp_nopush active l’option TCP_CORK de la pile TCP, puisque l’option TCP_NOPUSH n’existe que sous FreeBSD.

TCP_CORK, de l’anglais bouchon force TCP à n’envoyer que des paquets ayant atteint la MSS, qui est égale au MTU moins les 40 ou 60 octets d’en-tête selon que l’on se trouve en IPv4 ou IPv6,

Vie et mort de TCP_CORK dans une fenêtre de paquets

Tout cela est très bien expliqué dans les sources du noyau Linux.

/* Return false, if packet can be sent now without violation Nagle's rules:
 * 1. It is full sized.
 * 2. Or it contains FIN. (already checked by caller)
 * 3. Or TCP_CORK is not set, and TCP_NODELAY is set.
 * 4. Or TCP_CORK is not set, and all sent packets are ACKed.
 *    With Minshall's modification: all sent small packets are ACKed.
 */

static inline bool tcp_nagle_check(const struct tcp_sock *tp,
                                    const struct sk_buff *skb,
                                    unsigned int mss_now, int nonagle)
{
  return skb->len < mss_now &&
    ((nonagle & TCP_NAGLE_CORK) ||
    (!nonagle && tp->packets_out && tcp_minshall_check(tp)));
}

Une particularité de TCP_CORK, c’est que l’option doit être explicitement désactivée si l’on souhaite à nouveau envoyer des paquets à moitié vides (ou à moitié pleins, ou avec un buffer deux fois trop grand moins les en-têtes, c’est selon).

La page de manuel de TCP(7) spécifie que les options TCP_NODELAY et TCP_CORK sont mutuellement exclusives, mais qu’elles peuvent être combinées depuis le noyau 2.5.9.

Dans la configuration de Nginx, tcp_nopush est indissociable de l’option sendfile, c’est d’ailleurs là qu’elle devient vraiment intéressante.

sendfile

C’est la directive de configuration sendfile qui, associée à tcp_nodelay et tcp_nopush fait la puissance de Nginx pour servir des fichiers statiques. Cette directive permet de forcer l’utilisation de l’appel système sendfile(2) pour tout ce qui concerne l’envoi de fichiers.

sendfile(2) permet de transférer des données d’un descripteur de fichier vers un autre directement dans l’espace noyau. sendfile(2) permet de faire d’importantes économies de ressources à plusieurs niveaux :

  • sendfile(2) est un appel système. Il est donc directement exécuté dans l’espace noyau, ce qui permet d’éviter les changements de contexte.
  • sendfile(2) se substitue à l’utilisation combinée de read et write.
  • dans notre cas, sendfile(2) permet de faire du zéro copy, c’est à dire écrire directement dans le buffer kernel depuis la mémoire du block device par DMA sans passer par la case départ et sans toucher 20000 francs.

Malheureusement, sendfile(2) nécessite un descripteur de fichiers supportant mmap(2) et cie, ce qui exclut de facto son application sur une socket UNIX, par exemple pour accélérer le chargement de pages dynamiques générées par un serveur d’application.

The in_fd argument must correspond to a file which supports mmap(2)-like operations (i.e., it cannot be a socket).

En fonction de votre utilisation de Nginx, sendfile peut être complètement inutile ou totalement indispensable.

Si vous servez des fichiers statiques stockés localement, que ce soit sur le disque dur ou sur un tmpfs dans le cas de micro caching, sendfile est absolument indispensable pour améliorer les performances de votre serveur Web.

Si en revanche, vous n’utilisez nginx qu’en guise de reverse proxy devant un ou plusieurs serveurs d’application, sendfile ne vous servira absolument à rien, sauf, une fois encore, si vous mettez en place du micro caching, mais une telle mise en place sort du cadre de cet article.

Et on mélange le tout

Les choses deviennent intéressantes quand on mélange sendfile, tcp_nodelay et tcp_nopush. Je me demandais à quoi pouvait servir d’activer deux options antinomiques et exclusives l’une de l’autre. La réponse se trouve dans un thread (en russe) de 2005 de la liste de diffusion Nginx.

Dans le cas de sendfile, tcp_nopush s’assure que tous les paquets soient bien remplis avant d’être envoyés au client, ce qui limite l’overhead réseau, et peut accélérer sensiblement l’envoi de fichiers. Puis, quand on arrive au dernier demi paquet, Nginx désactive tcp_nopush sur la socket, et force l’envoi des données avec tcp_nodelay ce qui permet d’économiser jusqu’à 0,2 secondes.

Ce comportement est confirmé dans ce commentaire des sources de la pile TCP du noyau Linux à propos de TCP_CORK:

When set indicates to always queue non-full frames. Later the user clears this option and we transmit any pending partial frames in the queue. This is meant to be used alongside sendfile() to get properly filled frames when the user (for example) must write out headers with a write() call first and then use sendfile to send out the data parts. TCP_CORK can be set together with TCP_NODELAY and it is stronger than TCP_NODELAY.

Classe non ?

Voilà, c’est à peu près tout. J’ai volontairement passé sous silence l’existence de writev(2) comme une alternative à tcp_nopush afin de ne pas compliquer les choses. J’espère ne pas avoir dit trop de bêtises, aussi n’hésitez pas à m’envoyer un petit mail pour compléter ou corriger, je le publierai avec plaisir.

Merci à Arthur, Bruno, Bsdsx et Ludovic pour le travail de relecture tant sur le fond que sur la forme, et à Greg pour ses explications et pour m’avoir filé des coups de pieds au derrière jusqu’à ce que je revienne avec des réponses à toutes ses questions.

Perry the Platypus wants you to subscribe now! Even if you don't visit my site on a regular basis, you can get the latest posts delivered to you for free via Email: