Mise en cache NDB

La bibliothèque NDB gère les caches à votre place. Il existe deux niveaux de mise en cache : le premier est un cache contextuel, tandis que le deuxième constitue une passerelle vers Memcache, le service de mise en cache standard d'App Engine. Les deux caches sont activés par défaut pour tous les types d'entité, mais peuvent être configurés pour répondre à des besoins avancés. En outre, NDB met en œuvre une fonctionnalité de mise en lots automatique, qui tente de regrouper des opérations pour minimiser les allers-retours du serveur.

Présentation

La mise en cache aide la plupart des types d'applications. La bibliothèque NDB met automatiquement en cache les données qu'elle lit ou écrit (à moins qu'une application la configure pour qu'elle n'effectue pas ces actions). Il s'avère plus rapide de lire des données depuis le cache qu'à partir du datastore.

Vous pouvez modifier le comportement de mise en cache de nombreuses fonctions NDB en transmettant des arguments d'options de contexte. Vous avez par exemple la possibilité d'appeler key.get(use_cache=False, use_memcache=False) pour contourner la mise en cache. Vous pouvez également modifier la stratégie de mise en cache par défaut sur un contexte NDB, comme décrit ci-dessous.

Attention : Lorsque vous modifiez le contenu du datastore à l'aide du lecteur Datastore de la console d'administration, les valeurs mises en cache ne sont pas mises à jour. Votre cache peut alors ne pas être cohérent. Pour le cache contextuel, cela ne pose généralement pas de problème. Pour Memcache, nous vous recommandons de vider le cache à partir de la console d'administration.

Objets de contexte

La gestion du cache utilise une classe nommée Context, qui permet à chaque thread et chaque transaction de s'exécuter dans un nouveau contexte. Comme toutes les requêtes HTTP entrantes lancent un nouveau thread, chaque requête s'exécute également avec un nouveau contexte. Pour accéder au contexte actuel, utilisez la fonction ndb.get_context().

Attention : Il serait illogique de partager des objets Context entre plusieurs threads ou requêtes. N'enregistrez pas le contexte en tant que variable globale ! Stockez-le plutôt dans une variable locale ou locale au thread.

Les objets de contexte disposent de méthodes permettant de définir des stratégies de mise en cache et de manipuler le cache.

Cache contextuel

Le cache contextuel ne persiste que pendant la durée d'un seul thread. Cela signifie que chaque requête HTTP entrante se voit attribuer un nouveau cache contextuel et n'est "visible" que par le code qui traite cette requête. Si votre application génère des threads supplémentaires lors du traitement d'une requête, ces threads disposent également d'un nouveau cache contextuel distinct.

Le cache contextuel est rapide, car il réside au niveau de la mémoire. Lorsqu'une fonction NDB écrit des données dans le datastore, elle en écrit également dans le cache contextuel. Quand une fonction NDB lit une entité, elle vérifie d'abord le cache contextuel. Si l'entité est détectée, aucune interaction avec le datastore n'a lieu.

Lorsqu'une fonction NDB interroge le datastore, la liste des résultats est extraite de celui-ci. Toutefois, si un résultat individuel se trouve dans le cache contextuel, il est utilisé à la place de la valeur extraite de la requête Datastore. Les résultats de la requête sont réécrits dans le cache contextuel si la stratégie de mise en cache le spécifie (mais jamais dans Memcache).

L'exécution de requêtes de longue durée dans des tâches en arrière-plan peut amener le cache contextuel à consommer d'importantes quantités de mémoire. En effet, le cache conserve une copie de chaque entité extraite du contexte actuel ou stockée dans celui-ci. Pour éviter les exceptions de mémoire dans les tâches de longue durée, vous pouvez désactiver le cache ou définir une stratégie qui exclut les entités consommant le plus de mémoire.

Memcache

Memcache est le service de mise en cache standard d'App Engine. Il est bien plus rapide que le datastore, mais s'avère plus lent que le cache contextuel (l'un est évalué en millisecondes, l'autre en microsecondes).

Par défaut, un contexte non transactionnel met en cache l'ensemble des entités de Memcache. Tous les contextes d'une application utilisent le même serveur Memcache et ont accès à un ensemble cohérent de valeurs mises en cache.

Memcache n'est pas compatible avec les transactions. Ainsi, une mise à jour destinée à être appliquée à la fois au datastore et à Memcache pourrait n'affecter que l'un des deux. Pour maintenir la cohérence dans ce genre de scénarios (éventuellement au détriment des performances), l'entité mise à jour est supprimée de Memcache, puis écrite dans le datastore. Une opération de lecture ultérieure recherche ensuite l'entité manquante dans Memcache, la récupère à partir du datastore, puis la met à jour dans Memcache. De plus, les lectures NDB au sein de transactions ignorent Memcache.

Lorsque des entités sont écrites dans une transaction, Memcache n'est pas utilisé. Une fois la transaction validée, son contexte tente de supprimer toutes ces entités de Memcache. Notez toutefois que certaines défaillances peuvent empêcher ces suppressions.

Fonctions de stratégie

La mise en cache automatique s'avère utile pour la plupart des applications, mais si la vôtre est inhabituelle, vous souhaiterez peut-être désactiver cette fonctionnalité pour une partie ou l'ensemble des entités. Vous pouvez modifier le comportement des caches en définissant des fonctions de stratégie. Il existe une fonction de stratégie pour le cache en cours de traitement, définie avec le code suivant :

context = ndb.get_context()
context.set_cache_policy(func)

Memcache dispose également d'une fonction de stratégie, définie avec le code suivant :

context = ndb.get_context()
context.set_memcache_policy(func)

Chaque fonction de stratégie accepte une clé et renvoie un résultat booléen. Si la valeur False est renvoyée, l'entité identifiée par cette clé n'est pas enregistrée dans le cache correspondant. Si vous souhaitez par exemple contourner le cache en cours de traitement pour toutes les entités Account, vous pouvez utiliser le code suivant :

context = ndb.get_context()
context.set_cache_policy(lambda key: key.kind() != 'Account')

(Il existe toutefois une méthode plus simple, que nous décrivons plus bas.) Pour plus de commodité, vous pouvez transmettre True ou False au lieu d'une fonction qui renvoie toujours la même valeur. Les stratégies par défaut mettent en cache toutes les entités.

En outre, une fonction de stratégie Datastore régit quelles entités sont écrites dans le datastore :

context = ndb.get_context()
context.set_datastore_policy(func)

Ce code agit comme les fonctions de stratégie du cache contextuel et de Memcache. Si la fonction de stratégie Datastore renvoie False pour une clé donnée, l'entité correspondante n'est pas écrite dans le datastore. (Elle peut être écrite dans le cache en cours de traitement ou dans Memcache si leurs fonctions de stratégie le permettent.) Cela peut s'avérer utile si vous souhaitez mettre en cache des données de type entité, mais que vous n'avez pas besoin de les stocker dans le datastore. À l'instar des stratégies de mise en cache, vous pouvez transmettre True ou False au lieu d'une fonction qui renvoie toujours la même valeur.

Memcache fait automatiquement expirer les éléments en cas de saturation de la mémoire. Vous pouvez définir une fonction de stratégie d'expiration de délai Memcache pour déterminer la durée de vie maximale d'une entité dans le cache :

context = ndb.get_context()
context.set_memcache_timeout_policy(func)

Cette fonction est appelée avec un argument de clé et doit renvoyer un nombre entier spécifiant la durée de vie maximale, exprimée en secondes. Les valeurs 0 ou None indiquent une durée de vie indéfinie (tant que le serveur Memcache dispose de suffisamment de mémoire). Pour plus de commodité, vous pouvez simplement transmettre une constante sous la forme d'un entier au lieu d'une fonction qui renvoie toujours la même valeur. Consultez la documentation de Memcache pour en savoir plus sur les délais d'expiration.

Remarque : Il n'existe pas de stratégie de durée de vie distincte pour le cache contextuel, car la durée de vie du cache est identique à celle de son contexte (une requête HTTP entrante). Toutefois, vous pouvez vider le cache en cours de traitement en appelant la méthode suivante :
context = ndb.get_context()
context.clear_cache()

Un tout nouveau contexte commence avec un cache en cours de traitement vide.

Bien que les fonctions de stratégie soient très flexibles, la plupart d'entre elles sont simples dans la pratique. Exemple :

  • Ne pas mettre en cache des entités appartenant à une classe de modèle spécifique.
  • Définir le délai d'expiration Memcache des entités de cette classe de modèle sur 30 secondes.
  • Les entités de cette classe de modèle n'ont pas besoin d'être écrites dans le datastore.

Pour vous éviter d'avoir continuellement à écrire et mettre à jour des fonctions de stratégie triviales (ou pire, de remplacer les stratégies pour chaque opération à l'aide d'options de contexte), les fonctions par défaut obtiennent la classe de modèle à partir de la clé qui leur a été transmise, puis recherchent des variables de classe spécifiques dans la classe de modèle :

Variable de classeTypeDescription
_use_cache bool Spécifie si les entités doivent être stockées dans le cache en cours de traitement. Remplace la stratégie du cache en cours de traitement par défaut.
_use_memcache bool Spécifie si les entités doivent être stockées dans Memcache. Remplace la stratégie Memcache par défaut.
_use_datastore bool Spécifie si les entités doivent être stockées dans le datastore. Remplace la stratégie Datastore par défaut.
_memcache_timeout int Durée de vie maximale des entités dans Memcache. Remplace la stratégie d'expiration de délai Memcache par défaut.

Remarque : Il s'agit d'une fonctionnalité de la fonction de stratégie par défaut pour chaque stratégie. Si vous spécifiez votre propre fonction de stratégie, mais souhaitez également revenir à la stratégie par défaut, appelez les fonctions de stratégie par défaut explicitement en tant que méthodes statiques de la classe Context :

  • default_cache_policy(key)
  • default_memcache_policy(key)
  • default_datastore_policy(key)
  • default_memcache_timeout_policy(key)