Lire vos propres écritures : le modèle de cohérence de Zaris, énoncé honnêtement
Il y a une question que chaque développeur finit par poser à un store distribué, généralement juste après un rapport de bug déroutant : si j'écris une valeur puis la relis immédiatement, suis-je garanti de voir ce que j'ai écrit ? On dirait que cela devrait avoir une réponse en un mot. Ce n'est pas le cas — ni pour aucun store partitionné et répliqué, ni pour Zaris. La réponse honnête est « oui, au sein d'une partition, parce que les lectures vont au même propriétaire unique que votre écriture » — et la partie intéressante est tout ce qui est contenu dans ce qualificatif.
Cet article est le modèle de cohérence de Zaris, assemblé en un seul endroit. Nous avons écrit sur les pièces séparément — comparaison-et-échange optimiste, nouvelle tentative transparente lors du changement de propriété, le journal d'opérations rejouable qui alimente les réplicas, transactions multi-clés. Ici nous les relions du côté du lecteur et répondons à la seule question qui compte derrière un clavier : quand je lis, qu'est-ce qui m'est réellement promis ? Y compris, surtout, là où la promesse s'arrête.
Le modèle en une phrase
Zaris est un store à tendance AP avec la frontière de cohérence tracée autour de la propriété, et non autour de chaque écriture. Le plan de données reste disponible — un nœud isolé continue de servir les segments qu'il détient — et la discipline d'écrivain unique qui garde les lectures saines est imposée dans le plan de propriété : il y a exactement un propriétaire actif par partition à tout instant. Les écritures vers une partition sont sérialisées à travers ce nœud unique. Il n'y a délibérément aucun quorum ni consensus sur le chemin d'écriture, et Zaris ne revendique pas de linéarisabilité stricte globale.
Presque tout ce qu'un lecteur observe découle de ce seul fait structurel, il vaut donc la peine de rendre la chaîne explicite :
key → segment → partition → one runtime active owner
Une clé hache de façon déterministe vers un segment ; un segment appartient à une partition ; une partition a exactement un propriétaire actif qui accepte ses écritures et sert ses lectures. La propriété est un bail porté sur la carte de partitions sous la forme (LeaderTerm, OwnershipGeneration) — accordé par le leader du cluster, durable à travers les redémarrages, et propagé à chaque nœud. Ce bail est ce qui garantit l'« exactement un » — il a remplacé un ancien schéma « l'époque la plus haute gagne » qui pouvait se réinitialiser au redémarrage et produire brièvement deux nœuds croyant chacun posséder la même partition.
Pourquoi les lectures routées vers le propriétaire vous donnent la lecture de vos propres écritures
Voici la partie qui répond à la question d'ouverture. Dans Zaris, les lectures routent vers le propriétaire actif, le même nœud où est allée votre écriture. Les clients ne lisent pas depuis les réplicas par défaut. Ainsi, quand vous PutAsync une clé puis la GetAsync, les deux requêtes se résolvent — via la même table de routage, construite à partir de la carte de partitions plus l'instantané de propriété du runtime — vers le même nœud propriétaire, et ce nœud a déjà votre écriture dans son store local.
C'est tout le mécanisme. La lecture de vos propres écritures n'est pas boulonnée comme une garantie de session ou un cache côté client ; elle découle du routage. Vous lisez vos écritures au sein d'une partition parce qu'un nœud unique est l'autorité pour les lectures et les écritures de cette partition à la fois, et les écritures vers lui sont sérialisées une à la fois. Le réplica dans ce schéma pourrait être en retard de quelques millisecondes sur le propriétaire — la réplication a une traîne asynchrone — mais par défaut vous ne lisez jamais le réplica, donc ce retard vous est invisible.
Le qualificatif « au sein d'une partition » porte le poids, et nous reviendrons sur ce qu'il exclut.
La version que vous êtes autorisé à voir
Chaque valeur que le propriétaire sert porte deux nombres monotones, et ce sont eux qui font de « la même valeur » une affirmation précise plutôt qu'une impression :
- une
Revisionappartenant au store — un numéro de séquence strictement croissant que le propriétaire estampille sur chaque mutation dans l'ordre de validation, et - une
ItemVersionpar élément — la version de cette clé spécifique, qui est ce queIfMatchVersionprotège dans une écriture par comparaison-et-échange.
Parce que le propriétaire les assigne dans l'ordre de validation et sert les lectures depuis le même état validé, des lectures successives d'une clé depuis le propriétaire font avancer la version vers l'avant, jamais vers l'arrière. Vous ne lirez pas la version 42, puis la version 40 un instant plus tard. Cette monotonie est exactement ce qui permet à la boucle CAS d'être correcte : un lecteur voit une version, calcule contre elle, et réécrit conditionné au fait qu'elle soit encore cette version. Si le modèle laissait les lectures osciller entre versions, la concurrence optimiste serait bâtie sur du sable. Un propriétaire, des révisions ordonnées par validation, des lectures servies par le propriétaire — c'est le fondement sur lequel repose toute l'histoire de coordination de l'article sur le CAS.
La fenêtre où une lecture dit honnêtement « pas encore »
Les lectures routées vers le propriétaire sont propres en régime permanent. Les moments intéressants sont les transitions — un basculement, une migration, un primaire préféré qui est rendu — car pendant un transfert, le nœud vers lequel vous routeriez peut ne pas être actuellement l'autorité. Le choix de Zaris ici est l'honnête : plutôt que de servir une lecture depuis un nœud qui n'est pas le propriétaire stabilisé, il renvoie un statut réessayable. Une lecture (ou écriture) pendant une telle fenêtre peut revenir sous la forme :
Moved— votre table de routage est périmée ; la clé vit sur un nœud différent maintenant.OwnershipChanging— la propriété de ce segment est activement en transition en ce moment ; c'est un transfert, pas un bug de routage établi.Migrating— le segment est en cours de migration vers un nouveau propriétaire.ReplicaSyncPending— ce nœud met à jour la copie d'un segment et ne servira pas encore une lecture possiblement incomplète depuis celui-ci.Unavailable— la partition n'a aucun propriétaire joignable à cet instant.
Aucun de ceux-ci n'est une exception que vous devez extraire d'une trace de pile — ce sont des résultats lisibles par machine, et le client .NET les traite comme transitoires et réessaie avec une redirection, rafraîchissant sa carte du cluster et allant vers celui que la carte désigne désormais comme propriétaire. L'effet est que votre GetAsync prend simplement quelques millisecondes de plus puis renvoie la bonne valeur. La décision de conception sous-jacente mérite d'être énoncée clairement : Zaris préfère vous faire attendre un battement plutôt que de vous remettre une lecture dont il n'est pas sûr qu'elle soit autoritaire. Un « pas encore » réessayable est une meilleure réponse qu'une réponse confiante et fausse.
C'est aussi pourquoi la lecture de vos propres écritures survit à un basculement : le travail de barrière d'écriture signifie qu'une écriture acquittée n'est pas laissée pour compte dans la fenêtre de transfert (nous en viendrons à la limite honnête de cela ci-dessous), et la promotion ne fait d'un nœud le propriétaire qu'une fois qu'il est éligible — de sorte que la lecture qui redirige après un basculement redirige vers un nœud qui a vos données.
Le cadran que vous pouvez tourner — et ce qu'il fait réellement aujourd'hui
Zaris expose une surface de cohérence de lecture. GetOptions porte une ReadConsistency de Leader, Quorum ou Any, et un store a un drapeau AllowReplicaReads (désactivé par défaut). L'intention est le compromis classique : laisser les lectures être servies depuis les réplicas pour répartir la charge de lecture, en échange de l'acceptation qu'un réplica puisse être en retard sur le propriétaire — une péremption bornée à laquelle vous optez délibérément.
Voici l'état actuel honnête, car c'est exactement le genre de chose qu'un texte marketing flouterait : aujourd'hui, les lectures sont réservées au propriétaire. AllowReplicaReads et ReadConsistency.Any sont une surface de configuration et de mise en forme de requête qui existe et transite par la définition du store, mais il n'y a pas encore de chemin de code serveur qui serve réellement une lecture depuis un réplica. En pratique, cela signifie que vous obtenez l'extrémité forte du cadran dès maintenant — chaque lecture est une lecture du propriétaire, donc la lecture de vos propres écritures tient — et que vous n'obtenez pas actuellement le comportement de lecture-de-réplica-périmée, même si vous basculez le drapeau.
Pourquoi mentionner un cadran effectivement épinglé sur un seul réglage ? Parce qu'il vous dit ce que le contrat de cohérence va vous laisser échanger, et parce que l'asymétrie compte quand vous raisonnez sur la péremption. Quand les lectures servies par réplica arriveront, activer AllowReplicaReads sera le moment où vous cesseriez d'être garanti de lire vos propres écritures : une lecture routée vers un réplica peut observer une version en retard sur celle du propriétaire, aussi longtemps que la traîne de réplication asynchrone est en retard. D'ici là, la garantie est plus simple et plus forte que le bouton ne le suggère. Nous préférons que vous le sachiez plutôt que de supposer que vous répartissez déjà les lectures sur les réplicas alors que ce n'est pas le cas.
Ce que « acquitté » signifie pour le prochain lecteur
La lecture de vos propres écritures concerne vous qui lisez votre propre écriture sur le propriétaire. Une question différente et plus tranchante est : une fois que le propriétaire vous dit qu'une écriture est acquittée, cette écriture est-elle sûre si le propriétaire meurt ensuite, de sorte que le lecteur qui redirige vers le nouveau propriétaire la voit toujours ?
Le contrat d'acquittement honnête d'aujourd'hui : une écriture est acquittée quand elle atterrit dans le store local du propriétaire actif. La réplication vers les RF−1 réplicas suit une traîne asynchrone derrière cet acquittement. Les conséquences valent la peine d'être planifiées plutôt qu'éludées :
- À redondance 1 — aucun réplica synchronisé — une écriture est véritablement au mieux. Elle ne doit pas être traitée comme durable face à une panne unique, car il y a exactement une copie.
- Même avec des réplicas, si l'unique propriétaire actif est perdu dans la fenêtre étroite après qu'il a acquitté une écriture mais avant que cette écriture n'atteigne un réplica, l'écriture acquittée peut être perdue.
La propriété plus forte — acquitter seulement une fois que l'écriture est sur au moins un réplica synchronisé, de sorte qu'une écriture acquittée survive à la perte de n'importe quel nœud — est la cible conçue (la « barrière d'acquittement W1 »), pas encore la sémantique d'acquittement livrée. C'est la direction vers laquelle le travail sur l'épine dorsale de durabilité se consolide, et cet article est prudent sur la même distinction : l'affirmation forte qu'il valide est limitée aux écritures acquittées, lorsqu'un réplica synchronisé existe, sous les classes de pannes que l'exécution de chaos provoque — et non un « rien ne peut jamais être perdu » inconditionnel. La mécanique du journal d'opérations et de la migration est la façon dont un réplica ou un nouveau propriétaire rattrape l'état validé du propriétaire en premier lieu ; la barrière d'acquittement concerne le moment où le propriétaire est autorisé à dire « terminé ».
Donc l'énoncé précis côté lecteur est : la lecture de vos propres écritures sur le propriétaire est solide ; la durabilité inter-pannes d'une écriture acquittée est forte-et-de-plus-en-plus-forte, avec la réserve actuelle énoncée ci-dessus, et un plancher dur au RF1 qu'aucun store ne peut concevoir pour l'éliminer — une copie est une copie.
À travers les partitions, il n'y a pas d'horloge partagée
Le dernier endroit où le qualificatif « au sein d'une partition » mord : il n'y a pas d'ordre global ni d'instantané à travers les partitions. Chaque partition est sérialisée à travers son propre propriétaire ; deux partitions différentes sont deux propriétaires différents avec deux séquences de révision indépendantes. Si vous écrivez la clé A (partition 1) puis la clé B (partition 2), un lecteur concurrent peut observer la nouvelle valeur de B et l'ancienne de A, ou vice versa — il n'y a pas d'instant à l'échelle du cluster où « les deux se sont produits ». Les opérations en masse et par lot sont par clé en dessous ; elles sont pratiques, pas atomiques.
Quand vous avez véritablement besoin qu'un invariant s'étende sur plusieurs clés — déplacer un solde de A à B, maintenir un index au pas avec l'enregistrement qu'il pointe — c'est exactement à cela que servent les transactions multi-clés. Elles fournissent une isolation Serializable sur une validation coordonnée par le client à travers des versions optimistes, et elles sont la seule surface d'atomicité inter-clés. Les lectures et écritures multi-clés ordinaires ne sont pas une transaction et ne doivent pas être confondues avec une.
Ce qui est, et n'est pas, garanti
Énoncé clairement, pour que vous puissiez tenir tout le modèle d'un coup :
| Propriété | Zaris aujourd'hui |
|---|---|
| Propriétaire par partition | Exactement un propriétaire actif, tenu comme un bail durable. |
| Chemin de lecture | Route vers le propriétaire actif ; réplicas non lus par défaut. |
| Lecture de vos propres écritures | Oui, au sein d'une partition — le même propriétaire sert votre écriture et votre lecture. |
| Lectures monotones d'une clé | Oui — le propriétaire sert des révisions ordonnées par validation ; les versions avancent. |
| Lectures pendant un transfert | Statut réessayable (Moved / OwnershipChanging / Migrating / ReplicaSyncPending), le client réessaie et redirige. |
| Lectures de réplica / cadran de péremption | AllowReplicaReads + ReadConsistency.Any existent comme surface ; réservé au propriétaire en pratique aujourd'hui (pas encore de chemin servant par réplica). |
| Durabilité de l'acquittement | Locale au propriétaire aujourd'hui ; durable-face-à-une-panne-unique-avec-réplica-synchronisé est la cible (barrière d'acquittement W1). |
| Ordre inter-partitions | Aucun — pas d'instantané global ; utilisez les transactions pour l'atomicité inter-clés. |
| Linéarisabilité globale | Non revendiquée. |
La forme honnête de la chose
Un bon modèle de cohérence n'est pas celui avec la plus longue liste de garanties ; c'est celui dont vous pouvez réellement nommer les garanties et dont vous pouvez réellement voir les limites. Zaris trace sa frontière autour de la propriété et est franc sur le compromis : vous obtenez la lecture de vos propres écritures et des lectures monotones au sein d'une partition gratuitement, parce qu'un propriétaire sert les deux côtés et vous remet des versions ordonnées par validation ; vous obtenez un « pas encore » réessayable au lieu d'une réponse confiante et fausse pendant un transfert ; et vous abandonnez la linéarisabilité globale et l'ordre inter-partitions, que vous rachetez explicitement avec une transaction quand un invariant particulier l'exige.
Les deux réserves à garder en tête sont celles que cet article a énoncées plutôt que lissées : le cadran de lecture de réplica est, aujourd'hui, effectivement épinglé sur le réglage fort, de sorte que vous êtes plus cohérent que le bouton ne le laisse croire ; et « acquitté » signifie actuellement sur le propriétaire, avec la durabilité complète face à une panne unique comme cible que le travail de durabilité est en train de livrer, et un vrai plancher à la redondance 1. Connaissez ces deux choses et le reste du modèle est exactement aussi simple qu'il en a l'air : trouvez le propriétaire, lisez vos écritures.
Pour la conception plus profonde, voyez Cohérence et Partitionnement et réplication dans la documentation.