Système de cache sur AppEngine : quelles performances ?

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.

Une réflexion sur « Système de cache sur AppEngine : quelles performances ? »

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.