J’aime assez le système de cache de WordPress mis en place par le plugin WP-Super-Cache qui consiste à générer la page et à la mettre physiquement dans un dossier de cache, puis à dire à Apache de toujours regarder si la page demandée par un visiteur n’existe pas déjà avant d’en demander la génération. Sur AppEngine, il y a un problème de taille : il est impossible de gérer la création/suppression de fichiers en dur. Point de cache « physique », donc. Pour autant, on peut avoir envie de servir un grand nombre de pages, et donc d’avoir un système performant et pour autant, peu gourmand en performances. J’ai donc décidé de créer un petit script utilisant MemCached pour stocker le contenu généré, et j’ai regardé les différences de temps rapporté par les logs.
Explication du fonctionnement :
Très rapidement, voilà comment fonctionne le script que j’ai écris : a chaque URL demandée par le client (quand celle-ci est après le path /viamemcached ou /viadatastore), le script va chercher le contenu HTML de la page directement mémoire. Dans MemCached si l’URL demandée commence par /viamemcached ou dans la mémoire (pour AppEngine, il s’agit du dataStore) quand l’URL commence par /viadatastore).
Le principe est donc, quand l’URL demandée commence par /viadatastore, de simuler une recréation de page, à cause de la requête à la base de donnée. Et de tenter de reproduire le fonctionnement d’un cache en passant par MemCached.
Code :
Fichier main.py :
Le fichier ci-dessous répartit les requètes en 3 : la page d’admin (création de contenu), derrière /upload, la catégorie des pages nécessitant la génération de contenu via appel au datastore (via l’url /viadatatore), et enfin la catégorie des pages nécessitant juste la requête à Memcached.
import cgi import firstPage import userpage import wsgiref from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app ROUTES = [ ('/',firstPage.Home), ('/upload',userpage.Upload), ('/viadatastore/?([\w]*)/?', userpage.ContentUsage), ('/viamemcached/?([\w]*)/?', userpage.ContentCached), # other handlers here... ] def main(): application = webapp.WSGIApplication(ROUTES) wsgiref.handlers.CGIHandler().run(application) if __name__ == "__main__": main()
Fichier firstpage.py :
Le fichier firstpage.py est « juste » le fichier de Home, répartissant entre les trois sections (au cas où vous voudriez reproduire l’application chez vous).
from google.appengine.ext import webapp class Home(webapp.RequestHandler): def get(self): self.response.out.write("""<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Test</title> </head> <body> <center> <h1>TEST</h1> </center> <br><br> <center><div style="text-align:left;margin:0% 30% 0% 30%;"> <ul> <li> <a href="/upload">Upload</a> </li> <li> <a href="/viadatastore">viadatastore</a> </li> <li> <a href="/viamemcached">viamemcached</a> </li> </ul> </div> </center> </body> </html>""")
Fichier userpage.py :
Et enfin, le fichier userpage.py comprend les trois sections. Dans la classe Upload, on voit le formulaire d’envoi et le script d’enregistrement du contenu HTML dans le datastore. Dans la classe ContentUsage, on a la génération du contenu via DataStore, et dans ContentCached, celle via Memcached.
import cgi import logging from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app from google.appengine.ext import db from google.appengine.api import memcache class pageUrlContent(db.Model): url = db.StringProperty(multiline=False) content = db.TextProperty() date = db.DateTimeProperty(auto_now_add=True) def add_content_in_base(url,content): pageUrlContent = pageUrlContent() pageUrlContent.url = cgi.escape(url) pageUrlContent.content = db.Text(content) pageUrlContent.put() def render_list_previous_queries(url): listurls = pageUrlContent.all().filter("url =", url) return listurls def fetch_content_in_base(url): final_content = "" contents = render_list_previous_queries(url) if contents: for cont in contents: final_content = cont.content return final_content def fetch_content_in_cache(url): thiscontent = memcache.get(url) if thiscontent: return thiscontent else: content = fetch_content_in_base(url) if not memcache.set(url, content): logging.error("Memcache *set* failed in fetch_content_in_cache(url)>else>ifnot.") return content class Upload(webapp.RequestHandler): def get(self): self.response.out.write(""" <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Content Upload</title> </head> <body> <center> <form action="/upload" enctype="multipart/form-data" method="post"> <div><input type="text" name="url"></div> <div><textarea COLS=30 ROWS=30 name="content"></textarea></div> <div><input type="submit" value="Send IT"></div> </form> </center> </body> </html> """) def post(self): url = self.request.get("url") content = self.request.get("content") add_content_in_base(url,content) self.response.out.write("""<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Content Upload</title> </head> <body> <center> OK </center> </body> </html> """) class ContentUsage(webapp.RequestHandler): # part of ImageHandler class def get(self,id): content = fetch_content_in_base(id) self.response.out.write(content) class ContentCached(webapp.RequestHandler): # part of ImageHandler class def get(self,id): content = fetch_content_in_cache(id) self.response.out.write(content)
Performances :
Les logs de AppEngine sont de ce type :
08-09 06:46AM 39.408 /viamemcached/loremipsum 200 22ms 19cpu_ms 7kb Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.22 Safari/534.3,gzip(gfe)
149.6.164.150 – – [09/Aug/2010:06:46:39 -0700] « GET /viamemcached/loremipsum HTTP/1.1 » 200 7725 – « Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.22 Safari/534.3,gzip(gfe) » « abricocotierfrtests.appspot.com » ms=23 cpu_ms=19 api_cpu_ms=0 cpm_usd=0.001455
D’après ce que j’ai compris, on peut extraire 4 valeurs : ms=23 cpu_ms=19 api_cpu_ms=0 cpm_usd=0.001455. Je n’ai pas bien compris à quoi correspondaient tous ces temps, mais ce dont je suis sûr, c’est que le api_cpu_ms correspond au temps de l’API du DataStore.
En effet, j’ai fait une vingtaine de tests (enfin 20 reload par catégorie), et je mes ai mis dans un tableau, puis j’en ai fait des graphiques :
Le tableau de données :
Millisecondes :
CPU Millisecondes :
API CPU Millisecondes :
CPU USD :
Conclusion
De ce que l’on peut constater, effectivement l’API_CPU_MS est à zéro après le premier appel sur Memcached, ce qui correspond bien à la premier mise en cache (lors du premier appel), puis c’est le cache qui prend le relais, et le datastore n’est plus appelé (et on remarque que les appels au datastore font tous 22ms). Pour le reste, c’est plus compliqué. Il semble que, sur le global, MemCached soit plus rapide (légèrement), mais ce n’est pas une règle absolue, et on voit que selon les appels, les temps varient, si bien que certains appels via DataStore se révèlent plus rapides que certains via MemCached.
D’une manière générale, Google imposant de respecter les quotas (ce que je trouve louable), cela impose à coder relativement « économiquement », c’est à dire avec l’idée qu’il faut utiliser Memcached aussi souvent que possible. Je rajoute que pour mon test, l’appel au DataStore était relativement simple, pas de SELECT *, ce qui peut expliquer la proximité des résultats avec MemCached. Cependant, je pense que dans un cas plus proche de la réalité, où la génération d’une page nécessite plusieurs appels au DataStore, la différence devient plus évidente.
Je n’ai pas non plus testé via l’utilisation de templates Django, mais je doute que celà accélère grand chose au traitement de la page, et donc encore une fois, cela devrait augmenter la différence entre une page donc l’intégralité du code se trouve en MemCached, et une autre pour laquelle il faut réaliser une certaine quantité d’opération de génération.
Je te conseille d’aller voir de ce coté là: http://code.google.com/p/cirruxcache/