Lors de l'écriture d'applications Python, la mise en cache est importante. L'utilisation d'un cache pour éviter de recalculer les données ou d'accéder à une base de données lente peut vous permettre d'améliorer considérablement vos performances.
Python offre des possibilités intégrées de mise en cache, d'un simple dictionnaire à une structure de données plus complète telle que functools.lru_cache
. Ce dernier peut mettre en cache n'importe quel élément en utilisant un algorithme le moins récemment utilisé pour limiter la taille du cache.
Ces structures de données sont cependant par définition locales à votre processus Python. Lorsque plusieurs copies de votre application s'exécutent sur une grande plate-forme, l'utilisation d'une structure de données en mémoire interdit le partage du contenu mis en cache. Cela peut être un problème pour les applications à grande échelle et distribuées.
Par conséquent, lorsqu'un système est distribué sur un réseau, il a également besoin d'un cache qui est distribué sur un réseau. De nos jours, de nombreux serveurs réseau offrent une capacité de mise en cache. Nous avons déjà expliqué comment utiliser Redis pour la mise en cache avec Django.
Comme vous allez le voir dans ce tutoriel, memcached est une autre excellente option pour la mise en cache distribuée. Après une introduction rapide à l'utilisation de base de memcached, vous découvrirez des modèles avancés tels que "cache and set" et l'utilisation de caches de secours pour éviter les problèmes de performances du cache à froid.
Installer Memcached
Memcaché est disponible pour de nombreuses plates-formes :
- Si vous utilisez Linux , vous pouvez l'installer en utilisant
apt-get install memcached
ouyum install memcached
. Cela installera memcached à partir d'un package pré-construit, mais vous pouvez également créer memcached à partir de la source, comme expliqué ici. - Pour macOS , utiliser Homebrew est l'option la plus simple. Exécutez simplement
brew install memcached
après avoir installé le gestionnaire de paquets Homebrew. - Sous Windows , vous devrez compiler vous-même memcached ou trouver des binaires pré-compilés.
Une fois installé, memcaché peut simplement être lancé en appelant le memcached
commande :
$ memcached
Avant de pouvoir interagir avec memcached depuis Python-land, vous devez installer un client memcached bibliothèque. Vous verrez comment procéder dans la section suivante, ainsi que certaines opérations d'accès au cache de base.
Stocker et récupérer des valeurs mises en cache à l'aide de Python
Si vous n'avez jamais utilisé memcached , c'est assez facile à comprendre. Il fournit essentiellement un dictionnaire géant disponible sur le réseau. Ce dictionnaire a quelques propriétés qui sont différentes d'un dictionnaire Python classique, principalement :
- Les clés et les valeurs doivent être des octets
- Les clés et les valeurs sont automatiquement supprimées après un délai d'expiration
Par conséquent, les deux opérations de base pour interagir avec memcached sont set
et get
. Comme vous l'avez peut-être deviné, ils sont utilisés pour attribuer une valeur à une clé ou pour obtenir une valeur d'une clé, respectivement.
Ma bibliothèque Python préférée pour interagir avec memcached est pymemcache
—Je recommande de l'utiliser. Vous pouvez simplement l'installer en utilisant pip :
$ pip install pymemcache
Le code suivant montre comment vous pouvez vous connecter à memcached et utilisez-le comme cache distribué sur le réseau dans vos applications Python :
>>> from pymemcache.client import base
# Don't forget to run `memcached' before running this next line:
>>> client = base.Client(('localhost', 11211))
# Once the client is instantiated, you can access the cache:
>>> client.set('some_key', 'some value')
# Retrieve previously set data again:
>>> client.get('some_key')
'some value'
mémcaché Le protocole réseau est vraiment simple et sa mise en œuvre extrêmement rapide, ce qui le rend utile pour stocker des données qui seraient autrement lentes à récupérer à partir de la source canonique de données ou à recalculer :
Bien qu'assez simple, cet exemple permet de stocker des tuples clé/valeur sur le réseau et d'y accéder via plusieurs copies distribuées et en cours d'exécution de votre application. C'est simpliste, mais puissant. Et c'est un excellent premier pas vers l'optimisation de votre application.
Expiration automatique des données en cache
Lors du stockage de données dans memcached , vous pouvez définir un délai d'expiration—un nombre maximum de secondes pour memcached pour garder la clé et la valeur autour. Après ce délai, memcaché supprime automatiquement la clé de son cache.
À quoi devez-vous définir cette durée de cache ? Il n'y a pas de chiffre magique pour ce délai, et cela dépendra entièrement du type de données et d'application avec lesquels vous travaillez. Cela peut prendre quelques secondes ou quelques heures.
Invalidation du cache , qui définit quand supprimer le cache car il n'est pas synchronisé avec les données actuelles, est également quelque chose que votre application devra gérer. Surtout si vous présentez des données trop anciennes ou périmées est à éviter.
Là encore, il n'y a pas de recette magique; cela dépend du type d'application que vous construisez. Cependant, il existe plusieurs cas particuliers qui doivent être traités, que nous n'avons pas encore couverts dans l'exemple ci-dessus.
Un serveur de mise en cache ne peut pas croître à l'infini :la mémoire est une ressource finie. Par conséquent, les clés seront vidées par le serveur de mise en cache dès qu'il aura besoin de plus d'espace pour stocker d'autres choses.
Certaines clés peuvent également avoir expiré parce qu'elles ont atteint leur heure d'expiration (parfois appelée « durée de vie » ou TTL). Dans ces cas, les données sont perdues et la source de données canonique doit être interrogée à nouveau.
Cela semble plus compliqué qu'il ne l'est réellement. Vous pouvez généralement utiliser le modèle suivant lorsque vous travaillez avec memcached en Python :
from pymemcache.client import base
def do_some_query():
# Replace with actual querying code to a database,
# a remote REST API, etc.
return 42
# Don't forget to run `memcached' before running this code
client = base.Client(('localhost', 11211))
result = client.get('some_key')
if result is None:
# The cache is empty, need to get the value
# from the canonical source:
result = do_some_query()
# Cache the result for next time:
client.set('some_key', result)
# Whether we needed to update the cache or not,
# at this point you can work with the data
# stored in the `result` variable:
print(result)
Remarque : La manipulation des clés manquantes est obligatoire en raison des opérations de rinçage normales. Il est également obligatoire de gérer le scénario de cache à froid, c'est-à-dire lorsque memcached vient d'être commencé. Dans ce cas, le cache sera entièrement vide et le cache devra être entièrement re-rempli, une demande à la fois.
Cela signifie que vous devez considérer toutes les données mises en cache comme éphémères. Et vous ne devriez jamais vous attendre à ce que le cache contienne une valeur que vous y avez précédemment écrite.
Préchauffer un cache froid
Certains des scénarios de cache à froid ne peuvent pas être évités, par exemple un memcached crash. Mais certains peuvent, par exemple migrer vers un nouveau memcached serveur.
Lorsqu'il est possible de prédire qu'un scénario de cache froid se produira, il vaut mieux l'éviter. Un cache qui doit être rempli signifie que tout à coup, le stockage canonique des données mises en cache sera massivement touché par tous les utilisateurs du cache qui n'ont pas de données de cache (également connu sous le nom de problème du troupeau tonitruant.)
pymemcache fournit une classe nommée FallbackClient
qui aide à mettre en œuvre ce scénario, comme illustré ici :
from pymemcache.client import base
from pymemcache import fallback
def do_some_query():
# Replace with actual querying code to a database,
# a remote REST API, etc.
return 42
# Set `ignore_exc=True` so it is possible to shut down
# the old cache before removing its usage from
# the program, if ever necessary.
old_cache = base.Client(('localhost', 11211), ignore_exc=True)
new_cache = base.Client(('localhost', 11212))
client = fallback.FallbackClient((new_cache, old_cache))
result = client.get('some_key')
if result is None:
# The cache is empty, need to get the value
# from the canonical source:
result = do_some_query()
# Cache the result for next time:
client.set('some_key', result)
print(result)
Le FallbackClient
interroge l'ancien cache passé à son constructeur en respectant l'ordre. Dans ce cas, le nouveau serveur de cache sera toujours interrogé en premier, et en cas d'échec du cache, l'ancien sera interrogé, évitant ainsi un éventuel retour à la source principale de données.
Si une clé est définie, elle ne sera définie que sur le nouveau cache. Après un certain temps, l'ancien cache peut être désactivé et le FallbackClient
peut être remplacé dirigé par le new_cache
client.
Vérifier et régler
Lors de la communication avec un cache distant, le problème habituel de concurrence revient :plusieurs clients peuvent essayer d'accéder à la même clé en même temps. mémcaché fournit un vérifier et définir opération, abrégé en CAS , ce qui aide à résoudre ce problème.
L'exemple le plus simple est une application qui veut compter le nombre d'utilisateurs dont elle dispose. A chaque fois qu'un visiteur se connecte, un compteur est incrémenté de 1. Utiliser memcached , une implémentation simple serait :
def on_visit(client):
result = client.get('visitors')
if result is None:
result = 1
else:
result += 1
client.set('visitors', result)
Cependant, que se passe-t-il si deux instances de l'application tentent de mettre à jour ce compteur en même temps ?
Le premier appel client.get('visitors')
renverra le même nombre de visiteurs pour les deux, disons que c'est 42. Ensuite, les deux ajouteront 1, calculeront 43 et fixeront le nombre de visiteurs à 43. Ce nombre est faux et le résultat devrait être 44, c'est-à-dire 42 + 1 + 1.
Pour résoudre ce problème de concurrence, l'opération CAS de memcached est pratique. L'extrait de code suivant implémente une solution correcte :
def on_visit(client):
while True:
result, cas = client.gets('visitors')
if result is None:
result = 1
else:
result += 1
if client.cas('visitors', result, cas):
break
Le gets
La méthode renvoie la valeur, tout comme get
méthode, mais elle renvoie également une valeur CAS .
Le contenu de cette valeur n'est pas pertinent, mais il est utilisé pour la méthode suivante cas
appel. Cette méthode est équivalente au set
opération, sauf qu'elle échoue si la valeur a changé depuis le gets
opération. En cas de succès, la boucle est rompue. Sinon, l'opération est relancée depuis le début.
Dans le scénario où deux instances de l'application tentent de mettre à jour le compteur en même temps, une seule réussit à déplacer le compteur de 42 à 43. La seconde instance obtient un False
valeur retournée par le client.cas
appel et doivent réessayer la boucle. Il récupérera 43 comme valeur cette fois, l'incrémentera à 44, et son cas
l'appel réussira, résolvant ainsi notre problème.
L'incrémentation d'un compteur est intéressante comme exemple pour expliquer le fonctionnement de CAS car c'est simpliste. Cependant, memcaché fournit également le incr
et decr
méthodes pour incrémenter ou décrémenter un entier dans une seule requête, plutôt que de faire plusieurs gets
/cas
appels. Dans les applications du monde réel, gets
et cas
sont utilisés pour des types de données ou des opérations plus complexes
La plupart des serveurs de mise en cache distants et des magasins de données fournissent un tel mécanisme pour éviter les problèmes de concurrence. Il est essentiel d'être conscient de ces cas pour utiliser correctement leurs fonctionnalités.
Au-delà de la mise en cache
Les techniques simples illustrées dans cet article vous ont montré à quel point il est facile d'exploiter memcached pour accélérer les performances de votre application Python.
En utilisant simplement les deux opérations de base "set" et "get", vous pouvez souvent accélérer la récupération des données ou éviter de recalculer les résultats encore et encore. Avec memcached, vous pouvez partager le cache sur un grand nombre de nœuds distribués.
D'autres modèles plus avancés que vous avez vus dans ce tutoriel, comme le Check And Set (CAS) vous permet de mettre à jour les données stockées dans le cache simultanément sur plusieurs threads ou processus Python tout en évitant la corruption des données.
Si vous souhaitez en savoir plus sur les techniques avancées pour écrire des applications Python plus rapides et plus évolutives, consultez Scaling Python. Il couvre de nombreux sujets avancés tels que la distribution réseau, les systèmes de mise en file d'attente, le hachage distribué et le profilage de code.