Aller au contenu principal

Des notifications d'espace de clés qui survivent au basculement

· 10 minutes de lecture
Clustron Team
Ingénierie des systèmes distribués

Notifications d'espace de clés Redis sur le watch natif répliqué de Zaris

Les notifications d'espace de clés sont l'une des fonctionnalités les plus utiles de Redis et l'une de ses plus discrètement fragiles. L'idée est charmante : chaque fois qu'une clé est écrite, supprimée ou expire, le serveur publie un événement sur un canal pub/sub bien connu, et quiconque s'y intéresse peut PSUBSCRIBE et réagir — invalider un cache, réveiller un travailleur, rafraîchir une projection. Pas de scrutation, pas de GET dans une boucle à demander « a-t-elle changé encore ? ».

La fragilité est dans les petits caractères. Les notifications d'espace de clés Redis sont du pub/sub tire-et-oublie, émis par le nœud qui a traité la commande, vers les abonnés connectés à ce même nœud. Dans un déploiement en cluster, cela signifie qu'un abonné n'entend parler que des écritures qui ont justement atterri sur le nœud auquel il est rattaché, et si ce nœud bascule, l'abonnement — et le flux d'événements qu'il portait — part avec lui. La fonctionnalité marche magnifiquement sur une machine unique et devient subtile très vite dès qu'il y en a plus d'une.

Zaris parle exactement le même protocole — CONFIG SET notify-keyspace-events, les canaux __keyspace@0__ et __keyevent@0__, un PSUBSCRIBE ordinaire depuis n'importe quel client Redis dans n'importe quel langage. Mais la plomberie en dessous n'est pas une réimplémentation du notifieur de Redis. C'est un pont mince vers une fonctionnalité que Zaris possédait déjà : un watch natif, à l'échelle du cluster, répliqué. Cela change ce que valent les notifications.

L'activer — exactement à la manière de Redis​

Il n'y a rien de spécifique à Zaris dans la façon dont vous activez ou consommez la fonctionnalité. Pointez n'importe quel client Redis vers la façade RESP et utilisez les commandes que vous connaissez déjà :

# Enable notifications for keyspace (K) and keyevent (E) channels
redis-cli -p 6380 CONFIG SET notify-keyspace-events KEA

# Subscribe to every key-event, cluster-wide
redis-cli -p 6380 PSUBSCRIBE '__keyevent@0__:*'
1) "pmessage"
2) "__keyevent@0__:*"
3) "__keyevent@0__:set"
4) "user:42"

Depuis un client typé — ici StackExchange.Redis — c'est la même forme, sans SDK Zaris impliqué :

var mux = await ConnectionMultiplexer.ConnectAsync("localhost:6380");
var sub = mux.GetSubscriber();

await sub.SubscribeAsync(
new RedisChannel("__keyevent@0__:expired", RedisChannel.PatternMode.Literal),
(channel, key) => Console.WriteLine($"key expired: {key}"));

Le point qui mérite d'être souligné : aucun changement d'application. Un code écrit contre les notifications d'espace de clés Redis tourne contre Zaris sans modification. C'est toute la promesse de la façade RESP — vos clients n'ont pas besoin de savoir ce qu'il y a derrière le socket.

Les deux familles de canaux​

Zaris publie les deux familles de canaux Redis standard, avec la convention de charge utile standard — celle à laquelle vous vous abonnez dépend de si vous êtes indexé par la clé ou par l'événement :

CanalPublié quand le drapeau…Charge utileÀ utiliser quand ce qui vous intéresse c'est…
__keyspace@0__:<key>K est définile nom de l'événement (set / del / expired)une clé spécifique — « dis-moi tout ce qui arrive à session:abc »
__keyevent@0__:<event>E est définila clé qui a changéune classe d'événements — « dis-moi chaque clé qui vient d'expirer »

Le @0 est l'index de base de données, et dans Zaris il est toujours 0 — Zaris ne découpe pas l'espace de clés en bases de données numérotées, il y a donc un espace de noms de notification unique et plat plutôt que seize. Les trois noms d'événements correspondent aux trois choses qui peuvent arriver à une clé :

  • set — la clé a été écrite (un SET, un HSET, un LPUSH, toute mutation)
  • del — la clé a été supprimée
  • expired — la clé a été évincée parce que son TTL s'est écoulé

Ce qui se passe réellement en dessous​

Lorsque vous envoyez CONFIG SET notify-keyspace-events KEA, Zaris n'allume pas un émetteur d'événements Redis sur mesure câblé dans le chemin de commande. Il enregistre un watch de préfixe sur tout l'espace de clés sur le propre client de routage du nœud — la même primitive Watch que le client .NET natif expose pour des lectures réactives, sans scrutation. À partir de là, la seule logique qui vit dans la couche RESP est une correspondance de nommage : un événement de watch natif arrive, et le pont le transforme en le bon nom de canal Redis et la bonne charge utile.

// The entire RESP-side translation, essentially verbatim:
var eventName = e.EventType switch
{
WatchEventType.Put => "set",
WatchEventType.Delete => "del",
WatchEventType.Evicted => "expired",
_ => null, // snapshots / heartbeats carry no keyspace meaning
};

if (keyspace) PubSub.Publish($"__keyspace@0__:{e.Key}", eventName);
if (keyevent) PubSub.Publish($"__keyevent@0__:{eventName}", e.Key);

Cet appel Publish est le pub/sub natif — la même mécanique d'abonnement/livraison, avec sa propre diffusion à l'échelle du cluster, que le store exécute déjà. La façade RESP possède le vocabulaire (noms de canaux, convention de charge utile) ; le cluster possède la livraison. C'est un motif auquel nous nous tenons délibérément à travers tout le produit : chaque fonctionnalité est d'abord une capacité native, et RESP est une peau mince par-dessus — jamais une implémentation parallèle avec ses propres bugs.

Pourquoi « le watch est natif » est tout le propos​

Voici où les deux systèmes divergent, et ce n'est pas cosmétique.

Une notification d'espace de clés Redis est produite sur le nœud qui a exécuté la commande et livrée aux abonnés connectés à ce nœud. Rien de l'abonnement n'est partagé avec le reste du cluster, et rien de lui n'est durable. C'est du pub/sub au sens strict — si vous n'écoutiez pas sur le bon nœud au bon instant, l'événement n'a tout simplement pas existé pour vous.

Une notification d'espace de clés Zaris chevauche un watch natif, et un watch natif est un objet à l'échelle du cluster :

  • L'abonnement est répliqué entre les membres. WatchSyncService garde les abonnements de watch synchronisés entre les nœuds — le leader pousse l'ensemble courant vers un nœud qui rejoint, et un nœud qui monte demande au leader ce qui est actif. L'abonnement n'est pas un fait connu d'un seul processus ; c'est un état du cluster.
  • Il est relogé à travers le changement. Lorsque les partitions bougent — basculement, migration, un nœud qui part — le watch bouge avec les données qu'il observe, de la même façon que la propriété des clés. L'écriture produit toujours un événement une fois la poussière retombée, parce que l'observateur a suivi la partition.
  • La livraison se diffuse à l'échelle du cluster. Parce que la publication passe par le pub/sub natif, un client connecté à n'importe quel nœud reçoit les événements des écritures qui se sont produites sur n'importe quel nœud. Vous n'êtes pas discrètement abonné à l'activité d'un seul fragment.

La conséquence pratique : un abonné qui observe __keyevent@0__:expired pour piloter l'invalidation de cache continue de fonctionner à travers une panne de nœud. Dans Redis Cluster, ce même abonné est rattaché à un nœud et voit les expirations d'un nœud ; l'atténuation honnête là-bas est d'ouvrir un abonnement sur chaque nœud et de dédupliquer. Sur Zaris, c'est un seul abonnement, complet à l'échelle du cluster, et il survit au basculement qui l'aurait silencieusement sectionné.

Les limites honnêtes​

C'est un pont au-dessus d'un pub/sub au mieux, pas une mise à niveau magique de fiabilité. Trois choses à garder au clair :

La livraison reste au mieux, pas un journal garanti. Le pont publie chaque événement en tire-et-oublie : un échec de livraison à un abonné ne doit jamais bloquer le pipeline de watch pour tous les autres, donc une notification vers un client momentanément déconnecté est abandonnée, pas tamponnée. Cela correspond au propre contrat de Redis — les notifications d'espace de clés n'ont jamais été un journal d'événements exactement-une-fois, et vous ne devriez pas traiter celles de Zaris comme tel non plus. Si un événement manqué corrompait votre état, les notifications sont le réveil, pas la source de vérité : sur l'événement, allez lire la valeur autoritaire.

Pas de rattrapage. Le watch est armé sans instantané initial, donc vous obtenez les changements à partir du moment où les notifications sont activées — jamais un rejeu de ce que l'espace de clés contenait déjà. Activez les notifications, puis commencez à raisonner sur les deltas.

La granularité des drapeaux est grossière. Redis vous laisse sélectionner des classes d'événements avec une chaîne de drapeaux dense (g, $, l, s, h, z, x, e, …). Zaris lit les deux drapeaux qui décident où publier — K pour les canaux d'espace de clés, E pour les canaux d'événements (et A comme raccourci habituel « toutes les classes ») — et fait le pont des trois types d'événements natifs dès que l'un ou l'autre est défini. Il ne filtre pas jusqu'à des abonnements par classe. En pratique, vous vous abonnez aux canaux et motifs qui vous intéressent et vous laissez la correspondance de motif faire le resserrement, ce qui est de toute façon la façon dont la plupart du code de notification d'espace de clés est écrit.

Si vous avez besoin de notifications limitées à une partie de l'espace de clés plutôt qu'à sa totalité, définissez ZARIS_RESP_KEYSPACE_PREFIX sur le nœud et le watch sous-jacent s'arme sur ce préfixe au lieu de tout le store — un moyen bon marché d'empêcher un espace de clés occupé de publier des événements auxquels personne ne s'abonne.

Quand vous voulez un journal plutôt qu'une sonnette​

Les notifications d'espace de clés sont une sonnette : elles vous disent que quelque chose s'est passé, à l'instant, pour quiconque écoute. Quand ce dont vous avez réellement besoin est un enregistrement rejouable et durable — chaque événement, dans l'ordre, lisible par un consommateur arrivé en retard ou planté puis revenu — ce n'est pas une notification, c'est un stream. Zaris a des Redis Streams natifs pour exactement cela : XADD pour ajouter, XREADGROUP avec des groupes de consommateurs pour un traitement au moins une fois avec acquittements, des lectures bloquantes pour une faible latence. Recourez aux notifications quand un événement manqué signifie simplement « revérifie la prochaine fois » ; recourez aux Streams quand un événement manqué est un bug.

Utilisée pour ce qu'elle est — un réveil à faible latence, sans scrutation, qui, sur Zaris, se trouve être complet à l'échelle du cluster et durable au basculement — la notification d'espace de clés est l'une des plus belles choses que vous puissiez activer avec un seul CONFIG SET. La différence n'est visible que le jour où un nœud disparaît, qui est précisément le jour où vous auriez découvert à la dure les limites de la version à nœud unique.