Mettre en cache les données de transaction
Objectif : s’assurer qu’un utilisateur donné dispose d’une adresse de transaction personnelle
Récemment, j’ai partagé ma première expérimentation avec Bitcoin : fournir un lien de support sur Faeries Space. Vous pourrez d’ailleurs le retrouver ici
Pour cela, j’ai implémenté un backend exposant une adresse sous la forme d’un QR Code par API. Pour m’assurer qu’un utilisateur dispose d’une adresse qui lui soit propre (requis pour limiter le tracking), je me suis assuré qu’il fournisse une clé unique (générée via son navigateur). Il a ensuite fallu que je trouve une solution pour mémoriser ces clés utilisateur et les adresses associées.
Dans ce but, j’ai choisi de mettre en cache, pour une durée limitée, une paire clé/valeur par utilisateur, la clé étant celle fournit par l’utilisateur, et la valeur étant l’adresse Bitcoin.
Il m’a donc fallu choisir une technologie de stockage adaptée à notre besoin, parmi toutes celles à ma disposition.
Stockage clé / valeur ?
Quand on parle de stockage de donnée, on pense généralement « SQL ». Mais dans notre cas, c’est clairement sortir l’artillerie lourde pour écraser une mouche. Les bases de données « clé / valeur » sont des bases « NoSQL », dans la mesure où elles ne s’efforcent pas de respecter les mêmes principes que les bases SQL classiques (ACID, requêtes au format SQL, etc.).
Les bases de données clé / valeur ne font rien de plus que cela : stocker des clés, avec une valeur en face. Néanmoins, il en existe plein, parce qu’en fin de compte, ce n’est pas si simple.
Contraintes techniques : pas beaucoup de place …
Mon hébergement actuel étant le plus « low cost » possible, je ne dispose que d’une petite VM avec deux CPUs et 4 Go de RAM … Je dois donc opter pour la solution qui permet de stocker un max de données sous le plus faible volume possible.
Redis : la solution la plus courante
Redis est une solution historiquement très utilisée, pour gérer une base clé valeur. C’est aussi une solution de simplicité pour répondre à ce genre de besoin : elle est très répandue, très fonctionnelle et performante.
Par contre, Redis propose de nombreuses fonctionnalités (comme le support JSON natif, l’éviction LRU configurable, des procédures stockées au format Lua, ou des structures comme les listes) dont nous n’avons aucun besoin dans notre cas. Elles apportent donc une surcharge inutile à notre application.
D’autre part, Redis est mono-threadé, ce qui signifie qu’il ne traite qu’une opération à la fois. Il compte exclusivement sur sa performance pour garantir le traitement de toutes les opérations.
Memcached : encore plus simple
J’ai donc pris le temps de chercher une solution encore plus performante pour mon besoin, et je suis tombé sur Memcached. Contrairement à Redis, Memcached n’embarque pas de fonctionnalité étendue : il ne stocke que des structures clé/valeur, et les valeurs sont traitées comme de simples chaînes de caractères. Memcached a également l’avantage d’être multi-threadé.
Par conséquent, Memcached est plus simple, plus performant, et plus léger pour notre cas d’usage.
NB : bon objectivement, les deux jouent dans un mouchoir de poche pour mon cas d’usage, même si Memcached est « un poil » plus adapté.
Memcached sur Debian
L’installation de Memcached sur Debian est d’autant plus simple qu’il existe un package dédié :
- aptitude install memcached # le cœur
- aptitude install netcat-traditional # pour pouvoir interroger le serveur en CLI
Avec Netcat, on pourra interroger, et éventuellement manipuler notre serveur Memcached depuis la ligne
de commande. Memcached écoutant sur le port 11211 par défaut, nous pourrons l’interroger via la commande
nc localhost 11211.
- stats: Affiche les statistiques générales
- get my_key: Récupère la valeur associée à la clé « my_key »
- delete my_key: Supprime l’entrée associée à la clé « my_key » si elle existe (renvoie « DELETED » en cas de succès).
Par exemple, après mes tests, j’ai voulu supprimer le dernier index utilisé (et ainsi reset le compteur) :
get last_idx
VALUE last_idx 0 1
5
END
delete last_idx
DELETED
get last_idx
END
Intégration à notre API
Maintenant que Memcached est installé sur notre serveur, nous pouvons l’intégrer à notre API Python.
Pour commencer, on va récupérer la lib Python permettant d’interroger Memcached, en ajoutant pymemcache==4.0.0
dans notre requirements.txt (ou bien, un petit pip install pymemcache).
Une fois installée, cette bibliothèque s’utilise de façon assez similaire à Redis :
- Import : from pymemcache.client import Client(pour la connexion) etfrom pymemcache.exceptions import MemcacheError(pour la gestion propre des erreurs)
- Pour se connecter : self.cache = Client(('127.0.0.1', 11211))
- Pour créer, ou mettre à jour une clé : cache.set(avec clé, valeur, etexpireen secondes)
- Pour récupérer une valeur : cache.get(avec clé et valeur par défaut, qu’on utilisera notamment pourlast_idx)
À noter qu’ici, notre API et notre Memcached se trouvent sur la même machine, donc on est allé au plus simple, mais le client propose quelques paramètres d’initialisation pour des besoins plus avancés, permettant de configurer par exemple un timeout, ou une connexion SSL.
Pour ce qui est de notre service, l’intégration avec FastAPI consiste donc à :
- définir une classe Dataqui wrap l’accès à notre serveur Memcached, mais aussi l’accès au fichier contenant les adresses btc (pour ce petit projet, on a fait … très … simple) ;
- lui définir une méthode Data.get_accessen@classmethod, de sorte à pouvoir la passer en paramètre des routes de notre app (e.g.data=Depends(Data.get_access)) ;
- enfin, notre classe Dataexpose un attributcachepour faciliter l’accès aux données stockées dans Memcached.
L’explication est assez sommaire, parce que l’intégration est également très sommaire. Vous pouvez d’ailleurs la retrouver dans le code source, ici-même : https://framagit.org/Meier-Link/writings/-/blob/master/src/server/api.py
D’autres solutions ?
Au-delà de ces deux solutions que j’avais sous la main, il existe également d’autres alternatives capables de répondre à nos besoins. Parmi elles, je vais en développer DragonflyDB : c’est un clone multi-threadé de Redis. Quand je dis « clone », ce n’est pas tellement un abus de langage : il est conçu pour recevoir les requêtes sur le format de l’API Redis. Typiquement, en Python 3, on peut installer le package redis, et l’utiliser pour interagir avec DragonflyDB.
Si je reprends ma petite expérience avec Redis et Memcached, je dirais donc que DragonflyDB se retrouve quelque part entre les deux : une partie des avantages de Redis (en moins mature) et compatible avec Redis d’un côté, mais de l’autre côté, il offre une meilleure performance, donc comme Memcached, notamment grâce au multi-threading.
Limitation : peut-on aller encore plus loin, en optimisation ?
En termes de choix de base de données clé/valeur en cache, il existe probablement d’autres technologies à explorer (Grok m’en a sorti un certain nombre, qui ont titillé ma curiosité). En terme de stockage même, ma gestion de l’index en cache est loin d’être optimale, et mériterait un lifting (en gros, je stocke l’index en plus de l’adresse, alors que l’adresse seule suffit). Pour le reste, on est sur une expérimentation assez satisfaisante (du moins sur notre sujet du cache).
Autres évolutions : automatiser les déploiements / mises à jour ?
Pour le moment, on est sur un projet relativement simple. Mais avec l’ajout d’un backend à mon serveur se présente de nouveaux besoins. Notamment, il me faut tester mon API avec son cache, avant toute mise en production. Je serai donc amené, tôt ou tard, à faire évoluer mon Playbook Ansible pour gérer le déploiement et la configuration de Memcached sur un nouvel environnement de test … puis pour pousser les mises à jour en production.
Annexe : pour la culture, une petite liste de solutions
Pour ceux qui souhaiteraient aller plus loin, voici d’autres solutions intéressantes à explorer :
- LevelDB : Un store embarqué développé par Google, optimisé pour les écritures séquentielles et les lectures aléatoires.
- Voldemort : développé en Java par LinkedIn pour des cas d’usage massifs.
- Consul : solution Hashicorp pour stockage de configuration dynamique et distribuée.
- Etcd : stockage clé/valeur distribué, fiable et fortement consistant, conçu pour la gestion de configurations et la coordination dans les systèmes distribués.
