Cache HTTP des Pages dynamiques ZPT (presque finalisé)
Par macadames le 21/04/2005 18:41
Sur certains sites fortement consultés, il est quelquefois nécessaire de cacher des pages dynamiques produites par un portail. Ce post aborde la manière de mettre dans le cache du client le résultat d'un Page Template, en prenant comme exemple le main_template de Plone.
Catégories : CPS,CMF,Plone,Zope
Avant propos
Avant de s'attaquer aux pages dynamiques, assurez-vous de cacher correctement les contenus statiques produits par Zope, c'est mille fois plus important. Si certains ne connaissent pas trop le sujet, j'ai essayé de faire un point complet sur les caches http à cette adresse : http://macadames.com/Plone2/Administration__Zope/entetes_http/view .
Le problème
Pour un site avec un grand nombre de connexions silmutanées, pour booster les performances, il faudrait que le nombre de pages html produites dynamiquement par Zope soit réduit au strict minimum. Par exemple il faudrait qu'un utilisateur ayant cliqué deux fois sur la même page à 5 minutes d'intervalle prenne la page dans son cache et n'engendre pas une nouvelle requête au serveur Zope, surtout lorsque la page n'a pas changé. C'est le comportement d'un site Php standard. Pourquoi pas avec Zope ?
Quelles solutions on utilise habituellement ?
La plupart du temps les sites confrontés à un problème de performance avec Zope sortent de suite la grosse artillerie, SQUID en frontal, ZEO, des RAM Cache partout ... alors que ce n'est pas obligatoirement nécessaire. Et si malgré tout, le résultat n'est pas satisfaissant on adopte parfois des solutions draconiennes comme l'expiration de contenu sur les pages dynamiques (on force le client à cacher sans demande de revalidation), ce qui nuit très fortement au dynamisme du site et qui introduit des incohérences dans la navigation.
On essaie un truc vieux comme le monde (exemple avec Plone)
Sur plone.org, sur zopeur.org (mea culpa) et ailleurs, on trouve plein de tentatives d'optimisation du global_cache_settings de Plone, mais ça ne marche pas, sauf quand on met un entête "expires" solution extrême.
Premier problème : Zope ne teste pas les requêtes de type "If-modified-since" ou "If-none-match" et ne renverra pas 304 Not modified pour les PageTemplate, même si on a mis un cache-control avec la bonne date de modification et un must-revalidate, même si on mis un Etag. Donc il faudra faire le travail à la place de Zope, (ou bien patcher ZopePageTemplate ou Zserver ou autre.).
Autre problème : sur un site dynamique, la date de modification d'une page ne dépend pas seulement de la date de modification du contenu, car il y a les nombreux portlets tout autour par exemple. Sur une page d'accueil de portail tout est issu des multiples contenus du site. Donc il faut connaître la date de modification de tous les contenus pour last-modified.
Avec Plone, le template chargé des entêtes pour les rendus HTML est global_cache_settings appelé par main_template. On supprime l'appel à global_cache_settings, et on met la gestion des entêtes au tout début du main_template (merci Tarek), c'est seulement lui qu'on modifie. On teste les entêtes if-modified-since et if-none-match, si ça matche on renvoie "304 Not modified" sans rien d'autre dans le corps (sinon ça plante le browser).
Pour le calcul de last-modified, on va utiliser la logique d'affichage de Plone : on va dire que la date de modification de n'importe quelle page produite par le site (via main_template donc) est la date de modification du dernier contenu publié sur le site et non expiré. Ca ne peut pas prendre en compte les flux RSS, mais on ne cache que pendant une heure, c'est acceptable. On peut également prendre une logique plus Zopienne, en choisissant pour entête last-modified la date de modification du dernier contenu du site pour lequel les anonymes ont le droit "View". A vous d'adapter.
J'ai vraiment simplifié pour l'entête last-modified, remplacé le '+0000' non standard généré par rfc822() par 'GMT', normalement il y a une méthode python pour ça mais elle n'est pas autorisée dans Zope ...
Sur un site tout neuf sans un seul document publié le calcul de last-modified va planter, donc il faut affiner ou choisir une autre logique ...
Le corps du main_template utilisé est celui de Plone 2.0, là aussi il faudra obligatoirement adapter (pour Plone2.0x, CPS, etc ...).
Pour la comparaison last-modified<==> if-modified-since j'ai là aussi grandement simplifié avec '==' mais ça marche très bien
Le code du main_template de Plone2.0 modifié
Avec un frontal
Ce template fonctionne bien sans frontal. Si on utilise un frontal il y a souvent des petites adaptations à faire. Quelques exemples :
Avec les rewrite rules d'Apache :Suivant le paramétrage des connexions persistantes d'Apache, il faut ajouter dans ce main_template un setHeader('Connection','close') avant le setStatus(304), sans quoi le proxy peut envoyer une erreur 502 lorsqu'on redemande la page plusieurs fois de suite.
Avec frontal IIS : avec IIS et le fameux script error.asp, le status "304 Not modified" ne sera pas envoyé sans modification du script error.asp, ce n'est pas compliqué à faire.
Gestion des Etags
On peut se passer du test sur les Etags, mais je crois qu'il faut alors supprimer la variation de source (vary) et forcer la langue, le skin et l'encodage (à vérifier).
Bénéfices de l'opération
Les pages ne sont pas recalculées par Zope si elles sont redemandées avec moins d'une heure d'intervalle par le même utilisateur tant qu'il n'y a pas eu de modification pour les anonymes sur le site. L'utilisateur ne perd aucune modification, ne perd aucune préférence de langue de skin ou d'encodage, puisqu'il demande à chaque fois une validation de son cache. Plus les pages sont chargées en portlets ou templates complexes, plus le bénéfice est grand en terme de performance.
modifs à faire
Il y a aussi d'autres améliorations à apporter :
- tester si le Response "Connection Close" passe partout, pour un truc plus générique
- Générer un entête valide pour tous pays avec la bonne méthode rfc1123_date, c'est la méthode qu'utilise Accelerated HTTP Cache pour les entêtes last-modified qui eux sont standards, mais elle n'est pas autorisée dans Zope (je ne sais pas si cette histoire de standard de date est importante, dans le doute mieux vaut assurer ...)
- ne pas calculer l'Etag ni last-modified dans certaines conditions : si l'utilisateur n'est pas anonyme, ou bien si la page est le résultat d'une autre méthode que GET
- calculer last-published sur les autorisations "view" plutôt que sur l'état du doc (plus générique mais plus côuteux non ?)
- comparer vraiment les dates et interdire le cache au-delà du délai (ce n'est pas fait ici - on cache tant que lastPublishedDate n'a pas changé), surtout si le site contient des flux RSS ou des requêtes SQL externes.
Donc pas mal de petits détails à régler.
Proposer à la ML de Plone ? pourquoi pas, déjà faut améliorer le truc, ...
A+
(Merci Gillou)
Cache HTTP pages dynamiques (suite)
Voilà il y a pas mal de changements, pour l'optimisation d'abord (éviter l'appel au catalogue quand on en a pas besoin), mais surtout avec un traitement spécifique pour Internet Explorer dont le cache réagit de manière absolument absurde, presque à l'envers des standards :
- il ne connait pas must-revalidate,
- et chose complètement hallucinante il envoie if-modified-since quand on demande un refresh, c'est à dire justement quand il ne faut pas. Donc il a fallu une petite astuce pour arranger ça...
Bref vous trouverez le code du main_template (toujours à partir de Plone 2.0.x et donc faut modifier le corps central selon la version de Plone) et les scripts associés qui résolvent tous ces problèmes.
Code du main_template :
http://afpy.org/Members/macadames/main_template_with_htttpcache/document_view
Scripts Python associés :
http://afpy.org/Members/macadames/httpcache_scripts_python/document_view
Le résultat avec PTProfiler pour une même page très lourde demandée avec n'importe quel browser en anonyme :
Première demande de la page : 0,71 s
Deuxième demande à moins d'une heure d'intervalle si rien n'a bougé sur le site : 0,12 s
La seule chose qu'il faut vérifier ce sont les évolutions des browsers du marché, car peut-être que Microsoft va faire évoluer son logiciel plein de bugs, bah au pire les nouveaux MSIE prendraient 0.12s de plus par page.
Un autre truc important, de même qu'existe LiveHttpHeaders pour Mozilla et FireFox, il y a ieHttpHeaders pour MSIE qui est gratuit, avec cet outil vous allez halluciner quant au fonctionnement du cache d'Internet Explorer.








Je recommande en plus de placer toute cette logique d'optimisation du cache dans un python script pour les quelques raisons suivantes :
* le python direct est mieux adapté, maintenable et personnalisable que les expressions TALES pour faire de la pure logique.
* Ce sera un chouia plus rapide.
* Ca nécessite beaucoup moins de modifs de la main_template, donc beaucoup moins de travail d'adaptation quand Plone 2.1 sera dans vos serveurs)
Pourquoi ne pas proposer tout ça à la ML Plone pour le placer dans Plone 2.1 en standard.
Réponses à ce commentaire