hyperloglog – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Redis : pourquoi et comment ? http://sametmax.com/redis-pourquoi-et-comment/ http://sametmax.com/redis-pourquoi-et-comment/#comments Tue, 14 Oct 2014 11:31:17 +0000 http://sametmax.com/?p=12279 Redis fait partie de ces technologies tellement utiles et simples à mettre en oeuvre qu’il est facile d’oublier toutes les personnes qui ne savent toujours pas ce que c’est. D’autant plus qu’on l’associe avec beaucoup d’étiquettes : cache, queues, pub/sub, base de données, nosql… Et qu’est-ce que Redis comparé à Memcache, MongoDB, MySQL, RabbitMQ, des technos qui n’ont rien à voir et auxquelles on le compare ?

Bref, c’est pas clair tout ça.

Hello redis

D’abord, installer le bouzin.

Si vous êtes sous Linux, Redis est dans votre gestionnaire de paquets. Par exemple, sous Ubuntu :

sudo apt-get install redis-server

Pour Mac, via macport :

sudo port install redis
sudo port load redis

Je crois que brew install redis marche aussi pour les amateurs de homebrew.

Pour Windows, il y a un exe à télécharger ici.

Mais le plus fun avec redis, c’est que même quand il y a pas de binaire, c’est le truc le plus facile du monde à compiler. Et Dieu sait que je hais la compilation, donc quand je vous dis que c’est simple, c’est que c’est mega, ultra, simple.

Ensuite lui faire dire bonjour.

Utiliser Redis se fait à base de commandes, dont la liste est sur le site officiel. On peut envoyer ces commandes depuis n’importe quel langage, mais Redis fournit un shell qui permet de rentrer ces commandes directement:

$ redis-cli # lancer le shell redis
127.0.0.1:6379>  ECHO "Hello"
"Hello"

Redis comme base de données clés/valeurs expirables

L’usage de base de Redis, c’est de stocker des valeurs associées à des clés. Le serveur Redis est comme un gigantesque Hash Map, dictionnaire Python, object Javascript, Array associatif en PHP… En premier lieu, donc, on utilise Redis pour stocker des choses ainsi :

"cle1" => valeur1
"cle2" => valeur2
etc

La clé doit être une chaîne de caractères, de préférence ASCII. La valeur peut être n’importe quoi, vraiment, mais généralement c’est un gros bloc de texte, un entier ou un blob binaire.

Ca s’utilise avec les commandes SET et GET, qui peuvent, comme toutes les commandes, être tapées dans le shell de Redis :

127.0.0.1:6379> GET une_cle
(nil)
127.0.0.1:6379> SET une_cle "Une valeur"
OK
127.0.0.1:6379> GET une_cle
"Une valeur"
127.0.0.1:6379> SET une_cle 780708
OK
127.0.0.1:6379> GET une_cle
"780708"

Ce genre d’usage est surtout sollicité pour stocker des paramètres de configurations ou des compteurs.

En effet, la plupart des opérations sur les clés sont atomiques, rendant ce genre d’usage idéal. On peut même incrémenter ou décrémenter une valeur avec INCR et DECR atomiquement :

127.0.0.1:6379> INCR une_cle
(integer) 780709
127.0.0.1:6379> INCR une_cle
(integer) 780710
127.0.0.1:6379> DECR une_cle
(integer) 780709
127.0.0.1:6379>

Vous allez me dire, mais quel intérêt ? Je peux déjà faire ça avec une variable. Certes, mais Redis est accessible depuis n’importe quel programme de votre serveur. Vous pouvez donc facilement partager des valeurs entre vos processus. Par exemple, avec Django, on a souvent plusieurs workers WSGI, et Redis permet donc de partager des informations entre ces workers. Pour cette raison, on peut utiliser Redis pour créer un lock partagé.

Ainsi, un paramètre dynamique peut être changé et lu depuis tous les process de votre projet en utilisant Redis, et la valeur sera garantie d’être à jour. On peut bien entendu faire ça avec une base de données ordinaire, mais Redis a un plus : la performance.

En effet, Redis est par défaut configuré pour garder toutes les données de sa base en mémoire vive. Pour cette raison, il est très, très rapide, supportant 100000 lectures/écritures par seconde. Si vous comptez le nombre de visiteurs en ligne pour l’afficher sur chaque page, votre base de données vous dira merci de plutôt demander à Redis.

Plus encore, les clés peuvent expirer :

127.0.0.1:6379> SET une_autre_cle "Tu ne le sais pas mais tu es deja mort"
OK
127.0.0.1:6379> EXPIRE une_autre_cle 5 # cette clé expire dans 5 secondes
(integer) 1
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
(nil)
127.0.0.1:6379> GET une_autre_cle
(nil)

La limite en taille de ce qu’on peut stocker par couple est assez large.

On peut également utiliser EXPIREAT et un timestamp unique si on a une date en tête.

Cette caractéristique le rend similaire à Memcache, qui est basé sur des clés/valeurs en mémoire qui peuvent expirer. Et comme Memcache, cela fait de Redis un outil idéal pour gérer du cache : faites une opération longue, stockez-là avec une clé, mettez lui une date d’expiration et pouf, vous avez une valeur cachée globale à votre app.

Redis a néanmoins 3 différences majeures qui le sépare de Memcache :

  • Redis sauvegarde régulièrement toutes les données sur le disque. Si vous rebootez le serveur, vous perdez au pire, quelques secondes de données. Avec Memcache, vous perdez tout.
  • Memcache possède des fonctionalités de clusterisation que n’a pas encore Redis, bien que ce soit actuellement en beta.
  • Redis peut faire bien plus que du stockage clé/valeur.

Redis comme base NoSQL zarbie

Redis n’a pas besoin d’un schéma particulier à définir pour pouvoir sauvegarder ses données, mais n’est pas pour autant limité à une forme d’organisation basique. En fait, des types très proches de ceux qu’on trouve dans le langage Python sont disponibles pour stocker internalement les données, et ils sont décrits dans son excellente doc.

Liste

Une liste est juste une collection ordonnée d’éléments. On utilise toujours la logique de clé, mais au lieu d’une valeur, on a une séquence d’éléments.

Cela permet d’avoir :

cle => [
	valeur1,
	valeur2,
	...
]

Dans le shell :

127.0.0.1:6379> lpush batman na
(integer) 1
127.0.0.1:6379> lpush batman na
(integer) 2
127.0.0.1:6379> lpush batman na
(integer) 3
127.0.0.1:6379> lpush batman na
(integer) 4
127.0.0.1:6379> lpush batman na
(integer) 5
127.0.0.1:6379> lpush batman na
(integer) 6
127.0.0.1:6379> llen batman
(integer) 6
127.0.0.1:6379> LINDEX batman 3
"na"
127.0.0.1:6379> LINDEX batman 10
(nil)
127.0.0.1:6379> LRANGE batman 0 -1 # du début au dernier élément
1) "na"
2) "na"
3) "na"
4) "na"
5) "na"
6) "na"
127.0.0.1:6379> LRANGE batman 2 3
1) "na"
2) "na"
127.0.0.1:6379>

On peut faire toutes les opérations qu’on a l’habitude de faire sur des listes : récupérer un élément, en ajouter un, en retirer un, faire du LIFO, du FIFO, du pipo, etc.

Utile pour créer des files d’attente, des journaux d’évènements (pensez jeux vidéos) ou plus simplement une valeur de config plus complexe qu’une entrée.

Hash

Le Hash se comporte comme un Hash Map, dictionnaire Python, object Javascript, Array associatif en PHP… Bref, comme Redis lui-même en fait, mais sans expiration de clé. Cela permet d’avoir :

cle => {
	cle : valeur,
	cle : valeur,
}

Dans le shell :

127.0.0.1:6379> HSET scores titi 1
(integer) 1
127.0.0.1:6379> HSET scores grosminet 0
(integer) 1
127.0.0.1:6379> HSET scores tom 0
(integer) 1
127.0.0.1:6379> HSET scores jerry 1
(integer) 1
127.0.0.1:6379> HGET scores jerry
"1"
127.0.0.1:6379> HGETALL scores
1) "titi"
2) "1"
3) "grosminet"
4) "0"
5) "tom"
6) "0"
7) "jerry"
8) "1"
127.0.0.1:6379>

Notez encore une fois qu’il n’est pas utile de vérifier que la clé existe avant de rajouter une valeur dans le hash, bien qu’il y ait une commande pour le faire. Si on essaye de récupérer une clé qui n’existe pas, Redis retourne nil.

Le hash est fort pratique pour toute forme de compteurs groupés, ou juste pour mettre en cache des relations. Redis n’ayant pas de JOIN, on utilise parfois un hash pour faire le lien entre deux listes par exemple.

Set

Comme les sets en Python, le set est une collection NON ordonnée d’éléments uniques. Il ne peut pas y avoir de doublons dans un set, et vérifier si un élément fait partie d’un set est une opération très rapide. Ils permettent aussi de faire des opérations ensemblistes de manière performante, par exemple vérifier quels éléments d’un set sont ou non dans un autre set.

cle => { valeur1, valeur2, ...}

Dans le shell :

127.0.0.1:6379> SADD ip 192.168.1.1
(integer) 1
127.0.0.1:6379> SADD ip 192.168.1.1 # ajouter 2x le même ne fait rien
(integer) 0
127.0.0.1:6379> SADD ip 192.168.1.1
(integer) 0
127.0.0.1:6379> SADD ip 192.168.1.2
(integer) 1
127.0.0.1:6379> SADD ip 192.168.1.3
(integer) 1
127.0.0.1:6379> SCARD ip
(integer) 3
127.0.0.1:6379> SMEMBERS ip
1) "192.168.1.2"
2) "192.168.1.1"
3) "192.168.1.3"
127.0.0.1:6379> SADD ip:banned 192.168.1.3 # le ":" est une séparateur courant pour les clés
(integer) 1
127.0.0.1:6379> SADD ip:banned 192.168.1.10 # ip:banned est un AUTRE set
(integer) 1
127.0.0.1:6379> SINTER ip ip:banned # trouver les IP qui sont dans les 2 sets
1) "192.168.1.3"

On va utiliser les sets pour éviter les doublons (tirage au sort par exemple) ou pour vérifier facilement une appartenance (notion de groupes, de permissions, etc.).

Le set possède une variante, le set ordonné, qui associe à chaque élément du set un score. On peut changer ce score, l’incrémenter ou le décrémenter atomiquement, avoir deux scores égaux… Et au final, récupérer le set dans l’ordre ascendant ou descendant des scores.

cle => { valeur1 (1), valeur2 (3), ...}

Dans le shell :

127.0.0.1:6379> zadd participants 1 staline
(integer) 1
127.0.0.1:6379> zadd participants 1 hitler
(integer) 1
127.0.0.1:6379> zadd participants 2 "pol pot"
(integer) 1
127.0.0.1:6379> zadd participants 1999 "rainbow dash"
(integer) 1
127.0.0.1:6379> zrange participants 0 - 1
(error) ERR value is not an integer or out of range
127.0.0.1:6379> zrange participants 0 -1 
1) "hitler"
2) "staline"
3) "pol pot"
4) "rainbow dash"
127.0.0.1:6379> zrange participants 0 -1 withscores
1) "hitler"
2) "1"
3) "staline"
4) "1"
5) "pol pot"
6) "2"
7) "rainbow dash"
8) "1999"

HyperLogLog

J’ai déjà parlé de l’HyperLogLog ici et celui de Redis fonctionne pareil. Cette structure de données permettra de faire des compteurs d’éléments uniques, comme par exemple un compteur de visiteurs ou de gens connectés, qui soit approximatif (+ ou – 1%) mais prenne une taille fixe en mémoire.

Dans le shell :

127.0.0.1:6379> PFADD connectes toto
(integer) 1
127.0.0.1:6379> PFADD connectes toto
(integer) 0
127.0.0.1:6379> PFADD connectes toto
(integer) 0
127.0.0.1:6379> PFADD connectes tata
(integer) 1
127.0.0.1:6379> PFADD connectes titi
(integer) 1
127.0.0.1:6379> PFADD connectes tutu
(integer) 1
127.0.0.1:6379> PFADD connectes tutu
(integer) 0
127.0.0.1:6379> PFADD connectes tutu
(integer) 0
127.0.0.1:6379> PFADD connectes tete
(integer) 1
127.0.0.1:6379> PFADD connectes bob
(integer) 1
127.0.0.1:6379> PFcount connectes
(integer) 6
127.0.0.1:6379>

Redis comme super pote de Python

Vu que Redis est simple à installer et à configurer, c’est un outil qu’on dégaine facilement, même pour un petit script, pas forcément pour une grosse app Web. Par exemple, je fais une analyse de mon serveur, plutôt que de stocker le résultat dans un fichier, je peux mettre tout ça dans Redis, c’est tellement pratique.

D’abord, la lib pour communiquer avec Redis est à un pip du clavier :

pip install redis

Ensuite, si vous utilisez le client StrictRedis, l’API est exactement la même que les commandes du shell :

>>> import redis
>>> r = redis.StrictRedis()
>>> r.hgetall('scores')
{b'titi': b'1', b'tom': b'0', b'jerry': b'1', b'grosminet': b'0'}

Il existe aussi des drivers asynchrones pour tornado, twisted et asyncio.

Redis comme message broker

J’aime le pub/sub, je pense qu’après mon enthousiasme pour WAMP.ws, c’est clair.

Redis met à disposition des primitives pour créer des files d’attente de messages et les lire, comme une version simplifiée de RabbitMQ ou Crossbar.io. Pour que ce soit intéressant, il nous faut deux process. D’abord un shell Python qui écoute les messages arrivant sur la file “sametmax” :

>>> import redis
>>> r = redis.StrictRedis()
>>> listener = r.pubsub()
>>> listener.subscribe(['sametmax'])
>>> for item in listener.listen():
...     print(item)
...

Le code va bloquer, et affichera quelque chose à chaque fois qu’il reçoit un message.

Du coup si je fais ceci dans le shell Redis :

127.0.0.1:6379> publish sametmax yolo
(integer) 1
127.0.0.1:6379> publish sametmax carpediem
(integer) 1
127.0.0.1:6379> publish sametmax wololo
(integer) 1
127.0.0.1:6379> publish sametmax2 oyooyo
(integer) 0
127.0.0.1:6379>

Mon shell python va afficher :

{'type': 'subscribe', 'pattern': None, 'channel': b'sametmax', 'data': 1}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'yolo'}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'carpediem'}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'wololo'}

Il n’a pas affiché oyooyo puisque c’est sur une autre channel.

C’est une méthode assez simple de faire du pub/sub. C’est bas niveau, il n’y a pas de RPC, il faut boucler à la main et créer soit-même la mécanique pour arrêter d’écouter ou faire du multitask, mais c’est facile pour débuter. Pour cette raison, plein de gens utilisent une solution bricolée là-dessus pour faire du pub/sub.

Et donc

Non seulement Redis est facile à installer, simple à utiliser et performant, mais en plus il est accessible depuis de nombreux langages. Ajoutez à cela ses très nombreuses fonctionnalités, et vous avez là un système qui est installé par défaut sur la plupart de mes serveurs. Par ailleurs, suivez le blog de l’auteur, ce mec est un génie. Il écrit pas souvent, mais quand il écrit, c’est passionant, et humble. C’est beau.

]]>
http://sametmax.com/redis-pourquoi-et-comment/feed/ 23 12279