La nouvelle structure de données Redis 5, appelée flux, a suscité un vif intérêt dans la communauté. D'une manière ou d'une autre, je parlerai à ceux qui utilisent des flux en production et j'écrirai à ce sujet. Mais maintenant, je veux considérer un sujet légèrement différent. Il me semble que beaucoup de gens considèrent les flux comme une sorte d'outil surréaliste pour résoudre des tâches terriblement difficiles. En effet, cette structure de données * fournit également des messages, mais ce sera une simplification incroyable de supposer que la fonctionnalité de Redis Streams n'est limitée que par cela.
Les flux sont un modèle et un «modèle mental» formidables qui peuvent être utilisés avec grand succès dans la conception de systèmes, mais en réalité, comme la plupart des structures de données Redis, sont une structure plus générale et peuvent être utilisés pour un tas d'autres tâches. Dans cet article, nous présenterons les flux comme une structure de données pure, ignorant complètement les opérations de blocage, les groupes de destinataires et toutes les autres fonctionnalités de messagerie.
Streams - Ceci est CSV sur les stéroïdes
Si vous souhaitez enregistrer un certain nombre d'éléments de données structurés et pensez que la base de données sera un excès ici, vous pouvez simplement ouvrir le fichier en mode
append only
et écrire chaque ligne en CSV (Comma Separated Value):
(open data.csv in append only) time=1553096724033,cpu_temp=23.4,load=2.3 time=1553096725029,cpu_temp=23.2,load=2.1
Ça a l'air simple. Les gens l'ont fait il y a longtemps et le font toujours: c'est un modèle fiable, si vous savez quoi. Mais quel sera l'équivalent en mémoire? En mémoire, un traitement des données beaucoup plus avancé devient possible, et de nombreuses restrictions des fichiers CSV sont automatiquement supprimées, telles que:
- Il est difficile (inefficace) de répondre aux demandes de plage.
- Trop d'informations redondantes: chaque enregistrement a presque le même temps et les champs sont dupliqués. Dans le même temps, la suppression de données rendra le format moins flexible si je souhaite basculer vers un autre ensemble de champs.
- Les décalages d'éléments sont simplement des décalages d'octets dans le fichier: si nous changeons la structure du fichier, les décalages deviendront faux, il n'y a donc pas de véritable concept d'identifiant principal. Les inscriptions ne peuvent pas être présentées sans ambiguïté.
- Sans la possibilité de collecter les déchets et sans réécrire le journal, vous ne pouvez pas supprimer les entrées, mais uniquement les marquer comme non valides. La réécriture des journaux craint généralement pour plusieurs raisons, il est conseillé de l'éviter.
En même temps, un tel journal CSV est bon à sa manière: il n'y a pas de structure fixe, les champs peuvent changer, il est trivial de le générer et il est assez compact. L'idée avec les flux Redis était de préserver les vertus, mais de surmonter les limites. Le résultat est une structure de données hybride très similaire aux ensembles triés de Redis: ils * ressemblent * à la structure de données fondamentale, mais utilisent plusieurs représentations internes pour obtenir cet effet.
Introduction aux threads (vous pouvez sauter si vous connaissez déjà les bases)
Les flux Redis sont représentés comme des nœuds de macro compressés en delta connectés par une arborescence de base. Par conséquent, vous pouvez rechercher très rapidement des enregistrements aléatoires, obtenir des plages, supprimer d'anciens éléments, etc. En même temps, l'interface du programmeur est très similaire à un fichier CSV:
> XADD mystream * cpu-temp 23.4 load 2.3 "1553097561402-0" > XADD mystream * cpu-temp 23.2 load 2.1 "1553097568315-0"
Comme vous pouvez le voir dans l'exemple, la commande XADD génère et renvoie automatiquement l'identifiant de l'enregistrement, qui augmente de façon monotone et se compose de deux parties: <time> - <counter>. Temps en millisecondes et le compteur est incrémenté pour les enregistrements avec la même heure.
Ainsi, la première nouvelle abstraction pour l'idée d'un fichier CSV en mode
append only
est d'utiliser l'astérisque comme argument ID pour XADD: c'est ainsi que nous obtenons gratuitement l'identifiant d'enregistrement du serveur. Cet identifiant est utile non seulement pour indiquer un élément spécifique dans le flux, il est également associé à l'heure à laquelle l'enregistrement a été ajouté au flux. En fait, avec XRANGE, vous pouvez exécuter des requêtes de plage ou récupérer des éléments individuels:
> XRANGE mystream 1553097561402-0 1553097561402-0 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3"
Dans ce cas, j'ai utilisé le même ID pour démarrer et terminer la plage pour identifier un élément. Cependant, je peux utiliser n'importe quelle plage et argument COUNT pour limiter le nombre de résultats. De même, il n'est pas nécessaire de spécifier des identifiants complets pour une plage, je peux simplement utiliser uniquement le temps unix pour obtenir des éléments dans une plage de temps donnée:
> XRANGE mystream 1553097560000 1553097570000 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3" 2) 1) "1553097568315-0" 2) 1) "cpu-temp" 2) "23.2" 3) "load" 4) "2.1"
Pour le moment, il n'est pas nécessaire de vous montrer d'autres fonctionnalités de l'API, il existe une documentation pour cela. Pour l'instant, concentrons-nous uniquement sur ce modèle d'utilisation: XADD pour ajouter, XRANGE (et aussi XREAD) pour extraire des plages (selon ce que vous voulez faire), et voyons pourquoi les flux sont si puissants qu'ils les appellent des structures de données.
Si vous souhaitez en savoir plus sur les flux et les API, assurez-vous de lire le
didacticiel .
Joueurs de tennis
Il y a quelques jours, un de mes amis qui a commencé à étudier Redis et moi avons simulé une application pour suivre les courts de tennis, les joueurs et les matchs locaux. La façon de modéliser les joueurs est évidente, le joueur est un petit objet, nous n'avons donc besoin que d'un hachage avec des clés comme
player:<id>
. Ensuite, vous réaliserez immédiatement que vous avez besoin d'un moyen de suivre les matchs dans des clubs de tennis spécifiques. Si
player:1
et
player:2
joué entre eux et
player:1
gagné, nous pouvons envoyer le record suivant au stream:
> XADD club:1234.matches * player-a 1 player-b 2 winner 1 "1553254144387-0"
Une opération aussi simple nous donne:
- Identifiant de correspondance unique: ID dans le flux.
- Il n'est pas nécessaire de créer un objet pour l'identification de correspondance.
- Demandes de portée libre pour les matchs de pagination ou de regarder des matchs pour une date et une heure spécifiques.
Avant que les flux n'apparaissent, nous devions créer un ensemble trié par le temps: les éléments de l'ensemble trié seraient des identificateurs de correspondance, qui sont stockés dans une clé différente en tant que valeur de hachage. Ce n'est pas seulement plus de travail, mais aussi plus de mémoire. Beaucoup, beaucoup plus de mémoire (voir ci-dessous).
Maintenant, notre objectif est de montrer que les flux Redis sont une sorte d'ensemble trié en mode
append only
, avec des clés par heure, où chaque élément est un petit hachage. Et dans sa simplicité, c'est une véritable révolution dans le contexte de la modélisation.
La mémoire
Le cas d'utilisation ci-dessus n'est pas seulement un modèle de programmation plus cohérent. La consommation de mémoire dans les threads est tellement différente de l'ancienne approche avec un ensemble trié + hachage pour chaque objet que certaines choses commencent maintenant à fonctionner qui étaient auparavant impossible à implémenter.
Voici des statistiques sur la quantité de mémoire pour stocker un million de correspondances dans la configuration présentée précédemment:
+ = 220 (242 RSS) = 16,8 (18.11 RSS)
La différence est supérieure à un ordre de grandeur (à savoir 13 fois). Cela signifie pouvoir travailler avec des tâches qui étaient auparavant trop coûteuses pour être exécutées en mémoire. Maintenant, ils sont tout à fait viables. La magie consiste à introduire les flux Redis: les nœuds de macro peuvent contenir plusieurs éléments qui sont codés de manière très compacte dans une structure de données appelée listpack. Cette structure prendra soin, par exemple, de coder des entiers sous forme binaire, même s'il s'agit de chaînes sémantiques. De plus, nous appliquons la compression delta et compressons les mêmes champs. Cependant, il reste possible de rechercher par ID ou par heure, car ces macro-nœuds sont liés dans une arborescence de base, également conçue avec une optimisation de la mémoire. Ensemble, cela explique l'utilisation économique de la mémoire, mais la partie intéressante est que sémantiquement l'utilisateur ne voit aucun détail d'implémentation qui rend les threads si efficaces.
Maintenant comptons. Si je peux stocker 1 million d'enregistrements dans environ 18 Mo de mémoire, je peux en stocker 10 millions sur 180 Mo et 100 millions sur 1,8 Go. Avec seulement 18 Go de mémoire, je peux avoir 1 milliard d'articles.
Séries chronologiques
Il est important de noter que l'exemple ci-dessus avec des matchs de tennis est sémantiquement * très différent * de l'utilisation de flux Redis pour les séries chronologiques. Oui, logiquement, nous enregistrons toujours une sorte d'événement, mais il y a une différence fondamentale. Dans le premier cas, nous enregistrons et créons des enregistrements pour le rendu des objets. Et dans la série chronologique, nous mesurons simplement quelque chose qui se passe à l'extérieur et qui ne représente pas réellement l'objet. Vous pouvez dire que cette distinction est triviale, mais elle ne l'est pas. Il est important de comprendre l'idée que les threads Redis peuvent être utilisés pour créer de petits objets avec un ordre commun et attribuer des identifiants à ces objets.
Mais même la manière la plus simple d'utiliser les séries temporelles est évidemment une énorme percée, car avant l'avènement des threads, Redis était pratiquement impuissant à faire quoi que ce soit ici. Les caractéristiques de la mémoire et la flexibilité des flux, ainsi que la possibilité de limiter les flux plafonnés (voir les paramètres XADD) sont des outils très importants entre les mains du développeur.
Conclusions
Les flux sont flexibles et offrent de nombreux cas d'utilisation, mais je voulais écrire un très court article pour montrer clairement les exemples et la consommation de mémoire. Pour de nombreux lecteurs, cette utilisation des flux était peut-être évidente. Cependant, les conversations avec les développeurs au cours des derniers mois m'ont donné l'impression que beaucoup ont une forte association entre les flux et les données en streaming, comme si la structure des données n'était bonne que là-bas. Ce n'est pas le cas. :-)