HTTP/1.1

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Ce billet fait partie d'une série en 10 volets : #1, #2, #3, #4, #5, #6, #7, #8, #9 et #10.

Notes de lecture de HTTP/1.x.

Améliorer les performances du protocole HTTP/1.0 était l'un des principaux objectifs du groupe de travail HTTP/1.1.

Si le pari fut en partie réussi, certaines améliorations n'ont pas été implémentées et les limitations du protocole ont conduit à l'élaboration de solutions de contournement pour réduire la latence.

Les ouvrages de référence en la matière :

HTTP keep-alive

Les connexions HTTP persistantes ou keep-alive sont une des principales améliorations des performances apportées par HTTP/1.1.

Le temps total minimum pour une requête HTTP est de deux allers-retours réseau : un pour le TCP handshake et un pour le cycle requête-réponse. C'est un coût fixe minimum (non HTTPS) imposé à toutes les sessions HTTP.

L'en-tête keep-alive permet de réutiliser la connexion HTTP pour les demandes supplémentaires : la première demande engendre deux allers-retours, et toutes les demandes suivantes n'engendrent qu'un aller-retour de latence.

L'économie en latence pour N requêtes est de (N-1) * RTT (RoundTrip Time) sachant que la valeur moyenne de N est de 75 ressources en 2019.

Les navigateurs tentent d'utiliser des connexions HTTP persistantes automatiquement.

HTTP Pipelining

Les connexions persistantes impliquent un traitement séquentiel strict des demandes (FIFO) : envoi d'une requête, attente de la réponse, envoi de la requête suivante depuis la file d'attente du client etc.

L'idée du pipelining HTTP est de transférer la file d'attente du client (mise en queue des requêtes) au serveur (mise en queue des réponses).

Il s'agit d'envoyer plusieurs requêtes sans attendre les résultats. Le serveur a la capacité de les traiter en parallèle (threads multiples, workers multiples etc.) mais doit renvoyer les réponses dans l'ordre une après l'autre car HTTP/1.x ne permet pas le multiplexage sur la même connexion.

À cause de l'absence de multiplexage, l'HTTP pipelining crée des problèmes subtils :

  • une réponse lente peut bloquer toutes les requêtes suivantes : parce qu'il doit renvoyer les réponses dans l'ordre, un serveur peut devoir mettre en cache une réponse prête avant la réponse à renvoyer pendant un traitement parallèle (que faire si l'une des réponses est volumineuse ?)
  • une réponse en échec peut mettre fin à la connexion TCP, forçant le client à redemander toutes les ressources et le serveur à un traitement en double
  • des intermédiaires (proxies) peuvent rendre difficile la mise en place, voire interrompre la connexion

En raison de ces complications et de l'absence de directives dans la norme HTTP/1.1, l'adoption du pipelining HTTP est restée limitée.

La plupart des navigateurs ont désactivé cette option. Le pipelining HTTP n'est pas une option dans le contexte des navigateurs Web mais peut l'être ailleurs !

Utilisation de plusieurs connexions TCP

À cause de l'absence de multiplexage dans HTTP/1.x, les navigateurs n'ont pas d'autre choix que d'ouvrir plusieurs sessions TCP en parallèle.

Ça permet d'envoyer autant de requêtes parallèles que de connexions ouvertes, mais au prix :

  • d'une augmentation de la consommation des ressources côté client, serveur et intermédiaires (cache et charge CPU engendré par les sockets supplémentaires)
  • d'une concurrence pour la bande passante entre les connexions TCP parallèles
  • d'une complexité d'implémentation pour la gestion des sockets multiples côté navigateur
  • d'une limite maximale de parallélisme en fonction des navigateurs pour, entre autres, prévenir des attaques par déni de service (DoS) intentionnelles ou non

Ce n'est pas une bonne solution à long terme.

Domain Sharding

Le nombre de connexions parallèles maximum peut encore être insuffisant pour certaines applications.

Le domain sharding permet de contourner cette limitation en utilisant plusieurs sous-domaines.

Avec des hostnames différents, on augmente le niveau de parallélisme au prix :

  • d'une résolution DNS supplémentaire par hostname
  • d'une augmentation de la consommation des ressources des deux côtés
  • d'une gestion manuelle de l'endroit du stockage et de la répartition des données

En pratique plusieurs sous-domaines peuvent pointer vers la même IP via des enregistrements de type CNAME.

Le domain sharding est souvent mal utilisé provoquant des flux TCP sous-utilisés.

Il vaut mieux commencer par le nombre minimum de shards (c'est à dire aucun) puis augmenter en mesurant les gains.

Peu de sites bénéficient de plus d'une douzaine de connexions et un dépassement de ce seuil devrait inciter à limiter le nombre de ressources demandées.

Métadonnées

En HTTP/1.x, les en-têtes (des requêtes ou des réponses) sont extensibles et envoyées en texte brut pour rester compatibles avec les précédentes versions d'HTTP.

Chaque requête HTTP émanant d'un navigateur transporte de 500 à 800 octets d'en-têtes HTTP, sans compter les cookies.

Avec les cookies, ces métadonnées HTTP (non compressées) peuvent ajouter plusieurs kilo-octets à chaque requête HTTP/1.x.

Dans les API Web par exemple (communiquant en JSON) il arrive que les en-têtes soient plus lourdes que le corps des requêtes.

La réduction des métadonnées peut permettre d'économiser des allers-retours de latence. De nombreux développeurs oublient que les cookies ajoutent une surcharge importante à chaque requête.

Concaténation et Spriting

Pour limiter le nombre de requêtes émises, une astuce consiste à regrouper plusieurs ressources en une seule :

  • concaténation : plusieurs fichiers JavaScript ou CSS sont combinés en un seul
  • spriting : plusieurs images sont combinées en une seule

Ces techniques peuvent parfois avoir l'effet inverse de celui escompté à cause de leurs inconvénients :

  • les bundles (fichiers combinés) peuvent contenir des ressources qui ne sont pas nécessaires pour la page en cours
  • la mise à jour d'un bundle nécessitera son invalidation en cache et son nouveau téléchargement complet (d'où les stratégies de cache busting)
  • JavaScript et CSS sont exécutés uniquement lorsque le transfert est terminé, ce qui peut retarder la première la mise en peinture par le navigateur
  • pour le spriting, le navigateur doit stocker l'image entière en mémoire quelle que soit la taille réelle de la zone affichée

Inlining des ressources

Une autre astuce consiste à inliner les ressources directement dans le document pour économiser des requêtes.

Les fichiers JavaScript et CSS peuvent être inclus en ligne, et les autres ressources comme les images, l'audio ou les fichiers PDF le peuvent aussi via le data URI scheme.

Les navigateurs imposent une limite aux données incluses via le data URI.

Le data URIs est particulièrement adapté aux ressources de petite taille et idéalement limitées à des pages spécifiques. En effet, quand une ressource est incluse en ligne dans une page, elle ne peut plus être mise en cache individuellement. Si elle est incluse dans plusieurs pages, alors elle sera téléchargée à chaque fois, ce qui augmentera la taille de chaque page. Si la ressource doit être mise à jour, il faudra alors invalider le cache de toutes les pages dans lesquelles elle apparaît.

Un autre inconvénient de cette technique est qu'elle nécessite, pour les ressources non textuelles, un encodage en base64 qui génère un overhead significatif. Une bonne pratique consiste à envisager l'inlining pour les ressources inférieures à 1-2 KB, car les ressources plus grosses engendrent un overhead plus élevé que la ressource elle-même.

Avant ABC de la performance Web Après HTTP/2

Tag Kemar Joint