<?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/de/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>de</language>
        <item>
            <title><![CDATA[Viele Keys, wenige Round-Trips: Wie Zaris eine Bulk-Operation per Fan-out verteilt]]></title>
            <link>https://clustron.io/de/blog/bulk-and-batch-fan-out/</link>
            <guid>https://clustron.io/de/blog/bulk-and-batch-fan-out/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein GetMany über tausend Keys sollte nicht tausend Netzwerk-Round-Trips kosten. Zaris gruppiert die Keys nach ihrem besitzenden Node, sendet pro Owner parallel eine Anfrage und fügt die Antworten in der Reihenfolge des Aufrufers wieder zusammen. Hier steht der exakte Fan-out-Pfad, warum jedes Item einen Index trägt und warum eine Bulk-Operation bewusst keine Transaktion ist.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Eine Bulk-Operation per Fan-out an die besitzenden Nodes verteilen" 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>Fragen Sie einen verteilten Store nach einem Key, wird der Aufwand von einer einzigen Zahl dominiert: dem Round-Trip zum Node, der ihn besitzt. Der Lookup selbst dauert Nanosekunden; das Netzwerk Mikrosekunden. Fragen Sie nun nach tausend Keys. Wenn Sie in einer Schleife tausendmal <code>Get</code> aufrufen, zahlen Sie diesen Round-Trip tausendmal, seriell, und der Store verbringt fast die gesamte Wanduhrzeit untätig mit dem Warten auf Pakete. Die Keys könnten auf vier Nodes verteilt sein, die parallel hätten antworten können — aber eine naive Schleife gibt ihnen nie die Chance.</p>
<p>Zaris hat drei Bulk-APIs — <code>GetManyAsync</code>, <code>PutManyAsync</code>, <code>DeleteManyAsync</code> — und darunter ein gemischtes <code>ExecuteBatchAsync</code>, und ihr ganzer Daseinszweck ist, „tausend Keys" in „eine Anfrage pro besitzendem Node, parallel abgesetzt" zu verwandeln. Dieser Beitrag ist der Fan-out-Pfad von Anfang bis Ende: wie der Client Keys nach Owner in Buckets sortiert, warum alle vier APIs auf eine einzige Engine zusammenfallen, wie ein <code>Index</code>-Feld die Antworten durch zwei Schichten von Nebenläufigkeit in Reihenfolge hält und warum — anders als bei einer <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktion</a> — eine Bulk-Operation pro Item arbeitet und ein Teilerfolg normal ist.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-kostenmodell-round-trips-nicht-operationen">Das Kostenmodell: Round-Trips, nicht Operationen<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#das-kostenmodell-round-trips-nicht-operationen" class="hash-link" aria-label="Direkter Link zur Das Kostenmodell: Round-Trips, nicht Operationen" title="Direkter Link zur Das Kostenmodell: Round-Trips, nicht Operationen" translate="no">​</a></h2>
<p>Wogegen eine Bulk-API kämpft, ist nicht die CPU. Ein <code>GET</code> auf einen einzelnen Key ist auf einem besitzenden Node ein Dictionary-Lookup — der <a class="" href="https://clustron.io/de/blog/latency-floor/">Latenzboden</a> ist eine Mikrosekunde echter Arbeit in zehn Mikrosekunden Installation. Der teure Teil ist die Installation: Die Anfrage muss das Netzwerk zum Owner überqueren, und die Antwort muss zurückkommen. Nennen wir diese Round-Trip-Zeit <code>R</code>, in einem Datacenter-Netzwerk typischerweise zehn bis hundert Mikrosekunden, um Größenordnungen mehr als der Lookup.</p>
<p>Eine serielle Schleife über <code>N</code> Keys kostet etwa <code>N × R</code>, weil jeder Aufruf auf die Rückkehr des Pakets des vorherigen wartet, bevor er den nächsten sendet. Das ist die Falle. Die Lösung ist kein schnellerer Lookup — es ist, aufzuhören, <code>R</code> pro Key zu zahlen. Wenn die <code>N</code> Keys auf <code>M</code> besitzenden Nodes liegen, ist der Boden <code>R</code>, <code>M</code>-mal <em>nebenläufig</em> gezahlt, also insgesamt etwa ein <code>R</code>, unabhängig davon, wie viele Keys jeder Node hält. Bei tausend Keys auf vier Nodes ist das der Unterschied zwischen tausend seriellen Round-Trips und effektiv einem. Das gesamte Design unten existiert, um <code>N × R</code> auf <code>~R</code> zusammenzufalten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eine-engine-alles-ist-ein-batch-von-items">Eine Engine: alles ist ein Batch von Items<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#eine-engine-alles-ist-ein-batch-von-items" class="hash-link" aria-label="Direkter Link zur Eine Engine: alles ist ein Batch von Items" title="Direkter Link zur Eine Engine: alles ist ein Batch von Items" translate="no">​</a></h2>
<p>Das erste Überraschende im Cluster-Client ist, dass <code>GetManyAsync</code>, <code>PutManyAsync</code> und <code>DeleteManyAsync</code> nicht drei getrennte Implementierungen sind. Jede ist ein dünner Shim, der ihre Argumente in eine Liste von <code>KvBatchItem</code>s verwandelt und an die eine Methode übergibt, die die eigentliche Arbeit erledigt, <code>ExecuteBatchAsync</code>. So macht <code>GetManyAsync</code> genau das:</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> und <code>DeleteManyAsync</code> tun dasselbe mit <code>KvBatchOp.Put</code> und <code>KvBatchOp.Delete</code>. Die Unterscheidung, die die Samples zwischen <em>Bulk</em> (jeweils eine Operationsart) und <em>Batch</em> (eine Mischung aus PUT / GET / DELETE in einem Aufruf) ziehen, ist also eine Annehmlichkeit der Oberfläche: Die Bulk-Helfer sind nur ein Batch, in dem zufällig jedes Item dieselbe Operation hat. Es gibt genau eine Fan-out-Engine, und sie ist operationsagnostisch.</p>
<p>Beachten Sie <code>Index = i</code>. Jedes Item wird mit seiner Position in der Liste des Aufrufers gestempelt, <em>bevor</em> irgendetwas gruppiert, umsortiert oder gesendet wird. Dieser Stempel überlebt die zwei nebenläufigen Hops, die noch kommen, und erlaubt es, die endgültige Antwort wieder in Reihenfolge zu bringen. Merken Sie sich das.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-fan-out-nach-owner-in-buckets-sortieren-je-eine-anfrage">Der Fan-out: nach Owner in Buckets sortieren, je eine Anfrage<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#der-fan-out-nach-owner-in-buckets-sortieren-je-eine-anfrage" class="hash-link" aria-label="Direkter Link zur Der Fan-out: nach Owner in Buckets sortieren, je eine Anfrage" title="Direkter Link zur Der Fan-out: nach Owner in Buckets sortieren, je eine Anfrage" translate="no">​</a></h2>
<p>Hier ist das Herz des ganzen Beitrags — der Batch-Dispatch des Cluster-Clients, im Wesentlichen vollständig:</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>Lesen Sie es als vier Züge:</p>
<ol>
<li class="">
<p><strong>Jeden Key lokal zu seinem Owner auflösen.</strong> <code>SegmentMapper.ComputeSegmentId</code> ist dieselbe Platzierungsfunktion, die der Single-Key-Pfad nutzt — CRC16 über den Hash-Tag, modulo Segmentanzahl —, und <code>GetClientForSegment</code> bildet dieses Segment auf die bereits geöffnete Verbindung zu seinem besitzenden Node ab. Kein Proxy, kein Koordinator: Der Client berechnet das Ownership selbst, genau wie in <a class="" href="https://clustron.io/de/blog/client-partition-aware-routing/">Ein Hop zum Owner</a> beschrieben. Der Unterschied ist, dass er das hier für jeden Key der Menge auf einmal tut.</p>
</li>
<li class="">
<p><strong>Bucketing.</strong> Keys, die demselben Node gehören, häufen sich in dessen Liste. Tausend Keys, verteilt auf vier Owner, werden zu vier Listen. Hier werden aus <code>N</code> Operationen <code>M</code> Anfragen.</p>
</li>
<li class="">
<p><strong>Nebenläufig absetzen.</strong> <code>buckets.Select(...)</code> baut pro Owner einen <code>ExecuteBatchAsync</code>-Aufruf, und <code>Task.WhenAll</code> lässt alle gleichzeitig starten. Vier Nodes antworten parallel; die Wanduhrzeit ist die Antwort des langsamsten einzelnen Nodes, nicht die Summe.</p>
</li>
<li class="">
<p><strong>In Reihenfolge neu zusammensetzen.</strong> Jeder Owner liefert seinen Anteil der Ergebnisse; <code>SelectMany</code> flacht sie ab, und <code>OrderBy(r =&gt; r.Index)</code> stellt die ursprüngliche Reihenfolge des Aufrufers wieder her. Der Aufrufer erhält eine Liste, in der Position <em>i</em> die Antwort für Key <em>i</em> ist, obwohl die Keys zu verschiedenen Nodes gereist und in der Reihenfolge zurückgekehrt sind, in der das Netzwerk sie lieferte.</p>
</li>
</ol>
<!-- -->
<p>Das ist die vollständige Amortisationsbehauptung, konkret gemacht: Die Anzahl der Round-Trips entspricht der Anzahl der <em>verschiedenen besitzenden Nodes</em> in der Key-Menge, nicht der Anzahl der Keys.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="innerhalb-eines-owners-wieder-nebenläufig-wieder-geordnet">Innerhalb eines Owners: wieder nebenläufig, wieder geordnet<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#innerhalb-eines-owners-wieder-nebenl%C3%A4ufig-wieder-geordnet" class="hash-link" aria-label="Direkter Link zur Innerhalb eines Owners: wieder nebenläufig, wieder geordnet" title="Direkter Link zur Innerhalb eines Owners: wieder nebenläufig, wieder geordnet" translate="no">​</a></h2>
<p>Jeder Sub-Batch pro Owner landet im <code>BatchCommandHandler</code> dieses Nodes. Auch er führt die Items nicht nacheinander aus — er führt sie alle gleichzeitig aus und sortiert dann neu:</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> ist ein kleiner Switch über die Operation, der jedes Item an den Store weiterleitet — <code>GetRawAsync</code>, <code>PutRawAsync</code> oder <code>DeleteRawAsync</code> —, sodass ein gemischter Batch aus Lese- und Schreibzugriffen Item für Item innerhalb derselben Anfrage verarbeitet wird. (Für die In-Process- und Single-Node-Router erscheint dieselbe Form als <code>Parallel.ForEachAsync</code> über die Keys, begrenzt durch <code>MaxDegreeOfParallelism</code> — standardmäßig eine Option <code>MaxParallelism</code> oder <code>Environment.ProcessorCount</code> —, weshalb <a class="" href="https://clustron.io/de/blog/reads-scale-with-cores/">Lesezugriffe mit Kernen skalieren</a>: Ein dicker Sub-Batch hält jeden Kern beschäftigt statt nur einen.)</p>
<p>Der <code>Index</code>-Stempel verdient sich seinen Lohn also <em>zweimal</em>. Nebenläufigkeit bringt die Reihenfolge auf dem Node durcheinander, daher sortiert der Handler vor der Antwort nach <code>Index</code>. Fan-out bringt die Reihenfolge auf dem Client durcheinander, daher sortiert der Client nach dem Zusammenführen nach <code>Index</code>. Der Aufrufer sieht nie eines der beiden Durcheinander. Eine kleine Ganzzahl, ab dem Moment, in dem die Anfrage gebaut wird, an jedem Item mitgeführt, ist der gesamte Mechanismus, der es sicher macht, „wirf alles parallel auf den Cluster" als geordnete Liste bereitzustellen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eine-bulk-operation-ist-keine-transaktion">Eine Bulk-Operation ist keine Transaktion<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#eine-bulk-operation-ist-keine-transaktion" class="hash-link" aria-label="Direkter Link zur Eine Bulk-Operation ist keine Transaktion" title="Direkter Link zur Eine Bulk-Operation ist keine Transaktion" translate="no">​</a></h2>
<p>Das ist die Unterscheidung, die die Leute am häufigsten falsch verstehen, daher soll sie scharf formuliert werden. Jedes Item eines Batches trägt sein <em>eigenes</em> Ergebnis. Der Ergebnistyp pro Item hält Erfolg, Status und — für die Operationen, die sie haben — Revision, Version und TTL dieses einen Keys fest:</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>Es gibt keinen gemeinsamen Commit-Punkt. Ein <code>GetMany</code> über hundert Keys, von denen dreißig nicht existieren, liefert siebzig <code>Success</code>-Ergebnisse und dreißig <code>NotFound</code>-Ergebnisse, nach Index verzahnt — keinen Fehler, keine leere Liste. Ein <code>PutMany</code>, bei dem das Segment eines Keys gerade migriert, liefert dieses eine Item mit Status <code>Moved</code> und den Rest mit <code>Success</code>. Teilerfolg ist die normale, erwartete Form der Antwort, und der Aufrufer soll jedes Item prüfen.</p>
<p>Das ist das genaue Gegenteil einer <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktion</a>, die per Konstruktion alles-oder-nichts ist: ein clientkoordinierter Two-Phase-Commit über optimistische Versionen, bei dem entweder jeder Key sich bewegt oder keiner. Die beiden Features sehen von außen ähnlich aus — beide nehmen viele Keys in einem Aufruf — und sind für entgegengesetzte Bedürfnisse gebaut. Greifen Sie zu einem Batch, wenn die Keys unabhängig sind und Sie Durchsatz wollen; greifen Sie zu einer Transaktion, wenn eine Invariante die Keys umspannt und ein halb angewendetes Ergebnis sie verletzen würde. Zaris hält sie als getrennte APIs, genau damit die Semantik nie mehrdeutig ist: Ein Batch rollt nie stillschweigend zurück, und eine Transaktion committet nie stillschweigend halb.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="überleben-wenn-das-ownership-mitten-im-batch-wandert">Überleben, wenn das Ownership mitten im Batch wandert<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#%C3%BCberleben-wenn-das-ownership-mitten-im-batch-wandert" class="hash-link" aria-label="Direkter Link zur Überleben, wenn das Ownership mitten im Batch wandert" title="Direkter Link zur Überleben, wenn das Ownership mitten im Batch wandert" translate="no">​</a></h2>
<p>Eine Bulk-Operation ist ein breiteres Ziel als ein einzelnes <code>GET</code> für das Problem, dass sich das Ownership darunter ändert. Zwischen dem Moment, in dem der Client einen Key in den Bucket von Node A sortiert, und dem Moment, in dem Node A den Sub-Batch verarbeitet, kann ein <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">Partition-Cut-over</a> dieses Segment zu Node B verschoben haben. Node A beantwortet diese Items dann mit dem Status <code>Moved</code>, statt zu raten.</p>
<p>Der Cluster-Client umhüllt den gesamten Fan-out für genau diesen Fall mit einem begrenzten Retry:</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>Bei einem Retry hat sich die Routing-Tabelle meist aktualisiert, sodass das erneute Bucketing die verschobenen Keys an ihren neuen Owner sendet. Der Retry ist bewusst flach — zwei Versuche, ein kurzes Backoff —, weil das Ziel ist, eine kurzzeitig veraltete Routing-Tabelle zu überbrücken, nicht einen tatsächlich ausgefallenen Node zu übertünchen. Das ist dieselbe Disziplin wie in <a class="" href="https://clustron.io/de/blog/retries-under-ownership-churn/">Retries bei Ownership-Churn</a> beschrieben, angewendet auf eine ganze Key-Menge auf einmal: Der Retry führt die <em>Gruppierung</em> erneut aus, nicht nur das Senden, sodass jeder Versuch das neueste Ownership widerspiegelt statt einen veralteten Plan zu wiederholen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ein-echter-bug-weil-die-wire-form-zwei-gestalten-hat">Ein echter Bug, weil die Wire-Form zwei Gestalten hat<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#ein-echter-bug-weil-die-wire-form-zwei-gestalten-hat" class="hash-link" aria-label="Direkter Link zur Ein echter Bug, weil die Wire-Form zwei Gestalten hat" title="Direkter Link zur Ein echter Bug, weil die Wire-Form zwei Gestalten hat" translate="no">​</a></h2>
<p>Es lohnt sich, eine Stelle zu zeigen, an der dieser Pfad brach, weil sie etwas speziell über Bulk-Operationen aussagt. Der Wert eines <code>GET</code>-Ergebnisses kann auf der Leitung in zwei Formen eintreffen — in einen <code>ZarisEntry</code>-Umschlag gewickelt oder als roher serialisierter Wert —, je nachdem, wie er gespeichert wurde (siehe <a class="" href="https://clustron.io/de/blog/what-crosses-the-wire/">Was über die Leitung geht</a>). Der ursprüngliche Bulk-GET-Decoder nahm nur die Wrapper-Form an und warf bei der rohen eine Exception.</p>
<p>Bei einem einzelnen <code>GET</code> ist das ein Fehler für einen Key. Bei einem <code>GetMany</code> war es auf subtile Weise katastrophal: <em>Jeder</em> Key im Batch schlug fehl, weil die eine unbehandelte Kodierung eine Exception warf, bevor irgendein Ergebnis dekodiert wurde. Ein Zertifizierungslauf markierte es als „Bulk-Get-Fehler", und die Lösung war, die Dekodierung über einen gemeinsamen Helfer mit Doppel-Kodierung zu leiten, der beide Formen behandelt:</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>Die Lehre, die sich verallgemeinern lässt: In einem Bulk-Pfad wird ein Defekt in der <em>gemeinsamen</em> Maschinerie pro Item mit der Batch-Größe multipliziert, und ein Fehler, der für einen Key lästig ist, ist für tausend ein Ausfall. Das Ergebnismodell pro Item begrenzt den Schadensradius — sobald die Dekodierung korrekt ist, scheitert ein fehlerhafter Wert an einem Item und lässt die anderen 999 intakt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-der-fan-out-bringt">Was der Fan-out bringt<a href="https://clustron.io/de/blog/bulk-and-batch-fan-out/#was-der-fan-out-bringt" class="hash-link" aria-label="Direkter Link zur Was der Fan-out bringt" title="Direkter Link zur Was der Fan-out bringt" translate="no">​</a></h2>
<p>Fügt man die Teile zusammen, ist die Form des Gewinns klar. Ein Bulk-Aufruf stempelt jedes Item mit seinem Index, löst den Owner jedes Keys mit derselben Platzierungsmathematik auf, die der Single-Key-Pfad nutzt, sortiert die Keys in Buckets, sodass aus <code>N</code> Operationen eine Anfrage pro besitzendem Node wird, setzt diese Anfragen nebenläufig ab und setzt die Antworten in der Reihenfolge des Aufrufers wieder zusammen — überlebt eine Routing-Tabelle, die sich mitten im Flug verschiebt, und meldet das Schicksal jedes Keys unabhängig. Die Zahl der Round-Trips sinkt von der Zahl der Keys auf die Zahl der Nodes, und das ist die einzige Zahl, auf die es je ankommen konnte.</p>
<p>Nichts davon erforderte einen neuen Speicherpfad oder einen neuen Wire-Record. Die Bulk-APIs sind eine <em>Scheduling</em>-Schicht über den Primitiven, die Zaris bereits hat: die <a class="" href="https://clustron.io/de/blog/client-partition-aware-routing/">partitionsbewusste Platzierung</a>, die dem Client sagt, wer was besitzt, den <a class="" href="https://clustron.io/de/blog/what-crosses-the-wire/">Vertrag pro Record</a>, den jedes Item-Ergebnis wiederverwendet, und die <a class="" href="https://clustron.io/de/blog/reads-scale-with-cores/">kernparallele Ausführung</a>, die einen Sub-Batch durchkaut, sobald er gelandet ist. Der schwere Teil des schnellen Umgangs mit vielen Keys waren nie die Keys — es war die Weigerung, das Netzwerk einen Key nach dem anderen zu bezahlen.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Ein Hop zum Owner: Wie der Zaris-Client einen Key routet]]></title>
            <link>https://clustron.io/de/blog/client-partition-aware-routing/</link>
            <guid>https://clustron.io/de/blog/client-partition-aware-routing/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein Zaris-Client schickt Ihr GET nicht an einen Load Balancer oder einen Proxy, der es weiterleitet. Er berechnet den Owner des Keys selbst — CRC16 über den Hash-Tag, modulo 16384, Slot zu Segment, Segment zum Owner-Node — und öffnet den Socket direkt zu diesem Node. Hier steht der exakte Routing-Pfad, die einzelne Platzierungsfunktion, die Client und Server teilen, und die drei Wege, auf denen der Client seine Map aktuell hält, wenn sich das Ownership unter ihm bewegt.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Wie der Zaris-Client einen Key zu seinem Owner routet" 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>Es gibt zwei ehrliche Wege, einen Client für einen verteilten Cache zu bauen. Der erste: irgendeinen Node wählen, ihm die Anfrage senden und den Cluster sie intern an den Besitzer des Keys weiterleiten lassen — einfach für den Client, aber jede Anfrage, die auf dem falschen Node landet, zahlt für einen zusätzlichen Netzwerk-Hop innerhalb des Clusters. Der zweite: den <em>Client</em> klug genug machen, um zu wissen, welcher Node den Key besitzt, bevor er irgendetwas sendet, sodass der erste Socket, den er berührt, derjenige ist, der tatsächlich antworten kann. Zaris geht den zweiten Weg. Ein <code>GET</code> verlässt Ihren Prozess bereits an den Node adressiert, der den Key hält — kein Proxy, kein Weiterleitungs-Hop, kein Koordinator in der Mitte.</p>
<p>Dieser Beitrag ist der Blick ins Innere, wie das funktioniert: die Funktion, die einen Key in einen Owner verwandelt, die Lookup-Tabelle, die der Client im Speicher hält, und — der Teil, den man leicht falsch macht — wie der Client korrekt bleibt, wenn sich das Ownership bewegt, während Ihr Traffic unterwegs ist.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-behauptung-präzise-formuliert">Die Behauptung, präzise formuliert<a href="https://clustron.io/de/blog/client-partition-aware-routing/#die-behauptung-pr%C3%A4zise-formuliert" class="hash-link" aria-label="Direkter Link zur Die Behauptung, präzise formuliert" title="Direkter Link zur Die Behauptung, präzise formuliert" translate="no">​</a></h2>
<p>Wenn Sie <code>client.GetAsync("user:42")</code> aufrufen, tut der Client drei Dinge, bevor irgendwelche Bytes das Netzwerk berühren:</p>
<ol>
<li class=""><strong>Den Key auf ein Segment hashen.</strong> Eine reine Funktion bildet den Key-String auf eines der Segmente des Clusters ab.</li>
<li class=""><strong>Den Owner des Segments nachschlagen.</strong> Ein In-Memory-Array bildet dieses Segment direkt auf die eine Verbindung ab, die den aktiven Owner-Node erreicht.</li>
<li class=""><strong>Owner-direkt senden.</strong> Die Anfrage geht über diese Verbindung und nirgendwo sonst hin.</li>
</ol>
<p>Kein Schritt befragt einen zentralen Router. Schritt 1 ist Arithmetik, die Schritte 2 und 3 sind ein Array-Index und ein Socket-Schreibvorgang. Der Client hält eine Verbindung pro Node und eine kleine Routing-Tabelle, und das ist die gesamte Apparatur. Das Interessante an der Ingenieursarbeit ist nicht der Happy Path — es ist, diese Tabelle ehrlich zu halten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-1--der-key-wird-zu-einem-segment">Schritt 1 — der Key wird zu einem Segment<a href="https://clustron.io/de/blog/client-partition-aware-routing/#schritt-1--der-key-wird-zu-einem-segment" class="hash-link" aria-label="Direkter Link zur Schritt 1 — der Key wird zu einem Segment" title="Direkter Link zur Schritt 1 — der Key wird zu einem Segment" translate="no">​</a></h2>
<p>Zaris hasht Keys absichtlich genau so wie ein Redis-Cluster-Client. Die Platzierungsfunktion lebt in <code>SegmentMapper</code>, und ihr Doc-Kommentar ist unverblümt darüber, warum sie so aussieht, wie sie aussieht:</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>Diese letzte Zeile ist die wichtigste. Es heißt nicht „der Client berechnet die Platzierung auf die eine Weise und der Server validiert sie auf eine andere." Es ist <em>derselbe Code</em>, in die Client-Assembly und die Server-Assembly kompiliert, der jedes Mal dasselbe Segment für denselben Key erzeugt. Wären sich Client und Server uneinig, wo ein Key lebt, wäre jeder Schreibzugriff ein Münzwurf zwischen der Landung beim Owner und der Landung auf einem Node, der ihn ablehnen muss. Das Teilen der Funktion macht diese Klasse von Bugs nicht darstellbar.</p>
<p>Die Berechnung selbst besteht aus zwei Zügen:</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>Zuerst wird der Key mit XMODEM-CRC16 auf einen <strong>Slot</strong> im festen Bereich <code>[0, 16384)</code> reduziert — dasselbe CRC16-CCITT-Polynom (<code>0x1021</code>, Init <code>0x0000</code>), das Redis verwendet. Dann werden die 16384 Slots proportional auf so viele Segmente abgebildet, wie der Cluster tatsächlich hat:</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>Zwei Slots können im selben Segment landen; ein Segment ist ein zusammenhängender Bereich von Slots. Diese Ausrichtung erlaubt es Zaris, <code>CLUSTER SLOTS</code> für einen Redis-Cluster-Client korrekt zu beantworten und intern dennoch nach seinem eigenen Segmentmodell zu routen — die beiden Sichten sind dieselbe Sicht in unterschiedlicher Auflösung.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hash-tags-entscheiden-was-co-lokalisiert-wird">Hash-Tags: entscheiden, was co-lokalisiert wird<a href="https://clustron.io/de/blog/client-partition-aware-routing/#hash-tags-entscheiden-was-co-lokalisiert-wird" class="hash-link" aria-label="Direkter Link zur Hash-Tags: entscheiden, was co-lokalisiert wird" title="Direkter Link zur Hash-Tags: entscheiden, was co-lokalisiert wird" translate="no">​</a></h3>
<p>Im Key-Hashing gibt es eine bewusste Hintertür. Enthält ein Key eine <code>{...}</code>-Gruppe mit mindestens einem Zeichen zwischen den Klammern, werden nur die Bytes innerhalb der Klammern gehasht:</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>So erzwingen Sie, dass zusammengehörige Keys auf demselben Segment landen. <code>{u1}:profile</code> und <code>{u1}:sessions</code> hashen beide auf <code>u1</code>, teilen also ein Segment und damit einen Owner — genau das, was Sie für eine <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktion</a> oder jede Operation brauchen, die mehrere Keys atomar auf einem Node berühren muss. Keys ohne Tag verteilen sich über den gesamten Keyspace. Auch hierin sind sich Client und Server einig, denn auf beiden Seiten ist es dasselbe <code>HashTagRange</code>.</p>
<p>Ein weiteres Detail für den Hot Path: Die CRC16-Schleife hat einen reinen ASCII-Schnellpfad, der die Zeichen ohne Allokation als Bytes hasht und nur für Nicht-ASCII-Keys auf ein UTF-8-Encoding zurückfällt. Jede Operation zu routen heißt, dass dies bei jeder Operation läuft, daher wurde es so geschrieben, dass es im Normalfall nicht allokiert.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-2--das-segment-wird-zu-einer-verbindung">Schritt 2 — das Segment wird zu einer Verbindung<a href="https://clustron.io/de/blog/client-partition-aware-routing/#schritt-2--das-segment-wird-zu-einer-verbindung" class="hash-link" aria-label="Direkter Link zur Schritt 2 — das Segment wird zu einer Verbindung" title="Direkter Link zur Schritt 2 — das Segment wird zu einer Verbindung" translate="no">​</a></h2>
<p>Jetzt hat der Client eine Segmentnummer. Er braucht den Node, der es gerade besitzt. Der Client hält eine unveränderliche <code>RoutingTable</code>, deren Felder die ganze Geschichte erzählen:</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>Die zentrale Designentscheidung ist <code>SegmentMap</code>. Es ist ein nach Segment indiziertes Array, dessen Einträge <em>bereits aufgelöst</em> sind auf die Verbindung zum Owner dieses Segments. Einen Key zu routen ist kein Graphdurchlauf von Segment über Partition und Node zur Verbindung zur Request-Zeit — diese Auflösung geschieht im Voraus, wenn die Tabelle gebaut wird, sodass die Kosten pro Request ein einziger Array-Index sind:</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> hält genau einen <code>ZarisRemoteClient</code> — eine Verbindung — pro Node. Der Client öffnet keinen frischen Socket pro Request oder pro Key; er multiplext den gesamten Traffic für einen Node über dessen einzelne Verbindung, und <code>SegmentMap</code> zeigt mit vielen Segmenten einfach auf dasselbe Verbindungsobjekt (Referenzgleichheit dedupliziert sie, wenn der Client die Menge der verschiedenen aktiven Owner braucht, zum Beispiel für den Fan-out eines <a class="" href="https://clustron.io/de/blog/scanning-a-moving-keyspace/">Scans</a>). Die Speicherkosten der Partitionsbewusstheit sind also eine Verbindung pro Node plus ein paar Arrays in Größe der Segmentanzahl — sie wachsen nicht mit der Zahl der Keys oder Requests.</p>
<!-- -->
<p>Der Gewinn: Es gibt keinen Node im Cluster, dessen Aufgabe es ist, fehlgeroutete Requests entgegenzunehmen und weiterzuleiten. Jeder gut geroutete Request wird vom ersten Node bedient, den er erreicht. Das entfernt eine ganze Ebene aus dem Latenzbudget und, ebenso wichtig, aus der Fehlerfläche — man kann keinen Request in einem Weiterleitungs-Hop verlieren, den es nicht gibt. (Der Begleitbeitrag zum <a class="" href="https://clustron.io/de/blog/latency-floor/">Latenzboden</a> geht durch, was im Budget <em>übrig</em> bleibt, sobald der Proxy-Hop weg ist.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-3--die-map-bewegt-sich-unter-ihnen">Schritt 3 — die Map bewegt sich unter Ihnen<a href="https://clustron.io/de/blog/client-partition-aware-routing/#schritt-3--die-map-bewegt-sich-unter-ihnen" class="hash-link" aria-label="Direkter Link zur Schritt 3 — die Map bewegt sich unter Ihnen" title="Direkter Link zur Schritt 3 — die Map bewegt sich unter Ihnen" translate="no">​</a></h2>
<p>Hier kommt der schwere Teil. Die Routing-Tabelle ist ein Cache einer Tatsache — „Segment S gehört Node N" —, die der Cluster jederzeit ändern kann. Ein <a class="" href="https://clustron.io/de/blog/anatomy-of-a-failover/">Owner kann ausfallen</a>, und eine Replica wird befördert; eine <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">Partition kann migriert werden</a>, um einen Node zu vergrößern oder zu leeren. Sobald das geschieht, zeigt <code>SegmentMap[S]</code> des Clients auf eine Verbindung, die nicht mehr der Owner ist. Ein Client, der seiner Tabelle blind vertraute, würde Schreibzugriffe an einen Node senden, der sie korrekterweise ablehnt.</p>
<p>Zaris hält die Tabelle auf drei Wegen ehrlich, in der Reihenfolge der Präferenz.</p>
<p><strong>Push — der Server sagt es dem Client.</strong> Ändert sich das Ownership, sendet der Cluster ein <code>RuntimeOwnershipChangedEvent</code>, und der Client hat einen Handler, der es abonniert hat:</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> baut nicht die ganze Tabelle neu. Es klont das <code>SegmentMap</code>-Array, setzt nur die Segmente um, deren Partition tatsächlich den Owner gewechselt hat, und tauscht die unveränderliche Tabelle mit einer einzigen Referenzzuweisung aus. Leser sehen nie eine halb aktualisierte Tabelle — sie lesen entweder die alte oder die neue, nie eine zerrissene Mischung. Das ist Copy-on-Write-Routing: Der Normalfall (nichts hat sich geändert) kostet nichts, und eine Änderung berührt nur die betroffenen Segmente.</p>
<p><strong>Poll — ein langsamer Backstop.</strong> Ein Timer aktualisiert die Cluster-Map unabhängig davon alle zehn Sekunden, sodass sich die Sicht des Clients selbst dann innerhalb eines Intervalls heilt, wenn ein Push-Ereignis verpasst wird (ein Reconnect, eine verlorene Benachrichtigung):</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 — der Request selbst korrigiert die Map.</strong> Die letzte Verteidigungslinie ist der Request, der falsch routet. Sendet der Client an einen Node, der nicht mehr der Owner ist, leitet der Node ihn nicht stillschweigend weiter — er liefert einen Status zurück, der dem Client sagt, dass seine Map veraltet ist, und der Retry-Helfer aktualisiert und versucht es erneut:</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>Kommt eine wiederholbare Operation mit <code>Moved</code>, <code>OwnershipChanging</code> oder <code>ReplicaWriteRejected</code> zurück, ruft der Client seinen Refresh-Hook auf und versucht es erneut — eine begrenzte Schleife mit einigen Versuchen und kurzem Backoff, kein offenes Drehen im Kreis. Im aktuellen Code steckt eine subtile Lehre: Eine frühere Version aktualisierte die Map bei <em>jedem</em> Retry-Versuch, was bei starkem Node-Churn eine einzelne fehlschlagende Operation in Dutzende Aktualisierungen verwandelte, die alle hinter einem Lock serialisiert waren. Die Lösung war, nur dann zu aktualisieren, wenn der Status tatsächlich sagt „Ihre Map ist falsch", nicht schon deshalb, weil ein Versuch fehlschlug. Die tiefere Behandlung dieser Retry-Disziplin ist ein eigener Beitrag — <a class="" href="https://clustron.io/de/blog/retries-under-ownership-churn/">Retries bei Ownership-Churn</a> —, aber die Kurzfassung lautet: <em>Eine veraltete Map ist ein selbstkorrigierender Zustand, und die Korrektur ist gerade deshalb billig, weil sie selten und gezielt ist.</em></p>
<p>Die drei Mechanismen schichten sich: Push macht den Normalfall sofortig, Poll macht einen verpassten Push letztlich konsistent, und der reaktive Retry macht jede verbleibende Veraltung für Ihren Aufruf unsichtbar — er kostet im Moment des Ownership-Wechsels einen zusätzlichen Round-Trip und ist ab dem nächsten Aufruf korrekt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-überhaupt-auf-dem-client">Warum überhaupt auf dem Client?<a href="https://clustron.io/de/blog/client-partition-aware-routing/#warum-%C3%BCberhaupt-auf-dem-client" class="hash-link" aria-label="Direkter Link zur Warum überhaupt auf dem Client?" title="Direkter Link zur Warum überhaupt auf dem Client?" translate="no">​</a></h2>
<p>Partitionsbewusstes Client-Routing kostet etwas. Der Client muss ein Modell des Clusters tragen, es aktuell halten und dieselbe Platzierungslogik mitliefern wie der Server. Ein Proxy-basiertes Design schiebt all das in den Cluster und lässt den Client dumm bleiben. Warum also zahlen?</p>
<p>Weil die Alternative bei jedem einzelnen Request für immer zahlt. Ein Proxy oder ein Design „an irgendwen senden, wir leiten es weiter" fügt dem Hot Path jeder Operation einen Hop hinzu — einen Hop, der reiner Overhead ist, selbst wenn nichts ausgefallen ist und nichts sich bewegt. Zaris zahlt die Kosten des Routings <em>einmal, im Voraus, und nur dann wieder, wenn sich das Ownership tatsächlich ändert</em> — was in einem gesunden Cluster fast nie vorkommt. Der Normalzustand ist ein Array-Index und ein Socket-Schreibvorgang zum richtigen Node. Das ist der ganze Grund, warum <a class="" href="https://clustron.io/de/blog/reads-scale-with-cores/">Lesezugriffe mit Ihren Kernen skalieren</a>, statt an einer mittleren Ebene zum Flaschenhals zu werden: Es gibt keine mittlere Ebene, die zum Flaschenhals werden könnte.</p>
<p>Und es lässt sich mit allem anderen kombinieren. Dieselbe Auflösung <code>GetClientForKey</code> ist das, worüber ein <a class="" href="https://clustron.io/de/blog/redis-keyspace-notifications/">Key-Watch</a>, ein Counter-Inkrement, ein <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Compare-and-Swap</a> und eine Datenstruktur-Operation auf einem einzelnen Key alle routen, sodass sie alle die Owner-direkte Zustellung und dieselbe selbstheilende Map erben. Der Client ist keine dumme Leitung mit angeschraubtem Routing; das Routing ist das, was der Client ist.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-ehrlichen-kompromisse">Die ehrlichen Kompromisse<a href="https://clustron.io/de/blog/client-partition-aware-routing/#die-ehrlichen-kompromisse" class="hash-link" aria-label="Direkter Link zur Die ehrlichen Kompromisse" title="Direkter Link zur Die ehrlichen Kompromisse" translate="no">​</a></h2>
<p>Owner-direktes Routing ist nicht frei von Kanten, und es lohnt sich, sie zu benennen.</p>
<ul>
<li class=""><strong>Der Client muss jeden Node erreichen können.</strong> Da es keinen Proxy gibt, öffnet der Client eine Verbindung zu jedem Node, zu dem er routet. In einem flachen Netzwerk ist das kein Problem; über eine Grenze hinweg — sagen wir, ein Client außerhalb von Kubernetes — müssen diese Nodes einzeln adressierbar sein. Genau dieses Problem soll die <a class="" href="https://clustron.io/de/blog/kubernetes-external-clients/">Adressierung externer Clients</a> lösen, und es ist eine direkte Folge davon, dass es keine einzelne Eingangstür gibt.</li>
<li class=""><strong>Eine kurze Map-Verzögerung ist möglich, kein Verlust.</strong> In dem Fenster zwischen dem Wandern des Ownerships und dem Erfahren des Clients kann ein Request zum alten Owner routen. Er wird abgelehnt und wiederholt, nicht falsch angewendet — der reaktive Pfad oben garantiert, dass der Retry beim echten Owner landet. Sie zahlen einen Round-Trip, keine falsche Antwort.</li>
<li class=""><strong>Die Platzierungsfunktion ist ein Kompatibilitätsvertrag.</strong> Weil Client und Server <code>SegmentMapper</code> teilen, können Sie nicht ändern, wie Keys auf Segmente gehasht werden, ohne dass beide Seiten zustimmen — genau deshalb wurde das Hashing am stabilen Redis-Slot-Modell ausgerichtet statt an einem internen Schema, das driften könnte.</li>
</ul>
<p>Klar gesagt: Der Zaris-Client weiß, wo Ihre Keys leben, hält dieses Wissen aktuell, ohne dass Sie daran denken müssen, und verbraucht für jede Operation einen Netzwerk-Hop — den, der tatsächlich die Arbeit erledigt. Die Raffinesse liegt nicht im schnellen Pfad, der bewusst langweilig ist. Sie liegt in den drei stillen Mechanismen, die den schnellen Pfad korrekt halten, während sich der Cluster unter ihm neu formt.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>.NET</category>
            <category>Performance</category>
        </item>
        <item>
            <title><![CDATA[Ein Owner, der neu startet, hat kein Recht auf seine eigene Leere]]></title>
            <link>https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/</link>
            <guid>https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein Node, der eine Partition besaß, stirbt und kommt mit einem leeren Store zurück. Der gefährliche Instinkt ist, dieser Leere zu vertrauen. Hier steht, wie Zaris einen neu gestarteten Owner aus seinen Live-Replicas wiederaufbaut — ein klebriges Certified-Flag, das einem frischen Owner jede Autorität verweigert, leer zu sein, ein Reverse-Sync-Pull, ein Serve-Gate, das die Antwort verweigert, bis der lokale Op-Log-Head den der Quelle einholt, und ein nicht-destruktiver Merge, der keine Daten überschreibt, die eine Replica angesammelt hat, während der Owner weg war.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Einen neu gestarteten Owner aus seinen Replicas wiederaufbauen" 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>Ein <a class="" href="https://clustron.io/de/blog/anatomy-of-a-failover/">Failover</a> deckt den Moment ab, in dem ein Owner stirbt: Der Cluster wählt einen Überlebenden und bedient weiter. Eine <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">geplante Migration</a> deckt das absichtliche Verschieben der Partition eines gesunden Owners ab. Dieser Beitrag handelt vom dritten Akt, dem, der nach dem Ende der Sirenen abläuft — der ursprüngliche Owner-Prozess startet neu, tritt dem Cluster wieder bei und will seine Partition zurück. Sein Store, der nichts auf der Platte und alles im Speicher hält, kam <strong>leer</strong> zurück. Die Daten, für die er verantwortlich ist, leben noch, aber auf den Replicas, die weiter bedient haben, während er ausgefallen war.</p>
<p>Das gesamte Korrektheitsproblem steckt in einem Satz: <em>Dem neu gestarteten Owner darf nicht erlaubt werden, seiner eigenen Leere zu trauen.</em> Ein leerer Store sieht genau so aus wie ein Store, den jemand bewusst geleert hat. Nimmt der Node an „ich bin der Owner und habe keine Daten, also ist diese Partition leer", wird er bereitwillig <code>NotFound</code> für Millionen lebender Keys ausliefern und, schlimmer, seinen leeren Zustand zurück auf die Replicas fließen lassen und sie löschen. Dieser Beitrag geht die fünf kleinen, reinen Policies durch, mit denen Zaris sicherstellt, dass das nie geschieht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-leere-von-innen-nicht-entscheidbar-ist">Warum Leere von innen nicht entscheidbar ist<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#warum-leere-von-innen-nicht-entscheidbar-ist" class="hash-link" aria-label="Direkter Link zur Warum Leere von innen nicht entscheidbar ist" title="Direkter Link zur Warum Leere von innen nicht entscheidbar ist" translate="no">​</a></h2>
<p>Beginnen wir mit der Falle, denn jede Regel unten ist von ihr geprägt. Ein Node, der hochfährt und null Records für ein Segment hält, das er besitzt, befindet sich in einem von zwei Zuständen, und <strong>er kann allein aus lokalen Informationen nicht sagen, in welchem</strong>:</p>
<ol>
<li class="">Er ist leer, weil er gerade neu gestartet ist und seine Daten nie zurückgeholt hat. Er <em>muss</em> pullen.</li>
<li class="">Er ist leer, weil er diese Keys maßgeblich gelöscht hat oder sie abgelaufen sind, und eine Replica, die kurz ausgefallen war, hält noch die veralteten Kopien. Pullt er, <em>lässt er gelöschte Daten wiederauferstehen</em>.</li>
</ol>
<p>Diese beiden sind lokal ununterscheidbar — der Store ist in beiden Fällen leer. Eine frühere Version von Zaris versuchte, das mit einem <code>EmptyActive</code>-Anomaliesignal aufzulösen, und es wurde gerade deshalb entfernt, weil das Signal eine Vermutung ist. Man kann nicht fragen „bin ich aus gutem Grund leer?" und von dem leeren Node eine vertrauenswürdige Antwort erhalten.</p>
<p>Zaris fragt die Leere daher gar nicht erst. Es leitet die Entscheidung über eine andere Tatsache: ein klebriges Flag auf Partitionsebene namens <strong>certified</strong>, das genau bedeutet: „Mein Startup-Sync ist abgeschlossen, und mein Store ist nun maßgeblich." Ein Node setzt es erst, nachdem er den Besitz <em>nachgewiesen</em> hat. Ein frisch neu gestarteter Owner ist per Definition <strong>nicht certified</strong> — und ein nicht zertifizierter Owner hat keine Autorität, leer zu sein. Er muss von dem pullen, der die Daten hält. Ein zertifizierter Owner ist maßgeblich und wird strikt in Ruhe gelassen, sodass ein legitimes Leeren nie wiederaufersteht.</p>
<p>Diese eine Umdeutung — von <em>Leere</em> (unentscheidbar) zu <em>Zertifizierung</em> (eine nachgewiesene, klebrige Tatsache) — macht den Rest der Wiederherstellung sicher.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-1--der-neu-gestartete-owner-pullt-statt-anzunehmen">Schritt 1 — der neu gestartete Owner pullt, statt anzunehmen<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#schritt-1--der-neu-gestartete-owner-pullt-statt-anzunehmen" class="hash-link" aria-label="Direkter Link zur Schritt 1 — der neu gestartete Owner pullt, statt anzunehmen" title="Direkter Link zur Schritt 1 — der neu gestartete Owner pullt, statt anzunehmen" translate="no">​</a></h2>
<p>Wenn der Node wieder beitritt, weist die Map ihn weiterhin als Owner seiner alten Partition aus. Der Reverse-Sync-Trigger entscheidet, ob jedes besessene Segment <em>von einer Live-Replica gepullt</em> werden soll, statt als lokal maßgeblich behandelt zu werden:</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>Drei Dinge an diesem winzigen Prädikat sind tragend, und jedes ist ein Bug, der einen Zertifizierungslauf erreichte, bevor die Regel aufgeschrieben wurde.</p>
<p><strong>Es ignoriert <code>isMapPreferredPrimary</code>.</strong> Dieser Parameter wird weiterhin übergeben — als dokumentierter Entscheidungskontext —, aber die Entscheidung liest ihn bewusst nicht. Eine frühere Version löste Reverse-Sync nur aus, wenn der Node <em>zusätzlich</em> der bevorzugte Primary der Map war. Ein Node, der zur Laufzeit aktiv, aber <em>nicht</em> der Map-Primary ist — genau der Zustand, in dem ein neu verorteter oder nach der Wiederherstellung beförderter Owner landet —, hatte seine besessenen Segmente komplett übersprungen. Er forderte seine Daten nie an und blieb für immer leer gestrandet, während eine Live-Replica jeden Record hielt (ein Cert-Lauf: Active hielt <code>0</code>, Replica hielt <code>4600</code> auf fünfzehn Segmenten, über acht Minuten nie konvergiert). Das Ownership des Daten-Pulls darf nicht daran hängen, der <em>bevorzugte</em> Primary zu sein; es muss für jeden nicht zertifizierten Active feuern.</p>
<p><strong>Das Signal ist <code>!partitionCertified</code>, nicht Leere.</strong> Das ist die Lösung der Unentscheidbarkeit aus dem vorigen Abschnitt, als Code. Ein nicht zertifizierter Active hat den Besitz nie nachgewiesen, also pullt er; ein zertifizierter ist maßgeblich und wird übersprungen.</p>
<p><strong><code>liveNonSelfReplicaExists</code> ist ein Guard, keine Nettigkeit.</strong> Ist die konfigurierte Replica <em>ausgefallen</em>, gibt es nichts, wovon man synchronisieren könnte. Die Wiederherstellung gegen eine tote Quelle zu treiben dreht sich ewig im Kreis (<code>RECOVERY STUCK source=self synced=0/128</code>). Existiert also keine Live-Replica, ist der Active die einzige überlebende Kopie — er <em>ist</em> die Autorität und zertifiziert sofort, statt auf eine Quelle zu warten, die nie antworten wird. Der Pull selbst ist durch Revisionen abgesichert und durch ein Watermark begrenzt, was ihn zu einem harmlosen No-op macht, wenn die Replica zufällig nichts hält.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-2--einen-key-nicht-bedienen-bei-dem-man-noch-nicht-aufgeholt-hat">Schritt 2 — einen Key nicht bedienen, bei dem man noch nicht aufgeholt hat<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#schritt-2--einen-key-nicht-bedienen-bei-dem-man-noch-nicht-aufgeholt-hat" class="hash-link" aria-label="Direkter Link zur Schritt 2 — einen Key nicht bedienen, bei dem man noch nicht aufgeholt hat" title="Direkter Link zur Schritt 2 — einen Key nicht bedienen, bei dem man noch nicht aufgeholt hat" translate="no">​</a></h2>
<p>Das Pullen ist nicht augenblicklich, und das Fenster, während es läuft, ist der Ort, an dem bestätigte Lesezugriffe stillschweigend zu <code>NotFound</code> werden. Ein naives Gate ist ein einzelner Boolean, <code>IsSynced</code>, der bedeutet „ein Sync-Durchlauf ist beendet." Dieser Boolean reicht nicht, und drei getrennte Cert-Fehler bewiesen es: Ein zurückfordernder Active wurde 34 Keys zu kurz befördert; ein Handoff verlor einen bestätigten Key; ein wieder beitretender Active lieferte während des Churns für 57 Keys <code>NotFound</code>. In jedem Fall <em>war</em> ein Sync-Durchlauf beendet — aber die Quelle (der temporäre Active, der weiter bediente) hatte inzwischen mehr Schreibzugriffe akzeptiert, die der wiederherstellende Node nicht gepullt hatte. <code>IsSynced</code> war true, und der Node hinkte trotzdem hinterher.</p>
<p>Die Lösung ist, an einer <em>Position</em> zu gaten, nicht an einem Flag. Die Op-Log-Revision ist eine replizierte, nodeübergreifend vergleichbare Zahl: Der Active vergibt sie und verschickt sie, sodass eine aufgeholte Replica zum selben Head konvergiert. Das Serve-Gate vergleicht Heads:</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> ist der neue Zustand, auf den es ankommt: Ein Sync-Durchlauf wurde beendet, aber der Head der Quelle (bei diesem Sync erfasst) liegt vor unserem, die Quelle hat also Schreibzugriffe akzeptiert, die wir nicht gepullt haben. Jetzt zu bedienen würde für genau diese Keys <code>NotFound</code> liefern. Der Node hält an, bis sein lokaler Head das Watermark erreicht. Beachten Sie den bewussten Rückfall: Ein aufgezeichnetes Watermark von <code>0</code> bedeutet „keine Watermark-Information", und das Gate fällt auf das reine <code>IsSynced</code>-Verhalten zurück, statt für immer an fehlenden Daten zu blockieren. Die Regel blockiert nie an der Abwesenheit einer Tatsache — nur an einer Tatsache, die sagt: <em>Sie hinken hinterher.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-3--pullen-aber-nicht-überschreiben">Schritt 3 — pullen, aber nicht überschreiben<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#schritt-3--pullen-aber-nicht-%C3%BCberschreiben" class="hash-link" aria-label="Direkter Link zur Schritt 3 — pullen, aber nicht überschreiben" title="Direkter Link zur Schritt 3 — pullen, aber nicht überschreiben" translate="no">​</a></h2>
<p>Nun die gefährliche Richtung: den Snapshot anwenden, den die Replica zurückschickt. Der Instinkt ist, den lokalen Store zu leeren und den Snapshot im Ganzen hineinzukopieren. Dieser Instinkt ist genau zur Hälfte richtig und zur anderen Hälfte katastrophal falsch. Die entscheidende Frage ist, <em>wessen Revisionen mit wessen vergleichbar sind</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>Drei Ergebnisse:</p>
<ul>
<li class=""><strong><code>Skip</code></strong> — die Content-Digests stimmen bereits überein. Nichts zu übertragen, nichts anzuwenden. (Siehe <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">das Cut-over-Protokoll</a>, warum ein Content-Digest und keine Revisionszählung das ist, was sich zu vergleichen lohnt.)</li>
<li class=""><strong><code>Reconcile</code></strong> (nicht-destruktiv) — der lokale Node ist ein <em>rein passiver Subordinate</em> des einen Live-Actives. Seine Revisionen pro Key stammen alle von diesem einen Active und sind daher direkt vergleichbar. Der Snapshot wird revisionsgesichert gemergt: Ein <code>PutRaw</code> behält den lokal neueren Record und füllt fehlende Keys auf; Tombstones werden über <code>DeleteRaw</code> angewendet. Entscheidend: Dies bewahrt Schreibzugriffe, die die Replica über den Live-Kanal während eines langsamen Bootstraps angesammelt hat, statt sie durch vorheriges Leeren wegzuwerfen.</li>
<li class=""><strong><code>Wipe</code></strong> (destruktives Leeren + Kopieren) — der lokale Node <em>ist oder war selbst</em> ein Active für diese Partition. Zwei unabhängig aktive Nodes haben <strong>unvergleichbare</strong> Revisionen pro Key: Dieselbe Revisionsnummer bezeichnet auf beiden unterschiedliche Schreibzugriffe. Sie zu mergen würde stillschweigend zwei Historien verzahnen. Der einzig sichere Schritt ist also, den lokalen Zustand zu verwerfen und den maßgeblichen Snapshot im Ganzen zu übernehmen.</li>
</ul>
<p>Der Default ist die sichere destruktive Kopie; <code>Reconcile</code> wird <em>nur</em> freigeschaltet, indem nachgewiesen wird, dass der Node ein rein passiver Subordinate ist. Das ist der „Wipe-Guard": Er verhindert den gefährlichen Merge über zwei frühere Actives hinweg und verhindert symmetrisch, dass ein Komplett-Wipe gute Live-Kanal-Schreibzugriffe auf einer einfachen Replica verwirft.</p>
<p>In <code>priorReconcileShortfall</code> steckt noch eine Falte, die direkt aus einem Abbruch der Zwölf-Stunden-Zertifizierung stammt. Ein revisionsgesicherter Reconcile kann Records <em>aussperren</em>, die er hätte anwenden sollen, und abschließen, während er noch hinter dem maßgeblichen Snapshot liegt — ein Segment hing bei <code>12643/15435</code> über 232 Sync-Sessions fest und stimmte jedes Mal dieselben Records aus dem Fenster ab, in einer Endlosschleife. Das Flag sagt: Wenn ein früherer Reconcile abgeschlossen wurde, uns aber zu kurz hielt, hör auf zu reconcilen und eskaliere zu einem Wipe, damit der eingefrorene Snapshot im Ganzen übernommen wird und wir tatsächlich konvergieren. Es wird nur <em>nach einem gemessenen Defizit</em> gesetzt — eine vorauseilende oder gleichstehende Replica löst es nie aus, sodass Live-Kanal-Schreibzugriffe nie verworfen werden, um einem Phantom nachzujagen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-4--die-andere-seite-ein-nicht-zertifizierter-owner-kann-trotzdem-eine-gute-quelle-sein">Schritt 4 — die andere Seite: ein nicht zertifizierter Owner kann trotzdem eine gute Quelle sein<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#schritt-4--die-andere-seite-ein-nicht-zertifizierter-owner-kann-trotzdem-eine-gute-quelle-sein" class="hash-link" aria-label="Direkter Link zur Schritt 4 — die andere Seite: ein nicht zertifizierter Owner kann trotzdem eine gute Quelle sein" title="Direkter Link zur Schritt 4 — die andere Seite: ein nicht zertifizierter Owner kann trotzdem eine gute Quelle sein" translate="no">​</a></h2>
<p>Bei der Wiederherstellung geht es nicht nur um den Node, der hinterherhinkt. Betrachten Sie den Spiegelfall: Node A startet neu, und Node B — eine Replica, die ausgefallen war — will nun <em>von</em> A synchronisieren. Soll A die Anfrage bedienen, solange A selbst noch nicht zertifiziert ist?</p>
<p>Der naive Guard sagt nein: „nicht zertifiziert heißt, mein Store ist leer, ein Bedienen würde B's gültige Daten mit meiner Lücke überschreiben." Aber diese Prämisse ist in dem Moment falsch, in dem A die maßgebliche Kopie tatsächlich <em>hält</em> — was auf zwei Wegen geschieht: A war ein bevorzugter Primary, der vor der Beförderung den Catch-up verifizierte, dessen Beförderungspfad aber nie <code>CertifyPartition</code> aufrief (das klebrige Flag bleibt also false, obwohl die Daten alle da sind), oder A übernahm das Ownership über eine Wiederherstellung nach Totalausfall der Partition und hält als Active die vollständigen Daten, ohne je als <code>IsSynced</code> gestempelt worden zu sein. In beiden Fällen <em>ist</em> A's Kopie die Quelle der Wahrheit, und die Verweigerung ließ die Replica bei null gestrandet zurück (ein Dump: 1.792 verweigerte Requests; ein anderer: alle 128 Segmente für eine ganze Partition verweigert).</p>
<p>Der Guard auf der Quellseite wird daher datenbewusst gemacht:</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>Verweigert wird <strong>nur</strong>, wenn der Node der Owner ist, nicht zertifiziert ist <em>und</em> das Segment sowohl unsynchronisiert ist als auch keine Daten hält — der eine echte Fall „mitten in der Wiederherstellung und tatsächlich leer", in dem das Bedienen einer Lücke den Anfragenden gegen sie zertifizieren lassen könnte. Da der Empfänger jeden Chunk revisionsgesichert anwendet, ist das Bedienen echter Daten immer sicher; der Guard beschränkt die Verweigerung auf den einen Zustand, in dem es nichts Sicheres zu bedienen gibt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-5--den-sieg-erklären-genau-einmal">Schritt 5 — den Sieg erklären, genau einmal<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#schritt-5--den-sieg-erkl%C3%A4ren-genau-einmal" class="hash-link" aria-label="Direkter Link zur Schritt 5 — den Sieg erklären, genau einmal" title="Direkter Link zur Schritt 5 — den Sieg erklären, genau einmal" translate="no">​</a></h2>
<p>Schließlich hat der wiederherstellende Active jedes besessene Segment gepullt und jedes auf den Head seiner Quelle aufgeholt. Er ist wieder maßgeblich und muss das <em>sagen</em> — das klebrige Certified-Flag umlegen —, damit der Reverse-Sync-Trigger aus Schritt 1 aufhört, seine Segmente erneut einzureihen, und der Cluster die Partition als konvergiert registriert.</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>Die Zertifizierung ist der symmetrische Abschluss des Triggers: Der Trigger reiht die Segmente eines nicht zertifizierten Actives zum Pullen ein; dies hier legt das Flag um, sobald <em>jedes</em> besessene Segment synchronisiert ist, und die Partition beruhigt sich. Der bisherige Certify-Pfad lief nur für den bevorzugten Primary der Map, sodass ein neu verorteter <em>Nicht-Primary</em>-Active zwar alle seine Daten korrekt pullte und dann nie zertifizierte — er reihte Reverse-Sync endlos neu ein, und der Cluster registrierte sich nie als konvergiert (ein Lauf: 30 Checkpoints liefen ~106 Sekunden nach dem Start in den Timeout, während <code>Missing=0</code> und die Zählungen nie auseinanderliefen — die Daten waren <em>richtig</em>, nur das Zur-Ruhe-Kommen fehlte).</p>
<p>Und beachten Sie die eine Abkürzung, die diese Policy bewusst <strong>verweigert</strong>: Es gibt keinen Pfad „keine Live-Replica, also zertifiziere ich meine Leere als Autorität" für einen Nicht-Primary-Active. Ein Nicht-Primary-Active ist nicht die Autorität der Map; ihn leer zu zertifizieren, während eine vorübergehend ausgefallene Replica die Daten noch hält, würde dieser Replica erlauben, später <em>von</em> diesem leeren Node zu synchronisieren und alles zu verlieren. Die Autorität, leer zu sein, gehört nur dem bevorzugten Primary der Map, in seinem eigenen Reconcile-Pfad. Überall sonst bringt Leere nichts ein.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-form-des-ganzen">Die Form des Ganzen<a href="https://clustron.io/de/blog/rebuilding-an-owner-from-replicas/#die-form-des-ganzen" class="hash-link" aria-label="Direkter Link zur Die Form des Ganzen" title="Direkter Link zur Die Form des Ganzen" translate="no">​</a></h2>
<p>Tritt man zurück, sind die fünf Policies eine Idee an fünf Stellen angewendet: <strong>Besitz wird nachgewiesen, nie angenommen.</strong></p>
<ul>
<li class="">Ein neu gestarteter Owner ist <strong>nicht zertifiziert</strong>, also pullt er, statt seinem leeren Store zu trauen (Schritt 1).</li>
<li class="">Er <strong>bedient</strong> ein Segment nicht, bevor sein Op-Log-Head den der Quelle erreicht — ein beendeter Sync-Durchlauf ist nicht dasselbe wie aufgeholt zu sein (Schritt 2).</li>
<li class="">Er <strong>mergt</strong>, statt zu überschreiben, wenn Revisionen vergleichbar sind, und überschreibt, statt zu verzahnen, wenn sie es nicht sind (Schritt 3).</li>
<li class="">Als <em>Quelle</em> bedient er immer dann, wenn er die Daten wirklich hält, und verweigert nur, wenn er mitten in der Wiederherstellung wirklich leer ist (Schritt 4).</li>
<li class="">Er <strong>zertifiziert</strong> — legt das klebrige Autoritäts-Flag um — nur auf den Nachweis, dass jedes besessene Segment synchronisiert ist, ohne eine Abkürzung „Leere als Autorität" für einen Nicht-Primary (Schritt 5).</li>
</ul>
<p>Jede dieser Regeln ist ein reines, IO-freies Prädikat — eine Handvoll Booleans und zwei Revisionsnummern hinein, eine Entscheidung heraus —, weshalb sie isoliert unit-testbar sind und jede an genau den Zertifizierungsfehler festgenagelt werden konnte, der sie motivierte. Sie sind die Innenansicht der Invariante, die die <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">zwölfstündige Chaos-Zertifizierung</a> von außen beweist: einen Owner mitten im Flug töten, seine Replicas die Last tragen lassen, ihn zurückbringen und zusehen, wie <code>Missing=0</code> über Millionen bestätigter Operationen hält. Der Grund, warum der Neustart keinen Schreibzugriff verliert, ist, dass dem zurückkehrenden Owner nie auch nur einmal erlaubt wird, seiner eigenen Leere zu glauben. Er muss sich seine Daten zurückverdienen — von den Replicas, die sie in Sicherheit gehalten haben.</p>
<p>Wenn Sie die Teile wollen, auf die sich dies stützt: Das <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">wiederholbare Op-Log</a> macht „hole meinen Head auf den Head der Quelle auf" zu einer echten, lückenlosen Operation, und <a class="" href="https://clustron.io/de/blog/reading-your-own-writes/">Eigene Schreibzugriffe lesen</a> ist das clientseitige Versprechen, das das Serve-Gate aus Schritt 2 einhalten soll.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
            <category>Betrieb</category>
        </item>
        <item>
            <title><![CDATA[Wie ein Zaris-Cluster hochfährt: Mitgliedschaft, die Map und durch Konvergenz gesteuerte Readiness]]></title>
            <link>https://clustron.io/de/blog/cluster-formation-readiness/</link>
            <guid>https://clustron.io/de/blog/cluster-formation-readiness/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Bevor auch nur ein einziger Key bedient werden kann, muss ein Zaris-Cluster drei Fragen beantworten: Wer gehört dazu, wem gehört was, und ist es schon sicher, Traffic anzunehmen? Dieser Beitrag folgt dieser Boot-Sequenz — einem Mitgliedschaftsmodell auf Basis einer Konfigurations-Roster mit einer Karenzzeit, bevor ein Node als verloren gilt, einer vom Leader verfassten Partition-Map, die unter einer strikt monotonen Versionsprüfung committet wird, und einem zweistufigen Readiness-Gate, das einen Node erst dann bedient, wenn seine committete Map und sein Laufzeit-Ownership übereinstimmen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Wie ein Zaris-Cluster hochfährt: Mitgliedschaft, die Map und durch Konvergenz gesteuerte Readiness" 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>Die meisten Texte über verteilte Stores handeln vom Normalbetrieb — Lesen, Schreiben, Replikation — oder von den dramatischen Ausfällen: einem <a class="" href="https://clustron.io/de/blog/anatomy-of-a-failover/">Failover</a>, wenn ein Owner stirbt, oder einem <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">Cut-over</a>, wenn eine Partition umzieht. Doch es gibt einen leiseren Moment, der darüber entscheidet, ob all das funktioniert: <strong>der Moment, in dem der Cluster hochfährt.</strong> Bevor auch nur ein Key geroutet werden kann, müssen sich die Nodes einigen, wer zum Cluster gehört, wem welcher Ausschnitt des Keyspace gehört und — der Schritt, den die meisten Systeme falsch machen — <em>ob es überhaupt sicher ist, mit dem Bedienen zu beginnen.</em></p>
<p>Dieser Beitrag folgt dieser Boot-Sequenz in der Reihenfolge, in der Zaris sie tatsächlich ausführt: Mitgliedschaft, Partition-Map und die Readiness-Gates, die zwischen „der Prozess läuft" und „der Node nimmt Traffic an" stehen. Außerdem ziehen wir ehrlich eine Grenze zwischen dem, was Zaris selbst implementiert, und dem, was es von der zugrunde liegenden Cluster-Laufzeit erbt, auf der es aufbaut.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wer-gehört-zum-cluster-mitgliedschaft">Wer gehört zum Cluster: Mitgliedschaft<a href="https://clustron.io/de/blog/cluster-formation-readiness/#wer-geh%C3%B6rt-zum-cluster-mitgliedschaft" class="hash-link" aria-label="Direkter Link zur Wer gehört zum Cluster: Mitgliedschaft" title="Direkter Link zur Wer gehört zum Cluster: Mitgliedschaft" translate="no">​</a></h2>
<p>Das Erste, was ehrlicherweise gesagt werden muss, ist, wo die Grenze verläuft. Der Low-Level-Mitgliedschaftstransport — Peer-Heartbeats, Fehlererkennung und Leader-Election — wird von der Schicht <strong>Clustron.Nodus</strong> bereitgestellt, auf der Zaris aufbaut. Zaris nutzt sie über ein <code>INodusNode</code>-Interface: Es fragt Nodus, wer die Mitglieder sind und wer die Leadership hält, und wird benachrichtigt, wenn der Heartbeat eines Peers ausbleibt oder fehlschlägt. Zaris implementiert Raft oder ein Gossip-basiertes Discovery-Protokoll <em>nicht</em> neu; es setzt seine eigene Partition- und Ownership-Steuerungsebene auf diesem Fundament auf.</p>
<p>Was Zaris hinzufügt, ist eine explizite <strong>Mitgliedschafts-Zustandsmaschine</strong>, die rohe Heartbeat-Ereignisse in Ownership-relevante Entscheidungen übersetzt. Sie lebt in <code>MembershipStateTracker</code>, und ihre Zustände sind bewusst differenzierter als „oben / unten":</p>
<!-- -->
<p>Die Zustände fallen in zwei Gruppen, und genau diese Trennung ist der Kern. <code>Alive</code>, <code>Suspected</code> und <code>Unreachable</code> betreffen nur das <em>Laufzeit-Ownership</em> — sie können eine Replica-Promotion auslösen oder eine Partition vorübergehend als nicht verfügbar markieren, aber <strong>sie verändern niemals die Partition-Map.</strong> Nur <code>Lost</code>, <code>Evicted</code> und <code>Decommissioned</code> machen einen Node für eine Neuberechnung der Map zugänglich.</p>
<p>Diese Unterscheidung existiert, weil das Neuberechnen der Partition-Map teuer und, wenn zu voreilig, gefährlich ist — es kann gesunde Partitionen unnötig umverteilen. Wenn also der Heartbeat eines Nodes ausfällt, schreibt Zaris die Map <em>nicht</em> sofort neu. Es setzt den Node auf <code>Unreachable</code> und startet eine <strong>Karenzzeit</strong> (standardmäßig 30 Sekunden). Während dieses Fensters führt der Cluster nur ein Laufzeit-Failover durch — er befördert eine Replica, damit Lese- und Schreibzugriffe weiterfließen — und gibt dem Node die Chance zurückzukehren. Ein Flattern, eine GC-Pause, ein kurzer Netzwerkaussetzer: Nichts davon soll das Ownership neu schreiben. Erst wenn die Karenzzeit abläuft, wird der Node zu <code>Lost</code>, und erst wenn eine ganze Partitionsgruppe entfernt wurde, wird der einzige fehlergetriebene Pfad zur Map-Neuberechnung ausgelöst. Die Designregel, im Code, der sie verdrahtet, klar formuliert, lautet: <strong>Ein ausscheidender Node berechnet die Map niemals von sich aus neu.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wie-sich-nodes-finden">Wie sich Nodes finden<a href="https://clustron.io/de/blog/cluster-formation-readiness/#wie-sich-nodes-finden" class="hash-link" aria-label="Direkter Link zur Wie sich Nodes finden" title="Direkter Link zur Wie sich Nodes finden" translate="no">​</a></h3>
<p>Das Discovery ist <strong>Roster-basiert, nicht Gossip-Discovery.</strong> Die maßgebliche Liste der Nodes ist Konfiguration, nichts, was der Cluster zur Laufzeit aufspürt. In einer handgeschriebenen Konfiguration ist es das <code>nodes</code>-Array — jeder Eintrag deklariert eine ID, einen Host, einen Cluster-Port (Peer) und einen Client-Port. Unter Kubernetes wird diese Roster stattdessen aus den StatefulSet-Pod-Ordinalzahlen synthetisiert, statt von Hand gepflegt zu werden; die Preferred-Primary-Rolle eines Nodes wird direkt aus seiner Ordinalzahl abgeleitet.</p>
<p>Es <em>gibt</em> einen Gossip-Mechanismus in Zaris, aber er erfüllt eine andere Aufgabe — er hält die <em>Sicht der Nodes auf den Cluster-Zustand</em> konvergent, er dient nicht dazu, die Existenz von Peers zu entdecken. Darauf kommen wir bei der Readiness zurück.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wem-gehört-was-die-partition-map">Wem gehört was: die Partition-Map<a href="https://clustron.io/de/blog/cluster-formation-readiness/#wem-geh%C3%B6rt-was-die-partition-map" class="hash-link" aria-label="Direkter Link zur Wem gehört was: die Partition-Map" title="Direkter Link zur Wem gehört was: die Partition-Map" translate="no">​</a></h2>
<p>Sobald die Mitgliedschaft bekannt ist, braucht der Cluster einen Plan für den Keyspace. Dieser Plan ist die <strong>Partition-Map</strong> — ein einziges, versioniertes Dokument, nach dem jeder Node routet.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="von-einem-key-zu-einem-owner-in-zwei-schritten">Von einem Key zu einem Owner, in zwei Schritten<a href="https://clustron.io/de/blog/cluster-formation-readiness/#von-einem-key-zu-einem-owner-in-zwei-schritten" class="hash-link" aria-label="Direkter Link zur Von einem Key zu einem Owner, in zwei Schritten" title="Direkter Link zur Von einem Key zu einem Owner, in zwei Schritten" translate="no">​</a></h3>
<p>Ein Key wird nicht direkt einer Partition zugeordnet. Zaris verwendet eine zweistufige Zuordnung, die bewusst <strong>Byte für Byte an Redis Cluster ausgerichtet</strong> ist, sodass dieselbe Routing-Mathematik für native Clients und das RESP-Front-End funktioniert:</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>Der erste Schritt hasht den Key mit CRC16 (CCITT/XMODEM, Polynom <code>0x1021</code>) modulo 16384 — exakt die Funktion, die Redis Cluster verwendet, einschließlich der Berücksichtigung der Hash-Tag-Co-Location: Ein Key wie <code>user:{42}:profile</code> hasht nur den Teil innerhalb der geschweiften Klammern, sodass zusammengehörige Keys beieinander landen. Die 16384 Slots werden dann proportional auf die <strong>Segmente</strong> des Clusters abgebildet — standardmäßig 256, für die Lebensdauer des Clusters fest — und eine <code>SegmentToPartition</code>-Tabelle weist jedes Segment einer Partition zu. Die Partition schließlich ist das, was Owner hat.</p>
<p>Warum die zusätzliche Segmentschicht, statt direkt auf Partitionen zu hashen? Weil Segmente eine stabile, feingranulare Einheit sind, die Sie zwischen Partitionen <em>verschieben</em> können, wenn der Cluster wächst oder neu balanciert wird, ohne jeden Key neu zu hashen. Der Schritt von CRC16 zum Slot ändert sich nie; nur die Zuordnung von Segment zu Partition ändert sich.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-eine-partition-map-enthält">Was eine Partition-Map enthält<a href="https://clustron.io/de/blog/cluster-formation-readiness/#was-eine-partition-map-enth%C3%A4lt" class="hash-link" aria-label="Direkter Link zur Was eine Partition-Map enthält" title="Direkter Link zur Was eine Partition-Map enthält" translate="no">​</a></h3>
<p>Die Map selbst ist eine <code>PartitionMap</code> — ein MessagePack-serialisierter Record, dessen Struktur zeigt, wie sorgfältig das Ownership nachverfolgt wird. Die wissenswerten Felder:</p>
<ul>
<li class=""><strong><code>Version</code></strong> — ein monotoner Zähler, der bei <em>jeder</em> Map-Änderung hochzählt. Das ist die Zahl, die den Commit sicher macht (mehr dazu unten).</li>
<li class=""><strong><code>TopologyVersion</code></strong> — zählt nur bei einer echten <em>strukturellen</em> Änderung hoch (ein Node oder eine Partition kommt hinzu oder geht, ein Re-Tile), nicht bei einem routinemäßigen Migrations-Cut-over. Die Trennung von „die Form hat sich geändert" und „ein Owner hat sich geändert" erlaubt dem Rest des Systems, verhältnismäßig zu reagieren.</li>
<li class=""><strong><code>TotalSegments</code></strong> — standardmäßig 256, für die Lebensdauer des Clusters konstant.</li>
<li class=""><strong><code>Partitions</code></strong> — die Liste der Zuweisungen, jeweils mit einer <code>PrimaryNodeId</code> (der Platzierungs-Primary), <code>ReplicaNodeIds</code> und einer stabilen <code>PartitionIdentity</code> (einer bei der Entstehung erzeugten GUID, die eine Umnummerierung überdauert) sowie Ownership-Lease-Feldern: <code>ActiveOwnerNodeId</code>, einer vom Leader erzeugten <code>OwnershipGeneration</code> und dem <code>LeaderTerm</code>, der die Lease erteilt hat — wodurch der Cluster einen veralteten Ex-Leader daran hindern kann, Ownership zu verfassen.</li>
</ul>
<p>Beachten Sie die bewusste Trennung zwischen <code>PrimaryNodeId</code> (wo die Map eine Partition <em>bevorzugt</em> ansiedelt) und <code>ActiveOwnerNodeId</code> (wer sie <em>tatsächlich gerade bedient</em>). Beide können sich während eines Failovers oder einer Migration unterscheiden, und die Trennung in separate Felder macht diese Übergänge darstellbar, ohne den Plan für den Normalbetrieb zu verfälschen. Der Beitrag zum <a class="" href="https://clustron.io/de/blog/reading-your-own-writes/">Konsistenzmodell</a> erklärt, warum Lesezugriffe zu diesem einen aktiven Owner geroutet werden.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wie-die-map-geplant-wird">Wie die Map geplant wird<a href="https://clustron.io/de/blog/cluster-formation-readiness/#wie-die-map-geplant-wird" class="hash-link" aria-label="Direkter Link zur Wie die Map geplant wird" title="Direkter Link zur Wie die Map geplant wird" translate="no">​</a></h3>
<p>Die Map wird nicht gierig zusammengesetzt. Sie wird von einem reinen, deterministischen Planer erzeugt — <code>DesiredMapPlanner</code> —, der die Node-IDs der Mitglieder, die Segmentanzahl, eine gewünschte Partitionsanzahl und den Replikationsfaktor entgegennimmt und einen Plan zurückgibt. Zwei Eigenschaften sind erwähnenswert:</p>
<ol>
<li class=""><strong>Ausgewogene, zusammenhängende Segmentaufteilung.</strong> Segmente werden so gleichmäßig wie möglich in zusammenhängenden Abschnitten verteilt; die ersten Partitionen nehmen den Rest auf, wenn die Teilung nicht glatt aufgeht.</li>
<li class=""><strong>Platzierung in disjunkten Gruppen.</strong> Die ersten <em>N</em> Mitglieder werden die verschiedenen Partition-Primaries; die übrigen Mitglieder füllen die Replica-Slots reihum, sodass kein Node in zwei verschiedenen Partitionsgruppen landet.</li>
</ol>
<p>Der Grund, warum der Planer als reine Funktion der Eingaben geschrieben ist, ist ein hart erkämpfter: Dadurch ist eine ganze Klasse fehlerhafter Maps — eine Partition mit doppeltem Primary, ein nicht zusammenhängender Segmentbereich, eine degenerierte Zuweisung — <em>per Konstruktion nicht darstellbar</em>, statt dass man sie nachträglich mit einem Validator abfängt. Ehrlicherweise sollte auch die aktuelle Einschränkung genannt werden: Die Platzierung ist heute positionsbasiert und folgt disjunkten Gruppen; eine Churn-minimierende, Rendezvous-artige Platzierung, die bei jeder Neuberechnung möglichst wenige Daten verschiebt, ist eine ausdrückliche künftige Verfeinerung, nicht etwas, das ausgeliefert ist. Ausgeliefert ist ein Sticky-Placement-Schritt, der bei einer Neuberechnung durch Mitgliedschafts-Churn einen live bedienenden Owner an Ort und Stelle belässt, sodass eine gesunde Partition nicht ohne Grund verschoben wird.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-commit-bedeutet">Was „Commit" bedeutet<a href="https://clustron.io/de/blog/cluster-formation-readiness/#was-commit-bedeutet" class="hash-link" aria-label="Direkter Link zur Was „Commit&quot; bedeutet" title="Direkter Link zur Was „Commit&quot; bedeutet" translate="no">​</a></h3>
<p>Die Map wird von <strong>einem Node — dem Leader der Steuerungsebene — und niemandem sonst</strong> verfasst. Der Leader erzeugt eine Kandidaten-Map, schickt sie durch eine Projektion des Ownership-Logs (der Leader ist der einzige Schreiber des Ownership-Logs, und die bediente Map ist eine Faltung dieses Logs) und <em>committet sie dann lokal</em>, bevor er jemandem davon erzählt. Der Commit folgt einer einzigen, strikten Regel, die in <code>TopologyManager.ApplyMap</code> durchgesetzt wird:</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>Eine Map mit einer Version kleiner oder gleich der bereits gehaltenen wird abgelehnt, Punkt. Erst nachdem der Leader committet hat, sendet er ein <code>MapChangedEvent</code>, schickt direkte Map-Snapshots an Peers und veröffentlicht die Routing-Änderung an Clients. Follower holen die Map ab (beim Kaltstart fordern sie sie von bereits bekannten Peers an und versuchen es erneut, bis einer antwortet) und wenden sie über dieselbe monotone Prüfung an. Da jeder Node nur strikt höhere Versionen akzeptiert, können sich zwei Nodes nie auf widersprüchliche Maps derselben Version einigen — die Versionsnummer ist der Serialisierungspunkt.</p>
<!-- -->
<p>Eine ehrliche Besonderheit beim Kaltstart: Ein führerloser Node, der allein hochfährt, kann eine einzelne <em>provisorische</em> Ein-Node-Map erzeugen, damit er nicht handlungsunfähig ist; sie ist als provisorisch markiert und wird später durch die maßgebliche Multi-Node-Map ersetzt, sobald sich der Cluster gebildet hat. Das ist ein Verfügbarkeitsmittel, nicht der Pfad im Normalbetrieb.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ist-es-sicher-zu-bedienen-durch-konvergenz-gesteuerte-readiness">Ist es sicher zu bedienen: durch Konvergenz gesteuerte Readiness<a href="https://clustron.io/de/blog/cluster-formation-readiness/#ist-es-sicher-zu-bedienen-durch-konvergenz-gesteuerte-readiness" class="hash-link" aria-label="Direkter Link zur Ist es sicher zu bedienen: durch Konvergenz gesteuerte Readiness" title="Direkter Link zur Ist es sicher zu bedienen: durch Konvergenz gesteuerte Readiness" translate="no">​</a></h2>
<p>Hier ist der Schritt, der einen vertrauenswürdigen Store von einem unterscheidet, der lediglich startet. Dass ein Prozess läuft und sogar eine Map vorliegt, heißt nicht, dass ein Node Traffic annehmen sollte. Beginnt ein Node zu bedienen, bevor sein <em>Laufzeit-Ownership</em> zu der <em>Map</em> aufgeschlossen hat, die er gerade angewendet hat, kann er eine Anfrage an eine Partition routen, die er noch gar nicht hält. Zaris weigert sich, das zu tun, und zwar mit zwei unterschiedlichen Gates.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gate-eins-stabilität-der-cluster-bildung">Gate eins: Stabilität der Cluster-Bildung<a href="https://clustron.io/de/blog/cluster-formation-readiness/#gate-eins-stabilit%C3%A4t-der-cluster-bildung" class="hash-link" aria-label="Direkter Link zur Gate eins: Stabilität der Cluster-Bildung" title="Direkter Link zur Gate eins: Stabilität der Cluster-Bildung" translate="no">​</a></h3>
<p>Das erste Gate entscheidet, wann der Leader die <em>erste echte Map</em> erzeugen darf. <code>ClusterReadinessGate</code> wartet, bis die Mitgliedschaft stabil ist — mindestens eine Mindestanzahl von Nodes ist <em>durchgehend</em> für ein Stabilitätsfenster anwesend (standardmäßig 3 Sekunden) —, bevor eine Map committet wird. Das verhindert, dass der Leader eine Map gegen einen halb gebildeten Cluster verfasst, in dem Nodes noch einer nach dem anderen beitreten, was eine sofortige Neuberechnung eine Sekunde später bedeuten würde. Wird die Stabilität nicht innerhalb des Timeouts erreicht (standardmäßig 30 Sekunden), führt das Gate trotzdem einen <em>erzwungenen Bootstrap</em> durch: Verfügbarkeit schlägt endloses Warten. Der Cluster fährt lieber etwas zu früh hoch, als hängen zu bleiben.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gate-zwei-das-traffic-gate-pro-node">Gate zwei: das Traffic-Gate pro Node<a href="https://clustron.io/de/blog/cluster-formation-readiness/#gate-zwei-das-traffic-gate-pro-node" class="hash-link" aria-label="Direkter Link zur Gate zwei: das Traffic-Gate pro Node" title="Direkter Link zur Gate zwei: das Traffic-Gate pro Node" translate="no">​</a></h3>
<p>Das zweite Gate entscheidet tatsächlich, ob ein Node seinen Client-Listener öffnet. <code>ClusterTrafficReadinessGate.IsReady()</code> liefert nur dann true, wenn <strong>alle</strong> der folgenden Bedingungen erfüllt sind:</p>
<ul>
<li class="">eine committete Map existiert,</li>
<li class="">ihre Segment-zu-Partition-Tabelle ist vollständig befüllt (<code>length == TotalSegments</code>),</li>
<li class="">eine Laufzeit-Ownership-Map existiert,</li>
<li class=""><strong><code>runtime.MapVersion == map.Version</code></strong> — das Laufzeit-Ownership des Nodes stimmt mit der exakten Version der von ihm committeten Map überein,</li>
<li class="">und jede Partition hat einen Laufzeiteintrag.</li>
</ul>
<p>Die vierte Bedingung ist der Kern. Ein Node meldet sich nicht als bereit zum Bedienen, bevor seine eigene committete Map und sein eigenes Laufzeit-Ownership dieselbe Version haben. Darauf zeigt das Titelbild oben. Es gibt einen 60-Sekunden-Backstop, damit ein Node, der nicht konvergieren kann, den Start nicht ewig blockiert — danach fährt er mit einer Warnung fort, statt zu hängen —, aber im Normalfall hält das Gate die Tür geschlossen, bis beide Ebenen übereinstimmen.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="konvergent-bleiben-digests-und-detect-and-heal">Konvergent bleiben: Digests und Detect-and-Heal<a href="https://clustron.io/de/blog/cluster-formation-readiness/#konvergent-bleiben-digests-und-detect-and-heal" class="hash-link" aria-label="Direkter Link zur Konvergent bleiben: Digests und Detect-and-Heal" title="Direkter Link zur Konvergent bleiben: Digests und Detect-and-Heal" translate="no">​</a></h3>
<p>Readiness ist eine Eigenschaft der Startphase; <em>konvergent zu bleiben</em> ist eine des Normalbetriebs, und hier zahlt sich Zaris' Gossip tatsächlich aus. Jeder Heartbeat trägt einen kleinen <strong><code>ClusterStateDigest</code></strong> — die Epoche des Nodes, wen er für den Leader hält, seine Map-Version, seine Node-Anzahl und die Ownership-Generationen pro Partition. Keine zusätzlichen RPCs; er reist auf Traffic mit, den der Cluster ohnehin sendet.</p>
<p>Ein <code>DigestAccumulator</code> sammelt die Digests der Peers und vergleicht sie alle paar Sekunden. Weicht ein Node in zwei aufeinanderfolgenden Auswertungszyklen von der Mehrheit ab, löst er ein Drift-Signal aus, und ein <code>HealingOrchestrator</code> reagiert darauf — der Leader sendet korrigierende Direktiven (mit einer Abklingzeit, damit er nicht flattert), und ein Node, der sich als einsamer Ausreißer findet, heilt sich selbst durch erneutes Abholen. Das ist Konvergenz durch <strong>Erkennen und Reparieren</strong>, nicht durch eine Quorum-Abstimmung vor dem Bedienen.</p>
<p>Diese Unterscheidung ist für die Ehrlichkeit wichtig, daher möchte ich präzise sein. In Produktion wird die Übereinstimmung der Map-Version im gesamten Cluster durch das Traffic-Gate pro Node plus diese Digest-getriebene Heilungsschleife aufrechterhalten — nicht durch eine globale Barriere, die jedes Bedienen blockiert, bis jeder Node über eine Version abgestimmt hat. Die stärkere Aussage — <em>jeder</em> Node-Snapshot stimmt in einem Leader, einer Leader-Epoche und einer Map-Version überein — ist das, was das <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">12-Stunden-Chaos-Zertifizierungs</a>-Harness fortlaufend als Pass/Fail-Invariante prüft. Die Zertifizierung belegt, dass die Eigenschaft gilt; die Laufzeit erreicht sie durch Gating und Heilung statt durch eine blockierende Konsensrunde auf dem Serve-Pfad.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="replikationsfaktor-und-ein-ehrlicher-default">Replikationsfaktor und ein ehrlicher Default<a href="https://clustron.io/de/blog/cluster-formation-readiness/#replikationsfaktor-und-ein-ehrlicher-default" class="hash-link" aria-label="Direkter Link zur Replikationsfaktor und ein ehrlicher Default" title="Direkter Link zur Replikationsfaktor und ein ehrlicher Default" translate="no">​</a></h2>
<p>Ein letztes Stück der Cluster-Bildung, denn es verändert, was „bereit" für Ihre Dauerhaftigkeit bedeutet. Der Replikationsfaktor ist standardmäßig <strong>1</strong>. Mit <code>RF=1</code> hat jede Partition einen Primary und <em>keine Replica</em> — eine einzige Kopie. Der Planer platziert <code>RF - 1</code> Replicas pro Partition, im Default also null. Ein Cluster mit <code>RF=1</code> bildet sich, konvergiert und bedient einwandfrei, aber der Verlust eines Nodes ist ein Datenverlust für die Partitionen, die er besaß — by Design, nicht zufällig. Wenn Sie möchten, dass eine Partition einen Node-Ausfall überlebt, setzen Sie <code>RF &gt;= 2</code> und benötigen genug Nodes, um diese Replicas tatsächlich auf verschiedenen Maschinen zu platzieren — der Beitrag zur <a class="" href="https://clustron.io/de/blog/kubernetes-durability-sizing/">Dimensionierung der Dauerhaftigkeit unter Kubernetes</a> rechnet genau durch, wie viele Pods das erfordert. Die Partitionsanzahl hat einen ähnlich ehrlichen Default: Ist sie nicht gesetzt, verwendet der Cluster eine Partition pro Node.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-sequenz-im-ganzen">Die Sequenz im Ganzen<a href="https://clustron.io/de/blog/cluster-formation-readiness/#die-sequenz-im-ganzen" class="hash-link" aria-label="Direkter Link zur Die Sequenz im Ganzen" title="Direkter Link zur Die Sequenz im Ganzen" translate="no">​</a></h2>
<p>Zusammengenommen fährt ein Zaris-Cluster so hoch:</p>
<ol>
<li class=""><strong>Die Mitgliedschaft wird aufgelöst</strong> aus der Konfigurations-Roster (oder den k8s-Ordinalzahlen), und Nodus wählt einen Leader der Steuerungsebene.</li>
<li class="">Der Leader wartet hinter dem <strong>Formations-Gate</strong> auf stabile Mitgliedschaft, plant dann eine Map mit <code>DesiredMapPlanner</code> und <strong>committet sie lokal</strong> unter der strikten Prüfung auf monotone Versionen.</li>
<li class="">Der Leader <strong>sendet</strong> die Map per Broadcast; Follower holen sie ab und wenden sie unter derselben Prüfung an, wobei sie nur strikt höhere Versionen akzeptieren.</li>
<li class="">Jeder Node wartet hinter seinem <strong>Traffic-Gate</strong>, bis <code>runtime.MapVersion == map.Version</code> gilt und das Routing vollständig befüllt ist — erst dann öffnet er seinen Client-Listener.</li>
<li class="">Im Normalbetrieb halten <strong>Heartbeat-gebundene Digests</strong> die Sicht jedes Nodes konvergent, wobei Drift-Erkennung und Leader-getriebene Heilung jede Lücke schließen.</li>
</ol>
<p>Keiner dieser Schritte ist für sich genommen dramatisch. Zusammen machen sie den Unterschied zwischen einem Cluster, der <em>läuft</em>, und einem, der <em>bereit</em> ist — und in der Lücke zwischen diesen beiden Worten verliert ein Store stillschweigend Daten, wenn er die Frage nicht ernst nimmt.</p>
<hr>
<p><em>Zaris ist der verteilte Key/Value-Store von Clustron. Wenn Sie die Steuerungsebene aus Sicht des Operators sehen möchten, erstellt, startet und inspiziert die <a class="" href="https://clustron.io/de/blog/control-plane-powershell/">PowerShell-Anleitung</a> live einen Cluster; und <a class="" href="https://clustron.io/de/blog/elastic-scaling/">Elastic Scaling</a> behandelt, was mit der Map passiert, wenn Sie einem bereits bedienenden Cluster einen Server hinzufügen.</em></p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Zwei Cluster, ein Change Feed: Replikation über ein WAN, ohne Zaris zu strecken]]></title>
            <link>https://clustron.io/de/blog/cross-datacenter-change-feed/</link>
            <guid>https://clustron.io/de/blog/cross-datacenter-change-feed/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein Cluster ist auf ein Datacenter-LAN abgestimmt — Heartbeats im Mikrosekundenbereich, schnelles Failover, ein Ownership-Protokoll, das eine enge Anbindung voraussetzt. Streckt man ihn über ein WAN, wenden sich diese Annahmen gegen einen. Die sichere Form sind zwei unabhängige Cluster, verbunden durch einen wiederaufnehmbaren, nach Revision geordneten Change Feed. Hier steht, warum — und wie das Operation-Log bereits den Cursor liefert, um ihn zu bauen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Zwei unabhängige Cluster, verbunden durch einen wiederaufnehmbaren Change Feed über ein 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>Früher oder später stellt jemand die Frage: <em>Wir haben einen Cluster in einer Region und Nutzer in einer anderen — können wir nicht einfach dort ein paar Nodes hinzufügen und einen Cluster beide Regionen umspannen lassen?</em> Das ist ein nachvollziehbarer Instinkt. Ein Cluster, ein Namespace, ein Connection-String, Daten überall. Der Instinkt ist aber auch eine Falle, und es lohnt sich, genau zu erklären, warum.</p>
<p>Ein Zaris-Cluster ist für ein Datacenter-Netzwerk konstruiert. Die Mitgliedschaft wird mit Heartbeats auf kurzen Timern verfolgt. Das Ownership einer Partition wechselt zwischen Nodes über einen Handshake, der voraussetzt, dass die Gegenseite schnell antwortet. Das Failover fenced einen vermutlich toten Owner und befördert eine Replica im Sekundenbereich. Jeder dieser Mechanismen ist für eine Verbindung kalibriert, bei der die Round-Trip-Zeit in Mikrosekunden gemessen wird und ein „langsamer" Node tatsächlich krank ist — nicht bloß achtzig Millisekunden und einen Ozean entfernt. Setzt man die Hälfte der Nodes über ein WAN, hat man keinen georedundanten Cluster gebaut, sondern einen Cluster, der normale WAN-Latenz nun für einen Ausfall hält, das Ownership flattern lässt und beim ersten Aussetzer der Verbindung zwischen den Regionen ein Split-Brain riskiert.</p>
<p>Die richtige Form ist also nicht ein gestreckter Cluster. Es sind <strong>zwei unabhängige Cluster</strong> — jeder in sich geschlossen, mit eigenem Ownership und Failover, das vollständig im eigenen LAN bleibt —, verbunden durch einen <strong>asynchronen, wiederaufnehmbaren Change Feed</strong>. Dieser Beitrag handelt von diesem Muster: warum Unabhängigkeit die tragende Entscheidung ist und wie das Operation-Log von Zaris Ihnen bereits das Eine liefert, was ein WAN-Replikator tatsächlich braucht — einen Cursor, von dem aus Sie fortsetzen können.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-ein-gestreckter-cluster-das-falsche-werkzeug-ist">Warum ein gestreckter Cluster das falsche Werkzeug ist<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#warum-ein-gestreckter-cluster-das-falsche-werkzeug-ist" class="hash-link" aria-label="Direkter Link zur Warum ein gestreckter Cluster das falsche Werkzeug ist" title="Direkter Link zur Warum ein gestreckter Cluster das falsche Werkzeug ist" translate="no">​</a></h2>
<p>Es hilft, die konkreten Annahmen zu benennen, die ein einzelner Cluster trifft, denn jede zerbricht über ein WAN auf andere Weise.</p>
<!-- -->
<ul>
<li class=""><strong>Heartbeat-basierte Mitgliedschaft</strong> entscheidet, dass ein Node verschwunden ist, wenn er einige Beats verpasst. Eine Verbindung zwischen Regionen, die Latenz hinzufügt und gelegentlich partitioniert, wird diesen Detektor im gesunden Betrieb auslösen. Der Cluster wird lebende Nodes für tot erklären.</li>
<li class=""><strong>Das Ownership-Protokoll</strong> verschiebt Partitionen zwischen Nodes mit einem Handshake, der eine prompte Antwort erwartet. Über ein WAN stocken, wiederholen und konkurrieren diese Übergänge — genau der Churn, den die <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">Ownership- und Cut-over-Mechanik</a> in einem LAN <em>schnell</em> auflösen soll.</li>
<li class=""><strong>Failover-Timer</strong> befördern eine Replica, wenn der Owner tot aussieht. Über ein WAN sind „sieht tot aus" und „ist nur weit weg" nicht mehr unterscheidbar, sodass es zu unnötigen Promotions kommt — und wenn die Verbindung heilt und auf jeder Seite eine beförderte Replica sitzt, haben Sie zwei Owner für eine Partition.</li>
</ul>
<p>Nichts davon ist ein Defekt. Es ist ein Store, der genau das tut, was ein Store mit niedriger Latenz im Datacenter tun sollte, angewendet auf ein Netzwerk, das er nie überspannen sollte. Die Lösung ist nicht, jeden Timer so weit zu lockern, bis der Cluster ein WAN toleriert — das würde ihn auch echte Ausfälle viel zu langsam tolerieren lassen. Die Lösung ist, nicht länger von einem Cluster zu verlangen, es zu tun.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-baustein-den-sie-bereits-haben-ein-wiederholbares-log">Der Baustein, den Sie bereits haben: ein wiederholbares Log<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#der-baustein-den-sie-bereits-haben-ein-wiederholbares-log" class="hash-link" aria-label="Direkter Link zur Der Baustein, den Sie bereits haben: ein wiederholbares Log" title="Direkter Link zur Der Baustein, den Sie bereits haben: ein wiederholbares Log" translate="no">​</a></h2>
<p>Zaris führt pro Segment ein <strong>nach Revision geordnetes Operation-Log</strong>. Jeder akzeptierte Schreibvorgang erhöht eine monotone Revision und hängt einen Eintrag an. Dieses Log ist das Substrat, mit dem Replicas und neue Partition-Owner <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">aufholen, ohne Schreibzugriffe einzufrieren</a>: Ein Consumer, der die zuletzt gesehene Revision kennt, kann ab diesem Punkt vorwärts abspielen und zur aktuellen Wahrheit konvergieren.</p>
<p>Diese Eigenschaft — <em>ab einer bekannten Revision vorwärts abspielen</em> — ist das gesamte Fundament eines WAN-Replikators. Ein Change Feed ist nichts weiter als ein langlebiger Leser dieses Logs, der Einträge in Revisionsreihenfolge streamt und sich merkt, wo er aufgehört hat. Das, was er sich merkt, ist ein <strong>Cursor</strong>: die höchste Revision, die er dauerhaft angewendet hat, pro Segment.</p>
<!-- -->
<p>Weil das Log nach Revision geordnet ist und das Abspielen an einem von Ihnen gewählten Punkt beginnt, hat der Feed die beiden Eigenschaften, die eine WAN-Verbindung verlangt:</p>
<ol>
<li class=""><strong>Wiederaufnehmbar.</strong> Die Verbindung <em>wird</em> abbrechen — das ist die definierende Tatsache eines WAN. Kommt sie zurück, verbindet sich der Follower neu und fragt nach allem nach seinem gespeicherten Cursor. Keine vollständige Neukopie, keine Koordination, einfach „bei Revision N fortsetzen".</li>
<li class=""><strong>Idempotent beim Abspielen.</strong> Einen Eintrag nur anwenden, wenn seine Revision größer ist als die zuletzt für dieses Segment angewendete. Liefert ein Reconnect das Ende erneut aus, überspringt der Follower, was er schon hat. Doppelte Zustellung wird zum No-op, wodurch der Feed At-least-once sein darf, statt Exactly-once sein zu müssen.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-muster-von-anfang-bis-ende">Das Muster, von Anfang bis Ende<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#das-muster-von-anfang-bis-ende" class="hash-link" aria-label="Direkter Link zur Das Muster, von Anfang bis Ende" title="Direkter Link zur Das Muster, von Anfang bis Ende" translate="no">​</a></h2>
<p>Zwei Cluster. Nennen wir sie <strong>A</strong> (die Quelle) und <strong>B</strong> (der Follower). Jeder ist ein gewöhnlicher, unabhängiger Zaris-Cluster, der nichts von den Interna des anderen weiß.</p>
<p>In Cluster B betreiben Sie einen <strong>Replikations-Follower</strong> — einen Client, der den Change Feed von A konsumiert und die Operationen lokal in B schreibt. Sein Zustand ist winzig und dauerhaft: ein Cursor pro Segment. Die Schleife ist unspektakulär, und das ist der Punkt:</p>
<ol>
<li class="">Mit A verbinden, den Change Feed für die zu replizierenden Segmente (oder Key-Präfixe) anfordern, beginnend nach dem gespeicherten Cursor jedes Segments.</li>
<li class="">Einträge in Revisionsreihenfolge empfangen. Jede Operation auf B anwenden — <code>SET</code>, <code>DELETE</code>, Expiry —, abgesichert durch die Revisionsprüfung, sodass eine erneute Zustellung harmlos ist.</li>
<li class="">Den Cursor voranschreiten lassen und persistieren, sobald Einträge dauerhaft angewendet sind.</li>
<li class="">Bei jeder Trennung zurück zu Schritt 1. Der Cursor ist, wo Sie aufgehört haben.</li>
</ol>
<p>Alles, was der Follower braucht, um eine Unterbrechung zu überstehen, steckt in diesem Cursor. Es gibt keine verteilte Transaktion über das WAN, keine gemeinsame Mitgliedschaft, kein regionsübergreifendes Quorum. Ein Aussetzer der Verbindung kostet nichts außer etwas Aktualität.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-sie-eintauschen-rpo-ist-nicht-null-und-das-ist-der-deal">Was Sie eintauschen: RPO ist nicht null, und das ist der Deal<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#was-sie-eintauschen-rpo-ist-nicht-null-und-das-ist-der-deal" class="hash-link" aria-label="Direkter Link zur Was Sie eintauschen: RPO ist nicht null, und das ist der Deal" title="Direkter Link zur Was Sie eintauschen: RPO ist nicht null, und das ist der Deal" translate="no">​</a></h2>
<p>Diese Replikation ist <strong>asynchron</strong>. Der Follower hinkt der Quelle immer etwas hinterher — um so viele Revisionen, wie in A erzeugt, aber in B noch nicht angewendet wurden. Dieser Abstand ist Ihr <strong>Recovery Point Objective</strong>: Verschwindet Region A in diesem Augenblick, verlieren Sie, was B noch nicht erreicht hatte.</p>
<p>Das ist kein Fehler, für den man sich entschuldigen müsste; es ist der Kompromiss, den Sie ausdrücklich wählen. Die Alternative — Schreibzugriffe in A auf eine Bestätigung von B warten zu lassen, bevor sie committen — würde einen WAN-Round-Trip auf Ihren Schreibpfad legen und die niedrige Latenz opfern, die der ganze Grund ist, Zaris zu betreiben. Synchrone regionsübergreifende Replikation macht jeden Schreibvorgang zu einer transkontinentalen Anfrage. Das will fast niemand, sobald er die Latenzrechnung sieht.</p>
<p>Die ehrliche Formulierung lautet also: <strong>Sie wählen den RPO.</strong> Ein gut versorgter Follower auf einer gesunden Verbindung bleibt innerhalb weniger Revisionen — in der Praxis Staleness unter einer Sekunde. Eine gesättigte oder flatternde Verbindung lässt ihn weiter abdriften. Der Cursor macht die Drift <em>begrenzt und sichtbar</em> — Sie können genau messen, wie weit B zurückliegt, indem Sie seinen Cursor mit der aktuellen Revision von A vergleichen —, aber er macht sie nicht null. Dimensionieren Sie Ihre Toleranz entsprechend und alarmieren Sie bei Cursor-Lag, so wie Sie bei Replica-Lag innerhalb eines Clusters alarmieren würden.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wenn-die-quelle-ein-failover-durchführt-ist-es-dem-feed-egal">Wenn die Quelle ein Failover durchführt, ist es dem Feed egal<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#wenn-die-quelle-ein-failover-durchf%C3%BChrt-ist-es-dem-feed-egal" class="hash-link" aria-label="Direkter Link zur Wenn die Quelle ein Failover durchführt, ist es dem Feed egal" title="Direkter Link zur Wenn die Quelle ein Failover durchführt, ist es dem Feed egal" translate="no">​</a></h2>
<p>Hier zahlt sich der Aufbau auf dem Operation-Log ein zweites Mal aus. Innerhalb von Cluster A wird die Partition, die Ihr Feed liest, gelegentlich den Owner wechseln — ein Node stirbt, eine Replica wird befördert, eine Partition migriert. Das Log überlebt das: Es ist nach Revision geschlüsselt, und ein neuer Owner setzt dieselbe Revisionssequenz fort. Für den Follower ist ein Failover auf der Quellseite nur ein weiterer Reconnect. Er baut den Feed gegen die aktuelle Topologie von A neu auf und setzt bei seinem Cursor fort. Die Revisionen, die er schon angewendet hat, sind weiterhin die Revisionen, die er schon angewendet hat; die, die ihm fehlen, liegen weiterhin hinter dem Cursor. Der Follower muss nie verstehen, <em>wer</em> in A die Partition besitzt — nur, <em>welche Revision</em> er zuletzt gesehen hat.</p>
<p>Das ist der stille Grund, warum das Design mit Cursor über dem Log mehr wert ist als ein naives Relay, das „die Schreibzugriffe mitliest und weiterschickt". Ein Relay, das Live-Schreibzugriffe ohne dauerhafte, nach Revision geschlüsselte Position streamt, hat nichts, womit es fortsetzen könnte, wenn sich die Quelltopologie verschiebt. Das Log gibt dem Follower ein stabiles Koordinatensystem, das jeden einzelnen Owner überdauert.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-umfang-ist-eine-wahl-und-konflikte-sind-eine-grenze">Der Umfang ist eine Wahl, und Konflikte sind eine Grenze<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#der-umfang-ist-eine-wahl-und-konflikte-sind-eine-grenze" class="hash-link" aria-label="Direkter Link zur Der Umfang ist eine Wahl, und Konflikte sind eine Grenze" title="Direkter Link zur Der Umfang ist eine Wahl, und Konflikte sind eine Grenze" translate="no">​</a></h2>
<p>Zwei weitere Dinge sollten bewusst entschieden werden.</p>
<p><strong>Datenumfang.</strong> Sie müssen nicht alles replizieren. Der Feed arbeitet pro Segment, und Sie können die Segmente oder Key-Präfixe auswählen, auf die es ankommt — einen bestimmten Tenant, einen bestimmten Datensatz, den Ausschnitt, den eine Disaster-Recovery-Region wirklich braucht. Weniger zu replizieren bedeutet weniger WAN-Traffic und einen kleineren Follower; alles zu replizieren ergibt ein vollständiges Warm-Standby. Entscheiden Sie bewusst statt per Default.</p>
<p><strong>Richtung und Konflikte.</strong> Das obige Muster ist <strong>unidirektional</strong>: A ist die Quelle der Wahrheit, B ist ein überwiegend lesender Follower / Standby. Das ist der saubere Fall, und er hat kein Konfliktproblem — es gibt genau einen Schreiber pro Key. In dem Moment, in dem Sie es <strong>bidirektional</strong> machen — beide Regionen akzeptieren Schreibzugriffe auf dieselben Keys —, haben Sie nebenläufige Schreiber eingeführt, die durch ein WAN getrennt sind, und keine Menge an Change-Feed-Plumbing löst das für Sie. Nun schulden Sie eine Konfliktauflösungsrichtlinie: Last-Writer-Wins auf einer Uhr, der Sie vertrauen, Ownership pro Key, sodass die beiden Regionen nie denselben Key schreiben, oder eine Merge-Logik in der Anwendung. Active-Active ist ein reales und manchmal notwendiges Design, aber es ist eine <em>Datenmodellierungs</em>-Entscheidung, kein Transport-Feature. Greifen Sie zuerst zum unidirektionalen Modell; setzen Sie bidirektional nur ein, wenn Sie ausdrücklich entschieden haben, wie zwei Schreiber abgeglichen werden.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-form-zum-merken">Die Form zum Merken<a href="https://clustron.io/de/blog/cross-datacenter-change-feed/#die-form-zum-merken" class="hash-link" aria-label="Direkter Link zur Die Form zum Merken" title="Direkter Link zur Die Form zum Merken" translate="no">​</a></h2>
<!-- -->
<p>Ein WAN ist kein langsames LAN; es ist ein anderes Medium, in dem Partitionen ein normales Ereignis sind und kein Notfall. Ein einzelner Cluster weigert sich korrekterweise, etwas anderes vorzutäuschen — seine Timer und sein Ownership-Protokoll sind darauf abgestimmt, eine lange Stille als Ausfall zu werten, weil sie das innerhalb eines Datacenters fast immer ist. Also strecken Sie den Cluster nicht. Sie betreiben zwei, jeder souverän in seinem eigenen Netzwerk, und verbinden sie mit dem einen Primitiv, das darauf ausgelegt ist, eine Verbindung zu tolerieren, die kommt und geht: ein nach Revision geordnetes Log, das Sie abspielen können, und ein Cursor, der sich merkt, wo Sie waren.</p>
<p>Unabhängigkeit ist die Entscheidung, die den Rest sicher macht. Der Change Feed ist nur das Rohr. Das Operation-Log hat Ihnen den Cursor bereits geliefert.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
            <category>Betrieb</category>
        </item>
        <item>
            <title><![CDATA[Der Latenzboden: Eine Mikrosekunde Arbeit in zehn Mikrosekunden Installation]]></title>
            <link>https://clustron.io/de/blog/latency-floor/</link>
            <guid>https://clustron.io/de/blog/latency-floor/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Wir haben den Weg instrumentiert, den ein einzelnes GET durch einen Zaris-Node nimmt, vom Socket zur Partition und zurück. Der In-Memory-Store erledigt seine Aufgabe in etwa 1,7 Mikrosekunden. Die Operation kostet zehn bis dreizehn. Dieser Beitrag zeigt, wohin die anderen elf gehen — die Egress-Hop-Kette, die Kopien, der dreimal berechnete Hash und eine Lizenzprüfung, die bei jedem Request einen Lock nimmt — und was nötig ist, um sie zusammenzufassen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Eine Mikrosekunde Arbeit in zehn Mikrosekunden Installation" 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>Hier ist eine Zahl, die verändert, wie man über einen Key/Value-Store denkt. Wenn wir den In-Memory-Store von Zaris isolieren und ihm Operationen zuführen, ohne dass etwas anderes im Weg ist — kein Socket, keine Serialisierung, kein Routing —, bedient ein einzelner Thread mehr als eine Million Lesezugriffe pro Sekunde und skaliert sauber auf mehr als acht Millionen über acht Threads. Der Store erledigt die eigentliche Arbeit eines GET also in unter zwei Mikrosekunden und ist ausdrücklich <em>nicht</em> der Engpass.</p>
<p>Und doch erreicht ein vollständiges GET über eine echte TCP-Verbindung auf derselben Maschine ein Plateau bei einigen Hunderttausend pro Sekunde. Der Store kann die Arbeit fünfmal schneller erledigen, als der Server sie liefern kann. Die interessante Performance-Frage zu Zaris lautet also nicht „Wie schnell ist der Store?" Sondern genau umgekehrt: <em>Wohin geht die ganze Zeit zwischen Socket und Store?</em> Dieser Beitrag ist die Antwort, Operation für Operation nachverfolgt, denn die Lücke zwischen 1,7 Mikrosekunden Logik und 10 bis 13 Mikrosekunden Server-CPU pro Operation ist kein Rauschen — sie ist der Latenzboden, und fast alles davon ist Installation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-messung-mit-der-die-gesamte-untersuchung-beginnt">Die Messung, mit der die gesamte Untersuchung beginnt<a href="https://clustron.io/de/blog/latency-floor/#die-messung-mit-der-die-gesamte-untersuchung-beginnt" class="hash-link" aria-label="Direkter Link zur Die Messung, mit der die gesamte Untersuchung beginnt" title="Direkter Link zur Die Messung, mit der die gesamte Untersuchung beginnt" translate="no">​</a></h2>
<p>Man kann einen Pfad nicht optimieren, den man nicht in Schichten getrennt hat. Deshalb haben wir als Erstes eine Reihe von Ablations-Benchmarks gebaut — Harnesses, die das System jeweils auf eine einzige Schicht reduzieren.</p>
<ul>
<li class=""><strong>Nur Store.</strong> Den In-Process-Store der Partition direkt aufrufen, im selben Prozess, ohne Netzwerk. Ergebnis: ~1,15 Mio. GET/s und ~0,5 Mio. PUT/s <em>pro Thread</em>, linear skalierend auf ~8,2 Mio. / ~2,0 Mio. bei acht Threads.</li>
<li class=""><strong>Reines Socket-Echo.</strong> Ein einfacher .NET-Socket, der Frames derselben Form verarbeitet, ohne Zaris-Logik: ~7,2 Mio. Frames/s auf derselben Maschine.</li>
<li class=""><strong>Vollständiger nativer TCP-Pfad.</strong> Der echte Client spricht mit dem echten Server: ~300–324K Ops/s, wobei auf jeder Seite nur etwa drei von vierzehn logischen Kernen ausgelastet sind.</li>
</ul>
<p>Betrachtet man diese drei zusammen, fallen sofort zwei übliche Verdächtige weg. Der Store ist nicht die Grenze — er ist 3- bis 4-mal schneller als der vollständige Pfad. Das Betriebssystem und das Loopback sind es auch nicht — ein reiner Socket bewegt Frames zwanzigmal schneller. Was in der Mitte bleibt, zwischen einem Socket, der Millionen schafft, und einem Store, der Millionen schafft, ist die eigene Request-Installation von Zaris, und sie verbrennt etwa <strong>10–13 Mikrosekunden Server-CPU pro Operation, um rund 1,7 Mikrosekunden echter Arbeit zu umhüllen.</strong></p>
<p>Dieses Verhältnis ist das Thema. Folgen wir einem GET durch sie hindurch.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-weg-eines-einzelnen-get">Der Weg eines einzelnen GET<a href="https://clustron.io/de/blog/latency-floor/#der-weg-eines-einzelnen-get" class="hash-link" aria-label="Direkter Link zur Der Weg eines einzelnen GET" title="Direkter Link zur Der Weg eines einzelnen GET" translate="no">​</a></h2>
<!-- -->
<p>Nichts in diesem Diagramm ist auf den ersten Blick falsch oder verschwenderisch — es ist die ehrliche Form eines vernetzten Stores. Die Kosten stecken in den <em>Details</em> jedes Pfeils: wie oft ein Thread aufwachen muss, wie oft die Bytes kopiert werden und wie viel feste Arbeit pro Request mitläuft, die nichts mit dem gelesenen Key zu tun hat. In diesen Details leben die elf Mikrosekunden, und sie gruppieren sich in drei Bereiche.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="engpass-eins-die-egress-hop-kette">Engpass eins: die Egress-Hop-Kette<a href="https://clustron.io/de/blog/latency-floor/#engpass-eins-die-egress-hop-kette" class="hash-link" aria-label="Direkter Link zur Engpass eins: die Egress-Hop-Kette" title="Direkter Link zur Engpass eins: die Egress-Hop-Kette" translate="no">​</a></h2>
<p>Die größten Einzelkosten entstehen nicht beim Lesen des Wertes — sondern bei seiner <em>Auslieferung</em>. Das Zurücksenden einer einzigen Antwort an den Client durchquert in der aktuellen Struktur <strong>drei interne Channels, weckt drei Threads und kopiert die Payload viermal</strong> auf ihrem Weg nach draußen:</p>
<ol>
<li class="">Die Antwort wird vom Shard-Worker an den Sendepfad der Server-Pipeline übergeben.</li>
<li class="">Der Sendepfad übergibt sie an eine Schreibschleife pro Verbindung, die sie in einen gepoolten Buffer kodiert.</li>
<li class="">Die Schreibschleife stellt sie in die Sendewarteschlange der Transportverbindung.</li>
<li class="">Der Transport führt die eingereihten Buffer zu einem Stream zusammen und schreibt auf den Socket.</li>
</ol>
<p>Jede Übergabe ist ein Thread-Wake-up, und jedes Wake-up ist ein Scheduling-Ereignis, das das Betriebssystem bedienen muss — einzeln billig, bei Hunderttausenden pro Sekunde verheerend. Und in jeder Stufe werden die Bytes erneut kopiert: gepoolter Encode-Buffer, Sendewarteschlange, Coalescing-Buffer, Socket. Auf der <em>eingehenden</em> Seite gibt es entsprechende Kosten: Jeder Request, der auf einem IOCP-Completion-Thread ankommt, wird an einen Shard-Worker übergeben, bevor irgendeine Arbeit stattfindet — ein weiteres Wake-up und eine weitere Warteschlange.</p>
<p>Der Grund für diese Struktur ist vernünftig — die Entkopplung von Empfang, Dispatch, Encoding und Übertragung macht jede Schicht einfacher nachvollziehbar und lässt langsame Consumer Backpressure ausüben, ohne den Socket zu blockieren. Aber für den Normalfall — einen RF1-Lesezugriff, dessen Antwort bereits im Speicher des Nodes liegt, der den Request empfangen hat — ist das enorm mehr Maschinerie, als die Arbeit verdient. Die Antwort könnte im Prinzip <em>auf dem Empfangspfad selbst</em> berechnet und zurückgeschrieben werden, mit einer Kopie in den Socket-Buffer und ohne jegliche Übergaben.</p>
<p>Das ist das erste und größte Stück struktureller Arbeit, auf das die Messung hinweist: <strong>den Egress auf einen Hop und eine Kopie zusammenfassen</strong> für den Hot Path und den RF1-Lese-/Schreibzugriff inline auf dem empfangenden Thread ausführen, statt ihn über den Worker-Pool springen zu lassen. Das ist der Unterschied zwischen einem Design, das leicht zu zeichnen ist, und einem Design, das billig zu betreiben ist, und beides ist nicht dasselbe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="engpass-zwei-redundantes-hashing-und-ein-lock-pro-request">Engpass zwei: redundantes Hashing und ein Lock pro Request<a href="https://clustron.io/de/blog/latency-floor/#engpass-zwei-redundantes-hashing-und-ein-lock-pro-request" class="hash-link" aria-label="Direkter Link zur Engpass zwei: redundantes Hashing und ein Lock pro Request" title="Direkter Link zur Engpass zwei: redundantes Hashing und ein Lock pro Request" translate="no">​</a></h2>
<p>Die zweite Gruppe ist fester Overhead pro Operation — Arbeit, die bei <em>jedem</em> Request anfällt, unabhängig vom berührten Key, und die größtenteils nicht anfallen sollte.</p>
<p><strong>CRC16, dreimal, Bit für Bit.</strong> Zaris routet Keys mit einem CRC16-Hash zu Partitionen — derselben Hash-Familie, die Redis Cluster verwendet, <a class="" href="https://clustron.io/de/blog/redis-protocol-any-language/">so bleibt das RESP-Front-End Slot-kompatibel</a>. Das Problem ist nicht die Wahl des Hashs; es ist, dass die Implementierung <em>bitseriell</em> ist (sie läuft Bit für Bit, statt eine Lookup-Tabelle zu nutzen) und dass ein einzelnes GET sie beim Durchlaufen des Routings <strong>dreimal</strong> berechnet. Bei rund 260 Nanosekunden pro Berechnung sind das ~784 ns pro GET fürs Hashing — mehr als der gesamte MessagePack-Decode-plus-Encode-Roundtrip, der bei etwa 533 ns liegt. Ein tabellengetriebenes CRC16, <em>einmal</em> berechnet und mitgeführt, würde die größte feste Einzelposition zu einem Rundungsfehler machen.</p>
<p><strong>Eine Lizenzprüfung, die bei jedem Request einen Lock nimmt.</strong> Überraschender noch: Der Einstiegspunkt der Requests befragt bei jeder einzelnen Operation den Lizenzdienst, und diese Abfrage nimmt einen prozessweiten Lock und liest die Wanduhr (<code>DateTime.UtcNow</code>), um zu entscheiden, ob die aktuelle Lease noch gültig ist. Ein Lock auf dem Hot Path ist genau die Art von Ding, die in einem Single-Thread-Mikrobenchmark nicht auftaucht und dann unter Nebenläufigkeit stillschweigend alles serialisiert. Das Lizenzurteil ändert sich höchstens ein paar Mal während der Lebensdauer eines Prozesses — wenn eine Lease erteilt, erneuert oder abläuft. Es hat nichts damit zu tun, bei jedem GET unter einem Lock neu berechnet zu werden. Die Lösung ist, das Urteil in einem einzelnen <code>volatile</code>-Feld zu veröffentlichen, das eine Hintergrunderneuerung aktualisiert, und den Hot Path es ohne Lock und ohne Uhr-Aufruf lesen zu lassen. (Die Lease-Mechanik selbst ist Thema des <a class="" href="https://clustron.io/de/blog/licensing-lifecycle/">Beitrags zum Lizenz-Lebenszyklus</a>; hier geht es nur darum, dass das <em>Lesen</em> des Urteils kostenlos sein muss.)</p>
<p>Beides ist kein tiefes algorithmisches Problem. Es sind Kosten, wie sie entstehen, wenn jedes Stück für sich korrekt geschrieben wurde und noch niemand gemessen hat, was die Summe auf dem Pfad kostet, der eine Million Mal pro Sekunde läuft.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="engpass-drei-der-client-verbraucht-so-viel-wie-der-server">Engpass drei: Der Client verbraucht so viel wie der Server<a href="https://clustron.io/de/blog/latency-floor/#engpass-drei-der-client-verbraucht-so-viel-wie-der-server" class="hash-link" aria-label="Direkter Link zur Engpass drei: Der Client verbraucht so viel wie der Server" title="Direkter Link zur Engpass drei: Der Client verbraucht so viel wie der Server" translate="no">​</a></h2>
<p>Es wäre bequem, dem Server an allem die Schuld zu geben, aber die Ablation sagt etwas anderes: Lokale Benchmarks sind <strong>client-limitiert</strong>, und allein das Festnageln des Clients auf Performance-Kerne hob ihn von 169K auf 299K Ops/s. Der Client verbraucht etwa <strong>9 Mikrosekunden und 1,8 KB pro GET</strong>, und die Kosten ähneln denen des Servers:</p>
<ul>
<li class=""><strong>Completions springen durch den Thread-Pool.</strong> Jeder ausstehende Request wird von einer <code>TaskCompletionSource</code> verfolgt, die so konfiguriert ist, dass ihre Fortsetzung asynchron läuft; wenn die Antwort eintrifft, wird die Completion also in den Thread-Pool gepostet, statt inline fortgesetzt zu werden — ein weiterer Scheduling-Hop bei jeder Operation.</li>
<li class=""><strong>Bei jeder einzelnen Operation wird eine Umgebungsvariable gelesen.</strong> Der Client ruft <code>Environment.GetEnvironmentVariable("ZARIS_REQUEST_TIMEOUT")</code> <em>pro Request</em> auf, um seine Deadline zu bestimmen. Das Lesen einer Umgebungsvariable durchläuft den Umgebungsblock des Prozesses; eine Million Mal pro Sekunde ist das der größte von Zaris verfasste Frame auf dem Client. Sie sollte einmal beim Start gelesen und gecacht werden.</li>
<li class=""><strong>Ein Semaphor und ein Channel-Lock pro Operation</strong>, dazu Allokationen pro Request — eine Client-ID, die jedem Request aufgestempelt wird, Eintragsmetadaten, die angefordert und zurückgegeben werden, auch wenn der Aufrufer sie nicht verlangt hat.</li>
</ul>
<p>Der rote Faden ist derselbe wie beim Server: Die Arbeit eines GET ist winzig und von Scheduling-Hops, Locks und Allokationen umgeben, die jeweils für sich kostenlos aussahen. Der Hot Path des Clients ist bereits auf dem richtigen Weg — der <a class="" href="https://clustron.io/de/blog/valuetask-hot-path/">Wechsel zu <code>ValueTask</code> für die Kernoperationen</a> hat eine Allokation pro Aufruf bei synchronem Abschluss beseitigt —, und der Rest der Liste ist dieselbe Idee weitergeführt: die Umgebungsvariable cachen, ohne Thread-Pool-Sprung abschließen, auf dem Socket mit einem <code>ValueTask</code> empfangen und aufhören, Dinge zu allokieren, die niemand verlangt hat.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-ist-der-boden-also-wirklich">Was ist der Boden also wirklich?<a href="https://clustron.io/de/blog/latency-floor/#was-ist-der-boden-also-wirklich" class="hash-link" aria-label="Direkter Link zur Was ist der Boden also wirklich?" title="Direkter Link zur Was ist der Boden also wirklich?" translate="no">​</a></h2>
<p>Rechnet man alles zusammen, ergibt sich folgendes ehrliche Bild. Auf einem Laptop erreicht der native TCP-Pfad bei etwa 300K Ops/s seine Obergrenze, nicht weil der Store langsam wäre — er schafft Millionen —, sondern weil jede Operation 10–13 Mikrosekunden Server-CPU und ~9 Mikrosekunden Client-CPU durch eine Kette aus Hops, Kopien, redundanten Hashes und Locks schleppt. Der <em>Boden</em> wird von der Installation bestimmt, nicht von der Arbeit.</p>
<p>Das ermutigende Korollar: Der Boden besteht aus behebbaren Dingen, und die Schätzung, was ihre Behebung bringt, ist konkret. Die strukturelle Liste lautet:</p>
<ol>
<li class="">Den Egress auf einen Hop und eine Kopie zusammenfassen.</li>
<li class="">Den RF1-Hot-Path inline auf dem Empfangs-Thread ausführen oder pro Empfang gebündelt.</li>
<li class="">Tabellengetriebenes CRC16, einmal pro Operation gehasht.</li>
<li class="">Client-Completions ohne Allokation und ohne Thread-Pool-Sprung.</li>
<li class=""><code>ValueTask</code>-basierter Socket-Empfang auf dem Client.</li>
<li class="">Die Umgebungsvariable für den Request-Timeout cachen, statt sie pro Operation zu lesen.</li>
<li class="">Das Lizenzurteil in einem <code>volatile</code>-Feld veröffentlichen — kein Lock, keine Uhr auf dem Hot Path.</li>
<li class="">Eine allgemeine Allokationsdiät: gepoolte Antworten, keine Client-ID pro Request, keine unverlangten Metadaten.</li>
</ol>
<p>Zusammen sollen diese Maßnahmen die Server-CPU pro Operation von ~10 auf ~3–4 Mikrosekunden senken — der Unterschied zwischen einem Dutzend ausgelasteter Kerne für eine Million Ops/s und etwa vier.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-ehrlichen-einschränkungen">Die ehrlichen Einschränkungen<a href="https://clustron.io/de/blog/latency-floor/#die-ehrlichen-einschr%C3%A4nkungen" class="hash-link" aria-label="Direkter Link zur Die ehrlichen Einschränkungen" title="Direkter Link zur Die ehrlichen Einschränkungen" translate="no">​</a></h2>
<p>Zwei Dinge halten diesen Beitrag wahrhaftig statt triumphal.</p>
<p>Erstens, <strong>die absoluten Zahlen hier sind Laptop-Zahlen</strong>, und sie liegen vermutlich 2- bis 4-mal unter dem, was echte Server-Hardware mit dedizierten Client-Maschinen erreicht. Was sich zwischen Umgebungen überträgt, ist nicht der absolute Durchsatz — sondern die <em>relative Zuordnung</em>: Der Store ist nicht der Engpass, der Egress ist der größte Serverkostenpunkt, und der Client verbraucht so viel wie der Server. Diese Schlussfolgerungen sind maschinenunabhängig; die endgültige Ops/s-Zahl ist es nicht, und die echte Validierung findet im großen Maßstab auf <a class="" href="https://clustron.io/de/blog/kubernetes-durability-sizing/">Kubernetes</a> statt, nicht auf einer Workstation.</p>
<p>Zweitens, <strong>Replikation verändert die Obergrenze.</strong> Alles oben ist die RF1-Geschichte — eine einzelne Kopie, der schnelle Fall. Ein RF2-PUT muss dauerhaft auf ein Backup repliziert werden, bevor er bestätigen kann, und das ist ein grundlegend anderer und teurerer Pfad; ein realistisches Ziel dafür sind 400–500K pro Node, nicht eine Million. Und wenn Sie <em>keine</em> Round-Trips pro Operation brauchen, amortisiert der <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">Batch-Pfad</a> fast diesen gesamten festen Overhead über viele Operationen und kann heute eine Million pro Node erreichen. Die Ein-Million-Ops-Zahl für einzelne Operationen auf einem einzelnen Node ist ein Ziel, das die obige strukturelle Arbeit erreichen soll — keine Zahl, die wir als ausgeliefert nennen.</p>
<p>Diese Unterscheidung ist die gesamte Disziplin vertrauenswürdiger Performance-Arbeit: trennen, was die Messung <em>beweist</em> (der Store ist schnell, die Installation ist der Boden, und hier ist, wohin jede Mikrosekunde geht), von dem, was sie <em>verspricht</em> (eine bestimmte Zahl nach einer bestimmten Liste von Änderungen). Das Erste ist erledigt und solide. Das Zweite ist Ingenieursarbeit, die wir nun mit einer Karte statt einer Vermutung angehen können — und genau deshalb instrumentiert man den Pfad, bevor man ihn anfasst.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Performance</category>
            <category>.NET</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Eine Partition ohne Datenverlust migrieren: das Cut-over-Protokoll]]></title>
            <link>https://clustron.io/de/blog/partition-cutover-protocol/</link>
            <guid>https://clustron.io/de/blog/partition-cutover-protocol/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Eine Partition muss den Owner wechseln, während weiter Schreibzugriffe auf ihr landen. Folgen Sie der exakten Sequenz, die Zaris ausführt — ein an eine Revision verankerter Snapshot, ein durch einen Write-Fence abgesichertes finales Delta, das an einen Content-Digest gebunden ist, ein Abgleich der Live-Replicas, der atomare Cut-over-Flip des Leaders und ein Drop, der sich weigert, die einzige Kopie der Quelle zu vernichten, sofern deren Digest nicht mehr dem Nachweis entspricht, den der neue Owner erhalten hat.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Das Zaris-Partition-Cut-over-Protokoll" 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>Ein <a class="" href="https://clustron.io/de/blog/anatomy-of-a-failover/">Failover</a> ist das, was passiert, wenn ein Owner <em>stirbt</em> — der Cluster hat Sekunden, um einen Überlebenden zu wählen, und die Partition-Map bewegt sich nie. Migration ist der ruhigere, gefährlichere Cousin: Der Owner ist völlig gesund, und Sie wollen eine Partition <em>absichtlich</em> von ihm wegbewegen — um den Cluster zu vergrößern, neu zu balancieren oder einen Node zu leeren, den Sie außer Dienst stellen wollen. Nichts ist ausgefallen, und genau deshalb liegt die Latte höher. Ein Failover darf im schlimmsten Fall einige ehrlich verlorene Schreibzugriffe am Ende in Kauf nehmen; eine geplante Migration nicht. Wenn das Verschieben einer Partition von Node A zu Node B einen einzigen bestätigten Schreibzugriff verlieren kann, ist Migration kein Feature, sondern eine Belastung.</p>
<p>Dieser Beitrag geht das Cut-over-Protokoll durch, das Zaris tatsächlich ausführt, in dieser Reihenfolge, und verbringt die meiste Zeit bei dem einen Schritt, den alle falsch machen: dem Moment, in dem die Quelle schließlich „frei" ist, ihre Kopie zu verwerfen. Dieser Schritt ist eine geladene Waffe, und das Interessante an der Ingenieursarbeit ist die Sicherung daran.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-form-des-problems">Die Form des Problems<a href="https://clustron.io/de/blog/partition-cutover-protocol/#die-form-des-problems" class="hash-link" aria-label="Direkter Link zur Die Form des Problems" title="Direkter Link zur Die Form des Problems" translate="no">​</a></h2>
<p>Eine Partition ist eine Menge von Segmenten, und ein Segment ist ein Beutel von Key/Value-Records, jeder mit einer monotonen Revision. Die Migration verschiebt die Daten eines Segments von einem <strong>Quell</strong>-Node zu einem <strong>Ziel</strong>-Node und kippt dann das Ownership, sodass Clients zum Ziel routen. Das Ganze geschieht, während das Segment <em>live</em> ist: Lese- und Schreibzugriffe landen die ganze Zeit weiter.</p>
<p>Diese Live-Eigenschaft ist das Problem. Wenn Sie die Quelle snapshotten, die Bytes verschicken und das Ownership kippen, liegt jeder Schreibzugriff, der <em>während</em> des Versands landete, auf der Quelle und nicht auf dem Ziel — und im Moment des Kippens verschwinden diese Schreibzugriffe aus der gerouteten Sicht. Sie haben sie bestätigt; der Client wurde über „committet" informiert; jetzt sind sie weg. Die naive Migration verliert genau die Schreibzugriffe, die stattfanden, während sie damit beschäftigt war, sorgfältig mit denen davor umzugehen.</p>
<p>Das Protokoll muss also zwei Dinge tun, die in Spannung zueinander stehen: bis unmittelbar zum Flip weiter Schreibzugriffe auf der Quelle bedienen <em>und</em> garantieren, dass das Ziel jeden dieser Schreibzugriffe hält, bevor der Flip wirksam wird. Zaris löst diese Spannung mit einer Sequenz aus fünf Schritten, von denen jeder sich weigert fortzufahren, bevor der vorherige nachweislich abgeschlossen ist.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-1--ein-an-eine-revision-verankerter-snapshot-mit-festgenageltem-trim-floor">Schritt 1 — ein an eine Revision verankerter Snapshot, mit festgenageltem Trim-Floor<a href="https://clustron.io/de/blog/partition-cutover-protocol/#schritt-1--ein-an-eine-revision-verankerter-snapshot-mit-festgenageltem-trim-floor" class="hash-link" aria-label="Direkter Link zur Schritt 1 — ein an eine Revision verankerter Snapshot, mit festgenageltem Trim-Floor" title="Direkter Link zur Schritt 1 — ein an eine Revision verankerter Snapshot, mit festgenageltem Trim-Floor" translate="no">​</a></h2>
<p>Das Ziel beginnt damit, die Daten des Segments von der Quelle anzufordern (<code>SegmentMigrationRequest</code>). Die Quelle serialisiert nicht einfach ihr aktuelles Dictionary; sie erstellt einen <strong>an eine Revision verankerten Snapshot</strong>. Sie notiert die aktuelle Revision des Segments — nennen wir sie <code>R</code> — und erfasst die Live-Records zum Stand <code>R</code>.</p>
<p>In diesem einen Schritt stecken zwei Feinheiten. Erstens <strong>schaltet die Quelle ihr Op-Log scharf, bevor sie den Anker erfasst</strong>. Zaris kann das Op-Log für ein Segment mit einzigem Primary (RF1) als Optimierung des Schreibpfads überspringen; es zuerst scharf zu schalten bedeutet, dass jeder Schreibzugriff ab diesem Punkt protokolliert wird, sodass ein zuvor ohne Log geschriebener Schreibzugriff trotzdem eine Revision bei oder unter <code>R</code> hat und vom Snapshot selbst getragen wird. Das Delta, das wir später berechnen — „Operationen nach <code>R</code>" —, ist dann nachweislich lückenlos.</p>
<p>Zweitens <strong>nagelt die Quelle den Trim-Floor ihres Op-Logs bei <code>R</code> fest</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>Normalerweise trimmt das Op-Log alte Einträge, um Speicher zurückzugewinnen. Während einer Migration wäre das ein Desaster: Wenn der Floor während der laufenden Massenübertragung über <code>R</code> hinaus vorrückte, wären die Schreibzugriffe zwischen <code>R</code> und dem Flip nicht mehr abspielbar, und das finale Delta wäre stillschweigend unvollständig. Das Festnageln des Floors bei <code>R</code> garantiert, dass diese Schreibzugriffe überleben, bis das Ziel sie hat. Die Quelle streamt dann die Snapshot-Einträge — <strong>und bedient die ganze Zeit weiter Lese- und Schreibzugriffe.</strong> Sie ist weiterhin der geroutete Owner. Noch hat sich nichts bewegt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-2--das-ziel-wartet-absichtlich">Schritt 2 — das Ziel wartet, absichtlich<a href="https://clustron.io/de/blog/partition-cutover-protocol/#schritt-2--das-ziel-wartet-absichtlich" class="hash-link" aria-label="Direkter Link zur Schritt 2 — das Ziel wartet, absichtlich" title="Direkter Link zur Schritt 2 — das Ziel wartet, absichtlich" translate="no">​</a></h2>
<p>Das Ziel wendet den Bulk-Snapshot an und tut dann etwas, das wie Untätigkeit aussieht, aber das Herz des Designs ist: Es versetzt das Segment in einen Zustand <em>Warten auf Cut-over</em> und <strong>hält an</strong>. Es bestätigt der Quelle nichts. Es erhebt keinen Anspruch auf Ownership. Es sagt dem Leader nicht, dass es fertig ist. Der Kommentar im Code ist unverblümt, warum:</p>
<blockquote>
<p>Kein Ack an die Quelle (sie ist noch der geroutete Owner), KEIN Ownership-Anspruch und noch KEINE Completion-Benachrichtigung — die Completion wird zurückgehalten, bis das gefencte finale Delta bestätigt, dass dieser Node alles hält, was die Quelle bestätigt hat.</p>
</blockquote>
<p>Das ist der „Old-owner-authoritative"-Vertrag. Bis zum Flip antwortet genau ein Node für das Segment, und das ist die Quelle. Ein Ziel, das voreilig startet — und von einem Snapshot bedient, der bereits um einige Hundert Schreibzugriffe veraltet ist —, wäre ein Split-Brain. Das Ziel hält also inne und bittet um das Eine, mit dem es nachweisen kann, dass es aufgeholt hat: das finale Delta.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-3--das-durch-einen-write-fence-abgesicherte-finale-delta-und-der-digest-nachweis">Schritt 3 — das durch einen Write-Fence abgesicherte finale Delta und der Digest-Nachweis<a href="https://clustron.io/de/blog/partition-cutover-protocol/#schritt-3--das-durch-einen-write-fence-abgesicherte-finale-delta-und-der-digest-nachweis" class="hash-link" aria-label="Direkter Link zur Schritt 3 — das durch einen Write-Fence abgesicherte finale Delta und der Digest-Nachweis" title="Direkter Link zur Schritt 3 — das durch einen Write-Fence abgesicherte finale Delta und der Digest-Nachweis" translate="no">​</a></h2>
<p>Das Ziel sendet ein <code>SegmentMigrationFinalDeltaRequest</code>, verankert bei <code>R</code>. Hier erledigt die Quelle den sorgfältigen Teil.</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>Sie <strong>aktiviert einen Write-Fence</strong> auf dem Segment und liest <em>dann</em> die aktuelle Revision. Die Reihenfolge zählt: erst fencen, dann messen, damit es kein Fenster gibt, in dem ein Schreibzugriff an der Messung vorbeischlüpft. Dann liest sie jeden Op-Log-Eintrag nach <code>R</code> — das Delta von allem, was während der Massenübertragung landete — und berechnet einen <strong>Content-Digest</strong> des Live-Zustands des Segments. Dieser Digest ist der Dreh- und Angelpunkt des gesamten Protokolls, daher notiert die Quelle ihn als <strong>Cut-over-Nachweis</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>Genau hier ist ein ehrlicher Fehlermodus eingebaut. Sollte der Trim-Floor tatsächlich über <code>R</code> hinaus vorgerückt sein — ein verlorener oder zurückgesetzter Pin —, wäre „Operationen nach <code>R</code>" unvollständig, und das Verschicken dieses Deltas würde bestätigte Schreibzugriffe stillschweigend verwerfen. Die Quelle weigert sich:</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>Ein Abbruch unter dem Floor ist kein Verlust; er baut ab und erstellt von einem frischen Anker aus einen neuen Snapshot. Die Regel, an die sich der Code hält, lautet: <em>lauter Abbruch statt stillem Teilergebnis</em> — die Migration startet lieber neu, als auf einem unvollständigen Delta zu kippen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-4--das-gate-das-ziel-weist-nach-dass-es-dominiert">Schritt 4 — das Gate: das Ziel weist nach, dass es dominiert<a href="https://clustron.io/de/blog/partition-cutover-protocol/#schritt-4--das-gate-das-ziel-weist-nach-dass-es-dominiert" class="hash-link" aria-label="Direkter Link zur Schritt 4 — das Gate: das Ziel weist nach, dass es dominiert" title="Direkter Link zur Schritt 4 — das Gate: das Ziel weist nach, dass es dominiert" translate="no">​</a></h2>
<p>Das Ziel wendet das Delta an, berechnet dann seinen <em>eigenen</em> Content-Digest des Segments und vergleicht ihn mit dem der Quelle:</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>Sind die beiden Digests gleich, hält das Ziel Byte für Byte das, was die Quelle am Fence hielt — der Zustand des Ziels ist eine Obermenge jedes Schreibzugriffs, den die Quelle bestätigt hat. Unterscheiden sie sich, liegt es fast immer daran, dass der gestreamte Snapshot noch angewendet wird, wenn das erste Delta berechnet wird (das Completion-Signal kann eingereihte Daten-Batches überholen); das Ziel bleibt dann ausstehend und lässt einen zeitlich versetzten Retry das Delta erneut anfordern, statt zu spinnen. <strong>Eine Abweichung kippt nie.</strong> Der Flip hängt an einem positiven Nachweis der Gleichheit, nicht an der Abwesenheit eines Fehlers.</p>
<p>Es gibt noch etwas, das das Ziel tut, bevor es den Sieg erklärt, und es ist eine Garantie, die die meisten Migrationsimplementierungen auslassen. Die Quelle ist nicht die einzige Kopie, die vorne liegen könnte — eine <em>Replica</em> des Segments könnte einen Schreibzugriff halten, den die gefencte Quelle verpasst hat. Bevor es abschließt, faltet das Ziel daher den schreibgeschützten Tail jeder Live-Replica ein:</p>
<blockquote>
<p>Der Flip darf nur ausgelöst werden, wenn der Digest des neuen Owners die Quelle <strong>und jede Live-Replica</strong> des Segments dominiert.</p>
</blockquote>
<p>Die Faltung erhält die Revisionen: Ältere Records einer hinterherhinkenden Replica werden vom Revisions-Gate ignoriert, und eine vorauseilende Replica steuert genau den Schreibzugriff bei, den die Quelle nicht hatte. Erst wenn das Ziel die Quelle <em>und</em> jede Live-Replica dominiert, geht es weiter. Das ist die Invariante „Dauerhaftigkeit vor Ownership" — Ownership ist die Belohnung für nachweisbaren Besitz, nicht seine Voraussetzung.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-5--der-flip-und-der-drop-der-fast-ihre-daten-verliert">Schritt 5 — der Flip und der Drop, der fast Ihre Daten verliert<a href="https://clustron.io/de/blog/partition-cutover-protocol/#schritt-5--der-flip-und-der-drop-der-fast-ihre-daten-verliert" class="hash-link" aria-label="Direkter Link zur Schritt 5 — der Flip und der Drop, der fast Ihre Daten verliert" title="Direkter Link zur Schritt 5 — der Flip und der Drop, der fast Ihre Daten verliert" translate="no">​</a></h2>
<p>Jetzt meldet das Ziel dem Leader, dass es fertig ist (<code>SegmentMigrationCompletionNotify</code>). Der Leader validiert die Completion gegen den aktuellen Migrations-Override und schaltet eine <strong>atomare Cut-over-Neuberechnung</strong> der Partition-Map scharf — eine einzige versionierte Map-Änderung, die den Owner des Segments auf das Ziel kippt und den Migrations-Override entfernt. Nachdem diese Map angewendet wurde, ist das Ziel der geroutete Owner. Clients routen zu ihm. Der Umzug ist aus Sicht des Clusters erledigt.</p>
<p>Und die Quelle, die nun ein Segment hält, für das sie keine Rolle mehr hat, führt einen Aufräumlauf aus, um die verwaiste Kopie zu verwerfen und den Speicher freizugeben. Das ist die geladene Waffe.</p>
<p>Hier ist die Falle, und sie ist subtil genug, dass sie bis in einen Zertifizierungslauf überlebte, bevor sie gefangen wurde (<code>Missing=8629</code> bestätigte Keys weg). Ein Write-Fence <strong>reduziert</strong> das Bestätigen, bringt es bei einem Segment mit einer einzigen Kopie unter Last aber nicht sofort perfekt auf null. Also ist folgende Sequenz möglich:</p>
<ol>
<li class="">Die Quelle liefert den Digest des finalen Deltas als Nachweis. Das Ziel gleicht ihn ab und kippt.</li>
<li class="">Im winzigen Zeitfenster zwischen der Auslieferung des Nachweises und dem Wirksamwerden des Flips wird auf der Quelle noch ein weiterer Schreibzugriff bestätigt.</li>
<li class="">Das Ziel hat diesen Schreibzugriff nie erhalten — der Nachweis, den es abgeglichen hat, stammte von <em>vor</em> ihm.</li>
<li class="">Die Quelle, nun verwaist, verwirft ihre Kopie, um Platz zu gewinnen.</li>
<li class="">Dieser letzte Schreibzugriff existierte nur auf der Quelle. Er ist weg. Einem Client wurde gesagt, er sei committet.</li>
</ol>
<p>Die Lösung besteht darin, den Drop <em>davon abhängig zu machen, dass der Nachweis weiterhin zutrifft</em>. Die Quelle vernichtet ein verwaistes Segment nicht blind. Sie vergleicht den <strong>aktuellen</strong> Live-Digest des Segments mit dem Digest, den sie als Cut-over-Nachweis notiert hat, und zerstört die Daten nur, wenn beide noch übereinstimmen:</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>Ist nach dem Nachweis ein Schreibzugriff gelandet, unterscheidet sich der aktuelle Digest vom Nachweis-Digest, <code>ShouldRetainAfterCutover</code> liefert <code>true</code>, und die Quelle <strong>behält</strong> ihre Kopie:</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>Die Asymmetrie ist der ganze Punkt. <strong>Behalten ist immer sicher</strong>: Die Quelle ist nicht mehr der geroutete Owner, also ist eine behaltene Kopie ein redundantes, nicht bedientes Waisenkind — sie kann nie ein Split-Brain verursachen, sie kostet nur etwas Speicher, bis ein erneutes Bedienen den Tail an den neuen Owner ausliefert und ein späterer Lauf, der die Digests wieder übereinstimmend findet, sie sauber verwirft. <strong>Zerstören auf einem veralteten Nachweis ist nie sicher</strong>: Es ist die eine Operation, die einen bestätigten Schreibzugriff dauerhaft löschen kann. Die Richtlinie ist deshalb bewusst eng gefasst — sie <em>überstimmt</em> ausschließlich einen Drop, der sonst stattfinden würde, und nur für ein Segment mit einem aufgezeichneten Nachweis, dessen Digest nicht mehr passt. Segmente ohne Nachweis (eine Eviction, ein sofortiger Flip, ein nie migriertes Segment) behalten ihr bisheriges Verhalten unverändert. Das Gate fügt ein Behalten hinzu; es fügt nie ein Verwerfen hinzu.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-der-digest-und-nicht-eine-revisionsnummer">Warum der Digest und nicht eine Revisionsnummer?<a href="https://clustron.io/de/blog/partition-cutover-protocol/#warum-der-digest-und-nicht-eine-revisionsnummer" class="hash-link" aria-label="Direkter Link zur Warum der Digest und nicht eine Revisionsnummer?" title="Direkter Link zur Warum der Digest und nicht eine Revisionsnummer?" translate="no">​</a></h2>
<p>Man könnte fragen, warum der Nachweis ein Content-Digest ist und nicht einfach „die höchste Revision, die das Ziel bestätigt hat". Die Antwort: Eine Revisionsnummer sagt Ihnen, <em>wie weit</em> das Ziel gekommen ist, aber ein Digest sagt Ihnen, <em>ob das, was es bekommen hat, das ist, was Sie hatten</em>. Beides kann in den Nahtstellen auseinanderlaufen — eine Replica-Faltung, ein neu verankerter Retry, ein noch abfließender Snapshot —, und die einzige Frage, die Daten sicher hält, ist die inhaltliche: <em>Hält der neue Owner genau dies?</em> Ein Digest beantwortet das direkt. Ein Watermark beantwortet nur einen Stellvertreter dafür, und Stellvertreter sind der Ort, an dem bestätigte Schreibzugriffe sterben.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-das-ehrlich-bringt">Was das ehrlich bringt<a href="https://clustron.io/de/blog/partition-cutover-protocol/#was-das-ehrlich-bringt" class="hash-link" aria-label="Direkter Link zur Was das ehrlich bringt" title="Direkter Link zur Was das ehrlich bringt" translate="no">​</a></h2>
<p>Klar gesagt lautet die Garantie: <strong>Eine geplante Partition-Migration verliert keinen bestätigten Schreibzugriff.</strong> Jeder Schreibzugriff, den die Quelle vor dem Fence bestätigt hat, ist im Snapshot oder im Delta; jeder Schreibzugriff, den sie <em>nach</em> dem Nachweis bestätigt hat, hält die Kopie der Quelle am Leben, bis auch der neue Owner ihn hat. Das Protokoll bricht lieber laut ab und startet neu, als auf einem unvollständigen Delta zu kippen, und es faltet Live-Replicas ein, sodass der neue Owner jede Kopie dominiert, nicht nur die Quelle.</p>
<p>Was es <em>nicht</em> ist, ist kostenlos. Das gefencte finale Delta ist ein synchroner Round-Trip, der Schreibzugriffe auf dem zu migrierenden Segment kurz fenced — eine kurze, begrenzte Pause auf einem Segment, nicht auf der Partition und nicht im Cluster. Eine Migration, die nicht konvergieren kann (ein Trim-Floor, der immer weiter vorrückt, ein Replica-Tail, der sich aus seinem Log nicht ausdrücken lässt), erstellt einen neuen Snapshot und wiederholt, statt Fortschritt zu erzwingen; unter pathologischem Churn bedeutet das, dass eine Migration länger dauert, statt eine Abkürzung durch Ihre Daten zu nehmen. Das sind die richtigen Kompromisse für eine geplante Operation: Ein Umzug, für den Sie sich entschieden haben, sollte nie der Grund sein, dass ein committeter Schreibzugriff verschwindet.</p>
<p>Dies ist dasselbe Rückgrat, das die <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">zwölfstündige Chaos-Zertifizierung</a> von außen belastet — einen Node mitten in der Migration stoppen, die Map durcheinanderwirbeln und zusehen, wie bestätigte Schreibzugriffe überleben. Dieser Beitrag ist die Innenansicht einer der Nahtstellen, die dieser Lauf als geschlossen nachweist: der Moment, in dem eine Partition den Besitzer wechselt, und die Sicherung am Abzug, die verhindert, dass die Übergabe genau die Schreibzugriffe verschlingt, die sie schützen soll. Er stützt sich auf zwei weitere Teile, die für sich lesenswert sind — das <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">wiederholbare Op-Log</a>, das „Operationen nach Revision R" zu einer echten, lückenlosen Abfrage macht, und das <a class="" href="https://clustron.io/de/blog/elastic-scaling/">elastische Scale-out</a>, der häufigste Grund, warum eine Partition überhaupt umziehen muss.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Einen Keyspace scannen, der nicht stillhält: Wie SCAN einen Cluster durchläuft]]></title>
            <link>https://clustron.io/de/blog/scanning-a-moving-keyspace/</link>
            <guid>https://clustron.io/de/blog/scanning-a-moving-keyspace/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein vollständiger SCAN in Zaris muss einen Keyspace durchlaufen, der über 256 Segmente auf mehreren Nodes verteilt, nirgends global sortiert und während des Lesens im Wandel ist. So funktioniert diese Iteration tatsächlich — ein lock-freier sortierter Snapshot pro Segment, ein k-Wege-Merge zu einem geordneten Seitenstrom pro Node, ein Resume-Token, das an eine serverseitige Session gebunden ist, und ein Client-Fan-out mit hartem Gesamtlimit. Dazu eine ehrliche Darstellung dessen, was ein SCAN verspricht und was nicht.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Viele sortierte Segmente, zusammengeführt zu einem geordneten, seitenweisen Strom" 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>Ein einzelnes <code>GET</code> auf einen Key ist leicht zu durchdenken: den Key hashen, den Owner finden, genau diesen einen Node fragen. <code>SCAN</code> ist die gegenteilige Art von Operation. Es hat keinen Key — es stellt eine Frage <em>über den gesamten Keyspace</em>: Gib mir jeden Key in diesem Bereich, Seite für Seite. Und in einem partitionierten, replizierten Store ist dieser Keyspace nicht eine Sache an einem Ort. Er ist über 256 Segmente verteilt, die auf so viele Nodes entfallen, wie Sie betreiben, wobei jeder Node nur die Ausschnitte hält, die er gerade besitzt, und während Sie iterieren, wird in die Gesamtheit geschrieben. Es gibt keinen globalen sortierten Index zum Durchlaufen. Woher kommt also die Ordnung, worauf zeigt ein Resume-Token genau, und was passiert mit Ihrem Scan, wenn mittendrin ein Key geschrieben wird — oder eine Partition umzieht?</p>
<p>Dieser Beitrag folgt einem <code>SCAN</code> durch die drei Schichten, die ihn tatsächlich bedienen: ein einzelnes Segment, ein einzelner Node und der Client, der die Nodes zusammenfügt. Jede Schicht löst ein Teilstück des Problems, und die ehrlichen Grenzen der Operation ergeben sich daraus, wie diese Teile zusammenpassen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-anfrage-ein-bereich-und-eine-seitengröße-kein-key">Die Anfrage: ein Bereich und eine Seitengröße, kein Key<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#die-anfrage-ein-bereich-und-eine-seitengr%C3%B6%C3%9Fe-kein-key" class="hash-link" aria-label="Direkter Link zur Die Anfrage: ein Bereich und eine Seitengröße, kein Key" title="Direkter Link zur Die Anfrage: ein Bereich und eine Seitengröße, kein Key" translate="no">​</a></h2>
<p>Ein Scan wird durch zwei kleine Objekte beschrieben. Ein <code>KeyRange</code> sagt, <em>welche</em> Keys — ein Start, ein Ende oder ein Präfix, jeweils optional:</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>Und <code>ScanOptions</code> sagt, <em>wie</em> sie geliefert werden — wie groß eine Seite ist, ob Werte oder nur Keys übertragen werden, ein optionales hartes Limit und ein Resume-Token, um einen vorherigen Aufruf fortzusetzen:</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>Nichts davon nennt einen Node oder ein Segment. Das ist der Punkt: Der Aufrufer beschreibt, <em>was er will</em>, und die drei Schichten darunter finden heraus, wo es liegt und in welcher Reihenfolge es zurückgegeben wird.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schicht-eins-ein-segment-scannt-sich-selbst-lock-frei-und-geordnet">Schicht eins: ein Segment scannt sich selbst, lock-frei und geordnet<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#schicht-eins-ein-segment-scannt-sich-selbst-lock-frei-und-geordnet" class="hash-link" aria-label="Direkter Link zur Schicht eins: ein Segment scannt sich selbst, lock-frei und geordnet" title="Direkter Link zur Schicht eins: ein Segment scannt sich selbst, lock-frei und geordnet" translate="no">​</a></h2>
<p>Die kleinste Einheit, die einen Teil eines Scans beantworten kann, ist ein Segment — die Einheit der partitionierten Speicherung in Zaris, gestützt auf ein <code>ConcurrentDictionary</code> mit Live-Records. Ein Dictionary hat keine Ordnung, also braucht ein Segment-Scan zuerst eine sortierte Sicht seiner Keys. Diese bei jeder Seite neu aufzubauen wäre ruinös, daher hält jedes Segment einen <strong>gecachten, unveränderlichen Snapshot sortierter Keys</strong> und baut ihn nur neu auf, wenn sich die Key-Menge ändert:</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>Das Array wird einmal zugewiesen und nie verändert; ein nebenläufiger Neuaufbau erzeugt einfach ein neues Array und tauscht die Referenz. Der Scan-Durchlauf selbst nimmt also <strong>überhaupt keinen Lock</strong> — er sucht per Binärsuche den Start-Key und läuft in ordinaler Reihenfolge vorwärts, wobei er Live-Records aus dem Concurrent-Dictionary liest. Lese- und Schreibzugriffe auf das Segment blockieren den Scan nie, und der Scan blockiert sie nie.</p>
<p>Zwei Arten von Keys werden beim Durchlaufen stillschweigend übersprungen:</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>Ein gelöschter Key ist ein <em>Tombstone</em> — ein echter Record, der als <code>IsDeleted</code> markiert ist, weil <a class="" href="https://clustron.io/de/blog/what-crosses-the-wire/">ein Löschvorgang ein Record sein muss und keine Abwesenheit</a>, damit die Replikation ihn weitertragen kann. Der Scan filtert diese heraus, ebenso Keys, deren TTL abgelaufen ist, deren Sweeper aber noch nicht gelaufen ist. Der Snapshot der sortierten Keys ist also eine Obermenge dessen, was Sie sehen; Ablauf und Löschung werden beim Lesen gegen den <em>Live</em>-Record aufgelöst und nicht im Snapshot eingefroren.</p>
<p>Der Durchlauf stoppt bei <code>PageSize</code> und liefert ein <code>ResumeToken</code>. Hier ist das erste ehrliche Detail: Auf Segmentebene ist das Token im Wesentlichen ein <strong>Offset</strong> — eine Zählung, wie viele passende Einträge bereits zurückgegeben wurden (<code>ReturnedSoFar</code>). Ein fortgesetzter Segment-Scan sucht erneut per Binärsuche denselben Start-Key und überspringt so viele Treffer, bevor er ausgibt. Es ist eine Position per Zählung, kein Seek per Key.</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>Diese Wahl ist wichtig für das, was die Operation verspricht, und wir kommen darauf zurück.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schicht-zwei-ein-node-führt-seine-segmente-zu-einem-einzigen-geordneten-strom-zusammen">Schicht zwei: ein Node führt seine Segmente zu einem einzigen geordneten Strom zusammen<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#schicht-zwei-ein-node-f%C3%BChrt-seine-segmente-zu-einem-einzigen-geordneten-strom-zusammen" class="hash-link" aria-label="Direkter Link zur Schicht zwei: ein Node führt seine Segmente zu einem einzigen geordneten Strom zusammen" title="Direkter Link zur Schicht zwei: ein Node führt seine Segmente zu einem einzigen geordneten Strom zusammen" translate="no">​</a></h2>
<p>Ein Node besitzt viele Segmente — nicht alle 256, sondern die, die den Partitionen zugeordnet sind, die er gerade bedient. Ein Scan auf Node-Ebene muss <em>alle</em> davon als ein geordnetes Ergebnis präsentieren, weil der Aufrufer nach einem Bereich gefragt hat, nicht nach Segment 42. Das ist ein klassisches Merge-Problem: Jedes Segment kann seine Keys sortiert liefern, und gewünscht ist die sortierte Vereinigung.</p>
<p>Die erste Seite eines Node-Scans tut genau das. Sie sammelt einen Fetcher für jedes lokal besessene Segment und führt mit einer Prioritätswarteschlange einen <strong>k-Wege-Merge</strong> über sie aus, wobei Keys ordinal verglichen werden:</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>Das zusammengeführte, geordnete Ergebnis wird in einen Buffer materialisiert, der von einem <strong><code>RouterQueryCoordinator</code></strong> gehalten wird — einer kurzlebigen, serverseitigen Session, die unter einer frisch erzeugten <code>QueryId</code> registriert ist:</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>Jede weitere Seite ist nur eine Scheibe dieses Buffers, und das Resume-Token des Nodes trägt die <code>QueryId</code>, damit der nächste Aufruf dieselbe Session findet:</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>Das ist das zweite ehrliche Detail, und es ist ein echter Kompromiss. Ein Scan auf Node-Ebene ist <strong>zustandsbehaftet</strong>: Das Paging ist stabil und die Ordnung pro Node exakt, aber der Preis ist, dass der Node beim Beginn des Scans seinen gesamten besessenen, zusammengeführten Keyspace in einen Buffer materialisiert und für die Dauer der Iteration eine Session hält. Scans sind ein Werkzeug für Administration und Massen-Iteration, kein Streaming-Cursor mit konstantem Speicherbedarf über Milliarden von Keys — und ein Resume-Token gilt nur gegenüber dem Node und der Session, die es erzeugt haben. Lassen Sie diese Session ablaufen oder versuchen Sie, an anderer Stelle fortzusetzen, erhalten Sie ein lautes „Session nicht gefunden" statt einer still falschen Antwort.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schicht-drei-der-client-verteilt-per-fan-out-und-verzahnt-die-owner">Schicht drei: Der Client verteilt per Fan-out und verzahnt die Owner<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#schicht-drei-der-client-verteilt-per-fan-out-und-verzahnt-die-owner" class="hash-link" aria-label="Direkter Link zur Schicht drei: Der Client verteilt per Fan-out und verzahnt die Owner" title="Direkter Link zur Schicht drei: Der Client verteilt per Fan-out und verzahnt die Owner" translate="no">​</a></h2>
<p>Ein Node antwortet für die Segmente, die er besitzt. Ein Cluster-Scan muss <em>jeden</em> Owner abdecken, daher öffnet der clientseitige <code>ScanClusterClient</code> einen Scan gegen jeden Node parallel und verzahnt deren Seiten, sobald sie eintreffen, statt einen Node vollständig abzuarbeiten, bevor der nächste beginnt:</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>Diese Parallelität ist der Grund, warum <code>LimitCount</code> Sorgfalt braucht. Jeder Node behandelt das Limit als Obergrenze <em>pro Node</em>; würde der Client es nicht zusätzlich durchsetzen, könnte ein <code>LimitCount</code> von 1.000 gegen vier Owner nach dem Zusammenführen bis zu 4.000 Zeilen liefern. Die Cluster-Schicht wendet das Limit daher als <strong>hartes Gesamtlimit</strong> an und liefert nichts mehr aus, sobald es erreicht ist — während sie die laufende Seite jedes Owners dennoch bis zum Ende abarbeitet, weil das Disposen eines asynchronen Enumerators mitten im Move nicht erlaubt ist und die Ergebnisse pro Node ohnehin begrenzt sind:</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>Die Konsequenz, die klar benannt sein muss: Innerhalb des Beitrags eines einzelnen Nodes kommen Keys geordnet an; <strong>zwischen</strong> Nodes werden sie nach Ankunft verzahnt, sodass ein vollständiger Cluster-Scan <em>nicht</em> global sortiert ist, es sei denn, Sie verlangen es ausdrücklich. (Der verwandte <code>Search</code>-Pfad trägt aus genau diesem Grund <code>SortFields</code> und führt mit einem Comparator zusammen.) Wenn Sie jeden Key im Cluster in ordinaler Reihenfolge brauchen, sortieren Sie das zusammengesetzte Ergebnis; <code>SCAN</code> selbst garantiert Ordnung pro Owner und Vollständigkeit, keine globale Sortierung.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-ein-scan-verspricht--und-was-nicht">Was ein SCAN verspricht — und was nicht<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#was-ein-scan-verspricht--und-was-nicht" class="hash-link" aria-label="Direkter Link zur Was ein SCAN verspricht — und was nicht" title="Direkter Link zur Was ein SCAN verspricht — und was nicht" translate="no">​</a></h2>
<p>Setzt man die drei Schichten zusammen, wird der tatsächliche Vertrag der Operation sichtbar, ohne dass etwas davon verborgen wird:</p>
<ul>
<li class=""><strong>Es ist kein Point-in-Time-Snapshot des Clusters.</strong> Jeder Node materialisiert <em>seinen eigenen</em> zusammengeführten Buffer, wenn <em>sein</em> Scan beginnt, und diese Zeitpunkte fallen nicht zusammen. Schreibzugriffe, die vor dem Aufbau des Buffers auf einem Node landen, sind enthalten; spätere nicht. Es gibt kein clusterweites Einfrieren.</li>
<li class=""><strong>Gelöschte und abgelaufene Keys erscheinen nie.</strong> Tombstones und Records mit abgelaufener TTL werden beim Durchlaufen des Segments gegen den Live-Record gefiltert, ein Scan spiegelt also den logischen, nicht den physischen Inhalt wider.</li>
<li class=""><strong>Das Fortsetzen ist eine Position per Zählung innerhalb einer Session.</strong> Da der Cursor pro Segment ein Offset ist, kann ein Key, der zwischen zwei Seiten <em>vor</em> der aktuellen Position eingefügt oder entfernt wird, die Grenze verschieben — ein Scan, der nebenläufig zu starker Mutation des gescannten Bereichs läuft, kann prinzipiell einen Key an der Nahtstelle wiederholen oder überspringen. Ein Scan eines ruhenden Bereichs ist exakt. Das ist dieselbe Garantiefamilie, die Redis <code>SCAN</code> bietet — eine Best-Effort-Vollständigkeits-Iteration, keine Snapshot-Isolation — und es ist der richtige Kompromiss für einen ungeordneten, nebenläufig beschriebenen Store.</li>
<li class=""><strong>Der Keyspace kann sich unter Ihnen bewegen.</strong> Der Client verteilt per Fan-out an die Owner, die beim Start des Scans existieren. Partitionen können mitten in der Iteration <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">migrieren</a> oder ein <a class="" href="https://clustron.io/de/blog/anatomy-of-a-failover/">Failover</a> erleben; ein Scan ist ein Best-Effort-Durchlauf eines Live-Clusters, weshalb er das Werkzeug für Massen-Export, Reindexierung und administrative Abfragen ist und nicht für transaktionale Lesezugriffe. Für Read-your-writes auf einem einzelnen Key <a class="" href="https://clustron.io/de/blog/reading-your-own-writes/">routen Sie stattdessen zum Owner</a>.</li>
</ul>
<p>Nichts davon sind Bugs, die man übertünchen müsste; es ist die ehrliche Form der Iteration über einen ungeordneten, geshardeten, sich verändernden Keyspace, ohne die Welt anzuhalten. Das Design macht den billigen, häufigen Fall schnell — einen lock-freien sortierten Snapshot und einen Merge pro Node — und ist bei den Rändern ausdrücklich.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ein-hinweis-zum-resp-front-end">Ein Hinweis zum RESP-Front-End<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#ein-hinweis-zum-resp-front-end" class="hash-link" aria-label="Direkter Link zur Ein Hinweis zum RESP-Front-End" title="Direkter Link zur Ein Hinweis zum RESP-Front-End" translate="no">​</a></h2>
<p>Redis-Clients erreichen dieselbe Maschinerie über <code>KEYS</code> und <code>SCAN</code> auf dem <a class="" href="https://clustron.io/de/blog/redis-protocol-any-language/">RESP-Front-End</a>, die auf den nativen Scan abgebildet werden. Ein Vorbehalt, der klar gesagt sein sollte: Das RESP-<code>SCAN</code> ist heute Single-Shot — es arbeitet den nativen Scan vollständig ab und antwortet in einem Round-Trip mit Cursor <code>0</code> (Iteration abgeschlossen), statt den inkrementellen Cursor von Redis durch jeden Aufruf zu fädeln. Es ist ehrlich bei der Vollständigkeit, aber noch nicht inkrementell. Der native Client ist derjenige, der mit einem Resume-Token seitenweise abruft.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eine-operation-drei-ehrliche-schichten">Eine Operation, drei ehrliche Schichten<a href="https://clustron.io/de/blog/scanning-a-moving-keyspace/#eine-operation-drei-ehrliche-schichten" class="hash-link" aria-label="Direkter Link zur Eine Operation, drei ehrliche Schichten" title="Direkter Link zur Eine Operation, drei ehrliche Schichten" translate="no">​</a></h2>
<p>Ein <code>GET</code> berührt einen Owner und einen Key; ein <code>SCAN</code> berührt jeden Owner und bittet jeden, seinen Ausschnitt eines geordneten Ganzen zu liefern. Die Arbeit teilt sich sauber auf: Ein Segment macht aus einem ungeordneten Dictionary einen sortierten, lock-freien Durchlauf; ein Node führt seine Segmente zu einem geordneten, stabil seitenweisen Strom hinter einem Session-Cursor zusammen; der Client verteilt per Fan-out an die Owner, verzahnt deren Seiten und setzt das eine Limit durch, das der Fan-out sonst brechen würde. Jede Schicht ist für sich einfach, und die Garantien der Operation — Ordnung pro Owner, Vollständigkeit über einen ruhenden Bereich, keine Snapshot-Isolation, kein globales Einfrieren — sind genau das, was diese drei einfachen Schichten gemeinsam ehrlicherweise liefern können.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Die Speicherobergrenze: Warum ein Zaris-Node nie unbegrenzt wächst]]></title>
            <link>https://clustron.io/de/blog/the-memory-ceiling/</link>
            <guid>https://clustron.io/de/blog/the-memory-ceiling/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein Zaris-Node ist ein In-Memory-Store, aber kein unbegrenzter. Jeder Node ist ein begrenzter Puffer mit einer Speicherobergrenze, einem stets aktiven LRU-Cache darunter und einem Hysterese-Band, das die Eviction am Rand vor dem Flattern bewahrt. Hier steht genau, was passiert, während sich ein Node füllt — und welche zwei sehr unterschiedlichen Limits ein Schreibzugriff treffen kann.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Ein Zaris-Node hält die Speichernutzung in einem ruhigen Band unterhalb seiner Obergrenze und führt bei 80 % eine Eviction bis hinunter auf 70 % durch, sodass Schreibzugriffe das Limit nie überschreiten" 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>Hier ist eine Frage, die einfach klingt und es nicht ist: Was passiert, wenn einem In-Memory-Store der Speicher ausgeht?</p>
<p>Die naive Antwort lautet „er stürzt ab" — der Prozess wächst, bis das Betriebssystem ihn beendet. Die etwas weniger naive Antwort lautet „er lehnt Schreibzugriffe ab". Zaris tut standardmäßig keines von beidem. Ein Zaris-Node ist ein In-Memory-Store, aber bewusst <em>kein</em> unbegrenzter: Jeder Node ist ein <strong>begrenzter Puffer</strong> mit einer harten Obergrenze, und wenn sich die Daten, die er besitzt, dieser Obergrenze nähern, entfernt der Node seine kältesten Daten, um Platz zu schaffen. Schreibzugriffe gelingen weiter; das Limit hält; der Prozess bläht sich nicht auf. Der Node verhält sich wie ein Cache, denn darunter ist er einer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="jeder-node-ist-ein-begrenzter-puffer">Jeder Node ist ein begrenzter Puffer<a href="https://clustron.io/de/blog/the-memory-ceiling/#jeder-node-ist-ein-begrenzter-puffer" class="hash-link" aria-label="Direkter Link zur Jeder Node ist ein begrenzter Puffer" title="Direkter Link zur Jeder Node ist ein begrenzter Puffer" translate="no">​</a></h2>
<p>Ein Zaris-Node wächst nicht ohne Grenze. Jeder Node bilanziert fortlaufend die Größe der Daten, die er aktiv besitzt, und hält diese laufende Summe unter einer konfigurierten <strong>Obergrenze</strong>. Diese Bilanzierung ist nie optional — ein Node ist <em>immer</em> ein begrenzter Puffer. Konfigurierbar sind nur, wie groß die Obergrenze ist und im Prinzip, was der Node tut, wenn er sie erreicht.</p>
<p>Die Standardobergrenze beträgt <strong>1 GiB</strong> aktiv besessener Daten pro Node. Eine Einstellung „unbegrenzt" gibt es bewusst nicht. Setzen Sie keine Obergrenze, erhalten Sie den Default — Sie bekommen nie versehentlich Unendlich.</p>
<p>Ein Detail ist wichtig, um den Footprint eines Clusters zu beurteilen: Die Obergrenze deckt die Daten ab, die ein Node <strong>aktiv besitzt</strong> — die Primärkopien, die er Clients bereitstellt. Replica-Kopien, die ein Node für <em>andere</em> Partitionen hält, werden separat erfasst und zählen nicht gegen die Obergrenze für aktive Owner. Der tatsächlich belegte Speicher eines Nodes ist also sein besessenes Working Set plus alle Replica-Kopien, die er für seine Peers trägt; die Obergrenze regelt Ersteres, nicht die Summe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="zwei-wege-die-linie-zu-halten">Zwei Wege, die Linie zu halten<a href="https://clustron.io/de/blog/the-memory-ceiling/#zwei-wege-die-linie-zu-halten" class="hash-link" aria-label="Direkter Link zur Zwei Wege, die Linie zu halten" title="Direkter Link zur Zwei Wege, die Linie zu halten" translate="no">​</a></h2>
<p>Ein begrenzter Puffer kann genau zwei Dinge tun, wenn ein Schreibzugriff ihn über seine Obergrenze triebe: etwas Altes verwerfen, um Platz zu machen, oder den neuen Schreibzugriff ablehnen.</p>
<table><thead><tr><th>Modus</th><th>Verhalten an der Obergrenze</th><th>Ein Schreibzugriff, der sie überschreiten würde</th></tr></thead><tbody><tr><td>Eviction aktiviert (Default)</td><td>Entfernt ältere Daten, um Platz zu schaffen</td><td>Zugelassen — kältere Daten werden verworfen</td></tr><tr><td>Eviction deaktiviert</td><td>Lehnt neue Daten ab</td><td>Abgelehnt mit <code>KvStatus.CapacityExceeded</code></td></tr></tbody></table>
<p>Bei aktivierter Eviction ist ein Node ein Cache: Unter Druck verwirft er die am wenigsten wertvollen Daten, damit Schreibzugriffe weiterfließen. Bei deaktivierter Eviction ist er ein Store, der bei voller Kapazität ablehnt und bestehende Daten schützt, indem er neue Schreibzugriffe abweist.</p>
<p>Hier kommt der ehrliche Teil. In der aktuellen Version läuft ein Node <strong>immer</strong> mit aktivierter Eviction und der LRU-Policy. Der Modus ohne Eviction und die alternativen Policies (LFU, TTL-bewusst) existieren in der Engine, sind aber noch nicht per Konfiguration wählbar. Praktische Folge: <code>CapacityExceeded</code> — der Status für Ablehnung bei voller Kapazität — ist heute im Wesentlichen nicht erreichbar: Ein voller Node entfernt Daten, statt abzulehnen. Es ist ein echter, typisierter Status, gegen den Sie Code schreiben <em>können</em>, aber unter der stets aktiven LRU-Policy werden Sie ihn normalerweise nicht sehen. Der eine Speicher-Regler, den Sie derzeit tatsächlich steuern, ist die Größe der Obergrenze selbst, <code>memory.maxSizeBytes</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-eviction-lauf-bewegt-sich-in-einem-ruhigen-band">Der Eviction-Lauf bewegt sich in einem ruhigen Band<a href="https://clustron.io/de/blog/the-memory-ceiling/#der-eviction-lauf-bewegt-sich-in-einem-ruhigen-band" class="hash-link" aria-label="Direkter Link zur Der Eviction-Lauf bewegt sich in einem ruhigen Band" title="Direkter Link zur Der Eviction-Lauf bewegt sich in einem ruhigen Band" translate="no">​</a></h2>
<p>Das Interessante an der Ingenieursarbeit ist nicht, <em>dass</em> ein Node Daten entfernt — sondern <em>wie</em>, denn ein naives Design „bei jedem Schreibzugriff entfernen, sobald voll" flattert kläglich. Stellen Sie sich einen Node vor, der genau an seiner Obergrenze sitzt: Jeder einzelne eingehende Schreibzugriff würde eine Eviction auslösen, um sich selbst Platz zu machen, und Sie würden diese Kosten für immer auf dem Hot Path zahlen.</p>
<p>Zaris vermeidet das mit zwei Maßnahmen. Erstens wird die Eviction von einer <strong>Hintergrundschleife</strong> getrieben, nicht vom einzelnen Schreibzugriff — der Schreibzugriff, der einen Node über die Kante kippt, wird zugelassen, und die Schleife schafft kurz darauf Platz, abseits des Request-Pfads. Zweitens nutzt die Schleife <strong>Hysterese</strong>: einen hohen Auslöser und ein niedrigeres Ziel, sodass sie in Schüben Platz schafft und dann ruhig wird, statt am Rand kontinuierlich zu feuern.</p>
<ul>
<li class=""><strong>Auslöser:</strong> Ein Lauf startet, wenn die aktive Nutzung über <strong>80 %</strong> der Obergrenze steigt.</li>
<li class=""><strong>Ziel:</strong> Einmal gestartet, entfernt der Lauf so lange Daten, bis wieder etwa <strong>30 %</strong> der Obergrenze frei sind — bis die Nutzung auf etwa <strong>70 %</strong> zurückgefallen ist.</li>
<li class=""><strong>Taktung:</strong> Die Schleife wacht in einem festen Intervall auf (standardmäßig eine Sekunde) und zieht in jedem Zyklus eine begrenzte Stichprobe von Kandidaten.</li>
</ul>
<p>Da das Ziel nach der Eviction (70 %) strikt <em>unter</em> dem Auslöser (80 %) liegt, senkt jeder Lauf die Nutzung in ein ruhiges Band, statt genau an dem Punkt zu stoppen, der ihn sofort wieder auslösen würde. Das ist der Sägezahn im Titelbild: Die Nutzung steigt auf 80 %, ein Lauf bringt sie zurück auf 70 %, und sie steigt erneut — nie an die Obergrenze gedrückt, nie mit einer Eviction pro Schreibzugriff flatternd.</p>
<!-- -->
<p>Die Eviction-Policy selbst ist <strong>LRU</strong> — Least Recently Used — und sie arbeitet per <em>Stichprobe</em>, nicht strikt. Statt eine global geordnete Liste jedes Keys zu führen (was Speicher und Lock-Contention auf dem Hot Path kosten würde), stempelt Zaris auf dem Lesepfad ungefähre Aktualitätssignale an Ort und Stelle ein und zieht, wenn ein Lauf feuert, eine kleine Menge von Kandidaten und verwirft die mit der niedrigsten Bewertung. Es ist derselbe Stichprobenansatz, den Redis verwendet, und derselbe Kompromiss: Sie geben die perfekte Reihenfolge „den einen ältesten Key entfernen" auf und erhalten dafür eine Eviction, die fast nichts in der Pflege kostet und nie einen separaten Index braucht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-andere-obergrenze-die-größe-pro-item">Die andere Obergrenze: die Größe pro Item<a href="https://clustron.io/de/blog/the-memory-ceiling/#die-andere-obergrenze-die-gr%C3%B6%C3%9Fe-pro-item" class="hash-link" aria-label="Direkter Link zur Die andere Obergrenze: die Größe pro Item" title="Direkter Link zur Die andere Obergrenze: die Größe pro Item" translate="no">​</a></h2>
<p>Die gesamte Speicherobergrenze ist ein Limit für die <em>Summe</em>. Es gibt ein zweites, unabhängiges Limit für jedes <em>einzelne</em> Item: Standardmäßig darf ein Wert <strong>4 MiB</strong> nicht überschreiten. Das wird bei jedem Schreibzugriff unabhängig vom Eviction-Modus geprüft, und hier trifft ein Schreibzugriff auf eine tatsächlich erreichbare Ablehnung.</p>
<p>Die Unterscheidung ist wichtig, weil die beiden Fehler entgegengesetzte Dinge bedeuten und entgegengesetzte Reaktionen verlangen:</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> ist in jedem Modus ein Live-, erreichbares Ergebnis — Eviction kann für einen Wert über dem Limit pro Item keinen Platz schaffen, denn bei dem Limit geht es nicht um freien Platz, sondern um einen einzelnen pathologischen Wert. <code>CapacityExceeded</code> erscheint, wie oben besprochen, nur im Modus ohne Eviction, den Sie noch nicht wählen können. Beide gleich zu behandeln ist ein Bug, der nur darauf wartet, aufzutreten: Einen <code>ItemTooLarge</code>-Schreibzugriff endlos zu wiederholen, wird nie gelingen.</p>
<!-- -->
<p>Eine verwandte Eigenheit liegt an der Grenze zwischen diesem Limit pro Item und der .NET-Runtime: Werte ab etwa 85.000 Bytes landen auf dem <strong>Large Object Heap</strong>, daher ist das Anheben von <code>maxItemSizeBytes</code>, um große Werte zuzulassen, eine bewusste Entscheidung mit GC-Folgen und kein Regler zum Nulltarif. Wenn Sie große einfache Werte per Hot-Update ändern, ist das Zerlegen in Stücke unterhalb der LOH-Grenze der Mechanismus, der den LOH vom Churn bewahrt — ein eigenes Feature mit eigenen Kompromissen, das wir in <a class="" href="https://clustron.io/de/blog/large-object-chunking/">Große Objekte speichern, ohne den Heap zu belasten</a> aufgeschlüsselt haben.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-sie-konfigurieren-und-was-nicht">Was Sie konfigurieren und was nicht<a href="https://clustron.io/de/blog/the-memory-ceiling/#was-sie-konfigurieren-und-was-nicht" class="hash-link" aria-label="Direkter Link zur Was Sie konfigurieren und was nicht" title="Direkter Link zur Was Sie konfigurieren und was nicht" translate="no">​</a></h2>
<p>Die Speicherobergrenze steht in der Store-Konfiguration im Abschnitt <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>Das sind 2 GiB. Setzen Sie <code>maxSizeBytes</code> auf <code>null</code> oder lassen Sie es weg, erhalten Sie den Default von 1 GiB. In der aktuellen Version ist das die Speichereinstellung, die Sie steuern — die LRU-Policy, die Hysterese-Schwellen 80 %/70 % und die Taktung von 1 Sekunde sind feste Defaults, keine Regler. Es ist wichtig, hier nüchtern zu sein: Die Blogbeiträge und die Engine beschreiben einen reichhaltigeren Policy-Raum, als die Konfigurationsoberfläche derzeit bereitstellt, und dieser Beitrag beschreibt, was Sie heute tatsächlich einstellen können.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-das-für-die-dimensionierung-eines-clusters-bedeutet">Was das für die Dimensionierung eines Clusters bedeutet<a href="https://clustron.io/de/blog/the-memory-ceiling/#was-das-f%C3%BCr-die-dimensionierung-eines-clusters-bedeutet" class="hash-link" aria-label="Direkter Link zur Was das für die Dimensionierung eines Clusters bedeutet" title="Direkter Link zur Was das für die Dimensionierung eines Clusters bedeutet" translate="no">​</a></h2>
<p>Da jeder Node ein begrenzter Puffer ist, entspricht die Gesamtkapazität eines Stores grob der Obergrenze pro Node multipliziert mit der Anzahl der Partitionen, die ein Node besitzt, angepasst um den Replikationsfaktor (Replicas halten zusätzliche Kopien, die außerhalb der Obergrenze für aktive Owner liegen). Daraus ergeben sich zwei praktische Konsequenzen:</p>
<ul>
<li class=""><strong>Dimensionieren Sie die Obergrenze nach Ihrem Working Set plus Eviction-Spielraum, nicht nach Ihren Gesamtdaten.</strong> Ein Node betreibt heute einen stets aktiven LRU-Cache, sodass kalte Daten unter Druck verworfen statt abgelehnt werden. Passt Ihr Working Set unter etwa 70 % der Obergrenze, mit Raum für das Band zum Atmen, bleibt die Eviction ruhig und das Hot Set resident. Dimensionieren Sie die Obergrenze unter Ihrem Working Set, haben Sie einen Cache gebaut, der ständig Daten verwirft, die er gleich wieder braucht — korrekt, aber langsam, und aus dem Sägezahn wird ein Mahlen.</li>
<li class=""><strong>Vergessen Sie den Replica-Footprint nicht.</strong> Die Obergrenze regelt besessene Daten, aber der tatsächliche RAM eines Nodes besteht aus besessenen Daten <em>plus</em> Replica-Kopien. Planen Sie beim Provisionieren von Speicher pro Pod oder pro Maschine beides ein, sonst läuft ein Node, der „auf seine Obergrenze" dimensioniert ist, immer noch heißer, als die Obergrenze vermuten lässt.</li>
</ul>
<p>Für die Kapazitätsrechnung im Zusammenhang mit Partitionen, Replikationsfaktor und Pod-Anzahl rechnet <a class="" href="https://clustron.io/de/blog/kubernetes-durability-sizing/">Einen Zaris-Cluster für Dauerhaftigkeit unter Kubernetes dimensionieren</a> die vollständige Berechnung durch.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-fazit">Das Fazit<a href="https://clustron.io/de/blog/the-memory-ceiling/#das-fazit" class="hash-link" aria-label="Direkter Link zur Das Fazit" title="Direkter Link zur Das Fazit" translate="no">​</a></h2>
<p>Ein In-Memory-Store, der ohne Grenze wächst, ist kein Feature — er ist ein Ausfall, der auf eine Traffic-Spitze wartet. Zaris behandelt jeden Node als begrenzten Puffer mit einer echten Obergrenze (standardmäßig 1 GiB, keine unbegrenzte Option), einem stets aktiven Sampled-LRU-Cache darunter und einem Hysterese-Band, das Platz in ruhigen Schüben zurückgewinnt, statt am Rand mit einer Eviction pro Schreibzugriff zu flattern. Die Gesamtobergrenze und das Limit pro Item sind zwei verschiedene Limits mit zwei verschiedenen typisierten Status, und der, der heute tatsächlich erreichbar ist — <code>ItemTooLarge</code> — verlangt eine andere Reaktion als eine Retry-Schleife. Dimensionieren Sie die Obergrenze nach Ihrem Working Set, planen Sie Replica-Kopien obendrauf ein, und der Node hält seine Linie, ohne dass Sie ihn beobachten müssen.</p>
<p>Das vollständige Modell, einschließlich der Policies, die die Engine mitbringt, aber noch nicht bereitstellt, steht in <a class="" href="https://clustron.io/de/docs/clustron/zaris/core-concepts/memory-and-eviction/">Speicher und Eviction</a>; die genauen Felder und Defaults stehen in der <a class="" href="https://clustron.io/de/docs/clustron/zaris/reference/configuration/#environment-variables">Konfigurationsreferenz</a>, und jeder Kapazitätsstatus ist in der <a class="" href="https://clustron.io/de/docs/clustron/zaris/reference/status-codes/">Status- und Exception-Referenz</a> katalogisiert.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Architektur</category>
            <category>Betrieb</category>
            <category>.NET</category>
        </item>
        <item>
            <title><![CDATA[Was über die Leitung geht: Die Anatomie eines Zaris-Records]]></title>
            <link>https://clustron.io/de/blog/what-crosses-the-wire/</link>
            <guid>https://clustron.io/de/blog/what-crosses-the-wire/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Jede Replikations- und Migrationsnachricht in Zaris trägt eine einzige Payload — einen StoredRecord. Dies ist eine Tour durch ihn, Feld für Feld: was darin steckt, warum ein Löschvorgang ein Record und keine Abwesenheit ist, warum zwei verschiedene Versionsnummern mitreisen und was den Node bewusst nie verlässt.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Die Anatomie eines gespeicherten Zaris-Records" 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>Das meiste von dem, was Zaris gut macht — einen Schreibzugriff auf ein Backup replizieren, eine hinterherhinkende Replica aufholen lassen, eine Partition auf einen neuen Owner umschalten —, läuft auf eine stille Frage hinaus: <em>Was genau ist die Dateneinheit, die zwischen Nodes wandert?</em> Stimmt diese Einheit, ist Konvergenz fast langweilig. Lässt sie ein Feld weg oder trägt das falsche, bekommen Sie einen wiederauferstandenen gelöschten Key, eine Replica, die Veraltetes nicht von Frischem unterscheiden kann, oder die Buchführung eines Nodes sickert auf einen anderen.</p>
<p>In Zaris ist diese Einheit ein einziger Typ — <code>StoredRecord</code> — und es lohnt sich, ihn Feld für Feld auseinanderzunehmen, denn jede Entscheidung darin ist tragend. Dieser Beitrag ist die Anatomie: was im Record steckt, wie er auf der Leitung gerahmt wird, warum ein Löschvorgang <em>als Record</em> statt als Abwesenheit reist, warum es zwei verschiedene Versionsnummern gibt und — genauso wichtig — was bewusst auf jedem Node zurückbleibt und die Leitung nie überquert.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eine-payload-überall">Eine Payload, überall<a href="https://clustron.io/de/blog/what-crosses-the-wire/#eine-payload-%C3%BCberall" class="hash-link" aria-label="Direkter Link zur Eine Payload, überall" title="Direkter Link zur Eine Payload, überall" translate="no">​</a></h2>
<p>Ein Key/Value-Store hat überraschend viele Stellen, an denen „ein Wert" präzise beschrieben werden muss: die In-Memory-Map, der Replikationsstrom, der Migrations-Snapshot, das Operation-Log. Zaris verwendet für alle <em>dieselbe</em> Record-Form. Was im Dictionary des Segments liegt, ist dasselbe, was eine Replica erhält, und dasselbe, was ein neuer Owner beim Cut-over erhält. Diese Einheitlichkeit ist der Grund, warum das <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">Operation-Log zwei Aufgaben zugleich erfüllen kann</a> und warum ein Migrations-Snapshot und ein Replica-Backfill keine getrennten Kodierungen brauchen.</p>
<p>Hier ist der gesamte Typ, reduziert auf seine Wire-Oberfläche:</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>Fünf Felder überqueren die Leitung. Zwei weitere leben auf dem Objekt, sind aber mit <code>[IgnoreMember]</code> markiert und reisen nie. Der Rest dieses Beitrags erklärt, warum jedes Feld dort sitzt, wo es sitzt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-rahmung-messagepack-mit-positionellen-keys">Die Rahmung: MessagePack mit positionellen Keys<a href="https://clustron.io/de/blog/what-crosses-the-wire/#die-rahmung-messagepack-mit-positionellen-keys" class="hash-link" aria-label="Direkter Link zur Die Rahmung: MessagePack mit positionellen Keys" title="Direkter Link zur Die Rahmung: MessagePack mit positionellen Keys" translate="no">​</a></h2>
<p>Zaris serialisiert Records mit <strong>MessagePack</strong>, und die <code>[Key(N)]</code>-Attribute sind der Grund, warum die Leitung klein bleibt. MessagePack kann ein Objekt auf zwei Arten serialisieren: als <em>Map</em> von Feldname → Wert (selbstbeschreibend, aber jede Nachricht wiederholt jeden Feldnamen als String) oder als positionelles <em>Array</em>, in dem jedes Feld durch einen ganzzahligen Index identifiziert wird. Die Attribute <code>[Key(0)]</code>, <code>[Key(1)]</code>, … wählen die Array-Form. Auf der Leitung ist ein Record ein kurzes Array von Werten in fester Reihenfolge — keine Feldnamen, keine JSON-artige Interpunktion, nur die Payloads.</p>
<p>Das ist wichtig, weil dieser Record die <em>heiße</em> Datenstruktur des gesamten Systems ist. Er wird bei jedem replizierten Schreibzugriff und jedem migrierten Key serialisiert; bei Hunderttausenden Schreibzugriffen pro Sekunde ist der Unterschied zwischen dem millionenfachen Versand von <code>"TimestampUtc"</code> als String und dem Versand von nichts als der Position keine Mikrooptimierung, sondern das Budget.</p>
<p>Das positionelle Schema legt auch die Regel für die sichere Weiterentwicklung des Formats fest: <strong>Ganzzahl-Keys sind Append-only.</strong> Ein neues Feld bekommt den nächsten unbenutzten Index; bestehende Indizes ändern nie ihre Bedeutung. Ein älterer Node, der Index 7 nicht kennt, ignoriert ihn einfach; ein neuerer Node, der einen alten Record liest, findet Index 7 nicht vor und fällt auf einen Default zurück. Wir werden ein Feld sehen — Labels bei Map-Key 3 innerhalb der Metadaten —, das sich genau auf dieses Verhalten „abwesend heißt Default" stützt, um im Nichtgebrauch nichts zu kosten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="im-entry-key-value-und-nicht-null-metadaten">Im Entry: Key, Value und nicht-null Metadaten<a href="https://clustron.io/de/blog/what-crosses-the-wire/#im-entry-key-value-und-nicht-null-metadaten" class="hash-link" aria-label="Direkter Link zur Im Entry: Key, Value und nicht-null Metadaten" title="Direkter Link zur Im Entry: Key, Value und nicht-null Metadaten" translate="no">​</a></h2>
<p><code>ZarisEntry</code> ist das Feld <code>Entry</code> — der Teil, den ein Benutzer als „seine Daten" erkennen würde:</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>Zwei Details hier lernt man nur, wenn man sich einmal verbrannt hat. Erstens ist <code>Value</code> als <code>object</code> typisiert, nicht als <code>byte[]</code> oder <code>string</code> — die konkrete Form des Wertes ist, was auch immer der Schreiber dort abgelegt hat, im Entry gerahmt; so trägt derselbe Record-Typ einen einfachen String, einen Zähler oder eine ganze <a class="" href="https://clustron.io/de/blog/data-structures-that-replicate/">replizierte Collection</a>, ohne dass der Record wissen müsste, welche.</p>
<p>Zweitens werden <code>Metadata</code> über einen Getter bereitgestellt, der nie null zurückgeben kann:</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 umgeht bei der Deserialisierung den Konstruktor und ruft Setter direkt auf, sodass ein einfacher Initialisierer <code>= new()</code> einen Round-Trip nicht überlebt — ein Record, der ohne Metadaten ankam, würde mit null deserialisiert, und jede nachgelagerte Aufrufstelle bräuchte eine Null-Prüfung. Der Getter macht die Garantie stattdessen strukturell: Metadaten sind <em>immer</em> da, auch wenn die Leitung keine trug. Eine Stelle, um die Invariante durchzusetzen, keine Null-Guards sonst irgendwo.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="metadaten-die-im-leeren-zustand-nichts-kosten">Metadaten, die im leeren Zustand nichts kosten<a href="https://clustron.io/de/blog/what-crosses-the-wire/#metadaten-die-im-leeren-zustand-nichts-kosten" class="hash-link" aria-label="Direkter Link zur Metadaten, die im leeren Zustand nichts kosten" title="Direkter Link zur Metadaten, die im leeren Zustand nichts kosten" translate="no">​</a></h3>
<p><code>EntryMetadata</code> trägt die optionalen Attribute eines Keys — <code>CreatedAt</code>, <code>Ttl</code>, <code>ContentType</code>, ein optionales Lease- oder Lock-Handle und Labels. Das Labels-Feld ist eine kleine Meisterklasse darin, nicht für zu bezahlen, was man nicht nutzt. Die meisten Keys haben keine Labels, und ein naives, pro Entry initialisiertes <code>Dictionary</code> ist reine Verschwendung — in einem realen Store hielten 390K Entries ohne Labels jeweils ein leeres 80-Byte-Dictionary, etwa 31 MB an Nichts.</p>
<p>Die Lösung ist zweiseitig. Auf dem Heap teilen sich Entries ohne Labels ein einziges statisches leeres Dictionary, sodass Leser weiterhin eine nicht-null (leere) Map sehen, aber nichts allokiert wird. Auf der Leitung ist die serialisierte Oberfläche null, wenn es keine Labels gibt:</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>Ein Entry ohne Labels schreibt <code>nil</code> an Index 3, und der Leser stellt daraus „keine Labels" wieder her. Hier zeigt das Append-only-Key-Schema seinen Wert: Der Normalfall kostet ein Byte, und das Feature ist in vollem Umfang da, sobald ein Key tatsächlich ein Label braucht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="zwei-zahlen-zwei-aufgaben-revision-und-itemversion">Zwei Zahlen, zwei Aufgaben: Revision und ItemVersion<a href="https://clustron.io/de/blog/what-crosses-the-wire/#zwei-zahlen-zwei-aufgaben-revision-und-itemversion" class="hash-link" aria-label="Direkter Link zur Zwei Zahlen, zwei Aufgaben: Revision und ItemVersion" title="Direkter Link zur Zwei Zahlen, zwei Aufgaben: Revision und ItemVersion" translate="no">​</a></h2>
<p>Die häufigste Verwirrung bei Zaris-Records ist, dass sie <em>zwei</em> monoton steigende Zahlen tragen. Sie sind nicht redundant — sie beantworten verschiedene Fragen, und sie zu vermischen würde die eine oder die andere Aufgabe brechen.</p>
<p><strong><code>ItemVersion</code> gilt pro Key.</strong> Sie beginnt bei 1 und wird um eins erhöht, jedes Mal, wenn <em>dieser Key</em> geschrieben wird. Das ist die Zahl, die der Client sieht, und die Zahl, gegen die das optimistische <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Compare-and-Swap</a> prüft: „nur committen, wenn dieser Key noch auf Version 7 steht." Wenn der Store ein Put anwendet, ist die neue Version schlicht die alte Version des Keys plus eins:</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> gilt pro Segment.</strong> Sie ist ein einziger Zähler, der von <em>jedem</em> Key der Partition geteilt und bei jedem Schreib- und Löschvorgang erhöht wird:</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>Damit ist <code>Revision</code> eine Totalordnung über alle Mutationen eines Segments, genau das, was Replikation und Recovery brauchen. Einer Replica ist es egal, dass der Key <code>user:1</code> auf seiner 7. Version ist; ihr kommt es darauf an, <em>welche segmentweiten Revisionen sie bereits gesehen hat</em>, damit sie alles ab diesem Punkt anfordern kann. Das Operation-Log ist nach Revision geschlüsselt, das Catch-up-Protokoll fordert „alles nach Revision R" an, und die Konfliktauflösung zwischen einem eingehenden und einem lokalen Record vergleicht Revisionen, nicht Versionen pro Key:</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>Also: <code>ItemVersion</code> ist die Einheit der <em>für den Client sichtbaren optimistischen Nebenläufigkeit</em> auf einem Key; <code>Revision</code> ist die Einheit der <em>Ordnung und Konvergenz über Nodes hinweg</em> im gesamten Segment. Derselbe Record, zwei Uhren, jede erledigt eine Aufgabe, die die andere nicht kann.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ein-löschvorgang-ist-ein-record-keine-abwesenheit">Ein Löschvorgang ist ein Record, keine Abwesenheit<a href="https://clustron.io/de/blog/what-crosses-the-wire/#ein-l%C3%B6schvorgang-ist-ein-record-keine-abwesenheit" class="hash-link" aria-label="Direkter Link zur Ein Löschvorgang ist ein Record, keine Abwesenheit" title="Direkter Link zur Ein Löschvorgang ist ein Record, keine Abwesenheit" translate="no">​</a></h2>
<p>Hier ist das Feld, das die Leute am meisten überrascht: <code>IsDeleted</code>. Wenn Sie in Zaris einen Key löschen, entfernt der Store den Entry nicht einfach aus seinem Dictionary. Er schreibt einen <code>StoredRecord</code> mit <code>IsDeleted = true</code> — einen <strong>Tombstone</strong> —, versieht ihn mit der nächsten Segment-Revision und hängt ihn als Löschung an das Operation-Log an:</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>Der Grund ist der ganze Sinn des Record-Formats. Wäre ein Löschvorgang bloß eine Abwesenheit — der Key verschwindet stillschweigend aus einer Map —, gäbe es nichts zu replizieren. Eine Replica, die den Moment der Löschung verpasst hat, würde den alten Wert ewig weiter ausliefern, denn „nichts" reist nicht und lässt sich nicht gegen die umgebenden Schreibzugriffe ordnen. Indem die Löschung ein erstklassiger Record mit eigener Revision wird, fließt sie durch genau denselben Replikations- und Catch-up-Pfad wie ein Schreibzugriff und ordnet sich an der richtigen Stelle in die Totalordnung des Segments ein.</p>
<p>Tombstones schützen auch vor einem subtileren Fehler: einem <em>wiederauferstehenden</em> veralteten Schreibzugriff. Stellen Sie sich eine Löschung bei Revision 50 und ein älteres Put für denselben Key, Revision 48, vor, das noch im Netzwerk unterwegs ist. Ohne Gedächtnis für die Löschung würde das späte Put landen und den Key von den Toten zurückholen. Zaris merkt sich die Lösch-Revision und weist alles ab, was bei oder vor ihr liegt:</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>Die Kehrseite von Tombstones ist, dass sie sich nicht ewig ansammeln dürfen, sonst würde eine löschlastige Workload Speicher verlieren. Zaris trimmt Lösch-Revisionen, sobald sie sicher unterhalb der ältesten Revision liegen, die irgendeine Replica noch braucht — dieselbe Low-Water-Mark-Logik, die das Operation-Log trimmt —, sodass der Tombstone seine Konvergenzaufgabe erfüllt und dann zurückgewonnen wird. (Die Trimm-Disziplin ist das Thema des <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">Operation-Log-Beitrags</a>.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-zu-hause-bleibt-die-ignoremember-felder">Was zu Hause bleibt: die <code>[IgnoreMember]</code>-Felder<a href="https://clustron.io/de/blog/what-crosses-the-wire/#was-zu-hause-bleibt-die-ignoremember-felder" class="hash-link" aria-label="Direkter Link zur was-zu-hause-bleibt-die-ignoremember-felder" title="Direkter Link zur was-zu-hause-bleibt-die-ignoremember-felder" translate="no">​</a></h2>
<p>Die beiden Felder, die der Record trägt, aber nie serialisiert, sind so bewusst gewählt wie die fünf, die er serialisiert. Beide sind mit <code>[IgnoreMember]</code> markiert, und beide sind <em>node-lokal by Design</em> — korrekt nur für den Node, der sie berechnet hat.</p>
<p><strong><code>AccountedBytes</code></strong> ist der Speicher, der diesem Record im Budget des Stores angerechnet wird, einmal beim Schreiben gestempelt, damit der Store Byte-Deltas melden kann, ohne den vorherigen Record neu zu vermessen. Es wird ausdrücklich <em>nicht</em> serialisiert, weil jeder Node es aus dem eigenen Key und Value des Records nach seinen eigenen Bilanzierungsregeln berechnet. Würde es mitreisen, trüge ein von Node A replizierter Record die Byte-Schätzung von A in die Bücher von Node B — die Buchführung eines Nodes würde stillschweigend die eines anderen verzerren. Mit <code>[IgnoreMember]</code> berechnet jeder Node es unabhängig neu, und ein per Replikation oder Migration eintreffender Record wird von dem Node bilanziert, der ihn nun hält.</p>
<p><strong><code>MaterializedValue</code></strong> ist eine träge aufgebaute, lebende In-Memory-Form einer nativen Collection (ein Hash, eine Liste, ein Set oder ein Sorted Set), die gecacht wird, damit Operationen auf Elementebene nicht jedes Mal den ganzen Wert neu deserialisieren müssen. Es ist reine lokale Beschleunigung. Die maßgebliche Form sind immer die gerahmten Wert-Bytes innerhalb von <code>Entry</code>; ein Record, der von einem anderen Node eintrifft, materialisiert diesen Cache träge beim ersten Zugriff. Ihn zu serialisieren hieße, den privaten Arbeitsspeicher eines Nodes zu versenden, als wären es Daten.</p>
<p>Die Disziplin hier ist eine klare, die man laut aussprechen sollte: <strong>Die Leitung trägt die Wahrheit, der Node trägt Annehmlichkeiten.</strong> Alles, was ein Node aus dem Record selbst neu berechnen kann — seine Speicherkosten, seine materialisierte Struktur —, bleibt lokal und wird mit <code>[IgnoreMember]</code> markiert. Alles, was zur <em>Identität und Ordnung</em> des Records gehört — Key, Value, Metadaten, die beiden Versionsnummern, das Tombstone-Flag —, überquert die Leitung. Die Attributgrenze ist genau die Grenze zwischen Wahrheit und Annehmlichkeit, und sie scharf zu halten macht den Record sicher kopierbar zwischen Nodes, ohne einen zweiten Gedanken.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-das-testbar-ist--und-getestet-wird">Warum das testbar ist — und getestet wird<a href="https://clustron.io/de/blog/what-crosses-the-wire/#warum-das-testbar-ist--und-getestet-wird" class="hash-link" aria-label="Direkter Link zur Warum das testbar ist — und getestet wird" title="Direkter Link zur Warum das testbar ist — und getestet wird" translate="no">​</a></h2>
<p>Weil die Record-Form tragend für die Konvergenz ist, wird ihr Wire-Verhalten durch Tests festgenagelt statt der Inspektion überlassen. Zwei Invarianten verdienen eigene Assertions: Das Tombstone-Flag muss einen Round-Trip überleben (ein verlorenes <code>IsDeleted</code> lässt einen gelöschten Key auf einer Replica wiederauferstehen), und <code>AccountedBytes</code> muss nach einem Round-Trip als null zurückkommen (Beweis, dass es nie gereist ist):</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>Diese lesen sich fast wie die These des ganzen Beitrags, als Assertions geschrieben: Was die Leitung überqueren muss, überlebt nachweislich; was nicht, verschwindet nachweislich.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-record-ist-der-vertrag">Der Record ist der Vertrag<a href="https://clustron.io/de/blog/what-crosses-the-wire/#der-record-ist-der-vertrag" class="hash-link" aria-label="Direkter Link zur Der Record ist der Vertrag" title="Direkter Link zur Der Record ist der Vertrag" translate="no">​</a></h2>
<p>Man denkt leicht, die schweren Probleme eines verteilten Stores lägen in den ausgefallenen Teilen — dem <a class="" href="https://clustron.io/de/blog/anatomy-of-a-failover/">Failover-Protokoll</a>, dem <a class="" href="https://clustron.io/de/blog/partition-cutover-protocol/">Cut-over-Handshake</a>, dem <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">Chaos-Testing</a>. Das tun sie. Aber diese Protokolle sind nur so korrekt wie das, was sie bewegen, und das, was sie bewegen, ist dieser eine kleine Record. Ein Tombstone statt einer Abwesenheit ist der Grund, warum eine Löschung konvergiert. Zwei Versionsnummern sind der Grund, warum die optimistische Nebenläufigkeit eines Keys und die Ordnung eines ganzen Segments entflochten bleiben. Positionelle MessagePack-Keys sind der Grund, warum der Hot Path billig bleibt und das Format dennoch wachsen kann. Und ein Paar <code>[IgnoreMember]</code>-Felder ist der Grund, warum die Replikation einen Record zwischen Nodes kopieren und darauf vertrauen kann, dass nichts Node-Lokales mitgereist ist.</p>
<p>Bringt man die Dateneinheit in Ordnung, haben die großen Protokolle etwas Solides, worauf sie stehen können. Das ist das ganze Argument dafür, einen Blogbeitrag auf fünf Felder und zwei, die zu Hause bleiben, zu verwenden.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Die Worker-Pool-Obergrenze: Wenn ein Node bei 25 % CPU ein Plateau erreicht]]></title>
            <link>https://clustron.io/de/blog/worker-pool-ceiling/</link>
            <guid>https://clustron.io/de/blog/worker-pool-ceiling/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein Zaris-Node, der Lesezugriffe mit knapp unter einer halben Million Ops/s bediente und dabei bei einem Viertel seiner CPU stand, war weder CPU-limitiert noch Client-limitiert — der Worker-Pool für das Request-Dispatching war die Obergrenze. Hier stehen der Mechanismus, die Lösung und wie Sie ehrlich mit ZARIS_SERVER_MAX_WORKERS abstimmen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Ein Node bei nahezu 25 % CPU mit flachem Durchsatz, weil der Dispatch-Worker-Pool die Obergrenze war" 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>Hier ist ein Benchmark-Ergebnis, das wie ein Widerspruch aussieht. Ein Zaris-Node, der eine leselastige Workload bedient, erreicht seine Obergrenze bei knapp unter einer halben Million Ops/s und weigert sich zu steigen — fügen Sie mehr Client-Verbindungen und mehr Lastgeneratoren hinzu, die Zahl bewegt sich nicht. Die naheliegende Diagnose lautet „er ist gesättigt." Nur sagt das CPU-Diagramm etwas anderes: Der Node steht bei etwa einem <em>Viertel</em> seiner verfügbaren CPU. Drei von vier Kernen sind untätig, und der Durchsatz ist flach.</p>
<p>Das ist kein CPU-limitierter Node und — wenn Sie das sorgfältig ausgeschlossen haben — auch kein Client-limitierter Benchmark. Es ist eine dritte Sache, und zwar ein Klassiker: Der <strong>Worker-Pool</strong> für das Request-Dispatching war schmaler, als die Arbeit es wollte, sodass Requests in der Warteschlange standen, während Kerne Däumchen drehten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="zuerst-die-üblichen-verdächtigen-ausschließen">Zuerst die üblichen Verdächtigen ausschließen<a href="https://clustron.io/de/blog/worker-pool-ceiling/#zuerst-die-%C3%BCblichen-verd%C3%A4chtigen-ausschlie%C3%9Fen" class="hash-link" aria-label="Direkter Link zur Zuerst die üblichen Verdächtigen ausschließen" title="Direkter Link zur Zuerst die üblichen Verdächtigen ausschließen" translate="no">​</a></h2>
<p>Zwei Erklärungen müssen ausscheiden, bevor diese überhaupt auf den Tisch kommt, denn sie sind weit häufiger.</p>
<p><strong>CPU-limitiert.</strong> Steht der Node bei oder nahe 100 % CPU und der Durchsatz ist flach, ist Ihnen tatsächlich die CPU ausgegangen — die Antwort sind mehr Kerne oder mehr Nodes, und Zaris <a class="" href="https://clustron.io/de/blog/reads-scale-with-cores/">skaliert Lesezugriffe durch zusätzliche Kerne</a>. Das ist hier nicht der Fall: Die CPU ist <em>untätig</em>.</p>
<p><strong>Client-limitiert.</strong> Ein einzelner Lastgenerator sättigt sich lange vor dem Server, und dann messen Sie Ihren Client, nicht Zaris. Ist der Durchsatz flach, während Sie <em>Server</em>-Kerne hinzufügen, verdächtigen Sie zuerst Ihren Client — fügen Sie Generatoren und Verbindungen hinzu, bis die Zahl auf der Serverseite tatsächlich reagiert. Wir haben diese Falle im Beitrag zum Lesen behandelt, und sie ist die, die Sie ausschließen müssen, bevor Sie weitermachen.</p>
<p>Was nach beiden übrig bleibt, ist der interessante Fall: genug angebotene Last, um den Node zu beschäftigen, reichlich CPU-Spielraum und trotzdem flacher Durchsatz. Wenn der Engpass im Server liegt, aber nicht die CPU ist, ist das, was man sich ansehen muss, die Nebenläufigkeit — wie viele Requests der Node <em>gleichzeitig bearbeitet</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-dispatch-pool-ist-das-nebenläufigkeitslimit">Der Dispatch-Pool ist das Nebenläufigkeitslimit<a href="https://clustron.io/de/blog/worker-pool-ceiling/#der-dispatch-pool-ist-das-nebenl%C3%A4ufigkeitslimit" class="hash-link" aria-label="Direkter Link zur Der Dispatch-Pool ist das Nebenläufigkeitslimit" title="Direkter Link zur Der Dispatch-Pool ist das Nebenläufigkeitslimit" translate="no">​</a></h2>
<p>Jeder Request, der an einem Zaris-Node ankommt, wird an einen Worker-Pool für das Request-Dispatching übergeben. Ein Worker nimmt den Request auf, routet ihn zur Partition, die den Key besitzt, führt die Operation gegen den In-Process-Store dieser Partition aus und schreibt die Antwort zurück. Bei einem Lesezugriff ist die Operation selbst ein Lookup im Speicher im Mikrosekundenbereich — wirklich winzig.</p>
<p>Die Breite dieses Pools — wie viele Worker gleichzeitig mitten in einem Request sein können — setzt eine harte Obergrenze für die Requests in Bearbeitung, und das ist eine andere Zahl als „wie schnell ist eine einzelne Operation". Der ursprüngliche Default leitete die Breite von einem kleinen Vielfachen von <code>Environment.ProcessorCount</code> ab. Auf einem Node mit 4 vCPUs sind zwei Mal die Kernzahl nur eine Handvoll Worker.</p>
<p>Hier ist die Feinheit, die das CPU-Diagramm falsch aussehen lässt. Würde jeder Worker seinen gesamten Slot damit verbringen, CPU für den Lookup zu verbrennen, wäre ein an die Kerne angepasster Pool genau richtig, und der Node <em>würde</em> 100 % CPU erreichen. Aber Worker verbringen nicht ihren ganzen Slot auf der CPU. Ein Worker parkt gelegentlich — eine <code>await</code>-Fortsetzung, ein Socket-Schreibvorgang, der nicht synchron abschließt, ein kurzer Lock auf eine gemeinsame Struktur. Ein geparkter Worker verbrennt keine CPU, aber <strong>er hält weiterhin seinen Pool-Slot</strong>. Bei nur einer Handvoll Slots genügen einige geparkte Worker, um den ganzen Node zum Stillstand zu bringen: Neue Requests stauen sich hinter ihnen, und weil die beschäftigten Worker meist warten statt zu rechnen, leuchten die Kerne nie auf. Flacher Durchsatz, untätige CPU. Die Pool-Breite war die Obergrenze, nicht das Silizium.</p>
<!-- -->
<p>Das Erkennungsmerkmal, das dies von einem Client-limitierten Benchmark unterscheidet: Requests <em>stauen sich am Server</em> (die Latenz steigt, der Durchsatz ist flach), während die <em>Server</em>-CPU niedrig bleibt. Client-limitiert sieht anders aus — der Server ist untätig, weil nichts schnell genug ankommt. Hier kommt Arbeit an; der Node nimmt nur nicht genug davon gleichzeitig an.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-lösung-den-pool-für-die-arbeit-dimensionieren-nicht-für-die-kernzahl">Die Lösung: den Pool für die Arbeit dimensionieren, nicht für die Kernzahl<a href="https://clustron.io/de/blog/worker-pool-ceiling/#die-l%C3%B6sung-den-pool-f%C3%BCr-die-arbeit-dimensionieren-nicht-f%C3%BCr-die-kernzahl" class="hash-link" aria-label="Direkter Link zur Die Lösung: den Pool für die Arbeit dimensionieren, nicht für die Kernzahl" title="Direkter Link zur Die Lösung: den Pool für die Arbeit dimensionieren, nicht für die Kernzahl" translate="no">​</a></h2>
<p>Die Reparatur ist nicht exotisch. Lesezugriffe sind Lookups im Speicher im Mikrosekundenbereich, also sollte der Pool standardmäßig <em>breit</em> eingestellt sein — breit genug, dass eine Handvoll geparkter Worker den Node nicht aushungern kann — und er sollte abstimmbar sein, weil die richtige Breite vom Node und von der Workload abhängt, nicht von einem festen Vielfachen der Kernzahl.</p>
<p>Die Standardbreite des Pools beträgt jetzt <code>CPU-Anzahl × 16</code>, begrenzt auf ein sinnvolles Band von 64–1024, mit einer Untergrenze von <code>max(4, CPU-Anzahl × 4)</code>. Zwei Umgebungsvariablen überschreiben sie:</p>
<table><thead><tr><th>Variable</th><th>Default</th><th>Was sie bewirkt</th></tr></thead><tbody><tr><td><code>ZARIS_SERVER_MAX_WORKERS</code></td><td>CPU-Anzahl × 16 (begrenzt auf 64–1024)</td><td>Harte Überschreibung (1–4096) der Breite des Dispatch-Pools. Erhöhen Sie sie, wenn ein Node freie CPU hat und Requests sich stauen.</td></tr><tr><td><code>ZARIS_SERVER_MIN_WORKERS</code></td><td>max(4, CPU-Anzahl × 4)</td><td>Untergrenze (1–4096) der Pool-Breite.</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>Eine Eigenschaft ist hervorzuheben: Dies ist eine Änderung <strong>ausschließlich auf der Serverseite</strong>. Das Wire-Protokoll ändert sich nicht, das Client-SDK ändert sich nicht, und es gibt kein NuGet-Update, dem Sie nachlaufen müssten — Sie stimmen einen Node ab, indem Sie ihn mit einer anderen Umgebung neu starten, und jeder bestehende Client spricht unverändert weiter mit ihm.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ehrlich-abstimmen--verbreitern-ist-nicht-kostenlos">Ehrlich abstimmen — Verbreitern ist nicht kostenlos<a href="https://clustron.io/de/blog/worker-pool-ceiling/#ehrlich-abstimmen--verbreitern-ist-nicht-kostenlos" class="hash-link" aria-label="Direkter Link zur Ehrlich abstimmen — Verbreitern ist nicht kostenlos" title="Direkter Link zur Ehrlich abstimmen — Verbreitern ist nicht kostenlos" translate="no">​</a></h2>
<p>Der Reflex nach der Lektüre ist, <code>ZARIS_SERVER_MAX_WORKERS</code> auf jedem Node bis zur Obergrenze hochzudrehen. Tun Sie das nicht. Ein breiterer Pool hilft in genau einer Situation und bewirkt in den anderen nichts — oder schadet leicht.</p>
<ul>
<li class=""><strong>Er hilft, wenn Requests sich stauen, während die CPU untätig ist.</strong> Das ist der Fall in diesem ganzen Beitrag: Die Nebenläufigkeit der Requests in Bearbeitung ist das Limit, also füllt deren stärkere Zulassung die Kerne. Hier sehen Sie den Durchsatz springen.</li>
<li class=""><strong>Er bewirkt nichts, wenn Sie bereits CPU-limitiert sind.</strong> Steht der Node bei 100 % CPU, zaubern mehr Worker keinen Durchsatz aus dem Nichts — sie fügen einer Maschine ohne freie Zyklen nur Overhead durch Kontextwechsel hinzu. Mehr Worker sind nicht mehr Rechenleistung.</li>
<li class=""><strong>Er kann schaden, wenn man ihn auf eine absurde Breite treibt.</strong> Tausende Worker, die einen Node mit 2 vCPUs belasten, verbringen echte Zeit mit Kontextwechseln statt mit dem Bedienen. Deshalb gibt es eine Begrenzung, und deshalb endet der Regler bei 4096 statt bei unendlich.</li>
</ul>
<p>Die ehrliche Abstimmungsschleife lautet: Achten Sie auf die Signatur — steigende Latenz und flacher Durchsatz, <em>während die CPU deutlich unter der Sättigung liegt</em> —, erhöhen Sie <code>MAX_WORKERS</code> und messen Sie. Steigt die CPU und folgt der Durchsatz, war der Pool Ihre Obergrenze. Bewegt sich nichts, war er es nicht, und Sie sollten den Regler zurückstellen und anderswo suchen (den Netzwerkpfad, den Client oder den tatsächlichen Bedarf an mehr Nodes).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cpu-dort-messen-wo-sie-wirklich-stimmt">CPU dort messen, wo sie wirklich stimmt<a href="https://clustron.io/de/blog/worker-pool-ceiling/#cpu-dort-messen-wo-sie-wirklich-stimmt" class="hash-link" aria-label="Direkter Link zur CPU dort messen, wo sie wirklich stimmt" title="Direkter Link zur CPU dort messen, wo sie wirklich stimmt" translate="no">​</a></h2>
<p>Diese ganze Untersuchung hängt davon ab, der CPU-Zahl zu vertrauen, und diese Zahl ist leicht falsch zu bekommen.</p>
<ul>
<li class=""><strong>Lesen Sie die CPU aus der cgroup, nicht aus einer Schätzung des Cluster-Metrics-Servers.</strong> Besonders unter Kubernetes hinkt und glättet der Wert des Metrics-Servers — er sagt Ihnen, ein Node stehe bei 40 %, obwohl die cgroup-Bilanzierung sagt, er sei am Anschlag, oder umgekehrt. Diagnostizieren Sie einen Fall „flacher Durchsatz, untätige CPU" aus der cgroup, sonst jagen Sie einer Zahl nach, die nicht real ist.</li>
<li class=""><strong>Lokale Benchmarks sind Host-limitiert und verbergen die Obergrenze.</strong> Auf einem Laptop kämpfen Lastgenerator und Server um dieselben Kerne, sodass Sie dem Server nie saubere, ausreichende Last präsentieren — das Plateau, das Sie messen, ist Ihr Testaufbau, nicht Zaris. Die echte Worker-Pool-Obergrenze zeigte sich im großen Maßstab auf einem verwalteten Cluster mit Last von getrennten Maschinen, nicht auf einer Entwicklerkiste. Validieren Sie den Durchsatz dort, wo der Client nicht der Engpass sein kann.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-fazit">Das Fazit<a href="https://clustron.io/de/blog/worker-pool-ceiling/#das-fazit" class="hash-link" aria-label="Direkter Link zur Das Fazit" title="Direkter Link zur Das Fazit" translate="no">​</a></h2>
<p>Eine flache Durchsatzkurve hat drei sehr verschiedene Ursachen, und das CPU-Diagramm sagt Ihnen, welche Sie vor sich haben. Bei 100 % CPU sind Sie CPU-limitiert — fügen Sie Nodes hinzu. Ist der <em>Server</em> untätig, weil nichts schnell genug ankommt, sind Sie Client-limitiert — reparieren Sie den Test. Aber ein Node, der bei einem Viertel seiner CPU flach ist und bei dem sich Requests sichtbar stauen, ist keines von beidem: Es ist ein Nebenläufigkeitslimit, und bei Zaris ist das der Dispatch-Worker-Pool. Der Default ist jetzt breit genug, dass die meisten Workloads nie darauf stoßen, und <code>ZARIS_SERVER_MAX_WORKERS</code> gibt es für die, die es tun — um bewusst erhöht zu werden, wenn das CPU-Diagramm es sich verdient hat.</p>
<p>Die vollständige Oberfläche der Umgebungsvariablen und die genauen Defaults finden Sie in der <a class="" href="https://clustron.io/de/docs/clustron/zaris/reference/configuration/#environment-variables">Konfigurationsreferenz</a>. Wenn Sie einen Cluster für Durchsatz dimensionieren, führt der <a class="" href="https://clustron.io/de/docs/clustron/zaris/admin-guide/benchmarking/">Benchmarking-Leitfaden</a> durch die Einstellungen, die die Zahlen tatsächlich bewegen, und <a class="" href="https://clustron.io/de/blog/reads-scale-with-cores/">Lesezugriffe, die mit Ihren Kernen skalieren</a> behandelt die andere Hälfte der Geschichte — warum, sobald der Pool aus dem Weg ist, zusätzliche Kerne Lesekapazität hinzufügen, statt an eine Mauer mit einer einzigen Event-Loop zu stoßen.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Performance</category>
            <category>.NET</category>
            <category>Betrieb</category>
        </item>
        <item>
            <title><![CDATA[Drop-in-Cache für ASP.NET Core: IDistributedCache und HybridCache auf Zaris]]></title>
            <link>https://clustron.io/de/blog/aspnet-core-distributed-cache/</link>
            <guid>https://clustron.io/de/blog/aspnet-core-distributed-cache/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris liefert drei ASP.NET-Core-Integrationspakete — ein standardkonformes IDistributedCache, ein zweistufiges HybridCache mit Stampede-Schutz und Tag-Invalidierung sowie einen Session-Store mit Sliding-Expiry —, damit eine bestehende App auf einem Zaris-Cluster cachen kann, ohne den Code anzufassen, der den Cache nutzt. Hier steht, was jedes Paket leistet und wo jeweils die ehrlichen Grenzen liegen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Drop-in-verteilter-Cache für ASP.NET Core auf 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>Die meisten Gründe, in einer .NET-App zu einem verteilten Cache zu greifen, nennen den Cache gar nicht beim Namen. Sie aktivieren Output Caching, und es verlangt ein <code>IDistributedCache</code>. Sie aktivieren Sessions, und die Middleware will einen Backing-Store. Sie schalten das Teilen von Data-Protection-Schlüsseln über Instanzen hinweg ein, und wieder braucht es einen Ort für Bytes, den jeder Node sieht. Der Anwendungscode ist gegen eine <em>Abstraktion</em> geschrieben; offen ist nur, welche Implementierung Sie beim Start registrieren.</p>
<p>Zaris beantwortet diese Frage mit drei kleinen NuGet-Paketen, die jeweils eine bestimmte ASP.NET-Core-Nahtstelle bedienen: ein schlichtes <code>IDistributedCache</code>, ein zweistufiges <code>HybridCache</code> und ein Session-Store. Sie registrieren beim Start eine Zeile, und der Rest Ihrer App — einschließlich Drittanbieter-Bibliotheken, die nur die Abstraktion kennen — cacht auf einem Zaris-Cluster, ohne dass sich der Code ändert, der den Cache nutzt. Dieser Beitrag geht alle drei durch und ist dabei bewusst klar darüber, wo jedes ehrlicherweise endet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-drei-pakete">Die drei Pakete<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#die-drei-pakete" class="hash-link" aria-label="Direkter Link zur Die drei Pakete" title="Direkter Link zur Die drei Pakete" translate="no">​</a></h2>
<table><thead><tr><th>Paket</th><th>Implementiert</th><th>Verwenden für</th></tr></thead><tbody><tr><td><code>Clustron.Zaris.DistributedCache</code></td><td><code>IDistributedCache</code></td><td>Jede Bibliothek oder jeden Code, der die Standard-Abstraktion für verteilte Caches anfordert</td></tr><tr><td><code>Clustron.Zaris.HybridCache</code></td><td><code>HybridCache</code> (aus <code>Microsoft.Extensions.Caching.Hybrid</code>)</td><td>Einen Hot Path, der ein prozessinternes L1 vor dem gemeinsamen Cache wünscht, mit Stampede-Schutz</td></tr><tr><td><code>Clustron.Zaris.AspNetCore.Session</code></td><td><code>ISessionStore</code></td><td>ASP.NET-Core-Sessions mit gleitendem Idle-Timeout</td></tr></tbody></table>
<p>Alle drei setzen auf demselben Zaris-.NET-Client auf. Sie erfinden keinen Seitenkanal — sie lösen aus einer Connection-String einen normalen Zaris-Client auf und übersetzen <code>Get</code>/<code>Set</code>/<code>Remove</code> in <code>GetAsync</code>/<code>PutAsync</code>/<code>DeleteAsync</code> des Clients. Wenn Sie <a class="" href="https://clustron.io/de/blog/connection-strings/">Ein Connection-String, jede Plattform</a> gelesen haben, wirkt die Verdrahtung vertraut: Der Cache ist nur ein weiterer Consumer eines benannten Stores.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-das-standard-idistributedcache">1. Das Standard-<code>IDistributedCache</code><a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#1-das-standard-idistributedcache" class="hash-link" aria-label="Direkter Link zur 1-das-standard-idistributedcache" title="Direkter Link zur 1-das-standard-idistributedcache" translate="no">​</a></h2>
<p>Das ist das Drop-in. <code>IDistributedCache</code> ist die Schnittstelle, die <code>Microsoft.Extensions.Caching.Abstractions</code> seit Jahren bereitstellt, und ein großer Teil des .NET-Ökosystems — Session, Output Caching, Auth-Ticket-Stores, Drittanbieter-Bibliotheken — hängt genau von dieser Schnittstelle ab und von nichts weiterem. Registrieren Sie Zaris als Implementierung, und all das wird von Ihrem Cluster getragen.</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>Die erste Zeile registriert einen Zaris-Client unter dem DI-Schlüssel <code>app-cache</code>; die zweite registriert ein <code>IDistributedCache</code>, das ihn nutzt. Von da an funktioniert alles, was ein <code>IDistributedCache</code> entgegennimmt — auch Code, den Sie nicht geschrieben haben:</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>Intern ist <code>GetAsync</code> ein <code>client.GetAsync&lt;byte[]&gt;</code> mit angewendetem konfiguriertem Präfix, und ein Cache-Miss (der Schlüssel existiert nicht) liefert <code>null</code> — genau den Vertrag, den die Schnittstelle verspricht. <code>SetAsync</code> mit <code>AbsoluteExpirationRelativeToNow</code> bildet dieses Fenster direkt auf ein Zaris-TTL ab, sodass der Store den Eintrag für Sie ablaufen lässt.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="zwei-ehrliche-grenzen">Zwei ehrliche Grenzen<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#zwei-ehrliche-grenzen" class="hash-link" aria-label="Direkter Link zur Zwei ehrliche Grenzen" title="Direkter Link zur Zwei ehrliche Grenzen" translate="no">​</a></h3>
<p><strong>Nur die relative absolute Ablaufzeit wird abgebildet.</strong> Das Zaris-<code>IDistributedCache</code> liest <code>AbsoluteExpirationRelativeToNow</code> und macht daraus ein TTL. Setzen Sie keine Ablaufzeit, wird der Eintrag ohne TTL geschrieben und lebt, bis er per Eviction entfernt oder überschrieben wird. <code>SlidingExpiration</code> wird in diesem Allzweck-Cache <strong>nicht</strong> berücksichtigt — sein <code>Refresh</code>/<code>RefreshAsync</code> ist ein No-op. Das ist eine bewusste Abgrenzung, kein Versehen: Sliding Expiration ist ein Session-Thema, und das unten beschriebene Session-Paket implementiert sie ordentlich, statt sie hier halb zu implementieren.</p>
<p><strong>Sie können mehr als eines registrieren, und standardmäßig gewinnt das erste.</strong> Jeder Aufruf von <code>AddClustronDistributedCache</code> registriert eine benannte Cache-Instanz, die an einen bestimmten Store gebunden ist. Die zuerst registrierte wird zum appweiten <code>IDistributedCache</code>; mit <code>setAsDefault: true</code> machen Sie stattdessen eine spätere Registrierung zum Default. Wenn Sie mehrere Caches auf verschiedenen Stores wollen, lösen Sie diese explizit über <code>IClustronCacheProvider.GetCache("other-store")</code> auf, statt sich auf den ambienten Default zu verlassen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-hybridcache--eine-prozessinterne-stufe-vor-dem-cluster">2. <code>HybridCache</code> — eine prozessinterne Stufe vor dem Cluster<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#2-hybridcache--eine-prozessinterne-stufe-vor-dem-cluster" class="hash-link" aria-label="Direkter Link zur 2-hybridcache--eine-prozessinterne-stufe-vor-dem-cluster" title="Direkter Link zur 2-hybridcache--eine-prozessinterne-stufe-vor-dem-cluster" translate="no">​</a></h2>
<p>Die neuere <code>HybridCache</code>-Abstraktion von .NET löst ein Problem, das <code>IDistributedCache</code> Ihnen überlässt: die Kombination aus einem schnellen lokalen (L1) Cache und einem gemeinsamen verteilten (L2) Cache, plus <em>Stampede-Schutz</em> — sicherzustellen, dass nicht eine Thundering Herd gleichzeitiger Requests die teure Factory auf einmal ausführt, wenn ein beliebter Schlüssel abläuft.</p>
<p>Zaris implementiert die abstrakte Klasse <code>HybridCache</code> direkt. Der Kniff: <strong>Beide Stufen sind Zaris-Stores, die Sie benennen</strong> — L1 zeigen Sie auf einen prozessinternen <a class="" href="https://clustron.io/de/blog/embedded-inproc-mode/">Embedded-Store</a> (kein Netzwerk, kein Serialisierungs-Hop aus der Maschine heraus) und L2 auf den verteilten 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">"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>Der Lesepfad ist die klassische zweistufige Suche, und es lohnt sich, ihn vollständig zu sehen, denn jede Garantie unten ergibt sich daraus:</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>L1 wird zuerst ohne Lock geprüft. Bei einem Miss nimmt der Aufrufer ein <strong>Lock pro Schlüssel</strong>, prüft L1 erneut (falls ein anderer Aufrufer es gerade gefüllt hat), dann L2; nur wenn beide verfehlen, führt er die Factory aus, schreibt L2 und dann L1. Die TTLs stammen aus <code>HybridCacheEntryOptions</code> — <code>Expiration</code> für L2, <code>LocalCacheExpiration</code> für L1 —, und die Implementierung begrenzt L1 auf höchstens L2, sodass eine lokale Kopie die gemeinsame nie überleben kann. Ohne Optionen betragen beide standardmäßig fünf Minuten.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="stampede-schutz-gemessen">Stampede-Schutz, gemessen<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#stampede-schutz-gemessen" class="hash-link" aria-label="Direkter Link zur Stampede-Schutz, gemessen" title="Direkter Link zur Stampede-Schutz, gemessen" translate="no">​</a></h3>
<p>Das Lock pro Schlüssel ist der Stampede-Schutz. Der Test des Pakets feuert zwanzig gleichzeitige <code>GetOrCreateAsync</code>-Aufrufe auf einen kalten Schlüssel ab, jeweils mit einer Factory, die 100 ms schläft, und prüft, dass die Factory <strong>genau einmal</strong> lief — neunzehn Aufrufer warten auf das Lock, und der, der den Cache gefüllt hat, bedient alle. So wird ein potenzieller zwanzigfacher Hammerschlag auf Ihre Datenbank zu einem einzigen Ladevorgang.</p>
<p>Die ehrliche Grenze: Dieses Lock lebt in einem <code>ConcurrentDictionary&lt;string, SemaphoreSlim&gt;</code> <strong>innerhalb des Prozesses</strong>. Es bricht den Stampede innerhalb einer App-Instanz. Sind zehn App-Instanzen im selben Augenblick auf demselben Schlüssel kalt, können Sie weiterhin bis zu zehn Factory-Läufe sehen, einen je Prozess — nicht einen clusterweit. Das ist die normale Form des clientseitigen Stampede-Schutzes, und für den üblichen Fall (eine Handvoll Instanzen, eine Factory, die teuer, aber nicht katastrophal ist, ein paar Mal auszuführen) genau das, was Sie wollen. Brauchen Sie strikt einen Ladevorgang für die gesamte Flotte, ist das ein Problem für verteilte Locks, kein Cache-Feature — und eine Aufgabe, die sich besser mit dem Compare-and-Swap-Claim-Muster aus <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Race-freie Koordination</a> modellieren lässt.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="tag-invalidierung-und-warum-sie-lazy-ist">Tag-Invalidierung, und warum sie lazy ist<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#tag-invalidierung-und-warum-sie-lazy-ist" class="hash-link" aria-label="Direkter Link zur Tag-Invalidierung, und warum sie lazy ist" title="Direkter Link zur Tag-Invalidierung, und warum sie lazy ist" translate="no">​</a></h3>
<p><code>HybridCache</code> erlaubt es, Einträge zu taggen und eine ganze Gruppe auf einmal zu invalidieren:</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 implementiert das mit Zeitstempeln statt mit einem aktiven Sweep. Jeder Eintrag wird mit zwei Metadaten-Labels geschrieben: dem Zeitpunkt der Erstellung und seinen Tags. <code>RemoveByTagAsync("catalog")</code> schreibt einfach einen Schlüssel, der festhält, <em>„catalog wurde zum Zeitpunkt T invalidiert."</em> Beim nächsten Lesen eines getaggten Eintrags vergleicht der Cache: Wurde irgendein Tag des Eintrags <em>nach</em> dessen Erstellung invalidiert, wird der Eintrag als Miss behandelt, und die Factory läuft erneut. Der Test bestätigt es — Eintrag taggen, Tag invalidieren, erneut lesen, und die Factory wird ein zweites Mal aufgerufen.</p>
<p>Der Zielkonflikt, den Sie kennen sollten: Die Invalidierung ist <strong>lazy und lesegetriggert</strong>, kein eifriges Löschen. <code>RemoveByTagAsync</code> aufzurufen ist O(1) — es schreibt einen Marker und kehrt zurück —, aber die veralteten Einträge selbst bleiben im Store, bis sie jemand liest (und einen frischen Wert erhält) oder ihr TTL abläuft. Sie erhalten billige, sofortige <em>logische</em> Invalidierung; Sie erhalten keine sofortige Rückgewinnung des Speichers, den diese Einträge belegen. Für Cache-Daten ist das genau der richtige Handel. Verlassen Sie sich nur nicht darauf, um Daten aus Korrektheitsgründen zwangsweise zu löschen — dafür löscht <code>RemoveAsync(key)</code> beide Stufen ausdrücklich.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-sessions-mit-echter-sliding-expiration">3. Sessions, mit echter Sliding Expiration<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#3-sessions-mit-echter-sliding-expiration" class="hash-link" aria-label="Direkter Link zur 3. Sessions, mit echter Sliding Expiration" title="Direkter Link zur 3. Sessions, mit echter Sliding Expiration" translate="no">​</a></h2>
<p>ASP.NET-Core-Sessions brauchen einen Store, der etwas tut, was der allgemeine Cache bewusst nicht tut: die Ablaufzeit bei jedem Request, der die Session berührt, <strong>gleiten</strong> zu lassen, sodass die Session eines aktiven Benutzers ihm nie unter den Händen ablaufen, die eines inaktiven aber irgendwann schon. Das Session-Paket ist ein eigens gebautes <code>IDistributedCache</code>, das genau diese Semantik berücksichtigt und mit einem Aufruf eingebunden wird:</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>Geben Sie eine Connection-String an, registriert es den Backing-Zaris-Store für Sie; lassen Sie sie weg, bindet es sich an einen Store, den Sie bereits unter <code>StoreName</code> registriert haben. In beiden Fällen nutzen Sie <code>HttpContext.Session</code> danach genau wie immer.</p>
<p>Entscheidend ist <code>Refresh</code>. Lädt die Session-Middleware eine Session, die gelesen, aber nicht verändert wurde, ruft sie <code>RefreshAsync</code> auf, um sie am Leben zu halten. Der Zaris-Session-Cache implementiert das als Read-then-Re-put mit einem frischen TTL in Höhe des Idle-Timeouts — jeder Request, der die Session berührt, schiebt also das 30-Minuten-Fenster weiter. Ein Schreibvorgang (<code>SetAsync</code>) leitet sein TTL aus der Sliding- oder absoluten Ablaufzeit des Eintrags ab und fällt auf den konfigurierten Idle-Timeout zurück. Das Ergebnis ist das übliche ASP.NET-Core-Session-Verhalten, getragen von einem Zaris-Cluster, das den Neustart einer App-Instanz übersteht, weil die Session-Daten im Store liegen und nicht im Speicher der Instanz.</p>
<p>Ein bewusster Implementierungshinweis aus dem Code selbst: <code>Refresh</code> ist ein Read-plus-Re-put statt eines nativen „setze einfach das TTL zurück"-Aufrufs. Das kostet einen zusätzlichen Roundtrip, hält das Sliding aber auf <em>jedem</em> Zaris-Deployment korrekt — auch dem eingebetteten prozessinternen — ohne von einem serverseitigen TTL-Reset-Primitiv abzuhängen. Korrektheit über jede Topologie hinweg schlug das Einsparen eines Roundtrips bei einer davon.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-richtige-auswählen">Das Richtige auswählen<a href="https://clustron.io/de/blog/aspnet-core-distributed-cache/#das-richtige-ausw%C3%A4hlen" class="hash-link" aria-label="Direkter Link zur Das Richtige auswählen" title="Direkter Link zur Das Richtige auswählen" translate="no">​</a></h2>
<ul>
<li class="">Greifen Sie zu <strong><code>IDistributedCache</code></strong>, wenn etwas es bereits anfordert oder wenn Sie den einfachsten gemeinsamen Cache wollen und eine Bibliothek ihn für Sie nutzt. Es ist die Nahtstelle mit maximaler Kompatibilität.</li>
<li class="">Greifen Sie zu <strong><code>HybridCache</code></strong> auf einem wirklich heißen Lesepfad, auf dem sich ein prozessinternes L1 lohnt und ein Stampede auf einen kalten, beliebten Schlüssel wehtäte. Sie zahlen mit einem zusätzlichen beweglichen Teil (zwei Stores, lokaler Speicher für L1); Sie erhalten wiederholte Lesevorgänge mit nahezu null Latenz und eine auf einen Ladevorgang je Instanz zusammengefallene Herde.</li>
<li class="">Greifen Sie zu <strong><code>AddZarisSession</code></strong> speziell für ASP.NET-Core-Sessions — es ist das mit echter Sliding Expiration und kommt dem <code>IDistributedCache</code> Ihrer App nicht in die Quere.</li>
</ul>
<p>Keines der drei verlangt, dass Sie eine cachespezifische API lernen. Sie treffen das Framework dort, wo es die Abstraktion ohnehin definiert, registrieren sich in einer Zeile und legen die Bytes auf einen Cluster, den Sie unabhängig von der App davor skalieren, absichern und auf Kubernetes betreiben können. Die beste Integration ist die, die Ihr Anwendungscode nie bemerken muss — und erfreulich oft ist das genau die, die Sie bekommen.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>.NET</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Anatomie eines Failovers: Was mit einem laufenden Schreibvorgang geschieht, wenn der Owner stirbt]]></title>
            <link>https://clustron.io/de/blog/anatomy-of-a-failover/</link>
            <guid>https://clustron.io/de/blog/anatomy-of-a-failover/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ein Node, der die aktive Kopie Ihrer Partition hält, stirbt, während Schreibvorgänge auf ihm landen. Verfolgen Sie die exakte Abfolge, die Zaris als Nächstes ausführt — Erkennung und Bestätigung, eine Epoch-gefencte Promotion eines Replikats, ein Possession-Proof, der entscheidet, ob der neue Owner sofort bedient oder zuerst synchronisiert, und das Client-Reroute, das Ihren Retry beim neuen Owner landen lässt — plus die ehrlichen Fälle, in denen ein Schreibvorgang tatsächlich verloren geht.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Anatomie eines Failovers in 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>Ein Failover ist der Moment, in dem ein verteilter Store am ehrlichsten zeigt, was er wirklich ist. Im Normalbetrieb sehen alle Stores gleich aus: Ein Schreibvorgang landet auf einem Owner, wird bestätigt, weiter geht's. Das Design zeigt sich erst, wenn der Owner <em>mitten im Satz verschwindet</em> — ein Node hält die aktive Kopie einer Ihrer Partitionen, Schreibvorgänge landen gerade auf ihm, und dann ist er weg. Hart beendet. Netzwerkpartitioniert. OOM. Der Cluster hat ein paar Sekunden, um das zu bemerken, sich auf einen neuen Owner zu einigen und den Verkehr wieder fließen zu lassen — und ob er das korrekt tut, entscheidet darüber, ob Ihr bestätigter Schreibvorgang überlebt oder stillschweigend verdunstet.</p>
<p>Dieser Beitrag folgt dieser Abfolge von Anfang bis Ende, in der Reihenfolge, in der Zaris sie tatsächlich ausführt, und benennt die Fälle, in denen die Antwort wirklich „der Schreibvorgang ist verloren" lautet — denn ein Failover-Beitrag, der nur den Happy Path beschreibt, ist nicht lesenswert.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-eine-invariante-die-den-rest-handhabbar-macht">Die eine Invariante, die den Rest handhabbar macht<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#die-eine-invariante-die-den-rest-handhabbar-macht" class="hash-link" aria-label="Direkter Link zur Die eine Invariante, die den Rest handhabbar macht" title="Direkter Link zur Die eine Invariante, die den Rest handhabbar macht" translate="no">​</a></h2>
<p>Vor der Zeitachse die eine Designentscheidung, an der alles Weitere hängt: <strong>Zaris trennt die Partition-<em>Map</em> von der Partition-<em>Runtime</em>.</strong></p>
<p>Die Partition-Map ist die langsame, bedachte Zuordnung — <em>welche Nodes sind für Partition P zuständig</em>: ein bevorzugter Primary und seine Replikate. Sie ändert sich nur bei echten Topologieereignissen (ein Node tritt dauerhaft bei oder aus, ein Scale-out, ein Rebalance) und über eine geplante, versionierte Neuberechnung.</p>
<p>Die Runtime ist die schnelle, aktuelle Antwort auf eine andere Frage — <em>welcher zugewiesene Node ist gerade der aktive Owner, der P bedient, und in welchem Gesundheitszustand</em>. Bei einem Failover <strong>bewegt sich die Map überhaupt nicht.</strong> Nur der Runtime-Active wechselt. Das ist die im Failover-Service selbst festgehaltene Invariante: Ein unabhängiges Ausfallsignal öffnet das Runtime-Failover-Gate, „PartitionMap remains stable and only PartitionRuntimeMap.Active is allowed to move during failover."</p>
<p>Warum das wichtig ist: Eine Partition-Map neu zu berechnen ist teuer und gefährlich — sie kann gesunde Partitionen umverteilen und Datenmigration auslösen. Ein sterbender Node sollte das <em>nicht</em> auslösen. Er sollte eine billige, lokale Runtime-Entscheidung auslösen: ein überlebendes Replikat derselben Partition auswählen und zum Active machen. Die Map sagt weiterhin „P gehört n1, n2, n3"; die Runtime wechselt von „active = n1" auf „active = n2". Dass diese beiden Ebenen getrennt bleiben, macht das Failover schnell, ohne leichtsinnig zu sein.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-1--erkennen-aber-dem-ersten-gerücht-nicht-glauben">Schritt 1 — erkennen, aber dem ersten Gerücht nicht glauben<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#schritt-1--erkennen-aber-dem-ersten-ger%C3%BCcht-nicht-glauben" class="hash-link" aria-label="Direkter Link zur Schritt 1 — erkennen, aber dem ersten Gerücht nicht glauben" title="Direkter Link zur Schritt 1 — erkennen, aber dem ersten Gerücht nicht glauben" translate="no">​</a></h2>
<p>Das Failover wird vom Leader koordiniert. Ein Node im Cluster ist der gewählte Leader, und nur der Leader befördert — ein Nicht-Leader, der ein Node-left-Ereignis hört, ignoriert es einfach. So wird vermieden, dass zwei Nodes unabhängig voneinander zwei verschiedene neue Owner für dieselbe Partition wählen.</p>
<p>Erhält der Leader ein <code>NodeLeft</code>-Signal für einen Node, befördert er <em>nicht</em> sofort Replikate für jede Partition, die dieser Node besaß. Die Promotion wird im Code als „irreversible, high-impact action" bezeichnet und ist abgesichert:</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>Die Absicherung existiert, weil die Ausfall-<em>Nachricht</em> und die <em>Peer-Tabelle</em> der Mitgliedschaftsschicht asynchron aktualisiert werden. Ein <code>NodeLeft</code> kann einen Heartbeat vor der Peer-Tabelle eintreffen — oder schlicht falsch sein. Vor der folgenreichen Aktion prüft der Leader die Erreichbarkeit daher direkt erneut. Ein noch erreichbarer „toter" Owner bleibt genau dort, wo er ist.</p>
<p>Es gibt einen zweiten, langsameren Pfad für den Fall, den der erste nicht abdecken kann: ein aktiver Owner, der ohne sauberes <code>NodeLeft</code> verstummt ist. Ein periodischer Reconcile-Durchlauf (<code>HandleUnavailableActiveOwnersAsync</code>) geht jede Partition durch, und bei einem Owner, der nicht am Leben, aber nicht als ausgefallen bestätigt ist, startet er einen <strong>Verdachts-Timer</strong>, statt zu handeln:</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>Erst nachdem der Owner das gesamte Eskalationsfenster lang (standardmäßig zehn Sekunden) unerreichbar war, bestätigt der Leader den Ausfall und fährt fort. Das Design ist bewusst asymmetrisch: <em>Eine Partition wegen Replikatverlust als degradiert zu markieren ist billig und reversibel und erfolgt daher sofort; einen neuen Active zu befördern ist irreversibel und wartet daher auf einen Beweis.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-2--einen-überlebenden-wählen-und-die-epoch-erhöhen">Schritt 2 — einen Überlebenden wählen und die Epoch erhöhen<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#schritt-2--einen-%C3%BCberlebenden-w%C3%A4hlen-und-die-epoch-erh%C3%B6hen" class="hash-link" aria-label="Direkter Link zur Schritt 2 — einen Überlebenden wählen und die Epoch erhöhen" title="Direkter Link zur Schritt 2 — einen Überlebenden wählen und die Epoch erhöhen" translate="no">​</a></h2>
<p>Sobald der Owner als ausgefallen bestätigt ist, wählt der Leader ein Replikat zur Promotion. Die Auswahl ist <strong>Verfügbarkeit zuerst</strong>: Unter den zugewiesenen Replikaten der Partition bevorzugt er dasjenige, das die meisten Segmente synchronisiert hat — den Node, der am ehesten bald bedienen kann.</p>
<p>Die Promotion selbst ist ein einziger Broadcast, der eine <strong>Epoch</strong> je Partition erhöht:</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>Die Epoch ist das Fencing-Token und der stille Held des gesamten Mechanismus. Sie ist je Partition monoton, und der Generator <em>weigert sich, rückwärts zu gehen</em> — eine vorgeschlagene Epoch, die nicht strikt größer als die höchste bereits beobachtete ist, wird glatt abgelehnt:</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>Nach der Promotion ist der Active der Partition also „n2 bei Epoch 8". War der alte Owner n1 nur netzwerkpartitioniert (nicht tot) und kehrt zurück in dem Glauben, bei Epoch 7 noch der Active zu sein, verliert seine veraltete Epoch jeden Vergleich. Er kann den Besitz nicht zurückfordern, und jeder Schreibvorgang, den er als „Active" annehmen will, wird ausgesperrt. So vermeidet Zaris den Split-Brain mit zwei Primaries, bei dem ein geheilter, aber verwirrter alter Owner Schreibvorgänge annähme, die nie wieder jemand sieht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-3--der-possession-proof-bedienen-oder-zuerst-synchronisieren">Schritt 3 — der Possession-Proof: <em>bedienen</em> oder <em>zuerst synchronisieren</em>?<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#schritt-3--der-possession-proof-bedienen-oder-zuerst-synchronisieren" class="hash-link" aria-label="Direkter Link zur schritt-3--der-possession-proof-bedienen-oder-zuerst-synchronisieren" title="Direkter Link zur schritt-3--der-possession-proof-bedienen-oder-zuerst-synchronisieren" translate="no">​</a></h2>
<p>Hier kommt der subtile Teil, an dessen Korrektheit wir ernsthaft debuggen mussten. Kann das gewählte, am meisten <em>verfügbare</em> Replikat tatsächlich sofort Lesevorgänge bedienen?</p>
<p>Nicht unbedingt — denn „die meisten Segmente synchronisiert" ist nicht dasselbe wie „hält jeden bestätigten Schlüssel". Ein Replikat kann bei der <em>Anzahl</em> der Segmente aufgeholt haben und dennoch bei den Daten der Segmente, die es hat, hinter der Wahrheit zurückliegen. Befördert man einen solchen Node direkt in einen bedienenden Zustand, wird er zu einem <strong>unvollständigen Active</strong>: Er antwortet <code>NotFound</code> auf Schlüssel, die auf dem toten Owner dauerhaft bestätigt wurden und auf einem vollständigeren Geschwister <em>existieren</em>. Das ist stiller Datenverlust im Gewand eines erfolgreichen Lesevorgangs.</p>
<p>Bevor der beförderte Node bedienen darf, wendet Zaris daher einen <strong>Possession-Proof</strong> an. Der Leader führt eine frische, zum Term passende Tabelle darüber, wie viele Schlüssel jeder Node für die Partition zu halten meldet. Die Regel ist ein reiner Vergleich:</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>Zwei Ergebnisse:</p>
<ul>
<li class=""><strong>Es existiert kein strikt vollständigerer lebender Halter</strong> — der Kandidat ist der vollständigste Node oder ein echter einziger Überlebender, ohne etwas, von dem er synchronisieren könnte. Er wird zu <strong>Degraded</strong> befördert: einem <em>bedienenden</em> Zustand. Er hat nichts Besseres, auf das er warten könnte, und ihn nicht bedienen zu lassen, würde die Partition nur unnötig unverfügbar machen. Er bedient sofort.</li>
<li class=""><strong>Es existiert ein strikt vollständigerer lebender Halter</strong> — der Kandidat wird zu <strong>Recovering</strong> befördert: einem <em>nicht bedienenden</em> Zustand. Er synchronisiert zuerst von dieser besseren Quelle und beginnt erst zu bedienen, wenn seine Vollständigkeit erwiesen ist. Clients wird mitgeteilt, dass die Partition noch hochkommt, statt ihnen einen Node zu geben, der sie belügen würde.</li>
</ul>
<p>Die Unterscheidung zwischen Degraded und Recovering ist genau die zwischen „die Redundanz ist reduziert, aber die Wahrheit wird bedient" und „diesem Owner für Lesevorgänge noch nicht trauen". Einen einzigen Überlebenden korrekt in Degraded zu bringen ist ebenso wichtig, wie einen unvollständigen Node in Recovering zu halten — eine einzelne verbliebene Kopie darf nie dauerhaft nicht bedienend festgenagelt werden, nur weil sie die letzte Kopie ist.</p>
<p>Ein Wort der Ehrlichkeit: Dieser Nachweis ist <strong>zählbasiert</strong>, gewonnen aus Berichten gehaltener Schlüssel je Node mit einer Frischegrenze (ein Bericht eines verstummten Nodes zählt nicht als lebende Quelle). Er ist ein starker, billiger Schutz davor, einen offensichtlich zurückliegenden Node zum Bedienen zu befördern; er ist keine kryptografische Bestätigung, dass zwei Nodes byte-identische Daten halten. Die Konvergenz, die die Kopien <em>tatsächlich</em> identisch macht, ist der Replica-Sync-Pfad, der weiter unten behandelt wird.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-4--der-client-routet-um-und-ihr-retry-landet">Schritt 4 — der Client routet um, und Ihr Retry landet<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#schritt-4--der-client-routet-um-und-ihr-retry-landet" class="hash-link" aria-label="Direkter Link zur Schritt 4 — der Client routet um, und Ihr Retry landet" title="Direkter Link zur Schritt 4 — der Client routet um, und Ihr Retry landet" translate="no">​</a></h2>
<p>Während all das auf dem Server geschieht: Was sieht der Client, der mitten im Schreibvorgang war?</p>
<p>Er sieht die Wahrheit als typisierten Status. Eine Anfrage, die den alten Owner erreicht (jetzt gefenct oder schlicht weg), hängt nicht und wirft keinen vagen Fehler — sie kommt als <code>Moved</code> oder <code>OwnershipChanging</code> zurück. Der Zaris-Client behandelt diese als wiederholbare <em>Reroute</em>-Signale: Er aktualisiert seine Ownership-Map, entdeckt den neuen Active und spielt die Operation dort erneut ab. Im Normalfall dauert Ihr <code>PutAsync</code> einfach ein paar Millisekunden länger und liefert <code>Ok</code> — gegen den neu beförderten Owner, bei der neuen Epoch. Der Anwendungscode erfährt nie, dass der Owner gewechselt hat.</p>
<p>Diese Client-Mechanik — die Status-Taxonomie, der Retry mit Versuchsbudget und Deadline, die Map-Aktualisierung — ist eine eigene Geschichte, erzählt in <a class="" href="https://clustron.io/de/blog/retries-under-ownership-churn/">Retries, die Sie nicht schreiben müssen</a>. Für diesen Beitrag zählt nur, dass die Epoch-Erhöhung des Servers und das Reroute des Clients zwei Hälften einer Übergabe sind: Der Server macht den alten Owner nicht mehr maßgeblich, und der Client hört auf, mit ihm zu sprechen.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-5--konvergieren-dann-zurück-auf-healthy-heilen">Schritt 5 — konvergieren, dann zurück auf Healthy heilen<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#schritt-5--konvergieren-dann-zur%C3%BCck-auf-healthy-heilen" class="hash-link" aria-label="Direkter Link zur Schritt 5 — konvergieren, dann zurück auf Healthy heilen" title="Direkter Link zur Schritt 5 — konvergieren, dann zurück auf Healthy heilen" translate="no">​</a></h2>
<p>Die Promotion stellt die <em>Verfügbarkeit</em> wieder her, nicht die <em>Redundanz</em>. Direkt nach einem Failover läuft die Partition als Degraded mit einer Kopie weniger als ihr Ziel. Danach geschehen im Hintergrund zwei Dinge.</p>
<p>Erstens holen der beförderte Node (falls er als Recovering hochkam) und alle zurückliegenden oder zurückkehrenden Replikate auf, indem sie <strong>das Operations-Log je Segment ab einer bekannten Revision erneut abspielen</strong> — dasselbe wiederabspielbare Substrat, das die gewöhnliche Replikation antreibt. Ein Node, der gerade die Active-Rolle geerbt hat, und ein Replikat, das lediglich zurückgefallen ist, konvergieren über denselben Mechanismus; das ist das Thema von <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">One Log, Two Jobs</a>.</p>
<p>Zweitens überführt der Recovery-Durchlauf des Leaders die Partition zurück nach <strong>Healthy</strong>, sobald jeder zugewiesene Teilnehmer wieder lebt und synchron ist — aber nur über denselben Epoch-erhöhenden Broadcast und erst nach einer kurzen Abkühlzeit, damit ein flatternder Node die Runtime nicht durch einen Sturm von Übergängen treiben kann. Der Gesundheits-Zustandsautomat ist somit eine kleine, geschlossene Schleife: <code>Healthy → Degraded/Recovering → Healthy</code>, jede Kante eine Epoch-Erhöhung, jede Promotion gefenct.</p>
<p>Überlebt gar kein geeignetes Replikat — ist jeder zugewiesene Node der Partition ausgefallen —, tut der Leader das einzig Ehrliche: Er markiert die Partition als <strong>Unavailable</strong> (kein Owner, keine falsche Promotion) und startet einen Eviction-Countdown, der, falls niemand rechtzeitig zurückkehrt, eine geplante Map-Neuberechnung die Partition neu verorten lässt. Eine Unavailable-Partition lässt Lese- und Schreibvorgänge laut scheitern, statt eine Lüge zu bedienen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wo-ein-schreibvorgang-tatsächlich-verloren-geht--klar-benannt">Wo ein Schreibvorgang tatsächlich verloren geht — klar benannt<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#wo-ein-schreibvorgang-tats%C3%A4chlich-verloren-geht--klar-benannt" class="hash-link" aria-label="Direkter Link zur Wo ein Schreibvorgang tatsächlich verloren geht — klar benannt" title="Direkter Link zur Wo ein Schreibvorgang tatsächlich verloren geht — klar benannt" translate="no">​</a></h2>
<p>Ein Failover-Mechanismus verdient Vertrauen, indem er seine Grenzen präzise benennt, nicht indem er andeutet, es gäbe keine.</p>
<ul>
<li class=""><strong>Replikationsfaktor 1.</strong> Eine Partition mit einer einzigen Kopie und ohne Replikat hat nichts, was sie befördern könnte. Stirbt dieser Node, wird die Partition Unavailable, bis er zurückkehrt; die Daten sind nicht verloren (sie liegen im Speicher dieses Nodes), aber <em>nicht verfügbar</em>, und kehrt der Node nie zurück, sind sie weg. RF1 ist <strong>konstruktionsbedingt</strong> nicht dauerhaft — eine bewusste Wahl für Daten, die Sie sich leisten können zu verlieren, keine Garantie, die der Failover-Pfad retten könnte.</li>
<li class=""><strong>Asynchrone Bestätigung.</strong> Zaris erlaubt, einen Schreibvorgang auf dem aktiven Owner zu bestätigen, bevor er ein Replikat erreicht hat — für die Latenz. Das ist der schnelle Pfad, und sein ehrlicher Preis ist ein Fenster: Stirbt der Active in der Lücke zwischen der Bestätigung gegenüber dem Client und dem Eintreffen der Operation bei irgendeinem überlebenden Halter, ist dieser bestätigte Schreibvorgang verloren. Der Possession-Proof schützt davor, einen <em>bekanntermaßen</em> zurückliegenden Node zum Bedienen zu befördern; er kann keine Operation herbeizaubern, die den toten Node nie verlassen hat. Muss ein Schreibvorgang den Verlust seines Owners überleben, darf er erst bestätigt werden, nachdem er auf einem Replikat dauerhaft ist — der synchrone Pfad tauscht ein wenig Latenz genau gegen diese Garantie. Wählen Sie pro Workload, mit offenen Augen.</li>
<li class=""><strong>Das Bestätigungsfenster selbst.</strong> Während des Eskalations-Timeouts, nachdem ein Owner verstummt ist, befinden sich die Schreibvorgänge der Partition in der Schwebe — vom Client wiederholt, aber noch nicht auf einem neuen Owner gelandet. Die Korrektheit bleibt gewahrt (nichts wird fälschlich befördert), aber die Verfügbarkeit sinkt für dieses Fenster. Den Timeout zu verkürzen tauscht schnelleres Failover gegen mehr Fehlpromotions bei vorübergehenden Aussetzern; es ist ein Regler, kein Gratis-Mittagessen.</li>
</ul>
<p>Nichts davon sind Bugs, die der Failover-Pfad wegkonstruieren könnte — es sind die echten Zielkonflikte zwischen Verfügbarkeit, Langlebigkeit und Latenz, und Zaris' Aufgabe ist es, die <em>sichere</em> Wahl zum Default und die <em>schnelle</em> Wahl explizit zu machen, ohne je so zu tun, als gäbe es das Fenster nicht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-gesamtbild">Das Gesamtbild<a href="https://clustron.io/de/blog/anatomy-of-a-failover/#das-gesamtbild" class="hash-link" aria-label="Direkter Link zur Das Gesamtbild" title="Direkter Link zur Das Gesamtbild" translate="no">​</a></h2>
<p>Reduziert man das Failover, sind es vier Züge auf einer Invariante. Die Invariante: Die Map bleibt stehen; nur der Runtime-Active bewegt sich. Die Züge: den Tod <strong>bestätigen</strong>, bevor man darauf reagiert, einen Überlebenden hinter einer monotonen Epoch <strong>befördern</strong>, die den alten Owner aussperrt, den Besitz <strong>nachweisen</strong>, sodass der neue Owner die Wahrheit bedient oder erst synchronisiert, bevor er überhaupt bedient, und den Client <strong>umleiten</strong>, sodass sein Retry beim neuen Owner landet. Dann die Kopien wieder konvergieren lassen und auf Healthy heilen.</p>
<p>Dass das hält, ist keine Frage der Argumentation — es wird gemessen. Derselbe Promotion-, Possession- und Konvergenzpfad läuft stundenlang kontinuierlich unter gezieltem Node-Kill-Chaos, und am Ende wird das Ledger auf einen einzigen verlorenen bestätigten Schreibvorgang geprüft. Diese Zertifizierung ist eine eigene Geschichte: <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">Kein bestätigter Schreibvorgang bleibt zurück</a>.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Welches Primitiv für welches Problem: Ein Feldführer durch das Zaris-Datenmodell]]></title>
            <link>https://clustron.io/de/blog/choosing-a-zaris-primitive/</link>
            <guid>https://clustron.io/de/blog/choosing-a-zaris-primitive/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris ist ein einziger versionierter, partitionierter, replizierter Key/Value-Kern — mit einer Handvoll typisierter Views und Koordinationsschichten darüber. Das Schwierige ist nicht, was es kann, sondern das richtige Werkzeug für das Problem vor Ihnen zu wählen. Hier ist ein Entscheidungsleitfaden für einfaches KV, Compare-and-Swap, Multi-Key-Transaktionen, native Collections, Streams, Watch und Locks — jeweils mit ehrlicher Eignung und ehrlicher Grenze.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Das richtige Zaris-Primitiv für das Problem wählen" 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>Der Großteil dieses Blogs ist jeweils tief in ein einzelnes Thema eingestiegen — <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Compare-and-Swap</a>, <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktionen</a>, <a class="" href="https://clustron.io/de/blog/data-structures-that-replicate/">native Datenstrukturen</a>, <a class="" href="https://clustron.io/de/blog/redis-streams-native/">Streams</a>, <a class="" href="https://clustron.io/de/blog/2026/03/30/watch-changes/">Watch</a>, das darunterliegende <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">Op-Log</a>. Jeder Beitrag beantwortet „wie funktioniert <em>dies</em>". Dieser beantwortet eine andere, praktischere Frage: <strong>Wonach greifen Sie, wenn ein Problem vor Ihnen liegt?</strong></p>
<p>Diese Frage ist wichtig, weil die falsche Wahl selten ein Absturz ist. Es ist ein System, das in der Demo funktioniert und dann unter Last ein Update verliert, tausend Schreiber durch einen Schlüssel schleust oder eine Fire-and-Forget-Benachrichtigung nutzt, wo ein dauerhaftes Log nötig war. Die Primitive hindern Sie nicht daran, sie falsch zu benutzen. Dies ist also ein Feldführer: das mentale Modell, das sie verbindet, eine Entscheidungstabelle, ein Gang durch jeden Zweig mit ehrlicher Eignung und ehrlicher Grenze und am Ende ein Flussdiagramm.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ein-kern-viele-views">Ein Kern, viele Views<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#ein-kern-viele-views" class="hash-link" aria-label="Direkter Link zur Ein Kern, viele Views" title="Direkter Link zur Ein Kern, viele Views" translate="no">​</a></h2>
<p>Beginnen wir mit dem, worauf alles andere aufbaut. Unter jedem Feature in diesem Beitrag ist Zaris <strong>ein einziger partitionierter, replizierter Key/Value-Store, und jeder Wert trägt eine vom Store verwaltete monotone Version.</strong> Das ist alles. Das ist der Kern.</p>
<p>Alles andere ist eines von zwei Dingen:</p>
<ul>
<li class=""><strong>Eine typisierte, serverseitige <em>View</em> über Schlüssel</strong> — ein Hash, eine Liste, ein Set, ein Sorted Set, ein Stream. Das sind keine Blobs, die Sie abrufen und in Ihrem Prozess verändern; es sind echte serverseitige Collections, die auf der Partition liegen, die ihren Schlüssel besitzt, und sie replizieren genau wie ein einfacher Wert.</li>
<li class=""><strong>Eine Koordinations-<em>Schicht</em> über der Version</strong> — Compare-and-Swap, Multi-Key-Transaktionen, Locks, Watch. Sie nutzen die Version (und die Ownership-Mechanik darum herum), damit viele Clients sich darauf einigen können, was passiert ist, ohne sich auf die Füße zu treten.</li>
</ul>
<p>Behalten Sie dieses Bild im Kopf, und die gesamte Oberfläche wirkt nicht mehr wie ein Sammelsurium von Features, sondern wie das, was sie ist: ein kleiner Kern und eine Reihe gezielt gebauter Werkzeuge, die jeweils eine bestimmte Problemklasse darauf lösen. Wählen Sie nach der <em>Form Ihres Problems</em>, nicht nach dem Namen, der am ehesten passt.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-entscheidungstabelle">Die Entscheidungstabelle<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#die-entscheidungstabelle" class="hash-link" aria-label="Direkter Link zur Die Entscheidungstabelle" title="Direkter Link zur Die Entscheidungstabelle" translate="no">​</a></h2>
<table><thead><tr><th>Ihr Problem</th><th>Greifen Sie zu</th><th>Warum</th><th>Worauf Sie achten sollten</th></tr></thead><tbody><tr><td>Bytes ablegen, die andere Nodes zurücklesen können</td><td><strong>Einfaches <code>GetAsync</code>/<code>PutAsync</code></strong> oder eine <a class="" href="https://clustron.io/de/blog/aspnet-core-distributed-cache/">Cache-Integration</a></td><td>Der Kern, nichts weiter</td><td>Kein Ablauf beim Schreiben — das TTL ist ein zweiter Aufruf (siehe unten)</td></tr><tr><td>Ein einzelner Wert muss sich unter Nebenläufigkeit sicher ändern</td><td><strong><a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Compare-and-Swap</a></strong></td><td>Ein Gewinner pro Runde, keine Lost Updates, kein gehaltenes Lock</td><td>Ein heißer einzelner Schlüssel wird zum Serialisierungspunkt — sharden Sie ihn</td></tr><tr><td>Eine Invariante erstreckt sich über zwei oder mehr Schlüssel</td><td><strong><a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktion</a></strong></td><td>Alles-oder-nichts über Schlüssel <em>und</em> Nodes</td><td>Schwergewichtiger als CAS; es ist ein 2PC, nicht gratis</td></tr><tr><td>Die Daten <em>sind</em> eine Collection — Map, Liste, Mitgliedschaft, Ranking</td><td><strong><a class="" href="https://clustron.io/de/blog/data-structures-that-replicate/">Native Datenstrukturen</a></strong></td><td>Serverseitige Operationen pro Element, kein Neuschreiben des gesamten Werts</td><td>Wählen Sie die Collection, die zum Zugriffsmuster passt</td></tr><tr><td>Ein geordnetes, wiederabspielbares Event-Log mit At-least-once-Consumern</td><td><strong><a class="" href="https://clustron.io/de/blog/redis-streams-native/">Streams</a></strong></td><td>Dauerhaftes Log, Consumer Groups, Pending + Ack pro Consumer</td><td>Es ist ein Log, keine Queue mit Prioritäten</td></tr><tr><td>Auf Änderungen reagieren, während sie geschehen</td><td><strong><a class="" href="https://clustron.io/de/blog/2026/03/30/watch-changes/">Watch</a></strong> (nativ) / <a class="" href="https://clustron.io/de/blog/redis-keyspace-notifications/">Keyspace-Benachrichtigungen</a> (Redis-Clients)</td><td>Repliziert, clusterweit, übersteht Failover</td><td>Fire-and-Forget-Pub/Sub ist <em>kein</em> dauerhafter Feed</td></tr><tr><td>Grober gegenseitiger Ausschluss um eine Ressource</td><td><strong><a class="" href="https://clustron.io/de/blog/distributed-locks-explained/">Lock</a></strong> (Lease-basiert)</td><td>Ein benanntes, TTL-begrenztes Gate, das sich bei Absturz automatisch freigibt</td><td>Ein Lease-Lock ist kein Korrektheits-Fence — siehe unten</td></tr></tbody></table>
<p>Der Rest dieses Beitrags ist diese Tabelle in Zeitlupe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kv-und-cache-wenn-ein-wert-nur-ein-wert-ist">KV und Cache: wenn ein Wert nur ein Wert ist<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#kv-und-cache-wenn-ein-wert-nur-ein-wert-ist" class="hash-link" aria-label="Direkter Link zur KV und Cache: wenn ein Wert nur ein Wert ist" title="Direkter Link zur KV und Cache: wenn ein Wert nur ein Wert ist" translate="no">​</a></h2>
<p>Müssen Sie lediglich Bytes irgendwo ablegen, wo jeder Node sie lesen kann — ein gerendertes Fragment, ein serialisiertes DTO, einen Konfigurations-Snapshot —, brauchen Sie keine Koordinationsschicht. <code>PutAsync(key, value)</code> und <code>GetAsync&lt;T&gt;(key)</code> sind die ganze Geschichte, und serverseitig werden diese Bytes für Sie partitioniert und repliziert.</p>
<p>Zwei ehrliche Hinweise. Erstens: <strong>Der Ablauf wird nicht beim Schreiben gesetzt.</strong> Die Schreiboptionen haben kein TTL-Feld; ein Time-to-Live setzen Sie mit einem separaten Aufruf — <code>ExpireAsync(key, ttl)</code> —, entfernen es mit <code>PersistAsync(key)</code> oder lesen den Rest mit <code>GetTimeToLiveAsync(key)</code>. Zweitens: Ist Ihr Anwendungsfall buchstäblich „ein verteilter Cache für meine ASP.NET-Core-App", bauen Sie nichts davon selbst: Registrieren Sie eines der <a class="" href="https://clustron.io/de/blog/aspnet-core-distributed-cache/">Drop-in-Cache-Pakete</a>, und das <code>IDistributedCache</code> des Frameworks (oder <code>HybridCache</code> oder der Session-Store) wird von Ihrem Cluster getragen — mit einer einzeiligen Änderung beim Start und ohne dass Code die Cache-Abstraktion berührt.</p>
<p><strong>Greifen Sie zu einfachem KV, wenn</strong> der Wert keine Nebenläufigkeits-Invariante hat — Last-Writer-Wins ist in Ordnung, oder es gibt immer nur einen Schreiber. <strong>Tun Sie es nicht,</strong> sobald zwei Schreiber um denselben Schlüssel konkurrieren können und es Sie stören würde, wenn einer den anderen stillschweigend überschreibt. Das ist die Aufgabe des nächsten Werkzeugs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="compare-and-swap-wenn-ein-wert-unter-nebenläufigkeit-korrekt-bleiben-muss">Compare-and-Swap: wenn ein Wert unter Nebenläufigkeit korrekt bleiben muss<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#compare-and-swap-wenn-ein-wert-unter-nebenl%C3%A4ufigkeit-korrekt-bleiben-muss" class="hash-link" aria-label="Direkter Link zur Compare-and-Swap: wenn ein Wert unter Nebenläufigkeit korrekt bleiben muss" title="Direkter Link zur Compare-and-Swap: wenn ein Wert unter Nebenläufigkeit korrekt bleiben muss" translate="no">​</a></h2>
<p>Ein Zähler mit Obergrenze, ein Kontostand, der nicht negativ werden darf, ein Konfigurationsobjekt, das zwei Admins gleichzeitig bearbeiten. Der Fehler, den Sie vermeiden wollen, ist das <em>Lost Update</em> — lesen, lesen, schreiben, schreiben, und der zweite Schreibvorgang löscht den ersten. CAS macht aus diesem stillen Verlust einen erkannten, wiederholbaren Konflikt: einen Wert <em>samt Version</em> lesen, nur zurückschreiben, wenn sich die Version nicht bewegt hat, und ein <code>Conflict</code> erhalten statt eines Überschreibens, wenn Ihnen jemand zuvorkam.</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>Das ist der gesamte Mechanismus, und der <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">CAS-Feldführer</a> zeigt, wie weit er trägt — ein Rate Limiter, ein Wallet, das unter tausend gleichzeitigen Abbuchungen nie überzogen wird, ein Job-Claim mit nur einem Gewinner (über „nur erstellen, wenn nicht vorhanden"), Konfiguration ohne Lost Updates und der Idempotenz-Haken, der gratis mitläuft.</p>
<p><strong>Greifen Sie zu CAS, wenn</strong> die Invariante auf <em>einem</em> Schlüssel liegt und Sie Korrektheit ohne gehaltenes Lock wollen. <strong>Richten Sie nicht</strong> eine Menge Schreiber auf einen einzelnen heißen Schlüssel und erwarten Sie nicht, dass das skaliert — CAS lässt bewusst pro Runde genau einen Schreiber gewinnen, ein heißer Schlüssel wird also zum Serialisierungspunkt. Sharden Sie ihn (ein Bucket pro Client-Sekunde, ein in abgeglichene Teilguthaben zerlegter Kontostand), und die Contention schmilzt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="multi-key-transaktionen-wenn-die-invariante-schlüssel-überspannt">Multi-Key-Transaktionen: wenn die Invariante Schlüssel überspannt<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#multi-key-transaktionen-wenn-die-invariante-schl%C3%BCssel-%C3%BCberspannt" class="hash-link" aria-label="Direkter Link zur Multi-Key-Transaktionen: wenn die Invariante Schlüssel überspannt" title="Direkter Link zur Multi-Key-Transaktionen: wenn die Invariante Schlüssel überspannt" translate="no">​</a></h2>
<p>Die harte Linie, die CAS zieht, ist, dass es auf <em>einem</em> Schlüssel atomar ist. Geld von Konto A auf Konto B übertragen oder einen Sekundärindex im Gleichschritt mit dem Datensatz halten, auf den er zeigt — kein einzelnes Compare-and-Swap deckt beides ab. Das ist kein Grund, es mit zwei CAS-Schreibvorgängen vorzutäuschen und zu hoffen — es ist genau der Fall, für den die <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktion</a> existiert.</p>
<p>Sie öffnen eine Transaktion, stagen Lese- und Schreibvorgänge hinein und committen. Intern ist es ein vom Client koordiniertes <strong>Two-Phase-Commit über optimistische Versionen</strong>: Die Schlüssel können auf verschiedenen Nodes liegen, der Client gruppiert das Write-Set also nach Owner, jeder Node prüft, dass jeder von Ihnen gelesene Schlüssel <em>noch</em> auf der gelesenen Version steht, und nimmt ein kurzlebiges Lock; nur wenn alle mit Ja stimmen, wird das gesamte Set angewendet. Hat sich irgendeine Version bewegt, liefert der Commit <code>Conflict</code>, und nichts ist verändert.</p>
<p><strong>Greifen Sie zu einer Transaktion, wenn</strong> zwei oder mehr Schlüssel gemeinsam oder gar nicht wechseln müssen. <strong>Nutzen Sie sie nicht</strong> als Standard-Schreibpfad — sie ist ein echtes 2PC mit Prepare-/Commit-Roundtrips und kurzlebigen Locks, schwerer als ein einzelnes CAS. Passt die Invariante ehrlich auf einen Schlüssel, lassen Sie sie auf einem Schlüssel.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="native-datenstrukturen-wenn-die-daten-eine-collection-sind">Native Datenstrukturen: wenn die Daten eine Collection <em>sind</em><a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#native-datenstrukturen-wenn-die-daten-eine-collection-sind" class="hash-link" aria-label="Direkter Link zur native-datenstrukturen-wenn-die-daten-eine-collection-sind" title="Direkter Link zur native-datenstrukturen-wenn-die-daten-eine-collection-sind" translate="no">​</a></h2>
<p>Manchmal ist der Wert kein Skalar, den Sie schützen, sondern eine Form. Ein Benutzerprofil ist eine Map von Feldern. Ein Feed jüngster Aktivitäten ist eine Liste. Eine Tag-Mitgliedschaft ist ein Set. Eine Bestenliste ist ein Ranking. Sie <em>könnten</em> jedes als einen Blob serialisieren und per CAS absichern, aber dann schreibt jede Einzelfeld-Änderung das gesamte Objekt neu, und aus einem O(1)-Update wird ein O(n)-Update.</p>
<p>Zaris gibt diesen eigene <a class="" href="https://clustron.io/de/blog/data-structures-that-replicate/">native, serverseitige Collection-Typen</a> — Hashes, Listen, Sets, Sorted Sets —, bei denen eine Operation auf einem Feld oder Element nur dieses Element auf dem Server berührt, nicht den gesamten Wert in Ihrem Prozess. Sie werden wie jeder Schlüssel partitioniert und repliziert, und weil das <a class="" href="https://clustron.io/de/blog/redis-protocol-any-language/">RESP-Frontend</a> dieselben Redis-Befehle auf dieselben serverseitigen Collections abbildet, berühren ein .NET-Service und ein Python-Service, die <code>HSET</code> nutzen, denselben Hash.</p>
<p><strong>Greifen Sie zu einer Collection, wenn</strong> Ihr Zugriffsmuster elementweise ist — ein Feld setzen, einen Eintrag pushen, eine Mitgliedschaft prüfen, die Top N nach Score lesen. <strong>Greifen Sie nicht</strong> zu einem Sorted Set, wenn Sie eigentlich ein geordnetes, bestätigtes, wiederabspielbares Event-Log brauchen. Das ist ein Stream.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="streams-wenn-sie-ein-geordnetes-wiederabspielbares-log-brauchen">Streams: wenn Sie ein geordnetes, wiederabspielbares Log brauchen<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#streams-wenn-sie-ein-geordnetes-wiederabspielbares-log-brauchen" class="hash-link" aria-label="Direkter Link zur Streams: wenn Sie ein geordnetes, wiederabspielbares Log brauchen" title="Direkter Link zur Streams: wenn Sie ein geordnetes, wiederabspielbares Log brauchen" translate="no">​</a></h2>
<p>Eine Queue und ein Log sehen ähnlich aus, bis ein Consumer abstürzt. Zaris-<a class="" href="https://clustron.io/de/blog/redis-streams-native/">Streams</a> sind ein echtes Append-only-Log: Einträge erhalten monotone IDs, Sie können Bereiche lesen, und eine <strong>Consumer Group</strong> verteilt das Log auf einen Pool von Workern, verfolgt die <em>Pending</em>-Einträge jedes Workers (zugestellt, aber nicht bestätigt) und lässt einen anderen Worker übernehmen, was ein abgestürzter nie bestätigt hat. Diese Pending-und-Ack-Mechanik liefert At-least-once-Zustellung — das, was eine Fire-and-Forget-Benachrichtigung nicht leisten kann.</p>
<p><strong>Greifen Sie zu einem Stream, wenn</strong> Sie dauerhafte, geordnete, wiederabspielbare Events mit expliziter Bestätigung und Arbeitsteilung brauchen — Order-Ingestion, ein Outbox-Pattern, ein Audit-Trail, aus dem sich eine Projektion neu aufbaut. <strong>Nutzen Sie ihn nicht</strong> als Prioritäts-Queue oder Request/Response-Kanal; er ist ein Log, und seine Stärken sind Ordnung und Replay, nicht Routing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="watch-benachrichtigungen-und-die-fan-out-familie-wenn-sie-auf-änderungen-reagieren">Watch, Benachrichtigungen und die Fan-out-Familie: wenn Sie auf Änderungen reagieren<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#watch-benachrichtigungen-und-die-fan-out-familie-wenn-sie-auf-%C3%A4nderungen-reagieren" class="hash-link" aria-label="Direkter Link zur Watch, Benachrichtigungen und die Fan-out-Familie: wenn Sie auf Änderungen reagieren" title="Direkter Link zur Watch, Benachrichtigungen und die Fan-out-Familie: wenn Sie auf Änderungen reagieren" translate="no">​</a></h2>
<p>Es gibt mehrere Wege zu erfahren, dass sich etwas geändert hat, und sie sind <em>nicht</em> austauschbar — der Unterschied liegt darin, ob das Signal dauerhaft ist.</p>
<p>Der Weg aus .NET ist <strong><a class="" href="https://clustron.io/de/blog/2026/03/30/watch-changes/">Watch</a></strong>: ein nativer, clusterweiter Change Feed. Sie abonnieren einen Schlüssel oder ein Präfix, binden optional einen initialen Snapshot ein, damit zwischen „aktuellen Zustand lesen" und „Änderungen abonnieren" keine Lücke entsteht, und erhalten Events, sobald Schreibvorgänge landen:</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>Die wichtige Eigenschaft ist, dass der Watch <strong>clusterweit registriert ist und Failover übersteht</strong> — er ist nicht an den einen Node gebunden, der zufällig einen Schreibvorgang verarbeitet hat. Deshalb sind die <a class="" href="https://clustron.io/de/blog/redis-keyspace-notifications/">Redis-Keyspace-Benachrichtigungen</a>, die Zaris spricht (<code>__keyevent@0__</code> / <code>__keyspace@0__</code>, ein einfaches <code>PSUBSCRIBE</code> von jedem Redis-Client), hier mehr wert als auf einer einzelnen Redis-Box: Sie sind eine dünne Brücke auf denselben replizierten Watch, sodass ein Failover den Event-Strom nicht stillschweigend abreißen lässt.</p>
<p><strong>Greifen Sie zu Watch/Keyspace-Benachrichtigungen, wenn</strong> Sie auf Zustandsänderungen reagieren wollen — einen Cache invalidieren, einen Worker wecken, eine Projektion aktualisieren. <strong>Verwechseln Sie</strong> einfaches Best-Effort-Pub/Sub-Fan-out nicht mit einem dauerhaften Feed: Brauchen Sie <em>jedes</em> Event auch über Abstürze und Neustarts hinweg, ist das ein Stream, keine Benachrichtigung.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="locks-grober-gegenseitiger-ausschluss-mit-einem-scharfen-vorbehalt">Locks: grober gegenseitiger Ausschluss, mit einem scharfen Vorbehalt<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#locks-grober-gegenseitiger-ausschluss-mit-einem-scharfen-vorbehalt" class="hash-link" aria-label="Direkter Link zur Locks: grober gegenseitiger Ausschluss, mit einem scharfen Vorbehalt" title="Direkter Link zur Locks: grober gegenseitiger Ausschluss, mit einem scharfen Vorbehalt" translate="no">​</a></h2>
<p>Zaris hat tatsächlich einen erstklassigen verteilten <strong>Lock</strong> — ein benanntes, Lease-basiertes Gate mit TTL, vom Client erworben und beim Dispose oder bei Ablauf nach einem Absturz freigegeben:</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>Er ist wirklich nützlich für groben gegenseitigen Ausschluss — verhindern, dass zwei Scheduler denselben Sweep ausführen, einen Wartungstask serialisieren —, und das TTL sorgt dafür, dass ein abgestürzter Halter den Lock nicht für immer blockiert.</p>
<p>Doch hier ist der Vorbehalt, den die glänzende Version weglässt, und es ist der wichtigste Satz dieses Beitrags über Locks: <strong>Ein Lease-basierter Lock ist kein Korrektheits-Fence.</strong> Pausiert Ihr Halter — ein GC-Stall, ein langsamer Syscall — lange genug, dass die Lease abläuft, kann ein zweiter Halter denselben Lock erwerben, während der erste noch glaubt, ihn zu halten, und nun befinden sich zwei Worker gleichzeitig im kritischen Abschnitt. Für <em>Liveness und Effizienz</em> (meist läuft nur ein Worker den Job, geringe Contention) ist ein Lease-Lock genau richtig. Für eine <em>Korrektheits-Invariante, die nie verletzt werden darf</em>, verlassen Sie sich nicht darauf, dass der Lock sie durchsetzt — setzen Sie sie auf den Daten durch, mit <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Compare-and-Swap</a> oder einer <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Transaktion</a>, damit selbst ein irrtümlicher zweiter Akteur beim Schreiben durch eine Versionsprüfung abgewiesen wird.</p>
<p><strong>Greifen Sie zu einem Lock, wenn</strong> Sie groben Best-Effort-gegenseitigen-Ausschluss wollen und gelegentliche Überlappung lediglich verschwenderisch, nicht falsch ist. <strong>Greifen Sie stattdessen zu CAS, wenn</strong> Überlappung Daten beschädigen würde — lassen Sie die Version, nicht den Lock, das Nein sagen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-übergreifende-teil-ownership-langlebigkeit-konsistenz">Der übergreifende Teil: Ownership, Langlebigkeit, Konsistenz<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#der-%C3%BCbergreifende-teil-ownership-langlebigkeit-konsistenz" class="hash-link" aria-label="Direkter Link zur Der übergreifende Teil: Ownership, Langlebigkeit, Konsistenz" title="Direkter Link zur Der übergreifende Teil: Ownership, Langlebigkeit, Konsistenz" translate="no">​</a></h2>
<p>Drei Eigenschaften gelten für jedes obige Primitiv, weil sie Eigenschaften des Kerns sind, nicht eines einzelnen Werkzeugs:</p>
<ul>
<li class=""><strong>Der Owner wechselt, und Sie müssen es nicht bemerken.</strong> Jeder Schlüssel gehört einer Partition, die per Failover wechseln, migrieren oder zurückfallen kann. Der .NET-Client macht aus diesem Churn ein typisiertes, wiederholbares Signal und <a class="" href="https://clustron.io/de/blog/retries-under-ownership-churn/">routet zum neuen Owner um</a>, begrenzt durch ein Versuchsbudget und eine Wanduhr-Deadline — ein <code>PutAsync</code> während eines Failovers dauert also meist nur ein paar Millisekunden länger und liefert Erfolg.</li>
<li class=""><strong>Langlebigkeit entsteht durch Replikation, nicht durch Festplatte.</strong> Zaris hält Daten im Speicher und macht sie dauerhaft, indem es Kopien auf anderen Nodes hält. Deshalb ist <a class="" href="https://clustron.io/de/blog/kubernetes-durability-sizing/">der Replikationsfaktor nicht dasselbe wie Fehlertoleranz</a>: Die Zahl schützt Sie nur, wenn die Topologie tatsächlich eine zweite Kopie auf einem Node platziert, der den Verlust des ersten überleben kann. Das gesamte <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">Ownership-und-Recovery-Fundament</a> existiert, damit kein bestätigter Schreibvorgang verloren geht, während der Besitz in Bewegung ist.</li>
<li class=""><strong>Schreib- und Lesevorgänge werden zum aktuellen Owner geroutet.</strong> Die Retry-und-Reroute-Mechanik sorgt dafür, dass Ihre Operationen den Node finden, der den Schlüssel gerade besitzt, sodass ein Lesevorgang einem committeten Schreibvorgang folgt, statt dem soeben erfolgten Ownership-Wechsel vorauszueilen.</li>
</ul>
<p>Nichts davon ist ein Primitiv, das Sie wählen. Es ist der Boden, auf dem jede Wahl steht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-flussdiagramm">Das Flussdiagramm<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#das-flussdiagramm" class="hash-link" aria-label="Direkter Link zur Das Flussdiagramm" title="Direkter Link zur Das Flussdiagramm" translate="no">​</a></h2>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-kurzfassung">Die Kurzfassung<a href="https://clustron.io/de/blog/choosing-a-zaris-primitive/#die-kurzfassung" class="hash-link" aria-label="Direkter Link zur Die Kurzfassung" title="Direkter Link zur Die Kurzfassung" translate="no">​</a></h2>
<p>Die Länge der Feature-Liste war nie der Punkt. Zaris ist ein versionierter, partitionierter, replizierter Key/Value-Kern, plus typisierte serverseitige Views (Hash, Liste, Set, Sorted Set, Stream) und Koordinationsschichten über der Version (CAS, Transaktionen, Locks, Watch). Ordnen Sie das Werkzeug der <em>Form</em> des Problems zu: Ein Skalar unter Contention will CAS; eine schlüsselübergreifende Invariante will eine Transaktion; Daten, die eine Collection <em>sind</em>, wollen die passende Collection; dauerhafte geordnete Events wollen einen Stream; auf Änderungen zu reagieren will Watch; grober gegenseitiger Ausschluss will einen Lock — und alles, was nie verletzt werden darf, will seine Garantie auf den <em>Daten</em> durchgesetzt haben, mit einer Versionsprüfung, nicht auf einem Lock, der verfallen kann.</p>
<p>Wählen Sie nach der Form, verlassen Sie sich für die Korrektheit auf die Version, und der kleine Kern trägt erstaunlich viel System.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Architektur</category>
            <category>Verteilte Muster</category>
        </item>
        <item>
            <title><![CDATA[Die Steuerungsebene in Ihrer Shell: Einen Zaris-Cluster mit PowerShell betreiben]]></title>
            <link>https://clustron.io/de/blog/control-plane-powershell/</link>
            <guid>https://clustron.io/de/blog/control-plane-powershell/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Eine Day-one-Anleitung für Operatoren: einen Workspace anlegen, einen Store mit New-ZrStore bereitstellen, starten und seinen Live-Zustand mit Get-ZrStore, Get-ZrNode und Watch-ZrStoreMetrics auslesen — die AdminShell-Steuerungsebene, ehrlich zu Replikations- und Topologieentscheidungen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Einen Zaris-Cluster mit PowerShell bereitstellen und betreiben" 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>Das meiste, was man über einen Datenspeicher liest, handelt vom Datenpfad — Lese- und Schreibvorgänge, das Wire-Protokoll, wie schnell es ist. Doch bevor das alles zählt, muss jemand das Ding <em>aufsetzen</em>: entscheiden, wie viele Partitionen es hat, wie viele Kopien jeder davon, wo die Ports liegen, es starten, bestätigen, dass es gesund hochgekommen ist, und es im Betrieb beobachten. Das ist die Steuerungsebene, und in Zaris lebt sie in PowerShell.</p>
<p>Dies ist eine Day-one-Anleitung für Operatoren. Wir legen einen Workspace an, stellen einen Store bereit, starten ihn und lesen dann seinen Live-Zustand zurück — alles aus dem Modul <strong>AdminShell</strong>. Keine Web-Konsole nötig, nichts von Hand bearbeitet. Und weil bei einer Operator-Geschichte Vertrauen der ganze Punkt ist, benennen wir klar die Entscheidungen, die <code>New-ZrStore</code> Ihnen vorlegt, und was jede tatsächlich kostet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="zwei-shells-zwei-ebenen">Zwei Shells, zwei Ebenen<a href="https://clustron.io/de/blog/control-plane-powershell/#zwei-shells-zwei-ebenen" class="hash-link" aria-label="Direkter Link zur Zwei Shells, zwei Ebenen" title="Direkter Link zur Zwei Shells, zwei Ebenen" translate="no">​</a></h2>
<p>Zaris liefert seine Kommandozeilenoberfläche als zwei getrennte PowerShell-Module, und die Trennung ist nicht kosmetisch — sie spiegelt die beiden Ebenen des Systems:</p>
<ul>
<li class=""><strong>AdminShell</strong> ist die <strong>Steuerungsebene</strong>. Ihre Cmdlets stellen Stores bereit, starten, stoppen, inspizieren und skalieren sie. Sie sprechen mit dem Zaris-<strong>Management Service</strong> (dem „Manager"), nicht direkt mit den Datenknoten. Das sind die Verben <code>New-ZrStore</code>, <code>Get-ZrNode</code>, <code>Watch-ZrStoreMetrics</code>.</li>
<li class=""><strong>ClientShell</strong> ist die <strong>Datenebene</strong>. Ihre Cmdlets — <code>Connect-ZrStore</code>, <code>Get-ZrItem</code>, <code>Set-ZrItem</code> — öffnen eine Verbindung direkt zu den Nodes des Stores und bewegen Key/Value-Daten. Das ist die Oberfläche, die eine Anwendung nutzt, und das Thema des Beitrags zu <a class="" href="https://clustron.io/de/blog/connection-strings/">Connection-Strings</a>.</li>
</ul>
<p>Ein Operator, der einen Cluster aufsetzt, lebt fast ausschließlich in AdminShell. Der Manager, mit dem sie spricht, ist die Komponente, die die <em>gewünschte Form</em> jedes Stores kennt und die realen Prozesse darauf zutreibt. Das Erste ist also, Ihrer Shell zu sagen, mit welchen Managern sie sprechen soll.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-0--ein-workspace-als-ziel">Schritt 0 — ein Workspace als Ziel<a href="https://clustron.io/de/blog/control-plane-powershell/#schritt-0--ein-workspace-als-ziel" class="hash-link" aria-label="Direkter Link zur Schritt 0 — ein Workspace als Ziel" title="Direkter Link zur Schritt 0 — ein Workspace als Ziel" translate="no">​</a></h2>
<p>Ein <strong>Workspace</strong> ist eine benannte Menge von Managern, auf die Admin-Cmdlets zielen, und wird unter <code>~/.clustron</code> persistiert, sodass er Sitzungen überdauert. Sie legen ihn einmal an und wechseln in ihn, wann immer Sie eine Shell öffnen:</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>Jeder Manager-Endpunkt ist ein <em>Beobachter eines Clusters</em> — listen Sie alle auf, und die Admin-Cmdlets aggregieren den Zustand über den gesamten Cluster, statt über das, was eine einzelne Maschine zufällig weiß. Ist in Ihrem Cluster Security aktiviert, hinterlegen Sie ein Admin-Token im Workspace (<code>-Token</code>) oder authentifizieren die Sitzung mit <code>Connect-ZrManager -Credential (Get-Credential) -Force</code>, das Benutzername und Passwort gegen ein Sitzungs-Token tauscht. (<code>Connect-ZrManager</code> ist das ältere Ad-hoc-Connect und funktioniert weiterhin, aber die Workspace-Cmdlets sind der aktuelle Weg — auf sie bilden sich die gespeicherten Workspaces der Web-Konsole ab.)</p>
<p>Eine ehrliche Weggabelung: Die folgende Anleitung folgt dem <strong>Supervisor-Modell</strong>, bei dem jeder Manager seine lokalen Zaris-Prozesse forkt und besitzt — das native Windows-Deployment. Laufen Ihre Nodes stattdessen unter einem Orchestrator wie Kubernetes (das <strong>Attach-Modell</strong>), erstellt der Manager keine Prozesse; Sie definieren den Store mit Ihrem Orchestrator und <em>übernehmen</em> ihn mit <code>Register-ZrStore</code> statt <code>New-ZrStore</code>. <code>New-ZrStore</code> erkennt einen Manager im Attach-Modus und sagt es Ihnen, statt tief im Stack zu scheitern.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-1--den-store-bereitstellen">Schritt 1 — den Store bereitstellen<a href="https://clustron.io/de/blog/control-plane-powershell/#schritt-1--den-store-bereitstellen" class="hash-link" aria-label="Direkter Link zur Schritt 1 — den Store bereitstellen" title="Direkter Link zur Schritt 1 — den Store bereitstellen" translate="no">​</a></h2>
<p>Dies ist das Cmdlet mit den echten Entscheidungen. Die minimale Produktionsform ist kurz:</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>Das erstellt einen Store namens <code>orders</code> über alle Maschinen im Workspace, mit zwei Kopien jeder Partition. Vier Parameter tragen das Gewicht, also seien wir bei jedem präzise.</p>
<p><strong><code>-ReplicationFactor</code></strong> ist die Gesamtzahl der Kopien jeder Partition — Primary plus Replikate. In der Produktion darf er höchstens der Anzahl der Maschinen entsprechen, damit jede Kopie auf einer <em>anderen</em> Maschine liegt; das macht aus ihm Fehlertoleranz statt Dekoration. Auf einer einzelnen Dev-Box wird die Einschränkung mit einer Warnung gelockert — Replikate teilen sich eine Maschine, was zum Testen in Ordnung ist und bei einem echten Ausfall nichts bringt. Das Cmdlet sagt das laut, statt Sie etwas anderes glauben zu lassen.</p>
<p><strong><code>-PartitionsPerNode</code></strong> ist standardmäßig <code>1</code>, die Produktionstopologie. Die Gesamtzahl der Partitionen wird aus der aktuellen Cluster-Mitgliedschaft abgeleitet — <code>PartitionsPerNode × live nodes</code> — und <em>bei jeder Mitgliedschaftsänderung neu berechnet</em>, ist also keine Zahl, die Sie bei der Erstellung einfrieren. Sie erhöhen sie über 1 hauptsächlich für lokale Tests, wenn Sie partitionsbewusstes Verhalten (Segment-Routing, Rebalance beim Scale-out) auf einer einzelnen Maschine beobachten wollen, ohne ein Rack aufzubauen.</p>
<p><strong><code>-BaseClusterPort</code> / <code>-BaseClientPort</code></strong> sind die ersten Ports zweier Bereiche: der interne Gossip-/Cluster-Port und der clientseitige Port, pro Prozess auf jeder Maschine um eins erhöht. Das Cmdlet prüft, dass sich die beiden Bereiche nicht überschneiden, und weigert sich, einen Store zu erstellen, der mit sich selbst kollidieren würde — ein kleiner Schutz, der einen verwirrenden Fehlstart erspart.</p>
<p>Drei weitere sind erwähnenswert, weil sie einen echten Zielkonflikt bei der Dauerhaftigkeit abbilden:</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> ist standardmäßig <code>Async</code>: Der Primary schreibt lokal und kehrt sofort zurück, die Replikation läuft im Hintergrund. Das ist die schnellste Option und der richtige Default für einen Cache — aber machen Sie sich klar, dass ein unter <code>Async</code> bestätigter Schreibvorgang noch nicht garantiert auf einem Replikat liegt. Wählen Sie <code>Sync</code>, wartet der Primary auf die Bestätigung der Replikate, bevor er <code>Ok</code> liefert, und tauscht Latenz gegen Dauerhaftigkeit.</li>
<li class=""><strong><code>-WriteQuorumPolicy</code></strong> greift nur unter <code>Sync</code>: <code>All</code> (jedes Replikat muss bestätigen), <code>Majority</code> (<code>floor(RF/2)+1</code> müssen bestätigen) oder <code>BestEffort</code> (Fehler werden protokolliert, lassen den Schreibvorgang aber nicht scheitern).</li>
<li class=""><strong><code>-AllowReplicaReads</code></strong> erlaubt Clients, von Replikat-Nodes zu lesen, um die Leselast zu verteilen — mit dem ehrlichen Vorbehalt, dass ein Replikat-Lesevorgang <em>leicht veraltete</em> Daten liefern kann, weil ein Replikat dem Primary hinterherhinken kann. Lassen Sie es aus, wenn Sie Read-your-writes brauchen; schalten Sie es ein, wenn Sie lieber den Durchsatz hätten und den Lag tolerieren.</li>
</ul>
<p>Beachten Sie, was die Defaults über Zaris' Haltung sagen: <code>Async</code> + Lesen vom Primary ist ein schneller, standardmäßig korrekter Cache. Die Dauerhaftigkeits-Regler sind da, wenn Sie sie brauchen, klar benannt, jeweils mit sichtbaren Kosten.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-2--starten-und-bestätigen-dass-er-hochgekommen-ist">Schritt 2 — starten und bestätigen, dass er hochgekommen ist<a href="https://clustron.io/de/blog/control-plane-powershell/#schritt-2--starten-und-best%C3%A4tigen-dass-er-hochgekommen-ist" class="hash-link" aria-label="Direkter Link zur Schritt 2 — starten und bestätigen, dass er hochgekommen ist" title="Direkter Link zur Schritt 2 — starten und bestätigen, dass er hochgekommen ist" translate="no">​</a></h2>
<p>Das Erstellen eines Stores definiert seine Form; es führt die Prozesse nicht aus. <code>Start-ZrStore</code> zählt die Instanzen des Stores auf jedem Manager auf und startet sie, wobei Erfolg oder Misserfolg pro Instanz gemeldet wird:</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>Nun der Teil, der einen Operator von jemandem unterscheidet, der einen Befehl ausführte und hoffte. <code>Get-ZrStore</code> liest den Live-Zustand des Stores von den Managern zurück — Gesamtstatus, Uptime und den Laufzustand jedes Nodes:</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>Der Store-Status ist eine Zusammenfassung: <code>Running</code>, wenn jeder Node läuft, <code>Stopped</code>, wenn alle ausgefallen sind, und <code>Partial</code>, wenn manche laufen und manche nicht — genau der Zustand, den Sie laut angezeigt haben wollen, denn ein <code>Partial</code>-Store ist einer, der ungleichmäßig gestartet ist. Die Uptime ist bewusst die vom Kern gemeldete <em>Cluster-Bildungszeit</em> — der früheste Node-Start, der den Churn überlebt hat —, nicht der älteste aktuell laufende Prozess, damit ein Store, der seit Stunden bedient, nicht „ein paar Minuten" meldet, nur weil ein Node neu gestartet wurde.</p>
<p>Für die nächste Ebene darunter zeigt <code>Get-ZrNode</code> Rollen und die Partition-Map:</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>Die Spalte <code>Primary</code>/<code>Replica</code> zeigt, wer gerade jede Partition besitzt, und <code>v7</code> ist die <strong>Version der Partition-Map</strong> — die Generationsnummer der Ownership-Map. Stimmen alle Nodes in derselben Map-Version überein, ist der Besitz konvergiert; ein Node, der auf einer älteren Version festhängt, hat die jüngste Umverteilung noch nicht eingeholt. Fragen Sie einen einzelnen Node nach Details (<code>Get-ZrNode orders 10.0.0.10-n0</code>), erweitert sich das auf die exakten Primary- und Replikat-Partitions-IDs, die er hält, und wann sich seine Rolle zuletzt änderte — die Ansicht, zu der Sie greifen, wenn gerade ein Failover stattgefunden hat und Sie wissen wollen, wo der Besitz gelandet ist.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-3--im-betrieb-beobachten">Schritt 3 — im Betrieb beobachten<a href="https://clustron.io/de/blog/control-plane-powershell/#schritt-3--im-betrieb-beobachten" class="hash-link" aria-label="Direkter Link zur Schritt 3 — im Betrieb beobachten" title="Direkter Link zur Schritt 3 — im Betrieb beobachten" translate="no">​</a></h2>
<p>Ein Snapshot sagt Ihnen, dass der Store läuft. Eine Live-Ansicht sagt Ihnen, was er <em>tut</em>. <code>Watch-ZrStoreMetrics</code> fragt die Manager ab und zeichnet in einem Intervall eine Tabelle pro Node neu, bis Sie abbrechen:</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>Sie legt zuerst die alltäglichen Zähler aus — Request-Rate, Gets, Puts, Deletes, Item-Anzahl, Speicherverbrauch — und schiebt die erweiterten Replikations- und Sync-Diagnosen nach unten, sodass die Zahlen, die Sie auf einen Blick prüfen, oben stehen. Fehlerzähler werden rot, sobald ihre Rate über null steigt; der Durchsatz färbt sich grüner, je höher er steigt. Sie hebt auch Rollenwechsel inline hervor: Kippt ein Failover einen Primary zu einem Replikat, während Sie zusehen, wird eine Zeile <code>[ROLE CHANGE]</code> mit Zeitstempel ausgegeben — das Live-Signal, dass eine Partition gerade den Besitzer gewechselt hat.</p>
<p>Ein bewusstes Detail, das es wert ist, genannt zu werden, weil es ein Werkzeug um 2 Uhr nachts benutzbar macht: Die Ansicht holt den nächsten Frame, <em>bevor</em> sie den Bildschirm löscht, und ein vorübergehend leerer Poll behält den letzten guten Frame, statt zu leeren. Sie erhalten eine ruhige Tabelle statt einer flackernden, egal wie langsam ein Manager antwortet.</p>
<p>Zur Abrundung der Nur-Lese-Befehle meldet <code>Get-ZrInfo</code> die installierte Version, die Installations- und Datenverzeichnisse, beide Modulpfade, die Host-Runtime — und prüft jeden Manager auf Erreichbarkeit. Ein abgesicherter Manager, der mit <code>401</code> antwortet, wird als <em>erreichbar, aber Authentifizierung nötig</em> gemeldet, nicht als „down", sodass Sie ein Netzwerkproblem auf einen Blick von einem fehlenden Token unterscheiden können.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="schritt-4--stoppen-und-die-form-darunter">Schritt 4 — stoppen, und die Form darunter<a href="https://clustron.io/de/blog/control-plane-powershell/#schritt-4--stoppen-und-die-form-darunter" class="hash-link" aria-label="Direkter Link zur Schritt 4 — stoppen, und die Form darunter" title="Direkter Link zur Schritt 4 — stoppen, und die Form darunter" translate="no">​</a></h2>
<p>Einen Store herunterzufahren ist das Lebenszyklus-Gegenstück zum Starten:</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>Der Default ist ein schneller harter Kill, das richtige Verhalten, wenn der <em>gesamte</em> Store auf einmal herunterfährt — es gibt keinen überlebenden Cluster, an den ein scheidender Node übergeben könnte, ein Drain bringt also nichts. Verwenden Sie <code>-Graceful</code>, wenn jeder Node sauber drainen und den Cluster verlassen soll, was wichtig ist, wenn Sie nur eine Teilmenge statt des Ganzen stoppen.</p>
<p>Unter jedem Cmdlet in diesem Beitrag liegt eine Idee: Ein Zaris-Store hat eine <strong>gewünschte Form</strong> — eine kanonische Definition der Server und Partitionen, die er haben soll —, und der Manager läuft unter einem <strong>Supervisor-Modell</strong>, das die Live-Prozesse kontinuierlich darauf zutreibt. <code>New-ZrStore</code> schreibt diese Form; <code>Start-ZrStore</code> realisiert sie; <code>Get-ZrStore</code> und <code>Get-ZrNode</code> melden, wie nah die Realität ihr ist. Dasselbe Modell macht elastisches Resizing zu einem Einzeiler statt zu einem Runbook — die gewünschte Form mit <code>Add-ZrServer</code> / <code>Remove-ZrServer</code> ändern und den Supervisor abgleichen lassen, was eine eigene Geschichte über <a class="" href="https://clustron.io/de/blog/elastic-scaling/">elastische Skalierung</a> ist.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-ehrliche-zusammenfassung">Die ehrliche Zusammenfassung<a href="https://clustron.io/de/blog/control-plane-powershell/#die-ehrliche-zusammenfassung" class="hash-link" aria-label="Direkter Link zur Die ehrliche Zusammenfassung" title="Direkter Link zur Die ehrliche Zusammenfassung" translate="no">​</a></h2>
<p>Der vollständige Day-one-Lebenszyklus besteht aus fünf Befehlen und zwei Lesevorgängen:</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>Was dies zu einer Steuerungsebene macht und nicht nur zu einem Haufen Skripte, ist, dass die Cmdlets <em>live aggregierte Wahrheit</em> melden — nicht, was Sie angefordert haben, sondern was der Cluster tatsächlich ist — und dass die Entscheidungen mit echten Konsequenzen offen benannt sind: Replikationsfaktor gegenüber Maschinenanzahl, <code>Async</code> gegenüber <code>Sync</code>, Replikat-Lesevorgänge gegenüber Veralterung. Einen Store aufsetzen, die Konvergenz bestätigen und ihn im Auge behalten — alles, ohne die Shell zu verlassen.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Betrieb</category>
            <category>.NET</category>
        </item>
        <item>
            <title><![CDATA[Derselbe Code, mit oder ohne Netzwerk: Der Embedded-Modus von Zaris]]></title>
            <link>https://clustron.io/de/blog/embedded-inproc-mode/</link>
            <guid>https://clustron.io/de/blog/embedded-inproc-mode/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris läuft als verteilter Cluster über einen Socket — und, mit exakt derselben .NET-Client-Oberfläche, als eingebetteter In-Process-Store ganz ohne Netzwerk. Zwischen beiden wechseln Sie, indem Sie eine Connection-String ändern. Hier steht, wie der Embedded-Modus funktioniert, wofür er gedacht ist und wo er ehrlicherweise endet.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Zaris Embedded-In-Process-Modus" 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>Die meisten verteilten Datenspeicher zwingen Sie, sich früh und endgültig zwischen zwei Welten zu entscheiden. Da ist der „echte" Store — ein Cluster, den Sie deployen, auf den Sie einen Client über das Netzwerk richten und mit dem Sie über ein Wire-Protokoll sprechen. Und da ist der „falsche" Store, zu dem Sie in Unit-Tests greifen: ein In-Memory-Dictionary, ein Mock, ein handgeschriebener Stub, der gerade genug der Schnittstelle implementiert, damit der Test kompiliert. Beide haben nichts gemeinsam. Ihr Test prüft das Verhalten des Stubs, nicht das des Stores, und die Lücke dazwischen ist genau der Ort, an dem sich Bugs verstecken.</p>
<p>Zaris hebt diese Entscheidung auf. Derselbe typisierte .NET-Client, der über TLS mit einem verteilten Cluster spricht, betreibt den Store auch <strong>eingebettet in Ihrem Prozess</strong>, ohne Socket, ohne Partition-Map und ohne Deployment — und zwar mit <em>derselben Store-Engine</em>, nicht mit einem Ersatz. Welchen von beiden Sie bekommen, wählen Sie mit einer einzigen Connection-String. Dieser Beitrag erklärt, wie das funktioniert, wofür es sich wirklich eignet und wo die harte Grenze verläuft, an der der Embedded-Modus endet und ein Cluster beginnt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eine-zeichenfolge-ist-der-ganze-schalter">Eine Zeichenfolge ist der ganze Schalter<a href="https://clustron.io/de/blog/embedded-inproc-mode/#eine-zeichenfolge-ist-der-ganze-schalter" class="hash-link" aria-label="Direkter Link zur Eine Zeichenfolge ist der ganze Schalter" title="Direkter Link zur Eine Zeichenfolge ist der ganze Schalter" translate="no">​</a></h2>
<p>Ein Zaris-Client wird immer aus einer <a class="" href="https://clustron.io/de/blog/connection-strings/">Connection-String</a> konfiguriert. Die Zeichenfolge enthält den Transport, die Seed-Endpunkte, den Store-Namen und Optionen. Es gibt zwei Formen, und der Unterschied zwischen ihnen ist der Unterschied zwischen einem Cluster und einem eingebetteten Store:</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>Das Host-Token <code>inproc</code> ist <strong>reserviert</strong>. Sieht der Client es, gibt es keine Endpunkte zu wählen, keinen Transport auszuhandeln, keinen TLS-Handshake und kein Token — der Store wird aus dem Speicher im aktuellen Prozess bedient. Alles andere an Ihrem Code bleibt identisch:</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>In ASP.NET Core ist es dieselbe Geschichte über Dependency Injection — die Connection-String stammt üblicherweise aus der Konfiguration, der Wechsel ist also ein Konfigurationswert, keine Codeänderung:</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>Tragen Sie <code>zaris://inproc/orders</code> in <code>appsettings.Development.json</code> und <code>zariss://.../orders</code> in die Produktion ein, und dieselbe Binärdatei läuft eingebettet auf dem Laptop eines Entwicklers und geclustert in der Cloud, ohne dass etwas neu kompiliert wird.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-derselbe-client-der-punkt-ist">Warum „derselbe Client" der Punkt ist<a href="https://clustron.io/de/blog/embedded-inproc-mode/#warum-derselbe-client-der-punkt-ist" class="hash-link" aria-label="Direkter Link zur Warum „derselbe Client&quot; der Punkt ist" title="Direkter Link zur Warum „derselbe Client&quot; der Punkt ist" translate="no">​</a></h2>
<p>Der Grund, warum das wichtig ist, steckt im zurückgegebenen Typ. <code>ConnectAsync</code> liefert unabhängig vom Modus ein <code>IZarisClient</code> — oder, für die volle Feature-Oberfläche, <code>IZaris</code>. Der Embedded-Modus wird von einer Klasse namens <code>ZarisInProcClient</code> bedient, die exakt dieselben Schnittstellen wie der Cluster-Client implementiert:</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>Jede dieser Oberflächen ist im Embedded-Modus vorhanden, getragen von derselben Store-Engine, die der Cluster nutzt. Key/Value-<code>Get</code>/<code>Put</code>/<code>Delete</code>, Bulk und Batch, <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Item-Transaktionen</a>, TTL und Ablauf, <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">optimistisches Compare-and-Swap</a>, <a class="" href="https://clustron.io/de/blog/2026/03/30/watch-changes/">Watch für reaktive Lesevorgänge</a>, <a class="" href="https://clustron.io/de/blog/redis-streams-native/">native Streams</a> mit Consumer Groups, die <a class="" href="https://clustron.io/de/blog/data-structures-that-replicate/">replizierten Datenstrukturen</a> — all das wird gegen den eingebetteten Store aufgelöst, nicht gegen einen Mock.</p>
<!-- -->
<p>Diese gemeinsame Engine macht den eingebetteten Store zu einem getreuen Testziel. Eine Compare-and-Swap-Schleife, die darauf angewiesen ist, dass ein Konflikt präzise gemeldet wird, ein <code>Watch</code>-Abonnement, das feuern muss, wenn sich ein Schlüssel ändert, eine Transaktion, die atomar committen muss oder gar nicht — jedes verhält sich eingebettet so wie geclustert, weil es buchstäblich derselbe Codepfad auswertet. Ein <code>KvStatus.Conflict</code> bedeutet eingebettet dasselbe wie in der Produktion. Ein handgeschriebener Dictionary-Stub kann das nicht versprechen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-der-embedded-host-tatsächlich-ist">Was der Embedded-Host tatsächlich ist<a href="https://clustron.io/de/blog/embedded-inproc-mode/#was-der-embedded-host-tats%C3%A4chlich-ist" class="hash-link" aria-label="Direkter Link zur Was der Embedded-Host tatsächlich ist" title="Direkter Link zur Was der Embedded-Host tatsächlich ist" translate="no">​</a></h2>
<p>Es lohnt sich, genau zu sein, was <code>zaris://inproc</code> startet, denn die ehrlichen Grenzen ergeben sich direkt daraus. Der Embedded-Host ist eine <strong>feste Ein-Node-Topologie, die jedes Segment besitzt</strong>. Es gibt keine Partition-Map, weil es nur einen Teilnehmer gibt; es gibt keine Replikation, weil es keine zweite Kopie gibt; es gibt keinen Transport, weil es keinen zweiten Prozess gibt, den man erreichen müsste.</p>
<p>Was <em>tatsächlich</em> läuft, ist die echte Mechanik, die Sie in einem Test haben wollen. Der Store stellt sein Segment vorab bereit, bindet den Lease-Service, den Watch-Service und lokale Metriken ein und startet den <strong>TTL-Sweeper</strong> in seinem normalen Takt — ein Schlüssel, den Sie mit <code>ExpireAsync</code> auf 50 ms TTL setzen, verschwindet also tatsächlich, per Timer, genau wie in einem Cluster. Ablauf, Eviction-Buchhaltung, Versionierung und Watch-Benachrichtigungen sind alle aktiv. Es ist ein vollständiger Store, der zufällig die Cluster-Größe eins hat.</p>
<p>Innerhalb eines Prozesses werden Clients <strong>nach Store-Namen geteilt</strong>. Zwei Aufrufe <code>ConnectAsync("zaris://inproc/orders")</code> im selben Prozess lösen zum <em>selben</em> eingebetteten Store auf, sodass ein Wert, den der eine schreibt, der andere liest — praktisch, wenn Ihr Test einen Producer und einen Consumer startet und erwartet, dass sie sich gegenseitig sehen. Verschiedene Store-Namen ergeben verschiedene Stores; verschiedene Prozesse erhalten völlig getrennte eingebettete Stores (mehr zu dieser Grenze unten).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wo-sich-der-embedded-modus-bewährt">Wo sich der Embedded-Modus bewährt<a href="https://clustron.io/de/blog/embedded-inproc-mode/#wo-sich-der-embedded-modus-bew%C3%A4hrt" class="hash-link" aria-label="Direkter Link zur Wo sich der Embedded-Modus bewährt" title="Direkter Link zur Wo sich der Embedded-Modus bewährt" translate="no">​</a></h2>
<p><strong>Unit- und Integrationstests, die den echten Store ausüben.</strong> Das ist der Hauptanwendungsfall. Ihr Repository, Ihre Cache-Schicht, Ihr Saga-Koordinator — testen Sie sie gegen <code>zaris://inproc/test-{Guid}</code>, erhalten Sie pro Test einen frischen, isolierten Store und prüfen die echte Semantik: echte Versionierung für Ihre CAS-Schleifen, echten TTL-Ablauf auf einem echten Timer, echte Transaktions-Atomarität, echte Watch-Events. Kein Docker-Container, der in der CI gestartet werden muss, kein Port, der zu vergeben wäre, keine instabile Aufräumphase. Der Store wird beim ersten Connect erstellt und stirbt mit dem Prozess.</p>
<p><strong>Single-Process-Anwendungen, die später erwachsen werden könnten.</strong> Eine Desktop-App, ein Hintergrund-Worker, ein CLI-Tool oder ein kleiner Service, der heute einen schnellen strukturierten Store braucht, morgen aber ein geclustertes Deployment werden könnte. Beginnen Sie eingebettet. An dem Tag, an dem Sie diese Daten über Instanzen hinweg teilen müssen, ändern Sie die Connection-String auf einen Cluster — und weil Sie die ganze Zeit gegen <code>IZaris</code> programmiert haben, bewegt sich sonst nichts.</p>
<p><strong>Integrationen, die standardmäßig eingebettet ausgeliefert werden.</strong> Der Zaris-ASP.NET-Core-Session-Provider ist ein konkretes Beispiel: Zaris als Session-Store einzubinden ist ein Aufruf, und in seinem Sample läuft er vollständig eingebettet —</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>— sodass ein Entwickler die App mit <code>F5</code> starten und funktionierende Sessions ohne jede Infrastruktur haben kann, um dann für ein Multi-Instanz-Deployment auf <code>zariss://.../sessions</code> zu wechseln, ohne den Session-Code anzufassen.</p>
<p>Um den Embedded-Modus zu nutzen, referenzieren Sie das Paket <code>Clustron.Zaris.InProc</code> neben dem Client (es zielt auf .NET 8). Das Paket registriert sich selbst: Der Client entdeckt und lädt die In-Process-Factory bei der ersten Verwendung, die bloße Referenz genügt also. In einer Test-Assembly, in der Sie lieber explizit sein möchten, verdrahtet <code>new InProcBootstrap().Register();</code> es deterministisch.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wo-er-ehrlicherweise-endet">Wo er ehrlicherweise endet<a href="https://clustron.io/de/blog/embedded-inproc-mode/#wo-er-ehrlicherweise-endet" class="hash-link" aria-label="Direkter Link zur Wo er ehrlicherweise endet" title="Direkter Link zur Wo er ehrlicherweise endet" translate="no">​</a></h2>
<p>Der Embedded-Modus ist ein echter Store, aber ein <em>Single-Node-In-Memory</em>-Store, und etwas anderes vorzugeben wäre genau der Marketing-Nebel, den dieser Blog vermeiden will. Drei harte Grenzen, direkt aus dem Design:</p>
<p><strong>Keine Dauerhaftigkeit.</strong> Der eingebettete Store hält Daten im Prozessspeicher und nirgendwo sonst. In diesem Modus gibt es weder Replikation noch ein On-Disk-Log — beendet sich der Prozess, sind die Daten weg. Das ist genau das, was Sie für einen Test wollen (jeder Lauf mit sauberem Zustand), und genau das, worauf Sie sich bei allem, was einen Neustart überleben soll, nicht verlassen dürfen. Der Embedded-Modus ist keine Datenbank, auf die Sie Ihre Produktionsdaten stützen.</p>
<p><strong>Keine Hochverfügbarkeit, kein Scale-out.</strong> Ein Cluster aus einem Node toleriert den Verlust von null Nodes und skaliert bis zu Speicher und CPU eines Prozesses. All die Gründe, aus denen Sie zum verteilten Zaris greifen würden — einen Node-Ausfall ohne Datenverlust überstehen, <a class="" href="https://clustron.io/de/blog/kubernetes-durability-sizing/">Replikate für Dauerhaftigkeit dimensionieren</a>, <a class="" href="https://clustron.io/de/blog/elastic-scaling/">den Durchsatz über Kerne und Maschinen skalieren</a> — treffen auf einen Embedded-Host schlicht nicht zu, weil es nur einen davon gibt. Brauchen Sie diese Eigenschaften, brauchen Sie den Cluster.</p>
<p><strong>Kein prozessübergreifendes Teilen.</strong> Die In-Process-Registry teilt einen Store nach Namen <em>innerhalb eines Prozesses</em>. Zwei verschiedene Prozesse, die beide <code>zaris://inproc/orders</code> öffnen, erhalten zwei unabhängige Stores, die nichts voneinander wissen — zwischen ihnen gibt es per Definition keinen Socket. In dem Moment, in dem Ihre Daten für einen zweiten Prozess oder eine zweite Maschine sichtbar sein müssen, ist der Embedded-Modus das falsche Werkzeug und ein Cluster das richtige.</p>
<p>Nichts davon sind Bugs, die zu beheben wären; es ist die Definition von „embedded". Der Wert liegt nicht darin, dass der In-Process-Store heimlich verteilt wäre — sondern darin, dass an dem Tag, an dem Sie Verteilung brauchen, das <em>Einzige</em>, was Sie ändern, die Zeichenfolge ist, die sagt, wo der Store lebt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dasselbe-versprechen-zweimal-gehalten">Dasselbe Versprechen, zweimal gehalten<a href="https://clustron.io/de/blog/embedded-inproc-mode/#dasselbe-versprechen-zweimal-gehalten" class="hash-link" aria-label="Direkter Link zur Dasselbe Versprechen, zweimal gehalten" title="Direkter Link zur Dasselbe Versprechen, zweimal gehalten" translate="no">​</a></h2>
<p>Der Pitch ist klein und konkret. Sie schreiben Ihre Anwendung gegen eine Client-Oberfläche. In einem Test, auf einem Laptop oder in einem Single-Process-Tool wird diese Oberfläche von einem echten Zaris-Store bedient, der in Ihrem eigenen Speicher läuft — schnell erstellt, getreu in seiner Semantik und weg, wenn der Prozess endet. In der Produktion spricht derselbe Code über TLS mit einem verteilten Cluster. Das Verhalten, das Ihre Tests festgenagelt haben, ist das Verhalten, das Sie ausliefern, weil es nie eine andere Implementierung war — nur ein anderer Ort, an dem der Store lebt.</p>
<p>Das ist die ganze Idee hinter <code>zaris://inproc</code>: Die Lücke zwischen „dem Store in meinem Test" und „dem Store in der Produktion" ist eine Connection-String breit, und Sie entscheiden pro Umgebung, auf welcher Seite Sie stehen.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>.NET</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Die eigenen Schreibvorgänge lesen: Das Zaris-Konsistenzmodell, ehrlich dargestellt]]></title>
            <link>https://clustron.io/de/blog/reading-your-own-writes/</link>
            <guid>https://clustron.io/de/blog/reading-your-own-writes/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Was garantiert ein Lesevorgang in Zaris tatsächlich? Lesevorgänge werden zum einzigen aktiven Owner einer Partition geroutet, sodass Sie Ihre eigenen Schreibvorgänge innerhalb dieser Partition lesen — aber es gibt keine globale Linearisierbarkeit, partitionsübergreifende Lesevorgänge haben keine gemeinsame Reihenfolge, und „bestätigt“ heißt auf dem Owner committet, noch nicht auf einem Replikat. Hier ist das gesamte Modell, mit den Reglern und den Vorbehalten, offen benannt.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Die eigenen Schreibvorgänge lesen — ein Owner pro 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>Es gibt eine Frage, die jeder Entwickler irgendwann an einen verteilten Store richtet, meist direkt nach einem verwirrenden Bug-Report: <em>Wenn ich einen Wert schreibe und ihn sofort zurücklese, sehe ich dann garantiert, was ich geschrieben habe?</em> Das klingt, als müsste es eine Ein-Wort-Antwort geben. Gibt es nicht — nicht für irgendeinen partitionierten, replizierten Store, und nicht für Zaris. Die ehrliche Antwort lautet „ja, innerhalb einer Partition, weil Lesevorgänge zum selben einzelnen Owner gehen wie Ihr Schreibvorgang" — und das Interessante ist alles, was in dieser Einschränkung steckt.</p>
<p>Dieser Beitrag ist das Konsistenzmodell von Zaris an einem Ort zusammengetragen. Über die Einzelteile haben wir separat geschrieben — <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">optimistisches Compare-and-Swap</a>, <a class="" href="https://clustron.io/de/blog/retries-under-ownership-churn/">transparenter Retry bei Ownership-Churn</a>, das <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">wiederabspielbare Op-Log</a>, das Replikate speist, <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktionen</a>. Hier verbinden wir sie aus Sicht des Lesers und beantworten die einzige Frage, die an der Tastatur zählt: <strong>Was wird mir beim Lesen tatsächlich zugesichert?</strong> Einschließlich, und vor allem, wo die Zusicherung endet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-modell-in-einem-satz">Das Modell in einem Satz<a href="https://clustron.io/de/blog/reading-your-own-writes/#das-modell-in-einem-satz" class="hash-link" aria-label="Direkter Link zur Das Modell in einem Satz" title="Direkter Link zur Das Modell in einem Satz" translate="no">​</a></h2>
<p>Zaris ist ein <strong>AP-lastiger Store, bei dem die Konsistenzgrenze um die Ownership gezogen ist, nicht um jeden Schreibvorgang.</strong> Die Datenebene bleibt verfügbar — ein einzelner Node bedient weiter die Segmente, die er hält —, und die Single-Writer-Disziplin, die Lesevorgänge vernünftig hält, wird in der <em>Ownership</em>-Ebene durchgesetzt: <strong>Zu jedem Zeitpunkt gibt es genau einen aktiven Owner pro Partition.</strong> Schreibvorgänge auf eine Partition werden durch diesen einen Node serialisiert. Auf dem Schreibpfad gibt es bewusst <strong>kein Quorum und keinen Konsens</strong>, und Zaris behauptet <strong>nicht</strong> globale strikte Linearisierbarkeit.</p>
<p>Fast alles, was ein Leser beobachtet, folgt aus dieser einen strukturellen Tatsache, daher lohnt es, die Kette explizit zu machen:</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>Ein Schlüssel wird deterministisch auf ein Segment gehasht; ein Segment gehört zu einer Partition; eine Partition hat genau einen aktiven Owner, der ihre Schreibvorgänge annimmt <em>und ihre Lesevorgänge bedient</em>. Ownership ist eine <strong>Lease</strong>, die in der Partition-Map als <code>(LeaderTerm, OwnershipGeneration)</code> geführt wird — vom Cluster-Leader vergeben, über Neustarts hinweg dauerhaft und an jeden Node propagiert. Diese Lease garantiert das „genau einer" — sie ersetzte ein älteres Schema „höchste Epoch gewinnt", das bei einem Neustart zurückgesetzt werden konnte und kurzzeitig zwei Nodes hervorbrachte, die beide glaubten, dieselbe Partition zu besitzen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-owner-geroutete-lesevorgänge-read-your-writes-liefern">Warum Owner-geroutete Lesevorgänge Read-your-writes liefern<a href="https://clustron.io/de/blog/reading-your-own-writes/#warum-owner-geroutete-lesevorg%C3%A4nge-read-your-writes-liefern" class="hash-link" aria-label="Direkter Link zur Warum Owner-geroutete Lesevorgänge Read-your-writes liefern" title="Direkter Link zur Warum Owner-geroutete Lesevorgänge Read-your-writes liefern" translate="no">​</a></h2>
<p>Hier kommt der Teil, der die Eingangsfrage beantwortet. In Zaris <strong>werden Lesevorgänge zum aktiven Owner geroutet, demselben Node, an den Ihr Schreibvorgang ging.</strong> Clients lesen standardmäßig nicht von Replikaten. Wenn Sie also einen Schlüssel mit <code>PutAsync</code> schreiben und dann mit <code>GetAsync</code> lesen, werden beide Requests — über dieselbe Routing-Tabelle, aufgebaut aus der Partition-Map plus dem Runtime-Ownership-Snapshot — zum selben Owner-Node aufgelöst, und dieser Node hat Ihren Schreibvorgang bereits in seinem lokalen Store.</p>
<!-- -->
<p>Das ist der gesamte Mechanismus. Read-your-writes wird nicht als Session-Garantie oder clientseitiger Cache aufgesetzt; es ergibt sich aus dem Routing. Sie lesen Ihre Schreibvorgänge <strong>innerhalb einer Partition</strong>, weil ein einzelner Node die maßgebliche Instanz für Lese- und Schreibvorgänge dieser Partition ist und Schreibvorgänge darauf nacheinander serialisiert werden. Das Replikat in diesem Diagramm mag einige Millisekunden hinter dem Owner zurückliegen — die Replikation hat einen asynchronen Schwanz —, aber standardmäßig <em>lesen Sie nie das Replikat</em>, sodass dieser Lag für Sie unsichtbar bleibt.</p>
<p>Die Einschränkung „innerhalb einer Partition" ist tragend, und wir kommen darauf zurück, was sie ausschließt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-version-die-sie-sehen-dürfen">Die Version, die Sie sehen dürfen<a href="https://clustron.io/de/blog/reading-your-own-writes/#die-version-die-sie-sehen-d%C3%BCrfen" class="hash-link" aria-label="Direkter Link zur Die Version, die Sie sehen dürfen" title="Direkter Link zur Die Version, die Sie sehen dürfen" translate="no">​</a></h2>
<p>Jeder Wert, den der Owner ausliefert, trägt zwei monotone Zahlen, und sie machen „derselbe Wert" zu einer präzisen Aussage statt zu einem Gefühl:</p>
<ul>
<li class="">eine vom Store verwaltete <strong><code>Revision</code></strong> — eine strikt steigende Sequenznummer, die der Owner jeder Mutation in Commit-Reihenfolge aufprägt, und</li>
<li class="">eine <strong><code>ItemVersion</code></strong> pro Item — die Version dieses konkreten Schlüssels, die <code>IfMatchVersion</code> in einem <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">Compare-and-Swap-Schreibvorgang</a> absichert.</li>
</ul>
<p>Da der Owner diese in Commit-Reihenfolge vergibt und Lesevorgänge aus demselben committeten Zustand bedient, bewegen aufeinanderfolgende Lesevorgänge eines Schlüssels vom Owner die Version <strong>vorwärts, nie rückwärts.</strong> Sie lesen nicht Version 42 und einen Augenblick später Version 40. Diese Monotonie ist genau das, was die CAS-Schleife korrekt macht: Ein Leser sieht eine Version, rechnet dagegen und schreibt unter der Bedingung zurück, dass sie noch diese Version ist. Ließe das Modell Lesevorgänge zwischen Versionen flattern, stünde optimistische Nebenläufigkeit auf Sand. Ein Owner, in Commit-Reihenfolge vergebene Revisionen, vom Owner bediente Lesevorgänge — das ist das Fundament, auf dem die gesamte Koordinationsgeschichte im <a class="" href="https://clustron.io/de/blog/race-free-coordination-cas/">CAS-Beitrag</a> steht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-fenster-in-dem-ein-lesevorgang-ehrlich-noch-nicht-sagt">Das Fenster, in dem ein Lesevorgang ehrlich „noch nicht" sagt<a href="https://clustron.io/de/blog/reading-your-own-writes/#das-fenster-in-dem-ein-lesevorgang-ehrlich-noch-nicht-sagt" class="hash-link" aria-label="Direkter Link zur Das Fenster, in dem ein Lesevorgang ehrlich „noch nicht&quot; sagt" title="Direkter Link zur Das Fenster, in dem ein Lesevorgang ehrlich „noch nicht&quot; sagt" translate="no">​</a></h2>
<p>Owner-geroutete Lesevorgänge sind im Normalbetrieb sauber. Die interessanten Momente sind die Übergänge — ein Failover, eine Migration, ein bevorzugter Primary, der zurückfällt —, denn während einer Übergabe ist der Node, zu dem Sie routen würden, möglicherweise gerade nicht die maßgebliche Instanz. Zaris trifft hier die ehrliche Wahl: <strong>Statt einen Lesevorgang von einem Node zu bedienen, der nicht der beständige Owner ist, liefert es einen wiederholbaren Status.</strong> Ein Lese- (oder Schreib-)Vorgang in einem solchen Fenster kann zurückkommen als:</p>
<ul>
<li class=""><strong><code>Moved</code></strong> — Ihre Routing-Tabelle ist veraltet; der Schlüssel liegt jetzt auf einem anderen Node.</li>
<li class=""><strong><code>OwnershipChanging</code></strong> — der Besitz an diesem Segment befindet sich gerade aktiv im Übergang; es ist eine Übergabe, kein beständiger Routing-Fehler.</li>
<li class=""><strong><code>Migrating</code></strong> — das Segment wird gerade zu einem neuen Owner migriert.</li>
<li class=""><strong><code>ReplicaSyncPending</code></strong> — dieser Node bringt die Kopie eines Segments auf den neuesten Stand und bedient daraus noch keinen möglicherweise unvollständigen Lesevorgang.</li>
<li class=""><strong><code>Unavailable</code></strong> — die Partition hat in diesem Augenblick keinen erreichbaren Owner.</li>
</ul>
<p>Keiner davon ist eine Exception, die Sie aus einem Stacktrace herauslesen müssen — es sind maschinenlesbare Ergebnisse, und der .NET-Client behandelt sie als vorübergehend und <a class="" href="https://clustron.io/de/blog/retries-under-ownership-churn/">wiederholt mit Reroute</a>, aktualisiert seine Cluster-Map und geht zu dem, den die Map nun als Owner nennt. Der Effekt: Ihr <code>GetAsync</code> dauert einfach ein paar Millisekunden länger und liefert dann den richtigen Wert. Die Designentscheidung darunter verdient klare Worte: <strong>Zaris lässt Sie lieber einen Moment warten, als Ihnen einen Lesevorgang zu geben, bei dem es sich nicht sicher ist, ob er maßgeblich ist.</strong> Ein wiederholbares „noch nicht" ist eine bessere Antwort als ein selbstsicheres Falsch.</p>
<p>Deshalb überlebt Read-your-writes auch ein Failover: Die Write-Fence-Arbeit sorgt dafür, dass ein bestätigter Schreibvorgang im Übergabefenster nicht zurückbleibt (zur ehrlichen Grenze davon kommen wir unten), und die Promotion macht einen Node erst zum Owner, wenn er dazu berechtigt ist — der Lesevorgang, der nach einem Failover umgeroutet wird, geht also zu einem Node, der Ihre Daten hat.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="der-regler-den-sie-drehen-können--und-was-er-heute-tatsächlich-bewirkt">Der Regler, den Sie drehen können — und was er heute tatsächlich bewirkt<a href="https://clustron.io/de/blog/reading-your-own-writes/#der-regler-den-sie-drehen-k%C3%B6nnen--und-was-er-heute-tats%C3%A4chlich-bewirkt" class="hash-link" aria-label="Direkter Link zur Der Regler, den Sie drehen können — und was er heute tatsächlich bewirkt" title="Direkter Link zur Der Regler, den Sie drehen können — und was er heute tatsächlich bewirkt" translate="no">​</a></h2>
<p>Zaris stellt eine Oberfläche für Lesekonsistenz bereit. <code>GetOptions</code> trägt eine <code>ReadConsistency</code> von <code>Leader</code>, <code>Quorum</code> oder <code>Any</code>, und ein Store hat ein Flag <code>AllowReplicaReads</code> (standardmäßig <strong>aus</strong>). Die Absicht ist der klassische Handel: Lesevorgänge von Replikaten bedienen zu lassen, um die Leselast zu verteilen, im Austausch dafür, dass ein Replikat hinter dem Owner zurückliegen kann — eine begrenzte Veralterung, auf die Sie sich bewusst einlassen.</p>
<p>Hier der ehrliche aktuelle Stand, denn genau das würde Werbetext verwischen: <strong>Heute sind Lesevorgänge Owner-only.</strong> <code>AllowReplicaReads</code> und <code>ReadConsistency.Any</code> sind eine Konfigurations- und Request-Formungsoberfläche, die existiert und durch die Store-Definition fließt, aber es gibt noch keinen serverseitigen Codepfad, der einen Lesevorgang tatsächlich von einem Replikat bedient. In der Praxis heißt das: Sie bekommen derzeit das <em>starke</em> Ende des Reglers — jeder Lesevorgang ist ein Owner-Lesevorgang, also gilt Read-your-writes — und Sie bekommen <strong>nicht</strong> das Verhalten veralteter Replikat-Lesevorgänge, selbst wenn Sie das Flag umlegen.</p>
<p>Warum einen Regler erwähnen, der faktisch auf eine Einstellung festgenagelt ist? Weil er zeigt, was der Konsistenzvertrag Sie künftig tauschen <em>lassen</em> wird, und weil die Asymmetrie wichtig ist, wenn Sie über Veralterung nachdenken. Sobald Replikat-bedienende Lesevorgänge landen, ist das Einschalten von <code>AllowReplicaReads</code> der Moment, in dem Read-your-writes nicht mehr garantiert ist: Ein zu einem Replikat gerouteter Lesevorgang kann eine Version hinter der des Owners beobachten, solange der asynchrone Replikationsschwanz zurückliegt. Bis dahin ist die Garantie einfacher und stärker, als der Regler suggeriert. Wir möchten, dass Sie das wissen, statt anzunehmen, Sie verteilten Lesevorgänge bereits über Replikate, obwohl das nicht der Fall ist.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-bestätigt-für-den-nächsten-leser-bedeutet">Was „bestätigt" für den <em>nächsten</em> Leser bedeutet<a href="https://clustron.io/de/blog/reading-your-own-writes/#was-best%C3%A4tigt-f%C3%BCr-den-n%C3%A4chsten-leser-bedeutet" class="hash-link" aria-label="Direkter Link zur was-bestätigt-für-den-nächsten-leser-bedeutet" title="Direkter Link zur was-bestätigt-für-den-nächsten-leser-bedeutet" translate="no">​</a></h2>
<p>Read-your-writes handelt davon, dass <em>Sie</em> Ihren <em>eigenen</em> Schreibvorgang auf dem Owner lesen. Eine andere, schärfere Frage lautet: Sobald der Owner Ihnen einen Schreibvorgang bestätigt, ist dieser sicher, wenn der Owner danach stirbt, sodass der Leser, der zum neuen Owner umgeroutet wird, ihn noch sieht?</p>
<p>Der ehrliche Bestätigungsvertrag heute: <strong>Ein Schreibvorgang gilt als bestätigt, wenn er im lokalen Store des aktiven Owners gelandet ist.</strong> Die Replikation auf die RF−1 Replikate läuft als asynchroner Schwanz hinter dieser Bestätigung. Die Konsequenzen sollte man einplanen, statt sie wegzuwinken:</p>
<ul>
<li class="">Bei <strong>Redundanz 1</strong> — kein synchronisiertes Replikat — ist ein Schreibvorgang wirklich Best-Effort. Er darf nicht als Single-Fault-dauerhaft betrachtet werden, denn es gibt genau eine Kopie.</li>
<li class="">Selbst mit Replikaten kann ein bestätigter Schreibvorgang verloren gehen, wenn der einzige aktive Owner im schmalen Fenster <em>nach</em> der Bestätigung, aber <em>vor</em> dem Eintreffen des Schreibvorgangs bei einem Replikat verloren geht.</li>
</ul>
<p>Die stärkere Eigenschaft — <em>erst bestätigen, wenn der Schreibvorgang auf mindestens einem synchronisierten Replikat liegt</em>, sodass ein bestätigter Schreibvorgang den Verlust eines beliebigen Nodes überlebt — ist das <strong>Designziel (das „W1"-Ack-Gate), noch nicht die ausgelieferte Bestätigungssemantik.</strong> Es ist die Richtung, in die die <a class="" href="https://clustron.io/de/blog/zero-data-loss-under-chaos/">Arbeit am Durability-Fundament</a> konsolidiert, und dieser Beitrag ist bei derselben Unterscheidung sorgfältig: Die starke Aussage, die er validiert, ist auf <em>bestätigte Schreibvorgänge, wenn ein synchronisiertes Replikat existiert, unter den vom Chaos-Lauf ausgelösten Fehlerklassen</em> beschränkt — kein bedingungsloses „nichts kann je verloren gehen". Die <a class="" href="https://clustron.io/de/blog/op-log-replayable-substrate/">Op-Log- und Migrationsmechanik</a> ist, wie ein Replikat oder ein neuer Owner überhaupt zum committeten Zustand des Owners aufschließt; beim Ack-Gate geht es darum, <em>wann</em> der Owner „erledigt" sagen darf.</p>
<p>Die präzise Aussage für Leser lautet also: Read-your-writes auf dem Owner ist solide; die Fault-übergreifende Dauerhaftigkeit eines bestätigten Schreibvorgangs ist stark und wird stärker, mit dem oben genannten aktuellen Vorbehalt und einer harten Untergrenze bei RF1, die kein Store wegkonstruieren kann — eine Kopie ist eine Kopie.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="über-partitionen-hinweg-gibt-es-keine-gemeinsame-uhr">Über Partitionen hinweg gibt es keine gemeinsame Uhr<a href="https://clustron.io/de/blog/reading-your-own-writes/#%C3%BCber-partitionen-hinweg-gibt-es-keine-gemeinsame-uhr" class="hash-link" aria-label="Direkter Link zur Über Partitionen hinweg gibt es keine gemeinsame Uhr" title="Direkter Link zur Über Partitionen hinweg gibt es keine gemeinsame Uhr" translate="no">​</a></h2>
<p>Die letzte Stelle, an der die Einschränkung „innerhalb einer Partition" zubeißt: <strong>Es gibt keine globale Ordnung und keinen globalen Snapshot über Partitionen hinweg.</strong> Jede Partition wird durch ihren eigenen Owner serialisiert; zwei verschiedene Partitionen sind zwei verschiedene Owner mit zwei unabhängigen Revisionsfolgen. Schreiben Sie Schlüssel <code>A</code> (Partition 1) und dann Schlüssel <code>B</code> (Partition 2), kann ein gleichzeitiger Leser den neuen Wert von <code>B</code> und den alten von <code>A</code> beobachten oder umgekehrt — es gibt keinen clusterweiten Zeitpunkt, an dem „beides geschehen" ist. Bulk- und Batch-Operationen sind darunter pro Schlüssel; sie sind bequem, nicht atomar.</p>
<p>Wenn eine Invariante wirklich Schlüssel überspannen muss — einen Saldo von <code>A</code> nach <code>B</code> verschieben, einen Index im Gleichschritt mit dem Datensatz halten, auf den er zeigt —, sind genau dafür <a class="" href="https://clustron.io/de/blog/multi-key-transactions/">Multi-Key-Transaktionen</a> da. Sie bieten <code>Serializable</code>-Isolation über einen vom Client koordinierten Commit über optimistische Versionen und sind die <em>einzige</em> Oberfläche für schlüsselübergreifende Atomarität. Gewöhnliche Multi-Key-Lese- und Schreibvorgänge sind keine Transaktion und sollten nicht dafür gehalten werden.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-garantiert-ist-und-was-nicht">Was garantiert ist und was nicht<a href="https://clustron.io/de/blog/reading-your-own-writes/#was-garantiert-ist-und-was-nicht" class="hash-link" aria-label="Direkter Link zur Was garantiert ist und was nicht" title="Direkter Link zur Was garantiert ist und was nicht" translate="no">​</a></h2>
<p>Klar zusammengefasst, damit Sie das ganze Modell auf einmal im Kopf behalten:</p>
<table><thead><tr><th>Eigenschaft</th><th>Zaris heute</th></tr></thead><tbody><tr><td>Owner pro Partition</td><td>Genau ein aktiver Owner, gehalten als dauerhafte Lease.</td></tr><tr><td>Lesepfad</td><td>Routet zum aktiven Owner; Replikate werden standardmäßig nicht gelesen.</td></tr><tr><td>Read-your-writes</td><td><strong>Ja, innerhalb einer Partition</strong> — derselbe Owner bedient Ihren Schreib- und Ihren Lesevorgang.</td></tr><tr><td>Monotone Lesevorgänge eines Schlüssels</td><td>Ja — der Owner liefert in Commit-Reihenfolge vergebene Revisionen; Versionen bewegen sich vorwärts.</td></tr><tr><td>Lesevorgänge während der Übergabe</td><td>Wiederholbarer Status (<code>Moved</code> / <code>OwnershipChanging</code> / <code>Migrating</code> / <code>ReplicaSyncPending</code>), Client wiederholt und routet um.</td></tr><tr><td>Replikat-Lesevorgänge / Veralterungs-Regler</td><td><code>AllowReplicaReads</code> + <code>ReadConsistency.Any</code> existieren als Oberfläche; <strong>heute praktisch Owner-only</strong> (noch kein Replikat-Bedienpfad).</td></tr><tr><td>Dauerhaftigkeit der Bestätigung</td><td>Heute Owner-lokal; Single-Fault-dauerhaft mit synchronisiertem Replikat ist das Ziel (W1-Ack-Gate).</td></tr><tr><td>Partitionsübergreifende Ordnung</td><td><strong>Keine</strong> — kein globaler Snapshot; nutzen Sie Transaktionen für schlüsselübergreifende Atomarität.</td></tr><tr><td>Globale Linearisierbarkeit</td><td><strong>Nicht beansprucht.</strong></td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-ehrliche-form-des-ganzen">Die ehrliche Form des Ganzen<a href="https://clustron.io/de/blog/reading-your-own-writes/#die-ehrliche-form-des-ganzen" class="hash-link" aria-label="Direkter Link zur Die ehrliche Form des Ganzen" title="Direkter Link zur Die ehrliche Form des Ganzen" translate="no">​</a></h2>
<p>Ein gutes Konsistenzmodell ist nicht das mit der längsten Liste an Garantien, sondern das, dessen Garantien Sie tatsächlich benennen und dessen Grenzen Sie tatsächlich sehen können. Zaris zieht seine Grenze um die Ownership und ist offen über den Handel: Sie erhalten Read-your-writes und monotone Lesevorgänge <em>innerhalb einer Partition</em> gratis, weil ein Owner beide Seiten bedient und Ihnen in Commit-Reihenfolge vergebene Versionen gibt; Sie erhalten während einer Übergabe ein wiederholbares „noch nicht" statt einer selbstsicheren falschen Antwort; und Sie verzichten auf globale Linearisierbarkeit und partitionsübergreifende Ordnung, die Sie sich explizit mit einer Transaktion zurückkaufen, wenn eine bestimmte Invariante sie braucht.</p>
<p>Die beiden Vorbehalte, die Sie im Kopf behalten sollten, sind die, die dieser Beitrag benannt statt geglättet hat: Der Replikat-Lese-Regler ist heute faktisch auf die starke Einstellung festgenagelt, Sie sind also konsistenter, als der Regler vermuten lässt; und „bestätigt" bedeutet derzeit <em>auf dem Owner</em>, wobei volle Single-Fault-Dauerhaftigkeit ein Ziel ist, das die Durability-Arbeit gerade umsetzt, und es bei Redundanz 1 eine echte Untergrenze gibt. Wissen Sie diese zwei Dinge, ist der Rest des Modells genau so einfach, wie er aussieht: den Owner finden, die eigenen Schreibvorgänge lesen.</p>
<p>Für das tiefere Design siehe <a class="" href="https://clustron.io/de/docs/clustron/zaris/core-concepts/consistency/">Konsistenz</a> und <a class="" href="https://clustron.io/de/docs/clustron/zaris/architecture/partitioning-replication/">Partitionierung und Replikation</a> in der Dokumentation.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Verteilte Systeme</category>
            <category>Architektur</category>
        </item>
        <item>
            <title><![CDATA[Keyspace-Benachrichtigungen, die ein Failover überstehen]]></title>
            <link>https://clustron.io/de/blog/redis-keyspace-notifications/</link>
            <guid>https://clustron.io/de/blog/redis-keyspace-notifications/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris spricht Redis-Keyspace-Benachrichtigungen — CONFIG SET notify-keyspace-events, die Kanäle __keyspace@0__ / __keyevent@0__, ein einfaches PSUBSCRIBE von jedem Redis-Client. Darunter aber fährt jedes Event auf dem nativen clusterweiten Watch von Zaris: über die Mitglieder repliziert und über clusterweites Pub/Sub zugestellt, sodass Benachrichtigungen nicht verschwinden, wenn der Node, der sie erzeugt hat, verschwindet.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Redis-Keyspace-Benachrichtigungen auf dem replizierten nativen Watch von 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>Keyspace-Benachrichtigungen sind eines der nützlichsten Features von Redis und eines der unauffällig fragilsten. Die Idee ist wunderbar: Jedes Mal, wenn ein Schlüssel geschrieben, gelöscht oder ablaufend ist, veröffentlicht der Server ein Event auf einem wohlbekannten Pub/Sub-Kanal, und jeder Interessierte kann per <code>PSUBSCRIBE</code> reagieren — einen Cache invalidieren, einen Worker wecken, eine Projektion aktualisieren. Kein Polling, kein <code>GET</code> in einer Schleife, das fragt „hat es sich schon geändert?".</p>
<p>Die Fragilität steckt im Kleingedruckten. Redis-Keyspace-Benachrichtigungen sind <strong>Fire-and-Forget-Pub/Sub</strong>, ausgesendet von dem Node, der den Befehl verarbeitet hat, an Subscriber, die mit genau diesem Node verbunden sind. In einem geclusterten Deployment heißt das: Ein Subscriber hört nur von Schreibvorgängen, die zufällig auf dem Node landeten, an den er angebunden ist, und fällt dieser Node per Failover aus, geht die Subscription — und der Event-Strom, den sie trug — mit. Das Feature funktioniert auf einer einzelnen Box wunderbar und wird schnell subtil, sobald es mehr als eine gibt.</p>
<p>Zaris spricht exakt dasselbe Protokoll — <code>CONFIG SET notify-keyspace-events</code>, die Kanäle <code>__keyspace@0__</code> und <code>__keyevent@0__</code>, ein gewöhnliches <code>PSUBSCRIBE</code> von jedem Redis-Client in jeder Sprache. Die Mechanik darunter ist jedoch keine Neuimplementierung des Redis-Notifiers. Es ist eine dünne Brücke auf ein Feature, das Zaris bereits hatte: einen <strong>nativen, clusterweiten, replizierten Watch.</strong> Das ändert, was die Benachrichtigungen wert sind.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="einschalten--exakt-auf-redis-art">Einschalten — exakt auf Redis-Art<a href="https://clustron.io/de/blog/redis-keyspace-notifications/#einschalten--exakt-auf-redis-art" class="hash-link" aria-label="Direkter Link zur Einschalten — exakt auf Redis-Art" title="Direkter Link zur Einschalten — exakt auf Redis-Art" translate="no">​</a></h2>
<p>Am Aktivieren und Konsumieren des Features ist nichts Zaris-spezifisch. Richten Sie einen beliebigen Redis-Client auf das RESP-Frontend und verwenden Sie die Befehle, die Sie bereits kennen:</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>Von einem typisierten Client — hier StackExchange.Redis — sieht es genauso aus, ganz ohne Zaris-SDK:</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>Der Punkt, den es zu unterstreichen lohnt: <strong>keine Anwendungsänderung.</strong> Code, der gegen Redis-Keyspace-Benachrichtigungen geschrieben wurde, läuft unverändert gegen Zaris. Das ist das gesamte Versprechen des RESP-Frontends — Ihre Clients müssen nicht wissen, was hinter dem Socket steckt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-zwei-kanalfamilien">Die zwei Kanalfamilien<a href="https://clustron.io/de/blog/redis-keyspace-notifications/#die-zwei-kanalfamilien" class="hash-link" aria-label="Direkter Link zur Die zwei Kanalfamilien" title="Direkter Link zur Die zwei Kanalfamilien" translate="no">​</a></h2>
<p>Zaris veröffentlicht die zwei Standard-Redis-Kanalfamilien mit der Standard-Payload-Konvention — welche Sie abonnieren, hängt davon ab, ob Sie nach <em>dem Schlüssel</em> oder nach <em>dem Event</em> gehen:</p>
<table><thead><tr><th>Kanal</th><th>Veröffentlicht, wenn Flag …</th><th>Payload</th><th>Verwenden, wenn Sie sich interessieren für …</th></tr></thead><tbody><tr><td><code>__keyspace@0__:&lt;key&gt;</code></td><td><code>K</code> gesetzt ist</td><td>der Event-Name (<code>set</code> / <code>del</code> / <code>expired</code>)</td><td>einen <strong>bestimmten Schlüssel</strong> — „sag mir alles, was mit <code>session:abc</code> passiert"</td></tr><tr><td><code>__keyevent@0__:&lt;event&gt;</code></td><td><code>E</code> gesetzt ist</td><td>der Schlüssel, der sich geändert hat</td><td>eine <strong>Klasse von Events</strong> — „sag mir jeden Schlüssel, der gerade abgelaufen ist"</td></tr></tbody></table>
<p>Das <code>@0</code> ist der Datenbankindex und in Zaris immer <code>0</code> — Zaris teilt den Keyspace nicht in nummerierte Datenbanken auf, es gibt also einen einzigen, flachen Benachrichtigungs-Namensraum statt sechzehn. Die drei Event-Namen bilden die drei Dinge ab, die einem Schlüssel passieren können:</p>
<ul>
<li class=""><strong><code>set</code></strong> — der Schlüssel wurde geschrieben (ein <code>SET</code>, ein <code>HSET</code>, ein <code>LPUSH</code>, jede Mutation)</li>
<li class=""><strong><code>del</code></strong> — der Schlüssel wurde entfernt</li>
<li class=""><strong><code>expired</code></strong> — der Schlüssel wurde per Eviction entfernt, weil sein TTL abgelaufen ist</li>
</ul>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="was-darunter-tatsächlich-geschieht">Was darunter tatsächlich geschieht<a href="https://clustron.io/de/blog/redis-keyspace-notifications/#was-darunter-tats%C3%A4chlich-geschieht" class="hash-link" aria-label="Direkter Link zur Was darunter tatsächlich geschieht" title="Direkter Link zur Was darunter tatsächlich geschieht" translate="no">​</a></h2>
<p>Senden Sie <code>CONFIG SET notify-keyspace-events KEA</code>, schaltet Zaris keinen eigens gebauten Redis-Event-Emitter im Befehlspfad frei. Es registriert <strong>einen einzigen Prefix-Watch über den gesamten Keyspace</strong> auf dem eigenen Routing-Client des Nodes — dasselbe <code>Watch</code>-Primitiv, das der native .NET-Client für <a class="" href="https://clustron.io/de/blog/2026/03/30/watch-changes/">reaktive Lesevorgänge ohne Polling</a> bereitstellt. Ab da ist die einzige Logik in der RESP-Schicht eine Namenszuordnung: Ein nativer Watch-Event trifft ein, und die Brücke macht daraus den richtigen Redis-Kanalnamen und die richtige Payload.</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>Dieser <code>Publish</code>-Aufruf ist das native Pub/Sub — dieselbe Subscribe-/Deliver-Mechanik mit eigenem clusterweitem Fan-out, die der Store ohnehin betreibt. Das RESP-Frontend besitzt das <em>Vokabular</em> (Kanalnamen, Payload-Konvention); der Cluster besitzt die <em>Zustellung</em>. Dieses Muster halten wir im gesamten Produkt bewusst durch: <a class="" href="https://clustron.io/de/blog/redis-protocol-any-language/">Jedes Feature ist zuerst eine native Fähigkeit, und RESP ist eine dünne Haut darüber</a> — nie eine parallele Implementierung mit eigenen Bugs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-der-watch-ist-nativ-der-ganze-punkt-ist">Warum „der Watch ist nativ" der ganze Punkt ist<a href="https://clustron.io/de/blog/redis-keyspace-notifications/#warum-der-watch-ist-nativ-der-ganze-punkt-ist" class="hash-link" aria-label="Direkter Link zur Warum „der Watch ist nativ&quot; der ganze Punkt ist" title="Direkter Link zur Warum „der Watch ist nativ&quot; der ganze Punkt ist" translate="no">​</a></h2>
<p>Hier trennen sich die beiden Systeme, und das ist nicht kosmetisch.</p>
<p>Eine Redis-Keyspace-Benachrichtigung wird auf dem Node erzeugt, der den Befehl ausgeführt hat, und an Subscriber zugestellt, die mit <em>diesem</em> Node verbunden sind. Nichts an der Subscription wird mit dem Rest des Clusters geteilt, und nichts daran ist dauerhaft. Es ist Pub/Sub im strengen Sinne — waren Sie im richtigen Moment nicht auf dem richtigen Node am Lauschen, existierte das Event für Sie schlicht nicht.</p>
<p>Eine Zaris-Keyspace-Benachrichtigung fährt auf einem nativen Watch, und ein nativer Watch ist ein clusterweites Objekt:</p>
<ul>
<li class=""><strong>Die Subscription wird über die Mitglieder repliziert.</strong> <code>WatchSyncService</code> hält Watch-Subscriptions zwischen Nodes synchron — der Leader schiebt die aktuelle Menge an einen beitretenden Node, und ein hochkommender Node fragt den Leader, was aktiv ist. Die Subscription ist keine Tatsache, die nur ein Prozess kennt; sie ist Cluster-Zustand.</li>
<li class=""><strong>Sie wird bei Churn neu verortet.</strong> Wenn Partitionen wandern — Failover, Migration, ein Node verlässt den Cluster —, wandert der Watch mit den Daten, die er beobachtet, so wie der Schlüsselbesitz. Der Schreibvorgang erzeugt auch nachdem sich der Staub gelegt hat ein Event, weil der Watcher der Partition gefolgt ist.</li>
<li class=""><strong>Die Zustellung verteilt sich clusterweit.</strong> Weil der Publish über natives Pub/Sub läuft, erhält ein mit <em>irgendeinem</em> Node verbundener Client Events für Schreibvorgänge auf <em>irgendeinem</em> Node. Sie sind nicht stillschweigend nur auf die Aktivität eines Shards abonniert.</li>
</ul>
<p>Das praktische Ergebnis: Ein Subscriber, der <code>__keyevent@0__:expired</code> beobachtet, um eine Cache-Invalidierung zu steuern, funktioniert durch einen Node-Ausfall hindurch weiter. Im Redis Cluster hängt derselbe Subscriber an einem Node und sieht die Ablaufereignisse eines Nodes; die ehrliche Abhilfe dort ist, auf jedem Node eine Subscription zu öffnen und zu deduplizieren. Auf Zaris ist es eine Subscription, clustervollständig, und sie übersteht das Failover, das sie stillschweigend gekappt hätte.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-ehrlichen-grenzen">Die ehrlichen Grenzen<a href="https://clustron.io/de/blog/redis-keyspace-notifications/#die-ehrlichen-grenzen" class="hash-link" aria-label="Direkter Link zur Die ehrlichen Grenzen" title="Direkter Link zur Die ehrlichen Grenzen" translate="no">​</a></h2>
<p>Dies ist eine Brücke über Best-Effort-Pub/Sub, kein magisches Zuverlässigkeits-Upgrade. Drei Dinge sollten Sie auseinanderhalten:</p>
<p><strong>Die Zustellung ist weiterhin Best-Effort, kein garantiertes Log.</strong> Die Brücke veröffentlicht jedes Event Fire-and-Forget: Ein Zustellfehler bei einem Subscriber darf die Watch-Pipeline für alle anderen nie blockieren, daher wird eine Benachrichtigung an einen kurzzeitig getrennten Client verworfen, nicht gepuffert. Das entspricht dem eigenen Vertrag von Redis — Keyspace-Benachrichtigungen waren nie ein Exactly-once-Event-Log, und Sie sollten die von Zaris auch nicht als eines behandeln. Würde ein verpasstes Event Ihren Zustand beschädigen, sind Benachrichtigungen das <em>Aufwecken</em>, nicht die <em>Quelle der Wahrheit</em>: Lesen Sie beim Event den maßgeblichen Wert.</p>
<p><strong>Kein Backfill.</strong> Der Watch wird ohne initialen Snapshot scharf gestellt, Sie erhalten also Änderungen ab dem Moment, ab dem Benachrichtigungen aktiviert sind — nie einen Replay dessen, was der Keyspace bereits enthielt. Aktivieren Sie die Benachrichtigungen, <em>dann</em> denken Sie über Deltas nach.</p>
<p><strong>Die Flag-Granularität ist grob.</strong> Redis lässt Sie Event-<em>Klassen</em> mit einem dichten Flag-String auswählen (<code>g</code>, <code>$</code>, <code>l</code>, <code>s</code>, <code>h</code>, <code>z</code>, <code>x</code>, <code>e</code>, …). Zaris liest die zwei Flags, die entscheiden, <em>wohin</em> veröffentlicht wird — <code>K</code> für die Keyspace-Kanäle, <code>E</code> für die Keyevent-Kanäle (und <code>A</code> als die übliche Kurzform für „alle Klassen") — und überbrückt die drei nativen Event-Typen, sobald eines gesetzt ist. Es filtert nicht auf Subscriptions pro Klasse herunter. In der Praxis abonnieren Sie die Kanäle und Muster, die Sie interessieren, und lassen den Musterabgleich die Eingrenzung erledigen — so ist ohnehin der meiste Keyspace-Benachrichtigungs-Code geschrieben.</p>
<p>Brauchen Sie Benachrichtigungen für einen Teil des Keyspace statt für alles, setzen Sie <code>ZARIS_RESP_KEYSPACE_PREFIX</code> auf dem Node, und der zugrunde liegende Watch wird auf diesem Präfix statt auf dem gesamten Store scharf gestellt — ein billiger Weg, einen geschäftigen Keyspace davon abzuhalten, Events zu veröffentlichen, die niemand abonniert.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wenn-sie-ein-log-statt-einer-türklingel-wollen">Wenn Sie ein Log statt einer Türklingel wollen<a href="https://clustron.io/de/blog/redis-keyspace-notifications/#wenn-sie-ein-log-statt-einer-t%C3%BCrklingel-wollen" class="hash-link" aria-label="Direkter Link zur Wenn Sie ein Log statt einer Türklingel wollen" title="Direkter Link zur Wenn Sie ein Log statt einer Türklingel wollen" translate="no">​</a></h2>
<p>Keyspace-Benachrichtigungen sind eine Türklingel: Sie sagen Ihnen, dass <em>etwas geschehen ist</em>, genau jetzt, wem auch immer zuhört. Brauchen Sie dagegen ein <strong>wiederabspielbares, dauerhaftes Protokoll</strong> — jedes Event, in Reihenfolge, lesbar für einen Consumer, der spät dazukam oder abstürzte und zurückkehrte —, ist das keine Benachrichtigung, sondern ein Stream. Zaris hat dafür <a class="" href="https://clustron.io/de/blog/redis-streams-native/">native Redis Streams</a>: <code>XADD</code> zum Anhängen, <code>XREADGROUP</code> mit Consumer Groups für At-least-once-Verarbeitung mit Bestätigungen, blockierende Lesevorgänge für niedrige Latenz. Greifen Sie zu Benachrichtigungen, wenn ein verpasstes Event nur „nächstes Mal wieder nachsehen" bedeutet; greifen Sie zu Streams, wenn ein verpasstes Event ein Bug ist.</p>
<p>Verwendet für das, was sie ist — ein latenzarmes Aufwecken ohne Polling, das auf Zaris zufällig clustervollständig und Failover-dauerhaft ist —, gehört die Keyspace-Benachrichtigung zu den schönsten Dingen, die Sie mit einem einzigen <code>CONFIG SET</code> einschalten können. Der Unterschied zeigt sich erst an dem Tag, an dem ein Node verschwindet, also genau an dem Tag, an dem Sie die Grenzen der Ein-Node-Version auf die harte Tour entdeckt hätten.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Redis</category>
            <category>Verteilte Muster</category>
        </item>
        <item>
            <title><![CDATA[Kein Auth-Server: Offline verifizierbare Tokens, Rollen und Scopes in Zaris]]></title>
            <link>https://clustron.io/de/blog/token-auth-roles-scopes/</link>
            <guid>https://clustron.io/de/blog/token-auth-roles-scopes/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris sichert einen Cluster mit einem RS256-Signaturschlüssel und selbstenthaltenen JWTs. Jeder Manager stellt ein Token aus; jeder Node verifiziert es offline mit der öffentlichen Hälfte — kein zentraler Auth-Server, keine Datenbank, kein Roundtrip pro Operation. Hier sind das Token-Modell, der Rollen-/Scope-Katalog, die zwei Durchsetzungsebenen und die ehrlichen betrieblichen Grenzen.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Offline verifizierbare Tokens, Rollen und Scopes in 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>Die meisten Wege, einem verteilten Datenspeicher eine Authentifizierung vorzuschalten, enden damit, ein zweites verteiltes System zu seinem Betrieb hinzuzufügen: einen Auth-Dienst, den man aufrufen muss, eine Session-Datenbank, die repliziert werden muss, einen Token-Introspection-Endpunkt, den jeder Node auf dem Hot Path erreichen muss. Sie haben den Store gesichert und sich eine neue Verfügbarkeitsabhängigkeit und eine neue Latenzquelle eingekauft.</p>
<p>Zaris geht einen anderen Weg, und zwar einen bewusst langweiligen: <strong>ein Signaturschlüssel pro Cluster und Tokens, die jeder Node selbst verifizieren kann.</strong> Ein Token ist ein JWT, signiert mit dem privaten Schlüssel des Clusters; jeder Manager und jeder Node hält die öffentliche Hälfte und prüft die Signatur <strong>offline</strong> — kein Aufruf an einen Auth-Server, kein Quorum, kein Datenbank-Lookup. Security ist standardmäßig aus, und ein Cluster mit ausgeschalteter Security verhält sich exakt wie ein ungesicherter. Dieser Beitrag handelt davon, was sich einschaltet, wenn Sie sie aktivieren: die Form des Tokens, das Rollen-und-Scope-Modell, die beiden Ebenen, auf denen die Durchsetzung tatsächlich stattfindet, und — weil das Gegenteil vorzutäuschen der Weg zu einem falschen Sicherheitsgefühl ist — die betrieblichen Grenzen, die man leicht falsch macht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="das-token-ist-die-ganze-geschichte">Das Token ist die ganze Geschichte<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#das-token-ist-die-ganze-geschichte" class="hash-link" aria-label="Direkter Link zur Das Token ist die ganze Geschichte" title="Direkter Link zur Das Token ist die ganze Geschichte" translate="no">​</a></h2>
<p>Jedes Zaris-Token ist ein JWT. Es benennt ein <strong>Subject</strong> — einen Benutzer oder eine Anwendung — und trägt eine oder mehrere <strong><code>role @ scope</code>-Grants</strong>. Es wird mit dem privaten RS256-Schlüssel des Clusters signiert, der nur auf den Managern liegt; alles, was ein Token <em>prüfen</em> muss, hält nur den öffentlichen Schlüssel.</p>
<p>Diese eine Designentscheidung nimmt den Auth-Server aus dem Bild:</p>
<ul>
<li class=""><strong>Jeder Manager kann ein Token ausstellen.</strong> Es gibt keinen primären Auth-Node, keine Wahl, keinen gemeinsamen veränderlichen Session-Zustand zu koordinieren. Das Ausstellen ist eine lokale Signaturoperation.</li>
<li class=""><strong>Jeder Manager und jeder Node kann ein Token verifizieren</strong>, nur mit dem öffentlichen Schlüssel. Die Verifikation ist eine Signaturprüfung plus eine Ablauf- und Scope-Prüfung — reine CPU, kein Netzwerk. Das Token ist <strong>selbstenthaltend</strong>: Rolle und Scope sind <em>darin</em> eingebettet, ein Node, der eine Verbindung autorisiert, braucht also nichts außer dem öffentlichen Schlüssel. Es gibt auf dem Node keine Policy-Datenbank, die synchron gehalten werden müsste.</li>
</ul>
<p>Der praktische Gewinn zeigt sich auf dem Datenpfad. Ein Client legt sein Token <strong>einmal vor, wenn die Verbindung aufgebaut wird</strong> — das „Hello". Der Node verifiziert es, autorisiert die Verbindung für den Store und vertraut dieser Verbindung dann für ihre gesamte Lebensdauer. <strong>Tokens werden auf dem Per-Operation-Hot-Path nie erneut geprüft</strong>, das Einschalten von Security kostet Sie also pro <code>Get</code> oder <code>Set</code> nichts. Sie zahlen für die Authentifizierung einmal beim Verbindungsaufbau und für diese Verbindung nie wieder.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rollen-und-scopes">Rollen und Scopes<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#rollen-und-scopes" class="hash-link" aria-label="Direkter Link zur Rollen und Scopes" title="Direkter Link zur Rollen und Scopes" translate="no">​</a></h2>
<p>Berechtigungen sind ein kleiner vordefinierter Katalog von Rollen, die jeweils <em>auf einem Scope</em> vergeben werden, der festlegt, <strong>wo</strong> sie gelten.</p>
<table><thead><tr><th>Rolle</th><th>Scope-Ebene</th><th>Was sie gewährt</th></tr></thead><tbody><tr><td><code>ClusterAdmin</code></td><td>Cluster</td><td>alles — sämtliche Berechtigungen der Steuerungs- und Datenebene</td></tr><tr><td><code>StoreOperator</code></td><td>Store</td><td>Store starten/stoppen, Konfiguration bearbeiten, Nodes starten/stoppen, Diagnose, Metriken</td></tr><tr><td><code>Observer</code></td><td>Store</td><td>Diagnose und Metriken, nur lesend</td></tr><tr><td><code>DataWriter</code></td><td>Store</td><td>Daten lesen / schreiben / löschen / scannen</td></tr><tr><td><code>DataReader</code></td><td>Store</td><td>Daten lesen / scannen</td></tr></tbody></table>
<p>Ein <strong>Scope</strong> ist hierarchisch, und ein breiterer Scope deckt alles darunter ab:</p>
<ul>
<li class=""><code>zaris:cluster</code> — der gesamte Cluster, für clusterweite Administration.</li>
<li class=""><code>zaris:store:&lt;name&gt;</code> — ein einzelner Store, z. B. <code>zaris:store:orders</code>.</li>
<li class=""><em>root</em> (der leere Scope) — deckt alles ab; dem Bootstrap-Admin vorbehalten.</li>
</ul>
<p>Die Enthaltenseinsbeziehung ist <code>root ⊃ cluster ⊃ store</code>: Ein <code>DataWriter</code>, der auf <code>zaris:cluster</code> vergeben wurde, kann in jeden Store schreiben, einer auf <code>zaris:store:orders</code> nur dort. Aus diesen zwei Achsen setzen Sie Least Privilege zusammen: die kleinste Rolle wählen, die den Job erledigt, und sie an den engsten Scope binden, den die Identität braucht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="einschalten-ohne-sich-auszusperren">Einschalten, ohne sich auszusperren<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#einschalten-ohne-sich-auszusperren" class="hash-link" aria-label="Direkter Link zur Einschalten, ohne sich auszusperren" title="Direkter Link zur Einschalten, ohne sich auszusperren" translate="no">​</a></h2>
<p>Das eine, was ein Sicherheitssystem nie tun darf, ist, Sie aus dem eigenen Cluster auszusperren. Zaris löst das, indem es <strong>Initialisieren</strong> und <strong>Aktivieren</strong> zu einem einzigen Schritt macht: Die Durchsetzung schaltet sich erst ein, <em>sobald der erste Admin existiert</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>Der Admin ist eine <strong>echte Login-Identität</strong> — Benutzername und Passwort, wie ein Cloud-Root-Konto —, kein frei schwebendes Token, das Sie ewig hüten müssen. Danach melden sich Operatoren mit Zugangsdaten an und erhalten ein kurzlebiges Session-Token. Die Durchsetzung selbst ist ein <strong>Laufzeit-Schalter</strong>, pro Manager persistiert, der ohne Neustart oder Konfigurationsänderung umschaltet:</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>Beide wirken auf jeden Manager im Workspace, sodass der Cluster konsistent bleibt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="zwei-arten-von-identität-menschen-und-anwendungen">Zwei Arten von Identität: Menschen und Anwendungen<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#zwei-arten-von-identit%C3%A4t-menschen-und-anwendungen" class="hash-link" aria-label="Direkter Link zur Zwei Arten von Identität: Menschen und Anwendungen" title="Direkter Link zur Zwei Arten von Identität: Menschen und Anwendungen" translate="no">​</a></h2>
<p>Nach demselben Muster wie bei AWS IAM, Kubernetes und Vault <strong>gehört jedes Token zu einer registrierten Identität</strong> — Sie können kein Token für einen beliebigen Namen ausstellen. Es gibt genau zwei Arten, plus den einen Bootstrap-Admin.</p>
<p><strong>Benutzer sind Menschen.</strong> Sie werden mit einem Passwort angelegt (nur als gesalzener PBKDF2-Hash in <code>users.json</code> auf jedem Manager gespeichert — nie im Klartext, keine externe Datenbank) und <em>melden sich an</em>, um ein Session-Token zu erhalten:</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>Service-Accounts sind Anwendungen.</strong> Kein Passwort; das Anlegen eines solchen liefert ein selbstenthaltendes Token, das die App vorlegt und erneuert:</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> gibt es weiterhin, um ein Token auszustellen oder zu rotieren, aber das Subject <strong>muss bereits ein registrierter Benutzer oder Service-Account sein</strong>. Es gibt keine Ad-hoc-Tokens für nicht existierende Namen — diese Eigenschaft macht „wer darf diesen Store anfassen?" zu einer beantwortbaren Frage.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ein-token-verwenden">Ein Token verwenden<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#ein-token-verwenden" class="hash-link" aria-label="Direkter Link zur Ein Token verwenden" title="Direkter Link zur Ein Token verwenden" translate="no">​</a></h2>
<p>Auf der Datenebene fließt das Token direkt in die <a class="" href="https://clustron.io/de/blog/connection-strings/">Connection-String</a> ein — und Sie müssen nie ein Klartext-Secret hineinschreiben, weil die Indirektion über <code>env:</code> und <code>file:</code> erstklassig ist:</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>Aus .NET ist es dieselbe Zeichenfolge, oder im Code ein <code>TokenProvider</code>, wenn Sie Rotation wollen:</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>Fehlt das Token, ist es abgelaufen, ungültig oder nicht auf diesen Store gescoped, wird die Verbindung mit dem Status <em>Unauthorized</em> abgelehnt — Sie erfahren es beim Verbinden, nicht drei Operationen tief in einer Transaktion. Wenn Sie lieber kein Token von Hand ausstellen und einfügen möchten, kann <code>Connect-ZrStore</code> auch eine Benutzername-/Passwort-Zugangsdaten gegen eines beim Management Service tauschen und es im Hintergrund erneuern.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="erneuerung-die-nie-eine-verbindung-kappt">Erneuerung, die nie eine Verbindung kappt<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#erneuerung-die-nie-eine-verbindung-kappt" class="hash-link" aria-label="Direkter Link zur Erneuerung, die nie eine Verbindung kappt" title="Direkter Link zur Erneuerung, die nie eine Verbindung kappt" translate="no">​</a></h2>
<p>Hier zahlt sich „nur beim Verbinden verifiziert" doppelt aus. Weil ein Node das Token <strong>nur beim Öffnen der Verbindung</strong> prüft, wird eine <em>laufende</em> Verbindung nie unterbrochen, wenn ihr Token abläuft. Die Erneuerung muss nur sicherstellen, dass der Client beim nächsten Öffnen einer Verbindung ein <em>aktuell gültiges</em> Token hat — bei einem Reconnect, einem neuen Node, einem Node-Neustart.</p>
<p>Der Client liest sein Token daher bei jedem (Re-)Connect über einen <strong>Provider</strong>. Solange dieser Provider frische Tokens liefert, ist die Erneuerung völlig nahtlos:</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>Der Renew-Endpunkt (<code>POST /security/token/renew</code>) ist <strong>zustandslos</strong> — er verifiziert Ihr aktuelles Token offline gegen den gemeinsamen Schlüssel und liefert ein neues mit demselben Subject und denselben Rollen und frischem Ablauf zurück, sodass <em>jeder</em> Manager ihn bedienen kann und er Neustarts übersteht. Ein abgelaufenes Token kann nicht erneuert werden (erneuern Sie vor Ablauf — der eingebaute Provider tut das), und ein widerrufenes Token oder Subject wird abgelehnt. Die daraus folgende Empfehlung ist einfach: Bevorzugen Sie <strong>kurze Lebensdauern mit automatischer Erneuerung</strong> gegenüber einem langlebigen Token. Sie erhalten dieselbe Nahtlosigkeit bei weit geringerer Angriffsfläche.</p>
<p>Wollen Sie Client-Code gar nicht in der Schleife haben, zeigen Sie den Provider auf eine Umgebungsvariable oder eine Datei und lassen einen externen Rotator — einen Secret Manager, einen Sidecar, einen Cron, der neu ausstellt — sie außerhalb des Bandes aktualisieren:</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="der-ehrliche-teil-zwei-ebenen-bedeuten-zwei-schalter">Der ehrliche Teil: zwei Ebenen bedeuten zwei Schalter<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#der-ehrliche-teil-zwei-ebenen-bedeuten-zwei-schalter" class="hash-link" aria-label="Direkter Link zur Der ehrliche Teil: zwei Ebenen bedeuten zwei Schalter" title="Direkter Link zur Der ehrliche Teil: zwei Ebenen bedeuten zwei Schalter" translate="no">​</a></h2>
<p>Dieses Detail beißt die Leute, deshalb bekommt es einen eigenen Abschnitt. „Security ist an" ist nicht ein Fakt — es sind <strong>zwei</strong>, weil es zwei Durchsetzungspunkte gibt.</p>
<p>Die <strong>Steuerungsebene</strong> — der Management Service auf Port 7801, über den Sie Stores anlegen, Nodes starten und stoppen und Grants verwalten — setzt durch, sobald Sie Security initialisiert und aktiviert haben. Das ist der Schalter, den <code>Enable-ZrSecurity</code> umlegt.</p>
<p>Die <strong>Datenebene</strong> — die eigentlichen Zaris-Nodes, die Ihre Schlüssel bedienen — ist ein <em>eigener</em> Schalter. Ein Node setzt Tokens erst durch, wenn er den gemeinsamen öffentlichen Schlüssel in seiner Konfiguration hat, und den stellen Sie pro Store bereit:</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>Bis Sie das tun, laufen die Nodes des Stores mit einem <code>AllowAll</code>-Authenticator und akzeptieren jede Verbindung — mit oder ohne Token.</strong> Das ist Absicht: So können Sie zuerst die Steuerungsebene einschalten und die Durchsetzung auf der Datenebene Store für Store mit einem geplanten Neustart ausrollen, statt den ganzen Cluster auf einmal umzulegen. Es bedeutet aber, dass das <em>Aktivieren der Steuerungsebene allein Ihre Daten nicht sichert.</em> Die beiden passenden Fallen:</p>
<ul>
<li class=""><strong><code>Get-ZrSecurityStatus</code> meldet die Steuerungsebene.</strong> Seine Felder <code>Enabled</code> / <code>Initialized</code> / <code>Issuer</code> beschreiben die Durchsetzung auf der Management-Ebene. Ein <code>True</code> dort beweist <strong>nicht</strong>, dass die Nodes eines bestimmten Stores Tokens verifizieren — das ist die Aufgabe von <code>Enable-ZrStoreSecurity</code>, getrennt zu bestätigen. Akzeptiert ein „gesicherter" Store weiterhin Clients ohne Token, fehlt in der Node-Konfiguration der Schlüssel oder der Node wurde nicht neu gestartet.</li>
<li class=""><strong>Der Widerruf ist auf dem Datenpfad nicht sofort wirksam.</strong> Weil Daten-Tokens selbstenthaltend sind und nur beim Verbinden geprüft werden, wird <code>Revoke-ZrToken</code> von der Steuerungsebene und von der <em>Erneuerung</em> sofort beachtet (ein widerrufenes Subject kann nicht erneuern), eine bereits offene Datenverbindung funktioniert aber weiter, bis sie schließt. Der saubere Weg, eine Anwendung auf der Datenebene abzuschneiden, ist, Daten-Tokens mit einer bescheidenen <code>-LifetimeMinutes</code> auszustellen und das aktuelle ablaufen zu lassen oder ihre Verbindungen nach dem Widerruf neu zu starten. Brauchen Sie ein nahezu sofortiges Abschneiden, sind kurze Lebensdauern der Hebel — nicht die Erwartung, dass der Hot Path erneut prüft.</li>
</ul>
<p>Keines davon ist ein Bug; es sind die direkten Folgen von „offline, einmal, ohne Auth-Server verifizieren". Es ist nur der Teil, den Sie bewusst betreiben müssen. Der empfohlene Rollout ergibt sich genau daraus: Steuerungsebene initialisieren und aktivieren, mit <code>Get-ZrSecurityStatus</code> prüfen, Operator- und Anwendungs-Tokens ausstellen und deren Funktion bestätigen, dann <code>Enable-ZrStoreSecurity</code> pro Store mit geplantem Neustart, und schließlich Anwendungen auf kurzlebige Tokens mit automatischer Erneuerung umstellen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wo-es-im-stack-sitzt">Wo es im Stack sitzt<a href="https://clustron.io/de/blog/token-auth-roles-scopes/#wo-es-im-stack-sitzt" class="hash-link" aria-label="Direkter Link zur Wo es im Stack sitzt" title="Direkter Link zur Wo es im Stack sitzt" translate="no">​</a></h2>
<p>Token-Authentifizierung beantwortet, <em>wer diesen Store anfassen darf und was er darf</em>. Sie fügt sich mit den anderen beiden Hälften eines abgesicherten Zaris zusammen und ersetzt keine davon:</p>
<ul>
<li class=""><strong><a class="" href="https://clustron.io/de/blog/tls-ca-modes/">Pluggable TLS Trust</a></strong> verschlüsselt die Leitung und authentifiziert die <em>Endpunkte</em> — es ist der Transport <code>zariss://</code> unter dem Token, orthogonal dazu, wer das Token hält.</li>
<li class=""><strong><a class="" href="https://clustron.io/de/blog/helm-secure-e2e/">Die sichere Helm-Anleitung</a></strong> bringt beides auf Kubernetes Ende-zu-Ende zusammen, wo die Details von Connection-String und Zertifikat tatsächlich geübt werden.</li>
</ul>
<p>Es lohnt sich, die Form der Kernidee zu behalten. Ein RS256-Schlüssel, JWTs, die ihre eigene Autorität einbetten, eine Verifikation, die reine Offline-CPU ist, und ein Token, das pro Verbindung genau einmal geprüft wird. Das kauft Ihnen Authentifizierung und rollenbasierte Autorisierung über einen ganzen Cluster hinweg, ohne einen Auth-Server, einen Session-Store oder einen einzigen zusätzlichen Netzwerk-Hop auf dem Pfad, auf dem Ihre Anwendung tatsächlich läuft. Der Preis ist die Disziplin, der dieser Beitrag seine zweite Hälfte gewidmet hat: Denken Sie daran, dass die Datenebene ein eigener Schalter ist, und halten Sie Ihre Tokens kurzlebig. Tun Sie das, wird „gesichert" zu einer Eigenschaft, die Sie präzise benennen können, statt auf sie zu hoffen.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sicherheit</category>
            <category>Betrieb</category>
            <category>Verteilte Systeme</category>
        </item>
        <item>
            <title><![CDATA[Replikationsfaktor ist keine Ausfallsicherheit: Einen Zaris-Cluster auf Kubernetes für Dauerhaftigkeit dimensionieren]]></title>
            <link>https://clustron.io/de/blog/kubernetes-durability-sizing/</link>
            <guid>https://clustron.io/de/blog/kubernetes-durability-sizing/</guid>
            <pubDate>Fri, 02 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Auf Kubernetes hält Zaris Daten im Arbeitsspeicher und macht sie durch Replikation dauerhaft, nicht durch Festplatte. Doch replicationFactor=2 allein übersteht keinen Pod-Verlust — die Pod-Anzahl muss ein Vielfaches von RF sein, sonst läuft eine übrig gebliebene Partition als Einzelkopie und verliert ihre Keys, wenn dieser Pod stirbt. Hier die Platzierungsmathematik, warum eine ungerade Pod-Anzahl zubeißt und wie das Chart Sie absichert.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Einen Zaris-Cluster auf Kubernetes für Dauerhaftigkeit dimensionieren" 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>Beim Deployment eines replizierten Stores auf Kubernetes liegt ein verlockender Gedankengang nahe: <em>Ich habe <code>replicationFactor: 2</code> gesetzt, also hat jede Partition ein Backup, also überstehe ich den Verlust eines Nodes.</em> Das fühlt sich wasserdicht an. Für eine überraschend häufige Clustergröße ist es dennoch falsch — und zwar auf die stille Art, die erst auffällt, wenn tatsächlich zum ersten Mal ein Pod stirbt.</p>
<p>Zaris auf Kubernetes speichert Daten <strong>im Arbeitsspeicher</strong> und macht sie durch <strong>Replikation, nicht durch Festplatte</strong> dauerhaft: Das Node-Datenvolume ist ein <code>emptyDir</code>, und ein neu eingeplanter Pod baut sich aus der Kopie auf einem anderen Pod wieder auf. Dieses Design ist solide — verlässt sich aber vollständig darauf, dass es <em>tatsächlich</em> eine Kopie auf einem anderen Pod gibt. Ob diese Kopie existiert, entscheidet nicht <code>replicationFactor</code> allein. Es entscheidet die Arithmetik zwischen Pod-Anzahl und Replikationsfaktor, und wenn Sie diese Arithmetik falsch ansetzen, läuft der Store einwandfrei, bedient jeden Lese- und Schreibzugriff und hält stillschweigend eine Partition, die nirgendwohin replizieren kann.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-deployment-form">Die Deployment-Form<a href="https://clustron.io/de/blog/kubernetes-durability-sizing/#die-deployment-form" class="hash-link" aria-label="Direkter Link zur Die Deployment-Form" title="Direkter Link zur Die Deployment-Form" translate="no">​</a></h2>
<p>Das Zaris-Helm-Chart deployt die Node-Ebene als <strong>StatefulSet</strong>, eine Node-Instanz pro Pod (<code>zaris-0</code>, <code>zaris-1</code>, …), jede ein In-Memory-Shard-Owner. Ein Headless-Service gibt jedem Pod eine stabile DNS-Identität, die Readiness ist an die Cluster-Konvergenz gekoppelt, und ein PodDisruptionBudget plus ein Termination-Fenster mit sanftem Drain schützen die Menge bei freiwilligen Unterbrechungen.</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>Die eigenen Hinweise des Charts sind unmissverständlich, woher die Dauerhaftigkeit kommt:</p>
<blockquote>
<p><strong>Node data is emptyDir.</strong> In-memory durability is via replication (RF), not disk; a rescheduled node rebuilds from replicas.</p>
</blockquote>
<p>Ein Node-Pod kann also den ganzen Tag beendet und neu eingeplant werden, ohne Datenverlust — <em>vorausgesetzt</em>, seine Partitionen haben ein lebendes Replikat auf einem anderen Pod, aus dem er sich wieder aufbauen kann. Diese Voraussetzung ist das ganze Spiel.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wie-partitionen-auf-pods-landen">Wie Partitionen auf Pods landen<a href="https://clustron.io/de/blog/kubernetes-durability-sizing/#wie-partitionen-auf-pods-landen" class="hash-link" aria-label="Direkter Link zur Wie Partitionen auf Pods landen" title="Direkter Link zur Wie Partitionen auf Pods landen" translate="no">​</a></h2>
<p>Eine Partition in Zaris wird von <strong>RF verschiedenen Node-Instanzen</strong> gehalten — ein aktiver Owner plus <code>RF−1</code> Replikate —, und die Replikate werden stets auf <em>verschiedenen</em> Pods platziert (ein Replikat auf demselben Pod wie sein Aktiver wäre überhaupt kein Schutz). Die Runtime platziert Partitionen gierig:</p>
<blockquote>
<p>The runtime fills a partition to its full RF before forming the next one.</p>
</blockquote>
<p>Dieser eine Satz ist der Schlüssel zur gesamten Dimensionierung. Instanzen werden RF auf einmal verbraucht. Die Anzahl <strong>vollständig replizierter</strong> Partitionen ist daher:</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>und alle übrig gebliebenen Instanzen — <code>instances mod RF</code> davon — bilden eine <strong>Einzelkopie</strong>-Partition ohne Peer, auf den repliziert werden könnte. Sie ist eine echte, bedienende Partition. Sie hat nur kein Backup.</p>
<!-- -->
<p>Mit vier Pods bei RF 2 erhalten Sie zwei Partitionen mit je zwei Kopien auf verschiedenen Pods — verlieren Sie einen beliebigen Pod, läuft die überlebende Kopie weiter, während die Partition sich auf den neu eingeplanten Pod re-repliziert. Mit drei Pods bei RF 2 bilden Pod 0 und 1 die Partition A mit vollem RF, Pod 2 bleibt aber übrig und bildet die Partition B mit nur einer Kopie. Fügen Sie einen vierten Pod hinzu, wird er zum Replikat von B. Bis dahin ist B eine Belastung.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-dimensionierungsregel-als-tabelle">Die Dimensionierungsregel als Tabelle<a href="https://clustron.io/de/blog/kubernetes-durability-sizing/#die-dimensionierungsregel-als-tabelle" class="hash-link" aria-label="Direkter Link zur Die Dimensionierungsregel als Tabelle" title="Direkter Link zur Die Dimensionierungsregel als Tabelle" translate="no">​</a></h2>
<p>Da jede Partition RF verschiedene Pods braucht und Kubernetes eine Instanz pro Pod betreibt, lautet die Regel schlicht:</p>
<blockquote>
<p><strong>Betreiben Sie eine Pod-Anzahl, die ein Vielfaches von RF ist.</strong> Bei RF 2 verwenden Sie eine gerade Anzahl Pods.</p>
</blockquote>
<table><thead><tr><th>Pods (je 1 Instanz)</th><th>RF</th><th>Vollständig replizierte Partitionen</th><th>Übersteht den Verlust eines Pods?</th></tr></thead><tbody><tr><td>2</td><td>2</td><td>1</td><td>Ja</td></tr><tr><td><strong>3</strong></td><td>2</td><td>1 (+ <strong>1 Einzelkopie</strong>)</td><td><strong>Nein</strong> — der Verlust des Einzelkopie-Pods verliert diese Partition</td></tr><tr><td>4</td><td>2</td><td>2</td><td>Ja</td></tr><tr><td>6</td><td>2</td><td>3</td><td>Ja</td></tr></tbody></table>
<p>Das Muster lässt sich verallgemeinern: <code>N</code> Partitionen bei RF 2 brauchen <code>2N</code> Pods; bei RF 3 brauchen sie <code>3N</code>. Hochskalieren heißt, Pods jeweils um einen vollen RF hinzuzufügen, wenn jede neue Partition von Geburt an dauerhaft sein soll.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="warum-drei-pods-mit-rf-2-die-falle-sind">Warum drei Pods mit RF 2 die Falle sind<a href="https://clustron.io/de/blog/kubernetes-durability-sizing/#warum-drei-pods-mit-rf-2-die-falle-sind" class="hash-link" aria-label="Direkter Link zur Warum drei Pods mit RF 2 die Falle sind" title="Direkter Link zur Warum drei Pods mit RF 2 die Falle sind" translate="no">​</a></h2>
<p>Drei ist die Zahl, die zubeißt, denn drei Pods <em>sehen aus</em> wie ein gesunder, hochverfügbarer Cluster. Er besteht die Readiness. Er bedient Datenverkehr. <code>kubectl get pods</code> zeigt <code>zaris-0..2</code> alle als <code>1/1 Ready</code>. Nichts ist offensichtlich falsch.</p>
<p>Was falsch ist, bleibt unsichtbar, bis der Einzelkopie-Pod — der, der die einzige Kopie der Partition B hält — der Pod ist, der stirbt. In diesem Moment sind die Keys von B endgültig weg, weil es nie eine zweite Kopie gab, aus der man sich hätte aufbauen können. Das ist kein Bug; es ist dieselbe Einschränkung wie beim Betrieb auf einer einzelnen Maschine, nur auf eine Partition statt auf den gesamten Store begrenzt. Die Map wird nur neu erzeugt, wenn <em>alle</em> Kopien einer Partition verloren sind, und bei einer Einzelkopie-Partition ist „alle Kopien" ein Pod.</p>
<p>Das wurde gemessen, nicht angenommen. Auf einem <code>kind</code>-Cluster:</p>
<ul>
<li class="">Bei <strong><code>replicas=4</code></strong> (2 Partitionen × RF 2) verlor das Beenden des Active-Owner-Pods einer Partition <strong>0 von 200 Keys</strong> — das Replikat übernahm, und RF stellte sich auf dem neu eingeplanten Pod selbst wieder her.</li>
<li class="">Bei <strong><code>replicas=3</code></strong> verlor das Beenden des Einzelkopie-Pods die Keys dieser Partition vollständig.</li>
</ul>
<p>Derselbe Store, dieselbe RF-Einstellung, dasselbe Chaos. Die einzige Variable war, ob die Pod-Anzahl ein Vielfaches von RF war.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="die-leitplanken-die-das-chart-ihnen-bietet">Die Leitplanken, die das Chart Ihnen bietet<a href="https://clustron.io/de/blog/kubernetes-durability-sizing/#die-leitplanken-die-das-chart-ihnen-bietet" class="hash-link" aria-label="Direkter Link zur Die Leitplanken, die das Chart Ihnen bietet" title="Direkter Link zur Die Leitplanken, die das Chart Ihnen bietet" translate="no">​</a></h2>
<p>Das Chart überlässt das nicht der Folklore. Installieren Sie einen Cluster, dessen Pod-Anzahl kein Vielfaches von RF ist, gibt <code>helm install</code> in seinen Hinweisen eine Dauerhaftigkeitswarnung aus:</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>Über die Warnung hinaus sind mehrere Defaults so gesetzt, dass ein <em>korrekt dimensionierter</em> Cluster sich bei Unterbrechungen gut verhält:</p>
<ul>
<li class=""><strong>Durch Konvergenz gesteuerte Readiness.</strong> <code>/readyz</code> besteht erst, wenn Partition-Map und Zuständigkeit live sind, sodass ein <code>Ready</code>-Pod ein routbarer ist — Kubernetes sendet keinen Datenverkehr an einen Node, der die Zuständigkeit noch nicht übernommen hat.</li>
<li class=""><strong>PodDisruptionBudget.</strong> Standardmäßig wird höchstens ein Node gleichzeitig freiwillig unterbrochen, sodass ein Drain oder Rolling Upgrade nicht beide Kopien einer Partition auf einmal ausschalten kann.</li>
<li class=""><strong>Sanfter Drain bei Termination.</strong> Ein Termination-Grace-Fenster lässt einen ausscheidenden Node sauber übergeben, statt mitten im Betrieb abgerissen zu werden.</li>
<li class=""><strong>Hot Scale-out.</strong> Bei aktivierter Kubernetes-Discovery (Default) wird der Roster aus Pod-Ordinalzahlen und Headless-DNS synthetisiert, sodass das Hinzufügen von Pods ein schlichtes <code>kubectl scale statefulset</code> ist — neue Pods treten bei, und die Partition-Map wächst, ohne die bestehenden neu zu starten. Fügen Sie sie jeweils in einem Vielfachen von RF hinzu, und jede neue Partition ist von Geburt an dauerhaft.</li>
</ul>
<p>Keine davon hilft jedoch der Einzelkopie-Partition. Ein PDB, das alle bis auf einen Pod oben hält, schützt nicht, wenn einer dieser Pods die <em>einzige</em> Heimat einer Partition ist. Die Leitplanken setzen voraus, dass Sie den Cluster korrekt dimensioniert haben; einen falsch dimensionierten Cluster retten sie nicht.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="praktische-regeln">Praktische Regeln<a href="https://clustron.io/de/blog/kubernetes-durability-sizing/#praktische-regeln" class="hash-link" aria-label="Direkter Link zur Praktische Regeln" title="Direkter Link zur Praktische Regeln" translate="no">​</a></h2>
<p>Das ganze Thema lässt sich auf ein paar Gewohnheiten verdichten:</p>
<ol>
<li class=""><strong>Wählen Sie zuerst RF, dann machen Sie die Pod-Anzahl zu einem Vielfachen davon.</strong> RF 2 → gerade Pod-Anzahlen; RF 3 → Vielfache von drei. Das Standard-Chart liefert 4 Pods bei RF 2, gerade weil dies die kleinste ausfallsichere Form ist.</li>
<li class=""><strong>Behandeln Sie eine ungerade Pod-Anzahl als reine Evaluierung.</strong> Drei Pods bei RF 2 sind für eine Demo auf <code>kind</code> in Ordnung; als Dauerhaftigkeitshaltung in der Produktion taugen sie nicht, und das Chart sagt Ihnen das bei der Installation.</li>
<li class=""><strong>Skalieren Sie in RF-Schritten.</strong> Einen Pod zu einem dauerhaften Cluster hinzuzufügen, erzeugt vorübergehend eine Einzelkopie-Partition, bis der nächste Pod eintrifft. Wenn Sie für Dauerhaftigkeit wachsen, wachsen Sie um ein ganzes Replikations-Set auf einmal.</li>
<li class=""><strong>Denken Sie daran, dass das Volume ein emptyDir ist.</strong> Es gibt keine Festplatte als Rückfallebene. Replikation <em>ist</em> Ihre Dauerhaftigkeit, und genau deshalb muss die Kopie tatsächlich auf einem anderen Pod existieren.</li>
</ol>
<p>Der Replikationsfaktor sagt, wie viele Kopien eine Partition <em>haben will</em>. Die Pod-Anzahl entscheidet, ob sie sie <em>haben kann</em>. Bringen Sie diese beiden Zahlen in Einklang, und ein Pod-Verlust ist ein Nicht-Ereignis — das Replikat bedient, der neu eingeplante Pod baut sich wieder auf, und kein Key geht verloren. Lassen Sie sie auseinanderdriften, haben Sie einen Cluster gebaut, der hochverfügbar ist — bis genau zu dem Moment, auf den es ankommt.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Kubernetes</category>
            <category>Betrieb</category>
        </item>
    </channel>
</rss>