Les caches HTTP

Publié le 8 mai 2012 par Nicolas Zermati | front

Cet article est publié sous licence CC BY-NC-SA

Les applications web prennent de plus en plus de place dans le paysage applicatif actuel. Ce qui, hier, était une application lourde a une forte probabilité de se voir remplacée, demain, par une application web. Ce phénomène s’accentue par les nouvelles possibilités offertes par l’évolution des réseaux (ADSL, fibre, 4G, etc). Les utilisateurs sont interconnectés, les données partagées et mises à jour régulièrement ce qui encourage plus encore l’émergence des applications web.

Ce bouleversement dans l’organisation des applications n’est pas nouveau. Depuis toujours, dans le domaine de l’Informatique, on jongle entre centralisé et décentralisé. Dans le domaine des architectures matérielles, on a les systèmes clients - serveur s’opposant aux PC. Il en est de même pour le logiciel, le protocole FTP s’opposant à des protocoles décentralisés comme Bittorrent.

Les applications web se classent dans la mouvance des architectures centralisées. Le nombre de clients peut alors prendre des dimensions extraordinaires, comme pour les réseaux sociaux. Internet se base grandement sur HTTP (« Hypertext Transfer Protocol »). Ce protocole met à disposition des méthodes de mise en cache permettant d’optimiser les interactions entre client et serveur.

Cet article introduira depuis zéro ce que HTTP prévoit dans le domaine des caches. L’objectif de tels systèmes est d’éviter qu’une partie des requêtes des clients atteignent le serveur, améliorant ainsi la réactivité du serveur pour les autres requêtes. Les effets bénéfiques sont nombreux, moins de sollicitations du réseau, de la base de donnée, de temps de calcul pour le serveur, etc.

Introduction

En Informatique un cache est un système qui mémorise des données en vue d’une réutilisation ultérieure. L’objectif est d’utiliser la copie présente dans le système de cache plutôt que d’interroger la véritable source des données lors des accès futurs. Pour HTTP, les données correspondent aux contenus des pages web.

Mémoriser une donnée dans un cache implique de pouvoir identifier la donnée à mémoriser. Dans le domaine des mémoires matérielles, on utilise une adresse pour identifier les données. Dans le cas d’HTTP, on utilise l’URL pour identifier les pages. On verra par la suite que l’URL n’est pas le seul élément permettant d’identifier une page.

Un autre aspect fondamental dans ce domaine est l’expiration du contenu. En effet, il faut assurer la cohérence des données mémorisées dans le cache par rapport aux données fournies par la source. Pour atteindre ce but on doit disposer d’un moyen de valider les données mémorisées.

Les caches ne peuvent pas toujours contenir l’intégralité des données d’une source. Dans ces cas-là, ils doivent mettre en place des stratégies de remplacement des données dites cachées. Nous ne parlerons pas de cette problématique dans l’article.

HTTP ne fournit pas directement un système de cache, mais il permet, et c’est ce que nous verrons dans cet article, d’identifier, d’expirer et d’assurer la cohérence des données. Ce sera à un logiciel tiers d’exploiter ces informations ; les navigateurs et les proxys en sont deux exemples.


Exemple sans cache HTTP


Exemple avec cache HTTP

Un cache peut être partagé lorsque plusieurs programmes ou clients qui accèdent à un même serveur. Ceci implique souvent des préoccupations de sécurité puisqu’on ne peut pas toujours partager des données entre deux clients par soucis de confidentialité par exemple. Il faut donc des moyens afin de contrôler l’accès aux données cachées.


Multiple clients, attention au partage des données.

Contrôle du cache

Le contrôle du cache désigne la manière dont on va vouloir que le cache opère. On distingue deux types de contrôle : un issu d’une requête d’un client vers le serveur et un autre issu de la réponse du serveur à destination du client. Selon le type, la signification de la directive voit sa signification modifiée.

Pour spécifier la politique de cache souhaitée, on dispose de l’entête HTTP nommé Cache-Control. Ce champ peut être valué par une liste de paramètres séparés par des virgules. On peut aussi avoir plusieurs fois le champ Cache-Control dans l’entête. Ce champ permet d’uniformiser la communication entre les serveurs des uns et les systèmes de caches des autres.

Par défaut, toute réponse valide peut être mise en cache.

Voici la liste des valeurs que peut utiliser l’entête Cache-Control :

Requête

no-cache : le client désire passer outre le cache.

no-store : le client commande de ne mémoriser ni la requête, ni la réponse à cette dernière.

max-age=#SECONDS : le client accepte uniquement une réponse dont l’âge est inférieur à #SECONDS mais sans pour autant être expirée.

max-stale[=#SECONDS] : le client accepte une réponse expirée depuis moins de #SECONDS.

min-fresh=#SECONDS : le client exige une réponse validée depuis moins de #SECONDS.

no-transform : le client ne souhaite pas que des caches intermédiaires modifient la requête.

only-if-cached : le client ne souhaite qu’une réponse valide issue du cache ou bien une erreur 504.

Réponse

public : le serveur autorise la mémorisation de la page peu importe l’entête Authorization (voir la RFC2616 14.8).

private : le serveur interdit la mise en cache par un cache partagé.

no-cache : le serveur ne souhaite pas que sa réponse soit redistribuée sans validation préalable de sa part, malgré les directives du client.

no-store : le serveur interdit les caches de mémoriser en mémoire non volatile la réponse ou la requête à l’origine de celle-ci.

no-transform : le serveur interdit de modifier le contenu de sa réponse, par de la compression par exemple.

must-revalidate : le serveur interdit aux caches de retourner une page expirée, malgré les directives du client.

proxy-revalidate : le serveur interdit aux caches partagés de retourner une page expirée, malgré les directives du client.

max-age=#SECONDS : le serveur définit le temps restant avant expiration et implique que la réponse est public.

s-maxage=#SECONDS : le serveur définit le temps restant avant expiration, surpassant max-age ,et, implique que la réponse est proxy-revalidate.

Ces valeurs pour la directive Cache-Control montrent l’étendue des possibilités dont dispose un serveur pour s’interfacer avec un système de cache. Ces valeurs peuvent se combiner entre elles.

Expiration

Dans la directive Cache-Control nous venons de voir qu’il est question d’expiration et de validité. L’objectif d’un cache est d’éviter d’émettre des requêtes à destination du serveur source afin de ne pas surcharger ce dernier et de gagner en réactivité. Le cache doit donc décider s’il est nécessaire de solliciter ce serveur. Pour prendre cette décision, le système de cache vérifie que sa copie, s’il en possède une, n’est pas expirée ; qu’elle est toujours valide.

Âge

L’âge correspond au nombre de secondes écoulées depuis la génération de la page par le serveur.

L’âge d’une réponse est donc la différence temps-de-réception-de-la-réponse - temps-d-émission-de-la-réponse, le temps d’émission de la réponse est issue de l’entête Date qu’elle contient. L’âge peut aussi être directement présent dans l’entête Age, indépendamment de cette méthode de calcul.

Dans les cas ou il n’y a pas d’entête Date dans la requête, on se base sur Age ou, s’il n’y pas d’Age, on considère l’âge comme étant 0. Cette situation ne prend pas en compte le temps que la réponse a pu passer sur le réseau. On doit alors prendre en compte la différence temps-de-réception-de-la-réponse - temps-d-émission-de-la-requête et l’ajouter à l’âge.

Ces étapes permettent de calculer l’âge d’une page lorsqu’on la reçoit. Lorsqu’un cache retournera cette page, il devra ajouter y ajouter le temps écoulé depuis la réception de la réponse depuis le serveur source. En effet, la page est âgée de N secondes à sont arrivée dans le cache, si le cache sert à nouveau une page 10 secondes après l’avoir lui-même reçu, il devra lui donner un âge de N + 10 secondes.

Voici un récapitulatif issu de la RFC2616 :

apparent_age           = max(0, response_time - date_value)
corrected_received_age = max(apparent_age, age_value)
response_delay         = response_time - request_time
corrected_initial_age  = corrected_received_age + response_delay
resident_time          = now - response_time
current_age            = corrected_initial_age + resident_time

Calcul d’expiration

Une page est déclarée comme expirée ou viciée (stale en anglais) lorsque son âge est supérieur à l’âge autorisé par le serveur, ou lorsque le client en fait la demande. L’entête Cache-Control permet de calculer si oui ou non un cache est expiré. En plus de Cache-Control, il existe également l’entête Expires qui spécifie une date après laquelle la page est considéré comme expirée. La directive max-age de l’entête Cache-Control est prioritaire sur la date contenue dans l’entête Expires.

Si une réponse ne spécifie ni d’indication d’expiration ni de restriction concernant la politique de mise en cache alors le systèmes de caches sont libres d’utiliser des algorithmes de leur choix pour décider de la validité d’une page. L’une des informations pouvant être utilisées est l’entête Last-Modified.

Il est possible pour un client d’avoir pour une même URL deux pages différentes à un même instant. En effet, une requête peut passer par différents chemins sur le réseau et donc utiliser différents caches. Dans les cas ambigus les clients comme les caches considèrent que la réponse la plus pertinente est celle avec l’entête Date la plus récente. Lorsque la valeur de l’entête Date d’une page est inférieure ou égale à la date de la page cachée, on peut s’attendre a une nouvelle requête contenant Cache-Control: max-age=0 ou bien Cache-Control: no-cache pour forcer une revalidation auprès du serveur.

Validation

Maintenant qu’on a abordé les mécanismes du calcul de l’âge et d’expiration, on va pouvoir explorer les mécanismes de validation. La validation se produit lorsqu’un système de cache reçoit une requête de la part d’un client. Le système de cache doit alors décider de si oui ou non le contenu d’une page cachée doit être retourné au client.

Pour permettre de valider une page, les serveurs peuvent fournir des validateurs avec leurs pages. L’entity-tag et la date de dernière modification sont les deux validateurs qu’on peut retrouver dans l’entête HTTP. Le cache formulera alors des conditions sur la valeur de ces champs pour savoir si oui ou non, la page qu’il contient est valide.

Par défaut une page fraiche ne nécessite pas de validation ; c’est l’entête Cache-Control qui va permettre de jouer avec la validation.

ETag, Last-Modified et conditions de validation

Les entêtes ETag et Last-Modified et contiennent respectivement une chaine de caractère et une date au format HTTP. Un cache informe le serveur de ses conditions de validation par les entêtes suivantes :

  • If-Match: ETAG retourne la page demandée si elle dispose d’un entity-tag égal à ETAG (un code 412 sinon).
  • If-None-Match: ETAG retourne la page demandée si elle dispose d’un entity-tag différent de ETAG.
  • If-Modified-Since: DATE retourne la page demandée si elle est SRICTEMENT PLUS récente que DATE.
  • If-Unmodified-Since: DATE retourne la page demandée si elle est MOINS récente que DATE (un code 412 sinon).
  • If-Range: (DATE / ETAG) retourne la partie de la page demandée si elle n’a pas été modifiée : si elle est MOINS récente que DATE ou si elle dispose d’un entity-tag égal à ETAG.

On peut se demander en quoi il est intéressant de disposer de conditions comme If-Match ou If-Unmodified-Since puisqu’il s’agit de demander au serveur de ne retourner une page lorsqu’on dispose déjà de celle-ci. Ces conditions peuvent être utilisées pour n’exécuter une requête que si la page d’origine n’a pas changé. Par exemple, dans le contexte d’une architecture REST, on souhaite réaliser un PUT sur une URL mais on désire s’assurer qu’il n’y a pas eu de modification entre le moment où on a reçu le formulaire et celui où on le transmets au serveur. Dans ce cas, on peut utiliser If-Match ou If-Unmodified-Since pour exécuter notre requête uniquement si la ressource est identique à l’image que l’on avait d’elle.

Dans le diagramme suivant, un client est mis en relation avec un serveur qui lui sert une page « a » avec un client qui, 30 secondes après sa première requête, demande à nouveau la page « a » avec l’entête Cache-Control: max-age=0 ce qui signifie qu’il veut forcer la revalidation de la page. Le cache demande donc une validation au serveur en utilisant l’entête If-None-Match. Le serveur lui répond alors par un code 304 et en profite pour donner un nouvel âge maximum. Le cache exploitera alors ce nouvel âge.


Utilisation de max-age=0 durant un « Ctrl + R »

Validation forte ou faible

Lorsqu’une page (entêtes ou contenu) change, on s’attend à ce que les validateurs changent eux aussi.

On appelle validateur fort (strong validator), tout validateur vérifiant ce comportement. Cependant ce comportement n’a pas a être systématique et le serveur est libre de maintenir un validateur identique entre des pages différentes. On appelle ce dernier un validateur faible (weak validator). Dans certaines situations, le serveur peut donc considérer une page cachée comme assez proche de ce qu’il retournerait pour autoriser le cache à s’en servir.

L’ETag est un validateur fort par défaut. On peut l’utiliser en tant que validateur faible en plaçant W/ devant la valeur de l’entity-tag : ETag: W/"123456789. Last-Modified est un validateur implicitement faible puisqu’on peut modifier une page deux fois en une seconde. Dans certains cas, on peut considérer la date de dernière modification comme un validateur fort :

  • le serveur sait que la page ne peut être modifiée deux fois en une seconde ou
  • on connait la date de génération Date et elle est strictement supérieure à 60 secondes à la condition de validation If-Unmodified-Since ou If-Modified-Since ou à l’entête Last-Modified.

If-Range qui permet de récupérer une partie seulement du contenu d’une page n’utilise que des validateurs forts.

Règles autour des validateurs

HTTP spécifie des règles concernant les validateurs. Il faut bien les garder en tête lorsqu’on réalise un client ou un serveur utilisant HTTP.

Tout serveur HTTP/1.1 :

  • devrait envoyer un ETag, à moins qu’il soit impossible d’en générer un,
  • peut envoyer un ETag faible pour des raisons de performance ou car il est impossible d’obtenir un ETag fort,
  • devrait envoyer une Last-Modified date lorsque c’est possible et sans risque pour la cohérence des caches et
  • ne renvoie un statut 304 que si tout les validateurs présents dans la requête vont en ce sens.

Un client, ou un cache HTTP/1.1 :

  • utilise l’ETag, pour de la validation conditionnelle, lorsque ce dernier est présent,
  • devrait utiliser la Last-Modified date, pour de la validation conditionnelle, lorsque cette dernière est présente et qu’il n’est pas question d’une requête partielle (Range),
  • peut utiliser la Last-Modified date, pour de la validation conditionnelle, lorsque cette dernière est présente, qu’il est question d’une requête partielle et qu’il s’agit d’un serveur HTTP/1.0 et
  • devrait utiliser l’ETag et la Last-Modified date de manière conjointe, pour de la validation conditionnelle, lorsqu’ils sont disponibles.

Conclusion

Dans cet article j’ai voulu reprendre un peu le fonctionnement des caches d’HTTP et les moyens que nous avons d’interagir avec eux. Ce n’est pas exhaustif pour autant puisqu’il reste quelques sujets utiles liés à ce sujet comme l’invalidation des caches par les requêtes PUT, DELETE et POST ou la construction des réponses depuis les caches.

J’espère que cet article vous aura éclairé sur les bonnes pratiques à adopter pour tirer parti des caches.

Quelques liens

L’équipe Synbioz.

Libre d’être ensembles.