<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Clustron Blog</title>
        <link>https://clustron.io/fr/blog/</link>
        <description>Clustron Blog</description>
        <lastBuildDate>Mon, 05 Oct 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>fr</language>
        <item>
            <title><![CDATA[Beaucoup de clés, peu d'allers-retours : comment Zaris éventaille une opération en masse]]></title>
            <link>https://clustron.io/fr/blog/bulk-and-batch-fan-out/</link>
            <guid>https://clustron.io/fr/blog/bulk-and-batch-fan-out/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un GetMany sur un millier de clés ne devrait pas coûter un millier d'allers-retours réseau. Zaris groupe les clés par leur nœud propriétaire, envoie une requête par propriétaire en parallèle, et recoud les réponses dans l'ordre de l'appelant. Voici le chemin exact de l'éventail, pourquoi chaque item porte un index, et pourquoi une opération en masse n'est délibérément pas une transaction.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Éventailler une opération en masse vers ses nœuds propriétaires" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJGYW5uaW5nIG91dCBhIGJ1bGsgb3BlcmF0aW9uIGFjcm9zcyBvd25pbmcgbm9kZXMgaW4gWmFyaXMiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5QRVJGT1JNQU5DRTwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjEyNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij5NYW55IGtleXMsPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPmZldyByb3VuZC10cmlwczwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPkZhbm5pbmcgYSBidWxrIG9wZXJhdGlvbiBvdXQgdG8gaXRzIG93bmVyczwvdGV4dD4NCg0KICA8IS0tIGNsaWVudCBidWNrZXQgLS0+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY0MCwxMjApIj4NCiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjIzMCIgcng9IjEwIiBmaWxsPSIjMGMxYjE2IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iNzUiIHk9IjMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Y2xpZW50PC90ZXh0Pg0KICAgIDxsaW5lIHgxPSIxNiIgeTE9IjQyIiB4Mj0iMTM0IiB5Mj0iNDIiIHN0cm9rZT0iIzFkM2EzMCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4NCiAgICA8dGV4dCB4PSIxOCIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjY2RlZWUwIj5rMCAgazEgIGsyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjE4IiB5PSI5MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiNjZGVlZTAiPmszICBrNCAgazU8L3RleHQ+DQogICAgPHRleHQgeD0iMTgiIHk9IjExNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiNjZGVlZTAiPms2ICBrNyAgazg8L3RleHQ+DQogICAgPHRleHQgeD0iMTgiIHk9IjE0MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiNjZGVlZTAiPi4uLiAga8KtOTk5PC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijc1IiB5PSIxNzgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiBmaWxsPSIjNWE2YjY1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5DUkMxNiAmIzg1OTQ7IHNsb3Q8L3RleHQ+DQogICAgPHRleHQgeD0iNzUiIHk9IjE5NiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM1YTZiNjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPiYjODU5NDsgc2VnbWVudDwvdGV4dD4NCiAgICA8dGV4dCB4PSI3NSIgeT0iMjE0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzVhNmI2NSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+JiM4NTk0OyBvd25lcjwvdGV4dD4NCiAgPC9nPg0KDQogIDwhLS0gZmFuLW91dCBsaW5lcyAtLT4NCiAgPGcgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgb3BhY2l0eT0iMC44Ij4NCiAgICA8cGF0aCBkPSJNNzkwIDE3NSBDIDg4MCAxNzUsIDkwMCAxMTAsIDk5MCAxMTAiLz4NCiAgICA8cGF0aCBkPSJNNzkwIDIzNSBDIDg4MCAyMzUsIDkwMCAyMzUsIDk5MCAyMzUiLz4NCiAgICA8cGF0aCBkPSJNNzkwIDI5NSBDIDg4MCAyOTUsIDkwMCAzNjAsIDk5MCAzNjAiLz4NCiAgPC9nPg0KDQogIDwhLS0gb3duaW5nIG5vZGVzIC0tPg0KICA8ZyBjbGFzcz0ic2FucyI+DQogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTkwLDgwKSI+DQogICAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjYwIiByeD0iOCIgZmlsbD0iIzBjMWIxNiIgc3Ryb2tlPSIjMmJkNmEwIiBzdHJva2Utd2lkdGg9IjEuNSIvPg0KICAgICAgPHRleHQgeD0iMTYiIHk9IjI2IiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjY2RlZWUwIj5ub2RlIEE8L3RleHQ+DQogICAgICA8dGV4dCB4PSIxNiIgeT0iNDciIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM3ZmUzYmYiPjEgcmVxdWVzdDwvdGV4dD4NCiAgICA8L2c+DQogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTkwLDIwNSkiPg0KICAgICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI2MCIgcng9IjgiIGZpbGw9IiMwYzFiMTYiIHN0cm9rZT0iIzJiZDZhMCIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4NCiAgICAgIDx0ZXh0IHg9IjE2IiB5PSIyNiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2NkZWVlMCI+bm9kZSBCPC90ZXh0Pg0KICAgICAgPHRleHQgeD0iMTYiIHk9IjQ3IiBmb250LXNpemU9IjEyIiBmaWxsPSIjN2ZlM2JmIj4xIHJlcXVlc3Q8L3RleHQ+DQogICAgPC9nPg0KICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDk5MCwzMzApIj4NCiAgICAgIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxNTAiIGhlaWdodD0iNjAiIHJ4PSI4IiBmaWxsPSIjMGMxYjE2IiBzdHJva2U9IiMyYmQ2YTAiIHN0cm9rZS13aWR0aD0iMS41Ii8+DQogICAgICA8dGV4dCB4PSIxNiIgeT0iMjYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiNjZGVlZTAiPm5vZGUgQzwvdGV4dD4NCiAgICAgIDx0ZXh0IHg9IjE2IiB5PSI0NyIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzdmZTNiZiI+MSByZXF1ZXN0PC90ZXh0Pg0KICAgIDwvZz4NCiAgPC9nPg0KDQogIDx0ZXh0IHg9IjY0MCIgeT0iMzkyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzVhNmI2NSI+MTAwMCBrZXlzICYjMTgzOyAzIG93bmVycyAmIzg1OTQ7IDMgcm91bmQtdHJpcHMsIGluIHBhcmFsbGVsPC90ZXh0Pg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Demandez une clé à un store distribué et le coût est dominé par un seul nombre : l'aller-retour vers le nœud qui la possède. La recherche elle-même est de l'ordre de la nanoseconde ; le réseau est de l'ordre de la microseconde. Demandez maintenant un millier de clés. Si vous bouclez et appelez <code>Get</code> un millier de fois, vous payez cet aller-retour un millier de fois, en série, et le store passe presque tout le temps d'horloge à rester inactif en attendant des paquets. Les clés pourraient être réparties sur quatre nœuds qui auraient pu répondre en parallèle — mais une boucle naïve ne leur en donne jamais la chance.</p>
<p>Zaris a trois API en masse — <code>GetManyAsync</code>, <code>PutManyAsync</code>, <code>DeleteManyAsync</code> — et un <code>ExecuteBatchAsync</code> mixte en dessous, et toute leur raison d'exister est de transformer « un millier de clés » en « une requête par nœud propriétaire, émise en parallèle ». Cet article est le chemin de l'éventail de bout en bout : comment le client regroupe les clés par propriétaire, pourquoi les quatre API se réduisent à un seul moteur, comment un champ <code>Index</code> garde les réponses dans l'ordre à travers deux couches de concurrence, et pourquoi — contrairement à une <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transaction multi-clés</a> — une opération en masse est par item et le succès partiel est normal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-modèle-de-coût--des-allers-retours-pas-des-opérations">Le modèle de coût : des allers-retours, pas des opérations<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#le-mod%C3%A8le-de-co%C3%BBt--des-allers-retours-pas-des-op%C3%A9rations" class="hash-link" aria-label="Lien direct vers Le modèle de coût : des allers-retours, pas des opérations" title="Lien direct vers Le modèle de coût : des allers-retours, pas des opérations" translate="no">​</a></h2>
<p>Ce contre quoi une API en masse se bat n'est pas le CPU. Un <code>GET</code> à clé unique sur un nœud propriétaire est une recherche de dictionnaire — le <a class="" href="https://clustron.io/fr/blog/latency-floor/">plancher de latence</a> est d'une microseconde de travail réel dans dix de plomberie. La partie coûteuse est la plomberie : la requête doit traverser le réseau vers le propriétaire et la réponse doit revenir. Appelez ce temps d'aller-retour <code>R</code>, typiquement des dizaines à des centaines de microsecondes sur un réseau de datacenter, des ordres de grandeur de plus que la recherche.</p>
<p>Une boucle en série de <code>N</code> clés coûte environ <code>N × R</code>, car chaque appel attend que le paquet du précédent revienne avant d'envoyer le suivant. C'est le piège. La solution n'est pas une recherche plus rapide — c'est de cesser de payer <code>R</code> par clé. Si les <code>N</code> clés vivent sur <code>M</code> nœuds propriétaires, le plancher est <code>R</code> payé <code>M</code> fois <em>en concurrence</em>, c'est-à-dire environ un <code>R</code> au total quel que soit le nombre de clés que chaque nœud détient. Pour un millier de clés sur quatre nœuds, c'est la différence entre un millier d'allers-retours en série et en pratique un seul. Toute la conception ci-dessous existe pour réduire <code>N × R</code> à <code>~R</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-seul-moteur--tout-est-un-batch-ditems">Un seul moteur : tout est un batch d'items<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#un-seul-moteur--tout-est-un-batch-ditems" class="hash-link" aria-label="Lien direct vers Un seul moteur : tout est un batch d'items" title="Lien direct vers Un seul moteur : tout est un batch d'items" translate="no">​</a></h2>
<p>La première chose surprenante dans le client de cluster est que <code>GetManyAsync</code>, <code>PutManyAsync</code> et <code>DeleteManyAsync</code> ne sont pas trois implémentations distinctes. Chacune est une fine coquille qui transforme ses arguments en une liste de <code>KvBatchItem</code> et les remet à l'unique méthode qui fait le vrai travail, <code>ExecuteBatchAsync</code>. Voici <code>GetManyAsync</code> faisant exactement cela :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> list </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> keys</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">k</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">KvBatchItem</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Index </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> i</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">                 </span><span class="token comment" style="color:#999988;font-style:italic">// caller's position — the correlation key</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Op    </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> KvBatchOp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Get</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Key   </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> k</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    GetOptions </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> options</span><span class="token punctuation" style="color:#393A34">?.</span><span class="token function" style="color:#d73a49">ToGetOptions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> resp </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token generic-method function" style="color:#d73a49">ExecuteBatchAsync</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">T</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">KvBatchRequest</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Items </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> list</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        DefaultConsistency </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> options</span><span class="token punctuation" style="color:#393A34">?.</span><span class="token plain">Consistency </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> Defaults</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DefaultConsistency</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConfigureAwait</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>PutManyAsync</code> et <code>DeleteManyAsync</code> font de même avec <code>KvBatchOp.Put</code> et <code>KvBatchOp.Delete</code>. Donc la distinction que les exemples tracent entre <em>Bulk</em> (un seul type d'opération à la fois) et <em>Batch</em> (un mélange de PUT / GET / DELETE en un appel) est une commodité de surface : les helpers en masse ne sont qu'un batch où chaque item partage par hasard une op. Il y a exactement un moteur d'éventail, et il est agnostique à l'op.</p>
<p>Remarquez <code>Index = i</code>. Chaque item est estampillé de sa position dans la liste de l'appelant <em>avant</em> que quoi que ce soit ne soit groupé, réordonné ou envoyé. Cet estampillage est ce qui survit aux deux sauts concurrents à venir et permet de remettre la réponse finale dans l'ordre. Gardez-le en tête.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="léventail--regrouper-par-propriétaire-une-requête-chacun">L'éventail : regrouper par propriétaire, une requête chacun<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#l%C3%A9ventail--regrouper-par-propri%C3%A9taire-une-requ%C3%AAte-chacun" class="hash-link" aria-label="Lien direct vers L'éventail : regrouper par propriétaire, une requête chacun" title="Lien direct vers L'éventail : regrouper par propriétaire, une requête chacun" translate="no">​</a></h2>
<p>Voici le cœur de tout l'article — le dispatch de batch du client de cluster, pour l'essentiel en entier :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> table   </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _routing</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> buckets </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">Dictionary</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&lt;</span><span class="token constructor-invocation class-name">ZarisRemoteClient</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">,</span><span class="token constructor-invocation class-name"> List</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&lt;</span><span class="token constructor-invocation class-name">KvBatchItem</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&gt;</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">foreach</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> item </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> batch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Items</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> seg    </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> SegmentMapper</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ComputeSegmentId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">item</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TotalSegments</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GetClientForSegment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">buckets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryGetValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">client</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">out</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> list</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        buckets</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">client</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> list </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">List</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&lt;</span><span class="token constructor-invocation class-name">KvBatchItem</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    list</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">item</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> tasks </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> buckets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">kv </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    kv</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Key</span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">ExecuteBatchAsync</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">T</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">KvBatchRequest</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Items </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> kv</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        DefaultConsistency </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> batch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DefaultConsistency</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> perOwner </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> Task</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">WhenAll</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tasks</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConfigureAwait</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">KvBatchResponse</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> perOwner</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">SelectMany</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Results</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                      </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">OrderBy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Index</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                      </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Lisez-le comme quatre manœuvres :</p>
<ol>
<li class="">
<p><strong>Résoudre chaque clé vers son propriétaire, localement.</strong> <code>SegmentMapper.ComputeSegmentId</code> est la même fonction de placement qu'utilise le chemin à clé unique — CRC16 sur le hash-tag, modulo le nombre de segments — et <code>GetClientForSegment</code> mappe ce segment à la connexion déjà ouverte vers son nœud propriétaire. Pas de proxy, pas de coordinateur : le client calcule la possession lui-même, exactement comme décrit dans <a class="" href="https://clustron.io/fr/blog/client-partition-aware-routing/">un saut vers le propriétaire</a>. La différence est qu'ici il le fait pour chaque clé de l'ensemble d'un coup.</p>
</li>
<li class="">
<p><strong>Regrouper.</strong> Les clés possédées par le même nœud s'empilent dans la liste de ce nœud. Un millier de clés réparties sur quatre propriétaires deviennent quatre listes. C'est là que <code>N</code> opérations deviennent <code>M</code> requêtes.</p>
</li>
<li class="">
<p><strong>Dispatcher en concurrence.</strong> <code>buckets.Select(...)</code> construit un appel <code>ExecuteBatchAsync</code> par propriétaire et <code>Task.WhenAll</code> les laisse tous s'envoler d'un coup. Quatre nœuds répondent en parallèle ; le temps d'horloge est la réponse du nœud le plus lent, pas la somme.</p>
</li>
<li class="">
<p><strong>Recombiner dans l'ordre.</strong> Chaque propriétaire renvoie sa tranche de résultats ; <code>SelectMany</code> les aplatit et <code>OrderBy(r =&gt; r.Index)</code> restaure l'ordre original de l'appelant. L'appelant récupère une liste dont la position <em>i</em> est la réponse pour la clé <em>i</em>, même si les clés ont voyagé vers des nœuds différents et sont revenues dans l'ordre où le réseau les a livrées.</p>
</li>
</ol>
<!-- -->
<p>C'est la revendication d'amortissement complète rendue concrète : le nombre d'allers-retours égale le nombre de <em>nœuds propriétaires distincts</em> dans l'ensemble de clés, pas le nombre de clés.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="à-lintérieur-dun-propriétaire--concurrent-à-nouveau-ordonné-à-nouveau">À l'intérieur d'un propriétaire : concurrent à nouveau, ordonné à nouveau<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#%C3%A0-lint%C3%A9rieur-dun-propri%C3%A9taire--concurrent-%C3%A0-nouveau-ordonn%C3%A9-%C3%A0-nouveau" class="hash-link" aria-label="Lien direct vers À l'intérieur d'un propriétaire : concurrent à nouveau, ordonné à nouveau" title="Lien direct vers À l'intérieur d'un propriétaire : concurrent à nouveau, ordonné à nouveau" translate="no">​</a></h2>
<p>Chaque sous-batch par propriétaire atterrit sur le <code>BatchCommandHandler</code> de ce nœud. Il n'exécute pas non plus les items l'un après l'autre — il les exécute tous d'un coup puis retrie :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> items </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> batchReq</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Items</span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">Cast</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">KvBatchItemWire</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToArray</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Execute every item in the sub-batch concurrently.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> tasks   </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> items</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">item </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ExecuteItemAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">item</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToArray</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> Task</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">WhenAll</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tasks</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConfigureAwait</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">ZarisResponse</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Payload </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">BatchResponsePayload</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">KvBatchResponse</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            Results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> results</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">OrderBy</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Index</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><code>ExecuteItemAsync</code> est un petit switch sur l'op qui dispatche chaque item vers le store — <code>GetRawAsync</code>, <code>PutRawAsync</code> ou <code>DeleteRawAsync</code> — de sorte qu'un batch mixte de lectures et d'écritures est traité item par item à l'intérieur de la même requête. (Pour les routeurs en processus et mono-nœud, la même forme apparaît comme un <code>Parallel.ForEachAsync</code> sur les clés, borné par <code>MaxDegreeOfParallelism</code> — une option <code>MaxParallelism</code> ou <code>Environment.ProcessorCount</code> par défaut — ce qui est pourquoi <a class="" href="https://clustron.io/fr/blog/reads-scale-with-cores/">les lectures montent avec les cœurs</a> : un gros sous-batch garde chaque cœur occupé au lieu d'un seul.)</p>
<p>Donc l'estampillage <code>Index</code> gagne sa place <em>deux fois</em>. La concurrence brouille l'ordre sur le nœud, donc le handler trie par <code>Index</code> avant de répondre. L'éventail brouille l'ordre sur le client, donc le client trie par <code>Index</code> après la fusion. L'appelant ne voit jamais l'un ni l'autre brouillage. Un petit entier, porté sur chaque item dès l'instant où la requête est construite, est tout le mécanisme qui rend « tout balancer au cluster en parallèle » sûr à exposer comme une liste ordonnée.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="une-opération-en-masse-nest-pas-une-transaction">Une opération en masse n'est pas une transaction<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#une-op%C3%A9ration-en-masse-nest-pas-une-transaction" class="hash-link" aria-label="Lien direct vers Une opération en masse n'est pas une transaction" title="Lien direct vers Une opération en masse n'est pas une transaction" translate="no">​</a></h2>
<p>C'est la distinction que les gens se trompent le plus souvent, alors elle vaut la peine d'être énoncée sans détour. Chaque item d'un batch porte son <em>propre</em> résultat. Le type de résultat par item enregistre le succès, le statut et — pour les opérations qui en ont — la révision, la version et le TTL de cette seule clé :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name">KvBatchItemResult</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">MapKvToBatch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> index</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">KvBatchOp</span><span class="token plain"> op</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">KvResult</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Index   </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> index</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Op      </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> op</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Success </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">IsSuccess</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Status  </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Status</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">          </span><span class="token comment" style="color:#999988;font-style:italic">// e.g. Success, NotFound, Moved</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Revision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Revision</span><span class="token punctuation" style="color:#393A34">?.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ItemVersion </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version</span><span class="token punctuation" style="color:#393A34">?.</span><span class="token plain">Version</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Ttl     </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TimeToLive</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Il n'y a pas de point de commit partagé. Un <code>GetMany</code> sur une centaine de clés dont trente n'existent pas renvoie soixante-dix résultats <code>Success</code> et trente résultats <code>NotFound</code>, entrelacés par index — pas une erreur, pas une liste vide. Un <code>PutMany</code> où le segment d'une clé est en pleine migration renvoie cet unique item avec le statut <code>Moved</code> et le reste <code>Success</code>. Le succès partiel est la forme normale et attendue de la réponse, et l'appelant est censé inspecter chaque item.</p>
<p>C'est l'exact opposé d'une <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transaction multi-clés</a>, qui est tout-ou-rien par construction : un commit à deux phases coordonné par le client sur des versions optimistes où soit chaque clé bouge, soit aucune. Les deux fonctionnalités se ressemblent de l'extérieur — toutes deux prennent de nombreuses clés en un appel — et sont bâties pour des besoins opposés. Optez pour un batch quand les clés sont indépendantes et que vous voulez du débit ; optez pour une transaction quand un invariant s'étend sur les clés et qu'un résultat à moitié appliqué le corromprait. Zaris les garde comme des API séparées précisément pour que la sémantique ne soit jamais ambiguë : un batch ne fait jamais silencieusement un rollback, et une transaction ne commit jamais silencieusement à moitié.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="survivre-à-une-possession-qui-bouge-en-plein-batch">Survivre à une possession qui bouge en plein batch<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#survivre-%C3%A0-une-possession-qui-bouge-en-plein-batch" class="hash-link" aria-label="Lien direct vers Survivre à une possession qui bouge en plein batch" title="Lien direct vers Survivre à une possession qui bouge en plein batch" translate="no">​</a></h2>
<p>Une opération en masse est une cible plus large qu'un seul <code>GET</code> pour le problème de la possession qui change en dessous. Entre le moment où le client regroupe une clé vers le nœud A et le moment où le nœud A traite le sous-batch, un <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">cut-over de partition</a> peut avoir déplacé ce segment vers le nœud B. Le nœud A répondra à ces items avec le statut <code>Moved</code> plutôt que de deviner.</p>
<p>Le client de cluster enveloppe tout l'éventail dans un réessai borné pour exactement cela :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">override</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token return-type class-name">Task</span><span class="token return-type class-name punctuation" style="color:#393A34">&lt;</span><span class="token return-type class-name">KvBatchResponse</span><span class="token return-type class-name punctuation" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token generic-method function" style="color:#d73a49">ExecuteBatchAsync</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">T</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name">KvBatchRequest</span><span class="token plain"> batch</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">CancellationToken</span><span class="token plain"> ct </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> RetryHelper</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">RetryAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token generic-method function" style="color:#d73a49">ExecuteBatchClusterAsync</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">T</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">batch</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token named-parameter punctuation" style="color:#393A34">maxAttempts</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token named-parameter punctuation" style="color:#393A34">backoffMillis</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token named-parameter punctuation" style="color:#393A34">logger</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> _logger</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConfigureAwait</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Lors d'un réessai, la table de routage s'est généralement rafraîchie, donc le re-regroupement envoie les clés déplacées vers leur nouveau propriétaire. Le réessai est délibérément peu profond — deux tentatives, un bref backoff — car le but est de survivre à une table de routage momentanément périmée, pas de masquer un nœud réellement en panne. C'est la même discipline décrite dans <a class="" href="https://clustron.io/fr/blog/retries-under-ownership-churn/">les réessais sous churn de possession</a>, appliquée à tout un ensemble de clés d'un coup : le réessai réexécute le <em>regroupement</em>, pas juste l'envoi, de sorte que chaque tentative reflète la possession la plus récente plutôt que de rejouer un plan périmé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-vrai-bug-parce-que-la-forme-de-fil-a-deux-formes">Un vrai bug, parce que la forme de fil a deux formes<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#un-vrai-bug-parce-que-la-forme-de-fil-a-deux-formes" class="hash-link" aria-label="Lien direct vers Un vrai bug, parce que la forme de fil a deux formes" title="Lien direct vers Un vrai bug, parce que la forme de fil a deux formes" translate="no">​</a></h2>
<p>Il vaut la peine de montrer un endroit où ce chemin a cassé, car il dit quelque chose de spécifique aux opérations en masse. La valeur du résultat d'un <code>GET</code> peut arriver sur le fil sous deux formes — enveloppée dans une enveloppe <code>ZarisEntry</code>, ou comme une valeur sérialisée brute — selon la façon dont elle a été stockée (voir <a class="" href="https://clustron.io/fr/blog/what-crosses-the-wire/">ce qui traverse le fil</a>). Le décodeur de bulk-GET d'origine supposait seulement la forme enveloppée et levait une exception sur la forme brute.</p>
<p>Pour un seul <code>GET</code> c'est un échec à une clé. Pour un <code>GetMany</code> c'était catastrophique d'une façon subtile : <em>chaque</em> clé du batch échouait, car le seul encodage non géré levait avant qu'aucun résultat ne soit décodé. Une exécution de certification l'a signalé comme « échecs de bulk get », et la correction a été de router le décodage à travers un helper partagé de double encodage qui gère les deux formes :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Decode through the shared helper (handles both the ZarisEntry-wrapper and</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// the raw-value forms the server can emit). The old inline code assumed only</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// the wrapper and threw, failing every key of a bulk GET.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> obj </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> KvBatchItemResultExtensions</span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">GetValue</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">T</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La leçon qui se généralise : dans un chemin en masse, un défaut dans la machinerie <em>partagée</em> par item est multiplié par la taille du batch, et une erreur qui est une gêne pour une clé est une panne pour un millier. Le modèle de résultat par item est ce qui contient le rayon d'explosion — une fois le décodage correct, une valeur malformée échoue un item et laisse les 999 autres intacts.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-léventail-achète">Ce que l'éventail achète<a href="https://clustron.io/fr/blog/bulk-and-batch-fan-out/#ce-que-l%C3%A9ventail-ach%C3%A8te" class="hash-link" aria-label="Lien direct vers Ce que l'éventail achète" title="Lien direct vers Ce que l'éventail achète" translate="no">​</a></h2>
<p>Mettez les pièces ensemble et la forme du gain est claire. Un appel en masse estampille chaque item de son index, résout le propriétaire de chaque clé avec la même arithmétique de placement qu'utilise le chemin à clé unique, regroupe les clés pour que <code>N</code> opérations deviennent une requête par nœud propriétaire, tire ces requêtes en concurrence, et réassemble les réponses dans l'ordre de l'appelant — survivant à une table de routage qui se décale en plein vol et rapportant le sort de chaque clé indépendamment. Le nombre d'allers-retours tombe du nombre de clés au nombre de nœuds, qui est le seul nombre qui allait jamais compter.</p>
<p>Rien de tout cela n'a exigé un nouveau chemin de stockage ni un nouvel enregistrement de fil. Les API en masse sont une couche d'<em>ordonnancement</em> par-dessus les primitives que Zaris possède déjà : le <a class="" href="https://clustron.io/fr/blog/client-partition-aware-routing/">placement conscient des partitions</a> qui dit au client qui possède quoi, le <a class="" href="https://clustron.io/fr/blog/what-crosses-the-wire/">contrat par enregistrement</a> que chaque résultat d'item réutilise, et l'<a class="" href="https://clustron.io/fr/blog/reads-scale-with-cores/">exécution parallèle sur les cœurs</a> qui mâche un sous-batch une fois qu'il a atterri. La partie difficile de l'accélération sur de nombreuses clés n'a jamais été les clés — c'était de refuser de payer le réseau une clé à la fois.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Un saut vers le propriétaire : comment le client Zaris route une clé]]></title>
            <link>https://clustron.io/fr/blog/client-partition-aware-routing/</link>
            <guid>https://clustron.io/fr/blog/client-partition-aware-routing/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un client Zaris n'envoie pas votre GET à un load balancer ni à un proxy qui le relaie. Il calcule lui-même le propriétaire de la clé — CRC16 sur le hash-tag, mod 16384, slot vers segment, segment vers le nœud propriétaire — et ouvre la socket droit vers ce nœud. Voici le chemin de routage exact, l'unique fonction de placement que le client et le serveur partagent, et les trois façons dont le client garde sa map fraîche quand la possession bouge sous ses pieds.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Comment le client Zaris route une clé vers son propriétaire" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJIb3cgdGhlIFphcmlzIGNsaWVudCBmaW5kcyB0aGUgb3duZXI6IHBhcnRpdGlvbi1hd2FyZSByb3V0aW5nIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+Q0xJRU5UIFJPVVRJTkc8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+T25lIGhvcCB0byB0aGUgb3duZXIsPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPm5vIHByb3h5IGluIGJldHdlZW48L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIyMjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5DUkMxNihrZXkpICYjODU5NDsgc2xvdCAmIzg1OTQ7IHNlZ21lbnQgJiM4NTk0OyBvd25lciBub2RlICYjODU5NDsgZGlyZWN0IHNlbmQ8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwLDI4NikiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjIyIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjU4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMyIvPg0KICAgIDx0ZXh0IHg9Ijc1IiB5PSI0NyIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmNsaWVudDwvdGV4dD4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5DUkMxNiArIG1hcDwvdGV4dD4NCiAgICA8cGF0aCBkPSJNMTUwIDUxIEw4NTggNTEiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiLz4NCiAgICA8Y2lyY2xlIGN4PSIzNjAiIGN5PSI1MSIgcj0iNyIgZmlsbD0iIzJiZDZhMCIvPg0KICAgIDxjaXJjbGUgY3g9IjU2MCIgY3k9IjUxIiByPSI3IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iNzYwIiBjeT0iNTEiIHI9IjciIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8dGV4dCB4PSIzNjAiIHk9IjI5IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+c2xvdCAlIDE2Mzg0PC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjU2MCIgeT0iMjkiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5TZWdtZW50TWFwW3NlZ108L3RleHQ+DQogICAgPHRleHQgeD0iNzYwIiB5PSIyOSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPm93bmVyLWRpcmVjdDwvdGV4dD4NCiAgICA8cmVjdCB4PSI4NjAiIHk9IjIyIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjU4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI5MzUiIHk9IjQ3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+b3duZXIgbm9kZTwvdGV4dD4NCiAgICA8dGV4dCB4PSI5MzUiIHk9IjY4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+c2VydmVzIHRoZSBrZXk8L3RleHQ+DQogICAgPHRleHQgeD0iNTAwIiB5PSIxMTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij50aGUgc2FtZSBwbGFjZW1lbnQgZnVuY3Rpb24gcnVucyBvbiB0aGUgY2xpZW50IGFuZCB0aGUgc2VydmVyPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Il y a deux façons honnêtes de construire un client de cache distribué. La première est de choisir n'importe quel nœud, de lui envoyer la requête, et de laisser le cluster la relayer en interne vers celui qui possède la clé — simple côté client, mais chaque requête qui atterrit sur le mauvais nœud paie un saut réseau supplémentaire à l'intérieur du cluster. La seconde est de rendre le <em>client</em> assez intelligent pour savoir quel nœud possède la clé avant d'envoyer quoi que ce soit, de sorte que la première socket qu'il touche soit celle qui peut réellement répondre. Zaris prend la seconde voie. Un <code>GET</code> quitte votre processus déjà adressé au nœud qui détient la clé — pas de proxy, pas de saut de relais, pas de coordinateur au milieu.</p>
<p>Cet article est la vue de l'intérieur de son fonctionnement : la fonction qui transforme une clé en propriétaire, la table de correspondance que le client garde en mémoire, et — la partie facile à rater — comment le client reste correct quand la possession bouge pendant que votre trafic est en vol.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-revendication-énoncée-précisément">La revendication, énoncée précisément<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#la-revendication-%C3%A9nonc%C3%A9e-pr%C3%A9cis%C3%A9ment" class="hash-link" aria-label="Lien direct vers La revendication, énoncée précisément" title="Lien direct vers La revendication, énoncée précisément" translate="no">​</a></h2>
<p>Quand vous appelez <code>client.GetAsync("user:42")</code>, le client fait trois choses avant qu'aucun octet n'atteigne le réseau :</p>
<ol>
<li class=""><strong>Hacher la clé en un segment.</strong> Une fonction pure mappe la chaîne de clé à l'un des segments du cluster.</li>
<li class=""><strong>Rechercher le propriétaire du segment.</strong> Un tableau en mémoire mappe ce segment directement à l'unique connexion qui atteint son nœud propriétaire actif.</li>
<li class=""><strong>Envoyer en direct au propriétaire.</strong> La requête passe par cette connexion et nulle part ailleurs.</li>
</ol>
<p>Aucune étape ne consulte un routeur central. L'étape 1 est de l'arithmétique, les étapes 2 et 3 sont un index de tableau et une écriture de socket. Le client détient une connexion par nœud et une petite table de routage, et c'est tout l'appareillage. L'ingénierie intéressante n'est pas le chemin heureux — c'est de garder cette table honnête.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-1--la-clé-devient-un-segment">Étape 1 — la clé devient un segment<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#%C3%A9tape-1--la-cl%C3%A9-devient-un-segment" class="hash-link" aria-label="Lien direct vers Étape 1 — la clé devient un segment" title="Lien direct vers Étape 1 — la clé devient un segment" translate="no">​</a></h2>
<p>Zaris hache les clés exactement comme le fait un client Redis Cluster, à dessein. La fonction de placement vit dans <code>SegmentMapper</code>, et son commentaire de doc est brutal sur pourquoi elle a l'air de ce qu'elle a l'air :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Maps a key to its segment. Placement is aligned to the Redis Cluster slot model so a Redis cluster client</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// (which routes by CRC16(key) % 16384) and Zaris agree on where a key lives: the key's Redis slot is</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// computed the exact Redis way (XMODEM CRC16 over the hash-tag, mod 16384), then the 16384 slots are mapped</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// proportionally onto the cluster's segments.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// This is the SINGLE shared placement function used by both the client and the server.</span><br></span></code></pre></div></div>
<p>Cette dernière ligne est celle qui compte le plus. Ce n'est pas « le client calcule le placement d'une façon et le serveur le valide d'une autre ». C'est le <em>même code</em>, compilé dans l'assembly client et l'assembly serveur, produisant le même segment pour la même clé à chaque fois. Si le client et le serveur étaient en désaccord sur l'endroit où vit une clé, chaque écriture serait un pile ou face entre atterrir sur le propriétaire et atterrir sur un nœud qui doit la rejeter. Partager la fonction rend cette classe de bug irreprésentable.</p>
<p>Le calcul lui-même est deux manœuvres :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">ushort</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ComputeSegmentId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> shardCount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token range operator" style="color:#393A34">..</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> slot </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ComputeSlot</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">                 </span><span class="token comment" style="color:#999988;font-style:italic">// CRC16(hash-tag) % 16384</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">SlotToSegment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">slot</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> shardCount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// proportional slot -&gt; segment</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// The Redis Cluster key slot: XMODEM CRC16 over the hash-tag (or the whole key), mod 16384.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">int</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ComputeSlot</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">start</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> len</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">HashTagRange</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Crc16</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> start</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> len</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">%</span><span class="token plain"> RedisSlots</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// RedisSlots = 16384</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>D'abord la clé est réduite à un <strong>slot</strong> dans la plage fixe <code>[0, 16384)</code> en utilisant XMODEM CRC16 — le même polynôme CRC16-CCITT (<code>0x1021</code>, init <code>0x0000</code>) qu'utilise Redis. Puis les 16384 slots sont mappés proportionnellement sur le nombre de segments que le cluster possède réellement :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">ushort</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">SlotToSegment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> slot</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> shardCount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">ushort</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">long</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">slot </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> shardCount </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> RedisSlots</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Deux slots peuvent atterrir dans le même segment ; un segment est une plage contiguë de slots. Cet alignement est ce qui permet à Zaris de répondre correctement à <code>CLUSTER SLOTS</code> pour un client Redis Cluster tout en routant par son propre modèle de segment en interne — les deux vues sont la même vue à des résolutions différentes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hash-tags--décider-ce-qui-se-co-localise">Hash tags : décider ce qui se co-localise<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#hash-tags--d%C3%A9cider-ce-qui-se-co-localise" class="hash-link" aria-label="Lien direct vers Hash tags : décider ce qui se co-localise" title="Lien direct vers Hash tags : décider ce qui se co-localise" translate="no">​</a></h3>
<p>Il y a une échappatoire délibérée dans le hachage des clés. Si une clé contient un groupe <code>{...}</code> avec au moins un caractère à l'intérieur des accolades, seuls les octets à l'intérieur des accolades sont hachés :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Redis hash tag: if the key contains "{...}" with at least one char between the braces, only that</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// substring is hashed (so "{u1}:a" and "{u1}:b" co-locate). Otherwise the whole key is hashed.</span><br></span></code></pre></div></div>
<p>C'est ainsi que vous forcez des clés apparentées sur le même segment. <code>{u1}:profile</code> et <code>{u1}:sessions</code> hachent toutes deux sur <code>u1</code>, donc elles partagent un segment et par conséquent un propriétaire — ce qui est exactement ce dont vous avez besoin pour une <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transaction multi-clés</a> ou toute opération qui doit toucher plusieurs clés atomiquement sur un nœud. Les clés sans tag s'étalent sur tout le keyspace. Le client et le serveur s'accordent là-dessus aussi, car c'est le même <code>HashTagRange</code> des deux côtés.</p>
<p>Un détail de plus à noter pour le chemin chaud : la boucle CRC16 a un chemin rapide tout-ASCII qui hache les caractères comme des octets sans allocation, et ne retombe sur un encodage UTF-8 que pour les clés non-ASCII. Router chaque opération signifie que cela s'exécute à chaque opération, donc il a été écrit pour ne pas allouer dans le cas courant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-2--le-segment-devient-une-connexion">Étape 2 — le segment devient une connexion<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#%C3%A9tape-2--le-segment-devient-une-connexion" class="hash-link" aria-label="Lien direct vers Étape 2 — le segment devient une connexion" title="Lien direct vers Étape 2 — le segment devient une connexion" translate="no">​</a></h2>
<p>Le client a maintenant un numéro de segment. Il lui faut le nœud qui le possède actuellement. Le client garde une <code>RoutingTable</code> immuable dont les champs sont toute l'histoire :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// one live connection per node, keyed by node id</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">ImmutableDictionary</span><span class="token return-type class-name punctuation" style="color:#393A34">&lt;</span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">,</span><span class="token return-type class-name"> ZarisRemoteClient</span><span class="token return-type class-name punctuation" style="color:#393A34">&gt;</span><span class="token plain"> Clients </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">init</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// segment -&gt; the active-owner client it routes to (the precomputed answer)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">ZarisRemoteClient</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token return-type class-name punctuation" style="color:#393A34">[</span><span class="token return-type class-name punctuation" style="color:#393A34">]</span><span class="token plain"> SegmentMap </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">init</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// segment -&gt; partition, and partition -&gt; owning node id</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">ushort</span><span class="token return-type class-name punctuation" style="color:#393A34">[</span><span class="token return-type class-name punctuation" style="color:#393A34">]</span><span class="token plain"> SegmentToPartition </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">init</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">ImmutableDictionary</span><span class="token return-type class-name punctuation" style="color:#393A34">&lt;</span><span class="token return-type class-name keyword" style="color:#00009f">int</span><span class="token return-type class-name punctuation" style="color:#393A34">,</span><span class="token return-type class-name"> </span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token return-type class-name punctuation" style="color:#393A34">&gt;</span><span class="token plain"> RuntimeOwners </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">init</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Le choix de conception clé est <code>SegmentMap</code>. C'est un tableau, indexé par segment, dont les entrées sont <em>déjà résolues</em> vers la connexion du propriétaire de ce segment. Router une clé n'est pas un parcours de graphe de segment à partition à nœud à connexion au moment de la requête — cette résolution est faite à l'avance quand la table est construite, de sorte que le coût par requête est un seul index de tableau :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">internal</span><span class="token plain"> </span><span class="token return-type class-name">ZarisRemoteClient</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GetClientForKey</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> table </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _routing</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GetClientForSegment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        SegmentMapper</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ComputeSegmentId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TotalSegments</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        table</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name">ZarisRemoteClient</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GetClientForSegment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">ushort</span><span class="token plain"> segment</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name">RoutingTable</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segment </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SegmentMap</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Length</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">InvalidOperationException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token interpolation-string string" style="color:#e3116c">$"No client mapped for segment </span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">segment</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c"> — map may be mid-update, retry"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">table</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SegmentMap</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">segment</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> partitionId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> segment </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SegmentToPartition</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Length</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">?</span><span class="token plain"> table</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SegmentToPartition</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">segment</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">PartitionUnavailableException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">partitionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> segment</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><code>Clients</code> détient exactement un <code>ZarisRemoteClient</code> — une connexion — par nœud. Le client n'ouvre pas une socket fraîche par requête ou par clé ; il multiplexe tout le trafic d'un nœud sur l'unique connexion de ce nœud, et <code>SegmentMap</code> pointe simplement de nombreux segments vers le même objet de connexion (l'égalité de référence les déduplique quand le client a besoin de l'ensemble distinct des propriétaires actifs, par exemple pour éventailler un <a class="" href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/">scan</a>). Donc le coût mémoire de la conscience des partitions est d'une connexion par nœud plus quelques tableaux dimensionnés au nombre de segments — il ne croît pas avec le nombre de clés ou de requêtes.</p>
<!-- -->
<p>Le bénéfice est qu'il n'y a aucun nœud dans le cluster dont le travail est de recevoir des requêtes mal routées et de les relayer. Chaque requête bien routée est servie par le premier nœud qu'elle atteint. Cela retire tout un étage du budget de latence et, tout aussi important, de la surface de défaillance — vous ne pouvez pas perdre une requête dans un saut de relais qui n'existe pas. (La pièce compagnon sur <a class="" href="https://clustron.io/fr/blog/latency-floor/">le plancher de latence</a> parcourt ce qui <em>reste</em> dans le budget une fois le saut de proxy parti.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-3--la-map-bouge-sous-vos-pieds">Étape 3 — la map bouge sous vos pieds<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#%C3%A9tape-3--la-map-bouge-sous-vos-pieds" class="hash-link" aria-label="Lien direct vers Étape 3 — la map bouge sous vos pieds" title="Lien direct vers Étape 3 — la map bouge sous vos pieds" translate="no">​</a></h2>
<p>Voici la partie difficile. La table de routage est un cache d'un fait — « le segment S est possédé par le nœud N » — que le cluster peut changer à tout moment. Un <a class="" href="https://clustron.io/fr/blog/anatomy-of-a-failover/">propriétaire peut tomber</a> et un réplica est promu ; une <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">partition peut être migrée</a> pour faire grandir ou drainer un nœud. À l'instant où cela arrive, le <code>SegmentMap[S]</code> du client pointe vers une connexion qui n'est plus le propriétaire. Un client qui ferait aveuglément confiance à sa table enverrait des écritures à un nœud qui les refusera correctement.</p>
<p>Zaris garde la table honnête de trois façons, par ordre de préférence.</p>
<p><strong>Push — le serveur prévient le client.</strong> Quand la possession change, le cluster émet un <code>RuntimeOwnershipChangedEvent</code>, et le client a un handler abonné dessus :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">Task</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">HandleAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">IServerEvent</span><span class="token plain"> @</span><span class="token keyword" style="color:#00009f">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">@</span><span class="token keyword" style="color:#00009f">event</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token class-name">RuntimeOwnershipChangedEvent</span><span class="token plain"> runtimeEvent</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        _router</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryUpdateRuntimeOwnership</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">runtimeEvent</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> Task</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">CompletedTask</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><code>TryUpdateRuntimeOwnership</code> ne reconstruit pas toute la table. Il clone le tableau <code>SegmentMap</code>, repointe seulement les segments dont la partition a réellement changé de propriétaire, et échange la table immuable en place par une seule assignation de référence. Les lecteurs ne voient jamais une table à moitié mise à jour — ils lisent soit l'ancienne soit la nouvelle, jamais un mélange déchiré. C'est du routage copy-on-write : le cas courant (rien n'a changé) ne coûte rien, et un changement ne touche que les segments affectés.</p>
<p><strong>Poll — un garde-fou lent.</strong> Un timer rafraîchit la map du cluster toutes les dix secondes quoi qu'il arrive, de sorte que même si un événement push est manqué (une reconnexion, une notification perdue), la vue du client s'auto-répare en un intervalle :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> </span><span class="token class-name">TimeSpan</span><span class="token plain"> _pollInterval </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromSeconds</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><strong>React — la requête elle-même corrige la map.</strong> La dernière ligne de défense est la requête qui route mal. Si le client envoie à un nœud qui n'est plus le propriétaire, le nœud ne la relaie pas silencieusement — il renvoie un statut qui dit au client que sa map est périmée, et le helper de réessai rafraîchit et réessaie :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">RequiresMapRefresh</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">KvStatus</span><span class="token plain"> status</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> status </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> KvStatus</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Moved</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> status </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> KvStatus</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ReplicaWriteRejected</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token comment" style="color:#999988;font-style:italic">// Ownership is mid-handoff: the client must force-fetch the updated</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token comment" style="color:#999988;font-style:italic">// ownership and retry against the node that now owns the segment.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> status </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> KvStatus</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">OwnershipChanging</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Quand une op réessayable revient <code>Moved</code>, <code>OwnershipChanging</code> ou <code>ReplicaWriteRejected</code>, le client appelle son hook de rafraîchissement et réessaie — une boucle bornée de quelques tentatives avec un bref backoff, pas une rotation sans fin. Il y a une leçon subtile cuite dans le code actuel ici : une version antérieure rafraîchissait la map à <em>chaque</em> tentative de réessai, ce qui, sous un fort churn de nœuds, transformait une seule op défaillante en des dizaines de rafraîchissements tous sérialisés derrière un verrou. La correction a été de ne rafraîchir que lorsque le statut dit réellement « votre map est fausse », pas simplement parce qu'une tentative a échoué. Le traitement plus profond de cette discipline de réessai a son propre article — <a class="" href="https://clustron.io/fr/blog/retries-under-ownership-churn/">les réessais sous churn de possession</a> — mais la version en une ligne est : <em>une map périmée est une condition qui se corrige d'elle-même, et la correction est bon marché précisément parce qu'elle est rare et ciblée.</em></p>
<p>Les trois mécanismes se superposent : le push rend le cas courant instantané, le poll rend un push manqué consistant à terme, et le réessai réactif rend toute obsolescence résiduelle invisible à votre appel — il prend juste un aller-retour supplémentaire à l'instant où la possession a bougé, et est correct dès l'appel suivant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-le-faire-côté-client-du-tout-">Pourquoi le faire côté client du tout ?<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#pourquoi-le-faire-c%C3%B4t%C3%A9-client-du-tout-" class="hash-link" aria-label="Lien direct vers Pourquoi le faire côté client du tout ?" title="Lien direct vers Pourquoi le faire côté client du tout ?" translate="no">​</a></h2>
<p>Le routage conscient des partitions côté client coûte quelque chose. Le client doit porter un modèle du cluster, le garder frais, et expédier la même logique de placement qu'utilise le serveur. Une conception fondée sur un proxy pousse tout cela dans le cluster et laisse le client rester bête. Alors pourquoi payer ?</p>
<p>Parce que l'alternative paie à chaque requête, à jamais. Un proxy ou une conception « envoyez n'importe où, on le relaiera » ajoute un saut au chemin chaud de chaque opération — un saut qui est pure surcharge, présent même quand rien n'a échoué et rien ne bouge. Zaris paie le coût du routage <em>une fois, d'avance, et seulement à nouveau quand la possession change réellement</em> — ce qui, dans un cluster sain, n'arrive presque jamais. Le régime permanent est un index de tableau et une écriture de socket vers le bon nœud. C'est toute la raison pour laquelle <a class="" href="https://clustron.io/fr/blog/reads-scale-with-cores/">les lectures montent avec vos cœurs</a> au lieu de goulotter sur un étage intermédiaire : il n'y a pas d'étage intermédiaire sur quoi goulotter.</p>
<p>Et cela se compose avec tout le reste. La même résolution <code>GetClientForKey</code> est ce par quoi une <a class="" href="https://clustron.io/fr/blog/redis-keyspace-notifications/">surveillance de clé</a>, un incrément de compteur, un <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">compare-and-swap</a> et une opération de structure de données à clé unique routent tous, donc ils héritent tous de la livraison directe au propriétaire et de la même map auto-réparante. Le client n'est pas un tuyau bête avec du routage boulonné dessus ; le routage est ce qu'est le client.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-compromis-honnêtes">Les compromis honnêtes<a href="https://clustron.io/fr/blog/client-partition-aware-routing/#les-compromis-honn%C3%AAtes" class="hash-link" aria-label="Lien direct vers Les compromis honnêtes" title="Lien direct vers Les compromis honnêtes" translate="no">​</a></h2>
<p>Le routage direct au propriétaire n'est pas sans bords, et il vaut la peine de les nommer.</p>
<ul>
<li class=""><strong>Le client doit atteindre chaque nœud.</strong> Parce qu'il n'y a pas de proxy, le client ouvre une connexion vers chaque nœud vers lequel il route. Dans un réseau plat c'est un non-problème ; à travers une frontière — disons un client hors de Kubernetes — ces nœuds doivent être adressables individuellement. C'est exactement le problème que l'<a class="" href="https://clustron.io/fr/blog/kubernetes-external-clients/">adressage de client externe</a> existe pour résoudre, et c'est une conséquence directe de l'absence de porte d'entrée unique.</li>
<li class=""><strong>Un bref retard de map est possible, pas une perte.</strong> Dans la fenêtre entre le déplacement de la possession et le moment où le client l'apprend, une requête peut router vers l'ancien propriétaire. Elle est rejetée et réessayée, pas mal appliquée — le chemin réactif ci-dessus garantit que le réessai atterrit sur le vrai propriétaire. Vous payez un aller-retour, pas une mauvaise réponse.</li>
<li class=""><strong>La fonction de placement est un contrat de compatibilité.</strong> Parce que le client et le serveur partagent <code>SegmentMapper</code>, vous ne pouvez pas changer la façon dont les clés hachent vers les segments sans que les deux côtés s'accordent — ce qui est précisément pourquoi le hachage a été aligné sur le modèle de slot Redis stable plutôt que sur un schéma interne qui pourrait dériver.</li>
</ul>
<p>Dit simplement : le client Zaris sait où vivent vos clés, garde ce savoir à jour sans que vous y pensiez, et dépense un saut réseau — celui qui fait réellement le travail — à chaque opération. L'habileté n'est pas dans le chemin rapide, qui est délibérément ennuyeux. Elle est dans les trois mécanismes discrets qui gardent le chemin rapide correct pendant que le cluster se remodèle en dessous.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>.NET</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Un propriétaire qui redémarre n'a aucun droit à sa propre vacuité]]></title>
            <link>https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/</link>
            <guid>https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nœud qui possédait une partition meurt et revient avec un store vide. L'instinct dangereux est de faire confiance à cette vacuité. Voici comment Zaris reconstruit un propriétaire redémarré depuis ses réplicas vivants — un drapeau certifié collant qui refuse à un propriétaire frais toute autorité à être vide, une synchronisation inverse (reverse-sync), une barrière de service qui refuse de répondre tant que la tête de l'op-log locale n'a pas rattrapé celle de la source, et une fusion non destructive qui n'écrasera pas les données qu'un réplica a accumulées pendant l'absence du propriétaire.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Reconstruire un propriétaire redémarré depuis ses réplicas" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJBbiBvd25lciB0aGF0IHJlc3RhcnRzIGhhcyBubyByaWdodCB0byBpdHMgb3duIGVtcHRpbmVzczogcmVidWlsZGluZyBmcm9tIHJlcGxpY2FzIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+UkVDT1ZFUlk8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+QSByZXN0YXJ0IGhhcyBubyByaWdodDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjE4MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij50byBpdHMgb3duIGVtcHRpbmVzczwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPlVuY2VydGlmaWVkICYjODU5NDsgcHVsbCBmcm9tIGEgbGl2ZSByZXBsaWNhICYjODU5NDsgc2VydmUgb25seSB3aGVuIGNhdWdodCB1cCAmIzg1OTQ7IGNlcnRpZnk8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwLDI4NikiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjIyIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjU4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNSIvPg0KICAgIDx0ZXh0IHg9Ijg1IiB5PSI0NyIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnJlc3RhcnRlZCBvd25lcjwvdGV4dD4NCiAgICA8dGV4dCB4PSI4NSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5lbXB0eSwgdW5jZXJ0aWZpZWQ8L3RleHQ+DQogICAgPHJlY3QgeD0iODcwIiB5PSIyMiIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI1OCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSI5NDUiIHk9IjQ3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bGl2ZSByZXBsaWNhPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijk0NSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5ob2xkcyB0aGUgZGF0YTwvdGV4dD4NCiAgICA8cGF0aCBkPSJNODY4IDQyIEwxNzIgNDIiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiIG1hcmtlci1lbmQ9InVybCgjbm9uZSkiLz4NCiAgICA8Y2lyY2xlIGN4PSIzNDAiIGN5PSI0MiIgcj0iNyIgZmlsbD0iIzJiZDZhMCIvPg0KICAgIDxjaXJjbGUgY3g9IjUyMCIgY3k9IjQyIiByPSI3IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iNzAwIiBjeT0iNDIiIHI9IjciIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8dGV4dCB4PSIzNDAiIHk9IjIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+c2VydmUtZ2F0ZSBob2xkczwvdGV4dD4NCiAgICA8dGV4dCB4PSI1MjAiIHk9IjIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+cmV2ZXJzZS1zeW5jIHB1bGw8L3RleHQ+DQogICAgPHRleHQgeD0iNzAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmhlYWQgJiM4ODA1OyBzb3VyY2UgaGVhZDwvdGV4dD4NCiAgICA8dGV4dCB4PSI1MjAiIHk9IjExMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPmVtcHRpbmVzcyBpcyBuZXZlciBhdXRob3JpdHkgJiM4MjEyOyBjZXJ0aWZ5IG9ubHkgb24gZnVsbC1zeW5jIHByb29mPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Un <a class="" href="https://clustron.io/fr/blog/anatomy-of-a-failover/">basculement</a> couvre le moment où un propriétaire meurt : le cluster choisit un survivant et continue de servir. Une <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">migration planifiée</a> couvre le déplacement à dessein de la partition d'un propriétaire sain. Cet article porte sur le troisième acte, celui qui se joue après que les sirènes se taisent — le processus propriétaire d'origine redémarre, rejoint le cluster, et veut récupérer sa partition. Son store rien-sur-disque, tout-en-mémoire est revenu <strong>vide</strong>. Les données dont il est responsable sont toujours vivantes, mais elles sont vivantes sur les réplicas qui ont continué de servir pendant qu'il était à terre.</p>
<p>Tout le problème de correction tient en une phrase : <em>le propriétaire redémarré ne peut pas être autorisé à faire confiance à sa propre vacuité.</em> Un store vide ressemble exactement à un store que quelqu'un a délibérément vidé. Si le nœud suppose « je suis le propriétaire et je n'ai pas de données, donc cette partition est vide », il servira joyeusement <code>NotFound</code> pour des millions de clés vivantes et, pire, laissera son état vide refluer sur les réplicas et les supprimer. Cet article parcourt les cinq petites politiques pures que Zaris utilise pour s'assurer que cela n'arrive jamais.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-la-vacuité-est-indécidable-de-lintérieur">Pourquoi la vacuité est indécidable de l'intérieur<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#pourquoi-la-vacuit%C3%A9-est-ind%C3%A9cidable-de-lint%C3%A9rieur" class="hash-link" aria-label="Lien direct vers Pourquoi la vacuité est indécidable de l'intérieur" title="Lien direct vers Pourquoi la vacuité est indécidable de l'intérieur" translate="no">​</a></h2>
<p>Commençons par le piège, car chaque règle ci-dessous en est façonnée. Un nœud qui démarre en détenant zéro enregistrement pour un segment qu'il possède est dans l'un de deux états, et <strong>il ne peut pas dire lequel à partir de la seule information locale</strong> :</p>
<ol>
<li class="">Il est vide parce qu'il vient de redémarrer et n'a jamais retiré ses données. Il <em>doit</em> les retirer.</li>
<li class="">Il est vide parce qu'il a autoritairement vidé ou expiré ces clés, et un réplica qui était brièvement à terre détient encore les copies périmées. S'il retire, il <em>ressuscite des données supprimées</em>.</li>
</ol>
<p>Ces deux cas sont localement indiscernables — le store est vide dans les deux cas. Une version antérieure de Zaris a tenté de résoudre cela avec un signal d'anomalie <code>EmptyActive</code> et il a été retiré précisément parce que le signal est une supposition. Vous ne pouvez pas demander « suis-je vide pour une bonne raison ? » et obtenir une réponse digne de confiance du nœud vide.</p>
<p>Donc Zaris ne demande rien à la vacuité. Il route la décision à travers un autre fait : un drapeau collant, au niveau de la partition, appelé <strong>certified</strong>, qui signifie exactement « ma synchronisation de démarrage s'est terminée et mon store fait maintenant autorité ». Un nœud ne le fixe qu'après avoir <em>prouvé</em> la possession. Un propriétaire fraîchement redémarré est, par définition, <strong>non certifié</strong> — et un propriétaire non certifié n'a aucune autorité à être vide. Il doit retirer les données de quiconque les détient. Un propriétaire certifié fait autorité et est laissé strictement tranquille, de sorte qu'un vidage légitime n'est jamais ressuscité.</p>
<p>Ce seul recadrage — de la <em>vacuité</em> (indécidable) à la <em>certification</em> (un fait prouvé et collant) — est ce qui rend le reste de la récupération sûr.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-1--le-propriétaire-redémarré-retire-au-lieu-de-supposer">Étape 1 — le propriétaire redémarré retire, au lieu de supposer<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#%C3%A9tape-1--le-propri%C3%A9taire-red%C3%A9marr%C3%A9-retire-au-lieu-de-supposer" class="hash-link" aria-label="Lien direct vers Étape 1 — le propriétaire redémarré retire, au lieu de supposer" title="Lien direct vers Étape 1 — le propriétaire redémarré retire, au lieu de supposer" translate="no">​</a></h2>
<p>Quand le nœud rejoint, la map l'assigne toujours comme propriétaire de son ancienne partition. Le déclencheur de reverse-sync décide si chaque segment possédé doit être <em>retiré d'un réplica vivant</em> plutôt que traité comme localement autoritaire :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ShouldReverseSyncOwnedSegment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> partitionCertified</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> isMapPreferredPrimary</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> liveNonSelfReplicaExists</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">partitionCertified </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> liveNonSelfReplicaExists</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Trois choses à propos de ce petit prédicat sont porteuses, et chacune est un bug qui a atteint une exécution de certification avant que la règle ne soit écrite.</p>
<p><strong>Il ignore <code>isMapPreferredPrimary</code>.</strong> Ce paramètre est toujours passé — comme contexte de décision documenté — mais la décision ne le lit délibérément pas. Une version antérieure ne déclenchait le reverse-sync que lorsque le nœud était <em>aussi</em> le primaire préféré de la map. Un nœud qui est l'actif à l'exécution mais <em>pas</em> le primaire de la map — l'état exact dans lequel atterrit un propriétaire re-domicilié ou promu-après-récupération — voyait ses segments possédés entièrement sautés. Il ne demandait jamais ses données et restait vide à jamais pendant qu'un réplica vivant détenait chaque enregistrement (une exécution de cert : actif détenant <code>0</code> contre réplica détenant <code>4600</code> sur quinze segments, jamais convergé en huit minutes). La possession du retrait de données ne peut pas être conditionnée au fait d'être le primaire <em>préféré</em> ; elle doit se déclencher pour tout actif non certifié.</p>
<p><strong>Le signal est <code>!partitionCertified</code>, pas la vacuité.</strong> C'est la correction d'indécidabilité de la section précédente, en code. Un actif non certifié n'a jamais prouvé la possession, donc il retire ; un certifié fait autorité et est sauté.</p>
<p><strong><code>liveNonSelfReplicaExists</code> est une garde, pas une gentillesse.</strong> Si le réplica configuré est <em>à terre</em>, il n'y a rien à partir de quoi synchroniser. Piloter la récupération contre une source morte tourne à jamais (<code>RECOVERY STUCK source=self synced=0/128</code>). Donc quand aucun réplica vivant n'existe, l'actif est la seule copie survivante — il <em>est</em> l'autorité, et il certifie immédiatement plutôt que d'attendre une source qui ne répondra jamais. Le retrait lui-même est borné par révision et par filigrane (watermark), ce qui en fait un no-op inoffensif quand le réplica ne détient rien.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-2--ne-servez-pas-une-clé-que-vous-navez-pas-encore-rattrapée">Étape 2 — ne servez pas une clé que vous n'avez pas encore rattrapée<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#%C3%A9tape-2--ne-servez-pas-une-cl%C3%A9-que-vous-navez-pas-encore-rattrap%C3%A9e" class="hash-link" aria-label="Lien direct vers Étape 2 — ne servez pas une clé que vous n'avez pas encore rattrapée" title="Lien direct vers Étape 2 — ne servez pas une clé que vous n'avez pas encore rattrapée" translate="no">​</a></h2>
<p>Le retrait n'est pas instantané, et la fenêtre pendant qu'il s'exécute est là où les lectures acquittées se transforment discrètement en <code>NotFound</code>. Une barrière naïve est un unique booléen, <code>IsSynced</code>, signifiant « une passe de synchronisation s'est terminée ». Ce booléen ne suffit pas, et trois échecs de cert distincts l'ont prouvé : un actif en train de reprendre a promu avec 34 clés en moins ; une transmission a perdu une clé acquittée ; un actif qui rejoignait a servi <code>NotFound</code> pour 57 clés pendant le churn. Dans chaque cas une passe de synchronisation <em>s'était</em> terminée — mais la source (l'actif temporaire qui a continué de servir) avait depuis accepté davantage d'écritures que le nœud en récupération n'avait pas retirées. <code>IsSynced</code> était vrai et le nœud était toujours en retard.</p>
<p>La correction est de conditionner à une <em>position</em>, pas à un drapeau. La révision de l'op-log est un nombre répliqué et comparable entre nœuds : l'actif l'assigne et l'expédie, de sorte qu'un réplica rattrapé converge vers la même tête. La barrière de service compare les têtes :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name">SegmentServeDecision</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Decide</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> isSynced</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">long</span><span class="token plain"> localHeadRevision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">long</span><span class="token plain"> sourceHeadRevision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">isSynced</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> SegmentServeDecision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">HoldUnsynced</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// A watermark of 0 carries no information — do not block on it; trust the completed sync pass.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sourceHeadRevision </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> localHeadRevision </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> sourceHeadRevision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> SegmentServeDecision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">HoldBehindSource</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> SegmentServeDecision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Serve</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><code>HoldBehindSource</code> est le nouvel état qui compte : une passe de synchronisation s'est terminée, mais la tête de la source (capturée à cette synchronisation) est en avance sur la nôtre, donc la source a accepté des écritures que nous n'avons pas retirées. Servir maintenant renverrait <code>NotFound</code> pour exactement ces clés. Le nœud patiente jusqu'à ce que sa tête locale atteigne le filigrane. Notez la dégradation délibérée : un filigrane enregistré de <code>0</code> signifie « pas d'information de filigrane », et la barrière retombe sur le comportement basé sur le seul <code>IsSynced</code> plutôt que de bloquer à jamais sur une donnée manquante. La règle ne bloque jamais sur l'absence d'un fait — seulement sur un fait qui dit <em>vous êtes en retard</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-3--retirez-mais-nécrasez-pas">Étape 3 — retirez, mais n'écrasez pas<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#%C3%A9tape-3--retirez-mais-n%C3%A9crasez-pas" class="hash-link" aria-label="Lien direct vers Étape 3 — retirez, mais n'écrasez pas" title="Lien direct vers Étape 3 — retirez, mais n'écrasez pas" translate="no">​</a></h2>
<p>Maintenant la direction dangereuse : appliquer le snapshot que le réplica renvoie. L'instinct est de vider le store local et de copier le snapshot en bloc. Cet instinct est correct exactement la moitié du temps, et catastrophiquement faux l'autre moitié. La question décisive est <em>dont les révisions sont comparables à celles de qui</em> :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name">ResyncApplyMode</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Decide</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> digestMatches</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> isPassiveSubordinateReplica</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> priorReconcileShortfall </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> digestMatches</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">?</span><span class="token plain"> ResyncApplyMode</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Skip</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">isPassiveSubordinateReplica </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">priorReconcileShortfall</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">?</span><span class="token plain"> ResyncApplyMode</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Reconcile</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ResyncApplyMode</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Wipe</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Trois issues :</p>
<ul>
<li class=""><strong><code>Skip</code></strong> — les digests de contenu correspondent déjà. Rien à transférer, rien à appliquer. (Voir <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">le protocole de cut-over</a> pour savoir pourquoi un digest de contenu, pas un décompte de révisions, est la chose qui vaut d'être comparée.)</li>
<li class=""><strong><code>Reconcile</code></strong> (non destructif) — le nœud local est un <em>pur subordonné passif</em> de l'unique actif vivant. Ses révisions par clé viennent toutes de ce seul actif, donc elles sont directement comparables. Le snapshot est fusionné borné par révision : un <code>PutRaw</code> garde l'enregistrement localement plus récent et comble les clés manquantes ; les pierres tombales s'appliquent via <code>DeleteRaw</code>. Crucialement, cela préserve les écritures que le réplica a accumulées par le canal vivant pendant un bootstrap lent au lieu de les jeter en vidant d'abord.</li>
<li class=""><strong><code>Wipe</code></strong> (vidage destructif + copie) — le nœud local <em>est ou a été lui-même un actif</em> pour cette partition. Deux nœuds indépendamment actifs ont des révisions par clé <strong>incomparables</strong> : le même numéro de révision sur chacun se réfère à des écritures différentes. Les fusionner entrelacerait silencieusement deux historiques. Donc la seule manœuvre sûre est de déposer l'état local et d'adopter le snapshot faisant autorité en bloc.</li>
</ul>
<p>Le défaut est la copie destructive sûre ; <code>Reconcile</code> n'est débloqué <em>que</em> en prouvant que le nœud est un subordonné purement passif. C'est la « wipe-guard » : elle empêche la fusion dangereuse entre deux anciens actifs, et symétriquement elle empêche un vidage en bloc de jeter de bonnes écritures du canal vivant sur un simple réplica.</p>
<p>Il y a une ride de plus encodée dans <code>priorReconcileShortfall</code>, et elle vient directement d'un abandon de certification sur douze heures. Une réconciliation bornée par révision peut <em>clôturer dehors</em> (fence out) des enregistrements qu'elle aurait dû appliquer et se terminer tout en restant derrière le snapshot faisant autorité — un segment bloqué à <code>12643/15435</code> sur 232 sessions de synchronisation, réconciliant les mêmes enregistrements hors de la fenêtre à chaque fois, bouclant à jamais. Le drapeau dit : si une réconciliation précédente s'est terminée mais nous a laissés en déficit, cesser de réconcilier et escalader vers un wipe pour que le snapshot gelé soit adopté en entier et que nous convergions réellement. Il n'est jamais fixé qu'<em>après un déficit mesuré</em> — un réplica en avance ou à égalité ne le déclenche jamais, donc les écritures du canal vivant ne sont jamais jetées pour poursuivre un fantôme.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-4--lautre-côté--un-propriétaire-non-certifié-peut-quand-même-être-une-bonne-source">Étape 4 — l'autre côté : un propriétaire non certifié peut quand même être une bonne source<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#%C3%A9tape-4--lautre-c%C3%B4t%C3%A9--un-propri%C3%A9taire-non-certifi%C3%A9-peut-quand-m%C3%AAme-%C3%AAtre-une-bonne-source" class="hash-link" aria-label="Lien direct vers Étape 4 — l'autre côté : un propriétaire non certifié peut quand même être une bonne source" title="Lien direct vers Étape 4 — l'autre côté : un propriétaire non certifié peut quand même être une bonne source" translate="no">​</a></h2>
<p>La récupération ne concerne pas seulement le nœud qui est en retard. Considérez le cas miroir : le nœud A redémarre, et le nœud B — un réplica qui était à terre — veut maintenant synchroniser <em>depuis</em> A. A devrait-il servir la requête alors que A est lui-même encore non certifié ?</p>
<p>La garde naïve dit non : « non certifié signifie que mon store est vide, donc servir écraserait les données valides de B avec mon trou ». Mais cette prémisse est fausse à l'instant où A détient réellement la copie faisant autorité — ce qui arrive de deux façons : A était un primaire préféré qui a vérifié le rattrapage avant la promotion mais dont le chemin de promotion n'a jamais appelé <code>CertifyPartition</code> (donc le drapeau collant reste coincé à faux alors que les données sont toutes là), ou A a pris la possession via une récupération de défaillance totale de partition et détient les données complètes comme actif sans jamais avoir été estampillé <code>IsSynced</code>. Dans les deux cas, la copie de A <em>est</em> la source de vérité, et refuser a laissé le réplica bloqué à zéro (un relevé : 1 792 requêtes refusées ; un autre : les 128 segments refusés pour toute une partition).</p>
<p>Donc la garde côté source est rendue consciente des données :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ShouldRefuse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> selfIsRuntimeOwnerMapPrimary</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> partitionCertified</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> segmentSynced</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> selfHoldsSegmentData</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> selfIsRuntimeOwnerMapPrimary </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">partitionCertified </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">segmentSynced </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">selfHoldsSegmentData</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Refuser de servir <strong>seulement</strong> quand le nœud est le propriétaire, est non certifié, <em>et</em> que le segment est à la fois non synchronisé et ne détient aucune donnée — le seul cas véritable « en pleine récupération et réellement vide » où servir un trou pourrait laisser le demandeur se certifier contre lui. Parce que le récepteur applique chaque chunk borné par révision, servir des données réelles est toujours sûr ; la garde réduit le refus au seul état où il n'y a rien de sûr à servir.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-5--déclarer-victoire-exactement-une-fois">Étape 5 — déclarer victoire, exactement une fois<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#%C3%A9tape-5--d%C3%A9clarer-victoire-exactement-une-fois" class="hash-link" aria-label="Lien direct vers Étape 5 — déclarer victoire, exactement une fois" title="Lien direct vers Étape 5 — déclarer victoire, exactement une fois" translate="no">​</a></h2>
<p>Enfin l'actif en récupération a retiré chaque segment possédé et a rattrapé chacun jusqu'à la tête de sa source. Il fait à nouveau autorité, et il a besoin de <em>le dire</em> — basculer le drapeau certifié collant — pour que le déclencheur de reverse-sync de l'Étape 1 cesse de re-céder ses segments et que le cluster enregistre la partition comme convergée.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ShouldCertify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> selfIsRuntimeActive</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> partitionCertified</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> ownedSegmentCount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> syncedSegmentCount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> selfIsRuntimeActive</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">partitionCertified</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> ownedSegmentCount </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> syncedSegmentCount </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> ownedSegmentCount</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La certification est le pendant symétrique du déclencheur : le déclencheur met en file les segments d'un actif non certifié à retirer ; celui-ci bascule le drapeau une fois que <em>chaque</em> segment possédé est synchronisé, et la partition se stabilise. Le chemin de certification préexistant ne s'exécutait que pour le primaire préféré de la map, donc un actif re-domicilié <em>non-primaire</em> retirait toutes ses données correctement puis ne certifiait jamais — il re-mettait en file le reverse-sync à jamais, et le cluster ne s'enregistrait jamais comme convergé (une exécution : 30 points de contrôle expirant ~106 secondes après le début, alors que <code>Missing=0</code> et que les décomptes ne divergeaient jamais — les données étaient <em>justes</em>, seule la stabilisation manquait).</p>
<p>Et notez le seul raccourci que cette politique <strong>refuse</strong> délibérément : il n'y a pas de chemin « pas de réplica vivant, donc certifie ma vacuité comme autorité » pour un actif non-primaire. Un actif non-primaire n'est pas l'autorité de la map ; le certifier vide alors qu'un réplica temporairement à terre détient encore les données laisserait ce réplica plus tard synchroniser <em>depuis</em> ce nœud vide et tout perdre. Le raccourci de l'autorité-à-être-vide n'appartient qu'au primaire préféré de la map, dans son propre chemin de réconciliation. Partout ailleurs, la vacuité ne gagne rien.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forme-de-lensemble">La forme de l'ensemble<a href="https://clustron.io/fr/blog/rebuilding-an-owner-from-replicas/#la-forme-de-lensemble" class="hash-link" aria-label="Lien direct vers La forme de l'ensemble" title="Lien direct vers La forme de l'ensemble" translate="no">​</a></h2>
<p>Prenez du recul et les cinq politiques sont une seule idée appliquée en cinq endroits : <strong>la possession est prouvée, jamais supposée.</strong></p>
<ul>
<li class="">Un propriétaire redémarré est <strong>non certifié</strong>, donc il retire au lieu de faire confiance à son store vide (Étape 1).</li>
<li class="">Il ne <strong>servira</strong> pas un segment tant que la tête de son op-log n'aura pas atteint celle de la source — une passe de synchronisation terminée n'est pas la même chose qu'être rattrapé (Étape 2).</li>
<li class="">Il <strong>fusionne</strong> plutôt que d'écraser quand les révisions sont comparables, et écrase plutôt qu'entrelace quand elles ne le sont pas (Étape 3).</li>
<li class="">En tant que <em>source</em>, il sert chaque fois qu'il détient véritablement les données, et ne refuse que lorsqu'il est réellement vide en pleine récupération (Étape 4).</li>
<li class="">Il <strong>certifie</strong> — bascule le drapeau collant d'autorité — seulement sur preuve que chaque segment possédé est synchronisé, sans raccourci de vacuité-comme-autorité pour un non-primaire (Étape 5).</li>
</ul>
<p>Chacune de ces règles est un prédicat pur et sans E/S — une poignée de booléens et deux numéros de révision en entrée, une décision en sortie — ce qui est pourquoi elles sont testables unitairement en isolation et pourquoi chacune a pu être épinglée à l'échec de certification exact qui l'a motivée. Elles sont la vue de l'intérieur de l'invariant que la <a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">certification du chaos sur douze heures</a> prouve depuis l'extérieur : tuer un propriétaire en plein vol, laisser ses réplicas porter la charge, le ramener, et regarder <code>Missing=0</code> tenir sur des millions d'opérations acquittées. La raison pour laquelle le redémarrage ne perd pas une écriture est que le propriétaire de retour n'est jamais une seule fois autorisé à croire sa propre vacuité. Il doit regagner ses données — des réplicas qui les ont gardées en sécurité.</p>
<p>Si vous voulez les pièces sur lesquelles ceci s'appuie : l'<a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">op-log rejouable</a> est ce qui fait de « rattraper ma tête jusqu'à la tête de la source » une opération réelle et sans trou, et <a class="" href="https://clustron.io/fr/blog/reading-your-own-writes/">lire vos propres écritures</a> est la promesse côté client que la barrière de service de l'Étape 2 existe pour tenir.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
            <category>Opérations</category>
        </item>
        <item>
            <title><![CDATA[Comment un cluster Zaris démarre : appartenance, map et disponibilité conditionnée par la convergence]]></title>
            <link>https://clustron.io/fr/blog/cluster-formation-readiness/</link>
            <guid>https://clustron.io/fr/blog/cluster-formation-readiness/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Avant de pouvoir servir une seule clé, un cluster Zaris doit répondre à trois questions : qui en fait partie, qui possède quoi, et est-il déjà sûr de prendre du trafic. Cet article suit cette séquence de démarrage — un modèle d'appartenance fondé sur un registre de configuration avec une fenêtre de grâce avant qu'un nœud ne soit déclaré perdu, une partition map rédigée par le leader et validée sous une garde de version strictement monotone, et une barrière de disponibilité à deux niveaux qui refuse de servir un nœud tant que sa map validée et sa possession à l'exécution ne concordent pas.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Comment un cluster Zaris démarre : appartenance, map et disponibilité conditionnée par la convergence" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJIb3cgYSBaYXJpcyBjbHVzdGVyIGNvbWVzIHVwOiBtZW1iZXJzaGlwLCB0aGUgbWFwLCBhbmQgY29udmVyZ2VuY2UtZ2F0ZWQgcmVhZGluZXNzIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+Q0xVU1RFUiBGT1JNQVRJT048L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+SG93IGEgY2x1c3RlcjwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjE4MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij5jb21lcyB1cCByZWFkeTwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPk1lbWJlcnNoaXAsIHRoZSBtYXAsIGFuZCBjb252ZXJnZW5jZS1nYXRlZCByZWFkaW5lc3M8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczMCwyNjQpIj4NCiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI2NCIgeT0iMjMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlIDA8L3RleHQ+DQogICAgPHRleHQgeD0iNjQiIHk9IjQwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+bWFwIHY9MTI8L3RleHQ+DQogICAgPHJlY3QgeD0iMTQ4IiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSIyMTIiIHk9IjIzIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+bm9kZSAxPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjIxMiIgeT0iNDAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5tYXAgdj0xMjwvdGV4dD4NCiAgICA8cmVjdCB4PSIyOTYiIHk9IjAiIHdpZHRoPSIxMjgiIGhlaWdodD0iNDgiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjM2MCIgeT0iMjMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlIDI8L3RleHQ+DQogICAgPHRleHQgeD0iMzYwIiB5PSI0MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm1hcCB2PTEyPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik02NCA0OCBMMjEyIDk2IiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDxwYXRoIGQ9Ik0yMTIgNDggTDIxMiA5NiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8cGF0aCBkPSJNMzYwIDQ4IEwyMTIgOTYiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHJlY3QgeD0iOTYiIHk9IjEwMCIgd2lkdGg9IjIzMiIgaGVpZ2h0PSI2MCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIyMTIiIHk9IjEyNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPm1hcC5WZXJzaW9uID09IHJ1bnRpbWUuVmVyc2lvbjwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMTIiIHk9IjE0OCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPnRyYWZmaWMgZ2F0ZSBvcGVuICYjODU5NDsgc2VydmU8L3RleHQ+DQogIDwvZz4NCiAgPHRleHQgeD0iODAiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDYiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>La plupart des écrits sur les stores distribués traitent du régime permanent — lectures, écritures, réplication — ou des défaillances spectaculaires : un <a class="" href="https://clustron.io/fr/blog/anatomy-of-a-failover/">basculement</a> quand un propriétaire meurt, un <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">cut-over</a> quand une partition se déplace. Mais il existe un moment plus discret qui décide si tout cela fonctionne : <strong>le moment où le cluster démarre.</strong> 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 — <em>s'il est seulement sûr de commencer à servir.</em></p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qui-fait-partie-du-cluster--lappartenance">Qui fait partie du cluster : l'appartenance<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#qui-fait-partie-du-cluster--lappartenance" class="hash-link" aria-label="Lien direct vers Qui fait partie du cluster : l'appartenance" title="Lien direct vers Qui fait partie du cluster : l'appartenance" translate="no">​</a></h2>
<p>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 <strong>Clustron.Nodus</strong> sur laquelle Zaris est construit. Zaris la consomme via une interface <code>INodusNode</code> : 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 <em>pas</em> 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.</p>
<p>Ce que Zaris ajoute, c'est une <strong>machine à états d'appartenance</strong> explicite qui traduit les événements bruts de heartbeat en décisions pertinentes pour la possession. Elle vit dans <code>MembershipStateTracker</code>, et ses états sont délibérément plus nuancés que « actif / inactif » :</p>
<!-- -->
<p>Les états se répartissent en deux groupes, et cette division est tout l'enjeu. <code>Alive</code>, <code>Suspected</code> et <code>Unreachable</code> n'affectent que la <em>possession à l'exécution</em> — ils peuvent déclencher la promotion d'un réplica ou marquer une partition temporairement indisponible, mais <strong>ils ne modifient jamais la partition map.</strong> Seuls <code>Lost</code>, <code>Evicted</code> et <code>Decommissioned</code> rendent un nœud éligible à la régénération de la map.</p>
<p>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 <em>pas</em> immédiatement la map. Il fait passer le nœud en <code>Unreachable</code> et démarre une <strong>fenêtre de grâce</strong> (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 à <code>Lost</code>, 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 : <strong>un nœud qui part ne régénère jamais la map à lui seul.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="comment-les-nœuds-se-trouvent">Comment les nœuds se trouvent<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#comment-les-n%C5%93uds-se-trouvent" class="hash-link" aria-label="Lien direct vers Comment les nœuds se trouvent" title="Lien direct vers Comment les nœuds se trouvent" translate="no">​</a></h3>
<p>La découverte est <strong>fondée sur un registre, pas sur une découverte par gossip.</strong> 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 <code>nodes</code> — 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.</p>
<p>Il existe <em>bien</em> un mécanisme de gossip dans Zaris, mais il fait un autre travail — c'est ainsi que les nœuds maintiennent leur <em>vue de l'état du cluster</em> convergée, pas ainsi qu'ils découvrent que des pairs existent. Nous y reviendrons quand nous parlerons de disponibilité.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qui-possède-quoi--la-partition-map">Qui possède quoi : la partition map<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#qui-poss%C3%A8de-quoi--la-partition-map" class="hash-link" aria-label="Lien direct vers Qui possède quoi : la partition map" title="Lien direct vers Qui possède quoi : la partition map" translate="no">​</a></h2>
<p>Une fois l'appartenance connue, le cluster a besoin d'un plan pour le keyspace. Ce plan, c'est la <strong>partition map</strong> — un document unique et versionné contre lequel chaque nœud route.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="dune-clé-à-un-propriétaire-en-deux-sauts">D'une clé à un propriétaire, en deux sauts<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#dune-cl%C3%A9-%C3%A0-un-propri%C3%A9taire-en-deux-sauts" class="hash-link" aria-label="Lien direct vers D'une clé à un propriétaire, en deux sauts" title="Lien direct vers D'une clé à un propriétaire, en deux sauts" translate="no">​</a></h3>
<p>Une clé ne se mappe pas directement à une partition. Zaris utilise un mapping à deux niveaux délibérément <strong>aligné octet pour octet avec Redis Cluster</strong> afin que la même arithmétique de routage fonctionne aussi bien pour les clients natifs que pour le front-end RESP :</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">key --CRC16--&gt; slot (0..16383) --proportional--&gt; segment (0..255) --SegmentToPartition[]--&gt; partition</span><br></span></code></pre></div></div>
<p>Le premier saut hache la clé avec CRC16 (CCITT/XMODEM, polynôme <code>0x1021</code>) modulo 16384 — la fonction exacte qu'utilise Redis Cluster, y compris le respect de la co-localisation par hash-tag, où une clé écrite <code>user:{42}:profile</code> 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 <strong>segments</strong> du cluster — 256 par défaut, fixés pour la durée de vie du cluster — et une table <code>SegmentToPartition</code> affecte chaque segment à une partition. La partition, enfin, est ce qui a des propriétaires.</p>
<p>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 <em>déplacer</em> 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-contient-une-partition-map">Ce que contient une partition map<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#ce-que-contient-une-partition-map" class="hash-link" aria-label="Lien direct vers Ce que contient une partition map" title="Lien direct vers Ce que contient une partition map" translate="no">​</a></h3>
<p>La map elle-même est un <code>PartitionMap</code> — 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 :</p>
<ul>
<li class=""><strong><code>Version</code></strong> — un compteur monotone qui s'incrémente à <em>chaque</em> changement de map. C'est le nombre qui rend le commit sûr (plus de détails ci-dessous).</li>
<li class=""><strong><code>TopologyVersion</code></strong> — ne s'incrémente que lors d'un véritable changement <em>structurel</em> (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.</li>
<li class=""><strong><code>TotalSegments</code></strong> — 256 par défaut, constant pour la vie du cluster.</li>
<li class=""><strong><code>Partitions</code></strong> — la liste des affectations, chacune portant un <code>PrimaryNodeId</code> (le primaire de placement), des <code>ReplicaNodeIds</code>, et une <code>PartitionIdentity</code> stable (un GUID frappé à la naissance qui survit à un renumérotage) plus les champs de bail de possession : <code>ActiveOwnerNodeId</code>, une <code>OwnershipGeneration</code> frappée par le leader, et le <code>LeaderTerm</code> qui 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.</li>
</ul>
<p>Notez la scission délibérée entre <code>PrimaryNodeId</code> (où la map <em>préfère</em> qu'une partition réside) et <code>ActiveOwnerNodeId</code> (qui la <em>sert réellement</em> 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 <a class="" href="https://clustron.io/fr/blog/reading-your-own-writes/">modèle de cohérence</a> explique pourquoi les lectures routent vers cet unique propriétaire actif.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="comment-la-map-est-planifiée">Comment la map est planifiée<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#comment-la-map-est-planifi%C3%A9e" class="hash-link" aria-label="Lien direct vers Comment la map est planifiée" title="Lien direct vers Comment la map est planifiée" translate="no">​</a></h3>
<p>La map n'est pas assemblée de façon gloutonne. Elle est produite par un planificateur pur et déterministe — <code>DesiredMapPlanner</code> — 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 :</p>
<ol>
<li class=""><strong>Des découpages de segments équilibrés et contigus.</strong> 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.</li>
<li class=""><strong>Un placement en groupes disjoints.</strong> Les <em>N</em> 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.</li>
</ol>
<p>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 — <em>irreprésentables par construction</em>, 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 <em>est</em> 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que--commit--signifie">Ce que « commit » signifie<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#ce-que--commit--signifie" class="hash-link" aria-label="Lien direct vers Ce que « commit » signifie" title="Lien direct vers Ce que « commit » signifie" translate="no">​</a></h3>
<p>La map est rédigée par <strong>un seul nœud — le leader du plan de contrôle — et personne d'autre.</strong> 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 <em>la valide localement</em> avant d'en informer qui que ce soit. Le commit est une règle unique et stricte imposée dans <code>TopologyManager.ApplyMap</code> :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// A map is accepted only if it is strictly newer.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">_currentMap </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> map</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> _currentMap</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>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 <code>MapChangedEvent</code>, 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 <em>même</em> 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.</p>
<!-- -->
<p>Une ride honnête au démarrage à froid : un nœud sans leader qui démarre seul peut frapper une map <em>provisoire</em> à 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="est-il-sûr-de-servir--la-disponibilité-conditionnée-par-la-convergence">Est-il sûr de servir : la disponibilité conditionnée par la convergence<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#est-il-s%C3%BBr-de-servir--la-disponibilit%C3%A9-conditionn%C3%A9e-par-la-convergence" class="hash-link" aria-label="Lien direct vers Est-il sûr de servir : la disponibilité conditionnée par la convergence" title="Lien direct vers Est-il sûr de servir : la disponibilité conditionnée par la convergence" translate="no">​</a></h2>
<p>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 <em>possession à l'exécution</em> n'ait rattrapé la <em>map</em> 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="barrière-un--la-stabilité-de-la-formation-du-cluster">Barrière un : la stabilité de la formation du cluster<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#barri%C3%A8re-un--la-stabilit%C3%A9-de-la-formation-du-cluster" class="hash-link" aria-label="Lien direct vers Barrière un : la stabilité de la formation du cluster" title="Lien direct vers Barrière un : la stabilité de la formation du cluster" translate="no">​</a></h3>
<p>La première barrière décide quand le leader est autorisé à générer la <em>première vraie map</em>. <code>ClusterReadinessGate</code> attend que l'appartenance soit stable — au moins un nombre minimum de nœuds présents <em>continûment</em> 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 <em>force le bootstrap</em> 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="barrière-deux--la-barrière-de-trafic-par-nœud">Barrière deux : la barrière de trafic par nœud<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#barri%C3%A8re-deux--la-barri%C3%A8re-de-trafic-par-n%C5%93ud" class="hash-link" aria-label="Lien direct vers Barrière deux : la barrière de trafic par nœud" title="Lien direct vers Barrière deux : la barrière de trafic par nœud" translate="no">​</a></h3>
<p>La seconde barrière est celle qui décide réellement si un nœud ouvre son listener client. <code>ClusterTrafficReadinessGate.IsReady()</code> ne renvoie vrai que lorsque <strong>toutes</strong> les conditions suivantes sont réunies :</p>
<ul>
<li class="">une map validée existe,</li>
<li class="">sa table segment-vers-partition est entièrement peuplée (<code>length == TotalSegments</code>),</li>
<li class="">une map de possession à l'exécution existe,</li>
<li class=""><strong><code>runtime.MapVersion == map.Version</code></strong> — la possession à l'exécution du nœud est alignée sur la version exacte de la map qu'il a validée,</li>
<li class="">et chaque partition a une entrée à l'exécution.</li>
</ul>
<p>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 <em>même version</em>. 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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="rester-convergé--digests-et-détection-réparation">Rester convergé : digests et détection-réparation<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#rester-converg%C3%A9--digests-et-d%C3%A9tection-r%C3%A9paration" class="hash-link" aria-label="Lien direct vers Rester convergé : digests et détection-réparation" title="Lien direct vers Rester convergé : digests et détection-réparation" translate="no">​</a></h3>
<p>La disponibilité est une propriété de démarrage ; <em>rester</em> 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 <strong><code>ClusterStateDigest</code></strong> — 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à.</p>
<p>Un <code>DigestAccumulator</code> 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 <code>HealingOrchestrator</code> 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 <strong>détection-réparation</strong>, pas par un vote de quorum avant de servir.</p>
<p>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 — <em>chaque</em> 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 <a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">certification du chaos sur 12 heures</a> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="facteur-de-réplication-et-une-valeur-par-défaut-honnête">Facteur de réplication, et une valeur par défaut honnête<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#facteur-de-r%C3%A9plication-et-une-valeur-par-d%C3%A9faut-honn%C3%AAte" class="hash-link" aria-label="Lien direct vers Facteur de réplication, et une valeur par défaut honnête" title="Lien direct vers Facteur de réplication, et une valeur par défaut honnête" translate="no">​</a></h2>
<p>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 <strong>1</strong> par défaut. Avec <code>RF=1</code>, chaque partition a un primaire et <em>aucun réplica</em> — une seule copie. Le planificateur place <code>RF - 1</code> réplicas par partition, donc à la valeur par défaut c'est zéro. Un cluster à <code>RF=1</code> 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 <code>RF &gt;= 2</code>, et il vous faut assez de nœuds pour placer effectivement ces réplicas sur des machines distinctes — l'article sur le <a class="" href="https://clustron.io/fr/blog/kubernetes-durability-sizing/">dimensionnement de durabilité Kubernetes</a> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-séquence-de-bout-en-bout">La séquence, de bout en bout<a href="https://clustron.io/fr/blog/cluster-formation-readiness/#la-s%C3%A9quence-de-bout-en-bout" class="hash-link" aria-label="Lien direct vers La séquence, de bout en bout" title="Lien direct vers La séquence, de bout en bout" translate="no">​</a></h2>
<p>Mis bout à bout, un cluster Zaris démarre ainsi :</p>
<ol>
<li class=""><strong>L'appartenance se résout</strong> à partir du registre de configuration (ou des ordinaux k8s), et Nodus élit un leader de plan de contrôle.</li>
<li class="">Le leader attend derrière la <strong>barrière de formation</strong> une appartenance stable, puis planifie une map avec <code>DesiredMapPlanner</code> et <strong>la valide localement</strong> sous la garde de version strictement monotone.</li>
<li class="">Le leader <strong>diffuse</strong> la map ; les suiveurs la récupèrent et l'appliquent sous la même garde, n'acceptant que des versions strictement supérieures.</li>
<li class="">Chaque nœud patiente derrière sa <strong>barrière de trafic</strong> jusqu'à ce que <code>runtime.MapVersion == map.Version</code> et que le routage soit entièrement peuplé — seulement alors il ouvre son listener client.</li>
<li class="">En régime permanent, les <strong>digests portés par les heartbeats</strong> 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.</li>
</ol>
<p>Aucune de ces étapes n'est spectaculaire en soi. Ensemble, elles sont la différence entre un cluster qui <em>tourne</em> et un cluster qui est <em>prêt</em> — 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.</p>
<hr>
<p><em>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 <a class="" href="https://clustron.io/fr/blog/control-plane-powershell/">présentation PowerShell</a> crée, démarre et inspecte un cluster en direct ; et la <a class="" href="https://clustron.io/fr/blog/elastic-scaling/">mise à l'échelle élastique</a> couvre ce qui arrive à la map quand vous ajoutez un serveur à un cluster qui sert déjà.</em></p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Deux clusters, un seul change feed : répliquer à travers un WAN sans étirer Zaris]]></title>
            <link>https://clustron.io/fr/blog/cross-datacenter-change-feed/</link>
            <guid>https://clustron.io/fr/blog/cross-datacenter-change-feed/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un cluster est réglé pour un LAN de datacenter — heartbeats à la microseconde, basculement rapide, un protocole de possession qui suppose une liaison serrée. Étirez-le à travers un WAN et ces hypothèses se retournent contre vous. La forme sûre, ce sont deux clusters indépendants reliés par un change feed reprenable et ordonné par révision. Voici pourquoi, et comment le log d'opérations vous donne déjà le curseur pour le construire.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Deux clusters indépendants reliés par un change feed reprenable à travers un WAN" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJUd28gaW5kZXBlbmRlbnQgY2x1c3RlcnMgbGlua2VkIGJ5IGEgcmVzdW1hYmxlIGNoYW5nZSBmZWVkIGFjcm9zcyBhIFdBTiI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPkNST1NTLURBVEFDRU5URVIgUkVQTElDQVRJT048L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+VHdvIGNsdXN0ZXJzLDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjE4MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij5vbmUgY2hhbmdlIGZlZWQ8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIyMjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5SZXBsaWNhdGUgYWNyb3NzIGEgV0FOIHdpdGhvdXQgc3RyZXRjaGluZyB0aGUgY2x1c3RlcjwvdGV4dD4NCg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4MCwyODApIj4NCiAgICA8IS0tIERDLUEgcHJpbWFyeSBjbHVzdGVyIC0tPg0KICAgIDxyZWN0IHg9IjAiIHk9IjIwIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjExOCIgcng9IjEwIiBmaWxsPSJub25lIiBzdHJva2U9IiMyYmQ2YTAiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQogICAgPHRleHQgeD0iMTUwIiB5PSI0OCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIGxldHRlci1zcGFjaW5nPSIyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5EQVRBQ0VOVEVSIEE8L3RleHQ+DQogICAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMiI+DQogICAgICA8cmVjdCB4PSIyNiIgeT0iNjQiIHdpZHRoPSI3MCIgaGVpZ2h0PSI0NiIgcng9IjYiLz4NCiAgICAgIDxyZWN0IHg9IjExNSIgeT0iNjQiIHdpZHRoPSI3MCIgaGVpZ2h0PSI0NiIgcng9IjYiLz4NCiAgICAgIDxyZWN0IHg9IjIwNCIgeT0iNjQiIHdpZHRoPSI3MCIgaGVpZ2h0PSI0NiIgcng9IjYiLz4NCiAgICA8L2c+DQogICAgPHRleHQgeD0iNjEiIHk9IjkyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bm9kZTwvdGV4dD4NCiAgICA8dGV4dCB4PSIxNTAiIHk9IjkyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bm9kZTwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMzkiIHk9IjkyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bm9kZTwvdGV4dD4NCiAgICA8dGV4dCB4PSIxNTAiIHk9IjEzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm93biBvd25lcnNoaXAgJmFtcDsgZmFpbG92ZXI8L3RleHQ+DQoNCiAgICA8IS0tIGNoYW5nZSBmZWVkIGxpbmsgLS0+DQogICAgPHBhdGggZD0iTTMwMCA3OSBMNzAwIDc5IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1IiBzdHJva2UtZGFzaGFycmF5PSIyIDEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCiAgICA8cGF0aCBkPSJNNjk0IDcxIEw3MTAgNzkgTDY5NCA4NyBaIiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iNTAwIiB5PSI2MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPmNoYW5nZSBmZWVkIMK3IHJldi1vcmRlcmVkPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjUwMCIgeT0iMTA0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+V0FOIMK3IGFzeW5jIMK3IHJlc3VtZSBAIGN1cnNvcjwvdGV4dD4NCg0KICAgIDwhLS0gREMtQiBmb2xsb3dlciBjbHVzdGVyIC0tPg0KICAgIDxyZWN0IHg9IjcxMCIgeT0iMjAiIHdpZHRoPSIzMDAiIGhlaWdodD0iMTE4IiByeD0iMTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8dGV4dCB4PSI4NjAiIHk9IjQ4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgbGV0dGVyLXNwYWNpbmc9IjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPkRBVEFDRU5URVIgQjwvdGV4dD4NCiAgICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiI+DQogICAgICA8cmVjdCB4PSI3MzYiIHk9IjY0IiB3aWR0aD0iNzAiIGhlaWdodD0iNDYiIHJ4PSI2Ii8+DQogICAgICA8cmVjdCB4PSI4MjUiIHk9IjY0IiB3aWR0aD0iNzAiIGhlaWdodD0iNDYiIHJ4PSI2Ii8+DQogICAgICA8cmVjdCB4PSI5MTQiIHk9IjY0IiB3aWR0aD0iNzAiIGhlaWdodD0iNDYiIHJ4PSI2Ii8+DQogICAgPC9nPg0KICAgIDx0ZXh0IHg9Ijc3MSIgeT0iOTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijg2MCIgeT0iOTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijk0OSIgeT0iOTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijg2MCIgeT0iMTMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+Zm9sbG93ZXIgYXBwbGllcyBieSByZXZpc2lvbjwvdGV4dD4NCiAgPC9nPg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0MzgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Tôt ou tard, quelqu'un pose la question : <em>nous avons un cluster dans une région et des utilisateurs dans une autre — ne peut-on pas simplement ajouter quelques nœuds là-bas et laisser un seul cluster couvrir les deux ?</em> C'est un réflexe raisonnable. Un cluster, un espace de noms, une chaîne de connexion, des données partout. Ce réflexe est aussi un piège, et il vaut la peine d'être précis sur le pourquoi.</p>
<p>Un cluster Zaris est conçu pour un réseau de datacenter. L'appartenance est suivie avec des heartbeats sur des minuteries courtes. La possession d'une partition se déplace entre nœuds via une poignée de main qui suppose que l'autre côté répond vite. Le basculement clôture un propriétaire suspecté mort et promeut un réplica à l'échelle de quelques secondes. Chacun de ces mécanismes est calibré pour une liaison où l'aller-retour se mesure en microsecondes et où un nœud « lent » est véritablement malade — et non simplement à quatre-vingts millisecondes et un océan de distance. Mettez la moitié des nœuds de l'autre côté d'un WAN et vous n'avez pas construit un cluster géo-distribué ; vous avez construit un seul cluster qui prend désormais la latence WAN normale pour une défaillance, fait flapper la possession, et risque le split-brain au premier hoquet de la liaison entre régions.</p>
<p>La bonne forme n'est donc pas un seul cluster étiré. Ce sont <strong>deux clusters indépendants</strong> — chacun autonome, chacun gardant sa possession et son basculement entièrement sur son propre LAN — reliés par un <strong>change feed asynchrone et reprenable</strong>. Cet article porte sur ce motif : pourquoi l'indépendance est la décision porteuse, et comment le log d'opérations de Zaris vous remet déjà la seule chose dont un réplicateur WAN a réellement besoin — un curseur depuis lequel reprendre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-un-cluster-étiré-est-le-mauvais-outil">Pourquoi un cluster étiré est le mauvais outil<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#pourquoi-un-cluster-%C3%A9tir%C3%A9-est-le-mauvais-outil" class="hash-link" aria-label="Lien direct vers Pourquoi un cluster étiré est le mauvais outil" title="Lien direct vers Pourquoi un cluster étiré est le mauvais outil" translate="no">​</a></h2>
<p>Il est utile de nommer les hypothèses précises qu'un cluster unique formule, car chacune se brise différemment sur un WAN.</p>
<!-- -->
<ul>
<li class=""><strong>L'appartenance fondée sur les heartbeats</strong> décide qu'un nœud a disparu quand il manque quelques battements. Une liaison inter-régions qui ajoute de la latence et la partition occasionnelle déclenchera ce détecteur en fonctionnement sain. Le cluster déclarera morts des nœuds vivants.</li>
<li class=""><strong>Le protocole de possession</strong> déplace les partitions entre nœuds avec une poignée de main qui attend une réponse rapide. À travers un WAN, ces transitions calent, réessaient et se concurrencent — exactement le churn que la <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">machinerie de possession et de cut-over</a> est conçue pour résoudre <em>rapidement</em> sur un LAN.</li>
<li class=""><strong>Les minuteries de basculement</strong> promeuvent un réplica quand le propriétaire semble mort. Sur un WAN, « semble mort » et « est juste très loin » deviennent indiscernables, d'où des promotions parasites — et si la liaison se rétablit avec un réplica promu de chaque côté, vous avez deux propriétaires pour une même partition.</li>
</ul>
<p>Rien de tout cela n'est un défaut. C'est un store qui fait exactement ce qu'un store intra-datacenter à faible latence devrait faire, appliqué à un réseau qu'il n'a jamais été destiné à couvrir. La solution n'est pas de relâcher chaque minuterie jusqu'à ce que le cluster tolère un WAN — cela le rendrait aussi beaucoup trop lent à tolérer de vraies défaillances. La solution est de cesser de demander à un seul cluster de le faire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-brique-que-vous-possédez-déjà--un-log-rejouable">La brique que vous possédez déjà : un log rejouable<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#la-brique-que-vous-poss%C3%A9dez-d%C3%A9j%C3%A0--un-log-rejouable" class="hash-link" aria-label="Lien direct vers La brique que vous possédez déjà : un log rejouable" title="Lien direct vers La brique que vous possédez déjà : un log rejouable" translate="no">​</a></h2>
<p>Zaris conserve, par segment, un <strong>log d'opérations ordonné par révision</strong>. Chaque écriture acceptée fait avancer une révision monotone et ajoute une entrée. Ce log est le substrat que les réplicas et les nouveaux propriétaires de partition utilisent pour <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">se remettre à niveau sans geler les écritures</a> : un consommateur qui connaît la dernière révision qu'il a vue peut rejouer en avant depuis ce point et converger vers la vérité courante.</p>
<p>Cette propriété — <em>rejouer en avant depuis une révision connue</em> — est tout le fondement d'un réplicateur WAN. Un change feed n'est rien de plus qu'un lecteur de longue durée de ce log qui diffuse les entrées dans l'ordre des révisions et se souvient de l'endroit où il s'est arrêté. Ce dont il se souvient est un <strong>curseur</strong> : la révision la plus haute qu'il a durablement appliquée, par segment.</p>
<!-- -->
<p>Parce que le log est ordonné par révision et que la relecture démarre d'un point que vous choisissez, le feed possède les deux traits qu'exige une liaison WAN :</p>
<ol>
<li class=""><strong>Reprenable.</strong> La liaison <em>va</em> tomber — c'est le fait qui définit un WAN. Quand elle revient, le suiveur se reconnecte et demande tout ce qui suit son curseur stocké. Pas de re-copie complète, pas de coordination, juste « reprendre à la révision N ».</li>
<li class=""><strong>Idempotent à la relecture.</strong> N'appliquez une entrée que lorsque sa révision est supérieure à la dernière appliquée pour ce segment. Si une reconnexion redélivre la queue, le suiveur saute ce qu'il possède déjà. La livraison en double devient un no-op, ce qui permet au feed d'être au-moins-une-fois au lieu de devoir être exactement-une-fois.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-motif-de-bout-en-bout">Le motif, de bout en bout<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#le-motif-de-bout-en-bout" class="hash-link" aria-label="Lien direct vers Le motif, de bout en bout" title="Lien direct vers Le motif, de bout en bout" translate="no">​</a></h2>
<p>Deux clusters. Appelons-les <strong>A</strong> (la source) et <strong>B</strong> (le suiveur). Chacun est un cluster Zaris ordinaire et indépendant qui ne sait rien des rouages internes de l'autre.</p>
<p>Dans le cluster B, vous exécutez un <strong>suiveur de réplication</strong> — un client qui consomme le change feed de A et écrit les opérations dans B localement. Son état est minuscule et durable : un curseur par segment. La boucle est sans gloire, ce qui est tout l'intérêt :</p>
<ol>
<li class="">Se connecter à A, demander le change feed pour les segments (ou préfixes de clé) que vous voulez répliquer, en commençant après le curseur stocké de chaque segment.</li>
<li class="">Recevoir les entrées dans l'ordre des révisions. Pour chacune, appliquer l'opération à B — <code>SET</code>, <code>DELETE</code>, expiration — gardée par la vérification de révision afin que la redélivraison soit inoffensive.</li>
<li class="">Faire avancer et persister le curseur au fur et à mesure que les entrées sont durablement appliquées.</li>
<li class="">À toute déconnexion, revenir à l'étape 1. Le curseur est là où vous vous êtes arrêté.</li>
</ol>
<p>Tout ce dont le suiveur a besoin pour survivre à une interruption vit dans ce curseur. Il n'y a pas de transaction distribuée à travers le WAN, pas d'appartenance partagée, pas de quorum inter-régions. Un hoquet sur la liaison ne vous coûte qu'un peu de fraîcheur.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-vous-échangez--le-rpo-nest-pas-nul-et-cest-le-marché">Ce que vous échangez : le RPO n'est pas nul, et c'est le marché<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#ce-que-vous-%C3%A9changez--le-rpo-nest-pas-nul-et-cest-le-march%C3%A9" class="hash-link" aria-label="Lien direct vers Ce que vous échangez : le RPO n'est pas nul, et c'est le marché" title="Lien direct vers Ce que vous échangez : le RPO n'est pas nul, et c'est le marché" translate="no">​</a></h2>
<p>Cette réplication est <strong>asynchrone</strong>. Le suiveur est toujours à quelque distance derrière la source — autant de révisions produites dans A mais pas encore appliquées dans B. Cette distance est votre <strong>Objectif de point de récupération</strong> (Recovery Point Objective) : si la région A disparaît à l'instant, vous perdez tout ce qui n'avait pas encore atteint B.</p>
<p>Ce n'est pas un défaut dont il faut s'excuser ; c'est le compromis que vous choisissez explicitement. L'alternative — faire attendre aux écritures de A un accusé de réception de B avant de valider — mettrait un aller-retour WAN sur votre chemin d'écriture et abandonnerait la faible latence qui est toute la raison pour laquelle vous exécutez Zaris. La réplication inter-régions synchrone transforme chaque écriture en une requête transcontinentale. Presque personne n'en veut réellement une fois la facture de latence vue.</p>
<p>Le cadrage honnête est donc : <strong>vous choisissez le RPO.</strong> Un suiveur bien alimenté sur une liaison saine reste à quelques révisions près — une obsolescence inférieure à la seconde en pratique. Une liaison saturée ou qui flappe le laisse dériver davantage. Le curseur rend la dérive <em>bornée et visible</em> — vous pouvez mesurer exactement à quelle distance B est derrière en comparant son curseur à la révision courante de A — mais il ne la rend pas nulle. Dimensionnez votre tolérance en conséquence, et alertez sur le retard de curseur comme vous alerteriez sur le retard d'un réplica à l'intérieur d'un cluster.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quand-la-source-bascule-le-feed-sen-moque">Quand la source bascule, le feed s'en moque<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#quand-la-source-bascule-le-feed-sen-moque" class="hash-link" aria-label="Lien direct vers Quand la source bascule, le feed s'en moque" title="Lien direct vers Quand la source bascule, le feed s'en moque" translate="no">​</a></h2>
<p>Voici où le fait de bâtir sur le log d'opérations paie une seconde fois. À l'intérieur du cluster A, la partition que lit votre feed changera occasionnellement de propriétaire — un nœud meurt, un réplica est promu, une partition migre. Le log survit à cela : il est indexé par révision, et un nouveau propriétaire poursuit la même séquence de révisions. Pour le suiveur, un basculement côté source n'est qu'une reconnexion de plus. Il rétablit le feed contre la topologie courante de A et reprend depuis son curseur. Les révisions qu'il a déjà appliquées sont toujours les révisions qu'il a déjà appliquées ; celles qui lui manquent sont toujours après le curseur. Le suiveur n'a jamais à comprendre <em>qui</em>, dans A, possède la partition — seulement <em>quelle révision</em> il a vue en dernier.</p>
<p>C'est la raison discrète pour laquelle la conception curseur-sur-log vaut plus qu'un relais naïf « lire la queue des écritures et les expédier ». Un relais qui diffuse les écritures en direct sans position durable indexée par révision n'a rien depuis quoi reprendre quand la topologie source change. Le log donne au suiveur un système de coordonnées stable qui survit à n'importe quel propriétaire individuel.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-portée-est-un-choix-et-les-conflits-sont-une-frontière">La portée est un choix, et les conflits sont une frontière<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#la-port%C3%A9e-est-un-choix-et-les-conflits-sont-une-fronti%C3%A8re" class="hash-link" aria-label="Lien direct vers La portée est un choix, et les conflits sont une frontière" title="Lien direct vers La portée est un choix, et les conflits sont une frontière" translate="no">​</a></h2>
<p>Deux autres choses à décider délibérément.</p>
<p><strong>Portée des données.</strong> Vous n'avez pas à tout répliquer. Le feed est par segment, et vous pouvez sélectionner les segments ou préfixes de clé qui comptent — un locataire spécifique, un jeu de données spécifique, la tranche dont une région de reprise après sinistre a réellement besoin. Répliquer moins signifie moins de trafic WAN et un suiveur plus petit ; répliquer tout vous donne un secours chaud complet. Décidez à dessein plutôt que par défaut.</p>
<p><strong>Direction et conflits.</strong> Le motif ci-dessus est <strong>unidirectionnel</strong> : A est la source de vérité, B est un suiveur / secours majoritairement en lecture. C'est le cas propre, et il n'a pas de problème de conflit — il y a exactement un rédacteur par clé. Au moment où vous le rendez <strong>bidirectionnel</strong> — les deux régions acceptant des écritures pour les mêmes clés — vous avez introduit des rédacteurs concurrents séparés par un WAN, et aucune plomberie de change feed ne résout cela pour vous. Vous devez désormais une politique de résolution de conflits : last-writer-wins sur une horloge en laquelle vous avez confiance, possession par clé pour que les deux régions n'écrivent jamais la même clé, ou fusion au niveau applicatif. L'actif-actif est une conception réelle et parfois nécessaire, mais c'est une décision de <em>modélisation des données</em>, pas une fonctionnalité de transport. Optez d'abord pour l'unidirectionnel ; n'adoptez le bidirectionnel que lorsque vous avez décidé, explicitement, comment deux rédacteurs se réconcilient.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forme-à-retenir">La forme à retenir<a href="https://clustron.io/fr/blog/cross-datacenter-change-feed/#la-forme-%C3%A0-retenir" class="hash-link" aria-label="Lien direct vers La forme à retenir" title="Lien direct vers La forme à retenir" translate="no">​</a></h2>
<!-- -->
<p>Un WAN n'est pas un LAN lent ; c'est un médium différent où les partitions sont un événement normal plutôt qu'une urgence. Un cluster unique, à juste titre, refuse de prétendre le contraire — ses minuteries et son protocole de possession sont réglés pour traiter un long silence comme une défaillance, car à l'intérieur d'un datacenter c'en est presque toujours une. Donc vous n'étirez pas le cluster. Vous en exécutez deux, chacun souverain sur son propre réseau, et vous les reliez avec la seule primitive bâtie pour tolérer une liaison qui va et vient : un log ordonné par révision que vous pouvez rejouer, et un curseur qui se souvient d'où vous en étiez.</p>
<p>L'indépendance est la décision qui rend le reste sûr. Le change feed n'est que le tuyau. Le log d'opérations vous a déjà donné le curseur.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
            <category>Opérations</category>
        </item>
        <item>
            <title><![CDATA[Le plancher de latence : une microseconde de travail dans dix de plomberie]]></title>
            <link>https://clustron.io/fr/blog/latency-floor/</link>
            <guid>https://clustron.io/fr/blog/latency-floor/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Nous avons instrumenté le chemin qu'un seul GET parcourt à travers un nœud Zaris, de la socket à la partition et retour. Le store en mémoire fait son travail en environ 1,7 microseconde. L'opération en coûte dix à treize. Cet article explique où partent les onze autres — la chaîne de sauts en sortie, les copies, le hachage calculé trois fois, et une vérification de licence qui prend un verrou à chaque requête — et ce que leur réduction exige réellement.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Une microseconde de travail dans dix microsecondes de plomberie" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJUaGUgbGF0ZW5jeSBmbG9vcjogdGhlIHN0b3JlIGRvZXMgYSBtaWNyb3NlY29uZCBvZiB3b3JrIGluc2lkZSB0ZW4gbWljcm9zZWNvbmRzIG9mIHBsdW1iaW5nIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+UEVSRk9STUFOQ0U8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+VGhlIGxhdGVuY3kgZmxvb3I8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5PbmUgbWljcm9zZWNvbmQgb2Ygd29yayBpbnNpZGUgdGVuIG9mIHBsdW1iaW5nPC90ZXh0Pg0KDQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwLDI1MCkiPg0KICAgIDx0ZXh0IHg9IjAiIHk9Ii0xNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGxldHRlci1zcGFjaW5nPSIyIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjNWE2YjY1Ij5TRVJWRVIgQ1BVIFBFUiBHRVQ8L3RleHQ+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEwMzAiIGhlaWdodD0iNTYiIHJ4PSI4IiBmaWxsPSIjMGMxYjE2IiBzdHJva2U9IiMxZDNhMzAiIHN0cm9rZS13aWR0aD0iMS41Ii8+DQogICAgPHJlY3QgeD0iNCIgeT0iNCIgd2lkdGg9IjE0NiIgaGVpZ2h0PSI0OCIgcng9IjYiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICAgIDx0ZXh0IHg9Ijc3IiB5PSIzMyIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwNjIzMWEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmxvZ2ljPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijc3IiB5PSItMTYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjMmJkNmEwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5+MS43IHVzPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjU5MCIgeT0iMzMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5lZ3Jlc3MgaG9wcyAmIzE4MzsgY29waWVzICYjMTgzOyByZWR1bmRhbnQgaGFzaGluZyAmIzE4MzsgcGVyLW9wIGxpY2Vuc2UgbG9jazwvdGV4dD4NCiAgICA8dGV4dCB4PSIxMDIwIiB5PSItMTYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjNWE2YjY1IiB0ZXh0LWFuY2hvcj0iZW5kIj5+MTAmIzgyMTE7MTMgdXMgdG90YWw8L3RleHQ+DQogIDwvZz4NCg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4MCwzNTApIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+DQogICAgPGNpcmNsZSBjeD0iNiIgY3k9Ii00IiByPSI0IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iMjIiIHk9IjAiPjMgY2hhbm5lbHMsIDMgd2FrZS11cHMsIGNvcGllZCA0eCBvbiB0aGUgd2F5IG91dDwvdGV4dD4NCiAgICA8Y2lyY2xlIGN4PSI2IiBjeT0iMjYiIHI9IjQiIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMzAiPkNSQzE2IHJlY29tcHV0ZWQgM3ggcGVyIEdFVCwgYml0LXNlcmlhbDwvdGV4dD4NCiAgICA8Y2lyY2xlIGN4PSI2IiBjeT0iNTYiIHI9IjQiIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8dGV4dCB4PSIyMiIgeT0iNjAiPmxpY2Vuc2UgdmVyZGljdCB0YWtlbiB1bmRlciBhIHByb2Nlc3Mtd2lkZSBsb2NrLCBldmVyeSByZXF1ZXN0PC90ZXh0Pg0KICA8L2c+DQoNCiAgPHRleHQgeD0iODAiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDYiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>Voici un nombre qui recadre la façon dont vous pensez un store clé/valeur. Quand nous isolons le store en mémoire de Zaris et lui envoyons des opérations sans rien d'autre sur le chemin — pas de socket, pas de sérialisation, pas de routage — un seul thread sert les lectures à plus d'un million par seconde, et monte proprement à plus de huit millions sur huit threads. Le store, autrement dit, fait le travail réel d'un GET en moins de deux microsecondes, et il n'est emphatiquement <em>pas</em> le goulet d'étranglement.</p>
<p>Et pourtant un GET complet sur une vraie connexion TCP, sur la même machine, plafonne à quelques centaines de milliers par seconde. Le store peut faire le travail cinq fois plus vite que le serveur ne peut le délivrer. La question de performance intéressante au sujet de Zaris n'est donc pas « à quelle vitesse va le store ? ». C'est l'inverse : <em>où part tout le temps entre la socket et le store ?</em> Cet article en est la réponse, tracée opération par opération, car l'écart entre 1,7 microseconde de logique et 10 à 13 microsecondes de CPU serveur par opération n'est pas du bruit — c'est le plancher de latence, et il est presque entièrement de la plomberie.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-mesure-qui-lance-toute-lenquête">La mesure qui lance toute l'enquête<a href="https://clustron.io/fr/blog/latency-floor/#la-mesure-qui-lance-toute-lenqu%C3%AAte" class="hash-link" aria-label="Lien direct vers La mesure qui lance toute l'enquête" title="Lien direct vers La mesure qui lance toute l'enquête" translate="no">​</a></h2>
<p>On ne peut pas optimiser un chemin qu'on n'a pas séparé en couches, donc la première chose que nous avons construite fut un ensemble de benchmarks d'ablation — des harnais qui dépouillent le système jusqu'à une couche à la fois.</p>
<ul>
<li class=""><strong>Store seul.</strong> Appeler directement le store en processus de la partition, en processus, sans réseau. Résultat : ~1,15M GET/s et ~0,5M PUT/s <em>par thread</em>, montant linéairement à ~8,2M / ~2,0M sur huit threads.</li>
<li class=""><strong>Écho de socket nu.</strong> Une simple socket .NET faisant des trames de même forme, sans logique Zaris : ~7,2M trames/s sur la même machine.</li>
<li class=""><strong>Chemin TCP natif complet.</strong> Le vrai client parlant au vrai serveur : ~300-324K ops/s, avec seulement environ trois cœurs logiques sur quatorze occupés de chaque côté.</li>
</ul>
<p>Lisez ces trois résultats ensemble et deux suspects habituels s'écartent immédiatement. Le store n'est pas la limite — il est 3 à 4 fois plus rapide que le chemin complet. Le système d'exploitation et le loopback ne sont pas non plus la limite — une socket nue déplace des trames vingt fois plus vite. Ce qui reste au milieu, entre une socket qui peut faire des millions et un store qui peut faire des millions, c'est la plomberie de requête propre à Zaris, et elle dépense environ <strong>10 à 13 microsecondes de CPU serveur par opération pour envelopper environ 1,7 microseconde de travail réel.</strong></p>
<p>Ce ratio est le sujet. Suivons un GET à travers lui.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-chemin-dun-seul-get">Le chemin d'un seul GET<a href="https://clustron.io/fr/blog/latency-floor/#le-chemin-dun-seul-get" class="hash-link" aria-label="Lien direct vers Le chemin d'un seul GET" title="Lien direct vers Le chemin d'un seul GET" translate="no">​</a></h2>
<!-- -->
<p>Rien dans ce diagramme n'est faux ou gaspilleur au premier regard — c'est la forme honnête d'un store en réseau. Le coût est dans les <em>détails</em> de chaque flèche : combien de fois un thread doit se réveiller, combien de fois les octets sont copiés, et combien de travail fixe par requête voyage avec, sans aucun rapport avec la clé lue. Ces détails sont là où vivent les onze microsecondes, et ils se regroupent en trois ensembles.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="goulet-détranglement-un--la-chaîne-de-sauts-en-sortie">Goulet d'étranglement un : la chaîne de sauts en sortie<a href="https://clustron.io/fr/blog/latency-floor/#goulet-d%C3%A9tranglement-un--la-cha%C3%AEne-de-sauts-en-sortie" class="hash-link" aria-label="Lien direct vers Goulet d'étranglement un : la chaîne de sauts en sortie" title="Lien direct vers Goulet d'étranglement un : la chaîne de sauts en sortie" translate="no">​</a></h2>
<p>Le plus gros coût n'est pas la lecture de la valeur — c'est sa <em>livraison</em>. Renvoyer une réponse au client, telle qu'actuellement structurée, traverse <strong>trois canaux internes, réveille trois threads et copie la charge utile quatre fois</strong> sur son chemin de sortie :</p>
<ol>
<li class="">La réponse est passée du shard worker au chemin d'envoi du pipeline serveur.</li>
<li class="">Le chemin d'envoi la passe à une boucle d'écriture par connexion, qui l'encode dans un buffer poolé.</li>
<li class="">La boucle d'écriture la met en file dans la file d'envoi de la connexion de transport.</li>
<li class="">Le transport fusionne les buffers en file en un flux et écrit sur la socket.</li>
</ol>
<p>Chaque transmission est un réveil de thread, et chaque réveil est un événement d'ordonnancement que l'OS doit traiter — bon marché individuellement, ruineux à des centaines de milliers par seconde. Et à chaque étape les octets sont copiés à nouveau : buffer d'encodage poolé, file d'envoi, buffer de fusion, socket. Côté <em>entrée</em> il y a un coût symétrique : chaque requête qui arrive sur un thread de complétion IOCP est transmise à un shard worker avant qu'aucun travail n'ait lieu, ce qui est un autre réveil et une autre file.</p>
<p>La raison pour laquelle cette structure existe est raisonnable — découpler réception, dispatch, encodage et transmission rend chaque couche plus simple à raisonner et laisse les consommateurs lents exercer une contre-pression sans bloquer la socket. Mais pour le cas courant — une lecture RF1 dont la réponse est déjà en mémoire sur le nœud qui a reçu la requête — c'est énormément plus de machinerie que le travail ne le mérite. La réponse pourrait, en principe, être calculée et réécrite <em>sur le chemin de réception lui-même</em>, avec une copie dans le buffer de socket et aucune transmission du tout.</p>
<p>C'est la première et la plus grosse pièce de travail structurel que la mesure désigne : <strong>réduire la sortie à un saut et une copie</strong> pour le chemin chaud, et exécuter la lecture/écriture RF1 en ligne sur le thread de réception plutôt que de la faire rebondir à travers le pool de workers. C'est la différence entre une conception facile à dessiner et une conception peu coûteuse à exécuter, et les deux ne sont pas la même chose.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="goulet-détranglement-deux--hachage-redondant-et-un-verrou-par-requête">Goulet d'étranglement deux : hachage redondant et un verrou par requête<a href="https://clustron.io/fr/blog/latency-floor/#goulet-d%C3%A9tranglement-deux--hachage-redondant-et-un-verrou-par-requ%C3%AAte" class="hash-link" aria-label="Lien direct vers Goulet d'étranglement deux : hachage redondant et un verrou par requête" title="Lien direct vers Goulet d'étranglement deux : hachage redondant et un verrou par requête" translate="no">​</a></h2>
<p>Le deuxième ensemble est la surcharge fixe par opération — du travail qui a lieu à <em>chaque</em> requête quelle que soit la clé touchée, et qui pour l'essentiel ne devrait pas.</p>
<p><strong>CRC16, trois fois, bit par bit.</strong> Zaris route les clés vers les partitions avec un hachage CRC16 — la même famille de hachage qu'utilise Redis Cluster, ce qui est <a class="" href="https://clustron.io/fr/blog/redis-protocol-any-language/">comment le front-end RESP reste compatible slots</a>. Le problème n'est pas le choix du hachage ; c'est que l'implémentation est <em>bit-sérielle</em> (en bouclant bit par bit plutôt qu'avec une table de correspondance) et qu'un seul GET le calcule <strong>trois fois</strong> lorsque la requête traverse le routage. À environ 260 nanosecondes par calcul, cela fait ~784 ns par GET passées à hacher — plus que tout l'aller-retour de décodage-plus-encodage MessagePack, qui tourne autour de 533 ns. Un CRC16 piloté par table calculé <em>une seule fois</em> et transporté ensuite transformerait le plus gros coût fixe en une erreur d'arrondi.</p>
<p><strong>Une vérification de licence qui prend un verrou, à chaque requête.</strong> Plus surprenant : le point d'entrée de la requête consulte le service de licence à chaque opération, et cette consultation prend un verrou à l'échelle du processus et lit l'horloge murale (<code>DateTime.UtcNow</code>) pour décider si le bail courant est toujours valide. Un verrou sur le chemin chaud est exactement le genre de chose qui ne se montre pas dans un micro-benchmark mono-thread puis sérialise silencieusement tout en concurrence. Le verdict de licence change au plus quelques fois sur la vie d'un processus — quand un bail est accordé, renouvelé ou expire. Il n'a rien à faire à être recalculé sous un verrou à chaque GET. La correction consiste à publier le verdict dans un unique champ <code>volatile</code> qu'un renouvellement en arrière-plan met à jour, et à laisser le chemin chaud le lire sans verrou ni appel d'horloge. (La machinerie de bail elle-même fait l'objet de l'<a class="" href="https://clustron.io/fr/blog/licensing-lifecycle/">article sur le cycle de vie des licences</a> ; le point ici est purement que <em>lire</em> le verdict doit être gratuit.)</p>
<p>Aucun de ces deux problèmes n'est un problème algorithmique profond. Ce sont le genre de coût qui s'accumule quand chaque pièce est écrite correctement en isolation et que personne n'a encore mesuré ce que la somme coûte sur le chemin qui s'exécute un million de fois par seconde.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="goulet-détranglement-trois--le-client-dépense-autant-que-le-serveur">Goulet d'étranglement trois : le client dépense autant que le serveur<a href="https://clustron.io/fr/blog/latency-floor/#goulet-d%C3%A9tranglement-trois--le-client-d%C3%A9pense-autant-que-le-serveur" class="hash-link" aria-label="Lien direct vers Goulet d'étranglement trois : le client dépense autant que le serveur" title="Lien direct vers Goulet d'étranglement trois : le client dépense autant que le serveur" translate="no">​</a></h2>
<p>Il serait commode de tout mettre sur le dos du serveur, mais l'ablation dit le contraire : les benchmarks locaux sont <strong>limités par le client</strong>, et épingler le client sur les cœurs performants seuls l'a fait passer de 169K à 299K ops/s. Le client dépense environ <strong>9 microsecondes et 1,8 Ko par GET</strong>, et les coûts riment avec ceux du serveur :</p>
<ul>
<li class=""><strong>Les complétions rebondissent à travers le thread pool.</strong> Chaque requête en attente est suivie par un <code>TaskCompletionSource</code> configuré pour exécuter sa continuation de façon asynchrone, de sorte que lorsque la réponse arrive la complétion est postée sur le thread pool plutôt que reprise en ligne — un autre saut d'ordonnancement, à chaque opération.</li>
<li class=""><strong>Une variable d'environnement est lue à chaque opération.</strong> Le client appelle <code>Environment.GetEnvironmentVariable("ZARIS_REQUEST_TIMEOUT")</code> <em>par requête</em> pour décider de son échéance. Lire une variable d'environnement parcourt le bloc d'environnement du processus ; le faire un million de fois par seconde est la plus grosse trame rédigée par Zaris côté client. Elle devrait être lue une fois au démarrage et mise en cache.</li>
<li class=""><strong>Un sémaphore et un verrou de canal par opération</strong>, plus des allocations par requête — un id client estampillé sur chaque requête, des métadonnées d'entrée demandées et renvoyées même quand l'appelant ne les a pas demandées.</li>
</ul>
<p>Le fil conducteur avec le serveur est identique : le travail d'un GET est minuscule, et il est entouré de sauts d'ordonnancement, de verrous et d'allocations qui semblaient chacun gratuits en isolation. Le chemin chaud du client a déjà emprunté la bonne voie — le <a class="" href="https://clustron.io/fr/blog/valuetask-hot-path/">passage à <code>ValueTask</code> pour les opérations de base</a> a supprimé une allocation par appel lors d'une complétion synchrone — et le reste de la liste est la même idée appliquée plus loin : mettre en cache la variable d'environnement, compléter sans le rebond sur le thread pool, recevoir sur la socket avec un <code>ValueTask</code>, et cesser d'allouer des choses que personne n'a demandées.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="alors-quel-est-le-plancher-vraiment-">Alors, quel est le plancher, vraiment ?<a href="https://clustron.io/fr/blog/latency-floor/#alors-quel-est-le-plancher-vraiment-" class="hash-link" aria-label="Lien direct vers Alors, quel est le plancher, vraiment ?" title="Lien direct vers Alors, quel est le plancher, vraiment ?" translate="no">​</a></h2>
<p>Faites l'addition et l'image honnête est celle-ci. Sur un ordinateur portable, le chemin TCP natif plafonne autour de 300K ops/s non pas parce que le store est lent — il peut faire des millions — mais parce que chaque opération traîne 10 à 13 microsecondes de CPU serveur et ~9 microsecondes de CPU client à travers une chaîne de sauts, de copies, de hachages redondants et de verrous. Le <em>plancher</em> est fixé par la plomberie, pas par le travail.</p>
<p>Le corollaire encourageant est que le plancher est fait de choses réparables, et l'estimation de ce que leur réparation rapporte est concrète. La liste structurelle est :</p>
<ol>
<li class="">Réduire la sortie à un saut et une copie.</li>
<li class="">Exécuter le chemin chaud RF1 en ligne sur le thread de réception, ou par lots à chaque réception.</li>
<li class="">CRC16 piloté par table, haché une fois par opération.</li>
<li class="">Complétions client sans allocation ni rebond sur le thread pool.</li>
<li class="">Réception de socket fondée sur <code>ValueTask</code> côté client.</li>
<li class="">Mettre en cache la variable d'environnement de timeout de requête plutôt que de la lire par opération.</li>
<li class="">Publier le verdict de licence dans un champ <code>volatile</code> — pas de verrou, pas d'horloge, sur le chemin chaud.</li>
<li class="">Un régime d'allocation général : réponses poolées, pas d'id client par requête, pas de métadonnées non demandées.</li>
</ol>
<p>Ensemble, on estime que cela fait passer le CPU serveur par opération de ~10 à ~3-4 microsecondes — ce qui est la différence entre avoir besoin d'une douzaine de cœurs occupés pour un million d'ops/s et en avoir besoin d'environ quatre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-mises-en-garde-honnêtes">Les mises en garde honnêtes<a href="https://clustron.io/fr/blog/latency-floor/#les-mises-en-garde-honn%C3%AAtes" class="hash-link" aria-label="Lien direct vers Les mises en garde honnêtes" title="Lien direct vers Les mises en garde honnêtes" translate="no">​</a></h2>
<p>Deux choses gardent cet article véridique plutôt que triomphant.</p>
<p>Premièrement, <strong>les nombres absolus ici sont des nombres d'ordinateur portable</strong>, et ils sont probablement 2 à 4 fois trop bas face à du vrai matériel serveur avec des machines client dédiées. Ce qui se transfère entre environnements n'est pas le débit absolu — c'est l'<em>attribution relative</em> : le store n'est pas le goulet d'étranglement, la sortie est le plus gros coût serveur, et le client dépense autant que le serveur. Ces conclusions sont indépendantes de la machine ; le chiffre final d'ops/s ne l'est pas, et la vraie validation se fait à l'échelle sur <a class="" href="https://clustron.io/fr/blog/kubernetes-durability-sizing/">Kubernetes</a>, pas sur un poste de travail.</p>
<p>Deuxièmement, <strong>la réplication change le plafond.</strong> Tout ce qui précède est l'histoire RF1 — une seule copie, le cas rapide. Un PUT RF2 doit répliquer durablement vers une sauvegarde avant de pouvoir accuser réception, et c'est un chemin véritablement différent et plus coûteux ; une cible réaliste pour lui est de 400-500K par nœud, pas un million. Et quand vous <em>n'avez pas</em> besoin d'allers-retours par opération du tout, le <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">chemin par lots</a> amortit presque tout ce coût fixe sur de nombreuses opérations et peut atteindre un million par nœud dès aujourd'hui. Le chiffre d'un million d'ops par nœud unique pour des opérations individuelles est une cible que le travail structurel ci-dessus est censé atteindre — pas un nombre que nous citons comme livré.</p>
<p>Cette distinction est toute la discipline d'un travail de performance digne de confiance : séparer ce que la mesure <em>prouve</em> (le store est rapide, la plomberie est le plancher, et voici où part chaque microseconde) de ce qu'elle <em>promet</em> (un nombre précis après une liste précise de changements). La première est faite et solide. Le second est de l'ingénierie que nous pouvons désormais faire avec une carte au lieu d'une supposition — ce qui est toute la raison pour laquelle on instrumente le chemin avant d'y toucher.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Performance</category>
            <category>.NET</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Migrer une partition sans perte de données : le protocole de cut-over]]></title>
            <link>https://clustron.io/fr/blog/partition-cutover-protocol/</link>
            <guid>https://clustron.io/fr/blog/partition-cutover-protocol/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Une partition doit changer de propriétaire pendant que les écritures continuent d'y atterrir. Suivez la séquence exacte qu'exécute Zaris — un snapshot ancré sur une révision, un delta final sous barrière d'écriture conditionné par un digest de contenu, une réconciliation de réplica vivant, le basculement atomique de cut-over par le leader, et un drop qui refuse de détruire l'unique copie de la source tant que son digest correspond toujours à la preuve que le nouveau propriétaire a reçue.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Le protocole de cut-over de partition de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNaWdyYXRpbmcgYSBwYXJ0aXRpb24gd2l0aCB6ZXJvIGRhdGEgbG9zczogdGhlIGN1dC1vdmVyIHByb3RvY29sIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+TUlHUkFUSU9OPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPk1vdmluZyBhIHBhcnRpdGlvbiw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+bG9zaW5nIG5vIHdyaXRlPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+U25hcHNob3QgJiM4NTk0OyBmZW5jZWQgZmluYWwgZGVsdGEgJiM4NTk0OyBkaWdlc3QgcHJvb2YgJiM4NTk0OyBzYWZlIGRyb3A8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwLDI4NikiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjIyIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjU4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNDciIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zb3VyY2U8L3RleHQ+DQogICAgPHRleHQgeD0iNzUiIHk9IjY4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+c2VydmVzIHVudGlsIGZsaXA8L3RleHQ+DQogICAgPHJlY3QgeD0iODYwIiB5PSIyMiIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI1OCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSI5MzUiIHk9IjQ3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bmV3IG93bmVyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjkzNSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5wcm92ZW4gJiM4ODM5OyBzb3VyY2U8L3RleHQ+DQogICAgPHBhdGggZD0iTTE1MCA0MiBMODU4IDQyIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPGNpcmNsZSBjeD0iMzAwIiBjeT0iNDIiIHI9IjciIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8Y2lyY2xlIGN4PSI1MDAiIGN5PSI0MiIgcj0iNyIgZmlsbD0iIzJiZDZhMCIvPg0KICAgIDxjaXJjbGUgY3g9IjcwMCIgY3k9IjQyIiByPSI3IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iMzAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnNuYXBzaG90IEByZXY8L3RleHQ+DQogICAgPHRleHQgeD0iNTAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmZlbmNlICsgZGVsdGE8L3RleHQ+DQogICAgPHRleHQgeD0iNzAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmRpZ2VzdCBtYXRjaDwvdGV4dD4NCiAgICA8dGV4dCB4PSI1MDAiIHk9IjExMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPmRyb3Agb25seSBpZiBsaXZlIGRpZ2VzdCBzdGlsbCBlcXVhbHMgdGhlIHNlcnZlZCBwcm9vZjwvdGV4dD4NCiAgPC9nPg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>Un <a class="" href="https://clustron.io/fr/blog/anatomy-of-a-failover/">basculement</a>, c'est ce qui arrive quand un propriétaire <em>meurt</em> — le cluster a quelques secondes pour choisir un survivant et la partition map ne bouge jamais. La migration est le cousin plus calme et plus dangereux : le propriétaire est parfaitement sain, et vous voulez déplacer une partition hors de lui <em>à dessein</em> — pour faire grandir le cluster, pour rééquilibrer, pour drainer un nœud que vous êtes sur le point de retirer. Rien n'a échoué, ce qui est exactement pourquoi la barre est plus haute. Un basculement a droit à quelques écritures de queue honnêtement perdues dans le pire des cas ; une migration planifiée, non. Si déplacer une partition du nœud A au nœud B peut perdre une seule écriture acquittée, la migration n'est pas une fonctionnalité, c'est un passif.</p>
<p>Cet article parcourt le protocole de cut-over que Zaris exécute réellement, dans l'ordre, et passe l'essentiel de son temps sur l'étape que tout le monde rate : le moment où la source est enfin « libre » de déposer sa copie. Cette étape est un pistolet armé, et l'ingénierie intéressante est la sécurité dessus.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forme-du-problème">La forme du problème<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#la-forme-du-probl%C3%A8me" class="hash-link" aria-label="Lien direct vers La forme du problème" title="Lien direct vers La forme du problème" translate="no">​</a></h2>
<p>Une partition est un ensemble de segments, et un segment est un sac d'enregistrements clé-valeur, chacun portant une révision monotone. La migration déplace les données d'un segment d'un nœud <strong>source</strong> vers un nœud <strong>cible</strong> puis bascule la possession pour que les clients routent vers la cible. Le tout se produit pendant que le segment est <em>vivant</em> : lectures et écritures continuent d'atterrir tout du long.</p>
<p>Cette vivacité est le problème. Si vous prenez un snapshot de la source, expédiez les octets, et basculez la possession, chaque écriture qui a atterri <em>pendant</em> l'expédition est sur la source et pas sur la cible — et à l'instant où vous basculez, ces écritures disparaissent de la vue routée. Vous les avez acquittées ; on a dit au client « validé » ; maintenant elles sont perdues. La migration naïve perd exactement les écritures survenues pendant qu'elle était occupée à être prudente à propos de celles survenues avant.</p>
<p>Le protocole doit donc faire deux choses en tension : continuer de servir les écritures sur la source jusqu'au basculement, <em>et</em> garantir que la cible détient chacune de ces écritures avant que le basculement ne prenne effet. La façon dont Zaris réconcilie cette tension est une séquence de cinq étapes, dont chacune refuse d'avancer tant que la précédente n'est pas prouvée complète.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-1--un-snapshot-ancré-sur-une-révision-avec-le-plancher-de-trim-épinglé">Étape 1 — un snapshot ancré sur une révision, avec le plancher de trim épinglé<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#%C3%A9tape-1--un-snapshot-ancr%C3%A9-sur-une-r%C3%A9vision-avec-le-plancher-de-trim-%C3%A9pingl%C3%A9" class="hash-link" aria-label="Lien direct vers Étape 1 — un snapshot ancré sur une révision, avec le plancher de trim épinglé" title="Lien direct vers Étape 1 — un snapshot ancré sur une révision, avec le plancher de trim épinglé" translate="no">​</a></h2>
<p>La cible démarre en demandant à la source les données du segment (<code>SegmentMigrationRequest</code>). La source ne sérialise pas simplement son dictionnaire courant ; elle prend un <strong>snapshot ancré sur une révision</strong>. Elle enregistre la révision courante du segment — appelons-la <code>R</code> — et capture les enregistrements vivants à hauteur de <code>R</code>.</p>
<p>Deux subtilités vivent dans cette seule étape. D'abord, la source <strong>arme son op-log avant de capturer l'ancre</strong>. Zaris peut sauter l'op-log pour un segment à primaire unique (RF1) comme optimisation du chemin d'écriture ; l'armer d'abord signifie que chaque écriture à partir de ce point est loguée, de sorte qu'une écriture précédemment non loguée (skip-logged) a toujours une révision à ou en dessous de <code>R</code> et est portée par le snapshot lui-même. Le delta que nous calculons plus tard — « les ops après <code>R</code> » — est alors prouvé sans trou.</p>
<p>Ensuite, la source <strong>épingle le plancher de trim de son op-log à <code>R</code></strong> :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">snapshotRevision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> snapshotEntries</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">CreateSegmentSnapshot</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segmentId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Pin BEFORE streaming so any op the source acks between the snapshot and the</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// new owner's final-delta request is still replayable — the floor cannot advance past R.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">_store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">PinSegmentTrim</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segmentId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> snapshotRevision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Normalement l'op-log élague les anciennes entrées pour récupérer de la mémoire. Pendant une migration ce serait un désastre : si le plancher avançait au-delà de <code>R</code> pendant que le transfert en masse est en vol, les écritures entre <code>R</code> et le basculement seraient non-rejouables, et le delta final serait silencieusement partiel. Épingler le plancher à <code>R</code> garantit que ces écritures survivent jusqu'à ce que la cible les ait. La source diffuse ensuite les entrées du snapshot — <strong>et continue de servir lectures et écritures tout du long.</strong> Elle est toujours le propriétaire routé. Rien n'a encore bougé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-2--la-cible-attend-délibérément">Étape 2 — la cible attend, délibérément<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#%C3%A9tape-2--la-cible-attend-d%C3%A9lib%C3%A9r%C3%A9ment" class="hash-link" aria-label="Lien direct vers Étape 2 — la cible attend, délibérément" title="Lien direct vers Étape 2 — la cible attend, délibérément" translate="no">​</a></h2>
<p>La cible applique le snapshot en masse puis fait quelque chose qui ressemble à de l'inaction mais qui est le cœur de la conception : elle fait passer le segment dans un état <em>en attente de cut-over</em> et <strong>s'arrête</strong>. Elle n'accuse pas réception à la source. Elle ne revendique pas la possession. Elle ne dit pas au leader qu'elle a terminé. Le commentaire dans le code est brutal sur le pourquoi :</p>
<blockquote>
<p>NO Ack to the source (it is still the routed owner), NO ownership claim, and NO completion notify yet — the completion is withheld until the fenced final delta confirms this node holds everything the source acked.</p>
</blockquote>
<p>C'est le contrat « ancien-propriétaire-autoritaire ». Jusqu'au basculement, exactement un nœud répond pour le segment, et c'est la source. Une cible qui aurait brûlé l'étape — commencé à servir à partir d'un snapshot déjà périmé de quelques centaines d'écritures — serait un split-brain. La cible patiente donc, et demande la seule chose qui lui permettra de prouver qu'elle est à jour : le delta final.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-3--le-delta-final-sous-barrière-décriture-et-la-preuve-par-digest">Étape 3 — le delta final sous barrière d'écriture et la preuve par digest<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#%C3%A9tape-3--le-delta-final-sous-barri%C3%A8re-d%C3%A9criture-et-la-preuve-par-digest" class="hash-link" aria-label="Lien direct vers Étape 3 — le delta final sous barrière d'écriture et la preuve par digest" title="Lien direct vers Étape 3 — le delta final sous barrière d'écriture et la preuve par digest" translate="no">​</a></h2>
<p>La cible envoie un <code>SegmentMigrationFinalDeltaRequest</code> ancré à <code>R</code>. C'est là que la source fait la partie délicate.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Fence FIRST, then capture a revision after the fence — so no acked write can exist beyond it.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> fenceEpoch </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _topologyManager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">EngageMigrationWriteFence</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">CreationMapVersion</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> fenceRevision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetSegmentCurrentRevision</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Elle <strong>engage une barrière d'écriture</strong> sur le segment <em>puis</em> lit la révision courante. L'ordre compte : barrière d'abord, mesure ensuite, pour qu'il n'y ait aucune fenêtre où une écriture se glisse après la mesure. Puis elle lit chaque entrée d'op-log après <code>R</code> — le delta de tout ce qui a atterri pendant le transfert en masse — et calcule un <strong>digest de contenu</strong> de l'état vivant du segment. Ce digest est le pivot de tout le protocole, donc la source l'enregistre comme la <strong>preuve de cut-over</strong> :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Operations </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> finalOps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceDigest </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ComputeSegmentLiveDigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Record this served digest as the cut-over PROOF: the post-cut-over orphan drop will refuse to</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// destroy this segment's data unless its live digest still equals this exact value.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">_localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">RecordCutoverProofDigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Un mode de défaillance honnête est câblé juste ici. Si le plancher de trim <em>avait</em> d'une manière ou d'une autre avancé au-delà de <code>R</code> — un épinglage perdu ou régressé — alors « les ops après <code>R</code> » seraient incomplètes, et expédier ce delta perdrait silencieusement des écritures acquittées. La source refuse :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">_localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryGetSegmentOperationsAfter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SnapshotRevision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">out</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> finalOps</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Aborted </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AbortReason </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"FinalDeltaBelowTrimFloor"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// release the fence, re-snapshot, retry — a loud abort, never a silent partial cut-over.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Un abandon sous le plancher n'est pas une perte ; il démantèle et re-snapshote depuis une ancre fraîche. La règle que le code respecte est <em>un abandon bruyant plutôt qu'un partiel silencieux</em> — la migration préfère recommencer que basculer sur un delta incomplet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-4--la-barrière--la-cible-prouve-quelle-domine">Étape 4 — la barrière : la cible prouve qu'elle domine<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#%C3%A9tape-4--la-barri%C3%A8re--la-cible-prouve-quelle-domine" class="hash-link" aria-label="Lien direct vers Étape 4 — la barrière : la cible prouve qu'elle domine" title="Lien direct vers Étape 4 — la barrière : la cible prouve qu'elle domine" translate="no">​</a></h2>
<p>La cible applique le delta, puis recalcule son <em>propre</em> digest de contenu du segment et le compare à celui de la source :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> localDigest </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ComputeSegmentLiveDigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> matchesSource </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">localDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> StringComparison</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Si les deux digests sont égaux, la cible détient octet pour octet ce que la source détenait à la barrière — l'état de la cible est un sur-ensemble de chaque écriture que la source a acquittée. S'ils diffèrent, c'est presque toujours parce que le snapshot diffusé est encore en cours d'application quand le premier delta est calculé (le signal de complétion peut dépasser des lots de données en file), donc la cible reste en attente et laisse un réessai espacé redemander le delta plutôt que de tourner à vide. <strong>Une divergence ne bascule jamais.</strong> Le basculement est conditionné par une preuve positive d'égalité, pas par l'absence d'une erreur.</p>
<p>Il y a une chose de plus que la cible fait avant de crier victoire, et c'est une garantie que la plupart des implémentations de migration sautent. La source n'est pas la seule copie qui pourrait être en avance — un <em>réplica</em> du segment pourrait détenir une écriture que la source sous barrière a manquée. Donc avant de terminer, la cible intègre la queue en lecture seule de chaque réplica vivant :</p>
<blockquote>
<p>The flip may fire only when the new owner's digest dominates the source <strong>and every live replica</strong> of the segment.</p>
</blockquote>
<p>L'intégration préserve les révisions : les enregistrements plus anciens d'un réplica en retard sont ignorés par la garde de révision, et un réplica en avance contribue exactement l'écriture que la source n'avait pas. Ce n'est que lorsque la cible domine la source <em>et</em> chaque réplica vivant qu'elle poursuit. C'est l'invariant « durabilité avant possession » — la possession est une récompense pour une possession prouvable, pas une condition préalable à celle-ci.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-5--le-basculement-et-le-drop-qui-perd-presque-vos-données">Étape 5 — le basculement, et le drop qui perd presque vos données<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#%C3%A9tape-5--le-basculement-et-le-drop-qui-perd-presque-vos-donn%C3%A9es" class="hash-link" aria-label="Lien direct vers Étape 5 — le basculement, et le drop qui perd presque vos données" title="Lien direct vers Étape 5 — le basculement, et le drop qui perd presque vos données" translate="no">​</a></h2>
<p>Maintenant la cible dit au leader qu'elle a terminé (<code>SegmentMigrationCompletionNotify</code>). Le leader valide la complétion contre l'override de migration vivant et arme une <strong>régénération atomique de cut-over</strong> de la partition map — un unique changement de map versionné qui bascule le propriétaire du segment vers la cible et retire l'override de migration. Après que cette map s'applique, la cible est le propriétaire routé. Les clients routent vers elle. Le déplacement est, du point de vue du cluster, terminé.</p>
<p>Et la source, qui détient désormais un segment dont elle ne possède plus de rôle, exécute un balayage de ménage pour déposer la copie orpheline et récupérer la mémoire. C'est le pistolet armé.</p>
<p>Voici le piège, et il est assez subtil pour avoir survécu jusque dans une exécution de certification avant d'être attrapé (<code>Missing=8629</code> clés acquittées disparues). Une barrière d'écriture <strong>réduit</strong> l'acquittement mais, sur un segment à copie unique sous charge, ne le met pas parfaitement et instantanément à zéro. Cette séquence est donc possible :</p>
<ol>
<li class="">La source sert le digest du delta final comme preuve. La cible le fait correspondre et bascule.</li>
<li class="">Dans l'infime intervalle entre le service de la preuve et l'atterrissage du basculement, une écriture de plus est acquittée sur la source.</li>
<li class="">La cible n'a jamais reçu cette écriture — la preuve qu'elle a fait correspondre date d'<em>avant</em> elle.</li>
<li class="">La source, désormais orpheline, dépose sa copie pour récupérer de l'espace.</li>
<li class="">Cette dernière écriture n'existait que sur la source. Elle est perdue. On a dit à un client qu'elle était validée.</li>
</ol>
<p>La correction consiste à rendre le drop <em>conditionnel au fait que la preuve soit toujours vraie</em>. La source ne détruit pas aveuglément un segment orphelin. Elle compare le digest vivant <strong>courant</strong> du segment au digest qu'elle a enregistré comme preuve de cut-over, et ne détruit les données que s'ils correspondent toujours :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ShouldRetainAfterCutover</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> hasCutoverProof</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token class-name punctuation" style="color:#393A34">?</span><span class="token plain"> currentLiveDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token class-name punctuation" style="color:#393A34">?</span><span class="token plain"> proofDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">hasCutoverProof </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">IsNullOrEmpty</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">proofDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// not a proof-carrying cut-over segment -&gt; this policy does not intervene</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">currentLiveDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> proofDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> System</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">StringComparison</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Si une écriture a atterri après la preuve, le digest courant diffère du digest de preuve, <code>ShouldRetainAfterCutover</code> renvoie <code>true</code>, et la source <strong>conserve</strong> sa copie :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">CutoverDropSafetyPolicy</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ShouldRetainAfterCutover</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">hasProof</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> currentDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> proofDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">CutoverDropRetainedUnprovenTail</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">_logger</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Count</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> segmentId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// keep the store — a post-proof tail exists that the new owner has not received</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>L'asymétrie est tout l'enjeu. <strong>La conservation est toujours sûre</strong> : la source n'est plus le propriétaire routé, donc une copie conservée est un orphelin redondant et non servi — elle ne peut jamais causer de split-brain, elle coûte juste un peu de mémoire jusqu'à ce qu'un re-service redélivre la queue au nouveau propriétaire et qu'un balayage ultérieur, constatant que les digests concordent à nouveau, la dépose proprement. <strong>La destruction sur une preuve périmée n'est jamais sûre</strong> : c'est la seule opération qui peut supprimer définitivement une écriture acquittée. La politique est donc délibérément étroite — elle ne fait jamais qu'<em>outrepasser un drop qui aurait autrement lieu</em>, et seulement pour un segment portant une preuve enregistrée dont le digest ne correspond plus. Les segments sans preuve (une éviction, un basculement immédiat, un segment jamais migré) conservent leur comportement existant intact. La barrière ajoute une conservation ; elle n'ajoute jamais un drop.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-le-digest-et-pas-un-numéro-de-révision-">Pourquoi le digest, et pas un numéro de révision ?<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#pourquoi-le-digest-et-pas-un-num%C3%A9ro-de-r%C3%A9vision-" class="hash-link" aria-label="Lien direct vers Pourquoi le digest, et pas un numéro de révision ?" title="Lien direct vers Pourquoi le digest, et pas un numéro de révision ?" translate="no">​</a></h2>
<p>Vous pourriez demander pourquoi la preuve est un digest de contenu et pas simplement « la révision la plus haute que la cible a acquittée ». La réponse est qu'un numéro de révision vous dit <em>jusqu'où</em> la cible est allée, mais un digest vous dit <em>si ce qu'elle a obtenu est ce que vous aviez</em>. Les deux peuvent diverger dans les coutures — une intégration de réplica, un réessai ré-ancré, un snapshot encore en train de se vider — et la seule question qui garde les données en sécurité est la question de contenu : <em>le nouveau propriétaire détient-il exactement ceci ?</em> Un digest y répond directement. Un filigrane (watermark) ne répond qu'à un proxy de cette question, et les proxys sont là où les écritures acquittées vont mourir.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-cela-achète-honnêtement">Ce que cela achète, honnêtement<a href="https://clustron.io/fr/blog/partition-cutover-protocol/#ce-que-cela-ach%C3%A8te-honn%C3%AAtement" class="hash-link" aria-label="Lien direct vers Ce que cela achète, honnêtement" title="Lien direct vers Ce que cela achète, honnêtement" translate="no">​</a></h2>
<p>Dit simplement, la garantie est : <strong>une migration de partition planifiée ne perd pas d'écriture acquittée.</strong> Chaque écriture que la source a acquittée avant la barrière est dans le snapshot ou le delta ; chaque écriture qu'elle a acquittée <em>après</em> la preuve garde la copie de la source en vie jusqu'à ce que le nouveau propriétaire l'ait aussi. Le protocole abandonnera et recommencera bruyamment plutôt que de basculer sur un delta incomplet, et il intègre les réplicas vivants pour que le nouveau propriétaire domine chaque copie, pas seulement la source.</p>
<p>Ce qu'il n'est <em>pas</em>, c'est gratuit. Le delta final sous barrière est un aller-retour synchrone qui met brièvement les écritures sous barrière sur le segment en cours de cut-over — une pause courte et bornée sur un segment, pas sur la partition ni sur le cluster. Une migration qui ne peut pas converger (un plancher de trim qui n'arrête pas d'avancer, une queue de réplica qui ne peut pas être exprimée depuis son log) re-snapshote et réessaie plutôt que de forcer le progrès, ce qui, sous un churn pathologique, signifie qu'une migration prend plus de temps plutôt que de prendre un raccourci à travers vos données. Ce sont les bons compromis pour une opération planifiée : un déplacement que vous avez choisi de faire ne devrait jamais être la raison pour laquelle une écriture validée disparaît.</p>
<p>C'est la même colonne vertébrale que la <a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">certification du chaos sur douze heures</a> exerce depuis l'extérieur — arrêter un nœud en pleine migration, brasser la map, et regarder les écritures acquittées survivre. Cet article est la vue de l'intérieur de l'une des coutures que cette exécution prouve fermée : le moment où une partition change de main, et la sécurité sur la détente qui empêche la transmission de dévorer les écritures mêmes qu'elle a été bâtie pour protéger. Il s'appuie sur deux autres pièces qui valent la lecture à elles seules — l'<a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">op-log rejouable</a> qui fait de « les ops après la révision R » une requête réelle et sans trou, et la <a class="" href="https://clustron.io/fr/blog/elastic-scaling/">mise à l'échelle élastique</a> qui est la raison la plus courante pour laquelle une partition a besoin de se déplacer un jour.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Parcourir un keyspace qui ne tient pas en place : comment SCAN itère un cluster]]></title>
            <link>https://clustron.io/fr/blog/scanning-a-moving-keyspace/</link>
            <guid>https://clustron.io/fr/blog/scanning-a-moving-keyspace/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un SCAN complet dans Zaris doit parcourir un keyspace réparti sur 256 segments répartis sur plusieurs nœuds, trié nulle part globalement, en mutation pendant la lecture. Voici comment cette itération fonctionne réellement — un snapshot trié sans verrou par segment, une fusion k-voies en un flux ordonné de pages par nœud, un jeton de reprise lié à une session côté serveur, et un éventail client avec un plafond total strict. Plus un compte rendu honnête de ce qu'un SCAN promet et ne promet pas.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="De nombreux segments triés fusionnés en un flux ordonné et paginé" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJTY2FubmluZyBhIGtleXNwYWNlIHRoYXQgd29uJ3QgaG9sZCBzdGlsbCDigJQgc2VnbWVudHMgbWVyZ2VkIGludG8gb25lIG9yZGVyZWQsIHBhZ2VkIHN0cmVhbSI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPklURVJBVElPTiAmYW1wOyBDVVJTT1JTPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPlNjYW5uaW5nIGEga2V5c3BhY2U8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+dGhhdCB3b24ndCBob2xkIHN0aWxsPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+TWFueSBzb3J0ZWQgc2VnbWVudHMsIG1lcmdlZCBpbnRvIG9uZSBvcmRlcmVkLCBwYWdlZCBzdHJlYW08L3RleHQ+DQoNCiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODAsMjgwKSI+DQogICAgPCEtLSBzZWdtZW50cyAtLT4NCiAgICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiI+DQogICAgICA8cmVjdCB4PSIwIiB5PSI2IiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjM0IiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iMCIgeT0iNTIiIHdpZHRoPSIxNTAiIGhlaWdodD0iMzQiIHJ4PSI2Ii8+DQogICAgICA8cmVjdCB4PSIwIiB5PSI5OCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSIzNCIgcng9IjYiLz4NCiAgICA8L2c+DQogICAgPHRleHQgeD0iNzUiIHk9IjI5IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+c2VnbWVudCAwNzwvdGV4dD4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNzUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zZWdtZW50IDQyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijc1IiB5PSIxMjEiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zZWdtZW50IDE5ODwvdGV4dD4NCg0KICAgIDwhLS0gbWVyZ2UgYXJyb3dzIC0tPg0KICAgIDxwYXRoIGQ9Ik0xNTAgMjMgTDI1MCA2NiIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQogICAgPHBhdGggZD0iTTE1MCA2OSBMMjUwIDY5IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8cGF0aCBkPSJNMTUwIDExNSBMMjUwIDcyIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCg0KICAgIDwhLS0gbWVyZ2Ugbm9kZSAtLT4NCiAgICA8cmVjdCB4PSIyNTAiIHk9IjQ0IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQogICAgPHRleHQgeD0iMzE0IiB5PSI2NiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmstd2F5IG1lcmdlPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjMxNCIgeT0iODQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5vcmRpbmFsIG9yZGVyPC90ZXh0Pg0KDQogICAgPCEtLSBvcmRlcmVkIHN0cmVhbSAtLT4NCiAgICA8ZyBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiPg0KICAgICAgPHJlY3QgeD0iNDA2IiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iNDgwIiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iNTU0IiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iNjI4IiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgIDwvZz4NCiAgICA8dGV4dCB4PSI0MzgiIHk9Ijc0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+YToxPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjUxMiIgeT0iNzQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmZmZmIj5hOjI8L3RleHQ+DQogICAgPHRleHQgeD0iNTg2IiB5PSI3NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmI6OTwvdGV4dD4NCiAgICA8dGV4dCB4PSI2NjAiIHk9Ijc0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+YzozPC90ZXh0Pg0KDQogICAgPCEtLSBwYWdlIGN1cnNvciAtLT4NCiAgICA8cGF0aCBkPSJNNDA2IDExMiBMNjE4IDExMiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8cGF0aCBkPSJNNDA2IDEwOCBMNDA2IDExNiBNNjE4IDEwOCBMNjE4IDExNiIgc3Ryb2tlPSIjMmJkNmEwIiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI1MTIiIHk9IjEzMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPnBhZ2UgKHJlc3VtZSB0b2tlbik8L3RleHQ+DQoNCiAgICA8IS0tIHRhaWwgYXJyb3cgdG8gY2xpZW50IC0tPg0KICAgIDxwYXRoIGQ9Ik03MDAgNjkgTDc5MiA2OSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iNiIvPg0KICAgIDxwYXRoIGQ9Ik03ODQgNjEgTDgwMCA2OSBMNzg0IDc3IFoiIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8cmVjdCB4PSI4MTIiIHk9IjQ0IiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI4ODciIHk9IjY3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+Y2xpZW50PC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijg4NyIgeT0iODUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5pbnRlcmxlYXZlcyBvd25lcnM8L3RleHQ+DQogIDwvZz4NCg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>Un <code>GET</code> à clé unique est facile à raisonner : hacher la clé, trouver son propriétaire, demander à ce seul nœud. <code>SCAN</code> est le genre d'opération opposé. Il n'a pas de clé — il pose une question <em>sur tout le keyspace</em> : donne-moi chaque clé dans cette plage, une page à la fois. Et dans un store partitionné et répliqué, ce keyspace n'est pas une seule chose en un seul endroit. Il est réparti sur 256 segments étalés sur autant de nœuds que vous en exécutez, chaque nœud ne détenant que les tranches qu'il possède actuellement, et l'ensemble est en cours d'écriture pendant que vous itérez. Il n'y a pas d'index trié global à parcourir. Alors d'où vient l'ordre, que pointe exactement un jeton de reprise, et qu'arrive-t-il à votre scan quand une clé est écrite — ou qu'une partition se déplace — en plein milieu ?</p>
<p>Cet article suit un <code>SCAN</code> à travers les trois couches qui le servent réellement : un seul segment, un seul nœud, et le client qui recoud les nœuds ensemble. Chaque couche résout une pièce du problème, et les limites honnêtes de l'opération découlent de la façon dont ces pièces s'emboîtent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-requête--une-plage-et-une-taille-de-page-pas-une-clé">La requête : une plage et une taille de page, pas une clé<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#la-requ%C3%AAte--une-plage-et-une-taille-de-page-pas-une-cl%C3%A9" class="hash-link" aria-label="Lien direct vers La requête : une plage et une taille de page, pas une clé" title="Lien direct vers La requête : une plage et une taille de page, pas une clé" translate="no">​</a></h2>
<p>Un scan est décrit par deux petits objets. Un <code>KeyRange</code> dit <em>quelles</em> clés — un début, une fin ou un préfixe, chacun optionnel :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">sealed</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">KeyRange</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain"> Start  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain"> End    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain"> Prefix </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Et <code>ScanOptions</code> dit <em>comment</em> les délivrer — quelle taille de page, s'il faut expédier les valeurs ou juste les clés, une limite stricte optionnelle, et un jeton de reprise pour continuer un appel précédent :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">sealed</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">ScanOptions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">int</span><span class="token plain">     PageSize       </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain">    IncludeValues  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain">    IncludeMetadata</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain"> ResumeToken    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">int</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain">    LimitCount     </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Rien ici ne nomme un nœud ou un segment. C'est tout l'enjeu : l'appelant décrit <em>ce qu'il veut</em>, et les trois couches en dessous déterminent où cela réside et dans quel ordre le rendre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="couche-un--un-segment-se-scanne-lui-même-sans-verrou-et-dans-lordre">Couche un : un segment se scanne lui-même, sans verrou et dans l'ordre<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#couche-un--un-segment-se-scanne-lui-m%C3%AAme-sans-verrou-et-dans-lordre" class="hash-link" aria-label="Lien direct vers Couche un : un segment se scanne lui-même, sans verrou et dans l'ordre" title="Lien direct vers Couche un : un segment se scanne lui-même, sans verrou et dans l'ordre" translate="no">​</a></h2>
<p>La plus petite unité qui peut répondre à une partie d'un scan est un segment — l'unité de stockage partitionné de Zaris, soutenue par un <code>ConcurrentDictionary</code> d'enregistrements vivants. Un dictionnaire n'a pas d'ordre, donc la première chose dont un scan de segment a besoin est une vue triée de ses clés. La construire à chaque page serait ruineux, donc chaque segment conserve un <strong>snapshot trié de clés, immuable et mis en cache</strong> et ne le reconstruit que lorsque le jeu de clés change :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// No lock held during scan — the snapshot is immutable once built.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> sortedKeys </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GetOrBuildSortedKeys</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> startIdx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">BinarySearchKey</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sortedKeys</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> startKey</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> StringComparer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">startIdx </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> startIdx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">~</span><span class="token plain">startIdx</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Le tableau est assigné une fois et jamais muté ; une reconstruction concurrente produit simplement un nouveau tableau et échange la référence. Cela signifie que le parcours du scan lui-même ne prend <strong>aucun verrou du tout</strong> — il fait une recherche binaire jusqu'à la clé de départ et avance dans l'ordre ordinal, lisant les enregistrements vivants depuis le dictionnaire concurrent au fur et à mesure. Les lectures et écritures sur le segment ne bloquent jamais le scan, et le scan ne les bloque jamais.</p>
<p>Deux sortes de clés sont silencieusement passées au fil du parcours :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">_data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryGetValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">out</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">record</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// vanished since snapshot</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">record</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">IsDeleted </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> ExpirationManager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">IsExpired</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">record</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> now</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// tombstone / TTL</span><br></span></code></pre></div></div>
<p>Une clé supprimée est une <em>pierre tombale</em> (tombstone) — un enregistrement réel marqué <code>IsDeleted</code>, car <a class="" href="https://clustron.io/fr/blog/what-crosses-the-wire/">une suppression doit être un enregistrement, pas une absence</a>, pour que la réplication la transporte. Le scan les filtre, ainsi que les clés dont le TTL est écoulé mais dont le balayeur ne s'est pas encore déclenché. Le snapshot trié de clés est donc un sur-ensemble de ce que vous verrez ; expiration et suppression sont résolues contre l'enregistrement <em>vivant</em> au moment de la lecture, pas figées dans le snapshot.</p>
<p>Le parcours s'arrête à <code>PageSize</code> et renvoie un <code>ResumeToken</code>. Voici le premier détail honnête : au niveau du segment, le jeton est essentiellement un <strong>décalage</strong> (offset) — un décompte du nombre d'entrées correspondantes déjà renvoyées (<code>ReturnedSoFar</code>). Un scan de segment repris refait une recherche binaire jusqu'à la même clé de départ et saute ce nombre de correspondances avant d'émettre. C'est une position-par-décompte, pas une position-par-clé.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">cursor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ReturnedSoFar </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> taken</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> hasMore </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> results</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Count </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> pageSize </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> cursor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ReturnedSoFar </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> totalLimit</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Ce choix compte pour ce que l'opération promet, et nous y reviendrons.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="couche-deux--un-nœud-fusionne-ses-segments-en-un-seul-flux-ordonné">Couche deux : un nœud fusionne ses segments en un seul flux ordonné<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#couche-deux--un-n%C5%93ud-fusionne-ses-segments-en-un-seul-flux-ordonn%C3%A9" class="hash-link" aria-label="Lien direct vers Couche deux : un nœud fusionne ses segments en un seul flux ordonné" title="Lien direct vers Couche deux : un nœud fusionne ses segments en un seul flux ordonné" translate="no">​</a></h2>
<p>Un nœud possède de nombreux segments — pas les 256, juste ceux mappés aux partitions qu'il sert actuellement. Un scan au niveau du nœud doit présenter <em>tous</em> ces segments comme un seul résultat ordonné, car l'appelant a demandé une plage, pas le segment 42. C'est un problème de fusion classique : chaque segment peut produire ses clés dans l'ordre trié, et vous voulez l'union triée.</p>
<p>Donc la première page d'un scan de nœud fait exactement cela. Elle rassemble un fetcher pour chaque segment possédé localement et exécute une <strong>fusion k-voies</strong> à travers eux avec une file de priorité, comparant les clés de façon ordinale :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> heap </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">PriorityQueue</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&lt;</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">(</span><span class="token constructor-invocation class-name">KvResult</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&lt;</span><span class="token constructor-invocation class-name">ZarisEntry</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&gt;</span><span class="token constructor-invocation class-name"> Item</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">,</span><span class="token constructor-invocation class-name"> </span><span class="token constructor-invocation class-name keyword" style="color:#00009f">int</span><span class="token constructor-invocation class-name"> Source</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">)</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">,</span><span class="token constructor-invocation class-name"> KvResult</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&lt;</span><span class="token constructor-invocation class-name">ZarisEntry</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&gt;</span><span class="token constructor-invocation class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">comparison</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// seed with the head of each segment, then repeatedly pop the smallest</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// key and pull the next from that segment's enumerator</span><br></span></code></pre></div></div>
<p>Le résultat fusionné et ordonné est matérialisé dans un buffer détenu par un <strong><code>RouterQueryCoordinator</code></strong> — une session côté serveur de courte durée enregistrée sous un <code>QueryId</code> fraîchement frappé :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">coord </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">RouterQueryCoordinator</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> coord</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">InitScanAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fetchers</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">     </span><span class="token comment" style="color:#999988;font-style:italic">// drains + merges every owned segment</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _coordManager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Register</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">coord</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cursor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">QueryCursor</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> QueryId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ReturnedSoFar </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Chaque page suivante n'est qu'une tranche de ce buffer, et le jeton de reprise du nœud porte le <code>QueryId</code> pour que l'appel suivant retrouve la même session :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">IsNullOrEmpty</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">options</span><span class="token punctuation" style="color:#393A34">?.</span><span class="token plain">ResumeToken</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    cursor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> QueryCursor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Deserialize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">options</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ResumeToken</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">_coordManager</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryGet</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cursor</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">QueryId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">out</span><span class="token plain"> coord</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">InvalidOperationException</span><span class="token punctuation" style="color:#393A34">(</span><span class="token interpolation-string string" style="color:#e3116c">$"Search session </span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">cursor</span><span class="token interpolation-string interpolation expression language-csharp punctuation" style="color:#393A34">.</span><span class="token interpolation-string interpolation expression language-csharp">QueryId</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c"> not found (expired or cleaned)."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>C'est le deuxième détail honnête, et c'est un vrai compromis. Un scan au niveau du nœud est <strong>à état</strong> (stateful) : la pagination est stable et l'ordre par nœud est exact, mais le prix est que le nœud matérialise tout son keyspace possédé et fusionné dans un buffer quand le scan commence, et détient une session pour la durée de vie de l'itération. Les scans sont un outil d'administration et d'itération en masse, pas un curseur de streaming à mémoire constante sur des milliards de clés — et un jeton de reprise n'est valable que contre le nœud et la session qui l'ont frappé. Laissez cette session expirer, ou tentez de la reprendre ailleurs, et vous obtenez un bruyant « session not found » plutôt qu'une réponse silencieusement fausse.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="couche-trois--le-client-séventaille-et-entrelace-les-propriétaires">Couche trois : le client s'éventaille et entrelace les propriétaires<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#couche-trois--le-client-s%C3%A9ventaille-et-entrelace-les-propri%C3%A9taires" class="hash-link" aria-label="Lien direct vers Couche trois : le client s'éventaille et entrelace les propriétaires" title="Lien direct vers Couche trois : le client s'éventaille et entrelace les propriétaires" translate="no">​</a></h2>
<p>Un nœud répond pour les segments qu'il possède. Un scan de cluster doit couvrir <em>chaque</em> propriétaire, donc le <code>ScanClusterClient</code> côté client ouvre un scan contre chaque nœud en parallèle et entrelace leurs pages à mesure qu'elles arrivent, plutôt que de vider un nœud avant de commencer le suivant :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> enumerators </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> clients</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Scan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ScanAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">range</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> options</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetAsyncEnumerator</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// advance all owners concurrently; yield whichever produces next</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> finishedTask </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> Task</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">WhenAny</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pending</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Select</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">p </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Task</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Ce parallélisme est pourquoi <code>LimitCount</code> demande du soin. Chaque nœud traite la limite comme un plafond <em>par nœud</em> ; si le client ne l'imposait pas aussi, un <code>LimitCount</code> de 1 000 contre quatre propriétaires pourrait renvoyer jusqu'à 4 000 lignes une fois fusionnées. Donc la couche cluster applique la limite comme un <strong>plafond total strict</strong> et cesse d'émettre une fois qu'elle est atteinte — tout en continuant à vider jusqu'au bout la page en vol de chaque propriétaire, car disposer d'un énumérateur asynchrone en plein déplacement n'est pas permis et les résultats par nœud sont de toute façon déjà bornés :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">emitted </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> limit</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">yield</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> enumerator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Current</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    emitted</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// keep draining siblings even past the cap; do not yield-break mid-move</span><br></span></code></pre></div></div>
<p>La conséquence à clarifier : au sein de la contribution d'un seul nœud, les clés arrivent dans l'ordre ; <strong>entre</strong> les nœuds elles sont entrelacées par ordre d'arrivée, donc un scan de cluster complet n'est <em>pas</em> trié globalement sauf si vous le demandez explicitement. (Le chemin <code>Search</code> apparenté porte bien <code>SortFields</code> et fusionne avec un comparateur pour exactement cette raison.) Si vous avez besoin de chaque clé du cluster dans l'ordre ordinal, triez le résultat assemblé ; <code>SCAN</code> lui-même garantit l'ordre par propriétaire et l'exhaustivité, pas un tri global.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-quun-scan-promet--et-ne-promet-pas">Ce qu'un SCAN promet — et ne promet pas<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#ce-quun-scan-promet--et-ne-promet-pas" class="hash-link" aria-label="Lien direct vers Ce qu'un SCAN promet — et ne promet pas" title="Lien direct vers Ce qu'un SCAN promet — et ne promet pas" translate="no">​</a></h2>
<p>Mettez les trois couches ensemble et le vrai contrat de l'opération est visible, sans que rien n'en soit caché :</p>
<ul>
<li class=""><strong>Ce n'est pas un snapshot ponctuel du cluster.</strong> Chaque nœud matérialise <em>son propre</em> buffer fusionné quand <em>son</em> scan commence, et ces moments ne coïncident pas. Les écritures qui atterrissent sur un nœud avant que son buffer ne soit construit sont incluses ; celles d'après ne le sont pas. Il n'y a pas de gel à l'échelle du cluster.</li>
<li class=""><strong>Les clés supprimées et expirées ne font jamais surface.</strong> Les pierres tombales et les enregistrements au TTL écoulé sont filtrés contre l'enregistrement vivant au fil du parcours du segment, donc un scan reflète le contenu logique, pas physique.</li>
<li class=""><strong>La reprise est position-par-décompte à l'intérieur d'une session.</strong> Parce que le curseur par segment est un décalage, une clé insérée ou retirée <em>avant</em> la position courante entre deux pages peut décaler la frontière — un scan concurrent à une forte mutation de la plage scannée peut, en principe, répéter ou sauter une clé autour de la couture. Un scan d'une plage au repos est exact. C'est la même famille de garantie que donne le <code>SCAN</code> de Redis — une itération complète au mieux, pas un niveau d'isolation snapshot — et c'est le bon compromis pour un store non ordonné et écrit en concurrence.</li>
<li class=""><strong>Le keyspace peut bouger sous vos pieds.</strong> Le client s'éventaille vers les propriétaires qui existent au démarrage du scan. Les partitions peuvent <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">migrer</a> ou <a class="" href="https://clustron.io/fr/blog/anatomy-of-a-failover/">basculer</a> en pleine itération ; un scan est un balayage au mieux d'un cluster vivant, ce qui est pourquoi c'est l'outil de l'export en masse, de la réindexation et des requêtes d'administration plutôt que des lectures transactionnelles. Pour le read-your-writes sur une clé unique, <a class="" href="https://clustron.io/fr/blog/reading-your-own-writes/">routez vers le propriétaire</a> à la place.</li>
</ul>
<p>Aucune de ces propriétés n'est un bug à masquer ; ce sont la forme honnête de l'itération d'un keyspace non ordonné, partitionné et en mutation sans arrêter le monde. La conception rend le cas courant et bon marché rapide — un snapshot trié sans verrou et une fusion par nœud — et est explicite sur les bords.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="une-note-sur-le-front-end-resp">Une note sur le front-end RESP<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#une-note-sur-le-front-end-resp" class="hash-link" aria-label="Lien direct vers Une note sur le front-end RESP" title="Lien direct vers Une note sur le front-end RESP" translate="no">​</a></h2>
<p>Les clients Redis atteignent la même machinerie via <code>KEYS</code> et <code>SCAN</code> sur le <a class="" href="https://clustron.io/fr/blog/redis-protocol-any-language/">front-end RESP</a>, qui se mappent sur le scan natif. Une mise en garde à énoncer sans détour : le <code>SCAN</code> RESP est aujourd'hui en un seul coup — il vide le scan natif et répond avec le curseur <code>0</code> (itération terminée) en un aller-retour, plutôt que de filer le curseur incrémental de Redis à travers chaque appel. Il est honnête sur l'exhaustivité, pas encore incrémental. Le client natif est celui qui pagine avec un jeton de reprise.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="une-opération-trois-couches-honnêtes">Une opération, trois couches honnêtes<a href="https://clustron.io/fr/blog/scanning-a-moving-keyspace/#une-op%C3%A9ration-trois-couches-honn%C3%AAtes" class="hash-link" aria-label="Lien direct vers Une opération, trois couches honnêtes" title="Lien direct vers Une opération, trois couches honnêtes" translate="no">​</a></h2>
<p>Un <code>GET</code> touche un propriétaire et une clé ; un <code>SCAN</code> touche chaque propriétaire et demande à chacun de produire sa tranche d'un tout ordonné. Le travail se divise proprement : un segment transforme un dictionnaire non ordonné en un parcours trié sans verrou ; un nœud fusionne ses segments en un seul flux ordonné et paginé de façon stable derrière un curseur de session ; le client s'éventaille sur les propriétaires, entrelace leurs pages, et impose le seul plafond que l'éventail casserait autrement. Chaque couche est simple en soi, et les garanties de l'opération — ordre par propriétaire, exhaustivité sur une plage au repos, pas d'isolation snapshot, pas de gel global — sont exactement ce que ces trois couches simples peuvent honnêtement délivrer ensemble.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Le plafond de mémoire : pourquoi un nœud Zaris ne croît jamais sans borne]]></title>
            <link>https://clustron.io/fr/blog/the-memory-ceiling/</link>
            <guid>https://clustron.io/fr/blog/the-memory-ceiling/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nœud Zaris est un store en mémoire, mais il n'est pas illimité. Chaque nœud est un buffer borné avec un plafond de mémoire, un cache LRU toujours actif en dessous, et une bande d'hystérésis qui empêche l'éviction de s'emballer au bord. Voici exactement ce qui se passe quand un nœud se remplit — et les deux limites très différentes qu'une écriture peut atteindre.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Un nœud Zaris maintenant l&amp;#39;usage mémoire dans une bande calme sous son plafond, évinçant à 80 % jusqu&amp;#39;à 70 % pour que les écritures ne franchissent jamais la limite" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJBIFphcmlzIG5vZGUgaG9sZHMgaXRzIG1lbW9yeSB1c2FnZSBpbiBhIHF1aWV0IGJhbmQgYmVsb3cgdGhlIGNlaWxpbmcsIGV2aWN0aW5nIGF0IDgwIHBlcmNlbnQgZG93biB0byA3MCBwZXJjZW50IHNvIHdyaXRlcyBuZXZlciBjcm9zcyB0aGUgbGltaXQiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MCIgY3k9IjAuMTgiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KDQogIDwhLS0gZXllYnJvdyArIHRpdGxlIC0tPg0KICA8dGV4dCB4PSI4MCIgeT0iNTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+TUVNT1JZICYjMTgzOyBUSEUgQ0VJTElORzwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjkyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIzMSIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPkEgbm9kZSB0aGF0IG5ldmVyIGdyb3dzIHdpdGhvdXQgYm91bmQ8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE4IiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5FdmljdCBhdCA4MCUsIHNldHRsZSBhdCA3MCUgJiM4MjEyOyB3cml0ZXMga2VlcCBzdWNjZWVkaW5nLCB0aGUgbGltaXQgaG9sZHM8L3RleHQ+DQoNCiAgPCEtLSBwbG90IGdlb21ldHJ5OiB4IDE1MC4uMTA0MCA7IGJhc2VsaW5lIHk9NDAwICgwKSA7IGNlaWxpbmcgeT0xNTAgKDEwMCUgLyAxIEdpQikgLS0+DQogIDwhLS0gZm9yYmlkZGVuIHpvbmUgYWJvdmUgdGhlIGNlaWxpbmcgLS0+DQogIDxyZWN0IHg9IjE1MCIgeT0iMTI4IiB3aWR0aD0iODkwIiBoZWlnaHQ9IjIyIiBmaWxsPSIjZTI1NzRjIiBmaWxsLW9wYWNpdHk9IjAuMTAiLz4NCg0KICA8IS0tIGNlaWxpbmcgKDEwMCUgLyBtYXhTaXplQnl0ZXMpIC0tPg0KICA8bGluZSB4MT0iMTUwIiB5MT0iMTUwIiB4Mj0iMTA0MCIgeTI9IjE1MCIgc3Ryb2tlPSIjZTI1NzRjIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWRhc2hhcnJheT0iMiAwIi8+DQogIDx0ZXh0IHg9IjEwNDAiIHk9IjE0MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiNlNzhiODMiIHRleHQtYW5jaG9yPSJlbmQiPmNlaWxpbmcgJiM4MjEyOyBtZW1vcnkubWF4U2l6ZUJ5dGVzICgxIEdpQiBkZWZhdWx0KTwvdGV4dD4NCg0KICA8IS0tIHRyaWdnZXIgODAlIC0tPg0KICA8bGluZSB4MT0iMTUwIiB5MT0iMjAwIiB4Mj0iMTA0MCIgeTI9IjIwMCIgc3Ryb2tlPSIjM2E1ZDRmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjkgNiIvPg0KICA8dGV4dCB4PSIxNTgiIHk9IjE5MyIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM5ZGI0YWEiPjgwJSAmIzE4MzsgZXZpY3Rpb24gcnVuIHRyaWdnZXJzPC90ZXh0Pg0KDQogIDwhLS0gdGFyZ2V0IDcwJSAtLT4NCiAgPGxpbmUgeDE9IjE1MCIgeTE9IjIyNSIgeDI9IjEwNDAiIHkyPSIyMjUiIHN0cm9rZT0iIzI0NDYzYiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtZGFzaGFycmF5PSI5IDYiLz4NCiAgPHRleHQgeD0iMTU4IiB5PSIyNDMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjNmY5NDg4Ij43MCUgJiMxODM7IHJ1biBzZXR0bGVzIGhlcmU8L3RleHQ+DQoNCiAgPCEtLSBxdWlldCBiYW5kIGhpZ2hsaWdodCBiZXR3ZWVuIDcwJSBhbmQgODAlIC0tPg0KICA8cmVjdCB4PSIxNTAiIHk9IjIwMCIgd2lkdGg9Ijg5MCIgaGVpZ2h0PSIyNSIgZmlsbD0iIzEyYjU3ZSIgZmlsbC1vcGFjaXR5PSIwLjA4Ii8+DQoNCiAgPCEtLSBiYXNlbGluZSAtLT4NCiAgPGxpbmUgeDE9IjE1MCIgeTE9IjQwMCIgeDI9IjEwNDAiIHkyPSI0MDAiIHN0cm9rZT0iIzI0NDYzYiIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4NCg0KICA8IS0tIHVzYWdlIGN1cnZlOiBjbGltYnMgZnJvbSBlbXB0eSwgdGhlbiByaWRlcyB0aGUgcXVpZXQgYmFuZCBhcyBhIHNhd3Rvb3RoIChldmljdCAtPiByZWZpbGwpIC0tPg0KICA8cGF0aCBkPSJNMTUwIDM5OQ0KICAgICAgICAgICBDIDI2MCAzNjAgMzQwIDMwMCA0MzAgMjI1DQogICAgICAgICAgIEwgNDcwIDIwMCBMIDUwMCAyMjUNCiAgICAgICAgICAgTCA1NjAgMjAwIEwgNTkwIDIyNQ0KICAgICAgICAgICBMIDY1MCAyMDAgTCA2ODAgMjI1DQogICAgICAgICAgIEwgNzQwIDIwMCBMIDc3MCAyMjUNCiAgICAgICAgICAgTCA4MzAgMjAwIEwgODYwIDIyNQ0KICAgICAgICAgICBMIDkyMCAyMDAgTCA5NTAgMjI1DQogICAgICAgICAgIEwgMTAxMCAyMDAgTCAxMDQwIDIxMCINCiAgICAgICAgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KICA8Y2lyY2xlIGN4PSIxMDQwIiBjeT0iMjEwIiByPSI1IiBmaWxsPSIjMmJkNmEwIi8+DQoNCiAgPHRleHQgeD0iMzAwIiB5PSIzMDAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjOWRiNGFhIj5maWxsPC90ZXh0Pg0KICA8dGV4dCB4PSI3MDAiIHk9IjI2OCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmFsd2F5cy1vbiBMUlUgJiMxODM7IGNvbGQgZGF0YSBkcm9wcGVkLCBob3QgZGF0YSBzdGF5czwvdGV4dD4NCg0KICA8IS0tIGF4aXMgY2FwdGlvbnMgLS0+DQogIDx0ZXh0IHg9IjE1MCIgeT0iNDI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzdmOWE4ZiI+dGltZSAmIzg1OTQ7PC90ZXh0Pg0KICA8dGV4dCB4PSIxMzgiIHk9IjQwNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM3ZjlhOGYiIHRleHQtYW5jaG9yPSJlbmQiPjA8L3RleHQ+DQoNCiAgPHRleHQgeD0iODAiIHk9IjQ1MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTkiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDMiIHk9IjQ1MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTkiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>Voici une question qui semble simple et ne l'est pas : qu'arrive-t-il quand un store en mémoire tombe à court de mémoire ?</p>
<p>La réponse naïve est « il plante » — le processus grimpe jusqu'à ce que le système d'exploitation le tue. La réponse un peu moins naïve est « il refuse les écritures ». Zaris ne fait ni l'un ni l'autre par défaut. Un nœud Zaris est un store en mémoire, mais il n'est délibérément <em>pas</em> illimité : chaque nœud est un <strong>buffer borné</strong> avec un plafond dur, et quand les données qu'il possède approchent ce plafond, le nœud évince ses données les plus froides pour faire de la place. Les écritures continuent de réussir ; la limite tient ; le processus ne gonfle pas. Le nœud se comporte comme un cache, parce qu'en dessous, c'en est un.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="chaque-nœud-est-un-buffer-borné">Chaque nœud est un buffer borné<a href="https://clustron.io/fr/blog/the-memory-ceiling/#chaque-n%C5%93ud-est-un-buffer-born%C3%A9" class="hash-link" aria-label="Lien direct vers Chaque nœud est un buffer borné" title="Lien direct vers Chaque nœud est un buffer borné" translate="no">​</a></h2>
<p>Un nœud Zaris ne croît pas sans limite. Chaque nœud comptabilise en continu la taille des données qu'il possède activement et maintient ce total courant sous un <strong>plafond</strong> configuré. La comptabilité n'est jamais optionnelle — un nœud est <em>toujours</em> un buffer borné. Les seules choses que vous configurez sont la taille du plafond et, en principe, ce que le nœud fait quand il l'atteint.</p>
<p>Le plafond par défaut est de <strong>1 Gio</strong> de données activement possédées par nœud. Il n'y a, par conception, aucun réglage « illimité ». Si vous ne fixez pas de plafond, vous obtenez la valeur par défaut — vous n'obtenez jamais l'infini par accident.</p>
<p>Une subtilité compte pour la façon de raisonner l'empreinte d'un cluster : le plafond couvre les données qu'un nœud <strong>possède activement</strong> — les copies primaires qu'il sert aux clients. Les copies de réplica qu'un nœud détient pour <em>d'autres</em> partitions sont suivies séparément et ne comptent pas contre le plafond de propriétaire actif. La vraie mémoire résidente d'un nœud est donc son jeu de travail possédé plus les copies de réplica qu'il porte pour ses pairs ; le plafond régit le premier, pas la somme.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deux-façons-de-tenir-la-ligne">Deux façons de tenir la ligne<a href="https://clustron.io/fr/blog/the-memory-ceiling/#deux-fa%C3%A7ons-de-tenir-la-ligne" class="hash-link" aria-label="Lien direct vers Deux façons de tenir la ligne" title="Lien direct vers Deux façons de tenir la ligne" translate="no">​</a></h2>
<p>Il y a exactement deux choses qu'un buffer borné peut faire quand une écriture le pousserait au-delà de son plafond : déposer quelque chose d'ancien pour faire de la place, ou refuser la nouvelle écriture.</p>
<table><thead><tr><th>Mode</th><th>Comportement au plafond</th><th>Une écriture qui le dépasserait</th></tr></thead><tbody><tr><td>Éviction activée (par défaut)</td><td>Évince les données plus anciennes pour faire de la place</td><td>Admise — des données plus froides sont déposées</td></tr><tr><td>Éviction désactivée</td><td>Refuse les nouvelles données</td><td>Rejetée avec <code>KvStatus.CapacityExceeded</code></td></tr></tbody></table>
<p>Avec l'éviction activée, un nœud est un cache : sous pression il dépose les données les moins précieuses pour que les écritures continuent de circuler. Avec l'éviction désactivée, c'est un store qui rejette à capacité et protège les données existantes en refoulant les nouvelles écritures.</p>
<p>Voici la partie honnête. Dans la version actuelle, un nœud fonctionne <strong>toujours</strong> avec l'éviction activée, en utilisant la politique LRU. Le mode sans éviction et les politiques alternatives (LFU, TTL-aware) existent dans le moteur, mais ils ne sont pas encore sélectionnables par configuration. La conséquence pratique est que <code>CapacityExceeded</code> — le statut de rejet à capacité — est essentiellement inatteignable aujourd'hui : un nœud plein évince plutôt que de refuser. C'est un statut réel et typé contre lequel vous <em>pouvez</em> écrire du code, mais sous la politique LRU toujours active vous ne le verrez normalement pas. Le seul bouton mémoire que vous contrôlez réellement en ce moment est la taille du plafond lui-même, <code>memory.maxSizeBytes</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lexécution-déviction-chevauche-une-bande-calme">L'exécution d'éviction chevauche une bande calme<a href="https://clustron.io/fr/blog/the-memory-ceiling/#lex%C3%A9cution-d%C3%A9viction-chevauche-une-bande-calme" class="hash-link" aria-label="Lien direct vers L'exécution d'éviction chevauche une bande calme" title="Lien direct vers L'exécution d'éviction chevauche une bande calme" translate="no">​</a></h2>
<p>L'ingénierie intéressante n'est pas *qu'*un nœud évince — c'est <em>comment</em>, car une conception naïve « évincer à chaque écriture une fois plein » s'emballe misérablement. Imaginez un nœud posé exactement à son plafond : chaque écriture entrante déclencherait une éviction pour se faire de la place, et vous paieriez ce coût sur le chemin chaud à jamais.</p>
<p>Zaris évite cela avec deux manœuvres. D'abord, l'éviction est pilotée par une <strong>boucle d'arrière-plan</strong>, pas par l'écriture individuelle — l'écriture qui fait basculer un nœud par-dessus bord est admise, et la boucle récupère l'espace peu après, hors du chemin de requête. Ensuite, la boucle utilise l'<strong>hystérésis</strong> : un seuil de déclenchement haut et une cible plus basse, de sorte qu'elle récupère par rafales puis se tait au lieu de tirer continûment au bord.</p>
<ul>
<li class=""><strong>Déclenchement :</strong> une exécution démarre quand l'usage actif dépasse <strong>80 %</strong> du plafond.</li>
<li class=""><strong>Cible :</strong> une fois démarrée, l'exécution continue d'évincer jusqu'à ce qu'environ <strong>30 %</strong> du plafond soit à nouveau libre — jusqu'à ce que l'usage retombe à environ <strong>70 %</strong>.</li>
<li class=""><strong>Cadence :</strong> la boucle se réveille à intervalle fixe (une seconde par défaut) et échantillonne un ensemble borné d'items candidats à chaque cycle.</li>
</ul>
<p>Parce que la cible post-éviction (70 %) se situe strictement <em>en dessous</em> du déclenchement (80 %), chaque exécution fait retomber l'usage dans une bande calme plutôt que de s'arrêter juste au point qui le re-déclencherait immédiatement. C'est la dent de scie dans l'image d'en-tête : l'usage grimpe à 80 %, une exécution le ramène à 70 %, puis il remonte — jamais plaqué contre le plafond, jamais d'emballement d'une éviction par écriture.</p>
<!-- -->
<p>La politique d'éviction elle-même est <strong>LRU</strong> — least recently used, la moins récemment utilisée — et elle est <em>échantillonnée</em>, pas stricte. Plutôt que de maintenir une liste globalement ordonnée de chaque clé (ce qui coûterait de la mémoire et de la contention de verrou sur le chemin chaud), Zaris estampille des signaux de récence approximatifs sur place sur le chemin de lecture et, quand une exécution se déclenche, échantillonne un petit ensemble de candidats et dépose ceux au plus bas score. C'est la même approche échantillonnée qu'utilise Redis, et le même compromis : vous abandonnez l'ordre parfait « évincer la plus ancienne clé unique » en échange d'une éviction qui ne coûte presque rien à maintenir et n'a jamais besoin d'un index séparé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lautre-plafond--la-taille-par-item">L'autre plafond : la taille par item<a href="https://clustron.io/fr/blog/the-memory-ceiling/#lautre-plafond--la-taille-par-item" class="hash-link" aria-label="Lien direct vers L'autre plafond : la taille par item" title="Lien direct vers L'autre plafond : la taille par item" translate="no">​</a></h2>
<p>Le plafond de mémoire global est une limite sur le <em>total</em>. Il y a une seconde limite indépendante sur tout item <em>unique</em> : par défaut une valeur ne peut pas dépasser <strong>4 Mio</strong>. Elle est vérifiée à chaque écriture quel que soit le mode d'éviction, et c'est là qu'une écriture atteint un rejet véritablement atteignable.</p>
<p>La distinction compte parce que les deux défaillances signifient des choses opposées et veulent des réponses opposées :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">PutAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"report:2026-07"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">IsSuccess</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Status</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> KvStatus</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ItemTooLarge</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// This one value is too big. No amount of eviction helps —</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// split it, or store a reference to it elsewhere.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> KvStatus</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">CapacityExceeded</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// The node is full and eviction is disabled (not the default).</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic">// Retry later or shed load.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p><code>ItemTooLarge</code> est un résultat vivant et atteignable dans tous les modes — l'éviction ne peut pas faire de place pour une valeur qui dépasse le plafond par item, car le plafond n'est pas une question d'espace libre, c'est une question de valeur pathologique unique. <code>CapacityExceeded</code>, comme couvert ci-dessus, n'apparaît que dans le mode sans éviction que vous ne pouvez pas encore sélectionner. Les traiter de façon identique est un bug qui attend d'arriver : réessayer une écriture <code>ItemTooLarge</code> éternellement ne réussira jamais.</p>
<!-- -->
<p>Une ride apparentée vit à la frontière entre ce plafond par item et le runtime .NET : les valeurs d'environ 85 000 octets et plus atterrissent sur le <strong>Large Object Heap</strong>, donc relever <code>maxItemSizeBytes</code> pour admettre de grosses valeurs est une décision délibérée avec des conséquences GC, pas un cadran gratuit. Si vous mettez à jour à chaud de grosses valeurs simples, les découper en morceaux sous-LOH est le mécanisme qui empêche le LOH de s'agiter — une fonctionnalité séparée avec ses propres compromis, que nous avons détaillée dans <a class="" href="https://clustron.io/fr/blog/large-object-chunking/">Stocker de gros objets sans agiter le heap</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-vous-configurez-et-ce-que-vous-ne-configurez-pas">Ce que vous configurez, et ce que vous ne configurez pas<a href="https://clustron.io/fr/blog/the-memory-ceiling/#ce-que-vous-configurez-et-ce-que-vous-ne-configurez-pas" class="hash-link" aria-label="Lien direct vers Ce que vous configurez, et ce que vous ne configurez pas" title="Lien direct vers Ce que vous configurez, et ce que vous ne configurez pas" translate="no">​</a></h2>
<p>Le plafond de mémoire vit dans la configuration du store sous la section <code>memory</code> :</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"memory"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"maxSizeBytes"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2147483648</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Cela fait 2 Gio. Mettez <code>maxSizeBytes</code> à <code>null</code>, ou omettez-le, et vous obtenez la valeur par défaut de 1 Gio. Dans la version actuelle c'est le réglage de mémoire que vous contrôlez — la politique LRU, les seuils d'hystérésis 80 %/70 % et la cadence d'une seconde sont des valeurs par défaut fixes, pas des boutons. Il vaut la peine d'être lucide à ce sujet : les articles de blog et le moteur décrivent un espace de politiques plus riche que ce que la surface de configuration expose actuellement, et cet article décrit ce que vous pouvez réellement régler aujourd'hui.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que-cela-signifie-pour-dimensionner-un-cluster">Ce que cela signifie pour dimensionner un cluster<a href="https://clustron.io/fr/blog/the-memory-ceiling/#ce-que-cela-signifie-pour-dimensionner-un-cluster" class="hash-link" aria-label="Lien direct vers Ce que cela signifie pour dimensionner un cluster" title="Lien direct vers Ce que cela signifie pour dimensionner un cluster" translate="no">​</a></h2>
<p>Parce que chaque nœud est un buffer borné, la capacité totale d'un store est à peu près le plafond par nœud multiplié par le nombre de partitions qu'un nœud possède, ajusté par le facteur de réplication (les réplicas détiennent des copies supplémentaires qui vivent hors du plafond de propriétaire actif). Deux conséquences pratiques en découlent :</p>
<ul>
<li class=""><strong>Dimensionnez le plafond sur votre jeu de travail plus une marge d'éviction, pas sur vos données totales.</strong> Un nœud exécute aujourd'hui un cache LRU toujours actif, donc les données froides sont déposées sous pression plutôt que rejetées. Si votre jeu de travail tient sous ~70 % du plafond avec de la place pour que la bande respire, l'éviction reste calme et le jeu chaud reste résident. Si vous dimensionnez le plafond en dessous de votre jeu de travail, vous avez construit un cache qui évince constamment des données dont il va avoir à nouveau besoin — correct, mais lent, et la dent de scie se transforme en meule.</li>
<li class=""><strong>N'oubliez pas l'empreinte des réplicas.</strong> Le plafond régit les données possédées, mais la RAM réelle d'un nœud est les données possédées <em>plus</em> les copies de réplica. Quand vous provisionnez la mémoire par pod ou par machine, budgétez pour les deux, ou un nœud dimensionné « à son plafond » tournera quand même plus chaud que le plafond ne le laisse entendre.</li>
</ul>
<p>Pour le calcul de capacité dans le contexte des partitions, du facteur de réplication et des nombres de pods, <a class="" href="https://clustron.io/fr/blog/kubernetes-durability-sizing/">Dimensionner un cluster Zaris pour la durabilité sur Kubernetes</a> parcourt le calcul complet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="à-retenir">À retenir<a href="https://clustron.io/fr/blog/the-memory-ceiling/#%C3%A0-retenir" class="hash-link" aria-label="Lien direct vers À retenir" title="Lien direct vers À retenir" translate="no">​</a></h2>
<p>Un store en mémoire qui croît sans borne n'est pas une fonctionnalité — c'est une panne qui attend un pic de trafic. Zaris traite chaque nœud comme un buffer borné avec un vrai plafond (1 Gio par défaut, aucune option illimitée), un cache LRU échantillonné toujours actif en dessous, et une bande d'hystérésis qui récupère l'espace par rafales calmes plutôt que de s'emballer avec une éviction par écriture au bord. Le plafond total et le plafond par item sont deux limites différentes avec deux statuts typés différents, et celui qui est réellement atteignable aujourd'hui — <code>ItemTooLarge</code> — veut une réponse différente d'une boucle de réessai. Dimensionnez le plafond sur votre jeu de travail, budgétez les copies de réplica en plus, et le nœud tient sa ligne sans que vous ayez à le surveiller.</p>
<p>Le modèle complet, y compris les politiques que le moteur porte mais n'expose pas encore, est dans <a class="" href="https://clustron.io/fr/docs/clustron/zaris/core-concepts/memory-and-eviction/">Mémoire et éviction</a> ; les champs et valeurs par défaut exacts sont dans la <a class="" href="https://clustron.io/fr/docs/clustron/zaris/reference/configuration/#environment-variables">référence de configuration</a>, et chaque statut de capacité est catalogué dans la <a class="" href="https://clustron.io/fr/docs/clustron/zaris/reference/status-codes/">référence des statuts et exceptions</a>.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Architecture</category>
            <category>Opérations</category>
            <category>.NET</category>
        </item>
        <item>
            <title><![CDATA[Ce qui traverse le fil : l'anatomie d'un enregistrement Zaris]]></title>
            <link>https://clustron.io/fr/blog/what-crosses-the-wire/</link>
            <guid>https://clustron.io/fr/blog/what-crosses-the-wire/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Chaque message de réplication et de migration dans Zaris transporte une seule charge utile — un StoredRecord. Voici une visite champ par champ de son contenu, pourquoi une suppression est un enregistrement plutôt qu'une absence, pourquoi deux numéros de version différents voyagent avec, et ce qui délibérément ne quitte jamais le nœud.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="L&amp;#39;anatomie d&amp;#39;un enregistrement stocké Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJUaGUgYW5hdG9teSBvZiBhIFphcmlzIHN0b3JlZCByZWNvcmQgb24gdGhlIHdpcmUiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5BUkNISVRFQ1RVUkU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+V2hhdCBjcm9zc2VzPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPnRoZSB3aXJlPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+VGhlIGFuYXRvbXkgb2YgYSBaYXJpcyBzdG9yZWQgcmVjb3JkPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDAsMjUwKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjQxMCIgaGVpZ2h0PSIxODYiIHJ4PSIxMCIgZmlsbD0iIzBjMWIxNiIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjIyIiB5PSIzNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlN0b3JlZFJlY29yZDwvdGV4dD4NCiAgICA8bGluZSB4MT0iMjIiIHkxPSI0NiIgeDI9IjM4OCIgeTI9IjQ2IiBzdHJva2U9IiMxZDNhMzAiIHN0cm9rZS13aWR0aD0iMS41Ii8+DQogICAgPHRleHQgeD0iMjIiIHk9Ijc0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzBdIEVudHJ5PC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjIxMCIgeT0iNzQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWE2YjY1Ij5rZXkgJiMxODM7IHZhbHVlICYjMTgzOyBtZXRhZGF0YTwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTAwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzFdIFJldmlzaW9uPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjIxMCIgeT0iMTAwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzVhNmI2NSI+c2VnbWVudCBvcmRlcjwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzJdIFZlcnNpb248L3RleHQ+DQogICAgPHRleHQgeD0iMjEwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWE2YjY1Ij5wZXIta2V5IENBUzwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTUyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzNdIFRpbWVzdGFtcFV0YzwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTc4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzRdIElzRGVsZXRlZDwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMTAiIHk9IjE3OCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMyYmQ2YTAiPnRvbWJzdG9uZTwvdGV4dD4NCiAgICA8dGV4dCB4PSIwIiB5PSIyMTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWE2YjY1Ij5BY2NvdW50ZWRCeXRlcyAmIzE4MzsgTWF0ZXJpYWxpemVkVmFsdWU8L3RleHQ+DQogICAgPHRleHQgeD0iMCIgeT0iMjMyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzVhNmI2NSI+W0lnbm9yZU1lbWJlcl0gJiM4NTk0OyBzdGF5cyBub2RlLWxvY2FsPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>L'essentiel de ce que Zaris fait bien — répliquer une écriture vers une sauvegarde, remettre à niveau un réplica en retard, faire basculer une partition vers un nouveau propriétaire — se ramène à une question discrète : <em>quelle est, exactement, l'unité de données qui se déplace entre les nœuds ?</em> Si cette unité est juste, la convergence est presque ennuyeuse. Si elle oublie un champ ou en porte un faux, vous obtenez une clé supprimée ressuscitée, un réplica incapable de distinguer le périmé du frais, ou la comptabilité d'un nœud qui fuit sur un autre.</p>
<p>Dans Zaris cette unité est un seul type — <code>StoredRecord</code> — et il vaut la peine de le démonter champ par champ, car chaque décision qu'il contient est porteuse. Cet article est l'anatomie : ce qu'il y a dans l'enregistrement, comment il est encadré sur le fil, pourquoi une suppression voyage <em>comme un enregistrement</em> au lieu d'une absence, pourquoi il y a deux numéros de version différents, et — tout aussi important — ce qui est délibérément laissé sur chaque nœud et ne traverse jamais le fil du tout.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="une-charge-utile-partout">Une charge utile, partout<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#une-charge-utile-partout" class="hash-link" aria-label="Lien direct vers Une charge utile, partout" title="Lien direct vers Une charge utile, partout" translate="no">​</a></h2>
<p>Un store clé/valeur a un nombre surprenant d'endroits où « une valeur » doit être décrite précisément : la map en mémoire, le flux de réplication, le snapshot de migration, le log d'opérations. Zaris utilise la <em>même</em> forme d'enregistrement pour tous. La chose posée dans le dictionnaire du segment est la même chose qu'un réplica reçoit, est la même chose qu'un nouveau propriétaire reçoit pendant le cut-over. Cette uniformité est pourquoi le <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">log d'opérations peut faire deux travaux à la fois</a> et pourquoi un snapshot de migration et un backfill de réplica n'ont pas besoin d'encodages séparés.</p>
<p>Voici le type entier, réduit à sa surface de fil :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">MessagePackObject</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">StoredRecord</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">0</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">ZarisEntry</span><span class="token plain"> Entry </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">     </span><span class="token comment" style="color:#999988;font-style:italic">// key + value + metadata</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token function" style="color:#d73a49">Key</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">Revision</span><span class="token plain"> Revision </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// segment-wide ordering</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token function" style="color:#d73a49">Key</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">ItemVersion</span><span class="token plain"> Version </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// per-key CAS version</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token function" style="color:#d73a49">Key</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">DateTime</span><span class="token plain"> TimestampUtc </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">4</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> IsDeleted </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// tombstone flag</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">IgnoreMember</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">long</span><span class="token plain"> AccountedBytes </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// node-local</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">IgnoreMember</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">object</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain"> MaterializedValue </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// node-local</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Cinq champs traversent le fil. Deux autres vivent sur l'objet mais sont marqués <code>[IgnoreMember]</code> et ne voyagent jamais. Le reste de cet article explique pourquoi chacun est là où il est.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lencadrement--messagepack-avec-clés-positionnelles">L'encadrement : MessagePack avec clés positionnelles<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#lencadrement--messagepack-avec-cl%C3%A9s-positionnelles" class="hash-link" aria-label="Lien direct vers L'encadrement : MessagePack avec clés positionnelles" title="Lien direct vers L'encadrement : MessagePack avec clés positionnelles" translate="no">​</a></h2>
<p>Zaris sérialise les enregistrements avec <strong>MessagePack</strong>, et les attributs <code>[Key(N)]</code> sont la raison pour laquelle le fil reste petit. MessagePack peut sérialiser un objet de deux façons : comme une <em>map</em> de nom-de-champ → valeur (auto-descriptive, mais chaque message répète chaque nom de champ en chaîne), ou comme un <em>tableau</em> positionnel où chaque champ est identifié par un index entier. Les attributs <code>[Key(0)]</code>, <code>[Key(1)]</code>, … sélectionnent la forme tableau. Sur le fil, un enregistrement est un court tableau de valeurs dans un ordre fixe — pas de noms de champs, pas de ponctuation style JSON, juste les charges utiles.</p>
<p>Cela compte parce que cet enregistrement est la structure de données <em>chaude</em> de tout le système. Il est sérialisé à chaque écriture répliquée et à chaque clé migrée ; à des centaines de milliers d'écritures par seconde, la différence entre expédier <code>"TimestampUtc"</code> comme une chaîne un million de fois et n'expédier que la position n'est pas une micro-optimisation, c'est le budget.</p>
<p>Le schéma positionnel fixe aussi la règle pour faire évoluer le format en toute sécurité : <strong>les clés entières sont en ajout seul (append-only).</strong> Un nouveau champ reçoit le prochain index inutilisé ; les index existants ne changent jamais de sens. Un nœud plus ancien qui ne connaît pas l'index 7 l'ignore simplement ; un nœud plus récent lisant un ancien enregistrement trouve l'index 7 absent et retombe sur une valeur par défaut. Nous verrons un champ — les labels à la clé de map 3 à l'intérieur des métadonnées — qui s'appuie sur exactement ce comportement « absent signifie défaut » pour ne rien coûter quand il n'est pas utilisé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="à-lintérieur-de-lentry--clé-valeur-et-métadonnées-non-nulles">À l'intérieur de l'entry : clé, valeur et métadonnées non nulles<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#%C3%A0-lint%C3%A9rieur-de-lentry--cl%C3%A9-valeur-et-m%C3%A9tadonn%C3%A9es-non-nulles" class="hash-link" aria-label="Lien direct vers À l'intérieur de l'entry : clé, valeur et métadonnées non nulles" title="Lien direct vers À l'intérieur de l'entry : clé, valeur et métadonnées non nulles" translate="no">​</a></h2>
<p><code>ZarisEntry</code> est le champ <code>Entry</code> — la partie qu'un utilisateur reconnaîtrait comme « ses données » :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">MessagePackObject</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">ZarisEntry</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">0</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token plain"> Key </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Empty</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">1</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">object</span><span class="token plain"> Value </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">set</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">default</span><span class="token operator" style="color:#393A34">!</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">2</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">EntryMetadata</span><span class="token plain"> Metadata </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// getter guarantees non-null</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Deux détails ici sont le genre de chose qu'on n'apprend qu'en se brûlant une fois. D'abord, <code>Value</code> est typé <code>object</code>, pas <code>byte[]</code> ni <code>string</code> — la forme concrète de la valeur est ce que le rédacteur y a mis, encadrée à l'intérieur de l'entry, ce qui est comment le même type d'enregistrement transporte une simple chaîne, un compteur ou une <a class="" href="https://clustron.io/fr/blog/data-structures-that-replicate/">collection répliquée</a> entière sans que l'enregistrement ait besoin de savoir laquelle.</p>
<p>Ensuite, <code>Metadata</code> est servie via un getter qui ne peut jamais renvoyer null :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token class-name">EntryMetadata</span><span class="token class-name punctuation" style="color:#393A34">?</span><span class="token plain"> _metadata</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">2</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">EntryMetadata</span><span class="token plain"> Metadata</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">get</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _metadata </span><span class="token operator" style="color:#393A34">??=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">EntryMetadata</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">set</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _metadata </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">value</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>MessagePack contourne le constructeur pendant la désérialisation et appelle directement les setters, donc un simple initialiseur <code>= new()</code> ne survivrait pas à un aller-retour — un enregistrement arrivé sans métadonnées se désérialiserait avec une métadonnée null, et chaque site d'appel en aval aurait besoin d'une vérification de null. Le getter rend la garantie structurelle à la place : les métadonnées sont <em>toujours</em> là, même quand le fil n'en a porté aucune. Un seul endroit pour imposer l'invariant, zéro garde de null partout ailleurs.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="des-métadonnées-qui-ne-coûtent-rien-quand-elles-sont-vides">Des métadonnées qui ne coûtent rien quand elles sont vides<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#des-m%C3%A9tadonn%C3%A9es-qui-ne-co%C3%BBtent-rien-quand-elles-sont-vides" class="hash-link" aria-label="Lien direct vers Des métadonnées qui ne coûtent rien quand elles sont vides" title="Lien direct vers Des métadonnées qui ne coûtent rien quand elles sont vides" translate="no">​</a></h3>
<p><code>EntryMetadata</code> porte les attributs optionnels d'une clé — <code>CreatedAt</code>, <code>Ttl</code>, <code>ContentType</code>, un bail ou verrou optionnel, et des labels. Le champ des labels est une petite leçon magistrale sur l'art de ne pas payer pour ce qu'on n'utilise pas. La plupart des clés n'ont pas de labels, et un <code>Dictionary</code> naïf initialisé par entry est du pur gaspillage — dans un store réel, 390 000 entrées sans label détenaient chacune un dictionnaire vide de 80 octets, environ 31 Mo de rien.</p>
<p>La correction a deux faces. Sur le heap, les entrées sans label partagent un unique dictionnaire vide statique, de sorte que les lecteurs voient toujours une map non nulle (vide) mais n'allouent rien. Sur le fil, la surface sérialisée est null quand il n'y a pas de labels :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">3</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">Dictionary</span><span class="token return-type class-name punctuation" style="color:#393A34">&lt;</span><span class="token return-type class-name keyword" style="color:#00009f">string</span><span class="token return-type class-name punctuation" style="color:#393A34">,</span><span class="token return-type class-name"> LabelValue</span><span class="token return-type class-name punctuation" style="color:#393A34">&gt;</span><span class="token return-type class-name punctuation" style="color:#393A34">?</span><span class="token plain"> LabelsSerialized</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">get</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _labels </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> _labels</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Count </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">?</span><span class="token plain"> _labels </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">set</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _labels </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">value</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">value</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Count </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">?</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">value</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Une entrée sans label écrit <code>nil</code> à l'index 3 et le lecteur en restaure « pas de labels ». C'est le schéma de clés append-only qui gagne sa place : le cas courant coûte un octet, et la fonctionnalité est là en entier à l'instant où une clé a réellement besoin d'un label.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deux-nombres-deux-travaux--revision-et-itemversion">Deux nombres, deux travaux : Revision et ItemVersion<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#deux-nombres-deux-travaux--revision-et-itemversion" class="hash-link" aria-label="Lien direct vers Deux nombres, deux travaux : Revision et ItemVersion" title="Lien direct vers Deux nombres, deux travaux : Revision et ItemVersion" translate="no">​</a></h2>
<p>Le point de confusion le plus courant à propos des enregistrements Zaris est qu'ils portent <em>deux</em> nombres monotones croissants. Ils ne sont pas redondants — ils répondent à des questions différentes, et les confondre casserait l'un ou l'autre travail.</p>
<p><strong><code>ItemVersion</code> est par clé.</strong> Il démarre à 1 et s'incrémente de un chaque fois que <em>cette clé</em> est écrite. C'est le nombre que le client voit et le nombre contre lequel le <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">compare-and-swap</a> optimiste vérifie : « ne valide que si cette clé est toujours à la version 7 ». Quand le store applique un put, la nouvelle version est simplement la version de l'ancienne clé plus un :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">long</span><span class="token plain"> newVersion </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">existing</span><span class="token punctuation" style="color:#393A34">?.</span><span class="token plain">Version</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><strong><code>Revision</code> est par segment.</strong> C'est un unique compteur partagé par <em>chaque</em> clé de la partition, incrémenté à chaque écriture et à chaque suppression :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">revision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">Revision</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Interlocked</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">ref</span><span class="token plain"> _storeRevision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Cela fait de <code>Revision</code> un ordre total sur toutes les mutations d'un segment, ce qui est exactement ce dont la réplication et la récupération ont besoin. Un réplica se moque que la clé <code>user:1</code> en soit à sa 7e version ; ce qui l'intéresse est <em>quelles révisions à l'échelle du segment il a déjà vues</em> afin de pouvoir demander tout ce qui suit ce point. Le log d'opérations est indexé par révision, le protocole de rattrapage demande « tout après la révision R », et la résolution de conflit entre un enregistrement entrant et un enregistrement local compare les révisions, pas les versions par clé :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// An incoming record older than what we already hold is stale — ignore it.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">existing </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> existing</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Revision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">record</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Revision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Donc : <code>ItemVersion</code> est l'unité de la <em>concurrence optimiste visible par le client</em> sur une clé ; <code>Revision</code> est l'unité de l'<em>ordonnancement et de la convergence inter-nœuds</em> sur tout le segment. Même enregistrement, deux horloges, chacune faisant un travail que l'autre ne peut pas.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="une-suppression-est-un-enregistrement-pas-une-absence">Une suppression est un enregistrement, pas une absence<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#une-suppression-est-un-enregistrement-pas-une-absence" class="hash-link" aria-label="Lien direct vers Une suppression est un enregistrement, pas une absence" title="Lien direct vers Une suppression est un enregistrement, pas une absence" translate="no">​</a></h2>
<p>Voici le champ qui surprend le plus : <code>IsDeleted</code>. Quand vous supprimez une clé dans Zaris, le store ne retire pas simplement l'entrée de son dictionnaire. Il écrit un <code>StoredRecord</code> avec <code>IsDeleted = true</code> — une <strong>pierre tombale</strong> (tombstone) — l'estampille de la prochaine révision du segment, et l'ajoute au log d'opérations comme une suppression :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> revision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">Revision</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Interlocked</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">ref</span><span class="token plain"> _storeRevision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">_deleteRevisions</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> revision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">AppendOperationLog</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">revision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ReplicationOperation</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Delete</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">StoredRecord</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Revision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> revision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    IsDeleted </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La raison est tout l'enjeu du format d'enregistrement. Si une suppression n'était qu'une absence — la clé disparaissant discrètement d'une map — il n'y aurait rien à répliquer. Un réplica qui a manqué le moment de la suppression continuerait de servir l'ancienne valeur à jamais, car « rien » ne voyage pas et ne peut pas être ordonné par rapport aux écritures qui l'entourent. En faisant de la suppression un enregistrement de première classe avec sa propre révision, la suppression circule par le même chemin de réplication et de rattrapage qu'une écriture, et se glisse dans l'ordre total du segment à la bonne place.</p>
<p>Les pierres tombales défendent aussi contre une défaillance plus subtile : une écriture périmée <em>ressuscitante</em>. Imaginez une suppression à la révision 50 et un put plus ancien pour la même clé, révision 48, encore en vol à travers le réseau. Sans mémoire de la suppression, le put tardif atterrirait et ramènerait la clé d'entre les morts. Zaris enregistre la révision de suppression et rejette tout ce qui lui est antérieur ou égal :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">HasDeleteRevisionAtOrAfter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">long</span><span class="token plain"> revision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _deleteRevisions</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryGetValue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">out</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> deleteRevision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> deleteRevision </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> revision</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<!-- -->
<p>Le revers des pierres tombales est qu'elles ne peuvent pas s'accumuler à jamais, sinon une charge à forte suppression fuirait de la mémoire. Zaris élague les révisions de suppression une fois qu'elles sont en sécurité en dessous de la plus ancienne révision dont un réplica a encore besoin — la même logique de low-water-mark qui élague le log d'opérations — de sorte que la pierre tombale fait son travail de convergence puis est récupérée. (La discipline d'élagage fait l'objet de l'<a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">article sur le log d'opérations</a>.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-qui-reste-à-la-maison--les-champs-ignoremember">Ce qui reste à la maison : les champs <code>[IgnoreMember]</code><a href="https://clustron.io/fr/blog/what-crosses-the-wire/#ce-qui-reste-%C3%A0-la-maison--les-champs-ignoremember" class="hash-link" aria-label="Lien direct vers ce-qui-reste-à-la-maison--les-champs-ignoremember" title="Lien direct vers ce-qui-reste-à-la-maison--les-champs-ignoremember" translate="no">​</a></h2>
<p>Les deux champs que l'enregistrement porte mais ne sérialise jamais sont aussi délibérés que les cinq qu'il sérialise. Les deux sont marqués <code>[IgnoreMember]</code>, et les deux sont <em>node-local par conception</em> — corrects seulement pour le nœud qui les a calculés.</p>
<p><strong><code>AccountedBytes</code></strong> est la mémoire dont cet enregistrement est débité contre le budget du store, estampillée une fois au moment de l'écriture pour que le store puisse rapporter les deltas d'octets sans remesurer l'enregistrement précédent. Elle n'est explicitement <em>pas</em> sérialisée, car chaque nœud la calcule à partir de la clé et de la valeur de l'enregistrement sous ses propres règles de comptabilité. Si elle chevauchait le fil, un enregistrement répliqué depuis le nœud A porterait l'estimation d'octets de A sur les comptes du nœud B — la comptabilité d'un nœud faussant silencieusement celle d'un autre. La garder <code>[IgnoreMember]</code> signifie que chaque nœud la recalcule indépendamment, et un enregistrement arrivant par réplication ou migration est comptabilisé par le nœud qui le détient désormais.</p>
<p><strong><code>MaterializedValue</code></strong> est une forme en mémoire vivante, construite paresseusement, d'une collection native (un hash, une list, un set ou un sorted set) mise en cache pour que les opérations par élément ne re-désérialisent pas toute la valeur à chaque fois. C'est de la pure accélération locale. La forme faisant autorité est toujours les octets de valeur encadrés à l'intérieur d'<code>Entry</code> ; un enregistrement qui arrive d'un autre nœud matérialise ce cache paresseusement au premier accès. Le sérialiser reviendrait à expédier l'état de brouillon privé d'un nœud comme s'il s'agissait de données.</p>
<p>La discipline ici est nette et vaut la peine d'être énoncée à voix haute : <strong>le fil porte la vérité, le nœud porte les commodités.</strong> Tout ce qu'un nœud peut recalculer depuis l'enregistrement lui-même — son coût mémoire, sa structure matérialisée — reste local et est marqué <code>[IgnoreMember]</code>. Tout ce qui fait partie de l'<em>identité et de l'ordonnancement</em> de l'enregistrement — clé, valeur, métadonnées, les deux numéros de version, le drapeau de pierre tombale — traverse le fil. La frontière des attributs est exactement la frontière vérité/commodité, et la garder nette est ce qui rend l'enregistrement sûr à copier entre nœuds sans y repenser à deux fois.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-cest-testable-et-testé">Pourquoi c'est testable, et testé<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#pourquoi-cest-testable-et-test%C3%A9" class="hash-link" aria-label="Lien direct vers Pourquoi c'est testable, et testé" title="Lien direct vers Pourquoi c'est testable, et testé" translate="no">​</a></h2>
<p>Parce que la forme de l'enregistrement est porteuse pour la convergence, son comportement sur le fil est épinglé par des tests plutôt que laissé à l'inspection. Deux invariants en particulier gagnent leurs propres assertions : le drapeau de pierre tombale doit survivre à un aller-retour (un <code>IsDeleted</code> perdu ressuscite une clé supprimée sur un réplica), et <code>AccountedBytes</code> doit revenir à zéro après un aller-retour (prouvant qu'il n'a jamais voyagé) :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">back</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">IsDeleted</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Should</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">BeTrue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"a dropped tombstone flag resurrects a deleted key on the replica"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">back</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AccountedBytes</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Should</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Be</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"AccountedBytes is [IgnoreMember]; each node recomputes it, it must not cross the wire"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Ces assertions se lisent presque comme la thèse de tout l'article écrite sous forme d'assertions : les choses qui doivent traverser le fil sont prouvées y survivre, et celles qui ne le doivent pas sont prouvées disparaître.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lenregistrement-est-le-contrat">L'enregistrement est le contrat<a href="https://clustron.io/fr/blog/what-crosses-the-wire/#lenregistrement-est-le-contrat" class="hash-link" aria-label="Lien direct vers L'enregistrement est le contrat" title="Lien direct vers L'enregistrement est le contrat" translate="no">​</a></h2>
<p>Il est facile de penser que les problèmes difficiles d'un store distribué vivent dans les parties élégantes — le <a class="" href="https://clustron.io/fr/blog/anatomy-of-a-failover/">protocole de basculement</a>, la <a class="" href="https://clustron.io/fr/blog/partition-cutover-protocol/">poignée de main de cut-over</a>, le <a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">test du chaos</a>. C'est vrai. Mais ces protocoles ne sont corrects que dans la mesure où la chose qu'ils déplacent l'est, et la chose qu'ils déplacent est ce petit enregistrement unique. Une pierre tombale au lieu d'une absence est pourquoi une suppression converge. Deux numéros de version sont pourquoi la concurrence optimiste d'une clé et l'ordonnancement de tout un segment restent démêlés. Des clés MessagePack positionnelles sont pourquoi le chemin chaud reste bon marché et le format peut quand même grandir. Et une paire de champs <code>[IgnoreMember]</code> sont pourquoi la réplication peut copier un enregistrement entre nœuds et avoir confiance que rien de node-local n'est venu faire le voyage.</p>
<p>Obtenez la bonne unité de données et les grands protocoles ont quelque chose de solide sur quoi se tenir. C'est tout l'argument pour consacrer un article de blog à cinq champs et deux qui restent à la maison.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Le plafond du worker pool : quand un nœud plafonne à 25 % de CPU]]></title>
            <link>https://clustron.io/fr/blog/worker-pool-ceiling/</link>
            <guid>https://clustron.io/fr/blog/worker-pool-ceiling/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nœud Zaris qui servait les lectures à un peu moins d'un demi-million d'ops/s tout en restant au quart de son CPU n'était ni limité par les cœurs ni par le client — le worker pool de dispatch des requêtes était le plafond. Voici le mécanisme, la correction, et comment le régler honnêtement avec ZARIS_SERVER_MAX_WORKERS.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Un nœud bloqué près de 25 % de CPU avec un débit plat parce que le worker pool de dispatch était le plafond" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJBIG5vZGUgcGlubmVkIG5lYXIgMjUgcGVyY2VudCBDUFUgd2l0aCBmbGF0IHRocm91Z2hwdXQgYmVjYXVzZSB0aGUgZGlzcGF0Y2ggd29ya2VyIHBvb2wsIG5vdCB0aGUgY29yZXMsIHdhcyB0aGUgY2VpbGluZyI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgwIiBjeT0iMC4xOCIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQoNCiAgPCEtLSBleWVicm93ICsgdGl0bGUgLS0+DQogIDx0ZXh0IHg9IjgwIiB5PSI1NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5USFJPVUdIUFVUIHZzIENQVTwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjkyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIzMSIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPkEgbm9kZSBhdCAyNSUgQ1BVLCBhbmQgdGhyb3VnaHB1dCBmbGF0PC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxOCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+VGhlIGRpc3BhdGNoIHdvcmtlciBwb29sICYjODIxMjsgbm90IHRoZSBjb3JlcyAmIzgyMTI7IHdhcyB0aGUgY2VpbGluZzwvdGV4dD4NCg0KICA8IS0tIHBsb3QgZ2VvbWV0cnk6IHggMTUwLi4xMDQwIDsgYmFzZWxpbmUgeT00MDAgKDAgb3BzKSA7IGNhcGFjaXR5IHk9MTg1IChjb3JlcyBzYXR1cmF0ZWQpIC0tPg0KICA8IS0tIGNhcGFjaXR5IC8gZnVsbC1DUFUgcmVmZXJlbmNlIC0tPg0KICA8bGluZSB4MT0iMTUwIiB5MT0iMTg1IiB4Mj0iMTA0MCIgeTI9IjE4NSIgc3Ryb2tlPSIjM2E1ZDRmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjkgNiIvPg0KICA8dGV4dCB4PSIxMDQwIiB5PSIxNzYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjOWRiNGFhIiB0ZXh0LWFuY2hvcj0iZW5kIj5jb3JlcyBzYXR1cmF0ZWQgJiM4MjEyOyBsaW5lIHJhdGU8L3RleHQ+DQogIDxsaW5lIHgxPSIxNTAiIHkxPSI0MDAiIHgyPSIxMDQwIiB5Mj0iNDAwIiBzdHJva2U9IiMyNDQ2M2IiIHN0cm9rZS13aWR0aD0iMS41Ii8+DQoNCiAgPCEtLSBpZGxlIGhlYWRyb29tIHRoZSBuYXJyb3cgcG9vbCBjb3VsZCBub3QgdXNlIC0tPg0KICA8cmVjdCB4PSI0NzAiIHk9IjE4NSIgd2lkdGg9IjU3MCIgaGVpZ2h0PSIxNDUiIGZpbGw9IiMxMmI1N2UiIGZpbGwtb3BhY2l0eT0iMC4wNyIvPg0KICA8dGV4dCB4PSI3NTUiIHk9IjI2MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM2Zjk0ODgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmlkbGUgaGVhZHJvb20gJiMxODM7IGNvcmVzIHdhaXRpbmc8L3RleHQ+DQoNCiAgPCEtLSBjdXJ2ZSBBOiBuYXJyb3cgcG9vbCwgcGxhdGVhdXMgbG93IChQcm9jQ291bnQgeDIpIC0tPg0KICA8cGF0aCBkPSJNMTUwIDM5OSBDIDI1MCAzNjIgMzQwIDMzMyA0NzAgMzMwIEwxMDQwIDMzMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNmI4ZjgzIiBzdHJva2Utd2lkdGg9IjMiIHN0cm9rZS1kYXNoYXJyYXk9IjEgMCIvPg0KICA8Y2lyY2xlIGN4PSIxMDQwIiBjeT0iMzMwIiByPSI0LjUiIGZpbGw9IiM2YjhmODMiLz4NCiAgPHRleHQgeD0iNDcwIiB5PSIzNTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjOWRiNGFhIj5wb29sID0gUHJvY0NvdW50ICYjMjE1OyAyICYjODU5NDsgZmxhdCAmIzg3NzY7IDAuNU0gb3BzL3MsIENQVSAmIzg3NzY7IDI1JTwvdGV4dD4NCg0KICA8IS0tIGN1cnZlIEI6IHdpZGVuZWQgcG9vbCwgY2xpbWJzIHRvd2FyZCBzYXR1cmF0aW9uIC0tPg0KICA8cGF0aCBkPSJNMTUwIDM5OSBDIDMyMCAzMzAgNTQwIDI1MCA3NjAgMjEyIFMgOTgwIDE5MiAxMDQwIDE5MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iNCIvPg0KICA8Y2lyY2xlIGN4PSIxMDQwIiBjeT0iMTkwIiByPSI1IiBmaWxsPSIjMmJkNmEwIi8+DQogIDx0ZXh0IHg9IjcwNSIgeT0iMTY4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+d2lkZW5lZCBwb29sICYjMTgzOyBDUFUgJiMyMTU7IDE2LCB0dW5hYmxlPC90ZXh0Pg0KDQogIDwhLS0gYXhpcyBjYXB0aW9ucyAtLT4NCiAgPHRleHQgeD0iMTUwIiB5PSI0MjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjN2Y5YThmIj5vZmZlcmVkIGxvYWQgJiM4NTk0OzwvdGV4dD4NCiAgPHRleHQgeD0iMTM4IiB5PSI0MDQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjN2Y5YThmIiB0ZXh0LWFuY2hvcj0iZW5kIj4wPC90ZXh0Pg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0NTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE5IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQzIiB5PSI0NTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE5IiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Voici un résultat de benchmark qui ressemble à une contradiction. Un nœud Zaris servant une charge majoritairement en lecture plafonne à un peu moins d'un demi-million d'ops/s et refuse de grimper — ajoutez plus de connexions client, plus de générateurs de charge, et le nombre ne bouge pas. Le diagnostic évident est « il est saturé ». Sauf que le graphe de CPU dit le contraire : le nœud est posé à environ un <em>quart</em> de son CPU disponible. Trois cœurs sur quatre sont inactifs, et le débit est plat.</p>
<p>Ce n'est pas un nœud limité par les cœurs, et — si vous l'avez écarté avec soin — ce n'est pas non plus un benchmark limité par le client. C'est une troisième chose, et c'est un classique : le <strong>worker pool</strong> de dispatch des requêtes était plus étroit que le travail ne le voulait, donc les requêtes faisaient la queue pendant que les cœurs se tournaient les pouces.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dabord-écartez-les-suspects-habituels">D'abord, écartez les suspects habituels<a href="https://clustron.io/fr/blog/worker-pool-ceiling/#dabord-%C3%A9cartez-les-suspects-habituels" class="hash-link" aria-label="Lien direct vers D'abord, écartez les suspects habituels" title="Lien direct vers D'abord, écartez les suspects habituels" translate="no">​</a></h2>
<p>Deux explications doivent partir avant que celle-ci ne soit même sur la table, car elles sont bien plus courantes.</p>
<p><strong>Limité par les cœurs.</strong> Si le nœud est à ou près de 100 % de CPU et que le débit est plat, vous êtes véritablement à court de CPU — la réponse est plus de cœurs ou plus de nœuds, et Zaris <a class="" href="https://clustron.io/fr/blog/reads-scale-with-cores/">met à l'échelle les lectures en ajoutant des cœurs</a>. Ce n'est pas ce cas : ici le CPU est <em>inactif</em>.</p>
<p><strong>Limité par le client.</strong> Un seul générateur de charge sature bien avant le serveur, et alors vous mesurez votre client, pas Zaris. Si le débit est plat quand vous ajoutez des cœurs <em>serveur</em>, suspectez d'abord votre client — ajoutez des générateurs et des connexions jusqu'à ce que le nombre côté serveur réponde réellement. Nous avons couvert ce piège dans l'article sur les lectures, et c'est celui à éliminer avant d'aller plus loin.</p>
<p>Ce qui reste après ces deux-là est le cas intéressant : assez de charge offerte pour garder le nœud occupé, beaucoup de marge de CPU, et un débit toujours plat. Quand le goulet d'étranglement est à l'intérieur du serveur mais que ce n'est pas le CPU, la chose à regarder est la concurrence — combien de requêtes le nœud <em>traitera en même temps</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-pool-de-dispatch-est-la-limite-de-concurrence">Le pool de dispatch est la limite de concurrence<a href="https://clustron.io/fr/blog/worker-pool-ceiling/#le-pool-de-dispatch-est-la-limite-de-concurrence" class="hash-link" aria-label="Lien direct vers Le pool de dispatch est la limite de concurrence" title="Lien direct vers Le pool de dispatch est la limite de concurrence" translate="no">​</a></h2>
<p>Chaque requête qui arrive à un nœud Zaris est remise à un worker pool de dispatch de requêtes. Un worker la ramasse, la route vers la partition qui possède la clé, exécute l'opération contre le store en processus de cette partition, et réécrit la réponse. Pour une lecture, l'opération elle-même est une recherche en mémoire à l'échelle de la microseconde — véritablement minuscule.</p>
<p>La largeur de ce pool — combien de workers peuvent être en pleine requête simultanément — fixe un plafond dur sur les requêtes en vol, et c'est un nombre différent de « à quelle vitesse va une opération ». La valeur par défaut d'origine dérivait la largeur d'un petit multiple de <code>Environment.ProcessorCount</code>. Sur un nœud à 4 vCPU, un pool à quelques fois les cœurs n'est qu'une poignée de workers.</p>
<p>Voici la subtilité qui fait paraître le graphe de CPU faux. Si chaque worker passait toute sa tranche à brûler du CPU sur la recherche, un pool dimensionné aux cœurs serait exactement juste et le nœud <em>atteindrait</em> 100 % de CPU. Mais les workers ne passent pas toute leur tranche sur le CPU. Un worker se met occasionnellement en pause — une continuation <code>await</code>, une écriture de socket qui ne se termine pas de façon synchrone, un bref verrou sur une structure partagée. Un worker en pause ne brûle aucun CPU, mais <strong>il garde quand même son emplacement de pool</strong>. Avec seulement une poignée d'emplacements, quelques workers en pause suffisent à bloquer tout le nœud : les nouvelles requêtes font la queue derrière eux, et parce que les workers occupés attendent surtout plutôt que de calculer, les cœurs ne s'allument jamais. Débit plat, CPU inactif. La largeur du pool était le plafond, pas le silicium.</p>
<!-- -->
<p>L'indice qui distingue cela d'un benchmark limité par le client : les requêtes <em>font la queue au serveur</em> (la latence grimpe, le débit est plat) pendant que le CPU <em>serveur</em> reste bas. Le cas limité par le client a l'air différent — le serveur est inactif parce que rien n'arrive assez vite. Ici, le travail arrive ; le nœud refuse juste d'en prendre assez à la fois.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-correction--dimensionner-le-pool-pour-le-travail-pas-pour-le-nombre-de-cœurs">La correction : dimensionner le pool pour le travail, pas pour le nombre de cœurs<a href="https://clustron.io/fr/blog/worker-pool-ceiling/#la-correction--dimensionner-le-pool-pour-le-travail-pas-pour-le-nombre-de-c%C5%93urs" class="hash-link" aria-label="Lien direct vers La correction : dimensionner le pool pour le travail, pas pour le nombre de cœurs" title="Lien direct vers La correction : dimensionner le pool pour le travail, pas pour le nombre de cœurs" translate="no">​</a></h2>
<p>La réparation n'est pas exotique. Les lectures sont des recherches en mémoire à la microseconde, donc le pool devrait être fixé <em>large</em> par défaut — assez large pour qu'une poignée de workers en pause ne puisse pas affamer le nœud — et il devrait être réglable, car la bonne largeur dépend du nœud et de la charge, pas d'un multiple fixe du nombre de cœurs.</p>
<p>La largeur de pool par défaut est désormais <code>nombre de CPU × 16</code>, bornée dans une plage saine de 64 à 1024, avec un plancher de <code>max(4, nombre de CPU × 4)</code>. Deux variables d'environnement la remplacent :</p>
<table><thead><tr><th>Variable</th><th>Valeur par défaut</th><th>Ce qu'elle fait</th></tr></thead><tbody><tr><td><code>ZARIS_SERVER_MAX_WORKERS</code></td><td>nombre de CPU × 16 (borné 64–1024)</td><td>Remplacement dur (1–4096) pour la largeur du pool de dispatch. Relevez-la quand un nœud a du CPU en réserve et que les requêtes font la queue.</td></tr><tr><td><code>ZARIS_SERVER_MIN_WORKERS</code></td><td>max(4, nombre de CPU × 4)</td><td>Borne inférieure (1–4096) sur la largeur du pool.</td></tr></tbody></table>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># A node with spare CPU and requests queuing — widen the pool.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">ZARIS_SERVER_MAX_WORKERS</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">512</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">ZARIS_SERVER_MIN_WORKERS</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">64</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  dotnet Clustron.Zaris.Server.dll</span><br></span></code></pre></div></div>
<p>Une propriété qui mérite d'être soulignée : c'est un changement <strong>côté serveur</strong> et rien d'autre. Le protocole de fil ne change pas, le SDK client ne change pas, et il n'y a pas de bump NuGet à poursuivre — vous réglez un nœud en le redémarrant avec un environnement différent, et chaque client existant continue de lui parler sans changement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="réglez-le-honnêtement--élargir-nest-pas-gratuit">Réglez-le honnêtement — élargir n'est pas gratuit<a href="https://clustron.io/fr/blog/worker-pool-ceiling/#r%C3%A9glez-le-honn%C3%AAtement--%C3%A9largir-nest-pas-gratuit" class="hash-link" aria-label="Lien direct vers Réglez-le honnêtement — élargir n'est pas gratuit" title="Lien direct vers Réglez-le honnêtement — élargir n'est pas gratuit" translate="no">​</a></h2>
<p>Le réflexe après avoir lu ceci est de pousser <code>ZARIS_SERVER_MAX_WORKERS</code> au plafond sur chaque nœud. Ne le faites pas. Un pool plus large aide dans exactement une situation et ne fait rien — ou nuit légèrement — dans les autres.</p>
<ul>
<li class=""><strong>Il aide quand les requêtes font la queue pendant que le CPU est inactif.</strong> C'est le cas de tout cet article : la concurrence en vol est la limite, donc en admettre davantage remplit les cœurs. C'est là que vous verrez le débit bondir.</li>
<li class=""><strong>Il ne fait rien quand vous êtes déjà limité par le CPU.</strong> Si le nœud est à 100 % de CPU, plus de workers ne feront pas surgir du débit de nulle part — ils ajoutent juste de la surcharge de changement de contexte à une machine qui n'a aucun cycle en réserve. Plus de workers n'est pas plus de calcul.</li>
<li class=""><strong>Il peut nuire si vous le poussez à une largeur absurde.</strong> Des milliers de workers malmenant un nœud à 2 vCPU passent du vrai temps à changer de contexte au lieu de servir. C'est pourquoi il y a une borne, et pourquoi le bouton plafonne à 4096 plutôt qu'à l'infini.</li>
</ul>
<p>La boucle de réglage honnête est : guetter la signature — latence qui grimpe et débit plat <em>pendant que le CPU est bien en dessous de la saturation</em> — relever <code>MAX_WORKERS</code>, et mesurer. Si le CPU grimpe et que le débit suit, le pool était votre plafond. Si rien ne bouge, ce n'était pas lui, et vous devriez remettre le bouton et chercher ailleurs (le chemin réseau, le client, ou un besoin véritable de plus de nœuds).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mesurez-le-cpu-là-où-cest-réellement-vrai">Mesurez le CPU là où c'est réellement vrai<a href="https://clustron.io/fr/blog/worker-pool-ceiling/#mesurez-le-cpu-l%C3%A0-o%C3%B9-cest-r%C3%A9ellement-vrai" class="hash-link" aria-label="Lien direct vers Mesurez le CPU là où c'est réellement vrai" title="Lien direct vers Mesurez le CPU là où c'est réellement vrai" translate="no">​</a></h2>
<p>Toute cette enquête repose sur la confiance dans le nombre de CPU, et ce nombre est facile à obtenir faux.</p>
<ul>
<li class=""><strong>Lisez le CPU depuis le cgroup, pas depuis une estimation de metrics-server de cluster.</strong> Sur Kubernetes surtout, le chiffre du metrics-server est en retard et lissé — il vous dira qu'un nœud est à 40 % alors que la comptabilité du cgroup dit qu'il est saturé, ou l'inverse. Diagnostiquez un cas « débit plat, CPU inactif » depuis le cgroup, ou vous poursuivrez un nombre qui n'est pas réel.</li>
<li class=""><strong>Les benchmarks locaux sont limités par l'hôte et cacheront le plafond.</strong> Sur un ordinateur portable, le générateur de charge et le serveur se disputent les mêmes cœurs, donc vous ne présentez jamais au serveur une charge propre et suffisante — le plateau que vous mesurez est votre banc d'essai, pas Zaris. Le vrai plafond du worker pool est apparu à l'échelle sur un cluster managé avec la charge pilotée depuis des machines séparées, pas sur une machine de développeur. Validez le débit là où le client ne peut pas être le goulet d'étranglement.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="à-retenir">À retenir<a href="https://clustron.io/fr/blog/worker-pool-ceiling/#%C3%A0-retenir" class="hash-link" aria-label="Lien direct vers À retenir" title="Lien direct vers À retenir" translate="no">​</a></h2>
<p>Une courbe de débit plate a trois causes très différentes, et le graphe de CPU vous dit laquelle vous regardez. À 100 % de CPU, vous êtes limité par les cœurs — ajoutez des nœuds. Avec le serveur <em>inactif</em> parce que rien n'arrive assez vite, vous êtes limité par le client — corrigez le test. Mais un nœud plat au quart de son CPU, avec des requêtes qui font visiblement la queue, n'est ni l'un ni l'autre : c'est une limite de concurrence, et sur Zaris c'est le worker pool de dispatch. La valeur par défaut est désormais assez large pour que la plupart des charges ne le rencontrent jamais, et <code>ZARIS_SERVER_MAX_WORKERS</code> est là pour celles qui le rencontrent — à relever délibérément, quand le graphe de CPU l'a mérité.</p>
<p>Pour la surface complète des variables d'environnement et les valeurs par défaut exactes, voir la <a class="" href="https://clustron.io/fr/docs/clustron/zaris/reference/configuration/#environment-variables">référence de configuration</a>. Si vous dimensionnez un cluster pour le débit, le <a class="" href="https://clustron.io/fr/docs/clustron/zaris/admin-guide/benchmarking/">guide de benchmarking</a> parcourt les réglages qui font réellement bouger les nombres, et <a class="" href="https://clustron.io/fr/blog/reads-scale-with-cores/">Des lectures qui montent avec vos cœurs</a> couvre l'autre moitié de l'histoire — pourquoi, une fois le pool hors du chemin, ajouter des cœurs ajoute de la capacité de lecture au lieu de buter contre un mur à boucle d'événements unique.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Performance</category>
            <category>.NET</category>
            <category>Opérations</category>
        </item>
        <item>
            <title><![CDATA[Anatomie d'un basculement : ce qu'il advient d'une écriture en vol quand le propriétaire meurt]]></title>
            <link>https://clustron.io/fr/blog/anatomy-of-a-failover/</link>
            <guid>https://clustron.io/fr/blog/anatomy-of-a-failover/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nœud détenant la copie active de votre partition meurt alors que des écritures y atterrissent. Suivez la séquence exacte que Zaris exécute ensuite — détection et confirmation, promotion d'un réplica protégée par une époque, une preuve de possession qui décide si le nouveau propriétaire sert ou se synchronise d'abord, et la redirection du client qui fait atterrir votre nouvelle tentative sur le nouveau propriétaire — ainsi que les cas honnêtes où une écriture est véritablement perdue.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Anatomie d&amp;#39;un basculement dans Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJBbmF0b215IG9mIGEgZmFpbG92ZXI6IHdoYXQgaGFwcGVucyB0byBhbiBpbi1mbGlnaHQgd3JpdGUgd2hlbiB0aGUgb3duZXIgZGllcyI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPkZBSUxPVkVSPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPkFuYXRvbXkgb2YgYTwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjE4MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij5mYWlsb3ZlcjwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPldoYXQgaGFwcGVucyB0byBhbiBpbi1mbGlnaHQgd3JpdGUgd2hlbiB0aGUgb3duZXIgZGllczwvdGV4dD4NCiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUwLDMwMCkiPg0KICAgIDwhLS0gb2xkIGFjdGl2ZSwgZGVhZCAtLT4NCiAgICA8cmVjdCB4PSIwIiB5PSIyMCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI1NiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtZGFzaGFycmF5PSI2IDYiLz4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNDQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5hY3RpdmUgwrcgZXBvY2ggNzwvdGV4dD4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij4mIzEwMDA3OyBub2RlIGxvc3Q8L3RleHQ+DQogICAgPCEtLSBhcnJvdzogcHJvbW90ZSAtLT4NCiAgICA8cGF0aCBkPSJNMTYwIDQ4IEwyNTYgNDgiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiLz4NCiAgICA8dGV4dCB4PSIyMDgiIHk9IjM2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzJiZDZhMCI+cHJvbW90ZTwvdGV4dD4NCiAgICA8IS0tIHJlcGxpY2EgcHJvbW90ZWQgLS0+DQogICAgPHJlY3QgeD0iMjYyIiB5PSIyMCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSI1NiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIzNDciIHk9IjQ0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+cmVwbGljYSAmIzg1OTQ7IGFjdGl2ZTwvdGV4dD4NCiAgICA8dGV4dCB4PSIzNDciIHk9IjY0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzJiZDZhMCI+ZXBvY2ggOCDCtyBEZWdyYWRlZDwvdGV4dD4NCiAgICA8IS0tIGFycm93OiByZXJvdXRlIC0tPg0KICAgIDxwYXRoIGQ9Ik00NDIgNDggTDUzOCA0OCIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iNSIvPg0KICAgIDx0ZXh0IHg9IjQ5MCIgeT0iMzYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5Nb3ZlZCAmIzg1OTQ7IHJldHJ5PC90ZXh0Pg0KICAgIDwhLS0gY2xpZW50IHdyaXRlIGxhbmRzIC0tPg0KICAgIDxyZWN0IHg9IjU0NCIgeT0iMjAiIHdpZHRoPSIxNjAiIGhlaWdodD0iNTYiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iNjI0IiB5PSI0NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPndyaXRlIGNvbW1pdHRlZDwvdGV4dD4NCiAgICA8dGV4dCB4PSI2MjQiIHk9IjY0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+b24gdGhlIG5ldyBvd25lcjwvdGV4dD4NCiAgICA8Y2lyY2xlIGN4PSIyMDgiIGN5PSI0OCIgcj0iNiIgZmlsbD0iIzJiZDZhMCIvPg0KICAgIDxjaXJjbGUgY3g9IjQ5MCIgY3k9IjQ4IiByPSI2IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iMCIgeT0iMTA4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzVhNmI2NSI+bWFwIHN0YXlzIHB1dCAmIzE4Mzsgb25seSB0aGUgcnVudGltZSBhY3RpdmUgbW92ZXMgJiMxODM7IHRoZSBlcG9jaCBmZW5jZXMgdGhlIHN0YWxlIG93bmVyPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Un basculement est le moment où un store distribué est le plus honnête sur ce qu'il est réellement. En régime permanent, tout store se ressemble : une écriture atterrit sur un propriétaire, est acquittée, vous passez à la suite. La conception ne se montre que lorsque le propriétaire <em>disparaît au milieu d'une phrase</em> — un nœud détient la copie active de l'une de vos partitions, des écritures y atterrissent à l'instant même, puis il n'est plus là. Tué net. Partitionné par le réseau. OOM. Le cluster a quelques secondes pour s'en apercevoir, s'accorder sur un nouveau propriétaire et faire repartir le trafic — et le fait de s'y prendre correctement décide si votre écriture acquittée survit ou s'évapore discrètement.</p>
<p>Cet article suit cette séquence de bout en bout, dans l'ordre où Zaris l'exécute réellement, et nomme les cas où la réponse est véritablement « l'écriture est perdue » — car un article sur le basculement qui ne décrit que le chemin heureux ne vaut pas la lecture.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lunique-invariant-qui-rend-le-reste-traitable">L'unique invariant qui rend le reste traitable<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#lunique-invariant-qui-rend-le-reste-traitable" class="hash-link" aria-label="Lien direct vers L'unique invariant qui rend le reste traitable" title="Lien direct vers L'unique invariant qui rend le reste traitable" translate="no">​</a></h2>
<p>Avant la chronologie, l'unique décision de conception dont tout le reste dépend : <strong>Zaris sépare la <em>carte</em> de partitions du <em>runtime</em> de partitions.</strong></p>
<p>La carte de partitions est l'affectation lente et délibérée — <em>quels nœuds sont responsables de la partition P</em> : un primaire préféré et ses réplicas. Elle ne change que lors d'événements topologiques réels (un nœud rejoint ou quitte définitivement, une montée en charge, un rééquilibrage) et elle change par une régénération planifiée et versionnée.</p>
<p>Le runtime est la réponse rapide et vivante à une question différente — <em>quel nœud affecté est le propriétaire actif servant P à l'instant présent, et dans quelle santé</em>. Pendant un basculement, <strong>la carte ne bouge pas du tout.</strong> Seul le primaire actif du runtime bouge. C'est l'invariant énoncé dans le service de basculement lui-même : un signal de défaillance indépendant ouvre la barrière de basculement du runtime, « PartitionMap remains stable and only PartitionRuntimeMap.Active is allowed to move during failover. »</p>
<p>Pourquoi cela importe : régénérer une carte de partitions est coûteux et dangereux — cela peut rebrasser des partitions saines et déclencher une migration de données. Un nœud qui meurt ne devrait <em>pas</em> déclencher cela. Il devrait déclencher une décision de runtime bon marché et locale : choisir un réplica survivant de la même partition et en faire le primaire actif. La carte dit toujours « P est possédé par n1, n2, n3 » ; le runtime bascule de « actif = n1 » à « actif = n2 ». Garder ces deux plans séparés est ce qui permet au basculement d'être rapide sans être téméraire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-1--détecter-mais-ne-pas-croire-la-première-rumeur">Étape 1 — détecter, mais ne pas croire la première rumeur<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#%C3%A9tape-1--d%C3%A9tecter-mais-ne-pas-croire-la-premi%C3%A8re-rumeur" class="hash-link" aria-label="Lien direct vers Étape 1 — détecter, mais ne pas croire la première rumeur" title="Lien direct vers Étape 1 — détecter, mais ne pas croire la première rumeur" translate="no">​</a></h2>
<p>Le basculement est coordonné par le leader. Un nœud du cluster est le leader élu, et seul le leader promeut — un non-leader qui entend un événement node-left l'ignore simplement. Cela évite que deux nœuds n'élisent indépendamment deux nouveaux propriétaires différents pour la même partition.</p>
<p>Lorsque le leader reçoit un signal <code>NodeLeft</code> pour un nœud, il ne promeut <em>pas</em> immédiatement des réplicas pour chaque partition que ce nœud possédait. La promotion est désignée dans le code comme « an irreversible, high-impact action », et elle est protégée :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">activeOwner</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nodeId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> StringComparison</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// A NodeLeft message can race ahead of the peer-table update.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// If the "dead" owner is in fact still reachable, do NOT fail it over.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">IsNodeAlive</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">nodeId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NodeUnavailableRejectedReachable</span><span class="token punctuation" style="color:#393A34">(</span><span class="token comment" style="color:#999988;font-style:italic">/* ... */</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">PromoteOrMarkUnavailableAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">partition</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nodeId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> runtime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>La garde existe parce que le <em>message</em> de défaillance et la <em>table de pairs</em> de la couche d'appartenance se mettent à jour de façon asynchrone. Un <code>NodeLeft</code> peut arriver un battement de cœur avant que la table de pairs ne le reflète — ou être tout simplement faux. Donc, avant l'action à fort impact, le leader revérifie directement la vivacité. Un propriétaire « mort » encore joignable est laissé exactement où il est.</p>
<p>Il existe un second chemin, plus lent, pour le cas que le premier ne peut pas couvrir : un propriétaire actif devenu silencieux sans un <code>NodeLeft</code> propre. Un balayage de réconciliation périodique (<code>HandleUnavailableActiveOwnersAsync</code>) parcourt chaque partition, et pour un propriétaire qui n'est pas vivant mais n'a pas été confirmé en panne, il démarre une <strong>minuterie de suspicion</strong> plutôt que d'agir :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> suspectedSince </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _suspectedUnavailableSinceUtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetOrAdd</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">activeOwner</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _ </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> DateTime</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">UtcNow</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">DateTime</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">UtcNow </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> suspectedSince </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> _suspectEscalationTimeout</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">UnconfirmedActiveOwnerFailure</span><span class="token punctuation" style="color:#393A34">(</span><span class="token comment" style="color:#999988;font-style:italic">/* ... */</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// not yet — give it until the escalation timeout</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">ConfirmNodeUnavailable</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">activeOwner</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"RuntimeOwnershipSuspectEscalationTimeout"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Ce n'est qu'après que le propriétaire a été injoignable pendant toute la fenêtre d'escalade (dix secondes par défaut) que le leader le confirme en panne et poursuit. La conception est délibérément asymétrique : <em>marquer une partition dégradée pour perte de réplica est bon marché et réversible, donc cela se déclenche immédiatement ; promouvoir un nouveau primaire actif est irréversible, donc cela attend une preuve.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-2--choisir-un-survivant-et-incrémenter-lépoque">Étape 2 — choisir un survivant, et incrémenter l'époque<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#%C3%A9tape-2--choisir-un-survivant-et-incr%C3%A9menter-l%C3%A9poque" class="hash-link" aria-label="Lien direct vers Étape 2 — choisir un survivant, et incrémenter l'époque" title="Lien direct vers Étape 2 — choisir un survivant, et incrémenter l'époque" translate="no">​</a></h2>
<p>Une fois le propriétaire confirmé en panne, le leader choisit un réplica à promouvoir. La sélection est <strong>disponibilité d'abord</strong> : parmi les réplicas affectés de la partition, il préfère celui qui a synchronisé le plus de segments — le nœud le plus susceptible de pouvoir servir bientôt.</p>
<p>La promotion elle-même est une unique diffusion qui fait avancer une <strong>époque</strong> par partition :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> nextEpoch </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">GenerateNextEpoch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">partition</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">PartitionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> runtime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ReplicaPromotion"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> _runtimeOwnership</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">BroadcastAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    partition</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">PartitionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> promotedNodeId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nextEpoch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> promotionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>L'époque est le jeton de protection, et c'est le héros discret de tout le mécanisme. Elle est monotone par partition, et le générateur <em>refuse de revenir en arrière</em> — une époque proposée qui n'est pas strictement supérieure à la plus haute déjà observée est rejetée d'emblée :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> nextEpoch </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> baseEpoch </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">nextEpoch </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> highestObservedEpoch</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">StaleEpochGenerationRejected</span><span class="token punctuation" style="color:#393A34">(</span><span class="token comment" style="color:#999988;font-style:italic">/* ... */</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// never regress ownership</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Ainsi, après la promotion, le primaire actif de la partition est « n2 à l'époque 8 ». Si l'ancien propriétaire n1 était simplement partitionné (pas mort) et revient en pensant qu'il est toujours le primaire actif à l'époque 7, son époque périmée perd toutes les comparaisons. Il ne peut pas récupérer la propriété, et toute écriture qu'il essaie d'accepter en tant que « actif » est écartée. C'est ainsi que Zaris évite le split-brain à deux primaires qui laisserait sinon un ancien propriétaire guéri-mais-confus accepter des écritures que personne ne reverra jamais.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-3--la-preuve-de-possession--servir-ou-se-synchroniser-dabord-">Étape 3 — la preuve de possession : <em>servir</em>, ou <em>se synchroniser d'abord</em> ?<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#%C3%A9tape-3--la-preuve-de-possession--servir-ou-se-synchroniser-dabord-" class="hash-link" aria-label="Lien direct vers étape-3--la-preuve-de-possession--servir-ou-se-synchroniser-dabord-" title="Lien direct vers étape-3--la-preuve-de-possession--servir-ou-se-synchroniser-dabord-" translate="no">​</a></h2>
<p>Voici la partie subtile, et celle qui a demandé un vrai débogage pour être correcte. Ayant choisi le réplica le plus <em>disponible</em>, peut-il réellement commencer à servir les lectures immédiatement ?</p>
<p>Pas nécessairement — car « le plus de segments synchronisés » n'est pas la même chose que « détient chaque clé acquittée ». Un réplica peut être à jour sur le <em>nombre</em> de segments tout en étant encore en retard sur les vraies données des segments qu'il détient. Promouvez un tel nœud directement vers un état servant et il devient un <strong>primaire actif incomplet</strong> : il répond <code>NotFound</code> pour des clés qui étaient durablement acquittées sur le propriétaire mort et qui <em>existent</em> sur un frère plus complet. C'est une perte de données silencieuse déguisée en lecture réussie.</p>
<p>Donc, avant que le nœud promu ne soit autorisé à servir, Zaris applique une <strong>preuve de possession</strong>. Le leader tient une table fraîche, alignée sur le terme, du nombre de clés que chaque nœud déclare détenir pour la partition. La règle est une pure comparaison :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name">PartitionHealthState</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ResolvePromotionState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">long</span><span class="token plain"> candidateHeldKeys</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">long</span><span class="token plain"> bestOtherLiveHolderKeys</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> bestOtherLiveHolderKeys </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> candidateHeldKeys</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">?</span><span class="token plain"> PartitionHealthState</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Recovering   </span><span class="token comment" style="color:#999988;font-style:italic">// a more-complete live peer exists: sync first, do NOT serve</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> PartitionHealthState</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Degraded</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// candidate is the authority: serve now</span><br></span></code></pre></div></div>
<p>Deux issues :</p>
<ul>
<li class=""><strong>Aucun détenteur vivant strictement plus complet n'existe</strong> — le candidat est le nœud le plus complet, ou un véritable unique survivant n'ayant rien avec quoi se synchroniser. Il est promu en <strong>Degraded</strong> : un état <em>servant</em>. Il n'a rien de mieux à attendre, donc le bloquer en non-servant ne ferait que rendre la partition inutilement indisponible. Il sert immédiatement.</li>
<li class=""><strong>Un détenteur vivant strictement plus complet existe</strong> — le candidat est promu en <strong>Recovering</strong> : un état <em>non servant</em>. Il se synchronise d'abord depuis cette meilleure source et ne commence à servir qu'une fois sa complétude prouvée. On dit aux clients que la partition est encore en train de démarrer plutôt que de leur remettre un nœud qui leur mentirait.</li>
</ul>
<p>La distinction entre Degraded et Recovering est exactement la distinction entre « la redondance est réduite mais la vérité est servie » et « ne faites pas encore confiance à ce propriétaire pour les lectures ». Amener correctement un unique survivant en Degraded importe tout autant que maintenir un nœud incomplet en Recovering — une unique copie restante ne doit jamais être bloquée en non-servant pour toujours simplement parce que c'est la seule copie qui reste.</p>
<p>Une note rapide sur l'honnêteté : cette preuve est <strong>fondée sur le décompte</strong>, tirée des rapports de clés détenues par nœud avec un filigrane de fraîcheur (un rapport d'un nœud devenu silencieux n'est pas compté comme source vivante). C'est une garde forte et bon marché contre la promotion d'un nœud manifestement en retard pour servir ; ce n'est pas une attestation cryptographique que deux nœuds détiennent des données identiques octet pour octet. La convergence qui rend les copies <em>réellement</em> identiques est le chemin de synchronisation des réplicas, couvert ci-dessous.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-4--le-client-redirige-et-votre-nouvelle-tentative-atterrit">Étape 4 — le client redirige, et votre nouvelle tentative atterrit<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#%C3%A9tape-4--le-client-redirige-et-votre-nouvelle-tentative-atterrit" class="hash-link" aria-label="Lien direct vers Étape 4 — le client redirige, et votre nouvelle tentative atterrit" title="Lien direct vers Étape 4 — le client redirige, et votre nouvelle tentative atterrit" translate="no">​</a></h2>
<p>Pendant que tout cela se produit sur le serveur, que voit le client qui était en pleine écriture ?</p>
<p>Il voit la vérité, sous forme de statut typé. Une requête qui atteint l'ancien propriétaire (désormais écarté, ou simplement parti) ne reste pas suspendue et ne lève pas d'erreur vague — elle revient en <code>Moved</code> ou <code>OwnershipChanging</code>. Le client Zaris traite ceux-ci comme des signaux de <em>redirection</em> réessayables : il rafraîchit sa carte de propriété, découvre le nouveau primaire actif et rejoue l'opération là-bas. Dans le cas courant, votre <code>PutAsync</code> prend simplement quelques millisecondes de plus et renvoie <code>Ok</code> — face au propriétaire nouvellement promu, à la nouvelle époque. Le code de l'application n'apprend jamais que le propriétaire a bougé.</p>
<p>Cette mécanique côté client — la taxonomie des statuts, la nouvelle tentative avec budget-de-tentatives-et-échéance, le rafraîchissement de la carte — est une histoire en soi, racontée dans <a class="" href="https://clustron.io/fr/blog/retries-under-ownership-churn/">Les tentatives que vous n'écrivez pas</a>. Pour cet article, le point est simplement que l'incrément d'époque du serveur et la redirection du client sont les deux moitiés d'un seul transfert : le serveur rend l'ancien propriétaire non-autoritaire, et le client cesse de lui parler.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-5--converger-puis-guérir-jusquà-healthy">Étape 5 — converger, puis guérir jusqu'à Healthy<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#%C3%A9tape-5--converger-puis-gu%C3%A9rir-jusqu%C3%A0-healthy" class="hash-link" aria-label="Lien direct vers Étape 5 — converger, puis guérir jusqu'à Healthy" title="Lien direct vers Étape 5 — converger, puis guérir jusqu'à Healthy" translate="no">​</a></h2>
<p>La promotion restaure la <em>disponibilité</em> ; elle ne restaure pas la <em>redondance</em>. Juste après un basculement, la partition tourne en Degraded avec une copie de moins que sa cible. Deux choses se produisent alors en arrière-plan.</p>
<p>D'abord, le nœud promu (s'il est monté en Recovering) ainsi que tout réplica en retard ou de retour <strong>rattrapent en rejouant le journal d'opérations par segment</strong> depuis une révision connue — le même substrat rejouable qui pilote la réplication ordinaire. Un nœud qui vient d'hériter du rôle actif et un réplica simplement en retard convergent par le mécanisme identique ; c'est le sujet de <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">Un journal, deux rôles</a>.</p>
<p>Ensuite, une fois que chaque participant affecté est de nouveau vivant et synchronisé, le balayage de récupération du leader fait repasser la partition en <strong>Healthy</strong> — mais uniquement via la même diffusion qui fait avancer l'époque, et uniquement après une courte temporisation afin qu'un nœud instable ne puisse pas entraîner le runtime à travers une tempête de transitions. La machine à états de santé est donc une petite boucle fermée : <code>Healthy → Degraded/Recovering → Healthy</code>, chaque arête un incrément d'époque, chaque promotion protégée.</p>
<p>Si aucun réplica éligible ne survit du tout — chaque nœud affecté à la partition est en panne — le leader fait la seule chose honnête : il marque la partition <strong>Unavailable</strong> (aucun propriétaire, aucune fausse promotion) et démarre un compte à rebours d'éviction qui, si personne ne revient à temps, laisse une régénération de carte planifiée reloger la partition. Une partition Unavailable échoue bruyamment les lectures et écritures plutôt que de servir un mensonge.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="là-où-une-écriture-est-véritablement-perdue--énoncé-clairement">Là où une écriture est véritablement perdue — énoncé clairement<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#l%C3%A0-o%C3%B9-une-%C3%A9criture-est-v%C3%A9ritablement-perdue--%C3%A9nonc%C3%A9-clairement" class="hash-link" aria-label="Lien direct vers Là où une écriture est véritablement perdue — énoncé clairement" title="Lien direct vers Là où une écriture est véritablement perdue — énoncé clairement" translate="no">​</a></h2>
<p>Un mécanisme de basculement gagne la confiance en étant précis sur ses limites, et non en laissant entendre qu'il n'y en a pas.</p>
<ul>
<li class=""><strong>Facteur de réplication 1.</strong> Une partition avec une unique copie et aucun réplica n'a rien à promouvoir. Lorsque ce nœud meurt, la partition passe en Unavailable jusqu'à son retour ; les données ne sont pas perdues (elles sont dans la mémoire de ce nœud) mais elles sont <em>indisponibles</em>, et si le nœud ne revient jamais, elles sont disparues. Le RF1 est non durable <strong>par conception</strong> — c'est un choix délibéré pour des données que vous pouvez vous permettre de perdre, pas une garantie que le chemin de basculement peut sauver.</li>
<li class=""><strong>Acquittement asynchrone.</strong> Zaris permet qu'une écriture soit acquittée sur le propriétaire actif avant qu'elle n'ait atteint un réplica, pour la latence. C'est le chemin rapide — et son coût honnête est une fenêtre : si le primaire actif meurt dans l'intervalle entre l'acquittement au client et l'arrivée de l'opération sur un détenteur survivant, cette écriture acquittée est perdue. La preuve de possession vous protège de la promotion pour servir d'un nœud <em>connu</em> en retard ; elle ne peut pas faire apparaître une opération qui n'a jamais quitté le nœud mort. Si vous avez besoin qu'une écriture survive à la perte de son propriétaire, elle ne doit être acquittée qu'après être durable sur un réplica — le chemin synchrone échange un peu de latence contre exactement cette garantie. Choisissez par charge de travail, les yeux ouverts.</li>
<li class=""><strong>La fenêtre de confirmation elle-même.</strong> Pendant le délai d'escalade après qu'un propriétaire devient silencieux, les écritures de la partition sont dans les limbes — réessayées par le client, pas encore en train d'atterrir sur un nouveau propriétaire. La correction est préservée (rien n'est promu à tort), mais la disponibilité baisse pendant cette fenêtre. Resserrer le délai échange un basculement plus rapide contre davantage de fausses promotions sous des hoquets transitoires ; c'est un cadran, pas un repas gratuit.</li>
</ul>
<p>Aucun de ces cas n'est un bug que le chemin de basculement peut concevoir pour l'éliminer — ce sont les véritables compromis entre disponibilité, durabilité et latence, et le travail de Zaris est de faire du choix <em>sûr</em> le défaut et du choix <em>rapide</em> l'explicite, jamais de prétendre que la fenêtre n'existe pas.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forme-de-la-chose">La forme de la chose<a href="https://clustron.io/fr/blog/anatomy-of-a-failover/#la-forme-de-la-chose" class="hash-link" aria-label="Lien direct vers La forme de la chose" title="Lien direct vers La forme de la chose" translate="no">​</a></h2>
<p>Réduisez le basculement à l'essentiel et ce sont quatre mouvements au-dessus d'un invariant. L'invariant : la carte reste immobile ; seul le primaire actif du runtime bouge. Les mouvements : <strong>confirmer</strong> la mort avant d'agir, <strong>promouvoir</strong> un survivant derrière une époque monotone qui écarte l'ancien propriétaire, <strong>prouver la possession</strong> pour que le nouveau propriétaire serve la vérité ou se synchronise avant de servir tout court, et <strong>rediriger</strong> le client pour que sa nouvelle tentative atterrisse sur le nouveau propriétaire. Puis converger les copies et guérir jusqu'à Healthy.</p>
<p>Que cela tienne n'est pas affaire d'argument — c'est mesuré. Le même chemin de promotion, de possession et de convergence s'exécute en continu sous un chaos délibéré de mise à mort de nœuds pendant des heures d'affilée, avec le registre vérifié pour une seule écriture acquittée perdue à la fin. Cette certification est sa propre histoire : <a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">Aucune écriture acquittée laissée pour compte</a>.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Cache prêt à l'emploi pour ASP.NET Core : IDistributedCache et HybridCache sur Zaris]]></title>
            <link>https://clustron.io/fr/blog/aspnet-core-distributed-cache/</link>
            <guid>https://clustron.io/fr/blog/aspnet-core-distributed-cache/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris livre trois paquets d'intégration ASP.NET Core — un IDistributedCache standard, un HybridCache à deux niveaux avec protection contre les ruées et invalidation par étiquette, et un store de session à expiration glissante — pour qu'une application existante puisse mettre en cache sur un cluster Zaris sans toucher au code qui utilise le cache. Voici ce que fait chacun, et les limites honnêtes de chacun.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Cache distribué prêt à l&amp;#39;emploi pour ASP.NET Core sur Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJEcm9wLWluIGRpc3RyaWJ1dGVkIGNhY2hlIGZvciBBU1AuTkVUIENvcmUgb24gWmFyaXMiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5BU1AuTkVUIENPUkU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+RHJvcC1pbiBjYWNoZSw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+dHdvIHRpZXJzLCBvbmUgQVBJPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+SURpc3RyaWJ1dGVkQ2FjaGUgJiMzODsgSHlicmlkQ2FjaGUgb24gWmFyaXM8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcyNiwyNTApIj4NCiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI3NSIgeT0iMjkiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj55b3VyIGFwcDwvdGV4dD4NCiAgICA8cGF0aCBkPSJNNzUgNDggTDc1IDg2IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI0Ii8+DQogICAgPHJlY3QgeD0iMCIgeT0iODYiIHdpZHRoPSIxNTAiIGhlaWdodD0iNTYiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iNzUiIHk9IjExMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPkwxIMK3IGluLXByb2M8L3RleHQ+DQogICAgPHRleHQgeD0iNzUiIHk9IjEzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPm5vIG5ldHdvcms8L3RleHQ+DQogICAgPHBhdGggZD0iTTE2MCAxMTQgTDI1MiAxMTQiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtZGFzaGFycmF5PSI2IDYiLz4NCiAgICA8dGV4dCB4PSIyMDYiIHk9IjEwNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm1pc3M8L3RleHQ+DQogICAgPHJlY3QgeD0iMjU4IiB5PSI4NiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSI1NiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzM5IiB5PSIxMTAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5MMiDCtyBjbHVzdGVyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjMzOSIgeT0iMTMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+c2hhcmVkLCBkdXJhYmxlPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik0zMzkgMTQyIEwzMzkgMTgwIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWRhc2hhcnJheT0iNiA2Ii8+DQogICAgPHRleHQgeD0iMzM5IiB5PSIyMDAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5mYWN0b3J5IHJ1bnMgb25jZTwvdGV4dD4NCiAgICA8Y2lyY2xlIGN4PSI3NSIgY3k9IjExNCIgcj0iNiIgZmlsbD0iIzJiZDZhMCIvPg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>La plupart des raisons de recourir à un cache distribué dans une application .NET ne mentionnent jamais un cache par son nom. Vous ajoutez du cache de sortie et il réclame un <code>IDistributedCache</code>. Vous activez les sessions et l'intergiciel réclame un store de secours. Vous activez le partage des clés de protection des données entre instances et, là encore, il réclame un endroit où mettre des octets que chaque nœud peut voir. Le code de l'application est écrit contre une <em>abstraction</em> ; la seule question est quelle implémentation vous enregistrez au démarrage.</p>
<p>Zaris répond à cette question avec trois petits paquets NuGet, chacun ciblant une couture ASP.NET Core spécifique : un <code>IDistributedCache</code> nu, un <code>HybridCache</code> à deux niveaux et un store de session. Vous enregistrez une ligne au démarrage et le reste de votre application — y compris les bibliothèques tierces qui ne connaissent que l'abstraction — met en cache sur un cluster Zaris sans un seul changement dans le code qui utilise le cache. Cet article passe en revue les trois, et est délibéré sur l'endroit où chacun s'arrête honnêtement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-trois-paquets">Les trois paquets<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#les-trois-paquets" class="hash-link" aria-label="Lien direct vers Les trois paquets" title="Lien direct vers Les trois paquets" translate="no">​</a></h2>
<table><thead><tr><th>Paquet</th><th>Implémente</th><th>À utiliser pour</th></tr></thead><tbody><tr><td><code>Clustron.Zaris.DistributedCache</code></td><td><code>IDistributedCache</code></td><td>Toute bibliothèque ou code qui réclame l'abstraction de cache distribué standard</td></tr><tr><td><code>Clustron.Zaris.HybridCache</code></td><td><code>HybridCache</code> (de <code>Microsoft.Extensions.Caching.Hybrid</code>)</td><td>Un chemin chaud qui veut un L1 en cours de processus devant le cache partagé, avec protection contre les ruées</td></tr><tr><td><code>Clustron.Zaris.AspNetCore.Session</code></td><td><code>ISessionStore</code></td><td>Sessions ASP.NET Core avec un timeout d'inactivité glissant</td></tr></tbody></table>
<p>Les trois reposent sur le même client Zaris .NET. Ils n'inventent pas de canal parallèle — ils résolvent un client Zaris normal à partir d'une chaîne de connexion et traduisent <code>Get</code>/<code>Set</code>/<code>Remove</code> en <code>GetAsync</code>/<code>PutAsync</code>/<code>DeleteAsync</code> du client. Si vous avez lu <a class="" href="https://clustron.io/fr/blog/connection-strings/">Une chaîne de connexion, n'importe quelle plateforme</a>, le câblage vous paraîtra familier : le cache n'est qu'un autre consommateur d'un store nommé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-le-idistributedcache-standard">1. Le <code>IDistributedCache</code> standard<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#1-le-idistributedcache-standard" class="hash-link" aria-label="Lien direct vers 1-le-idistributedcache-standard" title="Lien direct vers 1-le-idistributedcache-standard" translate="no">​</a></h2>
<p>C'est le prêt-à-l'emploi. <code>IDistributedCache</code> est l'interface que <code>Microsoft.Extensions.Caching.Abstractions</code> expose depuis des années, et une grande partie de l'écosystème .NET — session, cache de sortie, stores de tickets d'authentification, bibliothèques tierces — dépend exactement de cette interface et de rien de plus. Enregistrez Zaris comme implémentation et tout cela est adossé à votre cluster.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZaris</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"app-cache"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"zaris://cache-host:7861/app-cache"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronDistributedCache</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">storeName</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"app-cache"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">configure</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> o </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">KeyPrefix </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"myapp:"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La première ligne enregistre un client Zaris sous la clé DI <code>app-cache</code> ; la seconde enregistre un <code>IDistributedCache</code> qui l'utilise. À partir de là, tout ce qui prend un <code>IDistributedCache</code> — y compris du code que vous n'avez pas écrit — fonctionne :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">sealed</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">WeatherController</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name">IDistributedCache</span><span class="token plain"> cache</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token return-type class-name">Task</span><span class="token return-type class-name punctuation" style="color:#393A34">&lt;</span><span class="token return-type class-name">Forecast</span><span class="token return-type class-name punctuation" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token plain"> city</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> cached </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token interpolation-string string" style="color:#e3116c">$"forecast:</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">city</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cached </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Deserialize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cached</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> fresh </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">FetchForecastAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">city</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">SetAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token interpolation-string string" style="color:#e3116c">$"forecast:</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">city</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token function" style="color:#d73a49">Serialize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">fresh</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">DistributedCacheEntryOptions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                AbsoluteExpirationRelativeToNow </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromMinutes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> fresh</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Sous le capot, <code>GetAsync</code> est un <code>client.GetAsync&lt;byte[]&gt;</code> avec le préfixe configuré appliqué, et un défaut de cache (la clé n'existe pas) renvoie <code>null</code> — exactement le contrat que l'interface promet. <code>SetAsync</code> avec <code>AbsoluteExpirationRelativeToNow</code> fait correspondre cette fenêtre directement à un TTL Zaris, de sorte que le store expire l'entrée pour vous.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="deux-limites-honnêtes">Deux limites honnêtes<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#deux-limites-honn%C3%AAtes" class="hash-link" aria-label="Lien direct vers Deux limites honnêtes" title="Lien direct vers Deux limites honnêtes" translate="no">​</a></h3>
<p><strong>Seule l'expiration absolue relative est prise en charge.</strong> Le <code>IDistributedCache</code> de Zaris lit <code>AbsoluteExpirationRelativeToNow</code> et le transforme en TTL. Si vous ne définissez aucune expiration, l'entrée est écrite sans TTL et vit jusqu'à être évincée ou écrasée. <code>SlidingExpiration</code> sur ce cache polyvalent n'est <strong>pas</strong> honoré — son <code>Refresh</code>/<code>RefreshAsync</code> est une opération nulle. C'est un choix de périmètre délibéré, pas un oubli : l'expiration glissante est une préoccupation de session, et le paquet de session ci-dessous l'implémente correctement plutôt que de la demi-implémenter ici.</p>
<p><strong>Vous pouvez en enregistrer plusieurs, et le premier l'emporte par défaut.</strong> Chaque appel à <code>AddClustronDistributedCache</code> enregistre une instance de cache nommée liée à un store spécifique. Le premier enregistré devient le <code>IDistributedCache</code> à l'échelle de l'application ; passez <code>setAsDefault: true</code> pour faire d'un enregistrement ultérieur le défaut à la place. Si vous voulez plusieurs caches sur différents stores, résolvez-les explicitement via <code>IClustronCacheProvider.GetCache("other-store")</code> plutôt que de vous reposer sur le défaut ambiant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-hybridcache--un-niveau-en-cours-de-processus-devant-le-cluster">2. <code>HybridCache</code> — un niveau en cours de processus devant le cluster<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#2-hybridcache--un-niveau-en-cours-de-processus-devant-le-cluster" class="hash-link" aria-label="Lien direct vers 2-hybridcache--un-niveau-en-cours-de-processus-devant-le-cluster" title="Lien direct vers 2-hybridcache--un-niveau-en-cours-de-processus-devant-le-cluster" translate="no">​</a></h2>
<p>L'abstraction plus récente <code>HybridCache</code> de .NET résout un problème que <code>IDistributedCache</code> vous laisse : la combinaison d'un cache local rapide (L1) et d'un cache distribué partagé (L2), plus la <em>protection contre les ruées</em> — s'assurer que, lorsqu'une clé populaire expire, une horde de requêtes concurrentes n'exécute pas toutes la fabrique coûteuse en même temps.</p>
<p>Zaris implémente directement la classe abstraite <code>HybridCache</code>. La particularité est que <strong>les deux niveaux sont des stores Zaris que vous nommez</strong> — vous pointez le L1 vers un <a class="" href="https://clustron.io/fr/blog/embedded-inproc-mode/">store embarqué</a> en cours de processus (aucun réseau, aucun saut de sérialisation hors machine) et le L2 vers le cluster distribué :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZaris</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"l1"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"zaris://inproc/l1"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">          </span><span class="token comment" style="color:#999988;font-style:italic">// embedded, in-process</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZaris</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"l2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"zaris://cache-host:7861/l2"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// the cluster</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronHybridCache</span><span class="token punctuation" style="color:#393A34">(</span><span class="token named-parameter punctuation" style="color:#393A34">l1Store</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l1"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token named-parameter punctuation" style="color:#393A34">l2Store</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l2"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Le chemin de lecture est la recherche classique à deux niveaux, et il vaut la peine d'être vu en entier car chaque garantie ci-dessous en découle :</p>
<!-- -->
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> product </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetOrCreateAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token interpolation-string string" style="color:#e3116c">$"product:</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">id</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">state</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">factory</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">LoadProductAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">options</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">HybridCacheEntryOptions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Expiration </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromMinutes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">          </span><span class="token comment" style="color:#999988;font-style:italic">// L2 TTL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        LocalCacheExpiration </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromMinutes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// L1 TTL (shorter)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">tags</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"catalog"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Le L1 est vérifié en premier sans verrou. En cas de défaut, l'appelant prend un <strong>verrou par clé</strong>, revérifie le L1 (au cas où un autre appelant viendrait de le remplir), puis le L2 ; ce n'est que si les deux échouent qu'il exécute la fabrique, écrit le L2, puis écrit le L1. Les TTL proviennent de <code>HybridCacheEntryOptions</code> — <code>Expiration</code> pour le L2, <code>LocalCacheExpiration</code> pour le L1 — et l'implémentation borne le L1 pour qu'il ne soit jamais plus long que le L2, de sorte que vous ne pouvez jamais avoir une copie locale qui survit à la copie partagée. Omettez les options et les deux prennent par défaut cinq minutes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="protection-contre-les-ruées-mesurée">Protection contre les ruées, mesurée<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#protection-contre-les-ru%C3%A9es-mesur%C3%A9e" class="hash-link" aria-label="Lien direct vers Protection contre les ruées, mesurée" title="Lien direct vers Protection contre les ruées, mesurée" translate="no">​</a></h3>
<p>Le verrou par clé est la protection contre les ruées. Le propre test du paquet déclenche vingt appels <code>GetOrCreateAsync</code> concurrents sur une seule clé froide, chacun avec une fabrique qui dort 100 ms, et vérifie que la fabrique s'est exécutée <strong>exactement une fois</strong> — dix-neuf appelants attendent sur le verrou, et celui qui a rempli le cache les sert tous. Cela transforme un marteau potentiel à vingt sur votre base de données en un unique chargement.</p>
<p>La limite honnête : ce verrou vit dans un <code>ConcurrentDictionary&lt;string, SemaphoreSlim&gt;</code> <strong>à l'intérieur du processus</strong>. Il effondre la ruée au sein d'une instance d'application. Avec dix instances d'application toutes froides sur la même clé au même instant, vous pouvez encore voir jusqu'à dix exécutions de fabrique, une par processus — pas une à l'échelle du cluster. C'est la forme normale de la protection contre les ruées côté client, et pour le cas courant (une poignée d'instances, une fabrique coûteuse mais pas catastrophique à exécuter quelques fois) c'est exactement ce que vous voulez. Si vous avez besoin d'un chargement strictement unique sur toute la flotte, c'est un problème de verrou distribué, pas une fonctionnalité de cache — et une tâche mieux modélisée avec le motif de revendication par comparaison-et-échange de <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">Coordination sans course</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="invalidation-par-étiquette-et-pourquoi-elle-est-paresseuse">Invalidation par étiquette, et pourquoi elle est paresseuse<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#invalidation-par-%C3%A9tiquette-et-pourquoi-elle-est-paresseuse" class="hash-link" aria-label="Lien direct vers Invalidation par étiquette, et pourquoi elle est paresseuse" title="Lien direct vers Invalidation par étiquette, et pourquoi elle est paresseuse" translate="no">​</a></h3>
<p><code>HybridCache</code> vous laisse étiqueter des entrées et invalider tout un groupe d'un coup :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">RemoveByTagAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"catalog"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// every entry tagged "catalog" is now stale</span><br></span></code></pre></div></div>
<p>Zaris implémente cela avec des horodatages plutôt qu'avec un balayage actif. Chaque entrée est écrite avec deux étiquettes de métadonnées : l'instant où elle a été créée, et ses étiquettes. <code>RemoveByTagAsync("catalog")</code> écrit simplement une clé enregistrant <em>« catalog a été invalidé à l'instant T. »</em> À la prochaine lecture d'une entrée étiquetée, le cache compare : si l'une des étiquettes de l'entrée a été invalidée <em>après</em> la création de l'entrée, l'entrée est traitée comme un défaut et la fabrique se réexécute. Le test le confirme — étiquetez une entrée, invalidez l'étiquette, relisez, et la fabrique est appelée une deuxième fois.</p>
<p>Le compromis de conception à connaître : l'invalidation est <strong>paresseuse et déclenchée par la lecture</strong>, pas une purge immédiate. Appeler <code>RemoveByTagAsync</code> est en O(1) — il écrit un marqueur et retourne — mais les entrées périmées elles-mêmes restent résidentes dans le store jusqu'à ce que quelque chose les lise (et obtienne une valeur fraîche) ou que leur TTL expire. Vous obtenez une invalidation <em>logique</em> bon marché et instantanée ; vous n'obtenez pas une récupération immédiate de la mémoire que ces entrées détiennent. Pour des données de cache, c'est exactement le bon marché. Ne vous appuyez simplement pas dessus comme moyen de forcer la suppression de données pour des raisons de correction — pour cela, <code>RemoveAsync(key)</code> supprime les deux niveaux d'emblée.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-sessions-avec-une-vraie-expiration-glissante">3. Sessions, avec une vraie expiration glissante<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#3-sessions-avec-une-vraie-expiration-glissante" class="hash-link" aria-label="Lien direct vers 3. Sessions, avec une vraie expiration glissante" title="Lien direct vers 3. Sessions, avec une vraie expiration glissante" translate="no">​</a></h2>
<p>Les sessions ASP.NET Core ont besoin d'un store qui fait quelque chose que le cache général ne fait délibérément pas : <strong>faire glisser</strong> l'expiration à chaque requête qui touche la session, de sorte que la session d'un utilisateur actif n'expire jamais sous ses pieds tandis qu'une session inactive finit par le faire. Le paquet de session est un <code>IDistributedCache</code> conçu spécialement qui honore exactement cette sémantique, câblé en un seul appel :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddZarisSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">o </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">StoreName        </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sessions"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ConnectionString </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"zaris://cache-host:7861/sessions"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">IdleTimeout      </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromMinutes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">30</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">CookieName       </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">".MyApp.Session"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ... later in the pipeline, as usual:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">UseSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Donnez-lui une chaîne de connexion et il enregistre le store Zaris de secours pour vous ; omettez-la et il se lie à un store que vous avez déjà enregistré sous <code>StoreName</code>. Dans les deux cas, vous utilisez ensuite <code>HttpContext.Session</code> exactement comme vous l'avez toujours fait.</p>
<p>La pièce qui compte est <code>Refresh</code>. Lorsque l'intergiciel de session charge une session lue mais non modifiée, il appelle <code>RefreshAsync</code> pour la maintenir en vie. Le cache de session Zaris implémente cela comme une lecture-puis-réécriture avec un TTL frais égal au timeout d'inactivité — de sorte que chaque requête qui touche la session fait glisser la fenêtre de 30 minutes vers l'avant. Une écriture (<code>SetAsync</code>) résout son TTL à partir de l'expiration glissante ou absolue de l'entrée, en se rabattant sur le timeout d'inactivité configuré. Le résultat est un comportement de session ASP.NET Core standard, adossé à un cluster Zaris, survivant à un redémarrage d'instance d'application parce que les données de session vivent dans le store et non dans la mémoire de l'instance.</p>
<p>Une note d'implémentation délibérée issue du code lui-même : <code>Refresh</code> est une lecture-plus-réécriture plutôt qu'un appel natif « réinitialise simplement le TTL ». Cela coûte un aller-retour supplémentaire, mais cela garde le glissement correct sur <em>chaque</em> déploiement Zaris — y compris celui embarqué en cours de processus — sans dépendre d'une primitive serveur de réinitialisation de TTL. La correction sur chaque topologie l'a emporté sur l'économie d'un aller-retour sur l'une d'elles.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="choisir-le-bon">Choisir le bon<a href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/#choisir-le-bon" class="hash-link" aria-label="Lien direct vers Choisir le bon" title="Lien direct vers Choisir le bon" translate="no">​</a></h2>
<ul>
<li class="">Recourez à <strong><code>IDistributedCache</code></strong> quand quelque chose le réclame déjà, ou quand vous voulez le cache partagé le plus simple et qu'une bibliothèque va le consommer pour vous. C'est la couture de compatibilité maximale.</li>
<li class="">Recourez à <strong><code>HybridCache</code></strong> sur un chemin de lecture véritablement chaud où un L1 en cours de processus gagne son pain et où une ruée sur une clé populaire froide ferait mal. Vous payez une pièce mobile supplémentaire (deux stores, mémoire locale pour le L1) ; vous obtenez des lectures répétées à latence quasi nulle et une horde effondrée à un chargement par instance.</li>
<li class="">Recourez à <strong><code>AddZarisSession</code></strong> pour les sessions ASP.NET Core spécifiquement — c'est celui avec une vraie expiration glissante, et il reste à l'écart du propre <code>IDistributedCache</code> de votre application.</li>
</ul>
<p>Aucun des trois ne vous demande d'apprendre une API spécifique au cache. Ils rencontrent le framework là où il définit déjà l'abstraction, s'enregistrent en une ligne, et mettent les octets sur un cluster que vous pouvez mettre à l'échelle, sécuriser et exécuter sur Kubernetes indépendamment de l'application qui est devant. La meilleure intégration est celle que le code de votre application n'a jamais à remarquer — et, ce qui est agréable, c'est souvent exactement celle que vous obtenez.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>.NET</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Quelle primitive, quel problème : guide de terrain du modèle de données Zaris]]></title>
            <link>https://clustron.io/fr/blog/choosing-a-zaris-primitive/</link>
            <guid>https://clustron.io/fr/blog/choosing-a-zaris-primitive/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris est un cœur clé-valeur unique, versionné, partitionné et répliqué — et une poignée de vues typées et de couches de coordination par-dessus. Le plus dur n'est pas ce qu'il peut faire ; c'est de choisir le bon outil pour le problème devant vous. Voici un guide de décision couvrant le KV nu, la comparaison-et-échange, les transactions multi-clés, les collections natives, les streams, le watch et les verrous — avec l'adéquation honnête et la limite honnête de chacun.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Choisir la bonne primitive Zaris pour le problème" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJDaG9vc2luZyB0aGUgcmlnaHQgWmFyaXMgcHJpbWl0aXZlIGZvciB0aGUgcHJvYmxlbSI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPkRFQ0lTSU9OIEdVSURFPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPldoaWNoIHByaW1pdGl2ZSw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+d2hpY2ggcHJvYmxlbT88L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIyMjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5PbmUgdmVyc2lvbmVkIEtWIGNvcmUsIG1hbnkgdHlwZWQgdmlld3Mgb24gdG9wPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MjYsMjUwKSI+DQogICAgPHJlY3QgeD0iMTMwIiB5PSI4MCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI0OCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIyMDUiIHk9IjExMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPnZlcnNpb25lZCBLVjwvdGV4dD4NCiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQwIiByeD0iNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI2NCIgeT0iMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5DQVM8L3RleHQ+DQogICAgPHJlY3QgeD0iMTUwIiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQwIiByeD0iNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSIyMTQiIHk9IjI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+dHJhbnNhY3Rpb25zPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjMwMCIgeT0iMCIgd2lkdGg9IjEyOCIgaGVpZ2h0PSI0MCIgcng9IjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzY0IiB5PSIyNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmhhc2ggwrcgbGlzdCDCtyBzZXQ8L3RleHQ+DQogICAgPHJlY3QgeD0iMCIgeT0iMTY4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQwIiByeD0iNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI2NCIgeT0iMTk0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+c3RyZWFtczwvdGV4dD4NCiAgICA8cmVjdCB4PSIxNTAiIHk9IjE2OCIgd2lkdGg9IjEyOCIgaGVpZ2h0PSI0MCIgcng9IjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjE0IiB5PSIxOTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj53YXRjaDwvdGV4dD4NCiAgICA8cmVjdCB4PSIzMDAiIHk9IjE2OCIgd2lkdGg9IjEyOCIgaGVpZ2h0PSI0MCIgcng9IjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzY0IiB5PSIxOTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5jYWNoZTwvdGV4dD4NCiAgICA8cGF0aCBkPSJNNjQgNDAgTDE5MCA4MiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8cGF0aCBkPSJNMjE0IDQwIEwyMTQgODAiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHBhdGggZD0iTTM2NCA0MCBMMjQwIDgyIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDxwYXRoIGQ9Ik02NCAxNjggTDE5MCAxMjYiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHBhdGggZD0iTTIxNCAxNjggTDIxNCAxMjgiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHBhdGggZD0iTTM2NCAxNjggTDI0MCAxMjYiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogIDwvZz4NCiAgPHRleHQgeD0iODAiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDYiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>L'essentiel de ce blog est allé en profondeur sur une chose à la fois — <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">comparaison-et-échange</a>, <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transactions multi-clés</a>, <a class="" href="https://clustron.io/fr/blog/data-structures-that-replicate/">structures de données natives</a>, <a class="" href="https://clustron.io/fr/blog/redis-streams-native/">streams</a>, <a class="" href="https://clustron.io/fr/blog/2026/03/30/watch-changes/">watch</a>, le <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">journal d'opérations</a> en dessous. Chaque article répond à « comment <em>ceci</em> fonctionne ». Celui-ci répond à une question différente, plus pratique : <strong>quand vous avez un problème devant vous, lequel choisissez-vous ?</strong></p>
<p>Cette question compte parce que le mauvais choix est rarement un plantage. C'est un système qui marche dans la démo puis perd une mise à jour sous charge, ou qui canalise un millier d'écrivains à travers une seule clé, ou qui utilise une notification tire-et-oublie là où il lui fallait un journal durable. Les primitives ne vous empêchent pas de les mal utiliser. Alors voici un guide de terrain : le modèle mental qui les relie, une table de décision, une descente de chaque branche avec son adéquation honnête et sa limite honnête, et un organigramme à la fin.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-cœur-plusieurs-vues">Un cœur, plusieurs vues<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#un-c%C5%93ur-plusieurs-vues" class="hash-link" aria-label="Lien direct vers Un cœur, plusieurs vues" title="Lien direct vers Un cœur, plusieurs vues" translate="no">​</a></h2>
<p>Commencez par la chose sur laquelle tout le reste est bâti. Sous chaque fonctionnalité de cet article, Zaris est <strong>un store clé-valeur partitionné et répliqué, et chaque valeur porte une version monotone appartenant au store.</strong> C'est tout. C'est le cœur.</p>
<p>Tout le reste est l'une de deux choses :</p>
<ul>
<li class=""><strong>Une <em>vue</em> typée, côté serveur, sur les clés</strong> — un hash, une liste, un set, un sorted set, un stream. Ce ne sont pas des blobs que vous récupérez et mutez dans votre processus ; ce sont de vraies collections côté serveur qui vivent sur la partition qui possède leur clé, et elles se répliquent exactement comme une valeur nue le fait.</li>
<li class=""><strong>Une <em>couche</em> de coordination au-dessus de la version</strong> — comparaison-et-échange, transactions multi-clés, verrous, watch. Celles-ci utilisent la version (et la mécanique de propriété autour d'elle) pour laisser de nombreux clients s'accorder sur ce qui s'est passé sans se marcher dessus.</li>
</ul>
<p>Gardez cette image et toute la surface cesse de ressembler à un fourre-tout de fonctionnalités et commence à ressembler à ce qu'elle est : un petit cœur, et un ensemble d'outils conçus à dessein qui résolvent chacun une classe spécifique de problème par-dessus. Choisissez par la <em>forme de votre problème</em>, pas par le nom qui sonne le plus proche.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-table-de-décision">La table de décision<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#la-table-de-d%C3%A9cision" class="hash-link" aria-label="Lien direct vers La table de décision" title="Lien direct vers La table de décision" translate="no">​</a></h2>
<table><thead><tr><th>Votre problème</th><th>Choisissez</th><th>Pourquoi</th><th>Attention à</th></tr></thead><tbody><tr><td>Ranger des octets que d'autres nœuds peuvent relire</td><td><strong><code>GetAsync</code>/<code>PutAsync</code> nus</strong>, ou une <a class="" href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/">intégration de cache</a></td><td>Le cœur, rien de plus</td><td>Pas d'expiration à l'écriture — le TTL est un second appel (ci-dessous)</td></tr><tr><td>Une valeur unique doit changer en toute sûreté sous concurrence</td><td><strong><a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">Comparaison-et-échange</a></strong></td><td>Un seul gagnant par tour, aucune mise à jour perdue, aucun verrou tenu</td><td>Une clé unique chaude devient un point de sérialisation — fragmentez-la</td></tr><tr><td>Un invariant s'étend sur deux clés ou plus</td><td><strong><a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">Transaction multi-clés</a></strong></td><td>Tout-ou-rien à travers les clés <em>et</em> les nœuds</td><td>Plus lourde que le CAS ; c'est un 2PC, pas gratuit</td></tr><tr><td>La donnée <em>est</em> une collection — map, liste, appartenance, classement</td><td><strong><a class="" href="https://clustron.io/fr/blog/data-structures-that-replicate/">Structures de données natives</a></strong></td><td>Opérations par élément côté serveur, pas de réécriture de la valeur entière</td><td>Choisissez la collection qui correspond au motif d'accès</td></tr><tr><td>Un journal d'événements ordonné et rejouable avec des consommateurs au moins une fois</td><td><strong><a class="" href="https://clustron.io/fr/blog/redis-streams-native/">Streams</a></strong></td><td>Journal durable, groupes de consommateurs, en attente + acquittement par consommateur</td><td>C'est un journal, pas une file avec priorités</td></tr><tr><td>Réagir aux changements au fur et à mesure</td><td><strong><a class="" href="https://clustron.io/fr/blog/2026/03/30/watch-changes/">Watch</a></strong> (natif) / <a class="" href="https://clustron.io/fr/blog/redis-keyspace-notifications/">notifications d'espace de clés</a> (clients Redis)</td><td>Répliqué, à l'échelle du cluster, survit au basculement</td><td>Le pub/sub tire-et-oublie n'est <em>pas</em> un flux durable</td></tr><tr><td>Exclusion mutuelle grossière autour d'une ressource</td><td><strong><a class="" href="https://clustron.io/fr/blog/distributed-locks-explained/">Verrou</a></strong> (fondé sur un bail)</td><td>Une barrière nommée, bornée par un TTL, qui se libère automatiquement au plantage</td><td>Un verrou à bail n'est pas une barrière de correction — voir ci-dessous</td></tr></tbody></table>
<p>Le reste de cet article est cette table, ralentie.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kv-et-cache--quand-une-valeur-nest-quune-valeur">KV et cache : quand une valeur n'est qu'une valeur<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#kv-et-cache--quand-une-valeur-nest-quune-valeur" class="hash-link" aria-label="Lien direct vers KV et cache : quand une valeur n'est qu'une valeur" title="Lien direct vers KV et cache : quand une valeur n'est qu'une valeur" translate="no">​</a></h2>
<p>Si tout ce dont vous avez besoin est de mettre des octets quelque part où chaque nœud peut les lire — un fragment rendu, un DTO sérialisé, un instantané de config — vous n'avez pas besoin d'une couche de coordination. <code>PutAsync(key, value)</code> et <code>GetAsync&lt;T&gt;(key)</code> sont toute l'histoire, et côté serveur ces octets sont partitionnés et répliqués pour vous.</p>
<p>Deux notes honnêtes. D'abord, <strong>l'expiration n'est pas définie à l'écriture.</strong> Il n'y a pas de champ TTL sur les options d'écriture ; vous définissez une durée de vie avec un appel séparé — <code>ExpireAsync(key, ttl)</code> — et vous l'effacez avec <code>PersistAsync(key)</code>, ou vous lisez ce qu'il reste avec <code>GetTimeToLiveAsync(key)</code>. Ensuite, si votre cas d'usage est littéralement « un cache distribué pour mon application ASP.NET Core », ne le fabriquez pas à la main : enregistrez l'un des <a class="" href="https://clustron.io/fr/blog/aspnet-core-distributed-cache/">paquets de cache prêts à l'emploi</a> et le <code>IDistributedCache</code> du framework (ou <code>HybridCache</code>, ou le store de session) est adossé à votre cluster avec un changement de démarrage d'une ligne et aucun code touchant l'abstraction de cache.</p>
<p><strong>Choisissez le KV nu quand</strong> la valeur n'a pas d'invariant de concurrence — le dernier écrivain qui gagne convient, ou il n'y a qu'un seul écrivain. <strong>Ne le faites pas</strong> dès l'instant où deux écrivains peuvent entrer en course sur la même clé et où vous seriez gêné si l'un écrasait silencieusement l'autre. C'est le travail de l'outil suivant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="comparaison-et-échange--quand-une-valeur-doit-rester-correcte-sous-concurrence">Comparaison-et-échange : quand une valeur doit rester correcte sous concurrence<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#comparaison-et-%C3%A9change--quand-une-valeur-doit-rester-correcte-sous-concurrence" class="hash-link" aria-label="Lien direct vers Comparaison-et-échange : quand une valeur doit rester correcte sous concurrence" title="Lien direct vers Comparaison-et-échange : quand une valeur doit rester correcte sous concurrence" translate="no">​</a></h2>
<p>Un compteur avec un plafond, un solde qui ne peut pas devenir négatif, un objet de config que deux administrateurs modifient en même temps. La défaillance que vous cherchez à éviter est la <em>mise à jour perdue</em> — lecture, lecture, écriture, écriture, et la seconde écriture efface la première. Le CAS transforme cette perte silencieuse en un conflit détecté et réessayable : lisez une valeur <em>et sa version</em>, réécrivez seulement si la version n'a pas bougé, et obtenez un <code>Conflict</code> plutôt qu'un écrasement si quelqu'un vous a devancé.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name">KvResult</span><span class="token class-name punctuation" style="color:#393A34">&lt;</span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token class-name punctuation" style="color:#393A34">&gt;</span><span class="token plain"> read </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">GetAsync</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name keyword" style="color:#00009f">int</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"counter:api"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> put </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">PutAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"counter:api"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> read</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">PutOptions</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> IfMatchVersion </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> read</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// put.Status == KvStatus.Success  -&gt; you won this round</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// put.Status == KvStatus.Conflict -&gt; someone wrote first; re-read and retry</span><br></span></code></pre></div></div>
<p>C'est tout le mécanisme, et le <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">guide de terrain du CAS</a> montre jusqu'où il va — un limiteur de débit, un portefeuille qui ne se met jamais à découvert sous un millier de débits concurrents, une revendication de tâche à un seul gagnant (via « créer seulement si absent »), une config sans mise à jour perdue, et le crochet d'idempotence qui vient gratuitement avec.</p>
<p><strong>Choisissez le CAS quand</strong> l'invariant vit sur <em>une</em> clé et que vous voulez la correction sans tenir de verrou. <strong>Ne</strong> pointez pas une foule d'écrivains vers une unique clé chaude en espérant que ça passe à l'échelle — le CAS laisse délibérément exactement un écrivain gagner par tour, de sorte qu'une clé chaude devient un point de sérialisation. Fragmentez-la (un seau par client-seconde, un solde scindé en sous-soldes réconciliés) et la contention fond.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="transactions-multi-clés--quand-linvariant-sétend-sur-plusieurs-clés">Transactions multi-clés : quand l'invariant s'étend sur plusieurs clés<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#transactions-multi-cl%C3%A9s--quand-linvariant-s%C3%A9tend-sur-plusieurs-cl%C3%A9s" class="hash-link" aria-label="Lien direct vers Transactions multi-clés : quand l'invariant s'étend sur plusieurs clés" title="Lien direct vers Transactions multi-clés : quand l'invariant s'étend sur plusieurs clés" translate="no">​</a></h2>
<p>La ligne dure que trace le CAS est qu'il est atomique sur <em>une</em> clé. Transférer de l'argent du compte A au compte B, ou maintenir un index secondaire au pas avec l'enregistrement qu'il pointe, et aucune comparaison-et-échange unique ne couvre les deux. Ce n'est pas une raison de faire semblant avec deux écritures CAS en espérant — c'est le cas exact pour lequel la <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transaction multi-clés</a> existe.</p>
<p>Vous ouvrez une transaction, y préparez des lectures et des écritures, et validez. Sous le capot, c'est une <strong>validation en deux phases coordonnée par le client sur des versions optimistes</strong> : les clés peuvent vivre sur différents nœuds, donc le client groupe l'ensemble d'écriture par propriétaire, chaque nœud vérifie que chaque clé que vous avez lue est <em>encore</em> à la version à laquelle vous l'avez lue et prend un verrou à courte durée de vie, et ce n'est que si tout le monde vote oui que l'ensemble complet s'applique. La moindre version qui a bougé, et la validation renvoie <code>Conflict</code> sans rien changer.</p>
<p><strong>Choisissez une transaction quand</strong> deux clés ou plus doivent bouger ensemble ou pas du tout. <strong>Ne</strong> l'utilisez pas comme votre chemin d'écriture par défaut — c'est un vrai 2PC avec des allers-retours prepare/commit et des verrous à courte durée de vie, plus lourd qu'un seul CAS. Si l'invariant tient honnêtement sur une clé, gardez-le sur une clé.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="structures-de-données-natives--quand-la-donnée-est-une-collection">Structures de données natives : quand la donnée <em>est</em> une collection<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#structures-de-donn%C3%A9es-natives--quand-la-donn%C3%A9e-est-une-collection" class="hash-link" aria-label="Lien direct vers structures-de-données-natives--quand-la-donnée-est-une-collection" title="Lien direct vers structures-de-données-natives--quand-la-donnée-est-une-collection" translate="no">​</a></h2>
<p>Parfois la valeur n'est pas un scalaire que vous protégez — c'est une forme. Un profil utilisateur est une map de champs. Un flux d'activité récente est une liste. Une appartenance d'étiquette est un set. Un classement est un ranking. Vous <em>pourriez</em> sérialiser chacun comme un seul blob et le CAS, mais alors chaque modification d'un seul champ réécrit l'objet entier, et vous avez transformé une mise à jour en O(1) en une mise à jour en O(n).</p>
<p>Zaris leur donne leurs propres <a class="" href="https://clustron.io/fr/blog/data-structures-that-replicate/">types de collection natifs, côté serveur</a> — hashes, listes, sets, sorted sets — où une opération sur un seul champ ou un seul élément ne touche que cet élément sur le serveur, pas la valeur entière dans votre processus. Ils partitionnent et se répliquent comme n'importe quelle clé, et parce que la <a class="" href="https://clustron.io/fr/blog/redis-protocol-any-language/">façade RESP</a> fait correspondre les mêmes commandes Redis aux mêmes collections côté serveur, un service .NET et un service Python utilisant <code>HSET</code> touchent le hash identique.</p>
<p><strong>Choisissez une collection quand</strong> votre motif d'accès est par élément — définir un champ, pousser une entrée, tester une appartenance, lire le top N par score. <strong>Ne</strong> choisissez pas un sorted set quand ce dont vous avez réellement besoin est un journal d'événements ordonné, acquitté et rejouable. Ça, c'est un stream.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="streams--quand-vous-avez-besoin-dun-journal-ordonné-et-rejouable">Streams : quand vous avez besoin d'un journal ordonné et rejouable<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#streams--quand-vous-avez-besoin-dun-journal-ordonn%C3%A9-et-rejouable" class="hash-link" aria-label="Lien direct vers Streams : quand vous avez besoin d'un journal ordonné et rejouable" title="Lien direct vers Streams : quand vous avez besoin d'un journal ordonné et rejouable" translate="no">​</a></h2>
<p>Une file et un journal se ressemblent jusqu'à ce qu'un consommateur plante. Les <a class="" href="https://clustron.io/fr/blog/redis-streams-native/">streams</a> Zaris sont un vrai journal en ajout-seulement : les entrées obtiennent des identifiants monotones, vous pouvez lire des plages, et un <strong>groupe de consommateurs</strong> divise le journal sur un pool de travailleurs, suit les entrées <em>en attente</em> (livrées-mais-non-acquittées) de chaque travailleur, et laisse un autre travailleur récupérer ce qu'un travailleur planté n'a jamais acquitté. Cette mécanique d'attente-et-acquittement est ce qui vous achète une livraison au moins une fois — la chose qu'une notification tire-et-oublie ne peut pas donner.</p>
<p><strong>Choisissez un stream quand</strong> vous avez besoin d'événements durables, ordonnés et rejouables avec un acquittement explicite et un partage du travail — ingestion de commandes, une boîte d'envoi, une piste d'audit qu'une projection reconstruit. <strong>Ne</strong> l'utilisez pas comme une file à priorités ou un canal requête/réponse ; c'est un journal, et ses forces sont l'ordre et le rejeu, pas le routage.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="watch-notifications-et-la-famille-de-diffusion--quand-vous-réagissez-au-changement">Watch, notifications et la famille de diffusion : quand vous réagissez au changement<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#watch-notifications-et-la-famille-de-diffusion--quand-vous-r%C3%A9agissez-au-changement" class="hash-link" aria-label="Lien direct vers Watch, notifications et la famille de diffusion : quand vous réagissez au changement" title="Lien direct vers Watch, notifications et la famille de diffusion : quand vous réagissez au changement" translate="no">​</a></h2>
<p>Il existe plusieurs façons d'apprendre que quelque chose a changé, et elles ne sont <em>pas</em> interchangeables — la différence est de savoir si le signal est durable.</p>
<p>Celle à choisir depuis .NET est <strong><a class="" href="https://clustron.io/fr/blog/2026/03/30/watch-changes/">Watch</a></strong> : un flux de changements natif, à l'échelle du cluster. Vous vous abonnez à une clé ou un préfixe, y intégrez éventuellement un instantané initial pour qu'il n'y ait pas de trou entre « lire l'état courant » et « s'abonner aux changements », et recevez les événements au fur et à mesure que les écritures atterrissent :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">subscription</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> initial</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Watch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">WatchKeyAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"job:42"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">WatchOptions</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> IncludeInitialSnapshot </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    evt </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Handle</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">evt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La propriété importante est que le watch est <strong>enregistré à l'échelle du cluster et survit au basculement</strong> — il n'est pas lié à l'unique nœud qui a justement traité une écriture. C'est aussi pourquoi les <a class="" href="https://clustron.io/fr/blog/redis-keyspace-notifications/">notifications d'espace de clés Redis</a> que Zaris parle (<code>__keyevent@0__</code> / <code>__keyspace@0__</code>, un simple <code>PSUBSCRIBE</code> depuis n'importe quel client Redis) valent davantage ici que sur une machine Redis unique : ce sont un pont mince vers ce même watch répliqué, de sorte qu'un basculement ne laisse pas tomber silencieusement le flux d'événements.</p>
<p><strong>Choisissez watch/notifications d'espace de clés quand</strong> vous voulez réagir à des changements d'état — invalider un cache, réveiller un travailleur, rafraîchir une projection. <strong>Ne</strong> confondez pas la simple diffusion pub/sub au mieux avec un flux durable : si vous avez besoin de <em>chaque</em> événement même à travers plantages et redémarrages, c'est un stream, pas une notification.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="verrous--exclusion-mutuelle-grossière-avec-un-avertissement-tranchant">Verrous : exclusion mutuelle grossière, avec un avertissement tranchant<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#verrous--exclusion-mutuelle-grossi%C3%A8re-avec-un-avertissement-tranchant" class="hash-link" aria-label="Lien direct vers Verrous : exclusion mutuelle grossière, avec un avertissement tranchant" title="Lien direct vers Verrous : exclusion mutuelle grossière, avec un avertissement tranchant" translate="no">​</a></h2>
<p>Zaris dispose bien d'un <strong>verrou</strong> distribué de première classe — une barrière nommée, fondée sur un bail avec un TTL, acquise depuis le client et libérée à la libération ou à l'expiration par plantage :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">using</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> gate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Locks</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AcquireAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"order:123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromSeconds</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">gate </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// someone else holds it — back off, do other work, or retry later</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// critical section; the lease auto-expires if we crash, and can be renewed</span><br></span></code></pre></div></div>
<p>Il est véritablement utile pour l'exclusion mutuelle grossière — empêcher deux planificateurs d'exécuter le même balayage, sérialiser une tâche de maintenance — et le TTL signifie qu'un détenteur planté ne bloque pas le verrou pour toujours.</p>
<p>Mais voici l'avertissement que la version léchée omet, et c'est la phrase la plus importante de cet article sur les verrous : <strong>un verrou fondé sur un bail n'est pas une barrière de correction.</strong> Si votre détenteur fait une pause — un blocage GC, un appel système lent — assez longtemps pour que le bail expire, un second détenteur peut acquérir le « même » verrou tandis que le premier croit encore le détenir, et voilà deux travailleurs dans la section critique à la fois. Pour la <em>vivacité et l'efficacité</em> (d'ordinaire un seul travailleur exécute la tâche, la contention est faible) un verrou à bail est exactement bon. Pour un <em>invariant de correction qui ne doit jamais être violé</em>, ne comptez pas sur le verrou pour l'imposer — imposez-le sur la donnée avec la <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">comparaison-et-échange</a> ou une <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transaction</a>, de sorte que même un second acteur parasite soit rejeté par une vérification de version à l'écriture.</p>
<p><strong>Choisissez un verrou quand</strong> vous voulez une exclusion mutuelle grossière, au mieux, et qu'un chevauchement occasionnel est simplement gaspilleur, pas incorrect. <strong>Choisissez plutôt le CAS quand</strong> un chevauchement corromprait les données — laissez la version, pas le verrou, être la chose qui dit non.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-partie-transversale--propriété-durabilité-cohérence">La partie transversale : propriété, durabilité, cohérence<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#la-partie-transversale--propri%C3%A9t%C3%A9-durabilit%C3%A9-coh%C3%A9rence" class="hash-link" aria-label="Lien direct vers La partie transversale : propriété, durabilité, cohérence" title="Lien direct vers La partie transversale : propriété, durabilité, cohérence" translate="no">​</a></h2>
<p>Trois propriétés tiennent à travers chaque primitive ci-dessus, parce que ce sont des propriétés du cœur, pas d'un outil en particulier :</p>
<ul>
<li class=""><strong>Le propriétaire bouge, et vous n'avez pas à le remarquer.</strong> Chaque clé est possédée par une partition qui peut basculer, migrer ou être rendue. Le client .NET transforme ce changement en un signal typé et réessayable et <a class="" href="https://clustron.io/fr/blog/retries-under-ownership-churn/">redirige vers le nouveau propriétaire</a>, borné par un budget de tentatives et une échéance en temps réel — de sorte qu'un <code>PutAsync</code> pendant un basculement prend d'ordinaire simplement quelques millisecondes de plus et renvoie un succès.</li>
<li class=""><strong>La durabilité est par réplication, pas par disque.</strong> Zaris garde les données en mémoire et durables en gardant des copies sur d'autres nœuds. C'est pourquoi <a class="" href="https://clustron.io/fr/blog/kubernetes-durability-sizing/">le facteur de réplication n'est pas la même chose que la tolérance aux pannes</a> : le nombre ne vous protège que si la topologie place réellement une seconde copie sur un nœud qui peut survivre à la perte du premier. Toute l'<a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">épine dorsale de propriété et de récupération</a> existe pour s'assurer qu'aucune écriture acquittée n'est perdue pendant que la propriété est en mouvement.</li>
<li class=""><strong>Les écritures et les lectures routent vers le propriétaire courant.</strong> La mécanique de nouvelle tentative et de redirection fait que vos opérations trouvent le nœud qui possède actuellement la clé, de sorte qu'une lecture suit une écriture validée plutôt que de devancer le changement de propriété qui vient de se produire.</li>
</ul>
<p>Aucune de celles-ci n'est une primitive que vous choisissez. Ce sont le socle sur lequel chaque choix repose.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lorganigramme">L'organigramme<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#lorganigramme" class="hash-link" aria-label="Lien direct vers L'organigramme" title="Lien direct vers L'organigramme" translate="no">​</a></h2>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-version-courte">La version courte<a href="https://clustron.io/fr/blog/choosing-a-zaris-primitive/#la-version-courte" class="hash-link" aria-label="Lien direct vers La version courte" title="Lien direct vers La version courte" translate="no">​</a></h2>
<p>La longueur de la liste de fonctionnalités n'a jamais été le propos. Zaris est un cœur clé-valeur versionné, partitionné et répliqué, plus des vues typées côté serveur (hash, liste, set, sorted set, stream) et des couches de coordination au-dessus de la version (CAS, transactions, verrous, watch). Faites correspondre l'outil à la <em>forme</em> du problème : un scalaire sous contention veut le CAS ; un invariant inter-clés veut une transaction ; une donnée qui <em>est</em> une collection veut la collection correspondante ; des événements ordonnés et durables veulent un stream ; réagir au changement veut watch ; l'exclusion mutuelle grossière veut un verrou — et tout ce qui ne doit jamais être violé veut sa garantie imposée sur la <em>donnée</em>, avec une vérification de version, pas sur un verrou qui peut expirer.</p>
<p>Choisissez par la forme, appuyez-vous sur la version pour la correction, et le petit cœur porte une quantité de système remarquable.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Architecture</category>
            <category>Modèles distribués</category>
        </item>
        <item>
            <title><![CDATA[Le plan de contrôle dans votre shell : piloter un cluster Zaris depuis PowerShell]]></title>
            <link>https://clustron.io/fr/blog/control-plane-powershell/</link>
            <guid>https://clustron.io/fr/blog/control-plane-powershell/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Une visite guidée de l'opérateur au jour zéro : créer un espace de travail, provisionner un store avec New-ZrStore, le démarrer, et lire son état en direct avec Get-ZrStore, Get-ZrNode et Watch-ZrStoreMetrics — le plan de contrôle AdminShell, resté honnête sur les choix de réplication et de topologie.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Provisionnement et pilotage d&amp;#39;un cluster Zaris depuis PowerShell" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJQcm92aXNpb25pbmcgYW5kIHJ1bm5pbmcgYSBaYXJpcyBjbHVzdGVyIGZyb20gUG93ZXJTaGVsbCI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fQ0KICAgIC5tb25ve2ZvbnQtZmFtaWx5OiJKZXRCcmFpbnMgTW9ubyIsIlNGTW9uby1SZWd1bGFyIixDb25zb2xhcywiTGliZXJhdGlvbiBNb25vIixNZW5sbyxtb25vc3BhY2U7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPk9QRVJBVElPTlM8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+VGhlIGNvbnRyb2wgcGxhbmU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+aW4geW91ciBzaGVsbDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPlByb3Zpc2lvbiBhbmQgcnVuIGEgWmFyaXMgY2x1c3RlciBmcm9tIFBvd2VyU2hlbGw8L3RleHQ+DQoNCiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzAwLDI1OCkiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIyNjAiIGhlaWdodD0iNDQiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8dGV4dCB4PSIxOCIgeT0iMjgiIGNsYXNzPSJtb25vIiBmb250LXNpemU9IjE2IiBmaWxsPSIjMmJkNmEwIj5OZXctWnJXb3Jrc3BhY2U8L3RleHQ+DQogICAgPHBhdGggZD0iTTEzMCA0NCBMMTMwIDY2IiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQoNCiAgICA8cmVjdCB4PSIwIiB5PSI2NiIgd2lkdGg9IjI2MCIgaGVpZ2h0PSI0NCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjIuNSIvPg0KICAgIDx0ZXh0IHg9IjE4IiB5PSI5NCIgY2xhc3M9Im1vbm8iIGZvbnQtc2l6ZT0iMTYiIGZpbGw9IiMyYmQ2YTAiPk5ldy1aclN0b3JlPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik0xMzAgMTEwIEwxMzAgMTMyIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQoNCiAgICA8cmVjdCB4PSIwIiB5PSIxMzIiIHdpZHRoPSIyNjAiIGhlaWdodD0iNDQiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8dGV4dCB4PSIxOCIgeT0iMTYwIiBjbGFzcz0ibW9ubyIgZm9udC1zaXplPSIxNiIgZmlsbD0iIzJiZDZhMCI+U3RhcnQtWnJTdG9yZTwvdGV4dD4NCiAgICA8cGF0aCBkPSJNMTMwIDE3NiBMMTMwIDE5NiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIuNSIvPg0KDQogICAgPGNpcmNsZSBjeD0iNzAiICBjeT0iMjE0IiByPSI5IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iMTEwIiBjeT0iMjE0IiByPSI5IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iMTUwIiBjeT0iMjE0IiByPSI5IiBmaWxsPSIjNWE2YjY1Ii8+DQogICAgPGNpcmNsZSBjeD0iMTkwIiBjeT0iMjE0IiByPSI5IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iMTMwIiB5PSIyNDYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zdXBlcnZpc29yIGRyaXZlcyB0aGUgY2x1c3RlcjwvdGV4dD4NCiAgPC9nPg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>L'essentiel de ce que vous lisez sur un store de données concerne le chemin de données — lectures, écritures, protocole de communication, sa rapidité. Mais avant que tout cela ne compte, quelqu'un doit <em>monter la chose</em> : décider combien de partitions elle a, combien de copies de chacune, où vivent les ports, la démarrer, confirmer qu'elle est montée en bonne santé et la surveiller pendant qu'elle tourne. C'est le plan de contrôle, et dans Zaris il vit dans PowerShell.</p>
<p>Ceci est une visite guidée de l'opérateur au jour zéro. Nous allons créer un espace de travail, provisionner un store, le démarrer, puis relire son état en direct — le tout depuis le module <strong>AdminShell</strong>. Aucune console web requise, rien édité à la main. Et parce que tout l'intérêt d'une histoire d'opérateur est la confiance, nous serons explicites sur les choix que <code>New-ZrStore</code> met devant vous et sur ce que chacun coûte réellement.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deux-shells-deux-plans">Deux shells, deux plans<a href="https://clustron.io/fr/blog/control-plane-powershell/#deux-shells-deux-plans" class="hash-link" aria-label="Lien direct vers Deux shells, deux plans" title="Lien direct vers Deux shells, deux plans" translate="no">​</a></h2>
<p>Zaris livre sa surface en ligne de commande sous la forme de deux modules PowerShell distincts, et la séparation n'est pas cosmétique — elle reflète les deux plans du système :</p>
<ul>
<li class=""><strong>AdminShell</strong> est le <strong>plan de contrôle</strong>. Ses cmdlets provisionnent, démarrent, arrêtent, inspectent et mettent à l'échelle les stores. Elles parlent au <strong>Management Service</strong> de Zaris (le « manager »), pas directement aux nœuds de données. Ce sont les verbes <code>New-ZrStore</code>, <code>Get-ZrNode</code>, <code>Watch-ZrStoreMetrics</code>.</li>
<li class=""><strong>ClientShell</strong> est le <strong>plan de données</strong>. Ses cmdlets — <code>Connect-ZrStore</code>, <code>Get-ZrItem</code>, <code>Set-ZrItem</code> — ouvrent une connexion directe vers les nœuds du store et déplacent des données clé-valeur. C'est la surface qu'utilise une application, et le sujet de l'article sur les <a class="" href="https://clustron.io/fr/blog/connection-strings/">chaînes de connexion</a>.</li>
</ul>
<p>Un opérateur qui monte un cluster vit presque entièrement dans AdminShell. Le manager auquel il parle est le composant qui connaît la <em>forme désirée</em> de chaque store et pilote les processus réels vers elle. La première chose à faire est donc d'indiquer à votre shell à quels managers parler.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-0--un-espace-de-travail-à-cibler">Étape 0 — un espace de travail à cibler<a href="https://clustron.io/fr/blog/control-plane-powershell/#%C3%A9tape-0--un-espace-de-travail-%C3%A0-cibler" class="hash-link" aria-label="Lien direct vers Étape 0 — un espace de travail à cibler" title="Lien direct vers Étape 0 — un espace de travail à cibler" translate="no">​</a></h2>
<p>Un <strong>espace de travail</strong> est un ensemble nommé de managers que les cmdlets admin ciblent, persisté sous <code>~/.clustron</code> pour qu'il survive aux sessions. Vous en créez un une fois et vous y basculez chaque fois que vous ouvrez un shell :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Create a workspace pointing at the cluster's managers (does not activate it)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">New-ZrWorkspace</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name </span><span class="token string" style="color:#e3116c">"prod"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Managers </span><span class="token string" style="color:#e3116c">"10.0.0.10:7801"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token string" style="color:#e3116c">"10.0.0.11:7801"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Make it the active target for this and future sessions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Use-ZrWorkspace</span><span class="token plain"> prod</span><br></span></code></pre></div></div>
<p>Chaque point de terminaison manager est un <em>observateur d'un cluster</em> — listez-les tous et les cmdlets admin agrègent l'état sur l'ensemble du cluster plutôt que sur ce qu'une machine se trouve connaître. Si votre cluster a la sécurité activée, enregistrez un jeton admin sur l'espace de travail (<code>-Token</code>) ou authentifiez la session avec <code>Connect-ZrManager -Credential (Get-Credential) -Force</code>, qui échange un nom d'utilisateur et un mot de passe contre un jeton de session. (<code>Connect-ZrManager</code> est l'ancienne connexion ad hoc et fonctionne toujours, mais les cmdlets d'espace de travail sont le chemin actuel — ce sont ce à quoi correspondent les espaces de travail enregistrés de la console web.)</p>
<p>Une bifurcation honnête du chemin ici : la visite guidée ci-dessous est le <strong>modèle superviseur</strong>, où chaque manager fork et possède ses processus Zaris locaux — le déploiement natif/Windows. Si vos nœuds tournent plutôt sous un orchestrateur comme Kubernetes (le <strong>modèle d'attachement</strong>), le manager ne crée pas de processus ; vous définissez le store avec votre orchestrateur et vous l'<em>adoptez</em> avec <code>Register-ZrStore</code> au lieu de <code>New-ZrStore</code>. <code>New-ZrStore</code> détecte un manager en mode attachement et vous le signale plutôt que d'échouer au fond de la pile.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-1--provisionner-le-store">Étape 1 — provisionner le store<a href="https://clustron.io/fr/blog/control-plane-powershell/#%C3%A9tape-1--provisionner-le-store" class="hash-link" aria-label="Lien direct vers Étape 1 — provisionner le store" title="Lien direct vers Étape 1 — provisionner le store" translate="no">​</a></h2>
<p>C'est la cmdlet qui porte les vraies décisions. La forme de production minimale est courte :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">New-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name </span><span class="token string" style="color:#e3116c">"orders"</span><span class="token plain"> `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">ReplicationFactor 2 `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">BaseClusterPort 7811 `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">BaseClientPort 7861</span><br></span></code></pre></div></div>
<p>Cela crée un store nommé <code>orders</code> sur chaque machine de l'espace de travail, avec deux copies de chaque partition. Quatre paramètres portent le poids, soyons donc précis sur chacun.</p>
<p><strong><code>-ReplicationFactor</code></strong> est le nombre total de copies de chaque partition — primaire plus réplicas. Pour la production, il doit être au plus égal au nombre de machines, de sorte que chaque copie atterrisse sur une machine <em>différente</em> ; c'est ce qui en fait de la tolérance aux pannes plutôt qu'une décoration. Sur une machine de dev unique, la contrainte est assouplie avec un avertissement — les réplicas partageront une machine, ce qui convient pour les tests et ne vous apporte rien lors d'une vraie panne. La cmdlet le dit à voix haute plutôt que de vous laisser croire autre chose.</p>
<p><strong><code>-PartitionsPerNode</code></strong> vaut <code>1</code> par défaut, ce qui est la topologie de production. Le nombre total de partitions est dérivé de l'appartenance en direct du cluster — <code>PartitionsPerNode × nœuds en direct</code> — et <em>recalculé à chaque changement d'appartenance</em>, ce n'est donc pas un nombre que vous figez à la création. Vous le montez au-dessus de 1 surtout pour les tests locaux, où vous voulez observer le comportement conscient des partitions (routage de segment, rééquilibrage à la montée en charge) sur une machine unique sans monter un rack.</p>
<p><strong><code>-BaseClusterPort</code> / <code>-BaseClientPort</code></strong> sont les premiers ports de deux plages : le port interne de gossip/cluster et le port exposé aux clients, incrémentés une fois par processus sur chaque machine. La cmdlet vérifie que les deux plages ne se chevauchent pas et refuse de créer un store qui entrerait en collision avec lui-même — une petite garde qui évite un échec déroutant au premier démarrage.</p>
<p>Il y en a trois autres qui valent la peine d'être connues parce qu'elles encodent un véritable compromis de durabilité :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">New-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name </span><span class="token string" style="color:#e3116c">"orders"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">ReplicationFactor 3 `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">BaseClusterPort 7811 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">BaseClientPort 7861 `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">ReplicationMode Sync `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">WriteQuorumPolicy Majority `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">AllowReplicaReads</span><br></span></code></pre></div></div>
<ul>
<li class=""><strong><code>-ReplicationMode</code></strong> vaut <code>Async</code> par défaut : le primaire écrit localement et retourne immédiatement, répliquant en arrière-plan. C'est l'option la plus rapide et le bon défaut pour un cache — mais soyez lucide : une écriture acquittée sous <code>Async</code> n'est pas encore garantie d'être sur un réplica. Choisissez <code>Sync</code> et le primaire attend l'acquittement du réplica avant de renvoyer <code>Ok</code>, échangeant de la latence contre de la durabilité.</li>
<li class=""><strong><code>-WriteQuorumPolicy</code></strong> ne mord que sous <code>Sync</code> : <code>All</code> (chaque réplica doit acquitter), <code>Majority</code> (<code>floor(RF/2)+1</code> doivent acquitter), ou <code>BestEffort</code> (les échecs sont journalisés mais ne font pas échouer l'écriture).</li>
<li class=""><strong><code>-AllowReplicaReads</code></strong> laisse les clients lire depuis les nœuds réplicas pour répartir la charge de lecture — avec l'avertissement honnête qu'une lecture de réplica peut renvoyer des données <em>légèrement périmées</em>, car un réplica peut traîner derrière le primaire. Laissez-le désactivé quand vous avez besoin de lire vos propres écritures ; activez-le quand vous préférez le débit et pouvez tolérer le retard.</li>
</ul>
<p>Remarquez ce que les défauts disent de la posture de Zaris : <code>Async</code> + lecture-depuis-le-primaire est un cache rapide et correct par défaut. Les boutons de durabilité sont là quand vous en avez besoin, nommés clairement, chacun avec un coût que vous pouvez voir.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-2--le-démarrer-et-confirmer-quil-est-monté">Étape 2 — le démarrer, et confirmer qu'il est monté<a href="https://clustron.io/fr/blog/control-plane-powershell/#%C3%A9tape-2--le-d%C3%A9marrer-et-confirmer-quil-est-mont%C3%A9" class="hash-link" aria-label="Lien direct vers Étape 2 — le démarrer, et confirmer qu'il est monté" title="Lien direct vers Étape 2 — le démarrer, et confirmer qu'il est monté" translate="no">​</a></h2>
<p>Créer un store définit sa forme ; cela n'exécute pas les processus. <code>Start-ZrStore</code> énumère les instances du store sur chaque manager et les démarre, en rapportant le succès ou l'échec par instance :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Start-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name </span><span class="token string" style="color:#e3116c">"orders"</span><br></span></code></pre></div></div>
<p>Voici maintenant la partie qui distingue un opérateur de quelqu'un qui a lancé une commande et espéré. <code>Get-ZrStore</code> relit l'état en direct du store depuis les managers — statut global, temps de disponibilité, et l'état d'exécution de chaque nœud :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Get-ZrStore</span><span class="token plain"> orders</span><br></span></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Store   : orders</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Status  : Running</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Uptime  : 2h 14m  (since 2026-10-03 09:41:12)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Nodes (4)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">───────────────────────────────────────────</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.10-n0   10.0.0.10:7861   Running   up 2h 14m</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.10-n1   10.0.0.10:7862   Running   up 2h 14m</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.11-n0   10.0.0.11:7861   Running   up 2h 14m</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.11-n1   10.0.0.11:7862   Running   up 2h 14m</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">───────────────────────────────────────────</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  Running: 4   Stopped: 0</span><br></span></code></pre></div></div>
<p>Le statut du store est une synthèse : <code>Running</code> quand chaque nœud est actif, <code>Stopped</code> quand tous sont en panne, et <code>Partial</code> quand certains sont actifs et d'autres non — ce qui est exactement l'état que vous voulez révélé bruyamment, car un store <code>Partial</code> est un store qui a démarré de façon inégale. Le temps de disponibilité est délibérément le <em>moment de formation du cluster</em> rapporté par le cœur — le démarrage de nœud le plus précoce qui a survécu au changement — et non le plus vieux processus actuellement en cours, de sorte qu'un store qui sert depuis des heures ne rapporte pas à tort « quelques minutes » simplement parce qu'un nœud a redémarré.</p>
<p>Pour la couche en dessous, <code>Get-ZrNode</code> montre les rôles et la carte de partitions :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Get-ZrNode</span><span class="token plain"> orders</span><br></span></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Nodes (4)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">─────────────────────────────────────────────────────────────────────────</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.10-n0   10.0.0.10:7861   Running   Primary   v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.10-n1   10.0.0.10:7862   Running   Replica   v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.11-n0   10.0.0.11:7861   Running   Primary   v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.11-n1   10.0.0.11:7862   Running   Replica   v7</span><br></span></code></pre></div></div>
<p>La colonne <code>Primary</code>/<code>Replica</code> indique qui possède actuellement chaque partition, et le <code>v7</code> est la <strong>version de la carte de partitions</strong> — le numéro de génération de la carte de propriété. Lorsque chaque nœud s'accorde sur la même version de carte, la propriété a convergé ; un nœud coincé sur une version plus ancienne est un nœud qui n'a pas rattrapé le dernier rebrassage. Demander le détail d'un nœud unique (<code>Get-ZrNode orders 10.0.0.10-n0</code>) développe cela en les identifiants exacts des partitions primaires et réplicas qu'il détient et le moment où son rôle a changé pour la dernière fois — la vue que vous consultez quand un basculement vient d'avoir lieu et que vous voulez savoir où la propriété a atterri.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-3--le-surveiller-pendant-quil-tourne">Étape 3 — le surveiller pendant qu'il tourne<a href="https://clustron.io/fr/blog/control-plane-powershell/#%C3%A9tape-3--le-surveiller-pendant-quil-tourne" class="hash-link" aria-label="Lien direct vers Étape 3 — le surveiller pendant qu'il tourne" title="Lien direct vers Étape 3 — le surveiller pendant qu'il tourne" translate="no">​</a></h2>
<p>Un instantané vous dit que le store est actif. Une vue en direct vous dit ce qu'il <em>fait</em>. <code>Watch-ZrStoreMetrics</code> interroge les managers et redessine une table par nœud à intervalle régulier jusqu'à ce que vous l'annuliez :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Watch-ZrStoreMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">StoreName orders </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">RefreshSec 2</span><br></span></code></pre></div></div>
<p>Elle dispose d'abord les compteurs du quotidien — taux de requêtes, gets, puts, deletes, nombre d'éléments, utilisation mémoire — et repousse les diagnostics avancés de réplication et de synchronisation vers le bas, de sorte que les nombres que vous vérifiez d'un coup d'œil sont en haut. Les compteurs d'erreurs passent au rouge dès que leur taux dépasse zéro ; le débit se teinte de vert lorsqu'il monte. Elle signale aussi les changements de rôle en ligne : si un basculement fait passer un primaire à réplica pendant que vous regardez, une ligne <code>[ROLE CHANGE]</code> s'affiche avec un horodatage — le signal en direct qu'une partition vient de changer de mains.</p>
<p>Un détail délibéré qui mérite d'être souligné, car c'est le genre de chose qui rend un outil utilisable à 2 h du matin : la vue récupère la trame suivante <em>avant</em> d'effacer l'écran, et une interrogation vide transitoire conserve la dernière bonne trame plutôt que de tout effacer. Vous obtenez une table stable, pas une table clignotante, quelle que soit la lenteur d'un manager à répondre.</p>
<p>Pour compléter l'ensemble en lecture seule, <code>Get-ZrInfo</code> rapporte la version installée, les répertoires d'installation et de données, les deux emplacements de module, le runtime hôte — et sonde chaque manager pour sa joignabilité. Un manager sécurisé qui répond <code>401</code> est rapporté comme <em>joignable mais nécessitant une authentification</em>, pas comme « en panne », de sorte que vous pouvez distinguer d'un coup d'œil un problème réseau d'un jeton manquant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="étape-4--arrêter-et-la-forme-sous-jacente-à-tout">Étape 4 — arrêter, et la forme sous-jacente à tout<a href="https://clustron.io/fr/blog/control-plane-powershell/#%C3%A9tape-4--arr%C3%AAter-et-la-forme-sous-jacente-%C3%A0-tout" class="hash-link" aria-label="Lien direct vers Étape 4 — arrêter, et la forme sous-jacente à tout" title="Lien direct vers Étape 4 — arrêter, et la forme sous-jacente à tout" translate="no">​</a></h2>
<p>Mettre un store à l'arrêt est le miroir du cycle de vie de son démarrage :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Fast whole-store stop (default: a hard kill, since every node goes down together)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Stop-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name orders</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Per-node drain and clean cluster-leave</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Stop-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name orders </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Graceful</span><br></span></code></pre></div></div>
<p>Le défaut est une mise à mort dure et rapide, qui est le bon comportement quand le store <em>entier</em> descend d'un coup — il n'y a pas de cluster survivant à qui un nœud partant puisse passer la main, donc drainer n'apporte rien. Utilisez <code>-Graceful</code> quand vous voulez que chaque nœud draine et quitte le cluster proprement, ce qui importe quand vous arrêtez un sous-ensemble plutôt que l'ensemble.</p>
<p>Sous chaque cmdlet de cet article se cache une seule idée : un store Zaris a une <strong>forme désirée</strong> — une définition canonique des serveurs et partitions qu'il est censé avoir — et le manager tourne sous un <strong>modèle superviseur</strong> qui pilote en continu les processus en direct vers elle. <code>New-ZrStore</code> écrit cette forme ; <code>Start-ZrStore</code> la réalise ; <code>Get-ZrStore</code> et <code>Get-ZrNode</code> rapportent à quel point la réalité en est proche. C'est ce même modèle qui fait du redimensionnement élastique une ligne unique plutôt qu'un runbook — changer la forme désirée avec <code>Add-ZrServer</code> / <code>Remove-ZrServer</code> et laisser le superviseur réconcilier, ce qui est sa propre histoire de <a class="" href="https://clustron.io/fr/blog/elastic-scaling/">mise à l'échelle élastique</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-résumé-honnête">Le résumé honnête<a href="https://clustron.io/fr/blog/control-plane-powershell/#le-r%C3%A9sum%C3%A9-honn%C3%AAte" class="hash-link" aria-label="Lien direct vers Le résumé honnête" title="Lien direct vers Le résumé honnête" translate="no">​</a></h2>
<p>Le cycle de vie complet du jour zéro, ce sont cinq commandes et deux lectures :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">New-ZrWorkspace</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name prod </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Managers </span><span class="token string" style="color:#e3116c">"10.0.0.10:7801"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token string" style="color:#e3116c">"10.0.0.11:7801"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Use-ZrWorkspace</span><span class="token plain"> prod</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">New-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name orders </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">ReplicationFactor 2 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">BaseClusterPort 7811 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">BaseClientPort 7861</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Start-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name orders</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Get-ZrStore</span><span class="token plain">  orders          </span><span class="token comment" style="color:#999988;font-style:italic"># is it Running, and are all nodes up?</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Get-ZrNode</span><span class="token plain">   orders          </span><span class="token comment" style="color:#999988;font-style:italic"># who owns what, and did the map converge?</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Watch-ZrStoreMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">StoreName orders </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">RefreshSec 2   </span><span class="token comment" style="color:#999988;font-style:italic"># what's it doing now?</span><br></span></code></pre></div></div>
<p>Ce qui fait de ceci un plan de contrôle et pas seulement un tas de scripts, c'est que les cmdlets rapportent une <em>vérité en direct, agrégée</em> — non pas ce que vous avez demandé, mais ce que le cluster est réellement — et que les choix aux conséquences réelles sont nommés en toutes lettres : facteur de réplication contre nombre de machines, <code>Async</code> contre <code>Sync</code>, lectures de réplica contre péremption. Montez un store, confirmez qu'il a convergé, et gardez-le à l'œil — le tout sans quitter le shell.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Opérations</category>
            <category>.NET</category>
        </item>
        <item>
            <title><![CDATA[Le même code, avec ou sans réseau : le mode embarqué de Zaris]]></title>
            <link>https://clustron.io/fr/blog/embedded-inproc-mode/</link>
            <guid>https://clustron.io/fr/blog/embedded-inproc-mode/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris s'exécute comme un cluster distribué sur un socket — et, depuis la même surface de client .NET exacte, comme un store embarqué en cours de processus sans aucun réseau. Basculez entre les deux en changeant une chaîne de connexion. Voici comment fonctionne le mode embarqué, à quoi il sert, et la ligne où il s'arrête honnêtement.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Mode embarqué en cours de processus de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJaYXJpcyBlbWJlZGRlZCBpbi1wcm9jZXNzIG1vZGU6IHRoZSBzYW1lIGNvZGUgd2l0aCBvciB3aXRob3V0IGEgbmV0d29yayI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPkVNQkVEREVEIE1PREU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+VGhlIHNhbWUgY29kZSw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+d2l0aCBvciB3aXRob3V0IGEgbmV0d29yazwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPk9uZSBJWmFyaXMgc3VyZmFjZSAmIzE4MzsgaW4tcHJvY2VzcyBmb3IgdGVzdHMsIGEgY2x1c3RlciBpbiBwcm9kdWN0aW9uPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MjAsMjYyKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjIzMCIgaGVpZ2h0PSI1MiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIxMTUiIHk9IjMyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+eW91ciBhcHAgJiMxODM7IElaYXJpcyBjbGllbnQ8L3RleHQ+DQogICAgPHBhdGggZD0iTTcwIDUyIEwzNiAxMTAiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiLz4NCiAgICA8cGF0aCBkPSJNMTYwIDUyIEwxOTYgMTEwIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWRhc2hhcnJheT0iNiA2Ii8+DQogICAgPHJlY3QgeD0iLTcwIiB5PSIxMTQiIHdpZHRoPSIxODYiIGhlaWdodD0iNjQiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iMjMiIHk9IjE0MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPnphcmlzOi8vaW5wcm9jL29yZGVyczwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMyIgeT0iMTY0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzJiZDZhMCI+ZW1iZWRkZWQgJiMxODM7IG5vIHNvY2tldDwvdGV4dD4NCiAgICA8cmVjdCB4PSIxMjQiIHk9IjExNCIgd2lkdGg9IjE4NiIgaGVpZ2h0PSI2NCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjE3IiB5PSIxNDIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj56YXJpc3M6Ly9ob3N0Ojc4NjM8L3RleHQ+DQogICAgPHRleHQgeD0iMjE3IiB5PSIxNjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5jbHVzdGVyICYjMTgzOyBvdmVyIFRMUzwvdGV4dD4NCiAgICA8dGV4dCB4PSIxMjAiIHk9IjIwMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm9ubHkgdGhlIGNvbm5lY3Rpb24gc3RyaW5nIGNoYW5nZXM8L3RleHQ+DQogIDwvZz4NCiAgPHRleHQgeD0iODAiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDYiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>La plupart des stores de données distribués vous obligent à choisir, tôt et définitivement, entre deux mondes. Il y a le « vrai » store — un cluster que vous déployez, vers lequel vous pointez un client sur le réseau, et auquel vous parlez via un protocole de communication. Et il y a le « faux » store auquel vous recourez dans les tests unitaires : un dictionnaire en mémoire, un mock, un bouchon fait main qui implémente juste assez de l'interface pour que le test compile. Les deux ne partagent rien. Votre test exerce le comportement du bouchon, pas celui du store, et l'écart entre eux est exactement là où se cachent les bugs.</p>
<p>Zaris supprime ce choix. Le même client .NET typé qui parle à un cluster distribué sur TLS exécute aussi le store <strong>embarqué à l'intérieur de votre processus</strong>, sans socket, sans carte de partitions et sans déploiement — et c'est le <em>même moteur de store</em>, pas un substitut. Vous sélectionnez lequel vous obtenez avec une seule chaîne de connexion. Cet article traite de la façon dont cela fonctionne, des cas pour lesquels il est véritablement bon, et de la ligne dure où le mode embarqué s'arrête et où un cluster commence.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="une-chaîne-est-tout-linterrupteur">Une chaîne est tout l'interrupteur<a href="https://clustron.io/fr/blog/embedded-inproc-mode/#une-cha%C3%AEne-est-tout-linterrupteur" class="hash-link" aria-label="Lien direct vers Une chaîne est tout l'interrupteur" title="Lien direct vers Une chaîne est tout l'interrupteur" translate="no">​</a></h2>
<p>Un client Zaris est toujours configuré à partir d'une <a class="" href="https://clustron.io/fr/blog/connection-strings/">chaîne de connexion</a>. La chaîne porte le transport, les points de terminaison d'amorçage, le nom du store et les options. Il y a deux formes, et la différence entre elles est la différence entre un cluster et un store embarqué :</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">zaris://inproc/orders                     # embedded — store runs inside this process</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">zariss://zaris.mycorp.com:7863/orders     # remote   — connect to a cluster over TLS</span><br></span></code></pre></div></div>
<p>Le jeton d'hôte <code>inproc</code> est <strong>réservé</strong>. Quand le client le voit, il n'y a aucun point de terminaison à composer, aucun transport à négocier, aucune poignée de main TLS et aucun jeton — le store est servi depuis la mémoire dans le processus courant. Tout le reste de votre code reste identique :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Embedded: no server to start, no port, no network.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name">IZarisClient</span><span class="token plain"> store </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ZarisClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConnectAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"zaris://inproc/orders"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Later, in production — the ONLY line that changed:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name">IZarisClient</span><span class="token plain"> store </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ZarisClient</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConnectAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"zariss://zaris.mycorp.com:7863/orders?ca=/etc/zaris/ca.crt"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Dans ASP.NET Core, c'est la même histoire via l'injection de dépendances — la chaîne de connexion provient d'ordinaire de la configuration, donc le basculement est une valeur de config, pas un changement de code :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Program.cs — one registration, driven entirely by the connection string.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZaris</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Configuration</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetConnectionString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#393A34">!</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Mettez <code>zaris://inproc/orders</code> dans <code>appsettings.Development.json</code> et <code>zariss://.../orders</code> en production, et le même binaire tourne embarqué sur l'ordinateur portable d'un développeur et en cluster dans le cloud, sans rien recompiler.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi--le-même-client--est-le-propos">Pourquoi « le même client » est le propos<a href="https://clustron.io/fr/blog/embedded-inproc-mode/#pourquoi--le-m%C3%AAme-client--est-le-propos" class="hash-link" aria-label="Lien direct vers Pourquoi « le même client » est le propos" title="Lien direct vers Pourquoi « le même client » est le propos" translate="no">​</a></h2>
<p>La raison pour laquelle cela compte réside dans le type que vous récupérez. <code>ConnectAsync</code> renvoie un <code>IZarisClient</code> — ou, pour la surface de fonctionnalités complète, <code>IZaris</code> — quel que soit le mode. Le mode embarqué est servi par une classe appelée <code>ZarisInProcClient</code>, et elle implémente exactement les mêmes interfaces que le client de cluster :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">interface</span><span class="token plain"> </span><span class="token class-name">IZaris</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token type-list class-name">IZarisClient</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">ILeasesClient</span><span class="token plain">     Leases     </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">ILocksClient</span><span class="token plain">      Locks      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">ICountersClient</span><span class="token plain">   Counters   </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">IScanClient</span><span class="token plain">       Scan       </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">IWatchClient</span><span class="token plain">      Watch      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">IPubSubClient</span><span class="token plain">     PubSub     </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">IHashClient</span><span class="token plain">       Hashes     </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">IListClient</span><span class="token plain">       Lists      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">ISetClient</span><span class="token plain">        Sets       </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">ISortedSetClient</span><span class="token plain">  SortedSets </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token return-type class-name">IStreamClient</span><span class="token plain">     Streams    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">get</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Chacune de ces surfaces est présente en mode embarqué, adossée au même moteur de store que le cluster utilise. <code>Get</code>/<code>Put</code>/<code>Delete</code> clé-valeur, en masse et par lot, <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transactions multi-éléments</a>, TTL et expiration, <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">comparaison-et-échange optimiste</a>, <a class="" href="https://clustron.io/fr/blog/2026/03/30/watch-changes/">Watch pour les lectures réactives</a>, <a class="" href="https://clustron.io/fr/blog/redis-streams-native/">Streams natifs</a> avec groupes de consommateurs, les <a class="" href="https://clustron.io/fr/blog/data-structures-that-replicate/">structures de données répliquées</a> — tout cela se résout contre le store embarqué, pas un mock.</p>
<!-- -->
<p>Ce moteur partagé est ce qui fait du store embarqué une cible de test fidèle. Une boucle de comparaison-et-échange qui dépend d'un conflit rapporté avec précision, un abonnement <code>Watch</code> qui doit se déclencher quand une clé change, une transaction qui doit valider atomiquement ou pas du tout — chacun se comporte embarqué comme il se comporte en cluster, parce que c'est littéralement le même chemin de code qui l'évalue. Un <code>KvStatus.Conflict</code> embarqué signifie ce qu'il signifie en production. Un bouchon de dictionnaire fait main ne peut pas le promettre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-quest-réellement-lhôte-embarqué">Ce qu'est réellement l'hôte embarqué<a href="https://clustron.io/fr/blog/embedded-inproc-mode/#ce-quest-r%C3%A9ellement-lh%C3%B4te-embarqu%C3%A9" class="hash-link" aria-label="Lien direct vers Ce qu'est réellement l'hôte embarqué" title="Lien direct vers Ce qu'est réellement l'hôte embarqué" translate="no">​</a></h2>
<p>Il vaut la peine d'être précis sur ce que <code>zaris://inproc</code> démarre, car les limites honnêtes en découlent directement. L'hôte embarqué est une <strong>topologie à nœud unique fixe qui possède chaque segment</strong>. Il n'y a pas de carte de partitions, parce qu'il n'y a qu'un seul participant ; il n'y a pas de réplication, parce qu'il n'y a pas de seconde copie ; il n'y a pas de transport, parce qu'il n'y a pas de second processus à atteindre.</p>
<p>Ce qui <em>tourne</em> bel et bien, c'est la vraie mécanique que vous voudriez dans un test. Le store provisionne son segment d'emblée, câble le service de bail, le service de watch et les métriques locales, et démarre le <strong>balayeur de TTL</strong> à sa cadence normale — de sorte qu'une clé que vous <code>ExpireAsync</code> avec un TTL de 50 ms disparaît réellement, sur une minuterie, exactement comme elle le ferait dans un cluster. L'expiration, la comptabilité d'éviction, le versionnage et les notifications de watch sont tous actifs. C'est un store complet qui se trouve avoir une taille de cluster de un.</p>
<p>Au sein d'un même processus, les clients sont <strong>partagés par nom de store</strong>. Deux appels <code>ConnectAsync("zaris://inproc/orders")</code> dans le même processus se résolvent vers le <em>même</em> store embarqué, de sorte qu'une valeur que l'un écrit, l'autre la lit — pratique quand votre test lance un producteur et un consommateur et attend qu'ils se voient. Des noms de store différents obtiennent des stores différents ; des processus différents obtiennent des stores embarqués entièrement séparés (plus sur cette limite ci-dessous).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="là-où-le-mode-embarqué-gagne-son-pain">Là où le mode embarqué gagne son pain<a href="https://clustron.io/fr/blog/embedded-inproc-mode/#l%C3%A0-o%C3%B9-le-mode-embarqu%C3%A9-gagne-son-pain" class="hash-link" aria-label="Lien direct vers Là où le mode embarqué gagne son pain" title="Lien direct vers Là où le mode embarqué gagne son pain" translate="no">​</a></h2>
<p><strong>Tests unitaires et d'intégration qui exercent le vrai store.</strong> C'est l'usage phare. Votre dépôt, votre couche de cache, votre coordinateur de saga — testez-les contre <code>zaris://inproc/test-{Guid}</code>, obtenez un store frais et isolé par test, et exercez la sémantique véritable : vrai versionnage pour vos boucles CAS, vraie expiration TTL sur une vraie minuterie, vraie atomicité de transaction, vrais événements de watch. Aucun conteneur Docker à démarrer en CI, aucun port à allouer, aucune instabilité de démontage. Le store est créé à la première connexion et meurt avec le processus.</p>
<p><strong>Applications à processus unique qui pourraient grandir plus tard.</strong> Une application de bureau, un travailleur d'arrière-plan, un outil en ligne de commande, ou un petit service qui a besoin aujourd'hui d'un store structuré rapide mais pourrait devenir un déploiement en cluster demain. Commencez embarqué. Le jour où vous avez besoin de partager ces données entre instances, vous changez la chaîne de connexion pour pointer vers un cluster — et parce que vous avez codé contre <code>IZaris</code> tout du long, rien d'autre ne bouge.</p>
<p><strong>Intégrations livrées embarquées par défaut.</strong> Le fournisseur de session ASP.NET Core de Zaris en est un exemple concret : câbler Zaris comme votre store de session est un seul appel, et dans son échantillon il tourne entièrement embarqué —</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddZarisSession</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">options </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    options</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ConnectionString </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"zaris://inproc/sessions"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>— ce qui signifie qu'un développeur peut <code>F5</code> l'application et avoir des sessions fonctionnelles avec zéro infrastructure, puis passer à <code>zariss://.../sessions</code> pour un déploiement multi-instance sans toucher au code de session.</p>
<p>Pour utiliser le mode embarqué, référencez le paquet <code>Clustron.Zaris.InProc</code> aux côtés du client (il cible .NET 8). Le paquet s'enregistre lui-même : le client découvre et charge la fabrique en cours de processus à la première utilisation, de sorte que le simple fait de le référencer suffit. Dans un assembly de test où vous préférez être explicite, <code>new InProcBootstrap().Register();</code> le câble de façon déterministe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="là-où-il-sarrête-honnêtement">Là où il s'arrête honnêtement<a href="https://clustron.io/fr/blog/embedded-inproc-mode/#l%C3%A0-o%C3%B9-il-sarr%C3%AAte-honn%C3%AAtement" class="hash-link" aria-label="Lien direct vers Là où il s'arrête honnêtement" title="Lien direct vers Là où il s'arrête honnêtement" translate="no">​</a></h2>
<p>Le mode embarqué est un vrai store, mais c'est un store <em>à nœud unique, en mémoire</em>, et prétendre autre chose serait le baratin marketing que ce blog cherche à éviter. Trois limites dures, droit sorties de la conception :</p>
<p><strong>Aucune durabilité.</strong> Le store embarqué garde les données dans la mémoire du processus et nulle part ailleurs. Il n'y a ni réplication ni journal sur disque dans ce mode — quand le processus se termine, les données sont parties. C'est exactement ce que vous voulez pour un test (table rase à chaque exécution) et exactement ce sur quoi vous ne devez pas compter pour quoi que ce soit qui doive survivre à un redémarrage. Le mode embarqué n'est pas une base de données avec laquelle vous adossez vos données de production.</p>
<p><strong>Aucune haute disponibilité, aucune montée en charge.</strong> Un cluster de un tolère la perte de zéro nœud et se met à l'échelle à la mémoire et au CPU d'un seul processus. Toutes les raisons pour lesquelles vous recourriez au Zaris distribué — survivre à une panne de nœud sans perte de données, <a class="" href="https://clustron.io/fr/blog/kubernetes-durability-sizing/">dimensionner les réplicas pour la durabilité</a>, <a class="" href="https://clustron.io/fr/blog/elastic-scaling/">mettre à l'échelle le débit sur les cœurs et les machines</a> — ne s'appliquent tout simplement pas à un hôte embarqué, parce qu'il n'y en a qu'un. Quand vous avez besoin de ces propriétés, vous avez besoin du cluster.</p>
<p><strong>Aucun partage inter-processus.</strong> Le registre en cours de processus partage un store par nom <em>au sein d'un processus</em>. Deux processus différents qui ouvrent tous deux <code>zaris://inproc/orders</code> obtiennent deux stores indépendants qui ne savent rien l'un de l'autre — il n'y a pas de socket entre eux, par définition. Dès l'instant où vos données doivent être visibles pour un second processus ou une seconde machine, le mode embarqué est le mauvais outil et un cluster est le bon.</p>
<p>Aucune de ces limites n'est un bug à corriger ; elles sont la définition d'« embarqué ». La valeur n'est pas que le store en cours de processus soit secrètement distribué — c'est que le jour où vous avez besoin de la distribution, la <em>seule</em> chose que vous changez est la chaîne qui dit où le store vit.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-même-promesse-tenue-deux-fois">La même promesse, tenue deux fois<a href="https://clustron.io/fr/blog/embedded-inproc-mode/#la-m%C3%AAme-promesse-tenue-deux-fois" class="hash-link" aria-label="Lien direct vers La même promesse, tenue deux fois" title="Lien direct vers La même promesse, tenue deux fois" translate="no">​</a></h2>
<p>L'argumentaire est petit et concret. Vous écrivez votre application contre une seule surface de client. Dans un test, ou sur un portable, ou à l'intérieur d'un outil à processus unique, cette surface est servie par un vrai store Zaris tournant dans votre propre mémoire — rapide à créer, fidèle dans sa sémantique, et disparu quand le processus se termine. En production, le code identique parle à un cluster distribué sur TLS. Le comportement que vos tests ont figé est le comportement que vous livrez, parce que ce n'a jamais été une implémentation différente — seulement un endroit différent où le store vit.</p>
<p>C'est toute l'idée derrière <code>zaris://inproc</code> : l'écart entre « le store dans mon test » et « le store en production » a la largeur d'une chaîne de connexion, et c'est à vous de décider, par environnement, de quel côté vous vous trouvez.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>.NET</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Lire vos propres écritures : le modèle de cohérence de Zaris, énoncé honnêtement]]></title>
            <link>https://clustron.io/fr/blog/reading-your-own-writes/</link>
            <guid>https://clustron.io/fr/blog/reading-your-own-writes/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Que garantit réellement une lecture dans Zaris ? Les lectures routent vers l'unique propriétaire actif d'une partition, donc vous lisez vos propres écritures au sein de cette partition — mais il n'y a pas de linéarisabilité globale, les lectures inter-partitions n'ont pas d'ordre partagé, et « acquitté » signifie validé sur le propriétaire, pas encore sur un réplica. Voici tout le modèle, avec les cadrans et les réserves énoncés clairement.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Lire vos propres écritures — un propriétaire par partition" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJSZWFkaW5nIHlvdXIgb3duIHdyaXRlczogdGhlIFphcmlzIGNvbnNpc3RlbmN5IG1vZGVsIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+Q09OU0lTVEVOQ1k8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+UmVhZGluZyB5b3VyPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPm93biB3cml0ZXM8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIyMjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5PbmUgb3duZXIgcGVyIHBhcnRpdGlvbiAmIzgyMTI7IHRoZSBjb25zaXN0ZW5jeSBtb2RlbCwgc3RhdGVkIGhvbmVzdGx5PC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MjYsMjUwKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjE0MCIgaGVpZ2h0PSI1NCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iNzAiIHk9IjI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+Y2xpZW50PC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjcwIiB5PSI0NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPnB1dCwgdGhlbiBnZXQ8L3RleHQ+DQogICAgPHBhdGggZD0iTTE0NiAyMCBMMjUwIDIwIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI2Ii8+DQogICAgPHRleHQgeD0iMTk4IiB5PSIxMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPndyaXRlPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik0yNTAgNDIgTDE0NiA0MiIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iNiIvPg0KICAgIDx0ZXh0IHg9IjE5OCIgeT0iNTgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5yZWFkIHY9NDI8L3RleHQ+DQogICAgPHJlY3QgeD0iMjU2IiB5PSItOCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSI3NiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIzNDEiIHk9IjIyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+YWN0aXZlIG93bmVyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjM0MSIgeT0iNDgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5rZXkgQCB2PTQyPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjI4NiIgeT0iMTA0IiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNiIvPg0KICAgIDx0ZXh0IHg9IjM2MSIgeT0iMTI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+cmVwbGljYSBAIHY9NDA8L3RleHQ+DQogICAgPHRleHQgeD0iMzYxIiB5PSIxNDMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5hc3luYyB0YWlsPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik0zNDEgNjggTDM2MSAxMDQiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtZGFzaGFycmF5PSI1IDUiLz4NCiAgPC9nPg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>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 : <em>si j'écris une valeur puis la relis immédiatement, suis-je garanti de voir ce que j'ai écrit ?</em> 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.</p>
<p>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 — <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">comparaison-et-échange optimiste</a>, <a class="" href="https://clustron.io/fr/blog/retries-under-ownership-churn/">nouvelle tentative transparente lors du changement de propriété</a>, le <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">journal d'opérations rejouable</a> qui alimente les réplicas, <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transactions multi-clés</a>. Ici nous les relions du côté du lecteur et répondons à la seule question qui compte derrière un clavier : <strong>quand je lis, qu'est-ce qui m'est réellement promis ?</strong> Y compris, surtout, là où la promesse s'arrête.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-modèle-en-une-phrase">Le modèle en une phrase<a href="https://clustron.io/fr/blog/reading-your-own-writes/#le-mod%C3%A8le-en-une-phrase" class="hash-link" aria-label="Lien direct vers Le modèle en une phrase" title="Lien direct vers Le modèle en une phrase" translate="no">​</a></h2>
<p>Zaris est un <strong>store à tendance AP avec la frontière de cohérence tracée autour de la propriété, et non autour de chaque écriture.</strong> 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 <em>propriété</em> : <strong>il y a exactement un propriétaire actif par partition à tout instant.</strong> Les écritures vers une partition sont sérialisées à travers ce nœud unique. Il n'y a délibérément <strong>aucun quorum ni consensus sur le chemin d'écriture</strong>, et Zaris ne revendique <strong>pas</strong> de linéarisabilité stricte globale.</p>
<p>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 :</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">key → segment → partition → one runtime active owner</span><br></span></code></pre></div></div>
<p>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 <em>et sert ses lectures</em>. La propriété est un <strong>bail</strong> porté sur la carte de partitions sous la forme <code>(LeaderTerm, OwnershipGeneration)</code> — 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-les-lectures-routées-vers-le-propriétaire-vous-donnent-la-lecture-de-vos-propres-écritures">Pourquoi les lectures routées vers le propriétaire vous donnent la lecture de vos propres écritures<a href="https://clustron.io/fr/blog/reading-your-own-writes/#pourquoi-les-lectures-rout%C3%A9es-vers-le-propri%C3%A9taire-vous-donnent-la-lecture-de-vos-propres-%C3%A9critures" class="hash-link" aria-label="Lien direct vers Pourquoi les lectures routées vers le propriétaire vous donnent la lecture de vos propres écritures" title="Lien direct vers Pourquoi les lectures routées vers le propriétaire vous donnent la lecture de vos propres écritures" translate="no">​</a></h2>
<p>Voici la partie qui répond à la question d'ouverture. Dans Zaris, <strong>les lectures routent vers le propriétaire actif, le même nœud où est allée votre écriture.</strong> Les clients ne lisent pas depuis les réplicas par défaut. Ainsi, quand vous <code>PutAsync</code> une clé puis la <code>GetAsync</code>, 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.</p>
<!-- -->
<p>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 <strong>au sein d'une partition</strong> 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 <em>vous ne lisez jamais le réplica</em>, donc ce retard vous est invisible.</p>
<p>Le qualificatif « au sein d'une partition » porte le poids, et nous reviendrons sur ce qu'il exclut.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-version-que-vous-êtes-autorisé-à-voir">La version que vous êtes autorisé à voir<a href="https://clustron.io/fr/blog/reading-your-own-writes/#la-version-que-vous-%C3%AAtes-autoris%C3%A9-%C3%A0-voir" class="hash-link" aria-label="Lien direct vers La version que vous êtes autorisé à voir" title="Lien direct vers La version que vous êtes autorisé à voir" translate="no">​</a></h2>
<p>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 :</p>
<ul>
<li class="">une <strong><code>Revision</code></strong> appartenant au store — un numéro de séquence strictement croissant que le propriétaire estampille sur chaque mutation dans l'ordre de validation, et</li>
<li class="">une <strong><code>ItemVersion</code></strong> par élément — la version de cette clé spécifique, qui est ce que <code>IfMatchVersion</code> protège dans une <a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">écriture par comparaison-et-échange</a>.</li>
</ul>
<p>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 <strong>vers l'avant, jamais vers l'arrière.</strong> 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'<a class="" href="https://clustron.io/fr/blog/race-free-coordination-cas/">article sur le CAS</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-fenêtre-où-une-lecture-dit-honnêtement--pas-encore-">La fenêtre où une lecture dit honnêtement « pas encore »<a href="https://clustron.io/fr/blog/reading-your-own-writes/#la-fen%C3%AAtre-o%C3%B9-une-lecture-dit-honn%C3%AAtement--pas-encore-" class="hash-link" aria-label="Lien direct vers La fenêtre où une lecture dit honnêtement « pas encore »" title="Lien direct vers La fenêtre où une lecture dit honnêtement « pas encore »" translate="no">​</a></h2>
<p>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 : <strong>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.</strong> Une lecture (ou écriture) pendant une telle fenêtre peut revenir sous la forme :</p>
<ul>
<li class=""><strong><code>Moved</code></strong> — votre table de routage est périmée ; la clé vit sur un nœud différent maintenant.</li>
<li class=""><strong><code>OwnershipChanging</code></strong> — la propriété de ce segment est activement en transition en ce moment ; c'est un transfert, pas un bug de routage établi.</li>
<li class=""><strong><code>Migrating</code></strong> — le segment est en cours de migration vers un nouveau propriétaire.</li>
<li class=""><strong><code>ReplicaSyncPending</code></strong> — ce nœud met à jour la copie d'un segment et ne servira pas encore une lecture possiblement incomplète depuis celui-ci.</li>
<li class=""><strong><code>Unavailable</code></strong> — la partition n'a aucun propriétaire joignable à cet instant.</li>
</ul>
<p>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 <a class="" href="https://clustron.io/fr/blog/retries-under-ownership-churn/">réessaie avec une redirection</a>, 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 <code>GetAsync</code> 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 : <strong>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.</strong> Un « pas encore » réessayable est une meilleure réponse qu'une réponse confiante et fausse.</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-cadran-que-vous-pouvez-tourner--et-ce-quil-fait-réellement-aujourdhui">Le cadran que vous pouvez tourner — et ce qu'il fait réellement aujourd'hui<a href="https://clustron.io/fr/blog/reading-your-own-writes/#le-cadran-que-vous-pouvez-tourner--et-ce-quil-fait-r%C3%A9ellement-aujourdhui" class="hash-link" aria-label="Lien direct vers Le cadran que vous pouvez tourner — et ce qu'il fait réellement aujourd'hui" title="Lien direct vers Le cadran que vous pouvez tourner — et ce qu'il fait réellement aujourd'hui" translate="no">​</a></h2>
<p>Zaris expose une surface de cohérence de lecture. <code>GetOptions</code> porte une <code>ReadConsistency</code> de <code>Leader</code>, <code>Quorum</code> ou <code>Any</code>, et un store a un drapeau <code>AllowReplicaReads</code> (désactivé <strong>par défaut</strong>). 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.</p>
<p>Voici l'état actuel honnête, car c'est exactement le genre de chose qu'un texte marketing flouterait : <strong>aujourd'hui, les lectures sont réservées au propriétaire.</strong> <code>AllowReplicaReads</code> et <code>ReadConsistency.Any</code> 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é <em>forte</em> 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 <strong>pas</strong> actuellement le comportement de lecture-de-réplica-périmée, même si vous basculez le drapeau.</p>
<p>Pourquoi mentionner un cadran effectivement épinglé sur un seul réglage ? Parce qu'il vous dit ce que le contrat de cohérence <em>va</em> 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 <code>AllowReplicaReads</code> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-que--acquitté--signifie-pour-le-prochain-lecteur">Ce que « acquitté » signifie pour le <em>prochain</em> lecteur<a href="https://clustron.io/fr/blog/reading-your-own-writes/#ce-que--acquitt%C3%A9--signifie-pour-le-prochain-lecteur" class="hash-link" aria-label="Lien direct vers ce-que--acquitté--signifie-pour-le-prochain-lecteur" title="Lien direct vers ce-que--acquitté--signifie-pour-le-prochain-lecteur" translate="no">​</a></h2>
<p>La lecture de vos propres écritures concerne <em>vous</em> qui lisez <em>votre propre</em> é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 ?</p>
<p>Le contrat d'acquittement honnête d'aujourd'hui : <strong>une écriture est acquittée quand elle atterrit dans le store local du propriétaire actif.</strong> 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 :</p>
<ul>
<li class="">À <strong>redondance 1</strong> — 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.</li>
<li class="">Même avec des réplicas, si l'unique propriétaire actif est perdu dans la fenêtre étroite <em>après</em> qu'il a acquitté une écriture mais <em>avant</em> que cette écriture n'atteigne un réplica, l'écriture acquittée peut être perdue.</li>
</ul>
<p>La propriété plus forte — <em>acquitter seulement une fois que l'écriture est sur au moins un réplica synchronisé</em>, de sorte qu'une écriture acquittée survive à la perte de n'importe quel nœud — est la <strong>cible conçue (la « barrière d'acquittement W1 »), pas encore la sémantique d'acquittement livrée.</strong> C'est la direction vers laquelle <a class="" href="https://clustron.io/fr/blog/zero-data-loss-under-chaos/">le travail sur l'épine dorsale de durabilité</a> se consolide, et cet article est prudent sur la même distinction : l'affirmation forte qu'il valide est limitée aux <em>écritures acquittées, lorsqu'un réplica synchronisé existe, sous les classes de pannes que l'exécution de chaos provoque</em> — et non un « rien ne peut jamais être perdu » inconditionnel. La <a class="" href="https://clustron.io/fr/blog/op-log-replayable-substrate/">mécanique du journal d'opérations et de la migration</a> 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 <em>moment</em> où le propriétaire est autorisé à dire « terminé ».</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="à-travers-les-partitions-il-ny-a-pas-dhorloge-partagée">À travers les partitions, il n'y a pas d'horloge partagée<a href="https://clustron.io/fr/blog/reading-your-own-writes/#%C3%A0-travers-les-partitions-il-ny-a-pas-dhorloge-partag%C3%A9e" class="hash-link" aria-label="Lien direct vers À travers les partitions, il n'y a pas d'horloge partagée" title="Lien direct vers À travers les partitions, il n'y a pas d'horloge partagée" translate="no">​</a></h2>
<p>Le dernier endroit où le qualificatif « au sein d'une partition » mord : <strong>il n'y a pas d'ordre global ni d'instantané à travers les partitions.</strong> 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é <code>A</code> (partition 1) puis la clé <code>B</code> (partition 2), un lecteur concurrent peut observer la nouvelle valeur de <code>B</code> et l'ancienne de <code>A</code>, 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.</p>
<p>Quand vous avez véritablement besoin qu'un invariant s'étende sur plusieurs clés — déplacer un solde de <code>A</code> à <code>B</code>, maintenir un index au pas avec l'enregistrement qu'il pointe — c'est exactement à cela que servent les <a class="" href="https://clustron.io/fr/blog/multi-key-transactions/">transactions multi-clés</a>. Elles fournissent une isolation <code>Serializable</code> sur une validation coordonnée par le client à travers des versions optimistes, et elles sont la <em>seule</em> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-qui-est-et-nest-pas-garanti">Ce qui est, et n'est pas, garanti<a href="https://clustron.io/fr/blog/reading-your-own-writes/#ce-qui-est-et-nest-pas-garanti" class="hash-link" aria-label="Lien direct vers Ce qui est, et n'est pas, garanti" title="Lien direct vers Ce qui est, et n'est pas, garanti" translate="no">​</a></h2>
<p>Énoncé clairement, pour que vous puissiez tenir tout le modèle d'un coup :</p>
<table><thead><tr><th>Propriété</th><th>Zaris aujourd'hui</th></tr></thead><tbody><tr><td>Propriétaire par partition</td><td>Exactement un propriétaire actif, tenu comme un bail durable.</td></tr><tr><td>Chemin de lecture</td><td>Route vers le propriétaire actif ; réplicas non lus par défaut.</td></tr><tr><td>Lecture de vos propres écritures</td><td><strong>Oui, au sein d'une partition</strong> — le même propriétaire sert votre écriture et votre lecture.</td></tr><tr><td>Lectures monotones d'une clé</td><td>Oui — le propriétaire sert des révisions ordonnées par validation ; les versions avancent.</td></tr><tr><td>Lectures pendant un transfert</td><td>Statut réessayable (<code>Moved</code> / <code>OwnershipChanging</code> / <code>Migrating</code> / <code>ReplicaSyncPending</code>), le client réessaie et redirige.</td></tr><tr><td>Lectures de réplica / cadran de péremption</td><td><code>AllowReplicaReads</code> + <code>ReadConsistency.Any</code> existent comme surface ; <strong>réservé au propriétaire en pratique aujourd'hui</strong> (pas encore de chemin servant par réplica).</td></tr><tr><td>Durabilité de l'acquittement</td><td>Locale au propriétaire aujourd'hui ; durable-face-à-une-panne-unique-avec-réplica-synchronisé est la cible (barrière d'acquittement W1).</td></tr><tr><td>Ordre inter-partitions</td><td><strong>Aucun</strong> — pas d'instantané global ; utilisez les transactions pour l'atomicité inter-clés.</td></tr><tr><td>Linéarisabilité globale</td><td><strong>Non revendiquée.</strong></td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forme-honnête-de-la-chose">La forme honnête de la chose<a href="https://clustron.io/fr/blog/reading-your-own-writes/#la-forme-honn%C3%AAte-de-la-chose" class="hash-link" aria-label="Lien direct vers La forme honnête de la chose" title="Lien direct vers La forme honnête de la chose" translate="no">​</a></h2>
<p>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 <em>au sein d'une partition</em> 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.</p>
<p>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 <em>sur le propriétaire</em>, 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.</p>
<p>Pour la conception plus profonde, voyez <a class="" href="https://clustron.io/fr/docs/clustron/zaris/core-concepts/consistency/">Cohérence</a> et <a class="" href="https://clustron.io/fr/docs/clustron/zaris/architecture/partitioning-replication/">Partitionnement et réplication</a> dans la documentation.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Systèmes distribués</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Des notifications d'espace de clés qui survivent au basculement]]></title>
            <link>https://clustron.io/fr/blog/redis-keyspace-notifications/</link>
            <guid>https://clustron.io/fr/blog/redis-keyspace-notifications/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris parle les notifications d'espace de clés Redis — CONFIG SET notify-keyspace-events, les canaux __keyspace@0__ / __keyevent@0__, un simple PSUBSCRIBE depuis n'importe quel client Redis. Mais en dessous, chaque événement chevauche le watch natif à l'échelle du cluster de Zaris : répliqué entre les membres et livré par un pub/sub à l'échelle du cluster, de sorte que les notifications ne s'évanouissent pas quand le nœud qui les a produites le fait.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Notifications d&amp;#39;espace de clés Redis sur le watch natif répliqué de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJSZWRpcyBrZXlzcGFjZSBub3RpZmljYXRpb25zIG9uIFphcmlzJ3MgcmVwbGljYXRlZCBuYXRpdmUgd2F0Y2giPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5SRURJUyBDT01QQVRJQklMSVRZPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPktleXNwYWNlIG5vdGlmaWNhdGlvbnM8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+dGhhdCBzdXJ2aXZlIGZhaWxvdmVyPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+UmVkaXMga2V5c3BhY2UgZXZlbnRzIG92ZXIgYSByZXBsaWNhdGVkIG5hdGl2ZSB3YXRjaDwvdGV4dD4NCg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDAsMjY2KSI+DQogICAgPCEtLSB3cml0ZSAtLT4NCiAgICA8cmVjdCB4PSIwIiB5PSI0MCIgd2lkdGg9IjEyOCIgaGVpZ2h0PSI1MCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iNjQiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+U0VUIHVzZXI6NDI8L3RleHQ+DQogICAgPHBhdGggZD0iTTEzNCA2NSBMMTg2IDY1IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPCEtLSByZXBsaWNhdGVkIHdhdGNoIC0tPg0KICAgIDxyZWN0IHg9IjE5MCIgeT0iMTgiIHdpZHRoPSIxNDgiIGhlaWdodD0iOTQiIHJ4PSIxMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMyIvPg0KICAgIDx0ZXh0IHg9IjI2NCIgeT0iNTAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmZmZmIj5uYXRpdmUgd2F0Y2g8L3RleHQ+DQogICAgPHRleHQgeD0iMjY0IiB5PSI3NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIuNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzJiZDZhMCI+cmVwbGljYXRlZDwvdGV4dD4NCiAgICA8dGV4dCB4PSIyNjQiIHk9Ijk0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMi41IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5yZS1ob21lZDwvdGV4dD4NCiAgICA8cGF0aCBkPSJNMzQ0IDY1IEwzOTYgNjUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiLz4NCiAgICA8IS0tIGNoYW5uZWwgLS0+DQogICAgPHJlY3QgeD0iNDAwIiB5PSI0MCIgd2lkdGg9IjU2IiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI0MjgiIHk9IjYzIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+X19rZXk8L3RleHQ+DQogICAgPHRleHQgeD0iNDI4IiB5PSI3OSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmV2ZW50QDBfXzwvdGV4dD4NCiAgICA8dGV4dCB4PSIyNjQiIHk9IjE1MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPmFueSBSZWRpcyBjbGllbnQgUFNVQlNDUklCRXM8L3RleHQ+DQogIDwvZz4NCg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>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 <code>PSUBSCRIBE</code> et réagir — invalider un cache, réveiller un travailleur, rafraîchir une projection. Pas de scrutation, pas de <code>GET</code> dans une boucle à demander « a-t-elle changé encore ? ».</p>
<p>La fragilité est dans les petits caractères. Les notifications d'espace de clés Redis sont du <strong>pub/sub tire-et-oublie</strong>, é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.</p>
<p>Zaris parle exactement le même protocole — <code>CONFIG SET notify-keyspace-events</code>, les canaux <code>__keyspace@0__</code> et <code>__keyevent@0__</code>, un <code>PSUBSCRIBE</code> 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 <strong>watch natif, à l'échelle du cluster, répliqué.</strong> Cela change ce que valent les notifications.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lactiver--exactement-à-la-manière-de-redis">L'activer — exactement à la manière de Redis<a href="https://clustron.io/fr/blog/redis-keyspace-notifications/#lactiver--exactement-%C3%A0-la-mani%C3%A8re-de-redis" class="hash-link" aria-label="Lien direct vers L'activer — exactement à la manière de Redis" title="Lien direct vers L'activer — exactement à la manière de Redis" translate="no">​</a></h2>
<p>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à :</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Enable notifications for keyspace (K) and keyevent (E) channels</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">redis-cli </span><span class="token parameter variable" style="color:#36acaa">-p</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">6380</span><span class="token plain"> CONFIG SET notify-keyspace-events KEA</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Subscribe to every key-event, cluster-wide</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">redis-cli </span><span class="token parameter variable" style="color:#36acaa">-p</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">6380</span><span class="token plain"> PSUBSCRIBE </span><span class="token string" style="color:#e3116c">'__keyevent@0__:*'</span><br></span></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">1) "pmessage"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">2) "__keyevent@0__:*"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">3) "__keyevent@0__:set"</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">4) "user:42"</span><br></span></code></pre></div></div>
<p>Depuis un client typé — ici StackExchange.Redis — c'est la même forme, sans SDK Zaris impliqué :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> mux </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ConnectionMultiplexer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ConnectAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"localhost:6380"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> sub </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> mux</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetSubscriber</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> sub</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">SubscribeAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">RedisChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"__keyevent@0__:expired"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> RedisChannel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">PatternMode</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Literal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">channel</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> Console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">WriteLine</span><span class="token punctuation" style="color:#393A34">(</span><span class="token interpolation-string string" style="color:#e3116c">$"key expired: </span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">key</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Le point qui mérite d'être souligné : <strong>aucun changement d'application.</strong> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-deux-familles-de-canaux">Les deux familles de canaux<a href="https://clustron.io/fr/blog/redis-keyspace-notifications/#les-deux-familles-de-canaux" class="hash-link" aria-label="Lien direct vers Les deux familles de canaux" title="Lien direct vers Les deux familles de canaux" translate="no">​</a></h2>
<p>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 <em>la clé</em> ou par <em>l'événement</em> :</p>
<table><thead><tr><th>Canal</th><th>Publié quand le drapeau…</th><th>Charge utile</th><th>À utiliser quand ce qui vous intéresse c'est…</th></tr></thead><tbody><tr><td><code>__keyspace@0__:&lt;key&gt;</code></td><td><code>K</code> est défini</td><td>le nom de l'événement (<code>set</code> / <code>del</code> / <code>expired</code>)</td><td>une <strong>clé spécifique</strong> — « dis-moi tout ce qui arrive à <code>session:abc</code> »</td></tr><tr><td><code>__keyevent@0__:&lt;event&gt;</code></td><td><code>E</code> est défini</td><td>la clé qui a changé</td><td>une <strong>classe d'événements</strong> — « dis-moi chaque clé qui vient d'expirer »</td></tr></tbody></table>
<p>Le <code>@0</code> est l'index de base de données, et dans Zaris il est toujours <code>0</code> — 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é :</p>
<ul>
<li class=""><strong><code>set</code></strong> — la clé a été écrite (un <code>SET</code>, un <code>HSET</code>, un <code>LPUSH</code>, toute mutation)</li>
<li class=""><strong><code>del</code></strong> — la clé a été supprimée</li>
<li class=""><strong><code>expired</code></strong> — la clé a été évincée parce que son TTL s'est écoulé</li>
</ul>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ce-qui-se-passe-réellement-en-dessous">Ce qui se passe réellement en dessous<a href="https://clustron.io/fr/blog/redis-keyspace-notifications/#ce-qui-se-passe-r%C3%A9ellement-en-dessous" class="hash-link" aria-label="Lien direct vers Ce qui se passe réellement en dessous" title="Lien direct vers Ce qui se passe réellement en dessous" translate="no">​</a></h2>
<p>Lorsque vous envoyez <code>CONFIG SET notify-keyspace-events KEA</code>, Zaris n'allume pas un émetteur d'événements Redis sur mesure câblé dans le chemin de commande. Il enregistre <strong>un watch de préfixe sur tout l'espace de clés</strong> sur le propre client de routage du nœud — la même primitive <code>Watch</code> que le client .NET natif expose pour des <a class="" href="https://clustron.io/fr/blog/2026/03/30/watch-changes/">lectures réactives, sans scrutation</a>. À 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.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// The entire RESP-side translation, essentially verbatim:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> eventName </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">EventType </span><span class="token keyword" style="color:#00009f">switch</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    WatchEventType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Put     </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"set"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    WatchEventType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Delete  </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"del"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    WatchEventType</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Evicted </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"expired"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    _ </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">                       </span><span class="token comment" style="color:#999988;font-style:italic">// snapshots / heartbeats carry no keyspace meaning</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">keyspace</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> PubSub</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Publish</span><span class="token punctuation" style="color:#393A34">(</span><span class="token interpolation-string string" style="color:#e3116c">$"__keyspace@0__:</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">e</span><span class="token interpolation-string interpolation expression language-csharp punctuation" style="color:#393A34">.</span><span class="token interpolation-string interpolation expression language-csharp">Key</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> eventName</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">keyevent</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> PubSub</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Publish</span><span class="token punctuation" style="color:#393A34">(</span><span class="token interpolation-string string" style="color:#e3116c">$"__keyevent@0__:</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">{</span><span class="token interpolation-string interpolation expression language-csharp">eventName</span><span class="token interpolation-string interpolation punctuation" style="color:#393A34">}</span><span class="token interpolation-string string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Cet appel <code>Publish</code> 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 <em>vocabulaire</em> (noms de canaux, convention de charge utile) ; le cluster possède la <em>livraison</em>. C'est un motif auquel nous nous tenons délibérément à travers tout le produit : <a class="" href="https://clustron.io/fr/blog/redis-protocol-any-language/">chaque fonctionnalité est d'abord une capacité native, et RESP est une peau mince par-dessus</a> — jamais une implémentation parallèle avec ses propres bugs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi--le-watch-est-natif--est-tout-le-propos">Pourquoi « le watch est natif » est tout le propos<a href="https://clustron.io/fr/blog/redis-keyspace-notifications/#pourquoi--le-watch-est-natif--est-tout-le-propos" class="hash-link" aria-label="Lien direct vers Pourquoi « le watch est natif » est tout le propos" title="Lien direct vers Pourquoi « le watch est natif » est tout le propos" translate="no">​</a></h2>
<p>Voici où les deux systèmes divergent, et ce n'est pas cosmétique.</p>
<p>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 à <em>ce</em> 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.</p>
<p>Une notification d'espace de clés Zaris chevauche un watch natif, et un watch natif est un objet à l'échelle du cluster :</p>
<ul>
<li class=""><strong>L'abonnement est répliqué entre les membres.</strong> <code>WatchSyncService</code> 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.</li>
<li class=""><strong>Il est relogé à travers le changement.</strong> 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.</li>
<li class=""><strong>La livraison se diffuse à l'échelle du cluster.</strong> Parce que la publication passe par le pub/sub natif, un client connecté à <em>n'importe quel</em> nœud reçoit les événements des écritures qui se sont produites sur <em>n'importe quel</em> nœud. Vous n'êtes pas discrètement abonné à l'activité d'un seul fragment.</li>
</ul>
<p>La conséquence pratique : un abonné qui observe <code>__keyevent@0__:expired</code> 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é.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-limites-honnêtes">Les limites honnêtes<a href="https://clustron.io/fr/blog/redis-keyspace-notifications/#les-limites-honn%C3%AAtes" class="hash-link" aria-label="Lien direct vers Les limites honnêtes" title="Lien direct vers Les limites honnêtes" translate="no">​</a></h2>
<p>C'est un pont au-dessus d'un pub/sub au mieux, pas une mise à niveau magique de fiabilité. Trois choses à garder au clair :</p>
<p><strong>La livraison reste au mieux, pas un journal garanti.</strong> 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 <em>réveil</em>, pas la <em>source de vérité</em> : sur l'événement, allez lire la valeur autoritaire.</p>
<p><strong>Pas de rattrapage.</strong> 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, <em>puis</em> commencez à raisonner sur les deltas.</p>
<p><strong>La granularité des drapeaux est grossière.</strong> Redis vous laisse sélectionner des <em>classes</em> d'événements avec une chaîne de drapeaux dense (<code>g</code>, <code>$</code>, <code>l</code>, <code>s</code>, <code>h</code>, <code>z</code>, <code>x</code>, <code>e</code>, …). Zaris lit les deux drapeaux qui décident <em>où</em> publier — <code>K</code> pour les canaux d'espace de clés, <code>E</code> pour les canaux d'événements (et <code>A</code> 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.</p>
<p>Si vous avez besoin de notifications limitées à une partie de l'espace de clés plutôt qu'à sa totalité, définissez <code>ZARIS_RESP_KEYSPACE_PREFIX</code> 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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quand-vous-voulez-un-journal-plutôt-quune-sonnette">Quand vous voulez un journal plutôt qu'une sonnette<a href="https://clustron.io/fr/blog/redis-keyspace-notifications/#quand-vous-voulez-un-journal-plut%C3%B4t-quune-sonnette" class="hash-link" aria-label="Lien direct vers Quand vous voulez un journal plutôt qu'une sonnette" title="Lien direct vers Quand vous voulez un journal plutôt qu'une sonnette" translate="no">​</a></h2>
<p>Les notifications d'espace de clés sont une sonnette : elles vous disent que <em>quelque chose s'est passé</em>, à l'instant, pour quiconque écoute. Quand ce dont vous avez réellement besoin est un <strong>enregistrement rejouable et durable</strong> — 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 <a class="" href="https://clustron.io/fr/blog/redis-streams-native/">Redis Streams natifs</a> pour exactement cela : <code>XADD</code> pour ajouter, <code>XREADGROUP</code> 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.</p>
<p>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 <code>CONFIG SET</code>. 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.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Redis</category>
            <category>Modèles distribués</category>
        </item>
        <item>
            <title><![CDATA[Pas de serveur d'authentification : jetons vérifiables hors ligne, rôles et portées dans Zaris]]></title>
            <link>https://clustron.io/fr/blog/token-auth-roles-scopes/</link>
            <guid>https://clustron.io/fr/blog/token-auth-roles-scopes/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris sécurise un cluster avec une seule clé de signature RS256 et des JWT autonomes. N'importe quel manager émet un jeton ; chaque nœud le vérifie hors ligne avec la moitié publique — pas de serveur d'authentification central, pas de base de données, pas d'aller-retour par opération. Voici le modèle de jeton, le catalogue des rôles/portées, les deux plans d'application, et les limites opérationnelles honnêtes.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Jetons vérifiables hors ligne, rôles et portées dans Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJPZmZsaW5lLXZlcmlmaWFibGUgdG9rZW5zLCByb2xlcywgYW5kIHNjb3BlcyBpbiBaYXJpcyI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPlNFQ1VSSVRZPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPk5vIGF1dGggc2VydmVyPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPm9uIHRoZSBob3QgcGF0aDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPk9mZmxpbmUtdmVyaWZpYWJsZSB0b2tlbnMsIHJvbGVzLCBhbmQgc2NvcGVzPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDAsMjUwKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjE5MCIgaGVpZ2h0PSI3OCIgcng9IjEwIiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iOTUiIHk9IjMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+SldUICYjMTgzOyBSUzI1NjwvdGV4dD4NCiAgICA8dGV4dCB4PSI5NSIgeT0iNTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zdWJqZWN0ICsgcm9sZUBzY29wZTwvdGV4dD4NCiAgICA8dGV4dCB4PSI5NSIgeT0iNzEiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5zaWduZWQgYnkgY2x1c3RlciBrZXk8L3RleHQ+DQogICAgPHBhdGggZD0iTTIwMCAyMCBMMzAwIDIwIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPHBhdGggZD0iTTIwMCA1OCBMMzAwIDU4IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPHJlY3QgeD0iMzA2IiB5PSItOCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSI1MiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzkxIiB5PSIxNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmNvbnRyb2wgcGxhbmU8L3RleHQ+DQogICAgPHRleHQgeD0iMzkxIiB5PSIzNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm1hbmFnZXIgJiMxODM7IDo3ODAxPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjMwNiIgeT0iNTIiIHdpZHRoPSIxNzAiIGhlaWdodD0iNTIiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjM5MSIgeT0iNzYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmZmZmIj5kYXRhIHBsYW5lPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjM5MSIgeT0iOTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5ub2RlcyAmIzE4MzsgdmVyaWZ5IEAgSGVsbG88L3RleHQ+DQogICAgPHRleHQgeD0iOTUiIHk9IjExMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPnZlcmlmaWVkIHdpdGggdGhlIHB1YmxpYyBrZXkgJiM4NTk0OyBubyByb3VuZC10cmlwPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>La plupart des façons de placer de l'authentification devant un store de données distribué finissent par ajouter un second système distribué à faire tourner : un service d'authentification à appeler, une base de données de sessions à répliquer, un point de terminaison d'introspection de jetons que chaque nœud doit atteindre sur le chemin chaud. Vous avez sécurisé le store et vous vous êtes acheté une nouvelle dépendance de disponibilité et une nouvelle source de latence.</p>
<p>Zaris prend une autre route, et c'est délibérément une route ennuyeuse : <strong>une clé de signature par cluster, et des jetons que chaque nœud peut vérifier par lui-même.</strong> Un jeton est un JWT signé avec la clé privée du cluster ; chaque manager et chaque nœud détiennent la moitié publique et vérifient la signature <strong>hors ligne</strong> — aucun appel à un serveur d'authentification, aucun quorum, aucune recherche en base de données. La sécurité est désactivée par défaut, et un cluster avec elle désactivée se comporte exactement comme un cluster non sécurisé. Cet article traite de ce qui s'active quand vous l'activez : la forme du jeton, le modèle de rôle-et-portée, les deux plans où l'application se produit réellement, et — car prétendre autrement est la façon dont les gens se donnent un faux sentiment de sécurité — les limites opérationnelles faciles à rater.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="le-jeton-est-toute-lhistoire">Le jeton est toute l'histoire<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#le-jeton-est-toute-lhistoire" class="hash-link" aria-label="Lien direct vers Le jeton est toute l'histoire" title="Lien direct vers Le jeton est toute l'histoire" translate="no">​</a></h2>
<p>Chaque jeton Zaris est un JWT. Il nomme un <strong>sujet</strong> — un utilisateur ou une application — et porte une ou plusieurs <strong>attributions <code>role @ scope</code></strong>. Il est signé par la clé privée RS256 du cluster, qui ne vit que sur les managers ; tout ce qui a besoin de <em>vérifier</em> un jeton ne détient que la clé publique.</p>
<p>Cette seule décision de conception est ce qui retire le serveur d'authentification du tableau :</p>
<ul>
<li class=""><strong>N'importe quel manager peut émettre un jeton.</strong> Il n'y a pas de nœud d'authentification primaire, pas d'élection, pas d'état de session mutable partagé à coordonner. L'émission est une opération de signature locale.</li>
<li class=""><strong>Chaque manager et chaque nœud peuvent vérifier un jeton</strong> avec seulement la clé publique. La vérification est une vérification de signature plus une vérification d'expiration et de portée — du pur CPU, aucun réseau. Le jeton est <strong>autonome</strong> : le rôle et la portée y sont embarqués <em>dedans</em>, de sorte qu'un nœud autorisant une connexion n'a besoin de rien d'autre que la clé publique. Il n'y a pas de base de données de politique sur le nœud à maintenir synchronisée.</li>
</ul>
<p>Le gain pratique apparaît sur le chemin de données. Un client présente son jeton <strong>une fois, quand la connexion est établie</strong> — le « Hello ». Le nœud le vérifie, autorise la connexion pour le store, puis fait confiance à cette connexion pour sa durée de vie. <strong>Les jetons ne sont jamais revérifiés sur le chemin chaud par opération</strong>, de sorte qu'activer la sécurité ne vous coûte rien par <code>Get</code> ou <code>Set</code>. Vous payez l'authentification au moment de la connexion, une fois, et plus jamais pour cette connexion.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rôles-et-portées">Rôles et portées<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#r%C3%B4les-et-port%C3%A9es" class="hash-link" aria-label="Lien direct vers Rôles et portées" title="Lien direct vers Rôles et portées" translate="no">​</a></h2>
<p>Les permissions sont un petit catalogue prédéfini de rôles, chacun accordé <em>à une portée</em> qui dit <strong>où</strong> il s'applique.</p>
<table><thead><tr><th>Rôle</th><th>Niveau de portée</th><th>Ce qu'il accorde</th></tr></thead><tbody><tr><td><code>ClusterAdmin</code></td><td>cluster</td><td>tout — toutes les permissions du plan de contrôle et du plan de données</td></tr><tr><td><code>StoreOperator</code></td><td>store</td><td>démarrer/arrêter le store, modifier la config, démarrer/arrêter les nœuds, diagnostics, métriques</td></tr><tr><td><code>Observer</code></td><td>store</td><td>diagnostics et métriques, lecture seule</td></tr><tr><td><code>DataWriter</code></td><td>store</td><td>lecture / écriture / suppression / scan de données</td></tr><tr><td><code>DataReader</code></td><td>store</td><td>lecture / scan de données</td></tr></tbody></table>
<p>Une <strong>portée</strong> est hiérarchique, et une portée plus large couvre tout ce qui est en dessous :</p>
<ul>
<li class=""><code>zaris:cluster</code> — tout le cluster, pour l'administration à l'échelle du cluster.</li>
<li class=""><code>zaris:store:&lt;name&gt;</code> — un store unique, par ex. <code>zaris:store:orders</code>.</li>
<li class=""><em>root</em> (la portée vide) — couvre tout ; réservée à l'administrateur d'amorçage.</li>
</ul>
<p>Le confinement est <code>root ⊃ cluster ⊃ store</code>, de sorte qu'un <code>DataWriter</code> accordé à <code>zaris:cluster</code> peut écrire dans chaque store, tandis qu'un accordé à <code>zaris:store:orders</code> ne peut écrire que là. Vous composez le moindre privilège à partir de ces deux axes : choisissez le plus petit rôle qui fait le travail, et épinglez-le à la portée la plus étroite dont l'identité a besoin.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lactiver-sans-vous-verrouiller-dehors">L'activer sans vous verrouiller dehors<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#lactiver-sans-vous-verrouiller-dehors" class="hash-link" aria-label="Lien direct vers L'activer sans vous verrouiller dehors" title="Lien direct vers L'activer sans vous verrouiller dehors" translate="no">​</a></h2>
<p>La seule chose qu'un système de sécurité ne doit jamais faire, c'est de vous laisser bloqué hors de votre propre cluster. Zaris gère cela en faisant de <strong>l'initialisation</strong> et de <strong>l'activation</strong> une seule étape : l'application ne s'active qu'<em>une fois que le premier administrateur existe</em>.</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Connect-ZrManager</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Managers mgr-01:7801</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> mgr-02:7801</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> mgr-03:7801</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Initialize-ZrSecurity</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">AdminSubject admin </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">AdminPassword </span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">Read-Host</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">AsSecureString</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># creates the 'admin' login, generates the shared RS256 key on the first manager,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># copies the SAME key to the rest, turns enforcement on, and signs this session in.</span><br></span></code></pre></div></div>
<p>L'administrateur est une <strong>vraie identité de connexion</strong> — un nom d'utilisateur et un mot de passe, comme un compte root cloud — pas un jeton flottant que vous devez garder pour toujours. Après cela, les opérateurs se connectent avec des identifiants et obtiennent un jeton de session à courte durée de vie. L'application elle-même est une <strong>bascule d'exécution</strong>, persistée par manager, qui s'active sans redémarrage ni modification de config :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Enable-ZrSecurity</span><span class="token plain">     </span><span class="token comment" style="color:#999988;font-style:italic"># control-plane enforcement ON  (requires an initialized admin)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Disable-ZrSecurity</span><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># OFF — keeps the key and policy, so you can re-enable instantly</span><br></span></code></pre></div></div>
<p>Les deux se diffusent à chaque manager de l'espace de travail, de sorte que le cluster reste cohérent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deux-sortes-didentité--personnes-et-applications">Deux sortes d'identité : personnes et applications<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#deux-sortes-didentit%C3%A9--personnes-et-applications" class="hash-link" aria-label="Lien direct vers Deux sortes d'identité : personnes et applications" title="Lien direct vers Deux sortes d'identité : personnes et applications" translate="no">​</a></h2>
<p>Suivant le même motif qu'AWS IAM, Kubernetes et Vault, <strong>chaque jeton appartient à une identité enregistrée</strong> — vous ne pouvez pas frapper un jeton pour un nom arbitraire. Il y en a exactement deux sortes, plus l'unique administrateur d'amorçage.</p>
<p><strong>Les utilisateurs sont des personnes.</strong> Ils sont créés avec un mot de passe (stocké uniquement comme un hachage PBKDF2 salé dans <code>users.json</code> sur chaque manager — jamais en clair, aucune base de données externe) et ils <em>se connectent</em> pour obtenir un jeton de session :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">New-ZrUser</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name alice </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Password </span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">Read-Host</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">AsSecureString</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Role StoreOperator </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Scope zaris:store:orders</span><br></span></code></pre></div></div>
<p><strong>Les comptes de service sont des applications.</strong> Pas de mot de passe ; en créer un vous remet un jeton autonome que l'application présente et renouvelle :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">New-ZrServiceAccount</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name orders-svc </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Role DataWriter `</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Scope zaris:store:orders </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">LifetimeMinutes 1440</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># prints the application token once — store it in the app's secret manager.</span><br></span></code></pre></div></div>
<p><code>New-ZrToken -Subject &lt;name&gt;</code> existe toujours pour émettre ou faire tourner un jeton, mais le sujet <strong>doit déjà être un utilisateur ou un compte de service enregistré</strong>. Il n'y a pas de jetons ad hoc pour des noms qui n'existent pas — c'est la propriété qui fait de « qui peut toucher à ce store ? » une question à laquelle on peut répondre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="utiliser-un-jeton">Utiliser un jeton<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#utiliser-un-jeton" class="hash-link" aria-label="Lien direct vers Utiliser un jeton" title="Lien direct vers Utiliser un jeton" translate="no">​</a></h2>
<p>Sur le plan de données, le jeton s'intègre directement dans la <a class="" href="https://clustron.io/fr/blog/connection-strings/">chaîne de connexion</a> — et vous n'avez jamais à y intégrer un secret littéral, car l'indirection <code>env:</code> et <code>file:</code> est de première classe :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Connect-ZrStore</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">ConnectionString </span><span class="token string" style="color:#e3116c">"zaris://node-a:7861,node-b:7861/orders?token=env:ZARIS_TOKEN"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Set-ZrItem</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Key customer:42 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Value </span><span class="token string" style="color:#e3116c">'{"name":"Ada"}'</span><br></span></code></pre></div></div>
<p>Depuis .NET, c'est la même chaîne, ou un <code>TokenProvider</code> dans le code quand vous voulez la rotation :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> services </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">ServiceCollection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZarisConnectionString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"zaris://node-a:7861,node-b:7861/orders?token=env:ZARIS_TOKEN"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">BuildServiceProvider</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> services</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">GetRequiredService</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">IZarisClientProvider</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// token presented once, at the connection Hello</span><br></span></code></pre></div></div>
<p>Si le jeton est manquant, expiré, invalide ou non limité à ce store, la connexion est refusée avec un statut <em>Unauthorized</em> — vous l'apprenez à la connexion, pas trois opérations plus loin dans une transaction. Si vous préférez ne pas frapper et coller un jeton à la main, <code>Connect-ZrStore</code> peut aussi échanger un identifiant nom d'utilisateur/mot de passe contre un jeton auprès du Management Service et le renouveler en arrière-plan.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-renouvellement-qui-ne-coupe-jamais-une-connexion">Un renouvellement qui ne coupe jamais une connexion<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#un-renouvellement-qui-ne-coupe-jamais-une-connexion" class="hash-link" aria-label="Lien direct vers Un renouvellement qui ne coupe jamais une connexion" title="Lien direct vers Un renouvellement qui ne coupe jamais une connexion" translate="no">​</a></h2>
<p>C'est ici que « vérifié seulement à la connexion » paie deux fois. Parce qu'un nœud vérifie le jeton <strong>seulement quand la connexion est ouverte</strong>, une connexion <em>en cours</em> n'est jamais interrompue quand son jeton expire. Le renouvellement n'a qu'à s'assurer que le client a un jeton <em>actuellement valide</em> la prochaine fois qu'il ouvre une connexion — lors d'une reconnexion, d'un nouveau nœud, d'un redémarrage de nœud.</p>
<p>Le client lit donc son jeton via un <strong>fournisseur</strong> à chaque (re)connexion. Gardez ce fournisseur renvoyant des jetons frais et le renouvellement est complètement transparent :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> renewer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">ZarisRenewingTokenProvider</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">initialToken</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://mgr-01:7801"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">renewer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Start</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// renews itself at 80% of lifetime, before expiry</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> options </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">ZarisClientOptions</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    StoreName </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Mode </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> ZarisClientMode</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Remote</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    TokenProvider </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> renewer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// read on every (re)connect</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Le point de terminaison de renouvellement (<code>POST /security/token/renew</code>) est <strong>sans état</strong> — il vérifie votre jeton courant hors ligne contre la clé partagée et en renvoie un nouveau avec le même sujet et les mêmes rôles et une expiration fraîche, de sorte que <em>n'importe quel</em> manager peut le servir et qu'il survit aux redémarrages. Un jeton expiré ne peut pas être renouvelé (renouvelez avant l'expiration — le fournisseur intégré le fait), et un jeton ou un sujet révoqué est refusé. La ligne directrice qui en découle est simple : préférez des <strong>durées de vie courtes avec renouvellement automatique</strong> à un seul jeton à longue durée de vie. Vous obtenez la même transparence avec bien moins d'exposition.</p>
<p>Si vous ne voulez pas du tout de code client dans la boucle, pointez le fournisseur vers une variable d'environnement ou un fichier et laissez un rotateur externe — un gestionnaire de secrets, un sidecar, un cron qui refrappe — le mettre à jour hors bande :</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">options</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TokenProvider </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> ZarisTokenProviders</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromEnvironment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ZARIS_TOKEN"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// or ZarisTokenProviders.FromFile("/etc/zaris/token");</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-partie-honnête--deux-plans-signifie-deux-interrupteurs">La partie honnête : deux plans signifie deux interrupteurs<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#la-partie-honn%C3%AAte--deux-plans-signifie-deux-interrupteurs" class="hash-link" aria-label="Lien direct vers La partie honnête : deux plans signifie deux interrupteurs" title="Lien direct vers La partie honnête : deux plans signifie deux interrupteurs" translate="no">​</a></h2>
<p>C'est le détail qui mord les gens, il a donc sa propre section. « La sécurité est activée » n'est pas un seul fait — c'en est <strong>deux</strong>, parce qu'il y a deux points d'application.</p>
<p>Le <strong>plan de contrôle</strong> — le Management Service sur le port 7801, où vous créez des stores, démarrez et arrêtez des nœuds, et gérez les attributions — applique dès que vous avez initialisé et activé la sécurité. C'est l'interrupteur que <code>Enable-ZrSecurity</code> actionne.</p>
<p>Le <strong>plan de données</strong> — les nœuds Zaris réels qui servent vos clés — est un interrupteur <em>séparé</em>. Un nœud n'applique les jetons qu'une fois qu'il a la clé publique partagée dans sa configuration, et vous provisionnez cela par store :</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Enable-ZrStoreSecurity</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Store orders            </span><span class="token comment" style="color:#999988;font-style:italic"># write the public key into every node's config</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Enable-ZrStoreSecurity</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Store orders </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Restart   </span><span class="token comment" style="color:#999988;font-style:italic"># ...and rolling-restart to activate now</span><br></span></code></pre></div></div>
<p><strong>Jusqu'à ce que vous fassiez cela, les nœuds du store exécutent un authentificateur <code>AllowAll</code> et acceptent chaque connexion — avec ou sans jeton.</strong> C'est intentionnel : cela vous laisse activer d'abord le plan de contrôle et déployer l'application du plan de données store par store, avec un redémarrage planifié, au lieu de basculer tout le cluster d'un coup. Mais cela signifie qu'<em>activer le plan de contrôle ne sécurise pas, à lui seul, vos données.</em> Les deux pièges correspondants :</p>
<ul>
<li class=""><strong><code>Get-ZrSecurityStatus</code> rapporte le plan de contrôle.</strong> Ses champs <code>Enabled</code> / <code>Initialized</code> / <code>Issuer</code> décrivent l'application du plan de gestion. Un <code>True</code> là-bas ne <strong>prouve pas</strong> que les nœuds d'un store donné vérifient les jetons — c'est le travail de <code>Enable-ZrStoreSecurity</code>, confirmé séparément. Si un store « sécurisé » accepte toujours des clients sans jeton, la config du nœud n'a pas la clé ou n'a pas été redémarrée.</li>
<li class=""><strong>La révocation n'est pas instantanée sur le chemin de données.</strong> Parce que les jetons de données sont autonomes et vérifiés seulement à la connexion, <code>Revoke-ZrToken</code> est honoré immédiatement par le plan de contrôle et par le <em>renouvellement</em> (un sujet révoqué ne peut pas se rafraîchir), mais une connexion de données déjà ouverte continue de fonctionner jusqu'à ce qu'elle se ferme. La façon propre de couper une application sur le plan de données est d'émettre des jetons de données avec un <code>-LifetimeMinutes</code> modeste et de laisser le jeton courant expirer, ou de redémarrer ses connexions après révocation. Si vous avez besoin d'une coupure quasi instantanée, les durées de vie courtes sont le levier — pas l'attente que le chemin chaud revérifie.</li>
</ul>
<p>Aucun de ces deux cas n'est un bug ; ce sont les conséquences directes de « vérifier hors ligne, une fois, sans serveur d'authentification ». C'est simplement la partie que vous devez opérer délibérément. Le déploiement recommandé en découle directement : initialisez et activez le plan de contrôle, vérifiez avec <code>Get-ZrSecurityStatus</code>, émettez des jetons d'opérateur et d'application et confirmez qu'ils fonctionnent, puis <code>Enable-ZrStoreSecurity</code> par store avec un redémarrage planifié, et enfin faites passer les applications sur des jetons à courte durée de vie avec renouvellement automatique.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="où-cela-se-situe-dans-la-pile">Où cela se situe dans la pile<a href="https://clustron.io/fr/blog/token-auth-roles-scopes/#o%C3%B9-cela-se-situe-dans-la-pile" class="hash-link" aria-label="Lien direct vers Où cela se situe dans la pile" title="Lien direct vers Où cela se situe dans la pile" translate="no">​</a></h2>
<p>L'authentification par jeton répond à <em>qui peut toucher à ce store et ce qu'il peut y faire</em>. Elle se compose avec les deux autres moitiés d'un Zaris verrouillé et n'en remplace aucune :</p>
<ul>
<li class=""><strong><a class="" href="https://clustron.io/fr/blog/tls-ca-modes/">La confiance TLS enfichable</a></strong> chiffre le fil et authentifie les <em>points de terminaison</em> — c'est le transport <code>zariss://</code> sous le jeton, orthogonal à qui détient le jeton.</li>
<li class=""><strong><a class="" href="https://clustron.io/fr/blog/helm-secure-e2e/">La visite guidée Helm sécurisée</a></strong> met les deux ensemble sur Kubernetes de bout en bout, qui est là où les détails de chaîne de connexion et de certificat sont réellement exercés.</li>
</ul>
<p>La chose à retenir est la forme de l'idée centrale. Une clé RS256, des JWT qui embarquent leur propre autorité, une vérification qui est du pur CPU hors ligne, et un jeton vérifié exactement une fois par connexion. C'est ce qui vous achète l'authentification et l'autorisation limitée par rôle à travers un cluster entier sans monter un serveur d'authentification, un store de session, ou un seul saut réseau supplémentaire sur le chemin que votre application exécute réellement. Le coût est la discipline à laquelle cet article a consacré sa seconde moitié : rappelez-vous que le plan de données est son propre interrupteur, et gardez vos jetons à courte durée de vie. Faites cela, et « sécurisé » devient une propriété que vous pouvez énoncer précisément plutôt qu'espérer.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sécurité</category>
            <category>Opérations</category>
            <category>Systèmes distribués</category>
        </item>
        <item>
            <title><![CDATA[Le facteur de réplication n'est pas la tolérance aux pannes : dimensionner un cluster Zaris pour la durabilité sur Kubernetes]]></title>
            <link>https://clustron.io/fr/blog/kubernetes-durability-sizing/</link>
            <guid>https://clustron.io/fr/blog/kubernetes-durability-sizing/</guid>
            <pubDate>Fri, 02 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Sur Kubernetes, Zaris garde les données en mémoire et durables par réplication, pas par disque. Mais définir replicationFactor=2 ne survit pas à lui seul à la perte d'un pod — le nombre de pods doit être un multiple du RF, sinon une partition restante tourne en copie unique et perd ses clés quand ce pod meurt. Voici le calcul de placement, pourquoi un nombre de pods impair mord, et comment le chart vous protège.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Dimensionner un cluster Zaris pour la durabilité sur Kubernetes" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJTaXppbmcgYSBaYXJpcyBjbHVzdGVyIGZvciBkdXJhYmlsaXR5IG9uIEt1YmVybmV0ZXMiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5LVUJFUk5FVEVTIMK3IERVUkFCSUxJVFk8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+UmVwbGljYXRpb24gZmFjdG9yPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPmlzIG5vdCBmYXVsdCB0b2xlcmFuY2U8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIyMjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5TaXppbmcgYSBaYXJpcyBjbHVzdGVyIHNvIGEgcG9kIGxvc3MgbmV2ZXIgbG9zZXMgZGF0YTwvdGV4dD4NCg0KICA8IS0tIFJpZ2h0IGRpYWdyYW06IDQgcG9kcyAtPiAyIGZ1bGx5LXJlcGxpY2F0ZWQgcGFydGl0aW9ucyAoZHVyYWJsZSkgLS0+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcyMCwyNjYpIj4NCiAgICA8dGV4dCB4PSIwIiB5PSIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgbGV0dGVyLXNwYWNpbmc9IjIiIGZpbGw9IiM1YTZiNjUiPjQgUE9EUyDCtyBSRiAyIMK3IDIgUEFSVElUSU9OUzwvdGV4dD4NCg0KICAgIDwhLS0gUGFydGl0aW9uIEEgLS0+DQogICAgPHJlY3QgeD0iMCIgeT0iMTYiIHdpZHRoPSIyMDAiIGhlaWdodD0iNjYiIHJ4PSIxMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjEyIiB5PSIzOCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMyYmQ2YTAiPnBhcnRpdGlvbiBBPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjEyIiB5PSI0NiIgd2lkdGg9IjgwIiBoZWlnaHQ9IjI4IiByeD0iNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI1MiIgeT0iNjUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5wb2QgMDwvdGV4dD4NCiAgICA8cmVjdCB4PSIxMDgiIHk9IjQ2IiB3aWR0aD0iODAiIGhlaWdodD0iMjgiIHJ4PSI2IiBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjE0OCIgeT0iNjUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5wb2QgMTwvdGV4dD4NCg0KICAgIDwhLS0gUGFydGl0aW9uIEIgLS0+DQogICAgPHJlY3QgeD0iMjIwIiB5PSIxNiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI2NiIgcng9IjEwIiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjMyIiB5PSIzOCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMyYmQ2YTAiPnBhcnRpdGlvbiBCPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjIzMiIgeT0iNDYiIHdpZHRoPSI4MCIgaGVpZ2h0PSIyOCIgcng9IjYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjcyIiB5PSI2NSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnBvZCAyPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjMyOCIgeT0iNDYiIHdpZHRoPSI4MCIgaGVpZ2h0PSIyOCIgcng9IjYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzY4IiB5PSI2NSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnBvZCAzPC90ZXh0Pg0KDQogICAgPHRleHQgeD0iMCIgeT0iMTEyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzJiZDZhMCI+c3Vydml2ZXMgb25lIHBvZCBsb3NzICYjMTAwMDM7PC90ZXh0Pg0KDQogICAgPCEtLSBvZGQgcG9kIHdhcm5pbmcgLS0+DQogICAgPHJlY3QgeD0iMCIgeT0iMTMwIiB3aWR0aD0iNDIwIiBoZWlnaHQ9IjM0IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNiIvPg0KICAgIDx0ZXh0IHg9IjE2IiB5PSIxNTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjOWZiM2FkIj5wb2RzIG1vZCBSRiAmIzg4MDA7IDAgJiM4NTk0OyBhIGxlZnRvdmVyIHBhcnRpdGlvbiBpcyBzaW5nbGUtY29weTwvdGV4dD4NCiAgPC9nPg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Il existe un raisonnement tentant lorsqu'on déploie un store répliqué sur Kubernetes : <em>j'ai défini <code>replicationFactor: 2</code>, donc chaque partition a une sauvegarde, donc je survis à la perte d'un nœud.</em> Cela semble inattaquable. C'est aussi, pour une taille de cluster étonnamment courante, faux — et faux de la manière silencieuse qui ne se révèle que la première fois qu'un pod meurt réellement.</p>
<p>Zaris sur Kubernetes stocke les données <strong>en mémoire</strong> et les rend durables par <strong>réplication, pas par disque</strong> : le volume de données du nœud est un <code>emptyDir</code>, et un pod reprogrammé se reconstruit à partir de la copie détenue par un autre pod. Cette conception est saine — mais elle repose entièrement sur l'<em>existence</em> d'une copie sur un autre pod. Que cette copie existe n'est pas décidé par <code>replicationFactor</code> seul. C'est décidé par l'arithmétique entre votre nombre de pods et votre facteur de réplication, et si vous vous trompez dans cette arithmétique, le store tournera parfaitement, servira chaque lecture et écriture, et détiendra silencieusement une partition qui n'a nulle part où se répliquer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forme-du-déploiement">La forme du déploiement<a href="https://clustron.io/fr/blog/kubernetes-durability-sizing/#la-forme-du-d%C3%A9ploiement" class="hash-link" aria-label="Lien direct vers La forme du déploiement" title="Lien direct vers La forme du déploiement" translate="no">​</a></h2>
<p>Le chart Helm de Zaris déploie le niveau nœud comme un <strong>StatefulSet</strong>, une instance de nœud par pod (<code>zaris-0</code>, <code>zaris-1</code>, …), chacune propriétaire de shard en mémoire. Un Service headless donne à chaque pod une identité DNS stable, la disponibilité (readiness) est conditionnée à la convergence du cluster, et un PodDisruptionBudget plus une fenêtre de terminaison à drain gracieux protègent l'ensemble pendant les perturbations volontaires.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># values.yaml (defaults)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">node</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">replicas</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic"># pod count — ONE node instance per pod</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">replicationFactor</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># copies of each partition, on distinct pods</span><br></span></code></pre></div></div>
<p>Les notes du chart lui-même sont sans détour sur l'origine de la durabilité :</p>
<blockquote>
<p><strong>Les données de nœud sont en emptyDir.</strong> La durabilité en mémoire passe par la réplication (RF), pas le disque ; un nœud reprogrammé se reconstruit à partir des réplicas.</p>
</blockquote>
<p>Un pod de nœud peut donc être tué et reprogrammé toute la journée sans perte de données — <em>à condition</em> que ses partitions aient un réplica vivant sur un autre pod depuis lequel se reconstruire. Cette condition est tout l'enjeu.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="comment-les-partitions-atterrissent-sur-les-pods">Comment les partitions atterrissent sur les pods<a href="https://clustron.io/fr/blog/kubernetes-durability-sizing/#comment-les-partitions-atterrissent-sur-les-pods" class="hash-link" aria-label="Lien direct vers Comment les partitions atterrissent sur les pods" title="Lien direct vers Comment les partitions atterrissent sur les pods" translate="no">​</a></h2>
<p>Une partition dans Zaris est détenue par <strong>RF instances de nœud distinctes</strong> — un propriétaire actif plus <code>RF−1</code> réplicas — et les réplicas sont toujours placés sur des pods <em>différents</em> (un réplica sur le même pod que son actif ne serait aucune protection). Le runtime place les partitions de façon gloutonne :</p>
<blockquote>
<p>Le runtime remplit une partition jusqu'à son RF complet avant de former la suivante.</p>
</blockquote>
<p>Cette seule phrase est la clé de toute l'histoire du dimensionnement. Les instances sont consommées RF par RF. Donc le nombre de partitions <strong>entièrement répliquées</strong> que vous obtenez est :</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">fully-replicated partitions = ⌊ instances ÷ RF ⌋</span><br></span></code></pre></div></div>
<p>et toutes les instances restantes — <code>instances mod RF</code> d'entre elles — forment une partition en <strong>copie unique</strong> sans pair pour se répliquer. C'est une véritable partition qui sert. Elle n'a simplement aucune sauvegarde.</p>
<!-- -->
<p>Avec quatre pods au RF 2, vous obtenez deux partitions, chacune avec deux copies sur des pods distincts — perdez n'importe quel pod et la copie survivante poursuit tandis que la partition se re-réplique sur le pod reprogrammé. Avec trois pods au RF 2, les pods 0 et 1 forment la partition A au RF complet, mais le pod 2 reste en surplus et forme la partition B avec une copie unique. Ajoutez un quatrième pod et il devient le réplica de B. Jusque-là, B est un risque.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-règle-de-dimensionnement-sous-forme-de-tableau">La règle de dimensionnement, sous forme de tableau<a href="https://clustron.io/fr/blog/kubernetes-durability-sizing/#la-r%C3%A8gle-de-dimensionnement-sous-forme-de-tableau" class="hash-link" aria-label="Lien direct vers La règle de dimensionnement, sous forme de tableau" title="Lien direct vers La règle de dimensionnement, sous forme de tableau" translate="no">​</a></h2>
<p>Comme chaque partition a besoin de RF pods distincts et que Kubernetes exécute une instance par pod, la règle est simplement :</p>
<blockquote>
<p><strong>Exécutez un nombre de pods qui est un multiple de RF.</strong> Avec RF 2, utilisez un nombre pair de pods.</p>
</blockquote>
<table><thead><tr><th>Pods (1 instance chacun)</th><th>RF</th><th>Partitions entièrement répliquées</th><th>Survit à la perte d'un pod ?</th></tr></thead><tbody><tr><td>2</td><td>2</td><td>1</td><td>Oui</td></tr><tr><td><strong>3</strong></td><td>2</td><td>1 (+ <strong>1 en copie unique</strong>)</td><td><strong>Non</strong> — perdre le pod en copie unique perd cette partition</td></tr><tr><td>4</td><td>2</td><td>2</td><td>Oui</td></tr><tr><td>6</td><td>2</td><td>3</td><td>Oui</td></tr></tbody></table>
<p>Le schéma se généralise : <code>N</code> partitions au RF 2 nécessitent <code>2N</code> pods ; au RF 3, elles nécessitent <code>3N</code>. La mise à l'échelle horizontale consiste à ajouter des pods un RF complet à la fois si vous voulez que chaque nouvelle partition naisse durable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pourquoi-trois-pods-au-rf-2-est-le-piège">Pourquoi trois pods au RF 2 est le piège<a href="https://clustron.io/fr/blog/kubernetes-durability-sizing/#pourquoi-trois-pods-au-rf-2-est-le-pi%C3%A8ge" class="hash-link" aria-label="Lien direct vers Pourquoi trois pods au RF 2 est le piège" title="Lien direct vers Pourquoi trois pods au RF 2 est le piège" translate="no">​</a></h2>
<p>Trois est le nombre qui mord, car trois pods <em>ressemblent</em> à un cluster sain et hautement disponible. Il passe la disponibilité (readiness). Il sert le trafic. <code>kubectl get pods</code> montre <code>zaris-0..2</code> tous <code>1/1 Ready</code>. Rien n'est manifestement anormal.</p>
<p>Ce qui est anormal reste invisible jusqu'à ce que le pod en copie unique — celui qui détient l'unique copie de la partition B — soit le pod qui meurt. À cet instant, les clés de B sont perdues, définitivement, car il n'y a jamais eu de deuxième copie depuis laquelle se reconstruire. Ce n'est pas un bug ; c'est la même mise en garde que faire tourner une seule machine, juste appliquée à une partition plutôt qu'à tout le store. La carte ne se régénère que lorsque <em>toutes</em> les copies d'une partition sont perdues, et pour une partition en copie unique « toutes les copies » signifie un pod.</p>
<p>Cela a été mesuré, non supposé. Sur un cluster <code>kind</code> :</p>
<ul>
<li class="">À <strong><code>replicas=4</code></strong> (2 partitions × RF 2), tuer le pod propriétaire actif d'une partition a perdu <strong>0 clé sur 200</strong> — le réplica a pris le relais et le RF s'est auto-restauré sur le pod reprogrammé.</li>
<li class="">À <strong><code>replicas=3</code></strong>, tuer le pod en copie unique a perdu purement et simplement les clés de cette partition.</li>
</ul>
<p>Même store, même réglage de RF, même chaos. La seule variable était de savoir si le nombre de pods était un multiple du RF.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="les-garde-fous-que-le-chart-vous-offre">Les garde-fous que le chart vous offre<a href="https://clustron.io/fr/blog/kubernetes-durability-sizing/#les-garde-fous-que-le-chart-vous-offre" class="hash-link" aria-label="Lien direct vers Les garde-fous que le chart vous offre" title="Lien direct vers Les garde-fous que le chart vous offre" translate="no">​</a></h2>
<p>Le chart ne laisse pas cela au folklore. Installez un cluster dont le nombre de pods n'est pas un multiple du RF et <code>helm install</code> affiche un avertissement de durabilité dans ses notes :</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">⚠  DURABILITY WARNING</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   node.replicas (3) is not a multiple of node.replicationFactor (2), so at least one</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   partition is SINGLE-COPY and will LOSE its data if that pod is lost. For fault</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">   tolerance set node.replicas to a multiple of 2 (e.g. 4).</span><br></span></code></pre></div></div>
<p>Au-delà de l'avertissement, plusieurs valeurs par défaut sont fixées pour qu'un cluster <em>correctement dimensionné</em> se comporte bien sous perturbation :</p>
<ul>
<li class=""><strong>Disponibilité conditionnée à la convergence.</strong> <code>/readyz</code> ne passe qu'une fois la carte des partitions et la propriété actives, de sorte qu'un pod <code>Ready</code> est routable — Kubernetes n'enverra pas de trafic à un nœud qui n'a pas encore pris possession.</li>
<li class=""><strong>PodDisruptionBudget.</strong> Par défaut, au plus un nœud est volontairement perturbé à la fois, de sorte qu'un drain ou une mise à jour progressive ne peut pas retirer les deux copies d'une partition en même temps.</li>
<li class=""><strong>Terminaison à drain gracieux.</strong> Une fenêtre de grâce de terminaison laisse un nœud sur le départ transmettre proprement plutôt que d'être démoli en plein vol.</li>
<li class=""><strong>Mise à l'échelle horizontale à chaud.</strong> Avec la découverte Kubernetes activée (le défaut), le registre est synthétisé à partir des ordinaux de pod et du DNS headless, de sorte qu'ajouter des pods est un simple <code>kubectl scale statefulset</code> — de nouveaux pods rejoignent et la carte des partitions grandit sans redémarrer les existants. Ajoutez-les un multiple de RF à la fois et chaque nouvelle partition est durable dès la naissance.</li>
</ul>
<p>Rien de tout cela n'aide pourtant la partition en copie unique. Un PDB qui maintient tous-sauf-un pods debout n'est aucune protection quand l'un de ces pods est le <em>seul</em> foyer d'une partition. Les garde-fous supposent que vous avez correctement dimensionné le cluster ; ils ne sauvent pas un cluster qui ne l'était pas.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="règles-pratiques">Règles pratiques<a href="https://clustron.io/fr/blog/kubernetes-durability-sizing/#r%C3%A8gles-pratiques" class="hash-link" aria-label="Lien direct vers Règles pratiques" title="Lien direct vers Règles pratiques" translate="no">​</a></h2>
<p>Tout le sujet se comprime en quelques habitudes :</p>
<ol>
<li class=""><strong>Choisissez d'abord le RF, puis faites du nombre de pods un multiple de celui-ci.</strong> RF 2 → nombres de pods pairs ; RF 3 → multiples de trois. Le chart par défaut livre 4 pods au RF 2 précisément parce que c'est la plus petite forme tolérante aux pannes.</li>
<li class=""><strong>Traitez un nombre de pods impair comme réservé à l'évaluation.</strong> Trois pods au RF 2 convient à une démo sur <code>kind</code> ; ce n'est pas une posture de durabilité de production, et le chart vous le dit au moment de l'installation.</li>
<li class=""><strong>Faites la mise à l'échelle horizontale par RF.</strong> Ajouter un seul pod à un cluster durable crée temporairement une partition en copie unique jusqu'à l'arrivée du pod suivant. Si vous grandissez pour la durabilité, grandissez d'un ensemble de réplication complet à la fois.</li>
<li class=""><strong>Souvenez-vous que le volume est emptyDir.</strong> Il n'y a pas de disque de secours. La réplication <em>est</em> votre durabilité, ce qui est exactement pourquoi la copie doit réellement exister sur un autre pod.</li>
</ol>
<p>Le facteur de réplication vous dit combien de copies une partition <em>veut</em>. Le nombre de pods décide si elle peut les <em>avoir</em>. Alignez ces deux nombres et la perte d'un pod devient un non-événement — le réplica sert, le pod reprogrammé se reconstruit, et pas une clé n'est perdue. Laissez-les dériver hors d'alignement et vous avez bâti un cluster hautement disponible jusqu'au moment précis où cela compte.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Kubernetes</category>
            <category>Opérations</category>
        </item>
    </channel>
</rss>