Comment un cluster Zaris démarre : appartenance, map et disponibilité conditionnée par la convergence
La plupart des écrits sur les stores distribués traitent du régime permanent — lectures, écritures, réplication — ou des défaillances spectaculaires : un basculement quand un propriétaire meurt, un cut-over quand une partition se déplace. Mais il existe un moment plus discret qui décide si tout cela fonctionne : le moment où le cluster démarre. Avant qu'une seule clé puisse être routée, les nœuds doivent s'accorder sur qui fait partie du cluster, qui possède quelle tranche du keyspace et — l'étape que la plupart des systèmes ratent — s'il est seulement sûr de commencer à servir.
Cet article suit cette séquence de démarrage dans l'ordre où Zaris l'exécute réellement : l'appartenance, la partition map, et les barrières de disponibilité qui se dressent entre « le processus est lancé » et « le nœud prend du trafic ». Il trace aussi, honnêtement, une ligne autour de ce que Zaris implémente lui-même par rapport à ce qu'il hérite du runtime de cluster sous-jacent sur lequel il est construit.
Qui fait partie du cluster : l'appartenance
La première chose honnête à dire est où se situe la frontière. Le transport d'appartenance de bas niveau — heartbeats entre pairs, détection de défaillance et élection du leader — est fourni par la couche Clustron.Nodus sur laquelle Zaris est construit. Zaris la consomme via une interface INodusNode : il demande à Nodus qui sont les membres, lequel détient le leadership, et il est notifié quand le heartbeat d'un pair est manqué ou échoue. Zaris ne réimplémente pas Raft ni un protocole de découverte par gossip ; il superpose son propre plan de contrôle de partition et de possession par-dessus ce socle.
Ce que Zaris ajoute, c'est une machine à états d'appartenance explicite qui traduit les événements bruts de heartbeat en décisions pertinentes pour la possession. Elle vit dans MembershipStateTracker, et ses états sont délibérément plus nuancés que « actif / inactif » :
Les états se répartissent en deux groupes, et cette division est tout l'enjeu. Alive, Suspected et Unreachable n'affectent que la possession à l'exécution — ils peuvent déclencher la promotion d'un réplica ou marquer une partition temporairement indisponible, mais ils ne modifient jamais la partition map. Seuls Lost, Evicted et Decommissioned rendent un nœud éligible à la régénération de la map.
Cette distinction existe parce que régénérer la partition map coûte cher et, fait trop hâtivement, est dangereux — cela peut rebrasser inutilement des partitions saines. Donc quand le heartbeat d'un nœud échoue, Zaris ne réécrit pas immédiatement la map. Il fait passer le nœud en Unreachable et démarre une fenêtre de grâce (30 secondes par défaut). Pendant cette fenêtre, le cluster ne fait que du basculement à l'exécution — promouvoir un réplica pour que lectures et écritures continuent de circuler — tout en laissant au nœud une chance de revenir. Un flapping, une pause de GC, un bref hoquet réseau : rien de tout cela ne devrait réécrire la possession. Ce n'est que si la fenêtre de grâce expire que le nœud passe à Lost, et ce n'est que lorsqu'un groupe de partition entier a été évincé que l'unique chemin de régénération de map piloté par la défaillance se déclenche. La règle de conception, énoncée sans détour dans le code qui l'assemble, est : un nœud qui part ne régénère jamais la map à lui seul.
Comment les nœuds se trouvent
La découverte est fondée sur un registre, pas sur une découverte par gossip. La liste faisant autorité des nœuds est de la configuration, pas quelque chose que le cluster renifle à l'exécution. Dans une configuration écrite à la main, c'est le tableau nodes — chaque entrée déclarant un id, un hôte, un port de cluster (pair) et un port client. Sur Kubernetes, ce registre est synthétisé à partir des ordinaux de pod du StatefulSet plutôt que maintenu à la main, le rôle de primaire préféré d'un nœud étant dérivé directement de son ordinal.
Il existe bien un mécanisme de gossip dans Zaris, mais il fait un autre travail — c'est ainsi que les nœuds maintiennent leur vue de l'état du cluster convergée, pas ainsi qu'ils découvrent que des pairs existent. Nous y reviendrons quand nous parlerons de disponibilité.
Qui possède quoi : la partition map
Une fois l'appartenance connue, le cluster a besoin d'un plan pour le keyspace. Ce plan, c'est la partition map — un document unique et versionné contre lequel chaque nœud route.
D'une clé à un propriétaire, en deux sauts
Une clé ne se mappe pas directement à une partition. Zaris utilise un mapping à deux niveaux délibérément aligné octet pour octet avec Redis Cluster afin que la même arithmétique de routage fonctionne aussi bien pour les clients natifs que pour le front-end RESP :
key --CRC16--> slot (0..16383) --proportional--> segment (0..255) --SegmentToPartition[]--> partition
Le premier saut hache la clé avec CRC16 (CCITT/XMODEM, polynôme 0x1021) modulo 16384 — la fonction exacte qu'utilise Redis Cluster, y compris le respect de la co-localisation par hash-tag, où une clé écrite user:{42}:profile ne hache que la portion entre accolades afin que les clés apparentées atterrissent ensemble. Les 16384 slots sont ensuite mappés proportionnellement sur les segments du cluster — 256 par défaut, fixés pour la durée de vie du cluster — et une table SegmentToPartition affecte chaque segment à une partition. La partition, enfin, est ce qui a des propriétaires.
Pourquoi cette couche de segment supplémentaire plutôt que de hacher directement vers les partitions ? Parce que les segments sont une unité stable et à grain fin que l'on peut déplacer entre partitions quand le cluster grandit ou se rééquilibre, sans re-hacher chaque clé. L'étape CRC16-vers-slot ne change jamais ; seule l'affectation segment-vers-partition change.
Ce que contient une partition map
La map elle-même est un PartitionMap — un enregistrement sérialisé en MessagePack dont la forme révèle avec quel soin la possession est suivie. Les champs qui valent d'être connus :
Version— un compteur monotone qui s'incrémente à chaque changement de map. C'est le nombre qui rend le commit sûr (plus de détails ci-dessous).TopologyVersion— ne s'incrémente que lors d'un véritable changement structurel (un nœud ou une partition qui rejoint ou quitte, un re-découpage), pas lors d'un cut-over de migration de routine. Séparer « la forme a changé » de « un propriétaire a changé » laisse le reste du système réagir proportionnellement.TotalSegments— 256 par défaut, constant pour la vie du cluster.Partitions— la liste des affectations, chacune portant unPrimaryNodeId(le primaire de placement), desReplicaNodeIds, et unePartitionIdentitystable (un GUID frappé à la naissance qui survit à un renumérotage) plus les champs de bail de possession :ActiveOwnerNodeId, uneOwnershipGenerationfrappée par le leader, et leLeaderTermqui a accordé le bail — ce qui permet au cluster de clôturer un ex-leader périmé hors de la rédaction de la possession.
Notez la scission délibérée entre PrimaryNodeId (où la map préfère qu'une partition réside) et ActiveOwnerNodeId (qui la sert réellement en ce moment). Ces deux peuvent différer pendant un basculement ou une migration, et les garder comme des champs distincts est ce qui rend ces transitions représentables sans mentir sur le plan de régime permanent. L'article sur le modèle de cohérence explique pourquoi les lectures routent vers cet unique propriétaire actif.
Comment la map est planifiée
La map n'est pas assemblée de façon gloutonne. Elle est produite par un planificateur pur et déterministe — DesiredMapPlanner — qui prend les ids des nœuds membres, le nombre de segments, un nombre de partitions souhaité et le facteur de réplication, et renvoie un plan. Deux propriétés valent d'être soulignées :
- Des découpages de segments équilibrés et contigus. Les segments sont divisés aussi uniformément que possible, en plages contiguës, les premières partitions absorbant le reste quand la division n'est pas nette.
- Un placement en groupes disjoints. Les N premiers membres deviennent les primaires distincts des partitions ; les membres restants remplissent les emplacements de réplica en round-robin, de sorte qu'aucun nœud ne se retrouve dans deux groupes de partition différents.
La raison pour laquelle le planificateur est écrit comme une fonction pure de ses entrées est chèrement acquise : cela rend toute une classe de maps défectueuses — une partition avec un primaire en double, une plage de segments non contiguë, une affectation dégénérée — irreprésentables par construction, plutôt que quelque chose que l'on rattrape après coup avec un validateur. Il vaut aussi la peine d'être honnête sur la limite actuelle : le placement d'aujourd'hui est positionnel et en groupes disjoints ; un placement de style rendezvous minimisant le churn, qui déplace le moins de données possible à chaque régénération, est un raffinement futur explicite, pas quelque chose de livré. Ce qui est livré, c'est une étape de placement collant qui, lors d'une régénération due au churn d'appartenance, maintient en place un propriétaire vivant qui sert, afin qu'une partition saine ne soit pas déplacée sans raison.
Ce que « commit » signifie
La map est rédigée par un seul nœud — le leader du plan de contrôle — et personne d'autre. Le leader génère une map candidate, la fait passer par une projection du log de possession (le leader est l'unique rédacteur du log de possession, et la map servie est un repli de ce log), puis la valide localement avant d'en informer qui que ce soit. Le commit est une règle unique et stricte imposée dans TopologyManager.ApplyMap :
// A map is accepted only if it is strictly newer.
if (_currentMap != null && map.Version <= _currentMap.Version)
return false;
Une map dont la version est inférieure ou égale à celle que vous détenez déjà est rejetée, point final. Ce n'est qu'après que le leader a validé qu'il diffuse un MapChangedEvent, pousse des snapshots de map directs vers les pairs, et publie le changement de routage aux clients. Les suiveurs récupèrent la map (au démarrage à froid ils la demandent aux pairs déjà connus, en réessayant jusqu'à ce que l'un réponde) et l'appliquent via la même garde monotone. Parce que chaque nœud n'accepte jamais qu'une version strictement supérieure, deux nœuds ne peuvent jamais se fixer sur des maps conflictuelles de même version — le numéro de version est le point de sérialisation.
Une ride honnête au démarrage à froid : un nœud sans leader qui démarre seul peut frapper une map provisoire à un seul nœud pour ne pas rester dans l'impasse, marquée comme provisoire et plus tard remplacée par la map multi-nœuds faisant autorité une fois le cluster formé. C'est un confort de disponibilité, pas le chemin de régime permanent.
Est-il sûr de servir : la disponibilité conditionnée par la convergence
Voici l'étape qui sépare un store auquel on peut se fier d'un store qui se contente de démarrer. Avoir un processus lancé, et même avoir une map, ne signifie pas qu'un nœud doive prendre du trafic. Si un nœud commence à servir avant que sa possession à l'exécution n'ait rattrapé la map qu'il vient d'appliquer, il peut router une requête vers une partition qu'il ne détient pas encore réellement. Zaris refuse de le faire, avec deux barrières distinctes.
Barrière un : la stabilité de la formation du cluster
La première barrière décide quand le leader est autorisé à générer la première vraie map. ClusterReadinessGate attend que l'appartenance soit stable — au moins un nombre minimum de nœuds présents continûment pendant une fenêtre de durée de stabilité (3 secondes par défaut) — avant de valider une map. Cela empêche le leader de rédiger une map contre un cluster à moitié formé où les nœuds rejoignent encore un par un, ce qui signifierait une régénération immédiate une seconde plus tard. Si la stabilité n'est pas atteinte dans le délai imparti (30 secondes par défaut), la barrière force le bootstrap quand même : la disponibilité l'emporte sur une attente éternelle. Le cluster préfère démarrer légèrement en avance plutôt que de se bloquer.
Barrière deux : la barrière de trafic par nœud
La seconde barrière est celle qui décide réellement si un nœud ouvre son listener client. ClusterTrafficReadinessGate.IsReady() ne renvoie vrai que lorsque toutes les conditions suivantes sont réunies :
- une map validée existe,
- sa table segment-vers-partition est entièrement peuplée (
length == TotalSegments), - une map de possession à l'exécution existe,
runtime.MapVersion == map.Version— la possession à l'exécution du nœud est alignée sur la version exacte de la map qu'il a validée,- et chaque partition a une entrée à l'exécution.
Cette quatrième condition en est le cœur. Un nœud ne s'annoncera pas comme prêt à servir tant que sa propre map validée et sa propre possession à l'exécution ne sont pas sur la même version. C'est ce que pointe l'image d'en-tête tout en haut. Il y a un garde-fou de 60 secondes pour qu'un nœud incapable de converger ne bloque pas le démarrage indéfiniment — passé ce délai il poursuit avec un avertissement plutôt que de se coincer — mais dans le cas normal, la barrière tient la porte jusqu'à ce que les deux plans concordent.
Rester convergé : digests et détection-réparation
La disponibilité est une propriété de démarrage ; rester convergé est une propriété de régime permanent, et c'est là que le gossip de Zaris gagne vraiment sa place. Chaque heartbeat transporte un petit ClusterStateDigest — l'epoch du nœud, qui il pense être le leader, sa version de map, son nombre de nœuds, et les générations de possession par partition. Aucun RPC supplémentaire ; il voyage avec le trafic que le cluster envoie déjà.
Un DigestAccumulator collecte les digests des pairs et, toutes les quelques secondes, les compare. Si un nœud diverge de la majorité pendant deux cycles d'évaluation consécutifs, il lève un signal de dérive, et un HealingOrchestrator y réagit — le leader envoie des directives correctives (avec un délai d'apaisement pour qu'il ne s'emballe pas), et un nœud qui se retrouve l'unique cas isolé s'auto-répare en re-récupérant la map. C'est de la convergence par détection-réparation, pas par un vote de quorum avant de servir.
Cette distinction importe pour l'honnêteté, alors soyons précis. En production, l'accord sur la version de map à travers tout le cluster est maintenu par la barrière de trafic par nœud plus cette boucle de réparation pilotée par les digests — pas par une barrière globale qui bloque tout service tant que chaque nœud n'a pas voté sur une version. L'assertion plus forte — chaque snapshot de nœud s'accordant sur un seul leader, une seule epoch de leader et une seule version de map — est ce que le harnais de certification du chaos sur 12 heures vérifie en continu comme un invariant pass/fail. La certification prouve que la propriété tient ; le runtime l'atteint par barrière et réparation plutôt que par un tour de consensus bloquant sur le chemin de service.
Facteur de réplication, et une valeur par défaut honnête
Une dernière pièce de l'histoire de la formation, car elle change ce que « prêt » signifie pour votre durabilité. Le facteur de réplication vaut 1 par défaut. Avec RF=1, chaque partition a un primaire et aucun réplica — une seule copie. Le planificateur place RF - 1 réplicas par partition, donc à la valeur par défaut c'est zéro. Un cluster à RF=1 se forme, converge et sert parfaitement bien, mais la perte d'un nœud est une perte de données pour les partitions qu'il possédait, par conception, pas par accident. Si vous voulez qu'une partition survive à une défaillance de nœud, vous mettez RF >= 2, et il vous faut assez de nœuds pour placer effectivement ces réplicas sur des machines distinctes — l'article sur le dimensionnement de durabilité Kubernetes détaille exactement combien de pods cela demande. Le nombre de partitions a une valeur par défaut tout aussi honnête : laissé non défini, le cluster utilise une partition par nœud.
La séquence, de bout en bout
Mis bout à bout, un cluster Zaris démarre ainsi :
- L'appartenance se résout à partir du registre de configuration (ou des ordinaux k8s), et Nodus élit un leader de plan de contrôle.
- Le leader attend derrière la barrière de formation une appartenance stable, puis planifie une map avec
DesiredMapPlanneret la valide localement sous la garde de version strictement monotone. - Le leader diffuse la map ; les suiveurs la récupèrent et l'appliquent sous la même garde, n'acceptant que des versions strictement supérieures.
- Chaque nœud patiente derrière sa barrière de trafic jusqu'à ce que
runtime.MapVersion == map.Versionet que le routage soit entièrement peuplé — seulement alors il ouvre son listener client. - En régime permanent, les digests portés par les heartbeats maintiennent la vue de chaque nœud convergée, avec détection de dérive et réparation pilotée par le leader comblant tout écart.
Aucune de ces étapes n'est spectaculaire en soi. Ensemble, elles sont la différence entre un cluster qui tourne et un cluster qui est prêt — et l'écart entre ces deux mots est là où un store perd silencieusement des données s'il ne prend pas la question au sérieux.
Zaris est le store clé-valeur distribué de Clustron. Si vous voulez voir le plan de contrôle du côté de l'opérateur, la présentation PowerShell crée, démarre et inspecte un cluster en direct ; et la mise à l'échelle élastique couvre ce qui arrive à la map quand vous ajoutez un serveur à un cluster qui sert déjà.