<?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/es/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>es</language>
        <item>
            <title><![CDATA[Muchas claves, pocos viajes de ida y vuelta: cómo Zaris hace fan-out de una operación masiva]]></title>
            <link>https://clustron.io/es/blog/bulk-and-batch-fan-out/</link>
            <guid>https://clustron.io/es/blog/bulk-and-batch-fan-out/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un GetMany sobre mil claves no debería costar mil viajes de ida y vuelta de red. Zaris agrupa las claves por su nodo propietario, envía una solicitud por propietario en paralelo, y cose las respuestas de vuelta en el orden del llamador. Aquí está la ruta de fan-out exacta, por qué cada ítem lleva un índice, y por qué una operación masiva deliberadamente no es una transacción.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Haciendo fan-out de una operación masiva a sus nodos propietarios" 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>Pídele a un almacén distribuido una clave y el costo lo domina un solo número: el viaje de ida y vuelta al nodo que la posee. La búsqueda en sí es nanosegundos; la red es microsegundos. Ahora pide mil claves. Si iteras y llamas a <code>Get</code> mil veces, pagas ese viaje de ida y vuelta mil veces, en serie, y el almacén gasta casi todo el tiempo de reloj sentado ocioso esperando paquetes. Las claves podrían estar repartidas sobre cuatro nodos que podrían haber respondido en paralelo —pero un bucle ingenuo nunca les da la oportunidad.</p>
<p>Zaris tiene tres APIs masivas —<code>GetManyAsync</code>, <code>PutManyAsync</code>, <code>DeleteManyAsync</code>— y un <code>ExecuteBatchAsync</code> mixto por debajo de ellas, y toda su razón de existir es convertir "mil claves" en "una solicitud por nodo propietario, emitidas en paralelo". Este artículo es la ruta de fan-out de principio a fin: cómo el cliente agrupa claves por propietario, por qué las cuatro APIs colapsan sobre un único motor, cómo un campo <code>Index</code> mantiene las respuestas en orden a través de dos capas de concurrencia, y por qué —a diferencia de una <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacción multiclave</a>— una operación masiva es por ítem y el éxito parcial es normal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-modelo-de-costos-viajes-de-ida-y-vuelta-no-operaciones">El modelo de costos: viajes de ida y vuelta, no operaciones<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#el-modelo-de-costos-viajes-de-ida-y-vuelta-no-operaciones" class="hash-link" aria-label="Enlace directo al El modelo de costos: viajes de ida y vuelta, no operaciones" title="Enlace directo al El modelo de costos: viajes de ida y vuelta, no operaciones" translate="no">​</a></h2>
<p>Lo que una API masiva combate no es la CPU. Un <code>GET</code> de clave única en un nodo propietario es una búsqueda en diccionario —el <a class="" href="https://clustron.io/es/blog/latency-floor/">piso de latencia</a> es un microsegundo de trabajo real dentro de diez de plomería. La parte cara es la plomería: la solicitud tiene que cruzar la red al propietario y la respuesta tiene que volver. Llama a ese tiempo de ida y vuelta <code>R</code>, típicamente decenas a cientos de microsegundos en una red de datacenter, órdenes de magnitud más que la búsqueda.</p>
<p>Un bucle en serie de <code>N</code> claves cuesta aproximadamente <code>N × R</code>, porque cada llamada espera a que el paquete de la anterior vuelva antes de enviar la siguiente. Esa es la trampa. El arreglo no es una búsqueda más rápida —es dejar de pagar <code>R</code> por clave. Si las <code>N</code> claves viven en <code>M</code> nodos propietarios, el piso es <code>R</code> pagado <code>M</code> veces <em>concurrentemente</em>, es decir, alrededor de un <code>R</code> en total sin importar cuántas claves sostenga cada nodo. Para mil claves en cuatro nodos esa es la diferencia entre mil viajes de ida y vuelta en serie y, en efecto, uno. Todo el diseño de abajo existe para colapsar <code>N × R</code> a <code>~R</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-motor-todo-es-un-lote-de-ítems">Un motor: todo es un lote de ítems<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#un-motor-todo-es-un-lote-de-%C3%ADtems" class="hash-link" aria-label="Enlace directo al Un motor: todo es un lote de ítems" title="Enlace directo al Un motor: todo es un lote de ítems" translate="no">​</a></h2>
<p>Lo primero sorprendente en el cliente de clúster es que <code>GetManyAsync</code>, <code>PutManyAsync</code> y <code>DeleteManyAsync</code> no son tres implementaciones separadas. Cada una es una fina capa (shim) que convierte sus argumentos en una lista de <code>KvBatchItem</code>s y los entrega al único método que hace el trabajo real, <code>ExecuteBatchAsync</code>. Aquí está <code>GetManyAsync</code> haciendo exactamente eso:</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> y <code>DeleteManyAsync</code> hacen lo mismo con <code>KvBatchOp.Put</code> y <code>KvBatchOp.Delete</code>. Así que la distinción que los ejemplos trazan entre <em>Bulk</em> (un tipo de operación a la vez) y <em>Batch</em> (una mezcla de PUT / GET / DELETE en una llamada) es una conveniencia de superficie: los ayudantes masivos son solo un lote donde cada ítem resulta compartir una operación. Hay exactamente un motor de fan-out, y es agnóstico a la operación.</p>
<p>Nota <code>Index = i</code>. Cada ítem se estampa con su posición en la lista del llamador <em>antes</em> de que nada se agrupe, reordene o envíe. Ese sello es lo que sobrevive a los dos saltos concurrentes por venir y permite que la respuesta final se devuelva en orden. Agárrate a él.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-fan-out-agrupar-por-propietario-una-solicitud-cada-uno">El fan-out: agrupar por propietario, una solicitud cada uno<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#el-fan-out-agrupar-por-propietario-una-solicitud-cada-uno" class="hash-link" aria-label="Enlace directo al El fan-out: agrupar por propietario, una solicitud cada uno" title="Enlace directo al El fan-out: agrupar por propietario, una solicitud cada uno" translate="no">​</a></h2>
<p>Aquí está el corazón de todo el artículo —el despacho de lotes del cliente de clúster, esencialmente completo:</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>Léelo como cuatro movimientos:</p>
<ol>
<li class="">
<p><strong>Resuelve cada clave a su propietario, localmente.</strong> <code>SegmentMapper.ComputeSegmentId</code> es la misma función de ubicación que usa la ruta de clave única —CRC16 sobre el hash-tag, módulo el conteo de segmentos— y <code>GetClientForSegment</code> mapea ese segmento a la conexión ya abierta de su nodo propietario. Sin proxy, sin coordinador: el cliente computa la propiedad él mismo, exactamente como se describe en <a class="" href="https://clustron.io/es/blog/client-partition-aware-routing/">un salto al propietario</a>. La diferencia es que aquí lo hace para cada clave del conjunto a la vez.</p>
</li>
<li class="">
<p><strong>Agrupa.</strong> Las claves poseídas por el mismo nodo se amontonan en la lista de ese nodo. Mil claves repartidas sobre cuatro propietarios se convierten en cuatro listas. Aquí es donde <code>N</code> operaciones se convierten en <code>M</code> solicitudes.</p>
</li>
<li class="">
<p><strong>Despacha concurrentemente.</strong> <code>buckets.Select(...)</code> construye una llamada <code>ExecuteBatchAsync</code> por propietario y <code>Task.WhenAll</code> deja que todas vuelen a la vez. Cuatro nodos responden en paralelo; el tiempo de reloj es la respuesta del nodo más lento, no la suma.</p>
</li>
<li class="">
<p><strong>Recombina en orden.</strong> Cada propietario devuelve su porción de resultados; <code>SelectMany</code> los aplana y <code>OrderBy(r =&gt; r.Index)</code> restaura el orden original del llamador. El llamador recibe de vuelta una lista cuya posición <em>i</em> es la respuesta para la clave <em>i</em>, aunque las claves viajaron a nodos distintos y volvieron en el orden que sea que la red entregó.</p>
</li>
</ol>
<!-- -->
<p>Esa es la afirmación completa de amortización hecha concreta: el número de viajes de ida y vuelta es igual al número de <em>nodos propietarios distintos</em> en el conjunto de claves, no al número de claves.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dentro-de-un-propietario-concurrente-de-nuevo-ordenado-de-nuevo">Dentro de un propietario: concurrente de nuevo, ordenado de nuevo<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#dentro-de-un-propietario-concurrente-de-nuevo-ordenado-de-nuevo" class="hash-link" aria-label="Enlace directo al Dentro de un propietario: concurrente de nuevo, ordenado de nuevo" title="Enlace directo al Dentro de un propietario: concurrente de nuevo, ordenado de nuevo" translate="no">​</a></h2>
<p>Cada sub-lote por propietario aterriza en el <code>BatchCommandHandler</code> de ese nodo. Tampoco ejecuta los ítems uno tras otro —los ejecuta todos a la vez y luego reordena:</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> es un pequeño switch sobre la operación que despacha cada ítem al almacén —<code>GetRawAsync</code>, <code>PutRawAsync</code> o <code>DeleteRawAsync</code>— así que un lote mixto de lecturas y escrituras se maneja ítem por ítem dentro de la misma solicitud. (Para los routers en proceso y de un solo nodo la misma forma aparece como un <code>Parallel.ForEachAsync</code> sobre las claves, acotado por <code>MaxDegreeOfParallelism</code> —una opción <code>MaxParallelism</code> o <code>Environment.ProcessorCount</code> por defecto— que es por qué las <a class="" href="https://clustron.io/es/blog/reads-scale-with-cores/">lecturas escalan con los núcleos</a>: un sub-lote gordo mantiene ocupado cada núcleo en lugar de uno.)</p>
<p>Así que el sello <code>Index</code> se gana el sustento <em>dos veces</em>. La concurrencia desordena el orden en el nodo, así que el handler ordena por <code>Index</code> antes de responder. El fan-out desordena el orden en el cliente, así que el cliente ordena por <code>Index</code> después de fusionar. El llamador nunca ve ninguno de los dos desórdenes. Un pequeño entero, llevado en cada ítem desde el momento en que se construye la solicitud, es el mecanismo entero que hace seguro exponer "lánzalo todo al clúster en paralelo" como una lista ordenada.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="una-operación-masiva-no-es-una-transacción">Una operación masiva no es una transacción<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#una-operaci%C3%B3n-masiva-no-es-una-transacci%C3%B3n" class="hash-link" aria-label="Enlace directo al Una operación masiva no es una transacción" title="Enlace directo al Una operación masiva no es una transacción" translate="no">​</a></h2>
<p>Esta es la distinción que la gente más a menudo entiende mal, así que vale la pena enunciarla con nitidez. Cada ítem en un lote lleva su <em>propio</em> resultado. El tipo de resultado por ítem registra el éxito, el estado y —para las operaciones que los tienen— la revisión, la versión y el TTL de esa única clave:</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>No hay punto de confirmación compartido. Un <code>GetMany</code> sobre cien claves donde treinta no existen devuelve setenta resultados <code>Success</code> y treinta resultados <code>NotFound</code>, intercalados por índice —no un error, no una lista vacía. Un <code>PutMany</code> donde el segmento de una clave está a mitad de migración devuelve ese único ítem con estado <code>Moved</code> y el resto <code>Success</code>. El éxito parcial es la forma normal y esperada de la respuesta, y se espera que el llamador inspeccione cada ítem.</p>
<p>Eso es exactamente lo opuesto de una <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacción multiclave</a>, que es todo-o-nada por construcción: un commit de dos fases coordinado por el cliente sobre versiones optimistas donde o cada clave se mueve o ninguna lo hace. Las dos características parecen similares desde fuera —ambas toman muchas claves en una llamada— y están construidas para necesidades opuestas. Recurre a un lote cuando las claves son independientes y quieres throughput; recurre a una transacción cuando un invariante abarca las claves y un resultado a medio aplicar lo corrompería. Zaris las mantiene como APIs separadas precisamente para que la semántica nunca sea ambigua: un lote nunca revierte silenciosamente, y una transacción nunca confirma silenciosamente a medias.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sobrevivir-a-una-propiedad-que-se-mueve-a-mitad-de-lote">Sobrevivir a una propiedad que se mueve a mitad de lote<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#sobrevivir-a-una-propiedad-que-se-mueve-a-mitad-de-lote" class="hash-link" aria-label="Enlace directo al Sobrevivir a una propiedad que se mueve a mitad de lote" title="Enlace directo al Sobrevivir a una propiedad que se mueve a mitad de lote" translate="no">​</a></h2>
<p>Una operación masiva es un blanco más amplio que un solo <code>GET</code> para el problema de que la propiedad cambie por debajo. Entre el momento en que el cliente agrupa una clave al nodo A y el momento en que el nodo A maneja el sub-lote, un <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">cut-over de partición</a> puede haber movido ese segmento al nodo B. El nodo A responderá esos ítems con estado <code>Moved</code> en lugar de adivinar.</p>
<p>El cliente de clúster envuelve todo el fan-out en un reintento acotado exactamente para esto:</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>En un reintento la tabla de enrutamiento usualmente se ha refrescado, así que el re-agrupamiento envía las claves movidas a su nuevo propietario. El reintento es deliberadamente superficial —dos intentos, un backoff corto— porque el objetivo es aguantar una tabla de enrutamiento que está momentáneamente obsoleta, no empapelar un nodo que realmente está caído. Esta es la misma disciplina descrita en <a class="" href="https://clustron.io/es/blog/retries-under-ownership-churn/">reintentos bajo churn de propiedad</a>, aplicada a todo un conjunto de claves a la vez: el reintento re-ejecuta el <em>agrupamiento</em>, no solo el envío, así que cada intento refleja la propiedad más reciente en lugar de reproducir un plan obsoleto.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-bug-real-porque-la-forma-de-cable-tiene-dos-variantes">Un bug real, porque la forma de cable tiene dos variantes<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#un-bug-real-porque-la-forma-de-cable-tiene-dos-variantes" class="hash-link" aria-label="Enlace directo al Un bug real, porque la forma de cable tiene dos variantes" title="Enlace directo al Un bug real, porque la forma de cable tiene dos variantes" translate="no">​</a></h2>
<p>Vale la pena mostrar un lugar donde esta ruta se rompió, porque dice algo sobre las operaciones masivas específicamente. El valor de un resultado <code>GET</code> puede llegar por el cable en dos formas —envuelto en un sobre <code>ZarisEntry</code>, o como un valor serializado en bruto— dependiendo de cómo se almacenó (ver <a class="" href="https://clustron.io/es/blog/what-crosses-the-wire/">lo que cruza el cable</a>). El decodificador original de GET masivo asumía solo la forma envuelta y lanzaba una excepción con la en bruto.</p>
<p>Para un solo <code>GET</code> eso es un fallo de una clave. Para un <code>GetMany</code> era catastrófico de forma sutil: <em>cada</em> clave en el lote fallaba, porque la única codificación no manejada lanzaba antes de que se decodificara cualquier resultado. Una corrida de certificación lo marcó como "fallos de get masivo", y el arreglo fue enrutar la decodificación a través de un ayudante compartido de codificación-dual que maneja ambas formas:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Decode through the shared helper (handles both the ZarisEntry-wrapper and</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// the raw-value forms the server can emit). The old inline code assumed only</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// the wrapper and threw, failing every key of a bulk GET.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> obj </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> KvBatchItemResultExtensions</span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">GetValue</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">T</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La lección que generaliza: en una ruta masiva, un defecto en la maquinaria por ítem <em>compartida</em> se multiplica por el tamaño del lote, y un error que es una molestia para una clave es una interrupción para mil. El modelo de resultado por ítem es lo que contiene el radio de explosión —una vez que la decodificación es correcta, un valor malformado falla un ítem y deja los otros 999 intactos.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-compra-el-fan-out">Qué compra el fan-out<a href="https://clustron.io/es/blog/bulk-and-batch-fan-out/#qu%C3%A9-compra-el-fan-out" class="hash-link" aria-label="Enlace directo al Qué compra el fan-out" title="Enlace directo al Qué compra el fan-out" translate="no">​</a></h2>
<p>Junta las piezas y la forma de la victoria es clara. Una llamada masiva estampa cada ítem con su índice, resuelve el propietario de cada clave con la misma aritmética de ubicación que usa la ruta de clave única, agrupa las claves para que <code>N</code> operaciones se conviertan en una solicitud por nodo propietario, dispara esas solicitudes concurrentemente, y reensambla las respuestas en el orden del llamador —sobreviviendo a una tabla de enrutamiento que cambia en pleno vuelo y reportando el destino de cada clave independientemente. El conteo de viajes de ida y vuelta cae del número de claves al número de nodos, que es el único número que alguna vez iba a importar.</p>
<p>Nada de ello requirió una nueva ruta de almacenamiento o un nuevo registro de cable. Las APIs masivas son una capa de <em>planificación</em> sobre las primitivas que Zaris ya tiene: la <a class="" href="https://clustron.io/es/blog/client-partition-aware-routing/">ubicación consciente de particiones</a> que le dice al cliente quién posee qué, el <a class="" href="https://clustron.io/es/blog/what-crosses-the-wire/">contrato por registro</a> que cada resultado de ítem reutiliza, y la <a class="" href="https://clustron.io/es/blog/reads-scale-with-cores/">ejecución núcleo-paralela</a> que mastica un sub-lote una vez que aterriza. La parte difícil de ir rápido en muchas claves nunca fueron las claves —era negarse a pagar por la red una clave a la vez.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Rendimiento</category>
        </item>
        <item>
            <title><![CDATA[Un salto al propietario: cómo el cliente de Zaris enruta una clave]]></title>
            <link>https://clustron.io/es/blog/client-partition-aware-routing/</link>
            <guid>https://clustron.io/es/blog/client-partition-aware-routing/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un cliente de Zaris no envía tu GET a un balanceador de carga o a un proxy que lo reenvía. Computa el propietario de la clave él mismo —CRC16 sobre el hash-tag, módulo 16384, slot a segmento, segmento al nodo propietario— y abre el socket directo a ese nodo. Aquí está la ruta de enrutamiento exacta, la única función de ubicación que el cliente y el servidor comparten, y las tres formas en que el cliente mantiene su mapa fresco cuando la propiedad se mueve por debajo.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Cómo el cliente de Zaris enruta una clave a su propietario" 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>Hay dos formas honestas de construir un cliente de caché distribuida. La primera es elegir cualquier nodo, enviarle la solicitud, y dejar que el clúster la reenvíe internamente a quien posea la clave —simple en el cliente, pero cada solicitud que aterriza en el nodo equivocado paga un salto de red adicional dentro del clúster. La segunda es hacer al <em>cliente</em> lo bastante listo como para saber qué nodo posee la clave antes de enviar nada, para que el primer socket que toque sea el que realmente puede responder. Zaris toma la segunda vía. Un <code>GET</code> sale de tu proceso ya dirigido al nodo que sostiene la clave —sin proxy, sin salto de reenvío, sin coordinador en el medio.</p>
<p>Este artículo es la vista interna de cómo funciona eso: la función que convierte una clave en un propietario, la tabla de búsqueda que el cliente mantiene en memoria, y —la parte que es fácil de equivocar— cómo el cliente se mantiene correcto cuando la propiedad se mueve mientras tu tráfico está en vuelo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-afirmación-enunciada-con-precisión">La afirmación, enunciada con precisión<a href="https://clustron.io/es/blog/client-partition-aware-routing/#la-afirmaci%C3%B3n-enunciada-con-precisi%C3%B3n" class="hash-link" aria-label="Enlace directo al La afirmación, enunciada con precisión" title="Enlace directo al La afirmación, enunciada con precisión" translate="no">​</a></h2>
<p>Cuando llamas a <code>client.GetAsync("user:42")</code>, el cliente hace tres cosas antes de que ningún byte toque la red:</p>
<ol>
<li class=""><strong>Hace hash de la clave a un segmento.</strong> Una función pura mapea la cadena de la clave a uno de los segmentos del clúster.</li>
<li class=""><strong>Busca el propietario del segmento.</strong> Un arreglo en memoria mapea ese segmento directamente a la única conexión que alcanza su nodo propietario activo.</li>
<li class=""><strong>Envía directo-al-propietario.</strong> La solicitud va por esa conexión y ninguna otra.</li>
</ol>
<p>Ningún paso consulta un router central. El paso 1 es aritmética, los pasos 2 y 3 son un índice de arreglo y una escritura de socket. El cliente sostiene una conexión por nodo y una pequeña tabla de enrutamiento, y ese es todo el aparato. La ingeniería interesante no es la ruta feliz —es mantener esa tabla honesta.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-1--la-clave-se-convierte-en-un-segmento">Paso 1 — la clave se convierte en un segmento<a href="https://clustron.io/es/blog/client-partition-aware-routing/#paso-1--la-clave-se-convierte-en-un-segmento" class="hash-link" aria-label="Enlace directo al Paso 1 — la clave se convierte en un segmento" title="Enlace directo al Paso 1 — la clave se convierte en un segmento" translate="no">​</a></h2>
<p>Zaris hace hash de las claves exactamente del modo en que lo hace un cliente de Redis Cluster, a propósito. La función de ubicación vive en <code>SegmentMapper</code>, y su comentario de documentación es contundente sobre por qué se ve como se ve:</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>Esa última línea es la que más importa. No es "el cliente computa la ubicación de una forma y el servidor la valida de otra". Es el <em>mismo código</em>, compilado en el ensamblado del cliente y el ensamblado del servidor, produciendo el mismo segmento para la misma clave cada vez. Si el cliente y el servidor discreparan sobre dónde vive una clave, cada escritura sería un lanzamiento de moneda entre aterrizar en el propietario y aterrizar en un nodo que tiene que rechazarla. Compartir la función hace esa clase de bug irrepresentable.</p>
<p>La computación en sí son dos movimientos:</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>Primero la clave se reduce a un <strong>slot</strong> en el rango fijo <code>[0, 16384)</code> usando XMODEM CRC16 —el mismo polinomio CRC16-CCITT (<code>0x1021</code>, init <code>0x0000</code>) que usa Redis. Luego los 16384 slots se mapean proporcionalmente sobre cuantos segmentos tenga realmente el clúster:</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>Dos slots pueden caer en el mismo segmento; un segmento es un rango contiguo de slots. Esa alineación es lo que permite a Zaris responder <code>CLUSTER SLOTS</code> correctamente para un cliente de Redis Cluster mientras sigue enrutando por su propio modelo de segmentos internamente —las dos vistas son la misma vista a distintas resoluciones.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="hash-tags-decidir-qué-se-co-ubica">Hash tags: decidir qué se co-ubica<a href="https://clustron.io/es/blog/client-partition-aware-routing/#hash-tags-decidir-qu%C3%A9-se-co-ubica" class="hash-link" aria-label="Enlace directo al Hash tags: decidir qué se co-ubica" title="Enlace directo al Hash tags: decidir qué se co-ubica" translate="no">​</a></h3>
<p>Hay una escotilla de escape deliberada en el hashing de claves. Si una clave contiene un grupo <code>{...}</code> con al menos un carácter dentro de las llaves, solo los bytes dentro de las llaves se hashean:</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>Así es como fuerzas claves relacionadas al mismo segmento. <code>{u1}:profile</code> y <code>{u1}:sessions</code> ambas hashean sobre <code>u1</code>, así que comparten un segmento y por tanto un propietario —que es exactamente lo que necesitas para una <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacción multiclave</a> o cualquier operación que tenga que tocar varias claves atómicamente en un nodo. Las claves sin un tag se reparten por todo el espacio de claves. El cliente y el servidor coinciden en esto también, porque es el mismo <code>HashTagRange</code> en ambos lados.</p>
<p>Un detalle más que vale la pena notar para la ruta caliente: el bucle CRC16 tiene una ruta rápida todo-ASCII que hashea los caracteres como bytes sin asignación, y solo recurre a una codificación UTF-8 para claves no-ASCII. Enrutar cada operación significa que esto corre en cada operación, así que se escribió para no asignar en el caso común.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-2--el-segmento-se-convierte-en-una-conexión">Paso 2 — el segmento se convierte en una conexión<a href="https://clustron.io/es/blog/client-partition-aware-routing/#paso-2--el-segmento-se-convierte-en-una-conexi%C3%B3n" class="hash-link" aria-label="Enlace directo al Paso 2 — el segmento se convierte en una conexión" title="Enlace directo al Paso 2 — el segmento se convierte en una conexión" translate="no">​</a></h2>
<p>Ahora el cliente tiene un número de segmento. Necesita el nodo que actualmente lo posee. El cliente mantiene una <code>RoutingTable</code> inmutable cuyos campos son toda la historia:</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>La elección de diseño clave es <code>SegmentMap</code>. Es un arreglo, indexado por segmento, cuyas entradas están <em>ya resueltas</em> a la conexión del propietario de ese segmento. Enrutar una clave no es un recorrido de grafo de segmento a partición a nodo a conexión en el momento de la solicitud —esa resolución se hace de antemano cuando se construye la tabla, así que el costo por solicitud es un único índice de arreglo:</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> sostiene exactamente un <code>ZarisRemoteClient</code> —una conexión— por nodo. El cliente no abre un socket fresco por solicitud o por clave; multiplexa todo el tráfico de un nodo sobre la única conexión de ese nodo, y <code>SegmentMap</code> simplemente apunta muchos segmentos al mismo objeto de conexión (la igualdad por referencia los deduplica cuando el cliente necesita el conjunto distinto de propietarios activos, por ejemplo para hacer fan-out de un <a class="" href="https://clustron.io/es/blog/scanning-a-moving-keyspace/">escaneo</a>). Así que el costo de memoria de la consciencia de particiones es una conexión por nodo más un par de arreglos dimensionados al conteo de segmentos —no crece con el número de claves o solicitudes.</p>
<!-- -->
<p>El beneficio es que no hay nodo en el clúster cuyo trabajo sea recibir solicitudes mal enrutadas y reenviarlas. Cada solicitud bien enrutada la sirve el primer nodo que alcanza. Eso elimina todo un nivel del presupuesto de latencia y, igual de importante, de la superficie de fallo —no puedes perder una solicitud en un salto de reenvío que no existe. (La pieza compañera sobre <a class="" href="https://clustron.io/es/blog/latency-floor/">el piso de latencia</a> recorre lo que <em>sí</em> queda en el presupuesto una vez que el salto de proxy se ha ido.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-3--el-mapa-se-mueve-por-debajo">Paso 3 — el mapa se mueve por debajo<a href="https://clustron.io/es/blog/client-partition-aware-routing/#paso-3--el-mapa-se-mueve-por-debajo" class="hash-link" aria-label="Enlace directo al Paso 3 — el mapa se mueve por debajo" title="Enlace directo al Paso 3 — el mapa se mueve por debajo" translate="no">​</a></h2>
<p>Aquí está la parte difícil. La tabla de enrutamiento es una caché de un hecho —"el segmento S es propiedad del nodo N"— que el clúster puede cambiar en cualquier momento. Un <a class="" href="https://clustron.io/es/blog/anatomy-of-a-failover/">propietario puede fallar</a> y una réplica es promovida; una <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">partición puede migrarse</a> para hacer crecer o drenar un nodo. En el instante en que eso ocurre, el <code>SegmentMap[S]</code> del cliente apunta a una conexión que ya no es el propietario. Un cliente que confiara en su tabla ciegamente enviaría escrituras a un nodo que correctamente las rechazará.</p>
<p>Zaris mantiene la tabla honesta de tres formas, en orden de preferencia.</p>
<p><strong>Push — el servidor le dice al cliente.</strong> Cuando la propiedad cambia, el clúster emite un <code>RuntimeOwnershipChangedEvent</code>, y el cliente tiene un handler suscrito a él:</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> no reconstruye toda la tabla. Clona el arreglo <code>SegmentMap</code>, re-apunta solo los segmentos cuya partición realmente cambió de propietario, e intercambia la tabla inmutable con una única asignación de referencia. Los lectores nunca ven una tabla a medio actualizar —o leen la vieja o la nueva, nunca una mezcla rota. Esto es enrutamiento copy-on-write: el caso común (nada cambió) no cuesta nada, y un cambio toca solo los segmentos afectados.</p>
<p><strong>Poll — un respaldo lento.</strong> Un temporizador refresca el mapa del clúster cada diez segundos sin importar nada, así que incluso si un evento push se pierde (una reconexión, una notificación dejada caer), la vista del cliente se auto-sana dentro de un intervalo:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">readonly</span><span class="token plain"> </span><span class="token class-name">TimeSpan</span><span class="token plain"> _pollInterval </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> TimeSpan</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromSeconds</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p><strong>React — la propia solicitud corrige el mapa.</strong> La última línea de defensa es la solicitud que enruta mal. Si el cliente envía a un nodo que ya no es el propietario, el nodo no lo reenvía silenciosamente —devuelve un estado que le dice al cliente que su mapa está obsoleto, y el ayudante de reintento refresca e intenta de nuevo:</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>Cuando una operación reintentable vuelve <code>Moved</code>, <code>OwnershipChanging</code> o <code>ReplicaWriteRejected</code>, el cliente llama a su gancho de refresco y reintenta —un bucle acotado de unos pocos intentos con un backoff corto, no un giro sin fin. Hay una lección sutil horneada en el código actual aquí: una versión anterior refrescaba el mapa en <em>cada</em> intento de reintento, lo que bajo churn intenso de nodos convertía una sola operación fallida en docenas de refrescos todos serializados detrás de un lock. El arreglo fue refrescar solo cuando el estado realmente dice "tu mapa está mal", no meramente porque un intento falló. El tratamiento más profundo de esa disciplina de reintentos es su propio artículo —<a class="" href="https://clustron.io/es/blog/retries-under-ownership-churn/">reintentos bajo churn de propiedad</a>— pero la versión de una línea es: <em>un mapa obsoleto es una condición auto-correctiva, y la corrección es barata precisamente porque es rara y dirigida.</em></p>
<p>Los tres mecanismos se superponen: el push hace el caso común instantáneo, el poll hace que un push perdido sea eventualmente consistente, y el reintento reactivo hace invisible cualquier obsolescencia residual para tu llamada —solo toma un viaje de ida y vuelta extra en el momento en que la propiedad se movió, y es correcto desde la siguiente llamada en adelante.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-hacerlo-en-el-cliente-en-absoluto">¿Por qué hacerlo en el cliente en absoluto?<a href="https://clustron.io/es/blog/client-partition-aware-routing/#por-qu%C3%A9-hacerlo-en-el-cliente-en-absoluto" class="hash-link" aria-label="Enlace directo al ¿Por qué hacerlo en el cliente en absoluto?" title="Enlace directo al ¿Por qué hacerlo en el cliente en absoluto?" translate="no">​</a></h2>
<p>El enrutamiento consciente de particiones del lado del cliente cuesta algo. El cliente tiene que llevar un modelo del clúster, mantenerlo fresco, y enviar la misma lógica de ubicación que usa el servidor. Un diseño basado en proxy empuja todo eso al clúster y deja que el cliente siga siendo tonto. Entonces, ¿por qué pagar?</p>
<p>Porque la alternativa paga en cada solicitud individual, para siempre. Un proxy o un diseño de "envía a cualquier lado, lo reenviaremos" añade un salto a la ruta caliente de cada operación —un salto que es pura sobrecarga, presente incluso cuando nada ha fallado y nada se está moviendo. Zaris paga el costo del enrutamiento <em>una vez, por adelantado, y solo de nuevo cuando la propiedad realmente cambia</em>— lo que, en un clúster saludable, es casi nunca. El estado estable es un índice de arreglo y una escritura de socket al nodo correcto. Esa es la razón entera por la que <a class="" href="https://clustron.io/es/blog/reads-scale-with-cores/">las lecturas escalan con tus núcleos</a> en lugar de embotellarse en un nivel intermedio: no hay nivel intermedio donde embotellarse.</p>
<p>Y compone con todo lo demás. La misma resolución <code>GetClientForKey</code> es por la que un <a class="" href="https://clustron.io/es/blog/redis-keyspace-notifications/">key-watch</a>, un incremento de contador, un <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">compare-and-swap</a> y una operación de estructura de datos de clave única enrutan todos, así que todos heredan la entrega directa-al-propietario y el mismo mapa auto-sanante. El cliente no es una tubería tonta con enrutamiento atornillado; el enrutamiento es la cosa que el cliente es.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="los-compromisos-honestos">Los compromisos honestos<a href="https://clustron.io/es/blog/client-partition-aware-routing/#los-compromisos-honestos" class="hash-link" aria-label="Enlace directo al Los compromisos honestos" title="Enlace directo al Los compromisos honestos" translate="no">​</a></h2>
<p>El enrutamiento directo-al-propietario no está libre de bordes, y vale la pena nombrarlos.</p>
<ul>
<li class=""><strong>El cliente debe alcanzar cada nodo.</strong> Como no hay proxy, el cliente abre una conexión a cada nodo al que enruta. En una red plana eso es un no-problema; a través de una frontera —digamos un cliente fuera de Kubernetes— esos nodos tienen que ser direccionables individualmente. Ese es exactamente el problema que el <a class="" href="https://clustron.io/es/blog/kubernetes-external-clients/">direccionamiento de clientes externos</a> existe para resolver, y es una consecuencia directa de que no haya una única puerta de entrada.</li>
<li class=""><strong>Un breve retraso del mapa es posible, no una pérdida.</strong> En la ventana entre que la propiedad se mueve y que el cliente se entera, una solicitud puede enrutar al propietario viejo. Se rechaza y reintenta, no se mal-aplica —la ruta reactiva de arriba garantiza que el reintento aterrice en el propietario real. Pagas un viaje de ida y vuelta, no una respuesta errónea.</li>
<li class=""><strong>La función de ubicación es un contrato de compatibilidad.</strong> Como el cliente y el servidor comparten <code>SegmentMapper</code>, no puedes cambiar cómo las claves hashean a segmentos sin que ambos lados coincidan —que es precisamente por qué el hashing se alineó al modelo de slots estable de Redis en lugar de a un esquema interno que podría derivar.</li>
</ul>
<p>Enunciado sin rodeos: el cliente de Zaris sabe dónde viven tus claves, mantiene ese conocimiento actual sin que lo pienses, y gasta un salto de red —el que realmente hace el trabajo— en cada operación. La astucia no está en la ruta rápida, que es deliberadamente aburrida. Está en los tres mecanismos silenciosos que mantienen correcta la ruta rápida mientras el clúster se reconfigura por debajo.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>.NET</category>
            <category>Rendimiento</category>
        </item>
        <item>
            <title><![CDATA[Un propietario que reinicia no tiene derecho a su propia vacuidad]]></title>
            <link>https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/</link>
            <guid>https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/</guid>
            <pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nodo que poseía una partición muere y vuelve con un almacén vacío. El instinto peligroso es confiar en esa vacuidad. Aquí está cómo Zaris reconstruye un propietario reiniciado a partir de sus réplicas vivas —un flag certified pegajoso que le niega a un propietario fresco cualquier autoridad para estar vacío, una extracción de sincronización inversa, una compuerta de servicio que se niega a responder hasta que la cabeza del log de operaciones local alcanza la de la fuente, y un merge no destructivo que no pisoteará datos que una réplica acumuló mientras el propietario no estaba.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Reconstruir un propietario reiniciado a partir de sus réplicas" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJBbiBvd25lciB0aGF0IHJlc3RhcnRzIGhhcyBubyByaWdodCB0byBpdHMgb3duIGVtcHRpbmVzczogcmVidWlsZGluZyBmcm9tIHJlcGxpY2FzIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+UkVDT1ZFUlk8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+QSByZXN0YXJ0IGhhcyBubyByaWdodDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjE4MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij50byBpdHMgb3duIGVtcHRpbmVzczwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPlVuY2VydGlmaWVkICYjODU5NDsgcHVsbCBmcm9tIGEgbGl2ZSByZXBsaWNhICYjODU5NDsgc2VydmUgb25seSB3aGVuIGNhdWdodCB1cCAmIzg1OTQ7IGNlcnRpZnk8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwLDI4NikiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjIyIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjU4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNSIvPg0KICAgIDx0ZXh0IHg9Ijg1IiB5PSI0NyIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnJlc3RhcnRlZCBvd25lcjwvdGV4dD4NCiAgICA8dGV4dCB4PSI4NSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5lbXB0eSwgdW5jZXJ0aWZpZWQ8L3RleHQ+DQogICAgPHJlY3QgeD0iODcwIiB5PSIyMiIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI1OCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSI5NDUiIHk9IjQ3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bGl2ZSByZXBsaWNhPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijk0NSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5ob2xkcyB0aGUgZGF0YTwvdGV4dD4NCiAgICA8cGF0aCBkPSJNODY4IDQyIEwxNzIgNDIiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiIG1hcmtlci1lbmQ9InVybCgjbm9uZSkiLz4NCiAgICA8Y2lyY2xlIGN4PSIzNDAiIGN5PSI0MiIgcj0iNyIgZmlsbD0iIzJiZDZhMCIvPg0KICAgIDxjaXJjbGUgY3g9IjUyMCIgY3k9IjQyIiByPSI3IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iNzAwIiBjeT0iNDIiIHI9IjciIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8dGV4dCB4PSIzNDAiIHk9IjIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+c2VydmUtZ2F0ZSBob2xkczwvdGV4dD4NCiAgICA8dGV4dCB4PSI1MjAiIHk9IjIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+cmV2ZXJzZS1zeW5jIHB1bGw8L3RleHQ+DQogICAgPHRleHQgeD0iNzAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmhlYWQgJiM4ODA1OyBzb3VyY2UgaGVhZDwvdGV4dD4NCiAgICA8dGV4dCB4PSI1MjAiIHk9IjExMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPmVtcHRpbmVzcyBpcyBuZXZlciBhdXRob3JpdHkgJiM4MjEyOyBjZXJ0aWZ5IG9ubHkgb24gZnVsbC1zeW5jIHByb29mPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Un <a class="" href="https://clustron.io/es/blog/anatomy-of-a-failover/">failover</a> cubre el momento en que un propietario muere: el clúster elige un superviviente y sigue sirviendo. Una <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">migración planificada</a> cubre mover la partición de un propietario sano a propósito. Este artículo trata del tercer acto, el que corre después de que las sirenas se apagan —el proceso propietario original reinicia, se reúne al clúster, y quiere su partición de vuelta. Su almacén de nada-en-disco, todo-en-memoria volvió <strong>vacío</strong>. Los datos de los que es responsable siguen vivos, pero están vivos en las réplicas que siguieron sirviendo mientras estaba caído.</p>
<p>Todo el problema de corrección está contenido en una frase: <em>al propietario reiniciado no se le puede permitir confiar en su propia vacuidad.</em> Un almacén vacío se ve exactamente como un almacén que alguien limpió deliberadamente. Si el nodo asume "soy el propietario y no tengo datos, por lo tanto esta partición está vacía", servirá alegremente <code>NotFound</code> para millones de claves vivas y, peor, dejará que su estado vacío fluya de vuelta a las réplicas y las elimine. Este artículo recorre las cinco políticas pequeñas y puras que Zaris usa para asegurar que eso nunca ocurra.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-la-vacuidad-es-indecidible-desde-dentro">Por qué la vacuidad es indecidible desde dentro<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#por-qu%C3%A9-la-vacuidad-es-indecidible-desde-dentro" class="hash-link" aria-label="Enlace directo al Por qué la vacuidad es indecidible desde dentro" title="Enlace directo al Por qué la vacuidad es indecidible desde dentro" translate="no">​</a></h2>
<p>Empieza con la trampa, porque cada regla de abajo está moldeada por ella. Un nodo que arranca sosteniendo cero registros para un segmento que posee está en uno de dos estados, y <strong>no puede saber cuál solo con información local</strong>:</p>
<ol>
<li class="">Está vacío porque acaba de reiniciar y nunca extrajo sus datos de vuelta. <em>Debe</em> extraer.</li>
<li class="">Está vacío porque autoritativamente limpió o expiró esas claves, y una réplica que estuvo brevemente caída todavía sostiene las copias obsoletas. Si extrae, <em>resucita datos eliminados</em>.</li>
</ol>
<p>Estos dos son localmente indistinguibles —el almacén está vacío de cualquier modo. Una versión anterior de Zaris intentó resolver esto con una señal de anomalía <code>EmptyActive</code> y se eliminó precisamente porque la señal es una conjetura. No puedes preguntar "¿estoy vacío por una buena razón?" y obtener una respuesta confiable del nodo vacío.</p>
<p>Así que Zaris no le pregunta nada a la vacuidad. Enruta la decisión a través de un hecho distinto: un flag pegajoso a nivel de partición llamado <strong>certified</strong> (certificado), que significa exactamente "mi sincronización de arranque se completó y mi almacén es ahora autoritativo". Un nodo lo establece solo después de haber <em>demostrado</em> la posesión. Un propietario recién reiniciado está, por definición, <strong>sin certificar</strong>— y un propietario sin certificar no tiene autoridad para estar vacío. Debe extraer de quien sea que sostenga los datos. Un propietario certificado es autoritativo y se deja estrictamente en paz, así que una limpieza legítima nunca se resucita.</p>
<p>Ese único reencuadre —de <em>vacuidad</em> (indecidible) a <em>certificación</em> (un hecho demostrado y pegajoso)— es lo que hace segura el resto de la recuperación.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-1--el-propietario-reiniciado-extrae-en-lugar-de-asumir">Paso 1 — el propietario reiniciado extrae, en lugar de asumir<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#paso-1--el-propietario-reiniciado-extrae-en-lugar-de-asumir" class="hash-link" aria-label="Enlace directo al Paso 1 — el propietario reiniciado extrae, en lugar de asumir" title="Enlace directo al Paso 1 — el propietario reiniciado extrae, en lugar de asumir" translate="no">​</a></h2>
<p>Cuando el nodo se reúne, el mapa todavía lo asigna como propietario de su partición vieja. El disparador de sincronización inversa decide si cada segmento poseído debería ser <em>extraído de una réplica viva</em> en lugar de tratarse como localmente autoritativo:</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>Tres cosas sobre este diminuto predicado son portantes, y cada una es un bug que alcanzó una corrida de certificación antes de que la regla se escribiera.</p>
<p><strong>Ignora <code>isMapPreferredPrimary</code>.</strong> Ese parámetro todavía se pasa —como contexto-de-decisión documentado— pero la decisión deliberadamente no lo lee. Una versión anterior solo disparaba la sincronización inversa cuando el nodo era <em>también</em> el primario preferido del mapa. Un nodo que es el activo en tiempo de ejecución pero <em>no</em> el primario del mapa —el estado exacto en el que aterriza un propietario re-ubicado o promovido-tras-recuperación— tenía sus segmentos poseídos omitidos por completo. Nunca solicitaba sus datos y quedaba varado vacío para siempre mientras una réplica viva sostenía cada registro (una corrida de certificación: activo sosteniendo <code>0</code> vs réplica sosteniendo <code>4600</code> en quince segmentos, nunca convergió a lo largo de ocho minutos). La propiedad de la extracción de datos no puede estar condicionada a ser el primario <em>preferido</em>; tiene que dispararse para cualquier activo sin certificar.</p>
<p><strong>La señal es <code>!partitionCertified</code>, no la vacuidad.</strong> Este es el arreglo de la indecidibilidad de la sección anterior, en código. Un activo sin certificar nunca ha demostrado posesión, así que extrae; uno certificado es autoritativo y se omite.</p>
<p><strong><code>liveNonSelfReplicaExists</code> es una guarda, no una cortesía.</strong> Si la réplica configurada está <em>caída</em>, no hay nada <em>de lo que</em> sincronizar. Impulsar la recuperación contra una fuente muerta gira para siempre (<code>RECOVERY STUCK source=self synced=0/128</code>). Así que cuando no existe una réplica viva, el activo es la única copia superviviente —<em>es</em> la autoridad, y certifica de inmediato en lugar de esperar a una fuente que nunca responderá. La extracción en sí está bloqueada por revisión (revision-fenced) y acotada por marca de agua (watermark-bounded), lo que la hace un no-op inofensivo cuando la réplica resulta no sostener nada.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-2--no-sirvas-una-clave-que-todavía-no-has-alcanzado">Paso 2 — no sirvas una clave que todavía no has alcanzado<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#paso-2--no-sirvas-una-clave-que-todav%C3%ADa-no-has-alcanzado" class="hash-link" aria-label="Enlace directo al Paso 2 — no sirvas una clave que todavía no has alcanzado" title="Enlace directo al Paso 2 — no sirvas una clave que todavía no has alcanzado" translate="no">​</a></h2>
<p>Extraer no es instantáneo, y la ventana mientras corre es donde las lecturas confirmadas se convierten silenciosamente en <code>NotFound</code>. Una compuerta ingenua es un único booleano, <code>IsSynced</code>, que significa "una pasada de sincronización terminó". Ese booleano no es suficiente, y tres fallos de certificación separados lo probaron: un activo que reclamaba promovió 34 claves corto; un traspaso perdió una clave confirmada; un activo que se reunía sirvió <code>NotFound</code> para 57 claves durante el churn. En cada caso una pasada de sincronización <em>había</em> terminado —pero la fuente (el activo temporal que siguió sirviendo) había desde entonces aceptado más escrituras que el nodo en recuperación no había extraído. <code>IsSynced</code> era verdadero y el nodo seguía atrasado.</p>
<p>El arreglo es condicionar sobre una <em>posición</em>, no un flag. La revisión del log de operaciones es un número replicado y comparable entre nodos: el activo la asigna y la envía, así que una réplica al día converge a la misma cabeza. La compuerta de servicio compara cabezas:</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> es el nuevo estado que importa: una pasada de sincronización se completó, pero la cabeza de la fuente (capturada en esa sincronización) va por delante de la nuestra, así que la fuente aceptó escrituras que no hemos extraído. Servir ahora devolvería <code>NotFound</code> para exactamente esas claves. El nodo se mantiene hasta que su cabeza local alcanza la marca de agua. Nota la degradación deliberada: una marca de agua registrada de <code>0</code> significa "sin información de marca de agua", y la compuerta recae en el comportamiento solo-<code>IsSynced</code> en lugar de bloquear para siempre sobre datos faltantes. La regla nunca bloquea sobre la ausencia de un hecho —solo sobre un hecho que dice <em>vas atrasado</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-3--extrae-pero-no-pisotees">Paso 3 — extrae, pero no pisotees<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#paso-3--extrae-pero-no-pisotees" class="hash-link" aria-label="Enlace directo al Paso 3 — extrae, pero no pisotees" title="Enlace directo al Paso 3 — extrae, pero no pisotees" translate="no">​</a></h2>
<p>Ahora la dirección peligrosa: aplicar la instantánea que la réplica envía de vuelta. El instinto es limpiar el almacén local y copiar la instantánea al por mayor. Ese instinto es correcto exactamente la mitad de las veces, y catastróficamente erróneo la otra mitad. La pregunta decisiva es <em>de quién son las revisiones comparables con las de quién</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>Tres resultados:</p>
<ul>
<li class=""><strong><code>Skip</code></strong> — los digests de contenido ya coinciden. Nada que transferir, nada que aplicar. (Ver <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">el protocolo de cut-over</a> para por qué un digest de contenido, no un conteo de revisiones, es lo que vale la pena comparar.)</li>
<li class=""><strong><code>Reconcile</code></strong> (no destructivo) — el nodo local es un <em>subordinado pasivo puro</em> del único activo vivo. Sus revisiones por clave todas vinieron de ese único activo, así que son directamente comparables. La instantánea se fusiona bloqueada-por-revisión: un <code>PutRaw</code> conserva el registro localmente-más-nuevo y rellena las claves faltantes; las tombstones se aplican vía <code>DeleteRaw</code>. Crucialmente, esto preserva las escrituras que la réplica acumuló sobre el canal vivo durante un arranque lento en lugar de tirarlas limpiando primero.</li>
<li class=""><strong><code>Wipe</code></strong> (limpieza destructiva + copia) — el nodo local <em>es o fue él mismo un activo</em> para esta partición. Dos nodos independientemente-activos tienen revisiones por clave <strong>incomparables</strong>: el mismo número de revisión en cada uno se refiere a escrituras distintas. Fusionarlas intercalaría silenciosamente dos historias. Así que el único movimiento seguro es descartar el estado local y adoptar la instantánea autoritativa al por mayor.</li>
</ul>
<p>El valor por defecto es la copia destructiva segura; <code>Reconcile</code> se desbloquea <em>solo</em> al demostrar que el nodo es un subordinado puramente pasivo. Este es el "wipe-guard": previene el merge peligroso entre dos ex-activos, y simétricamente previene que una limpieza al por mayor descarte buenas escrituras del canal vivo en una réplica simple.</p>
<p>Hay un matiz más codificado en <code>priorReconcileShortfall</code>, y sale directamente de un aborto de certificación de doce horas. Una reconciliación bloqueada-por-revisión puede <em>bloquear fuera</em> registros que debería haber aplicado y completar mientras sigue atrasada respecto a la instantánea autoritativa —un segmento atascado en <code>12643/15435</code> a lo largo de 232 sesiones de sincronización, reconciliando los mismos registros fuera de la ventana cada vez, girando para siempre. El flag dice: si una reconciliación anterior se completó pero nos dejó cortos, deja de reconciliar y escala a una limpieza para que la instantánea congelada se adopte entera y realmente converjamos. Solo se establece <em>tras un déficit medido</em> —una réplica adelantada-o-igual nunca lo dispara, así que las escrituras del canal vivo nunca se descartan para perseguir un fantasma.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-4--el-otro-lado-un-propietario-sin-certificar-aún-puede-ser-una-buena-fuente">Paso 4 — el otro lado: un propietario sin certificar aún puede ser una buena fuente<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#paso-4--el-otro-lado-un-propietario-sin-certificar-a%C3%BAn-puede-ser-una-buena-fuente" class="hash-link" aria-label="Enlace directo al Paso 4 — el otro lado: un propietario sin certificar aún puede ser una buena fuente" title="Enlace directo al Paso 4 — el otro lado: un propietario sin certificar aún puede ser una buena fuente" translate="no">​</a></h2>
<p>La recuperación no es solo sobre el nodo que va atrasado. Considera el caso espejo: el nodo A reinicia, y el nodo B —una réplica que ha estado caída— ahora quiere sincronizar <em>desde</em> A. ¿Debería A servir la solicitud mientras A mismo todavía está sin certificar?</p>
<p>La guarda ingenua dice que no: "sin certificar significa que mi almacén está vacío, así que servir sobrescribiría los datos válidos de B con mi hueco". Pero esa premisa es falsa en el momento en que A realmente <em>sostiene</em> la copia autoritativa —lo que ocurre de dos formas: A era un primario preferido que verificó la puesta al día antes de la promoción pero cuya ruta de promoción nunca llamó a <code>CertifyPartition</code> (así que el flag pegajoso quedó atascado en falso mientras los datos están todos ahí), o A tomó la propiedad mediante una recuperación de fallo-total-de-partición y sostiene los datos completos como el activo sin ser nunca estampado <code>IsSynced</code>. En ambos, la copia de A <em>es</em> la fuente de verdad, y rechazar varó la réplica en cero (un volcado: 1.792 solicitudes rechazadas; otro: los 128 segmentos rechazados para toda una partición).</p>
<p>Así que la guarda del lado de la fuente se hace consciente-de-datos:</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>Niégate a servir <strong>solo</strong> cuando el nodo es el propietario, está sin certificar, <em>y</em> el segmento está a la vez sin sincronizar y no sostiene datos —el único caso genuino de "a mitad de recuperación y realmente vacío" donde servir un hueco podría dejar que el solicitante certifique contra él. Como el receptor aplica cada chunk bloqueado-por-revisión, servir datos reales siempre es seguro; la guarda estrecha el rechazo al único estado donde no hay nada seguro que servir.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-5--declarar-la-victoria-exactamente-una-vez">Paso 5 — declarar la victoria, exactamente una vez<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#paso-5--declarar-la-victoria-exactamente-una-vez" class="hash-link" aria-label="Enlace directo al Paso 5 — declarar la victoria, exactamente una vez" title="Enlace directo al Paso 5 — declarar la victoria, exactamente una vez" translate="no">​</a></h2>
<p>Finalmente el activo en recuperación ha extraído cada segmento poseído y ha puesto al día cada uno hasta la cabeza de su fuente. Es autoritativo de nuevo, y necesita <em>decirlo</em> —girar el flag certified pegajoso— para que el disparador de sincronización inversa del Paso 1 deje de re-entregar sus segmentos y el clúster registre la partición como convergida.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ShouldCertify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> selfIsRuntimeActive</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> partitionCertified</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> ownedSegmentCount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token class-name keyword" style="color:#00009f">int</span><span class="token plain"> syncedSegmentCount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> selfIsRuntimeActive</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">partitionCertified</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> ownedSegmentCount </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">       </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> syncedSegmentCount </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> ownedSegmentCount</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La certificación es la completación simétrica del disparador: el disparador encola los segmentos de un activo sin certificar para extraer; esto gira el flag una vez que <em>cada</em> segmento poseído está sincronizado, y la partición se asienta. La ruta de certificación preexistente solo corría para el primario preferido del mapa, así que un activo re-ubicado <em>no-primario</em> extraía todos sus datos correctamente y luego nunca certificaba —re-encolaba la sincronización inversa para siempre, y el clúster nunca se registraba como convergido (una corrida: 30 checkpoints agotando el tiempo ~106 segundos después del inicio, mientras <code>Missing=0</code> y los conteos nunca divergían —los datos estaban <em>bien</em>, solo faltaba el asentamiento).</p>
<p>Y nota el único atajo que esta política deliberadamente <strong>rechaza</strong>: no hay ruta de "sin réplica viva, así que certifica mi vacuidad como autoridad" para un activo no-primario. Un activo no-primario no es la autoridad del mapa; certificarlo vacío mientras una réplica temporalmente-caída todavía sostiene los datos dejaría que esa réplica después sincronizara <em>desde</em> este nodo vacío y perdiera todo. El atajo de autoridad-para-estar-vacío pertenece solo al primario preferido del mapa, en su propia ruta de reconciliación. En todos los demás lugares, la vacuidad no gana nada.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forma-del-todo">La forma del todo<a href="https://clustron.io/es/blog/rebuilding-an-owner-from-replicas/#la-forma-del-todo" class="hash-link" aria-label="Enlace directo al La forma del todo" title="Enlace directo al La forma del todo" translate="no">​</a></h2>
<p>Da un paso atrás y las cinco políticas son una idea aplicada en cinco lugares: <strong>la posesión se demuestra, nunca se asume.</strong></p>
<ul>
<li class="">Un propietario reiniciado está <strong>sin certificar</strong>, así que extrae en lugar de confiar en su almacén vacío (Paso 1).</li>
<li class="">No <strong>servirá</strong> un segmento hasta que la cabeza de su log de operaciones alcance la de la fuente —una pasada de sincronización terminada no es lo mismo que estar al día (Paso 2).</li>
<li class=""><strong>Fusiona</strong> en lugar de pisotear cuando las revisiones son comparables, y pisotea en lugar de intercalar cuando no lo son (Paso 3).</li>
<li class="">Como <em>fuente</em>, sirve siempre que genuinamente sostenga los datos, y rechaza solo cuando está verdaderamente vacío a mitad de recuperación (Paso 4).</li>
<li class=""><strong>Certifica</strong> —gira el flag autoritativo pegajoso— solo ante la prueba de que cada segmento poseído está sincronizado, sin atajo de vacuidad-como-autoridad para un no-primario (Paso 5).</li>
</ul>
<p>Cada una de estas reglas es un predicado puro y sin E/S —un puñado de booleanos y dos números de revisión de entrada, una decisión de salida— que es por lo que son unit-testables de forma aislada y por lo que cada una pudo fijarse al fallo de certificación exacto que la motivó. Son la vista interna del invariante que la <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">certificación de caos de doce horas</a> demuestra desde fuera: mata un propietario en pleno vuelo, deja que sus réplicas lleven la carga, tráelo de vuelta, y observa cómo <code>Missing=0</code> se sostiene a lo largo de millones de operaciones confirmadas. La razón por la que el reinicio no pierde una escritura es que al propietario que regresa nunca se le permite ni una sola vez creer en su propia vacuidad. Tiene que ganarse sus datos de vuelta —de las réplicas que los mantuvieron seguros.</p>
<p>Si quieres las piezas en las que esto se apoya: el <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">log de operaciones reproducible</a> es lo que hace de "pon al día mi cabeza con la cabeza de la fuente" una operación real y sin agujeros, y <a class="" href="https://clustron.io/es/blog/reading-your-own-writes/">leer tus propias escrituras</a> es la promesa de cara al cliente que la compuerta de servicio del Paso 2 existe para mantener.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
            <category>Operaciones</category>
        </item>
        <item>
            <title><![CDATA[Cómo se forma un clúster de Zaris: membresía, el mapa y la disponibilidad condicionada a la convergencia]]></title>
            <link>https://clustron.io/es/blog/cluster-formation-readiness/</link>
            <guid>https://clustron.io/es/blog/cluster-formation-readiness/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Antes de poder servir una sola clave, un clúster de Zaris tiene que responder tres preguntas: quién lo integra, quién posee qué y si ya es seguro aceptar tráfico. Este artículo sigue esa secuencia de arranque: un modelo de membresía basado en un roster de configuración con una ventana de gracia antes de declarar perdido a un nodo, un mapa de particiones autorizado por el líder y confirmado bajo una guarda estricta de versión monótona, y una compuerta de disponibilidad de dos niveles que se niega a servir desde un nodo hasta que su mapa confirmado y su propiedad en tiempo de ejecución coincidan.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Cómo se forma un clúster de Zaris: membresía, el mapa y la disponibilidad condicionada a la convergencia" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJIb3cgYSBaYXJpcyBjbHVzdGVyIGNvbWVzIHVwOiBtZW1iZXJzaGlwLCB0aGUgbWFwLCBhbmQgY29udmVyZ2VuY2UtZ2F0ZWQgcmVhZGluZXNzIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+Q0xVU1RFUiBGT1JNQVRJT048L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+SG93IGEgY2x1c3RlcjwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjE4MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iNDQiIGZvbnQtd2VpZ2h0PSI4MDAiIGZpbGw9IiNmZmZmZmYiIGxldHRlci1zcGFjaW5nPSItMC41Ij5jb21lcyB1cCByZWFkeTwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPk1lbWJlcnNoaXAsIHRoZSBtYXAsIGFuZCBjb252ZXJnZW5jZS1nYXRlZCByZWFkaW5lc3M8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDczMCwyNjQpIj4NCiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI2NCIgeT0iMjMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlIDA8L3RleHQ+DQogICAgPHRleHQgeD0iNjQiIHk9IjQwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+bWFwIHY9MTI8L3RleHQ+DQogICAgPHJlY3QgeD0iMTQ4IiB5PSIwIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSIyMTIiIHk9IjIzIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+bm9kZSAxPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjIxMiIgeT0iNDAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5tYXAgdj0xMjwvdGV4dD4NCiAgICA8cmVjdCB4PSIyOTYiIHk9IjAiIHdpZHRoPSIxMjgiIGhlaWdodD0iNDgiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjM2MCIgeT0iMjMiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5ub2RlIDI8L3RleHQ+DQogICAgPHRleHQgeD0iMzYwIiB5PSI0MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm1hcCB2PTEyPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik02NCA0OCBMMjEyIDk2IiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDxwYXRoIGQ9Ik0yMTIgNDggTDIxMiA5NiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8cGF0aCBkPSJNMzYwIDQ4IEwyMTIgOTYiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHJlY3QgeD0iOTYiIHk9IjEwMCIgd2lkdGg9IjIzMiIgaGVpZ2h0PSI2MCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIyMTIiIHk9IjEyNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPm1hcC5WZXJzaW9uID09IHJ1bnRpbWUuVmVyc2lvbjwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMTIiIHk9IjE0OCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPnRyYWZmaWMgZ2F0ZSBvcGVuICYjODU5NDsgc2VydmU8L3RleHQ+DQogIDwvZz4NCiAgPHRleHQgeD0iODAiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDYiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>La mayor parte de lo que se escribe sobre almacenes distribuidos trata del estado estable —lecturas, escrituras, replicación— o de los fallos dramáticos: un <a class="" href="https://clustron.io/es/blog/anatomy-of-a-failover/">failover</a> cuando muere un propietario, un <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">cut-over</a> cuando se mueve una partición. Pero hay un momento más silencioso que decide si algo de eso funciona: <strong>el momento en que el clúster se forma.</strong> Antes de poder enrutar una sola clave, los nodos tienen que ponerse de acuerdo sobre quién está en el clúster, quién posee qué porción del espacio de claves y —el paso que la mayoría de los sistemas resuelven mal— <em>si es seguro empezar a servir siquiera.</em></p>
<p>Este artículo sigue esa secuencia de arranque en el orden en que Zaris realmente la ejecuta: membresía, el mapa de particiones y las compuertas de disponibilidad que se interponen entre "el proceso está en marcha" y "el nodo está aceptando tráfico". También traza una línea, con honestidad, entre lo que Zaris implementa por sí mismo y lo que hereda del runtime de clúster subyacente sobre el que está construido.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quién-está-en-el-clúster-membresía">Quién está en el clúster: membresía<a href="https://clustron.io/es/blog/cluster-formation-readiness/#qui%C3%A9n-est%C3%A1-en-el-cl%C3%BAster-membres%C3%ADa" class="hash-link" aria-label="Enlace directo al Quién está en el clúster: membresía" title="Enlace directo al Quién está en el clúster: membresía" translate="no">​</a></h2>
<p>Lo primero honesto que hay que decir es dónde está la frontera. El transporte de membresía de bajo nivel —heartbeats entre pares, detección de fallos y elección de líder— lo provee la capa <strong>Clustron.Nodus</strong> sobre la que está construido Zaris. Zaris la consume a través de una interfaz <code>INodusNode</code>: le pregunta a Nodus quiénes son los miembros, cuál ostenta el liderazgo y recibe notificaciones cuando se pierde o falla el heartbeat de un par. Zaris <em>no</em> reimplementa Raft ni un protocolo de descubrimiento basado en gossip; superpone su propio plano de control de particiones y propiedad sobre esa base.</p>
<p>Lo que Zaris añade es una <strong>máquina de estados de membresía</strong> explícita que traduce los eventos de heartbeat en bruto a decisiones relevantes para la propiedad. Vive en <code>MembershipStateTracker</code>, y sus estados son deliberadamente más matizados que "arriba / abajo":</p>
<!-- -->
<p>Los estados caen en dos grupos, y esa división es todo el objetivo. <code>Alive</code>, <code>Suspected</code> y <code>Unreachable</code> afectan únicamente a la <em>propiedad en tiempo de ejecución</em> —pueden disparar la promoción de una réplica o marcar una partición como temporalmente no disponible, pero <strong>nunca mutan el mapa de particiones.</strong> Solo <code>Lost</code>, <code>Evicted</code> y <code>Decommissioned</code> hacen que un nodo sea elegible para la regeneración del mapa.</p>
<p>Esa distinción existe porque regenerar el mapa de particiones es caro y, hecho con demasiada prisa, peligroso: puede reorganizar innecesariamente particiones sanas. Así que cuando falla el heartbeat de un nodo, Zaris <em>no</em> reescribe el mapa de inmediato. Mueve el nodo a <code>Unreachable</code> e inicia una <strong>ventana de gracia</strong> (30 segundos por defecto). Durante esa ventana el clúster solo hace failover en tiempo de ejecución —promover una réplica para que las lecturas y escrituras sigan fluyendo— mientras le da al nodo la oportunidad de volver. Un flapeo, una pausa de GC, un breve corte de red: ninguno de ellos debería reescribir la propiedad. Solo si la ventana de gracia expira el nodo pasa a <code>Lost</code>, y solo cuando un grupo de partición completo ha sido expulsado se dispara la única ruta de regeneración del mapa impulsada por fallo. La regla de diseño, enunciada sin rodeos en el código que la conecta, es: <strong>un nodo que se va nunca regenera el mapa por sí solo.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cómo-se-encuentran-los-nodos">Cómo se encuentran los nodos<a href="https://clustron.io/es/blog/cluster-formation-readiness/#c%C3%B3mo-se-encuentran-los-nodos" class="hash-link" aria-label="Enlace directo al Cómo se encuentran los nodos" title="Enlace directo al Cómo se encuentran los nodos" translate="no">​</a></h3>
<p>El descubrimiento es <strong>basado en roster, no en descubrimiento por gossip.</strong> La lista autorizada de nodos es configuración, no algo que el clúster olfatee en tiempo de ejecución. En una configuración escrita a mano es el arreglo <code>nodes</code> —cada entrada declara un id, un host, un puerto de clúster (de pares) y un puerto de cliente. En Kubernetes, ese roster se sintetiza a partir de los ordinales de los pods del StatefulSet en lugar de mantenerse a mano, con el rol de primario preferido de un nodo derivado directamente de su ordinal.</p>
<p><em>Sí</em> existe un mecanismo de gossip en Zaris, pero cumple una función distinta: es cómo los nodos mantienen convergente su <em>vista del estado del clúster</em>, no cómo descubren que los pares existen. Volveremos a él cuando hablemos de la disponibilidad.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="quién-posee-qué-el-mapa-de-particiones">Quién posee qué: el mapa de particiones<a href="https://clustron.io/es/blog/cluster-formation-readiness/#qui%C3%A9n-posee-qu%C3%A9-el-mapa-de-particiones" class="hash-link" aria-label="Enlace directo al Quién posee qué: el mapa de particiones" title="Enlace directo al Quién posee qué: el mapa de particiones" translate="no">​</a></h2>
<p>Una vez conocida la membresía, el clúster necesita un plan para el espacio de claves. Ese plan es el <strong>mapa de particiones</strong> —un único documento versionado contra el que enruta cada nodo.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="de-una-clave-a-un-propietario-en-dos-saltos">De una clave a un propietario, en dos saltos<a href="https://clustron.io/es/blog/cluster-formation-readiness/#de-una-clave-a-un-propietario-en-dos-saltos" class="hash-link" aria-label="Enlace directo al De una clave a un propietario, en dos saltos" title="Enlace directo al De una clave a un propietario, en dos saltos" translate="no">​</a></h3>
<p>Una clave no se mapea directamente a una partición. Zaris usa un mapeo de dos niveles que está deliberadamente <strong>alineado byte a byte con Redis Cluster</strong> para que la misma aritmética de enrutamiento funcione tanto para clientes nativos como para el front-end RESP:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">key --CRC16--&gt; slot (0..16383) --proportional--&gt; segment (0..255) --SegmentToPartition[]--&gt; partition</span><br></span></code></pre></div></div>
<p>El primer salto hace hash de la clave con CRC16 (CCITT/XMODEM, polinomio <code>0x1021</code>) módulo 16384 —la función exacta que usa Redis Cluster, incluyendo el respeto a la co-ubicación por hash-tag, donde una clave escrita como <code>user:{42}:profile</code> solo hace hash de la porción dentro de las llaves para que las claves relacionadas caigan juntas. Los 16384 slots se mapean luego proporcionalmente sobre los <strong>segmentos</strong> del clúster —256 por defecto, fijos durante toda la vida del clúster— y una tabla <code>SegmentToPartition</code> asigna cada segmento a una partición. La partición, finalmente, es lo que tiene propietarios.</p>
<p>¿Por qué la capa adicional de segmentos en lugar de hacer hash directo a particiones? Porque los segmentos son una unidad estable y de grano fino que puedes <em>mover</em> entre particiones cuando el clúster crece o se rebalancea, sin rehashear cada clave. El paso de CRC16 a slot nunca cambia; solo cambia la asignación de segmento a partición.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-contiene-un-mapa-de-particiones">Qué contiene un mapa de particiones<a href="https://clustron.io/es/blog/cluster-formation-readiness/#qu%C3%A9-contiene-un-mapa-de-particiones" class="hash-link" aria-label="Enlace directo al Qué contiene un mapa de particiones" title="Enlace directo al Qué contiene un mapa de particiones" translate="no">​</a></h3>
<p>El mapa en sí es un <code>PartitionMap</code> —un registro serializado con MessagePack cuya forma te dice con cuánto cuidado se rastrea la propiedad. Los campos que vale la pena conocer:</p>
<ul>
<li class=""><strong><code>Version</code></strong> — un contador monótono que se incrementa en <em>cada</em> cambio del mapa. Este es el número que hace seguro el commit (más abajo).</li>
<li class=""><strong><code>TopologyVersion</code></strong> — se incrementa solo ante un cambio <em>estructural</em> real (un nodo o partición que entra o sale, un re-tile), no ante un cut-over de migración rutinario. Separar "cambió la forma" de "cambió un propietario" permite que el resto del sistema reaccione de forma proporcional.</li>
<li class=""><strong><code>TotalSegments</code></strong> — 256 por defecto, constante durante toda la vida del clúster.</li>
<li class=""><strong><code>Partitions</code></strong> — la lista de asignaciones, cada una con un <code>PrimaryNodeId</code> (el primario de ubicación), <code>ReplicaNodeIds</code> y una <code>PartitionIdentity</code> estable (un GUID acuñado en su nacimiento que sobrevive a la renumeración) más los campos de arrendamiento de propiedad: <code>ActiveOwnerNodeId</code>, un <code>OwnershipGeneration</code> acuñado por el líder y el <code>LeaderTerm</code> que concedió el arrendamiento —que es lo que permite al clúster aislar (fence) a un ex-líder obsoleto para que no autorice propiedad.</li>
</ul>
<p>Nota la división deliberada entre <code>PrimaryNodeId</code> (dónde el mapa <em>prefiere</em> que viva una partición) y <code>ActiveOwnerNodeId</code> (quién la está <em>sirviendo realmente</em> ahora mismo). Esos pueden diferir durante un failover o una migración, y mantenerlos como campos separados es lo que hace esas transiciones representables sin mentir sobre el plan en estado estable. El artículo sobre el <a class="" href="https://clustron.io/es/blog/reading-your-own-writes/">modelo de consistencia</a> explica por qué las lecturas enrutan a ese único propietario activo.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cómo-se-planifica-el-mapa">Cómo se planifica el mapa<a href="https://clustron.io/es/blog/cluster-formation-readiness/#c%C3%B3mo-se-planifica-el-mapa" class="hash-link" aria-label="Enlace directo al Cómo se planifica el mapa" title="Enlace directo al Cómo se planifica el mapa" translate="no">​</a></h3>
<p>El mapa no se arma de forma codiciosa. Lo produce un planificador puro y determinista —<code>DesiredMapPlanner</code>— que toma los ids de los nodos miembros, el conteo de segmentos, un conteo deseado de particiones y el factor de replicación, y devuelve un plan. Dos propiedades merecen mención:</p>
<ol>
<li class=""><strong>Divisiones de segmentos equilibradas y contiguas.</strong> Los segmentos se dividen tan uniformemente como sea posible, en tramos contiguos, con las primeras particiones absorbiendo el resto cuando no divide de forma limpia.</li>
<li class=""><strong>Ubicación de grupos disjuntos.</strong> Los primeros <em>N</em> miembros se convierten en los primarios distintos de partición; los miembros restantes rellenan los espacios de réplica por turnos (round-robin), de modo que ningún nodo termina en dos grupos de partición distintos.</li>
</ol>
<p>La razón por la que el planificador está escrito como una función pura de sus entradas es ganada con esfuerzo: hace que toda una clase de mapas defectuosos —una partición con un primario duplicado, un rango de segmentos no contiguo, una asignación degenerada— sea <em>irrepresentable por construcción</em>, en lugar de algo que detectas después de los hechos con un validador. Vale la pena ser honestos también sobre la limitación actual: la ubicación hoy es posicional y de grupos disjuntos; una ubicación estilo rendezvous que minimice el churn y mueva la menor cantidad posible de datos en cada regeneración es un refinamiento futuro explícito, no algo que ya se entregue. Lo que <em>sí</em> se entrega es un paso de ubicación pegajosa (sticky) que, en una regeneración por churn de membresía, mantiene en su sitio a un propietario vivo que está sirviendo, para que una partición sana no se mueva sin razón.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-significa-commit">Qué significa "commit"<a href="https://clustron.io/es/blog/cluster-formation-readiness/#qu%C3%A9-significa-commit" class="hash-link" aria-label="Enlace directo al Qué significa &quot;commit&quot;" title="Enlace directo al Qué significa &quot;commit&quot;" translate="no">​</a></h3>
<p>El mapa lo autoriza <strong>un nodo —el líder del plano de control— y nadie más.</strong> El líder genera un mapa candidato, lo pasa por una proyección del log de propiedad (el líder es el único escritor del log de propiedad, y el mapa servido es un plegado de ese log), y luego <em>lo confirma localmente</em> antes de avisar a nadie. El commit es una única regla estricta impuesta en <code>TopologyManager.ApplyMap</code>:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// A map is accepted only if it is strictly newer.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">_currentMap </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">null</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> map</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> _currentMap</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Version</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Un mapa con una versión menor o igual a la que ya tienes es rechazado, punto. Solo después de que el líder confirma difunde un <code>MapChangedEvent</code>, envía instantáneas directas del mapa a los pares y publica el cambio de enrutamiento a los clientes. Los seguidores extraen (pull) el mapa (en arranque en frío lo solicitan a pares ya conocidos, reintentando hasta que uno responda) y lo aplican a través de la <em>misma</em> guarda monótona. Como cada nodo solo acepta una versión estrictamente superior, dos nodos nunca pueden asentarse sobre mapas conflictivos de la misma versión —el número de versión es el punto de serialización.</p>
<!-- -->
<p>Un matiz honesto de arranque en frío: un nodo sin líder que arranca solo puede acuñar un único mapa <em>provisional</em> de un solo nodo para no quedar paralizado, marcado como provisional y luego reemplazado por el mapa autoritativo multinodo una vez que el clúster se forma. Es una concesión a la disponibilidad, no la ruta de estado estable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="es-seguro-servir-disponibilidad-condicionada-a-la-convergencia">¿Es seguro servir?: disponibilidad condicionada a la convergencia<a href="https://clustron.io/es/blog/cluster-formation-readiness/#es-seguro-servir-disponibilidad-condicionada-a-la-convergencia" class="hash-link" aria-label="Enlace directo al ¿Es seguro servir?: disponibilidad condicionada a la convergencia" title="Enlace directo al ¿Es seguro servir?: disponibilidad condicionada a la convergencia" translate="no">​</a></h2>
<p>Aquí está el paso que separa un almacén en el que puedes confiar de uno que meramente arranca. Tener un proceso en marcha, e incluso tener un mapa, no significa que un nodo deba aceptar tráfico. Si un nodo empieza a servir antes de que su <em>propiedad en tiempo de ejecución</em> se haya puesto al día con el <em>mapa</em> que acaba de aplicar, puede enrutar una solicitud a una partición que en realidad todavía no posee. Zaris se niega a hacer eso, con dos compuertas distintas.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="compuerta-uno-estabilidad-de-la-formación-del-clúster">Compuerta uno: estabilidad de la formación del clúster<a href="https://clustron.io/es/blog/cluster-formation-readiness/#compuerta-uno-estabilidad-de-la-formaci%C3%B3n-del-cl%C3%BAster" class="hash-link" aria-label="Enlace directo al Compuerta uno: estabilidad de la formación del clúster" title="Enlace directo al Compuerta uno: estabilidad de la formación del clúster" translate="no">​</a></h3>
<p>La primera compuerta decide cuándo se le permite al líder generar el <em>primer mapa real</em>. <code>ClusterReadinessGate</code> espera a que la membresía sea estable —al menos un número mínimo de nodos presentes <em>de forma continua</em> durante una ventana de duración-estable (3 segundos por defecto)— antes de confirmar un mapa. Esto evita que el líder autorice un mapa contra un clúster a medio formar donde los nodos todavía se están uniendo uno a uno, lo que significaría una regeneración inmediata un segundo después. Si la estabilidad no se alcanza dentro del timeout (30 segundos por defecto), la compuerta <em>fuerza el arranque (force-bootstraps)</em> de todos modos: la disponibilidad gana frente a esperar para siempre. El clúster prefiere arrancar un poco pronto a quedarse atascado.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="compuerta-dos-la-compuerta-de-tráfico-por-nodo">Compuerta dos: la compuerta de tráfico por nodo<a href="https://clustron.io/es/blog/cluster-formation-readiness/#compuerta-dos-la-compuerta-de-tr%C3%A1fico-por-nodo" class="hash-link" aria-label="Enlace directo al Compuerta dos: la compuerta de tráfico por nodo" title="Enlace directo al Compuerta dos: la compuerta de tráfico por nodo" translate="no">​</a></h3>
<p>La segunda compuerta es la que realmente decide si un nodo abre su listener de cliente. <code>ClusterTrafficReadinessGate.IsReady()</code> devuelve verdadero solo cuando se cumplen <strong>todas</strong> las siguientes condiciones:</p>
<ul>
<li class="">existe un mapa confirmado,</li>
<li class="">su tabla segmento-a-partición está completamente poblada (<code>length == TotalSegments</code>),</li>
<li class="">existe un mapa de propiedad en tiempo de ejecución,</li>
<li class=""><strong><code>runtime.MapVersion == map.Version</code></strong> — la propiedad en tiempo de ejecución del nodo está alineada con la versión exacta del mapa que ha confirmado,</li>
<li class="">y cada partición tiene una entrada en tiempo de ejecución.</li>
</ul>
<p>Esa cuarta condición es el corazón del asunto. Un nodo no se anunciará como listo para servir hasta que su propio mapa confirmado y su propia propiedad en tiempo de ejecución estén en la <em>misma versión</em>. Esto es lo que señala la imagen principal en la parte superior. Hay un respaldo (backstop) de 60 segundos para que un nodo que no puede converger no cuelgue el arranque para siempre —pasado ese punto procede con una advertencia en lugar de quedarse atascado— pero en el caso normal, la compuerta mantiene la puerta cerrada hasta que los dos planos coinciden.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="mantenerse-convergente-digests-y-detectar-y-sanar">Mantenerse convergente: digests y detectar-y-sanar<a href="https://clustron.io/es/blog/cluster-formation-readiness/#mantenerse-convergente-digests-y-detectar-y-sanar" class="hash-link" aria-label="Enlace directo al Mantenerse convergente: digests y detectar-y-sanar" title="Enlace directo al Mantenerse convergente: digests y detectar-y-sanar" translate="no">​</a></h3>
<p>La disponibilidad es una propiedad del momento de arranque; <em>mantenerse</em> convergente es una del estado estable, y aquí es donde el gossip de Zaris realmente se gana el sustento. Cada heartbeat lleva un pequeño <strong><code>ClusterStateDigest</code></strong> —el epoch del nodo, quién cree que es el líder, su versión del mapa, su conteo de nodos y las generaciones de propiedad por partición. Sin RPC adicionales; viaja sobre el tráfico que el clúster ya envía.</p>
<p>Un <code>DigestAccumulator</code> recopila los digests de los pares y, cada pocos segundos, los compara. Si un nodo diverge de la mayoría durante dos ciclos de evaluación consecutivos, levanta una señal de deriva (drift), y un <code>HealingOrchestrator</code> actúa sobre ella —el líder envía directivas correctivas (con un enfriamiento para que no se descontrole), y un nodo que se encuentra como el único atípico se auto-sana re-extrayendo el mapa. Esto es convergencia por <strong>detectar-y-reparar</strong>, no por una votación de quórum previa al servicio.</p>
<p>Esa distinción importa para la honestidad, así que déjame ser preciso al respecto. En producción, el acuerdo sobre la versión del mapa en todo el clúster se mantiene mediante la compuerta de tráfico por nodo más este bucle de sanación impulsado por digests —no mediante una barrera global que bloquee todo el servicio hasta que cada nodo haya votado sobre una versión. La afirmación más fuerte —que <em>cada</em> instantánea de nodo coincida sobre un líder, un epoch de líder y una versión de mapa— es lo que el arnés de <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">certificación de caos de 12 horas</a> verifica continuamente como un invariante de aprobado/reprobado. La certificación prueba que la propiedad se sostiene; el runtime la logra mediante compuertas y sanación en lugar de una ronda de consenso bloqueante en la ruta de servicio.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="factor-de-replicación-y-un-valor-por-defecto-honesto">Factor de replicación, y un valor por defecto honesto<a href="https://clustron.io/es/blog/cluster-formation-readiness/#factor-de-replicaci%C3%B3n-y-un-valor-por-defecto-honesto" class="hash-link" aria-label="Enlace directo al Factor de replicación, y un valor por defecto honesto" title="Enlace directo al Factor de replicación, y un valor por defecto honesto" translate="no">​</a></h2>
<p>Una última pieza de la historia de la formación, porque cambia lo que "listo" significa para tu durabilidad. El factor de replicación por defecto es <strong>1</strong>. Con <code>RF=1</code>, cada partición tiene un primario y <em>ninguna réplica</em> —una sola copia. El planificador coloca <code>RF - 1</code> réplicas por partición, así que en el valor por defecto eso son cero. Un clúster con <code>RF=1</code> se forma, converge y sirve perfectamente bien, pero la pérdida de un nodo es una pérdida de datos para las particiones que poseía, por diseño, no por accidente. Si quieres que una partición sobreviva al fallo de un nodo, pones <code>RF &gt;= 2</code>, y necesitas suficientes nodos para colocar realmente esas réplicas en máquinas distintas —el artículo sobre <a class="" href="https://clustron.io/es/blog/kubernetes-durability-sizing/">dimensionamiento de durabilidad en Kubernetes</a> trabaja exactamente cuántos pods hacen falta. El conteo de particiones tiene un valor por defecto igualmente honesto: si no se establece, el clúster usa una partición por nodo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-secuencia-de-principio-a-fin">La secuencia, de principio a fin<a href="https://clustron.io/es/blog/cluster-formation-readiness/#la-secuencia-de-principio-a-fin" class="hash-link" aria-label="Enlace directo al La secuencia, de principio a fin" title="Enlace directo al La secuencia, de principio a fin" translate="no">​</a></h2>
<p>En conjunto, un clúster de Zaris se forma así:</p>
<ol>
<li class=""><strong>La membresía se resuelve</strong> a partir del roster de configuración (o los ordinales de k8s), y Nodus elige un líder del plano de control.</li>
<li class="">El líder espera detrás de la <strong>compuerta de formación</strong> a tener una membresía estable, luego planifica un mapa con <code>DesiredMapPlanner</code> y <strong>lo confirma localmente</strong> bajo la estricta guarda de versión monótona.</li>
<li class="">El líder <strong>difunde</strong> el mapa; los seguidores lo extraen y lo aplican bajo la misma guarda, aceptando solo versiones estrictamente superiores.</li>
<li class="">Cada nodo se mantiene detrás de su <strong>compuerta de tráfico</strong> hasta que <code>runtime.MapVersion == map.Version</code> y el enrutamiento está completamente poblado —solo entonces abre su listener de cliente.</li>
<li class="">En estado estable, los <strong>digests transportados por heartbeat</strong> mantienen convergente la vista de cada nodo, con detección de deriva y sanación impulsada por el líder cerrando cualquier brecha.</li>
</ol>
<p>Ninguno de estos pasos es dramático por sí solo. Juntos son la diferencia entre un clúster que está <em>corriendo</em> y uno que está <em>listo</em> —y la brecha entre esas dos palabras es donde un almacén pierde datos silenciosamente si no se toma la pregunta en serio.</p>
<hr>
<p><em>Zaris es el almacén clave-valor distribuido de Clustron. Si quieres ver el plano de control desde el lado del operador, el <a class="" href="https://clustron.io/es/blog/control-plane-powershell/">recorrido en PowerShell</a> crea, arranca e inspecciona un clúster en vivo; y el <a class="" href="https://clustron.io/es/blog/elastic-scaling/">escalado elástico</a> cubre qué le pasa al mapa cuando añades un servidor a un clúster que ya está sirviendo.</em></p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Dos clústeres, un change feed: replicar a través de una WAN sin estirar Zaris]]></title>
            <link>https://clustron.io/es/blog/cross-datacenter-change-feed/</link>
            <guid>https://clustron.io/es/blog/cross-datacenter-change-feed/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un clúster está afinado para una LAN de datacenter —heartbeats de microsegundos, failover rápido, un protocolo de propiedad que asume un enlace estrecho. Estíralo a través de una WAN y esas suposiciones se vuelven en tu contra. La forma segura son dos clústeres independientes unidos por un change feed reanudable y ordenado por revisión. Aquí está por qué, y cómo el log de operaciones ya te da el cursor para construirlo.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Dos clústeres independientes enlazados por un change feed reanudable a través de una 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>Tarde o temprano alguien hace la pregunta: <em>tenemos un clúster en una región y usuarios en otra —¿no podemos simplemente añadir unos cuantos nodos allá y dejar que un clúster abarque ambas?</em> Es un instinto razonable. Un clúster, un namespace, una cadena de conexión, datos en todas partes. El instinto también es una trampa, y vale la pena ser preciso sobre por qué.</p>
<p>Un clúster de Zaris está diseñado para una red de datacenter. La membresía se rastrea con heartbeats en temporizadores cortos. La propiedad de una partición se mueve entre nodos mediante un handshake que asume que el otro lado responde rápido. El failover aísla a un propietario sospechoso de estar muerto y promueve una réplica en cuestión de segundos. Cada uno de esos mecanismos está calibrado para un enlace donde el viaje de ida y vuelta se mide en microsegundos y un nodo "lento" está genuinamente enfermo —no meramente a ochenta milisegundos y un océano de distancia. Pon la mitad de los nodos al otro lado de una WAN y no habrás construido un clúster geo-distribuido; habrás construido un clúster que ahora confunde la latencia normal de WAN con un fallo, flapea la propiedad y arriesga split-brain la primera vez que el enlace entre regiones tiene un hipo.</p>
<p>Así que la forma correcta no es un clúster estirado. Son <strong>dos clústeres independientes</strong> —cada uno autocontenido, cada uno con su propia propiedad y failover quedándose enteramente en su propia LAN— unidos por un <strong>change feed asíncrono y reanudable</strong>. Este artículo trata sobre ese patrón: por qué la independencia es la decisión portante, y cómo el log de operaciones de Zaris ya te entrega lo único que un replicador de WAN realmente necesita —un cursor desde el cual reanudar.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-un-clúster-estirado-es-la-herramienta-equivocada">Por qué un clúster estirado es la herramienta equivocada<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#por-qu%C3%A9-un-cl%C3%BAster-estirado-es-la-herramienta-equivocada" class="hash-link" aria-label="Enlace directo al Por qué un clúster estirado es la herramienta equivocada" title="Enlace directo al Por qué un clúster estirado es la herramienta equivocada" translate="no">​</a></h2>
<p>Ayuda nombrar las suposiciones específicas que hace un único clúster, porque cada una se rompe de forma distinta sobre una WAN.</p>
<!-- -->
<ul>
<li class=""><strong>La membresía basada en heartbeats</strong> decide que un nodo se ha ido cuando pierde unos pocos latidos. Un enlace entre regiones que añade latencia y la partición ocasional hará saltar ese detector durante la operación saludable. El clúster declarará muertos a nodos vivos.</li>
<li class=""><strong>El protocolo de propiedad</strong> mueve particiones entre nodos con un handshake que espera una respuesta pronta. A través de una WAN, esas transiciones se estancan, reintentan y compiten —exactamente el churn que la <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">maquinaria de propiedad y cutover</a> está diseñada para resolver <em>rápidamente</em> en una LAN.</li>
<li class=""><strong>Los temporizadores de failover</strong> promueven una réplica cuando el propietario parece muerto. Sobre una WAN, "parece muerto" y "solo está lejos" se vuelven indistinguibles, así que obtienes promociones espurias —y si el enlace se sana con una réplica promovida en cada lado, tienes dos propietarios para una partición.</li>
</ul>
<p>Nada de esto es un defecto. Es un almacén haciendo exactamente lo que un almacén de baja latencia en datacenter debería hacer, aplicado a una red que nunca fue pensado para abarcar. El arreglo no es aflojar cada temporizador hasta que el clúster tolere una WAN —eso también lo haría tolerar fallos reales demasiado lentamente. El arreglo es dejar de pedirle a un clúster que lo haga.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-bloque-de-construcción-que-ya-tienes-un-log-reproducible">El bloque de construcción que ya tienes: un log reproducible<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#el-bloque-de-construcci%C3%B3n-que-ya-tienes-un-log-reproducible" class="hash-link" aria-label="Enlace directo al El bloque de construcción que ya tienes: un log reproducible" title="Enlace directo al El bloque de construcción que ya tienes: un log reproducible" translate="no">​</a></h2>
<p>Zaris mantiene, por segmento, un <strong>log de operaciones ordenado por revisión</strong>. Cada escritura aceptada avanza una revisión monótona y añade una entrada. Ese log es el sustrato que las réplicas y los nuevos propietarios de partición usan para <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">ponerse al día sin congelar las escrituras</a>: un consumidor que conoce la última revisión que vio puede reproducir hacia adelante desde ese punto y converger a la verdad actual.</p>
<p>Esa propiedad —<em>reproducir hacia adelante desde una revisión conocida</em>— es el fundamento entero de un replicador de WAN. Un change feed no es más que un lector de larga vida de ese log que transmite entradas en orden de revisión y recuerda dónde se detuvo. Lo que recuerda es un <strong>cursor</strong>: la revisión más alta que ha aplicado de forma durable, por segmento.</p>
<!-- -->
<p>Como el log está ordenado por revisión y la reproducción comienza desde un punto que eliges, el feed tiene los dos rasgos que un enlace de WAN exige:</p>
<ol>
<li class=""><strong>Reanudable.</strong> El enlace <em>se caerá</em> —ese es el hecho definitorio de una WAN. Cuando vuelve, el seguidor reconecta y pide todo lo posterior a su cursor almacenado. Sin re-copia completa, sin coordinación, solo "reanudar desde la revisión N".</li>
<li class=""><strong>Idempotente en la reproducción.</strong> Aplica una entrada solo cuando su revisión es mayor que la última aplicada para ese segmento. Si una reconexión re-entrega la cola, el seguidor omite lo que ya tiene. La entrega duplicada se convierte en un no-op, que es lo que permite que el feed sea al-menos-una-vez en lugar de tener que ser exactamente-una-vez.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-patrón-de-principio-a-fin">El patrón, de principio a fin<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#el-patr%C3%B3n-de-principio-a-fin" class="hash-link" aria-label="Enlace directo al El patrón, de principio a fin" title="Enlace directo al El patrón, de principio a fin" translate="no">​</a></h2>
<p>Dos clústeres. Llámalos <strong>A</strong> (la fuente) y <strong>B</strong> (el seguidor). Cada uno es un clúster de Zaris ordinario e independiente que no sabe nada de los internos del otro.</p>
<p>En el clúster B ejecutas un <strong>seguidor de replicación</strong> —un cliente que consume el change feed de A y escribe las operaciones en B localmente. Su estado es diminuto y durable: un cursor por segmento. El bucle es poco glamoroso, que es el punto:</p>
<ol>
<li class="">Conéctate a A, solicita el change feed para los segmentos (o prefijos de clave) que quieres replicar, empezando después del cursor almacenado de cada segmento.</li>
<li class="">Recibe entradas en orden de revisión. Para cada una, aplica la operación a B —<code>SET</code>, <code>DELETE</code>, expiración— protegida por la comprobación de revisión para que la re-entrega sea inofensiva.</li>
<li class="">Avanza y persiste el cursor a medida que las entradas se aplican de forma durable.</li>
<li class="">Ante cualquier desconexión, vuelve al paso 1. El cursor es donde lo dejaste.</li>
</ol>
<p>Todo lo que el seguidor necesita para sobrevivir a una interrupción vive en ese cursor. No hay transacción distribuida a través de la WAN, ni membresía compartida, ni quórum entre regiones. Un hipo en el enlace no te cuesta más que un poco de frescura.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lo-que-estás-negociando-el-rpo-no-es-cero-y-ese-es-el-trato">Lo que estás negociando: el RPO no es cero, y ese es el trato<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#lo-que-est%C3%A1s-negociando-el-rpo-no-es-cero-y-ese-es-el-trato" class="hash-link" aria-label="Enlace directo al Lo que estás negociando: el RPO no es cero, y ese es el trato" title="Enlace directo al Lo que estás negociando: el RPO no es cero, y ese es el trato" translate="no">​</a></h2>
<p>Esta replicación es <strong>asíncrona</strong>. El seguidor siempre está a cierta distancia por detrás de la fuente —tantas revisiones como se hayan producido en A pero aún no se hayan aplicado en B. Esa distancia es tu <strong>Objetivo de Punto de Recuperación (Recovery Point Objective)</strong>: si la región A desaparece en este instante, pierdes lo que aún no había llegado a B.</p>
<p>Eso no es un defecto por el que disculparse; es el trato que estás eligiendo explícitamente. La alternativa —hacer que las escrituras en A esperen un acuse de recibo de B antes de confirmar— pondría un viaje de ida y vuelta de WAN en tu ruta de escritura y rendiría la baja latencia que es la razón entera por la que ejecutas Zaris. La replicación síncrona entre regiones convierte cada escritura en una solicitud transcontinental. Casi nadie quiere eso en realidad una vez que ven la factura de latencia.</p>
<p>Así que el encuadre honesto es: <strong>tú eliges el RPO.</strong> Un seguidor bien alimentado sobre un enlace saludable se mantiene dentro de un puñado de revisiones —obsolescencia por debajo del segundo en la práctica. Un enlace saturado o que flapea lo deja derivar más. El cursor hace la deriva <em>acotada y visible</em> —puedes medir exactamente cuánto va B por detrás comparando su cursor con la revisión actual de A— pero no la hace cero. Dimensiona tu tolerancia en consecuencia, y alerta sobre el retraso del cursor igual que alertarías sobre el retraso de una réplica dentro de un clúster.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cuando-la-fuente-hace-failover-al-feed-no-le-importa">Cuando la fuente hace failover, al feed no le importa<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#cuando-la-fuente-hace-failover-al-feed-no-le-importa" class="hash-link" aria-label="Enlace directo al Cuando la fuente hace failover, al feed no le importa" title="Enlace directo al Cuando la fuente hace failover, al feed no le importa" translate="no">​</a></h2>
<p>Aquí es donde construir sobre el log de operaciones rinde una segunda vez. Dentro del clúster A, la partición que tu feed está leyendo ocasionalmente cambiará de propietario —un nodo muere, una réplica es promovida, una partición migra. El log sobrevive a eso: está indexado por revisión, y un nuevo propietario continúa la misma secuencia de revisión. Para el seguidor, un failover del lado de la fuente es solo otra reconexión. Re-establece el feed contra la topología actual de A y reanuda desde su cursor. Las revisiones que ya aplicó siguen siendo las revisiones que ya aplicó; las que le faltan siguen estando después del cursor. El seguidor nunca tiene que entender <em>quién</em> en A posee la partición —solo <em>qué revisión</em> vio por última vez.</p>
<p>Esta es la razón silenciosa por la que el diseño de cursor-sobre-log vale más que un relé ingenuo de "sigue las escrituras y envíalas". Un relé que transmite escrituras en vivo sin una posición durable e indexada por revisión no tiene nada desde lo cual reanudar cuando la topología de la fuente cambia. El log le da al seguidor un sistema de coordenadas estable que sobrevive a cualquier propietario individual.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-alcance-es-una-elección-y-los-conflictos-son-una-frontera">El alcance es una elección, y los conflictos son una frontera<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#el-alcance-es-una-elecci%C3%B3n-y-los-conflictos-son-una-frontera" class="hash-link" aria-label="Enlace directo al El alcance es una elección, y los conflictos son una frontera" title="Enlace directo al El alcance es una elección, y los conflictos son una frontera" translate="no">​</a></h2>
<p>Dos cosas más sobre las que ser deliberado.</p>
<p><strong>Alcance de los datos.</strong> No tienes que replicar todo. El feed es por segmento, y puedes seleccionar los segmentos o prefijos de clave que importan —un tenant específico, un dataset específico, la porción que una región de recuperación ante desastres realmente necesita. Replicar menos significa menos tráfico de WAN y un seguidor más pequeño; replicar todo te da un standby cálido completo. Decide a propósito en lugar de por defecto.</p>
<p><strong>Dirección y conflictos.</strong> El patrón anterior es <strong>unidireccional</strong>: A es la fuente de verdad, B es un seguidor / standby mayormente de lectura. Ese es el caso limpio, y no tiene problema de conflictos —hay exactamente un escritor por clave. En el momento en que lo haces <strong>bidireccional</strong> —ambas regiones aceptando escrituras para las mismas claves— has introducido escritores concurrentes separados por una WAN, y ninguna cantidad de plomería de change feed lo resuelve por ti. Ahora debes una política de resolución de conflictos: last-writer-wins sobre un reloj en el que confíes, propiedad por clave para que las dos regiones nunca escriban la misma clave, o merge a nivel de aplicación. Activo-activo es un diseño real y a veces necesario, pero es una decisión de <em>modelado de datos</em>, no una característica de transporte. Recurre a unidireccional primero; adopta bidireccional solo cuando hayas decidido, explícitamente, cómo se reconcilian dos escritores.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forma-a-recordar">La forma a recordar<a href="https://clustron.io/es/blog/cross-datacenter-change-feed/#la-forma-a-recordar" class="hash-link" aria-label="Enlace directo al La forma a recordar" title="Enlace directo al La forma a recordar" translate="no">​</a></h2>
<!-- -->
<p>Una WAN no es una LAN lenta; es un medio distinto con las particiones como un evento normal en lugar de una emergencia. Un único clúster, correctamente, se niega a fingir lo contrario —sus temporizadores y su protocolo de propiedad están afinados para tratar un silencio largo como un fallo, porque dentro de un datacenter casi siempre lo es. Así que no estiras el clúster. Ejecutas dos de ellos, cada uno soberano en su propia red, y los unes con la única primitiva que está construida para tolerar un enlace que va y viene: un log ordenado por revisión que puedes reproducir, y un cursor que recuerda dónde estabas.</p>
<p>La independencia es la decisión que hace seguro todo lo demás. El change feed es solo la tubería. El log de operaciones ya te dio el cursor.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
            <category>Operaciones</category>
        </item>
        <item>
            <title><![CDATA[El piso de latencia: un microsegundo de trabajo dentro de diez de plomería]]></title>
            <link>https://clustron.io/es/blog/latency-floor/</link>
            <guid>https://clustron.io/es/blog/latency-floor/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Instrumentamos la ruta que toma un solo GET a través de un nodo de Zaris, desde el socket hasta la partición y de vuelta. El almacén en memoria hace su trabajo en unos 1,7 microsegundos. La operación cuesta de diez a trece. Este artículo trata de a dónde van los otros once —la cadena de saltos de egreso, las copias, el hash computado tres veces, y una comprobación de licencia que toma un lock en cada solicitud— y qué requiere realmente colapsarlos.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Un microsegundo de trabajo dentro de diez microsegundos de plomería" 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>Aquí hay un número que reencuadra cómo piensas sobre un almacén clave/valor. Cuando aislamos el almacén en memoria de Zaris y le damos operaciones sin nada más en el camino —sin socket, sin serialización, sin enrutamiento— un solo hilo sirve lecturas a más de un millón por segundo, y escala limpiamente a más de ocho millones a lo largo de ocho hilos. El almacén, en otras palabras, hace el trabajo real de un GET en menos de dos microsegundos, y enfáticamente <em>no</em> es el cuello de botella.</p>
<p>Y sin embargo un GET completo sobre una conexión TCP real, en la misma máquina, se estanca en unos pocos cientos de miles por segundo. El almacén puede hacer el trabajo cinco veces más rápido de lo que el servidor puede entregarlo. Así que la pregunta de rendimiento interesante sobre Zaris no es "¿qué tan rápido es el almacén?". Es lo contrario: <em>¿a dónde va todo el tiempo entre el socket y el almacén?</em> Este artículo es la respuesta, trazada operación por operación, porque la brecha entre 1,7 microsegundos de lógica y 10 a 13 microsegundos de CPU de servidor por operación no es ruido —es el piso de latencia, y casi todo él es plomería.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-medición-que-inicia-toda-la-investigación">La medición que inicia toda la investigación<a href="https://clustron.io/es/blog/latency-floor/#la-medici%C3%B3n-que-inicia-toda-la-investigaci%C3%B3n" class="hash-link" aria-label="Enlace directo al La medición que inicia toda la investigación" title="Enlace directo al La medición que inicia toda la investigación" translate="no">​</a></h2>
<p>No puedes optimizar una ruta que no has separado en capas, así que lo primero que construimos fue un conjunto de benchmarks de ablación —arneses que despojan al sistema hasta una capa a la vez.</p>
<ul>
<li class=""><strong>Solo almacén.</strong> Llama directamente al almacén en proceso de la partición, en proceso, sin red. Resultado: ~1,15M GET/s y ~0,5M PUT/s <em>por hilo</em>, escalando linealmente a ~8,2M / ~2,0M a ocho hilos.</li>
<li class=""><strong>Eco de socket puro.</strong> Un socket .NET simple haciendo frames de la misma forma, sin lógica de Zaris: ~7,2M frames/s en la misma caja.</li>
<li class=""><strong>Ruta TCP nativa completa.</strong> El cliente real hablando con el servidor real: ~300-324K ops/s, con solo unos tres de catorce núcleos lógicos ocupados en cada lado.</li>
</ul>
<p>Lee esos tres juntos y dos sospechosos habituales caen de inmediato. El almacén no es el límite —es 3-4x más rápido que la ruta completa. El sistema operativo y el loopback tampoco son el límite —un socket puro mueve frames veinte veces más rápido. Lo que queda en el medio, entre un socket que puede hacer millones y un almacén que puede hacer millones, es la propia plomería de solicitudes de Zaris, y está gastando aproximadamente <strong>10-13 microsegundos de CPU de servidor por operación para envolver unos 1,7 microsegundos de trabajo real.</strong></p>
<p>Esa proporción es el tema. Sigamos un GET a través de ella.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-ruta-de-un-solo-get">La ruta de un solo GET<a href="https://clustron.io/es/blog/latency-floor/#la-ruta-de-un-solo-get" class="hash-link" aria-label="Enlace directo al La ruta de un solo GET" title="Enlace directo al La ruta de un solo GET" translate="no">​</a></h2>
<!-- -->
<p>Nada en ese diagrama está mal o es derrochador a primera vista —es la forma honesta de un almacén en red. El costo está en los <em>detalles</em> de cada flecha: cuántas veces tiene que despertar un hilo, cuántas veces se copian los bytes, y cuánto trabajo fijo por solicitud viaja junto que no tiene nada que ver con la clave que se lee. Esos detalles son donde viven los once microsegundos, y se agrupan en tres conjuntos.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cuello-de-botella-uno-la-cadena-de-saltos-de-egreso">Cuello de botella uno: la cadena de saltos de egreso<a href="https://clustron.io/es/blog/latency-floor/#cuello-de-botella-uno-la-cadena-de-saltos-de-egreso" class="hash-link" aria-label="Enlace directo al Cuello de botella uno: la cadena de saltos de egreso" title="Enlace directo al Cuello de botella uno: la cadena de saltos de egreso" translate="no">​</a></h2>
<p>El mayor costo individual no es leer el valor —es <em>entregarlo</em>. Enviar una respuesta de vuelta al cliente, tal como está estructurado actualmente, cruza <strong>tres canales internos, despierta tres hilos y copia la carga útil cuatro veces</strong> en su camino de salida:</p>
<ol>
<li class="">La respuesta se entrega del shard worker a la ruta de envío del pipeline del servidor.</li>
<li class="">La ruta de envío la entrega a un bucle de escritura por conexión, que la codifica en un buffer agrupado (pooled).</li>
<li class="">El bucle de escritura la encola en la cola de envío de la conexión de transporte.</li>
<li class="">El transporte fusiona (coalesces) los buffers encolados en un stream y escribe al socket.</li>
</ol>
<p>Cada entrega es un despertar de hilo, y cada despertar es un evento de planificación que el SO tiene que atender —barato individualmente, ruinoso a cientos de miles por segundo. Y en cada etapa los bytes se copian de nuevo: buffer de codificación agrupado, cola de envío, buffer de fusión, socket. En el lado <em>entrante</em> hay un costo equivalente: cada solicitud que llega en un hilo de completación IOCP se entrega a un shard worker antes de que ocurra cualquier trabajo, lo que es otro despertar y otra cola.</p>
<p>La razón por la que existe esta estructura es razonable —desacoplar recepción, despacho, codificación y transmisión hace que cada capa sea más simple de razonar y permite que los consumidores lentos apliquen backpressure sin bloquear el socket. Pero para el caso común —una lectura RF1 cuya respuesta ya está en memoria en el nodo que recibió la solicitud— es enormemente más maquinaria de la que el trabajo merece. La respuesta podría, en principio, computarse y escribirse de vuelta <em>en la propia ruta de recepción</em>, con una copia al buffer del socket y sin entregas en absoluto.</p>
<p>Ese es el primer y mayor trozo de trabajo estructural al que apunta la medición: <strong>colapsar el egreso a un salto y una copia</strong> para la ruta caliente, y ejecutar la lectura/escritura RF1 en línea en el hilo receptor en lugar de rebotarla a través del pool de workers. Esta es la diferencia entre un diseño que es fácil de dibujar y un diseño que es barato de ejecutar, y los dos no son lo mismo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cuello-de-botella-dos-hashing-redundante-y-un-lock-por-solicitud">Cuello de botella dos: hashing redundante y un lock por solicitud<a href="https://clustron.io/es/blog/latency-floor/#cuello-de-botella-dos-hashing-redundante-y-un-lock-por-solicitud" class="hash-link" aria-label="Enlace directo al Cuello de botella dos: hashing redundante y un lock por solicitud" title="Enlace directo al Cuello de botella dos: hashing redundante y un lock por solicitud" translate="no">​</a></h2>
<p>El segundo grupo es sobrecarga fija por operación —trabajo que ocurre en <em>cada</em> solicitud sin importar qué clave toque, y que en su mayoría no debería.</p>
<p><strong>CRC16, tres veces, bit a bit.</strong> Zaris enruta claves a particiones con un hash CRC16 —la misma familia de hash que usa Redis Cluster, que es <a class="" href="https://clustron.io/es/blog/redis-protocol-any-language/">cómo el front-end RESP se mantiene compatible con slots</a>. El problema no es la elección del hash; es que la implementación es <em>bit-serial</em> (iterando bit por bit en lugar de usar una tabla de búsqueda) y que un solo GET lo computa <strong>tres veces</strong> mientras la solicitud pasa por el enrutamiento. A unos 260 nanosegundos por computación, eso son ~784 ns por GET gastados en hashing —más que todo el viaje de ida y vuelta de decodificación-más-codificación de MessagePack, que ronda los 533 ns. Un CRC16 basado en tabla computado <em>una vez</em> y transportado junto convertiría el mayor costo fijo individual en un error de redondeo.</p>
<p><strong>Una comprobación de licencia que toma un lock, en cada solicitud.</strong> Más sorprendente: el punto de entrada de la solicitud consulta el servicio de licencias en cada operación individual, y esa consulta toma un lock de todo el proceso y lee el reloj de pared (<code>DateTime.UtcNow</code>) para decidir si el arrendamiento actual sigue siendo válido. Un lock en la ruta caliente es exactamente el tipo de cosa que no aparece en un microbenchmark de un solo hilo y luego serializa silenciosamente todo bajo concurrencia. El veredicto de la licencia cambia como mucho unas pocas veces a lo largo de la vida de un proceso —cuando un arrendamiento se concede, renueva o expira. No tiene por qué recomputarse bajo un lock en cada GET. El arreglo es publicar el veredicto en un único campo <code>volatile</code> que una renovación en segundo plano actualiza, y dejar que la ruta caliente lo lea sin lock y sin llamada al reloj. (La maquinaria de arrendamiento en sí es el tema del <a class="" href="https://clustron.io/es/blog/licensing-lifecycle/">artículo sobre el ciclo de vida de licencias</a>; el punto aquí es puramente que <em>leer</em> el veredicto debe ser gratis.)</p>
<p>Ninguno de estos es un problema algorítmico profundo. Son el tipo de costo que se acumula cuando cada pieza se escribe correctamente de forma aislada y nadie ha medido todavía cuánto cuesta la suma en la ruta que corre un millón de veces por segundo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cuello-de-botella-tres-el-cliente-gasta-tanto-como-el-servidor">Cuello de botella tres: el cliente gasta tanto como el servidor<a href="https://clustron.io/es/blog/latency-floor/#cuello-de-botella-tres-el-cliente-gasta-tanto-como-el-servidor" class="hash-link" aria-label="Enlace directo al Cuello de botella tres: el cliente gasta tanto como el servidor" title="Enlace directo al Cuello de botella tres: el cliente gasta tanto como el servidor" translate="no">​</a></h2>
<p>Sería conveniente culpar al servidor de todo, pero la ablación dice otra cosa: los benchmarks locales están <strong>limitados por el cliente (client-bound)</strong>, y fijar el cliente solo a núcleos de rendimiento lo llevó de 169K a 299K ops/s. El cliente gasta aproximadamente <strong>9 microsegundos y 1,8 KB por GET</strong>, y los costos riman con los del servidor:</p>
<ul>
<li class=""><strong>Las completaciones rebotan a través del thread pool.</strong> Cada solicitud pendiente se rastrea mediante un <code>TaskCompletionSource</code> configurado para ejecutar su continuación de forma asíncrona, así que cuando llega la respuesta la completación se publica al thread pool en lugar de reanudarse en línea —otro salto de planificación, en cada operación.</li>
<li class=""><strong>Se lee una variable de entorno en cada operación individual.</strong> El cliente llama a <code>Environment.GetEnvironmentVariable("ZARIS_REQUEST_TIMEOUT")</code> <em>por solicitud</em> para decidir su deadline. Leer una variable de entorno recorre el bloque de entorno del proceso; hacerlo un millón de veces por segundo es el mayor frame autoría-de-Zaris en el cliente. Debería leerse una vez al arranque y cachearse.</li>
<li class=""><strong>Un semáforo y un lock de canal por operación</strong>, más asignaciones por solicitud —un id de cliente estampado en cada solicitud, metadatos de entrada solicitados y devueltos incluso cuando el llamador no los pidió.</li>
</ul>
<p>El hilo conductor con el servidor es idéntico: el trabajo de un GET es diminuto, y está rodeado de saltos de planificación, locks y asignaciones que cada uno parecía gratis de forma aislada. La ruta caliente del cliente ya ha empezado a andar por el buen camino —el <a class="" href="https://clustron.io/es/blog/valuetask-hot-path/">paso a <code>ValueTask</code> para las operaciones principales</a> eliminó una asignación por llamada en la completación síncrona— y el resto de la lista es la misma idea aplicada más a fondo: cachea la variable de entorno, completa sin el rebote al thread pool, recibe en el socket con un <code>ValueTask</code>, y deja de asignar cosas que nadie pidió.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="entonces-cuál-es-el-piso-realmente">Entonces, ¿cuál es el piso, realmente?<a href="https://clustron.io/es/blog/latency-floor/#entonces-cu%C3%A1l-es-el-piso-realmente" class="hash-link" aria-label="Enlace directo al Entonces, ¿cuál es el piso, realmente?" title="Enlace directo al Entonces, ¿cuál es el piso, realmente?" translate="no">​</a></h2>
<p>Súmalo y la imagen honesta es esta. En un portátil, la ruta TCP nativa tope alrededor de 300K ops/s no porque el almacén sea lento —puede hacer millones— sino porque cada operación arrastra 10-13 microsegundos de CPU de servidor y ~9 microsegundos de CPU de cliente a través de una cadena de saltos, copias, hashes redundantes y locks. El <em>piso</em> lo fija la plomería, no el trabajo.</p>
<p>El corolario alentador es que el piso está hecho de cosas arreglables, y la estimación de lo que arreglarlas compra es concreta. La lista estructural es:</p>
<ol>
<li class="">Colapsar el egreso a un salto y una copia.</li>
<li class="">Ejecutar la ruta caliente RF1 en línea en el hilo de recepción, o por lotes por recepción.</li>
<li class="">CRC16 basado en tabla, hasheado una vez por operación.</li>
<li class="">Completaciones del cliente con cero asignación y sin rebote al thread pool.</li>
<li class="">Recepción de socket basada en <code>ValueTask</code> en el cliente.</li>
<li class="">Cachear la variable de entorno de timeout de solicitud en lugar de leerla por op.</li>
<li class="">Publicar el veredicto de licencia en un campo <code>volatile</code> —sin lock, sin reloj, en la ruta caliente.</li>
<li class="">Una dieta general de asignaciones: respuestas agrupadas, sin id de cliente por solicitud, sin metadatos no solicitados.</li>
</ol>
<p>Juntas se estima que llevan la CPU de servidor por operación de ~10 a ~3-4 microsegundos —que es la diferencia entre necesitar una docena de núcleos ocupados para un millón de ops/seg y necesitar unos cuatro.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="las-advertencias-honestas">Las advertencias honestas<a href="https://clustron.io/es/blog/latency-floor/#las-advertencias-honestas" class="hash-link" aria-label="Enlace directo al Las advertencias honestas" title="Enlace directo al Las advertencias honestas" translate="no">​</a></h2>
<p>Dos cosas mantienen este artículo veraz en lugar de triunfalista.</p>
<p>Primero, <strong>los números absolutos aquí son números de portátil</strong>, y probablemente son 2-4x bajos frente a hardware de servidor real con máquinas cliente dedicadas. Lo que se transfiere entre entornos no es el throughput absoluto —es la <em>atribución relativa</em>: el almacén no es el cuello de botella, el egreso es el mayor costo del servidor, y el cliente gasta tanto como el servidor. Esas conclusiones son independientes de la máquina; la cifra final de ops/seg no lo es, y la validación real está a escala en <a class="" href="https://clustron.io/es/blog/kubernetes-durability-sizing/">Kubernetes</a>, no en una estación de trabajo.</p>
<p>Segundo, <strong>la replicación cambia el techo.</strong> Todo lo anterior es la historia de RF1 —una sola copia, el caso rápido. Un PUT RF2 tiene que replicar de forma durable a un backup antes de poder confirmar, y esa es una ruta genuinamente distinta y más cara; un objetivo realista para ella es 400-500K por nodo, no un millón. Y cuando <em>no</em> necesitas viajes de ida y vuelta por operación en absoluto, la <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">ruta por lotes</a> amortiza casi todo este costo fijo a lo largo de muchas operaciones y puede alcanzar un millón por nodo hoy. La cifra de un millón de ops de un solo nodo para operaciones individuales es un objetivo que el trabajo estructural anterior pretende alcanzar —no un número que estemos citando como entregado.</p>
<p>Esa distinción es toda la disciplina del trabajo de rendimiento en el que vale la pena confiar: separar lo que la medición <em>prueba</em> (el almacén es rápido, la plomería es el piso, y aquí es a dónde va cada microsegundo) de lo que <em>promete</em> (un número específico tras una lista específica de cambios). Lo primero está hecho y sólido. Lo segundo es ingeniería que ahora podemos hacer con un mapa en lugar de una conjetura —que es la razón entera por la que instrumentas la ruta antes de tocarla.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Rendimiento</category>
            <category>.NET</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Migrar una partición con cero pérdida de datos: el protocolo de cut-over]]></title>
            <link>https://clustron.io/es/blog/partition-cutover-protocol/</link>
            <guid>https://clustron.io/es/blog/partition-cutover-protocol/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Una partición tiene que cambiar de propietario mientras siguen llegando escrituras a ella. Sigue la secuencia exacta que ejecuta Zaris —una instantánea anclada a una revisión, un delta final con escrituras bloqueadas (fenced) condicionado a un digest de contenido, una reconciliación de réplicas en vivo, el giro atómico de cut-over del líder, y un descarte que se niega a destruir la única copia de la fuente a menos que su digest siga coincidiendo con la prueba que recibió el nuevo propietario.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="El protocolo de cut-over de particiones de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNaWdyYXRpbmcgYSBwYXJ0aXRpb24gd2l0aCB6ZXJvIGRhdGEgbG9zczogdGhlIGN1dC1vdmVyIHByb3RvY29sIj4NCiAgPGRlZnM+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMSIgeTI9IjEiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGExNTEyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwZTIxMWIiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZ2xvdyIgY3g9IjAuODIiIGN5PSIwLjIiIHI9IjAuNyI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMC4zMCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L3JhZGlhbEdyYWRpZW50Pg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYWNjZW50IiB4MT0iMCIgeTE9IjEiIHgyPSIwIiB5Mj0iMCI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwZWE2NzIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzJiZDZhMCIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHN0eWxlPi5zYW5ze2ZvbnQtZmFtaWx5OiJTcGFjZSBHcm90ZXNrIiwtYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwiU2Vnb2UgVUkiLFJvYm90byxIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZjt9PC9zdHlsZT4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYmcpIi8+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNnbG93KSIvPg0KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNiIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjYWNjZW50KSIvPg0KICA8dGV4dCB4PSI4MCIgeT0iNzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE3IiBsZXR0ZXItc3BhY2luZz0iNCIgZm9udC13ZWlnaHQ9IjYwMCIgZmlsbD0iIzdmZTNiZiI+TUlHUkFUSU9OPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPk1vdmluZyBhIHBhcnRpdGlvbiw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+bG9zaW5nIG5vIHdyaXRlPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+U25hcHNob3QgJiM4NTk0OyBmZW5jZWQgZmluYWwgZGVsdGEgJiM4NTk0OyBkaWdlc3QgcHJvb2YgJiM4NTk0OyBzYWZlIGRyb3A8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwLDI4NikiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjIyIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjU4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNDciIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zb3VyY2U8L3RleHQ+DQogICAgPHRleHQgeD0iNzUiIHk9IjY4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+c2VydmVzIHVudGlsIGZsaXA8L3RleHQ+DQogICAgPHJlY3QgeD0iODYwIiB5PSIyMiIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI1OCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSI5MzUiIHk9IjQ3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+bmV3IG93bmVyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjkzNSIgeT0iNjgiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5wcm92ZW4gJiM4ODM5OyBzb3VyY2U8L3RleHQ+DQogICAgPHBhdGggZD0iTTE1MCA0MiBMODU4IDQyIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPGNpcmNsZSBjeD0iMzAwIiBjeT0iNDIiIHI9IjciIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8Y2lyY2xlIGN4PSI1MDAiIGN5PSI0MiIgcj0iNyIgZmlsbD0iIzJiZDZhMCIvPg0KICAgIDxjaXJjbGUgY3g9IjcwMCIgY3k9IjQyIiByPSI3IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iMzAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnNuYXBzaG90IEByZXY8L3RleHQ+DQogICAgPHRleHQgeD0iNTAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmZlbmNlICsgZGVsdGE8L3RleHQ+DQogICAgPHRleHQgeD0iNzAwIiB5PSIyMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmRpZ2VzdCBtYXRjaDwvdGV4dD4NCiAgICA8dGV4dCB4PSI1MDAiIHk9IjExMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPmRyb3Agb25seSBpZiBsaXZlIGRpZ2VzdCBzdGlsbCBlcXVhbHMgdGhlIHNlcnZlZCBwcm9vZjwvdGV4dD4NCiAgPC9nPg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>Un <a class="" href="https://clustron.io/es/blog/anatomy-of-a-failover/">failover</a> es lo que ocurre cuando un propietario <em>muere</em> —el clúster tiene segundos para elegir a un superviviente y el mapa de particiones nunca se mueve. La migración es la prima más tranquila y más peligrosa: el propietario está perfectamente sano, y quieres mover una partición fuera de él <em>a propósito</em> —para hacer crecer el clúster, para rebalancear, para drenar un nodo que estás a punto de retirar. Nada ha fallado, que es exactamente por qué el listón está más alto. A un failover se le permiten unas pocas escrituras de cola honestamente perdidas en el peor caso; a una migración planificada no. Si mover una partición del nodo A al nodo B puede perder una sola escritura confirmada, la migración no es una característica, es un pasivo.</p>
<p>Este artículo recorre el protocolo de cut-over que Zaris realmente ejecuta, en orden, y dedica la mayor parte de su tiempo al único paso que todos resuelven mal: el momento en que la fuente finalmente queda "libre" para descartar su copia. Ese paso es un arma cargada, y la ingeniería interesante es el seguro que lleva.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forma-del-problema">La forma del problema<a href="https://clustron.io/es/blog/partition-cutover-protocol/#la-forma-del-problema" class="hash-link" aria-label="Enlace directo al La forma del problema" title="Enlace directo al La forma del problema" translate="no">​</a></h2>
<p>Una partición es un conjunto de segmentos, y un segmento es un saco de registros clave-valor, cada uno con una revisión monótona. La migración mueve los datos de un segmento de un nodo <strong>fuente</strong> a un nodo <strong>destino</strong> y luego gira la propiedad para que los clientes enruten al destino. Todo ello ocurre mientras el segmento está <em>vivo</em>: las lecturas y escrituras siguen llegando todo el tiempo.</p>
<p>Esa vitalidad es el problema. Si haces una instantánea de la fuente, envías los bytes y giras la propiedad, cada escritura que llegó <em>durante</em> el envío está en la fuente y no en el destino —y en el instante en que giras, esas escrituras se desvanecen de la vista enrutada. Las confirmaste; al cliente se le dijo "confirmado"; ahora se han ido. La migración ingenua pierde exactamente las escrituras que ocurrieron mientras estaba ocupada siendo cuidadosa con las que ocurrieron antes.</p>
<p>Así que el protocolo tiene que hacer dos cosas que están en tensión: seguir sirviendo escrituras en la fuente justo hasta el giro, <em>y</em> garantizar que el destino tenga cada una de esas escrituras antes de que el giro surta efecto. La forma en que Zaris reconcilia esa tensión es una secuencia de cinco pasos, cada uno de los cuales se niega a avanzar hasta que el anterior esté demostrablemente completo.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-1--una-instantánea-anclada-a-una-revisión-con-el-piso-de-recorte-fijado">Paso 1 — una instantánea anclada a una revisión, con el piso de recorte fijado<a href="https://clustron.io/es/blog/partition-cutover-protocol/#paso-1--una-instant%C3%A1nea-anclada-a-una-revisi%C3%B3n-con-el-piso-de-recorte-fijado" class="hash-link" aria-label="Enlace directo al Paso 1 — una instantánea anclada a una revisión, con el piso de recorte fijado" title="Enlace directo al Paso 1 — una instantánea anclada a una revisión, con el piso de recorte fijado" translate="no">​</a></h2>
<p>El destino arranca pidiéndole a la fuente los datos del segmento (<code>SegmentMigrationRequest</code>). La fuente no serializa simplemente su diccionario actual; toma una <strong>instantánea anclada a una revisión</strong>. Registra la revisión actual del segmento —llámala <code>R</code>— y captura los registros vivos tal como están en <code>R</code>.</p>
<p>Dos sutilezas viven en este único paso. Primero, la fuente <strong>arma su log de operaciones antes de capturar el ancla</strong>. Zaris puede omitir el log de operaciones para un segmento de primario único (RF1) como optimización de la ruta de escritura; armarlo primero significa que cada escritura desde este punto en adelante se registra, así que una escritura que antes se omitía del log aún tiene una revisión en o por debajo de <code>R</code> y la lleva la propia instantánea. El delta que computamos después —"ops después de <code>R</code>"— es entonces demostrablemente sin agujeros.</p>
<p>Segundo, la fuente <strong>fija el piso de recorte de su log de operaciones en <code>R</code></strong>:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">snapshotRevision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> snapshotEntries</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">CreateSegmentSnapshot</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segmentId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Pin BEFORE streaming so any op the source acks between the snapshot and the</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// new owner's final-delta request is still replayable — the floor cannot advance past R.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">_store</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">PinSegmentTrim</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">segmentId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> snapshotRevision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Normalmente el log de operaciones recorta entradas viejas para recuperar memoria. Durante una migración eso sería un desastre: si el piso avanzara más allá de <code>R</code> mientras la transferencia masiva está en vuelo, las escrituras entre <code>R</code> y el giro serían no reproducibles, y el delta final sería silenciosamente parcial. Fijar el piso en <code>R</code> garantiza que esas escrituras sobrevivan hasta que el destino las tenga. La fuente entonces transmite las entradas de la instantánea —<strong>y sigue sirviendo lecturas y escrituras todo el tiempo.</strong> Sigue siendo el propietario enrutado. Nada se ha movido todavía.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-2--el-destino-espera-deliberadamente">Paso 2 — el destino espera, deliberadamente<a href="https://clustron.io/es/blog/partition-cutover-protocol/#paso-2--el-destino-espera-deliberadamente" class="hash-link" aria-label="Enlace directo al Paso 2 — el destino espera, deliberadamente" title="Enlace directo al Paso 2 — el destino espera, deliberadamente" translate="no">​</a></h2>
<p>El destino aplica la instantánea masiva y luego hace algo que parece inacción pero es el corazón del diseño: mueve el segmento a un estado de <em>espera-de-cut-over</em> y <strong>se detiene</strong>. No acusa recibo a la fuente. No reclama propiedad. No le dice al líder que ha terminado. El comentario en el código es contundente sobre por qué:</p>
<blockquote>
<p>NO Ack to the source (it is still the routed owner), NO ownership claim, and NO completion notify yet — the completion is withheld until the fenced final delta confirms this node holds everything the source acked.</p>
</blockquote>
<p>Este es el contrato "old-owner-authoritative" (propietario-antiguo-autoritativo). Hasta el giro, exactamente un nodo responde por el segmento, y es la fuente. Un destino que se adelantara —empezara a servir desde una instantánea que ya está obsoleta por unos cientos de escrituras— sería un split brain. Así que el destino se mantiene, y pide lo único que le permitirá probar que está al día: el delta final.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-3--el-delta-final-con-escrituras-bloqueadas-y-la-prueba-por-digest">Paso 3 — el delta final con escrituras bloqueadas y la prueba por digest<a href="https://clustron.io/es/blog/partition-cutover-protocol/#paso-3--el-delta-final-con-escrituras-bloqueadas-y-la-prueba-por-digest" class="hash-link" aria-label="Enlace directo al Paso 3 — el delta final con escrituras bloqueadas y la prueba por digest" title="Enlace directo al Paso 3 — el delta final con escrituras bloqueadas y la prueba por digest" translate="no">​</a></h2>
<p>El destino envía un <code>SegmentMigrationFinalDeltaRequest</code> anclado en <code>R</code>. Aquí es donde la fuente hace la parte cuidadosa.</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><strong>Engancha un bloqueo de escritura (write fence)</strong> en el segmento y <em>luego</em> lee la revisión actual. El orden importa: bloquear primero, medir después, para que no haya ventana en la que una escritura se cuele más allá de la medición. Luego lee cada entrada del log de operaciones después de <code>R</code> —el delta de todo lo que llegó durante la transferencia masiva— y computa un <strong>digest de contenido</strong> del estado vivo del segmento. Ese digest es la clave de todo el protocolo, así que la fuente lo registra como la <strong>prueba de cut-over</strong>:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Operations </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> finalOps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ToList</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceDigest </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ComputeSegmentLiveDigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Record this served digest as the cut-over PROOF: the post-cut-over orphan drop will refuse to</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// destroy this segment's data unless its live digest still equals this exact value.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">_localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">RecordCutoverProofDigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Hay un modo de fallo honesto conectado justo aquí. Si el piso de recorte <em>sí</em> avanzara de algún modo más allá de <code>R</code> —una fijación perdida o retrocedida— entonces "ops después de <code>R</code>" estaría incompleto, y enviar ese delta descartaría silenciosamente escrituras confirmadas. La fuente se niega:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">_localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">TryGetSegmentOperationsAfter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> request</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SnapshotRevision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">out</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> finalOps</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Aborted </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AbortReason </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"FinalDeltaBelowTrimFloor"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// release the fence, re-snapshot, retry — a loud abort, never a silent partial cut-over.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Un aborto por-debajo-del-piso no es una pérdida; derriba y re-instantánea desde un ancla fresca. La regla que el código sostiene es <em>aborto ruidoso antes que parcial silencioso</em> —la migración prefiere reiniciar a girar sobre un delta incompleto.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-4--la-compuerta-el-destino-prueba-que-domina">Paso 4 — la compuerta: el destino prueba que domina<a href="https://clustron.io/es/blog/partition-cutover-protocol/#paso-4--la-compuerta-el-destino-prueba-que-domina" class="hash-link" aria-label="Enlace directo al Paso 4 — la compuerta: el destino prueba que domina" title="Enlace directo al Paso 4 — la compuerta: el destino prueba que domina" translate="no">​</a></h2>
<p>El destino aplica el delta, luego recomputa su <em>propio</em> digest de contenido del segmento y lo compara con el de la fuente:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> localDigest </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> _localStore</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ComputeSegmentLiveDigest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> matchesSource </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">localDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">SourceDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> StringComparison</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>Si los dos digests son iguales, el destino tiene byte a byte lo que la fuente tenía en el bloqueo —el estado del destino es un superconjunto de cada escritura que la fuente confirmó. Si difieren, casi siempre es porque la instantánea transmitida todavía se está aplicando cuando se computa el primer delta (la señal de completación puede adelantarse a los lotes de datos encolados), así que el destino se mantiene pendiente y deja que un reintento espaciado re-solicite el delta en lugar de girar en vano. <strong>Un desajuste nunca gira.</strong> El giro está condicionado a una prueba positiva de igualdad, no a la ausencia de un error.</p>
<p>Hay una cosa más que el destino hace antes de declarar la victoria, y es una garantía que la mayoría de las implementaciones de migración omiten. La fuente no es la única copia que podría ir por delante —una <em>réplica</em> del segmento podría tener una escritura que la fuente bloqueada se perdió. Así que antes de completar, el destino pliega la cola de solo lectura de cada réplica viva:</p>
<blockquote>
<p>The flip may fire only when the new owner's digest dominates the source <strong>and every live replica</strong> of the segment.</p>
</blockquote>
<p>El plegado preserva las revisiones: los registros más viejos de una réplica rezagada son ignorados por la compuerta de revisión, y una réplica adelantada aporta exactamente la escritura que la fuente no tenía. Solo cuando el destino domina a la fuente <em>y</em> a cada réplica viva avanza. Este es el invariante "durabilidad antes que propiedad" —la propiedad es una recompensa por la posesión demostrable, no una precondición para ella.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-5--el-giro-y-el-descarte-que-casi-pierde-tus-datos">Paso 5 — el giro, y el descarte que casi pierde tus datos<a href="https://clustron.io/es/blog/partition-cutover-protocol/#paso-5--el-giro-y-el-descarte-que-casi-pierde-tus-datos" class="hash-link" aria-label="Enlace directo al Paso 5 — el giro, y el descarte que casi pierde tus datos" title="Enlace directo al Paso 5 — el giro, y el descarte que casi pierde tus datos" translate="no">​</a></h2>
<p>Ahora el destino le dice al líder que ha terminado (<code>SegmentMigrationCompletionNotify</code>). El líder valida la completación contra el override de migración vivo y arma una <strong>regeneración atómica de cut-over</strong> del mapa de particiones —un único cambio de mapa versionado que gira el propietario del segmento al destino y elimina el override de migración. Después de que ese mapa se aplica, el destino es el propietario enrutado. Los clientes enrutan a él. El movimiento está, en lo que respecta al clúster, hecho.</p>
<p>Y la fuente, ahora sosteniendo un segmento para el que ya no posee un rol, ejecuta un barrido de limpieza para descartar la copia huérfana y recuperar la memoria. Esta es el arma cargada.</p>
<p>Aquí está la trampa, y es lo bastante sutil como para haber sobrevivido a una corrida de certificación antes de ser atrapada (<code>Missing=8629</code> claves confirmadas desaparecidas). Un bloqueo de escritura <strong>reduce</strong> la confirmación de escrituras pero, en un segmento de copia única bajo carga, no lo aquieta perfectamente a cero de forma instantánea. Así que esta secuencia es posible:</p>
<ol>
<li class="">La fuente sirve el digest del delta final como la prueba. El destino lo iguala y gira.</li>
<li class="">En la rendija entre servir la prueba y el aterrizaje del giro, se confirma una escritura más en la fuente.</li>
<li class="">El destino nunca recibió esa escritura —la prueba que igualó era de <em>antes</em> de ella.</li>
<li class="">La fuente, ahora huérfana, descarta su copia para recuperar espacio.</li>
<li class="">Esa última escritura existía solo en la fuente. Se ha ido. A un cliente se le dijo que se confirmó.</li>
</ol>
<p>El arreglo es hacer el descarte <em>condicional a que la prueba siga siendo verdadera</em>. La fuente no destruye ciegamente un segmento huérfano. Compara el digest vivo <strong>actual</strong> del segmento con el digest que registró como la prueba de cut-over, y solo destruye los datos si siguen coincidiendo:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">static</span><span class="token plain"> </span><span class="token return-type class-name keyword" style="color:#00009f">bool</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ShouldRetainAfterCutover</span><span class="token punctuation" style="color:#393A34">(</span><span class="token class-name keyword" style="color:#00009f">bool</span><span class="token plain"> hasCutoverProof</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token class-name punctuation" style="color:#393A34">?</span><span class="token plain"> currentLiveDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token class-name keyword" style="color:#00009f">string</span><span class="token class-name punctuation" style="color:#393A34">?</span><span class="token plain"> proofDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">hasCutoverProof </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">IsNullOrEmpty</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">proofDigest</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// not a proof-carrying cut-over segment -&gt; this policy does not intervene</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">currentLiveDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> proofDigest</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> System</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">StringComparison</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Si una escritura llegó después de la prueba, el digest actual difiere del digest de la prueba, <code>ShouldRetainAfterCutover</code> devuelve <code>true</code>, y la fuente <strong>conserva</strong> su copia:</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>La asimetría es todo el punto. <strong>La retención siempre es segura</strong>: la fuente ya no es el propietario enrutado, así que una copia conservada es un huérfano redundante y no servido —nunca puede causar un split brain, solo cuesta un poco de memoria hasta que un re-servicio re-entrega la cola al nuevo propietario y un barrido posterior, al encontrar que los digests coinciden de nuevo, la descarta limpiamente. <strong>La destrucción sobre una prueba obsoleta nunca es segura</strong>: es la única operación que puede borrar permanentemente una escritura confirmada. Así que la política es deliberadamente estrecha —solo <em>anula un descarte que de otro modo ocurriría</em>, y solo para un segmento que lleva una prueba registrada cuyo digest ya no coincide. Los segmentos sin prueba (una expulsión, un giro inmediato, un segmento nunca migrado) conservan su comportamiento existente intacto. La compuerta añade una retención; nunca añade un descarte.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-el-digest-y-no-un-número-de-revisión">¿Por qué el digest, y no un número de revisión?<a href="https://clustron.io/es/blog/partition-cutover-protocol/#por-qu%C3%A9-el-digest-y-no-un-n%C3%BAmero-de-revisi%C3%B3n" class="hash-link" aria-label="Enlace directo al ¿Por qué el digest, y no un número de revisión?" title="Enlace directo al ¿Por qué el digest, y no un número de revisión?" translate="no">​</a></h2>
<p>Podrías preguntar por qué la prueba es un digest de contenido y no simplemente "la revisión más alta que el destino confirmó". La respuesta es que un número de revisión te dice <em>qué tan lejos</em> llegó el destino, pero un digest te dice <em>si lo que obtuvo es lo que tú tenías</em>. Los dos pueden divergir en las costuras —un plegado de réplica, un reintento re-anclado, una instantánea aún drenándose— y la única pregunta que mantiene los datos seguros es la pregunta de contenido: <em>¿tiene el nuevo propietario exactamente esto?</em> Un digest responde eso directamente. Una marca de agua solo responde un proxy de ello, y los proxies son donde las escrituras confirmadas van a morir.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-compra-esto-honestamente">Qué compra esto, honestamente<a href="https://clustron.io/es/blog/partition-cutover-protocol/#qu%C3%A9-compra-esto-honestamente" class="hash-link" aria-label="Enlace directo al Qué compra esto, honestamente" title="Enlace directo al Qué compra esto, honestamente" translate="no">​</a></h2>
<p>Enunciado sin rodeos, la garantía es: <strong>una migración planificada de partición no pierde una escritura confirmada.</strong> Cada escritura que la fuente confirmó antes del bloqueo está en la instantánea o el delta; cada escritura que confirmó <em>después</em> de la prueba mantiene viva la copia de la fuente hasta que el nuevo propietario también la tenga. El protocolo abortará y reiniciará ruidosamente en lugar de girar sobre un delta incompleto, y pliega las réplicas vivas para que el nuevo propietario domine cada copia, no solo la fuente.</p>
<p>Lo que <em>no</em> es, es gratis. El delta final bloqueado es un viaje de ida y vuelta síncrono que brevemente bloquea las escrituras en el segmento que se está traspasando —una pausa corta y acotada en un segmento, no la partición y no el clúster. Una migración que no puede converger (un piso de recorte que sigue avanzando, una cola de réplica que no puede expresarse desde su log) re-instantánea y reintenta en lugar de forzar el progreso, lo que bajo churn patológico significa que una migración tarda más en lugar de tomar un atajo a través de tus datos. Esos son los compromisos correctos para una operación planificada: un movimiento que elegiste hacer nunca debería ser la razón por la que una escritura confirmada desaparece.</p>
<p>Esta es la misma columna vertebral que la <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">certificación de caos de doce horas</a> ejercita desde el exterior —detén un nodo a mitad de migración, agita el mapa, y observa cómo las escrituras confirmadas sobreviven. Este artículo es la vista interna de una de las costuras que esa corrida demuestra cerradas: el momento en que una partición cambia de manos, y el seguro del gatillo que evita que el traspaso se coma las mismísimas escrituras que fue construido para proteger. Se apoya en otras dos piezas que vale la pena leer por su cuenta —el <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">log de operaciones reproducible</a> que hace de "ops después de la revisión R" una consulta real y sin agujeros, y el <a class="" href="https://clustron.io/es/blog/elastic-scaling/">escalado elástico</a> que es la razón más común por la que una partición necesita moverse siquiera.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Escanear un espacio de claves que no se queda quieto: cómo SCAN itera un clúster]]></title>
            <link>https://clustron.io/es/blog/scanning-a-moving-keyspace/</link>
            <guid>https://clustron.io/es/blog/scanning-a-moving-keyspace/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un SCAN completo en Zaris tiene que recorrer un espacio de claves dividido en 256 segmentos sobre varios nodos, mantenido ordenado en ninguna parte globalmente, mutando mientras se lee. Así es como funciona realmente esa iteración —una instantánea ordenada sin locks por segmento, una fusión k-vías en un único stream de páginas ordenado por nodo, un token de reanudación ligado a una sesión del lado del servidor, y un fan-out del cliente con un tope total duro. Más un recuento honesto de lo que un SCAN promete y no promete.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Muchos segmentos ordenados fusionados en un único stream paginado y ordenado" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJTY2FubmluZyBhIGtleXNwYWNlIHRoYXQgd29uJ3QgaG9sZCBzdGlsbCDigJQgc2VnbWVudHMgbWVyZ2VkIGludG8gb25lIG9yZGVyZWQsIHBhZ2VkIHN0cmVhbSI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPklURVJBVElPTiAmYW1wOyBDVVJTT1JTPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPlNjYW5uaW5nIGEga2V5c3BhY2U8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+dGhhdCB3b24ndCBob2xkIHN0aWxsPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+TWFueSBzb3J0ZWQgc2VnbWVudHMsIG1lcmdlZCBpbnRvIG9uZSBvcmRlcmVkLCBwYWdlZCBzdHJlYW08L3RleHQ+DQoNCiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODAsMjgwKSI+DQogICAgPCEtLSBzZWdtZW50cyAtLT4NCiAgICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiI+DQogICAgICA8cmVjdCB4PSIwIiB5PSI2IiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjM0IiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iMCIgeT0iNTIiIHdpZHRoPSIxNTAiIGhlaWdodD0iMzQiIHJ4PSI2Ii8+DQogICAgICA8cmVjdCB4PSIwIiB5PSI5OCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSIzNCIgcng9IjYiLz4NCiAgICA8L2c+DQogICAgPHRleHQgeD0iNzUiIHk9IjI5IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+c2VnbWVudCAwNzwvdGV4dD4NCiAgICA8dGV4dCB4PSI3NSIgeT0iNzUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zZWdtZW50IDQyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijc1IiB5PSIxMjEiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zZWdtZW50IDE5ODwvdGV4dD4NCg0KICAgIDwhLS0gbWVyZ2UgYXJyb3dzIC0tPg0KICAgIDxwYXRoIGQ9Ik0xNTAgMjMgTDI1MCA2NiIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQogICAgPHBhdGggZD0iTTE1MCA2OSBMMjUwIDY5IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8cGF0aCBkPSJNMTUwIDExNSBMMjUwIDcyIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCg0KICAgIDwhLS0gbWVyZ2Ugbm9kZSAtLT4NCiAgICA8cmVjdCB4PSIyNTAiIHk9IjQ0IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQogICAgPHRleHQgeD0iMzE0IiB5PSI2NiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmstd2F5IG1lcmdlPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjMxNCIgeT0iODQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5vcmRpbmFsIG9yZGVyPC90ZXh0Pg0KDQogICAgPCEtLSBvcmRlcmVkIHN0cmVhbSAtLT4NCiAgICA8ZyBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiPg0KICAgICAgPHJlY3QgeD0iNDA2IiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iNDgwIiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iNTU0IiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgICAgPHJlY3QgeD0iNjI4IiB5PSI0OCIgd2lkdGg9IjY0IiBoZWlnaHQ9IjQyIiByeD0iNiIvPg0KICAgIDwvZz4NCiAgICA8dGV4dCB4PSI0MzgiIHk9Ijc0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+YToxPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjUxMiIgeT0iNzQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmZmZmIj5hOjI8L3RleHQ+DQogICAgPHRleHQgeD0iNTg2IiB5PSI3NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmI6OTwvdGV4dD4NCiAgICA8dGV4dCB4PSI2NjAiIHk9Ijc0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+YzozPC90ZXh0Pg0KDQogICAgPCEtLSBwYWdlIGN1cnNvciAtLT4NCiAgICA8cGF0aCBkPSJNNDA2IDExMiBMNjE4IDExMiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8cGF0aCBkPSJNNDA2IDEwOCBMNDA2IDExNiBNNjE4IDEwOCBMNjE4IDExNiIgc3Ryb2tlPSIjMmJkNmEwIiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI1MTIiIHk9IjEzMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPnBhZ2UgKHJlc3VtZSB0b2tlbik8L3RleHQ+DQoNCiAgICA8IS0tIHRhaWwgYXJyb3cgdG8gY2xpZW50IC0tPg0KICAgIDxwYXRoIGQ9Ik03MDAgNjkgTDc5MiA2OSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iNiIvPg0KICAgIDxwYXRoIGQ9Ik03ODQgNjEgTDgwMCA2OSBMNzg0IDc3IFoiIGZpbGw9IiMyYmQ2YTAiLz4NCiAgICA8cmVjdCB4PSI4MTIiIHk9IjQ0IiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI4ODciIHk9IjY3IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+Y2xpZW50PC90ZXh0Pg0KICAgIDx0ZXh0IHg9Ijg4NyIgeT0iODUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5pbnRlcmxlYXZlcyBvd25lcnM8L3RleHQ+DQogIDwvZz4NCg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>Un <code>GET</code> de clave única es fácil de razonar: haz hash de la clave, encuentra su propietario, pregunta a ese único nodo. <code>SCAN</code> es el tipo de operación opuesto. No tiene clave —hace una pregunta <em>sobre todo el espacio de claves</em>: dame cada clave en este rango, una página a la vez. Y en un almacén particionado y replicado ese espacio de claves no es una sola cosa en un solo lugar. Está dividido en 256 segmentos repartidos sobre cuantos nodos ejecutes, cada nodo sosteniendo solo las porciones que posee actualmente, y todo el conjunto se está escribiendo mientras iteras. No hay índice ordenado global que recorrer. Entonces, ¿de dónde viene el orden, a qué apunta exactamente un token de reanudación, y qué le pasa a tu escaneo cuando una clave se escribe —o una partición se mueve— a mitad de camino?</p>
<p>Este artículo sigue un <code>SCAN</code> a través de las tres capas que realmente lo sirven: un solo segmento, un solo nodo, y el cliente que cose los nodos entre sí. Cada capa resuelve una pieza del problema, y los límites honestos de la operación caen de cómo esas piezas encajan.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-solicitud-un-rango-y-un-tamaño-de-página-no-una-clave">La solicitud: un rango y un tamaño de página, no una clave<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#la-solicitud-un-rango-y-un-tama%C3%B1o-de-p%C3%A1gina-no-una-clave" class="hash-link" aria-label="Enlace directo al La solicitud: un rango y un tamaño de página, no una clave" title="Enlace directo al La solicitud: un rango y un tamaño de página, no una clave" translate="no">​</a></h2>
<p>Un escaneo se describe con dos objetos pequeños. Un <code>KeyRange</code> dice <em>qué</em> claves —un inicio, un fin o un prefijo, cualquiera de ellos opcional:</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>Y <code>ScanOptions</code> dice <em>cómo</em> entregarlas —qué tan grande una página, si enviar valores o solo claves, un límite duro opcional, y un token de reanudación para continuar una llamada previa:</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>Nada aquí nombra un nodo o un segmento. Ese es el punto: el llamador describe <em>lo que quiere</em>, y las tres capas de abajo calculan dónde vive y en qué orden entregarlo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="capa-uno-un-segmento-se-escanea-a-sí-mismo-sin-locks-y-en-orden">Capa uno: un segmento se escanea a sí mismo, sin locks y en orden<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#capa-uno-un-segmento-se-escanea-a-s%C3%AD-mismo-sin-locks-y-en-orden" class="hash-link" aria-label="Enlace directo al Capa uno: un segmento se escanea a sí mismo, sin locks y en orden" title="Enlace directo al Capa uno: un segmento se escanea a sí mismo, sin locks y en orden" translate="no">​</a></h2>
<p>La unidad más pequeña que puede responder parte de un escaneo es un segmento —la unidad de almacenamiento particionado de Zaris, respaldada por un <code>ConcurrentDictionary</code> de registros vivos. Un diccionario no tiene orden, así que lo primero que un escaneo de segmento necesita es una vista ordenada de sus claves. Construir eso en cada página sería ruinoso, así que cada segmento mantiene una <strong>instantánea de claves ordenadas, cacheada e inmutable</strong> y la reconstruye solo cuando el conjunto de claves cambia:</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>El arreglo se asigna una vez y nunca se muta; una reconstrucción concurrente simplemente produce un nuevo arreglo e intercambia la referencia. Eso significa que el recorrido del escaneo en sí no toma <strong>ningún lock en absoluto</strong> —hace búsqueda binaria hasta la clave de inicio y avanza en orden ordinal, leyendo registros vivos del diccionario concurrente a medida que avanza. Las lecturas y escrituras al segmento nunca bloquean el escaneo, y el escaneo nunca las bloquea a ellas.</p>
<p>Dos tipos de clave se pasan por alto silenciosamente mientras avanza:</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>Una clave eliminada es una <em>tombstone</em> (lápida) —un registro real marcado como <code>IsDeleted</code>, porque <a class="" href="https://clustron.io/es/blog/what-crosses-the-wire/">una eliminación tiene que ser un registro, no una ausencia</a>, para que la replicación la lleve. El escaneo las filtra, junto con las claves cuyo TTL ha transcurrido pero cuyo barredor (sweeper) todavía no se ha disparado. Así que la instantánea de claves ordenadas es un superconjunto de lo que verás; la expiración y la eliminación se resuelven contra el registro <em>vivo</em> en el momento de lectura, no se congelan en la instantánea.</p>
<p>El recorrido se detiene en <code>PageSize</code> y devuelve un <code>ResumeToken</code>. Aquí está el primer detalle honesto: a nivel de segmento, el token es esencialmente un <strong>offset</strong> —un conteo de cuántas entradas coincidentes ya se devolvieron (<code>ReturnedSoFar</code>). Un escaneo de segmento reanudado vuelve a hacer búsqueda binaria hasta la misma clave de inicio y omite esa cantidad de coincidencias antes de emitir. Es una posición-por-conteo, no una búsqueda de posición-por-clave.</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>Esa elección importa para lo que la operación promete, y volveremos a ella.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="capa-dos-un-nodo-fusiona-sus-segmentos-en-un-único-stream-ordenado">Capa dos: un nodo fusiona sus segmentos en un único stream ordenado<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#capa-dos-un-nodo-fusiona-sus-segmentos-en-un-%C3%BAnico-stream-ordenado" class="hash-link" aria-label="Enlace directo al Capa dos: un nodo fusiona sus segmentos en un único stream ordenado" title="Enlace directo al Capa dos: un nodo fusiona sus segmentos en un único stream ordenado" translate="no">​</a></h2>
<p>Un nodo posee muchos segmentos —no los 256, solo los mapeados a las particiones que sirve actualmente. Un escaneo a nivel de nodo tiene que presentar <em>todos</em> ellos como un único resultado ordenado, porque el llamador preguntó por un rango, no por el segmento 42. Este es un problema clásico de fusión: cada segmento puede producir sus claves en orden ordenado, y quieres la unión ordenada.</p>
<p>Así que la primera página de un escaneo de nodo hace exactamente eso. Reúne un fetcher para cada segmento poseído localmente y ejecuta una <strong>fusión k-vías</strong> sobre ellos con una cola de prioridad, comparando claves ordinalmente:</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>El resultado fusionado y ordenado se materializa en un buffer sostenido por un <strong><code>RouterQueryCoordinator</code></strong> —una sesión del lado del servidor de corta vida registrada bajo un <code>QueryId</code> recién acuñado:</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>Cada página subsiguiente es solo una porción de ese buffer, y el token de reanudación del nodo lleva el <code>QueryId</code> para que la siguiente llamada encuentre la misma sesión:</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>Este es el segundo detalle honesto, y es un compromiso real. Un escaneo a nivel de nodo es <strong>con estado (stateful)</strong>: la paginación es estable y el orden por nodo es exacto, pero el precio es que el nodo materializa todo su espacio de claves poseído y fusionado en un buffer cuando el escaneo comienza, y sostiene una sesión durante toda la vida de la iteración. Los escaneos son una herramienta administrativa y de iteración masiva, no un cursor de streaming de memoria constante sobre miles de millones de claves —y un token de reanudación solo es válido contra el nodo y la sesión que lo acuñaron. Deja que esa sesión expire, o intenta reanudarla en otro lugar, y obtienes un ruidoso "session not found" en lugar de una respuesta silenciosamente errónea.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="capa-tres-el-cliente-hace-fan-out-e-intercala-a-los-propietarios">Capa tres: el cliente hace fan-out e intercala a los propietarios<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#capa-tres-el-cliente-hace-fan-out-e-intercala-a-los-propietarios" class="hash-link" aria-label="Enlace directo al Capa tres: el cliente hace fan-out e intercala a los propietarios" title="Enlace directo al Capa tres: el cliente hace fan-out e intercala a los propietarios" translate="no">​</a></h2>
<p>Un nodo responde por los segmentos que posee. Un escaneo de clúster tiene que cubrir a <em>cada</em> propietario, así que el <code>ScanClusterClient</code> del lado del cliente abre un escaneo contra cada nodo en paralelo e intercala sus páginas a medida que llegan, en lugar de drenar un nodo antes de empezar el siguiente:</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>Ese paralelismo es por qué <code>LimitCount</code> necesita cuidado. Cada nodo trata el límite como un tope <em>por nodo</em>; si el cliente no lo impusiera también, un <code>LimitCount</code> de 1.000 contra cuatro propietarios podría devolver hasta 4.000 filas una vez fusionadas. Así que la capa de clúster aplica el límite como un <strong>tope total duro</strong> y deja de entregar una vez que se alcanza —mientras sigue drenando la página en vuelo de cada propietario hasta completarla, porque desechar un enumerador asíncrono a mitad de movimiento no está permitido y los resultados por nodo ya están acotados de todos modos:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">emitted </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> limit</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">yield</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> enumerator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Current</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    emitted</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// keep draining siblings even past the cap; do not yield-break mid-move</span><br></span></code></pre></div></div>
<p>La consecuencia a dejar clara: dentro de la contribución de cualquier nodo, las claves llegan en orden; <strong>entre</strong> nodos se intercalan por llegada, así que un escaneo de clúster completo <em>no</em> está ordenado globalmente a menos que lo pidas explícitamente. (La ruta relacionada <code>Search</code> sí lleva <code>SortFields</code> y fusiona con un comparador exactamente por esa razón.) Si necesitas cada clave del clúster en orden ordinal, ordena el resultado ensamblado; <code>SCAN</code> en sí garantiza el orden por propietario y la completitud, no un orden global.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-promete-y-qué-no-promete-un-scan">Qué promete —y qué no promete— un SCAN<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#qu%C3%A9-promete-y-qu%C3%A9-no-promete-un-scan" class="hash-link" aria-label="Enlace directo al Qué promete —y qué no promete— un SCAN" title="Enlace directo al Qué promete —y qué no promete— un SCAN" translate="no">​</a></h2>
<p>Junta las tres capas y el contrato real de la operación es visible, sin que nada de él esté oculto:</p>
<ul>
<li class=""><strong>No es una instantánea puntual del clúster.</strong> Cada nodo materializa <em>su propio</em> buffer fusionado cuando <em>su</em> escaneo comienza, y esos momentos no coinciden. Las escrituras que llegan a un nodo antes de que su buffer se construya se incluyen; las posteriores no. No hay congelación de todo el clúster.</li>
<li class=""><strong>Las claves eliminadas y expiradas nunca aparecen.</strong> Las tombstones y los registros con TTL transcurrido se filtran contra el registro vivo a medida que se recorre el segmento, así que un escaneo refleja el contenido lógico, no el físico.</li>
<li class=""><strong>La reanudación es posición-por-conteo dentro de una sesión.</strong> Como el cursor por segmento es un offset, una clave insertada o eliminada <em>antes</em> de la posición actual entre dos páginas puede desplazar la frontera —un escaneo concurrente con mutación intensa del rango escaneado puede, en principio, repetir u omitir una clave alrededor de la costura. Un escaneo de un rango inactivo es exacto. Esta es la misma familia de garantía que da <code>SCAN</code> de Redis —una iteración completa de mejor esfuerzo, no un nivel de aislamiento de instantánea— y es el compromiso correcto para un almacén no ordenado y escrito concurrentemente.</li>
<li class=""><strong>El espacio de claves puede moverse bajo tus pies.</strong> El cliente hace fan-out a los propietarios que existen cuando el escaneo comienza. Las particiones pueden <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">migrar</a> o <a class="" href="https://clustron.io/es/blog/anatomy-of-a-failover/">hacer failover</a> a mitad de iteración; un escaneo es un barrido de mejor esfuerzo de un clúster vivo, que es por qué es la herramienta para exportación masiva, reindexación y consultas administrativas en lugar de para lecturas transaccionales. Para read-your-writes sobre una sola clave, <a class="" href="https://clustron.io/es/blog/reading-your-own-writes/">enruta al propietario</a> en su lugar.</li>
</ul>
<p>Ninguno de estos es un bug que empapelar; son la forma honesta de iterar un espacio de claves no ordenado, fragmentado y mutante sin detener el mundo. El diseño hace que el caso barato y común sea rápido —una instantánea ordenada sin locks y una fusión por nodo— y es explícito sobre los bordes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="una-nota-sobre-el-front-end-resp">Una nota sobre el front-end RESP<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#una-nota-sobre-el-front-end-resp" class="hash-link" aria-label="Enlace directo al Una nota sobre el front-end RESP" title="Enlace directo al Una nota sobre el front-end RESP" translate="no">​</a></h2>
<p>Los clientes de Redis alcanzan la misma maquinaria a través de <code>KEYS</code> y <code>SCAN</code> en el <a class="" href="https://clustron.io/es/blog/redis-protocol-any-language/">front-end RESP</a>, que se mapean sobre el escaneo nativo. Una advertencia que vale la pena declarar sin rodeos: el <code>SCAN</code> de RESP hoy es de un solo disparo —drena el escaneo nativo y responde con el cursor <code>0</code> (iteración completa) en un viaje de ida y vuelta, en lugar de enhebrar el cursor incremental de Redis a través de cada llamada. Es honesto sobre la completitud, no todavía incremental. El cliente nativo es el que pagina con un token de reanudación.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="una-operación-tres-capas-honestas">Una operación, tres capas honestas<a href="https://clustron.io/es/blog/scanning-a-moving-keyspace/#una-operaci%C3%B3n-tres-capas-honestas" class="hash-link" aria-label="Enlace directo al Una operación, tres capas honestas" title="Enlace directo al Una operación, tres capas honestas" translate="no">​</a></h2>
<p>Un <code>GET</code> toca un propietario y una clave; un <code>SCAN</code> toca a cada propietario y pide a cada uno que produzca su porción de un todo ordenado. El trabajo se divide limpiamente: un segmento convierte un diccionario no ordenado en un recorrido ordenado y sin locks; un nodo fusiona sus segmentos en un único stream ordenado y paginado de forma estable detrás de un cursor de sesión; el cliente hace fan-out entre propietarios, intercala sus páginas, e impone el único tope que el fan-out de otro modo rompería. Cada capa es simple por sí sola, y las garantías de la operación —orden por propietario, completitud sobre un rango inactivo, sin aislamiento de instantánea, sin congelación global— son exactamente lo que esas tres capas simples pueden entregar honestamente juntas.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[El techo de memoria: por qué un nodo de Zaris nunca crece sin límite]]></title>
            <link>https://clustron.io/es/blog/the-memory-ceiling/</link>
            <guid>https://clustron.io/es/blog/the-memory-ceiling/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nodo de Zaris es un almacén en memoria, pero no uno ilimitado. Cada nodo es un buffer acotado con un techo de memoria, una caché LRU siempre activa por debajo, y una banda de histéresis que evita que la expulsión se descontrole en el borde. Aquí está exactamente lo que ocurre a medida que un nodo se llena —y los dos límites muy distintos que una escritura puede alcanzar.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Un nodo de Zaris manteniendo el uso de memoria en una banda tranquila por debajo de su techo, expulsando al 80% hasta el 70% para que las escrituras nunca crucen el límite" 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>Aquí hay una pregunta que suena simple y no lo es: ¿qué ocurre cuando un almacén en memoria se queda sin memoria?</p>
<p>La respuesta ingenua es "se cae" —el proceso trepa hasta que el sistema operativo lo mata. La respuesta un poco menos ingenua es "rechaza escrituras". Zaris no hace ninguna de las dos por defecto. Un nodo de Zaris es un almacén en memoria, pero deliberadamente <em>no</em> es uno ilimitado: cada nodo es un <strong>buffer acotado</strong> con un techo duro, y cuando los datos que posee se acercan a ese techo el nodo expulsa sus datos más fríos para hacer espacio. Las escrituras siguen teniendo éxito; el límite se sostiene; el proceso no se hincha. El nodo se comporta como una caché, porque por debajo, lo es.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cada-nodo-es-un-buffer-acotado">Cada nodo es un buffer acotado<a href="https://clustron.io/es/blog/the-memory-ceiling/#cada-nodo-es-un-buffer-acotado" class="hash-link" aria-label="Enlace directo al Cada nodo es un buffer acotado" title="Enlace directo al Cada nodo es un buffer acotado" translate="no">​</a></h2>
<p>Un nodo de Zaris no crece sin límite. Cada nodo contabiliza continuamente el tamaño de los datos que posee activamente y mantiene ese total corriente por debajo de un <strong>techo</strong> configurado. La contabilidad nunca es opcional —un nodo es <em>siempre</em> un buffer acotado. Lo único que configuras es qué tan grande es el techo y, en principio, qué hace el nodo cuando lo alcanza.</p>
<p>El techo por defecto es <strong>1 GiB</strong> de datos poseídos activamente por nodo. No hay, por diseño, un ajuste "ilimitado". Si no estableces un techo, obtienes el valor por defecto —nunca obtienes accidentalmente el infinito.</p>
<p>Una sutileza importa para cómo razonas sobre la huella de un clúster: el techo cubre los datos que un nodo <strong>posee activamente</strong> —las copias primarias que sirve a los clientes. Las copias de réplica que un nodo sostiene para <em>otras</em> particiones se rastrean por separado y no cuentan contra el techo de propietario activo. Así que la memoria residente real de un nodo es su working set poseído más cualesquiera copias de réplica que lleve para sus pares; el techo gobierna lo primero, no la suma.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dos-formas-de-mantener-la-línea">Dos formas de mantener la línea<a href="https://clustron.io/es/blog/the-memory-ceiling/#dos-formas-de-mantener-la-l%C3%ADnea" class="hash-link" aria-label="Enlace directo al Dos formas de mantener la línea" title="Enlace directo al Dos formas de mantener la línea" translate="no">​</a></h2>
<p>Hay exactamente dos cosas que un buffer acotado puede hacer cuando una escritura lo empujaría más allá de su techo: descartar algo viejo para hacer espacio, o rechazar la nueva escritura.</p>
<table><thead><tr><th>Modo</th><th>Comportamiento en el techo</th><th>Una escritura que lo excedería</th></tr></thead><tbody><tr><td>Expulsión habilitada (por defecto)</td><td>Expulsa datos más viejos para hacer espacio</td><td>Admitida — se descartan los datos más fríos</td></tr><tr><td>Expulsión deshabilitada</td><td>Rechaza datos nuevos</td><td>Rechazada con <code>KvStatus.CapacityExceeded</code></td></tr></tbody></table>
<p>Con la expulsión activada, un nodo es una caché: bajo presión descarta los datos menos valiosos para que las escrituras sigan fluyendo. Con la expulsión desactivada, es un almacén de rechazo-en-capacidad que protege los datos existentes rechazando las nuevas escrituras.</p>
<p>Aquí está la parte honesta. En la versión actual un nodo <em>siempre</em> se ejecuta con la expulsión habilitada, usando la política LRU. El modo sin-expulsión y las políticas alternativas (LFU, consciente de TTL) existen en el motor, pero todavía no son seleccionables mediante configuración. La consecuencia práctica es que <code>CapacityExceeded</code> —el estado de rechazo-en-capacidad— es esencialmente inalcanzable hoy: un nodo lleno expulsa en lugar de rechazar. Es un estado real y tipado contra el que <em>puedes</em> escribir código, pero bajo la política LRU siempre activa normalmente no lo verás. La única perilla de memoria que realmente controlas ahora mismo es el tamaño del propio techo, <code>memory.maxSizeBytes</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-corrida-de-expulsión-monta-una-banda-tranquila">La corrida de expulsión monta una banda tranquila<a href="https://clustron.io/es/blog/the-memory-ceiling/#la-corrida-de-expulsi%C3%B3n-monta-una-banda-tranquila" class="hash-link" aria-label="Enlace directo al La corrida de expulsión monta una banda tranquila" title="Enlace directo al La corrida de expulsión monta una banda tranquila" translate="no">​</a></h2>
<p>La ingeniería interesante no es <em>que</em> un nodo expulse —es <em>cómo</em>, porque un diseño ingenuo de "expulsar en cada escritura una vez que estás lleno" se descontrola miserablemente. Imagina un nodo sentado exactamente en su techo: cada escritura entrante dispararía una expulsión para hacer espacio para sí misma, y pagarías ese costo en la ruta caliente para siempre.</p>
<p>Zaris evita eso con dos movimientos. Primero, la expulsión la impulsa un <strong>bucle en segundo plano</strong>, no la escritura individual —la escritura que empuja a un nodo por encima del borde se admite, y el bucle recupera espacio poco después, fuera de la ruta de solicitud. Segundo, el bucle usa <strong>histéresis</strong>: un disparador alto y un objetivo más bajo, de modo que recupera en ráfagas y luego se queda tranquilo en lugar de dispararse continuamente en el borde.</p>
<ul>
<li class=""><strong>Disparador:</strong> una corrida comienza cuando el uso activo sube por encima del <strong>80%</strong> del techo.</li>
<li class=""><strong>Objetivo:</strong> una vez iniciada, la corrida sigue expulsando hasta que aproximadamente el <strong>30%</strong> del techo esté libre de nuevo —hasta que el uso caiga de vuelta a alrededor del <strong>70%</strong>.</li>
<li class=""><strong>Cadencia:</strong> el bucle despierta en un intervalo fijo (un segundo por defecto) y muestrea un conjunto acotado de ítems candidatos cada ciclo.</li>
</ul>
<p>Como el objetivo post-expulsión (70%) se sitúa estrictamente <em>por debajo</em> del disparador (80%), cada corrida deja caer el uso a una banda tranquila en lugar de detenerse justo en el punto que la re-dispararía de inmediato. Ese es el diente de sierra de la imagen principal: el uso trepa al 80%, una corrida lo limpia de vuelta al 70%, y trepa de nuevo —nunca presionado contra el techo, nunca descontrolándose con una expulsión por escritura.</p>
<!-- -->
<p>La política de expulsión en sí es <strong>LRU</strong> —least recently used (menos usado recientemente)— y es <em>muestreada</em>, no estricta. En lugar de mantener una lista ordenada globalmente de cada clave (que costaría memoria y contención de locks en la ruta caliente), Zaris estampa señales aproximadas de recencia en el lugar, en la ruta de lectura y, cuando una corrida se dispara, muestrea un pequeño conjunto de candidatos y descarta los de menor puntuación. Es el mismo enfoque muestreado que usa Redis, y el mismo compromiso: renuncias al orden perfecto de "expulsar la única clave más vieja" a cambio de una expulsión que cuesta casi nada mantener y nunca necesita un índice separado.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-otro-techo-tamaño-por-ítem">El otro techo: tamaño por ítem<a href="https://clustron.io/es/blog/the-memory-ceiling/#el-otro-techo-tama%C3%B1o-por-%C3%ADtem" class="hash-link" aria-label="Enlace directo al El otro techo: tamaño por ítem" title="Enlace directo al El otro techo: tamaño por ítem" translate="no">​</a></h2>
<p>El techo de memoria general es un límite sobre el <em>total</em>. Hay un segundo límite independiente sobre cualquier ítem <em>individual</em>: por defecto un valor no puede exceder <strong>4 MiB</strong>. Esto se comprueba en cada escritura sin importar el modo de expulsión, y es donde una escritura alcanza un rechazo genuinamente alcanzable.</p>
<p>La distinción importa porque los dos fallos significan cosas opuestas y quieren respuestas opuestas:</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> es un resultado vivo y alcanzable en todos los modos —la expulsión no puede hacer espacio para un valor que está por encima del tope por ítem, porque el tope no es sobre espacio libre, es sobre un valor patológico. <code>CapacityExceeded</code>, como se cubrió arriba, solo aparece en el modo sin-expulsión que todavía no puedes seleccionar. Tratarlos idénticamente es un bug esperando a ocurrir: reintentar una escritura <code>ItemTooLarge</code> para siempre nunca tendrá éxito.</p>
<!-- -->
<p>Un matiz relacionado vive en la frontera entre este tope por ítem y el runtime de .NET: los valores de aproximadamente 85.000 bytes en adelante aterrizan en el <strong>Large Object Heap</strong>, así que subir <code>maxItemSizeBytes</code> para admitir valores grandes es una decisión deliberada con consecuencias de GC, no un dial gratuito. Si estás actualizando en caliente valores grandes y planos, trocearlos en piezas sub-LOH es el mecanismo que evita que el LOH se agite —una característica separada con sus propios compromisos, que desglosamos en <a class="" href="https://clustron.io/es/blog/large-object-chunking/">Almacenar objetos grandes sin agitar el heap</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-configuras-y-qué-no">Qué configuras, y qué no<a href="https://clustron.io/es/blog/the-memory-ceiling/#qu%C3%A9-configuras-y-qu%C3%A9-no" class="hash-link" aria-label="Enlace directo al Qué configuras, y qué no" title="Enlace directo al Qué configuras, y qué no" translate="no">​</a></h2>
<p>El techo de memoria vive en la configuración del almacén bajo la sección <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>Eso son 2 GiB. Establece <code>maxSizeBytes</code> en <code>null</code>, o déjalo fuera, y obtienes el valor por defecto de 1 GiB. En la versión actual este es el ajuste de memoria que controlas —la política LRU, los umbrales de histéresis del 80%/70%, y la cadencia de 1 segundo son valores por defecto fijos, no perillas. Vale la pena tener claro esto: los artículos del blog y el motor describen un espacio de políticas más rico del que la superficie de configuración expone actualmente, y este artículo describe lo que realmente puedes establecer hoy.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-significa-para-dimensionar-un-clúster">Qué significa para dimensionar un clúster<a href="https://clustron.io/es/blog/the-memory-ceiling/#qu%C3%A9-significa-para-dimensionar-un-cl%C3%BAster" class="hash-link" aria-label="Enlace directo al Qué significa para dimensionar un clúster" title="Enlace directo al Qué significa para dimensionar un clúster" translate="no">​</a></h2>
<p>Como cada nodo es un buffer acotado, la capacidad total de un almacén es aproximadamente el techo por nodo multiplicado por el número de particiones que un nodo posee, ajustado por el factor de replicación (las réplicas sostienen copias adicionales que viven fuera del techo de propietario activo). Dos consecuencias prácticas se desprenden de eso:</p>
<ul>
<li class=""><strong>Dimensiona el techo a tu working set más margen de expulsión, no a tus datos totales.</strong> Un nodo ejecuta una caché LRU siempre activa hoy, así que los datos fríos se descartan bajo presión en lugar de rechazarse. Si tu working set cabe por debajo de ~70% del techo con espacio para que la banda respire, la expulsión se mantiene tranquila y el conjunto caliente permanece residente. Si dimensionas el techo por debajo de tu working set, has construido una caché que está constantemente expulsando datos que está a punto de necesitar de nuevo —correcto, pero lento, y el diente de sierra se convierte en una trituración.</li>
<li class=""><strong>No olvides la huella de las réplicas.</strong> El techo gobierna los datos poseídos, pero la RAM real de un nodo son los datos poseídos <em>más</em> las copias de réplica. Cuando aprovisionas memoria por pod o por máquina, presupuesta para ambos, o un nodo dimensionado "a su techo" aún correrá más caliente de lo que el techo implica.</li>
</ul>
<p>Para la aritmética de capacidad en el contexto de particiones, factor de replicación y conteos de pods, <a class="" href="https://clustron.io/es/blog/kubernetes-durability-sizing/">Dimensionar un clúster de Zaris para durabilidad en Kubernetes</a> recorre el cálculo completo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-conclusión">La conclusión<a href="https://clustron.io/es/blog/the-memory-ceiling/#la-conclusi%C3%B3n" class="hash-link" aria-label="Enlace directo al La conclusión" title="Enlace directo al La conclusión" translate="no">​</a></h2>
<p>Un almacén en memoria que crece sin límite no es una característica —es una interrupción esperando un pico de tráfico. Zaris trata cada nodo como un buffer acotado con un techo real (1 GiB por defecto, sin opción ilimitada), una caché LRU muestreada siempre activa por debajo, y una banda de histéresis que recupera espacio en ráfagas tranquilas en lugar de descontrolarse con una expulsión por escritura en el borde. El techo total y el tope por ítem son dos límites distintos con dos estados tipados distintos, y el que realmente es alcanzable hoy —<code>ItemTooLarge</code>— quiere una respuesta distinta de un bucle de reintento. Dimensiona el techo a tu working set, presupuesta para las copias de réplica encima, y el nodo mantiene su línea sin que lo vigiles.</p>
<p>El modelo completo, incluyendo las políticas que el motor lleva pero todavía no expone, está en <a class="" href="https://clustron.io/es/docs/clustron/zaris/core-concepts/memory-and-eviction/">Memoria y expulsión</a>; los campos y valores por defecto exactos están en la <a class="" href="https://clustron.io/es/docs/clustron/zaris/reference/configuration/#environment-variables">referencia de configuración</a>, y cada estado de capacidad está catalogado en la <a class="" href="https://clustron.io/es/docs/clustron/zaris/reference/status-codes/">referencia de estados y excepciones</a>.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Arquitectura</category>
            <category>Operaciones</category>
            <category>.NET</category>
        </item>
        <item>
            <title><![CDATA[Lo que cruza el cable: la anatomía de un registro de Zaris]]></title>
            <link>https://clustron.io/es/blog/what-crosses-the-wire/</link>
            <guid>https://clustron.io/es/blog/what-crosses-the-wire/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Cada mensaje de replicación y migración en Zaris lleva una sola carga útil —un StoredRecord. Este es un recorrido campo por campo de lo que contiene, por qué una eliminación es un registro y no una ausencia, por qué viajan dos números de versión distintos, y qué deliberadamente nunca sale del nodo.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="La anatomía de un registro almacenado de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJUaGUgYW5hdG9teSBvZiBhIFphcmlzIHN0b3JlZCByZWNvcmQgb24gdGhlIHdpcmUiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5BUkNISVRFQ1RVUkU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+V2hhdCBjcm9zc2VzPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPnRoZSB3aXJlPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+VGhlIGFuYXRvbXkgb2YgYSBaYXJpcyBzdG9yZWQgcmVjb3JkPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDAsMjUwKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjQxMCIgaGVpZ2h0PSIxODYiIHJ4PSIxMCIgZmlsbD0iIzBjMWIxNiIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjIyIiB5PSIzNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlN0b3JlZFJlY29yZDwvdGV4dD4NCiAgICA8bGluZSB4MT0iMjIiIHkxPSI0NiIgeDI9IjM4OCIgeTI9IjQ2IiBzdHJva2U9IiMxZDNhMzAiIHN0cm9rZS13aWR0aD0iMS41Ii8+DQogICAgPHRleHQgeD0iMjIiIHk9Ijc0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzBdIEVudHJ5PC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjIxMCIgeT0iNzQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWE2YjY1Ij5rZXkgJiMxODM7IHZhbHVlICYjMTgzOyBtZXRhZGF0YTwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTAwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzFdIFJldmlzaW9uPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjIxMCIgeT0iMTAwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzVhNmI2NSI+c2VnbWVudCBvcmRlcjwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzJdIFZlcnNpb248L3RleHQ+DQogICAgPHRleHQgeD0iMjEwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWE2YjY1Ij5wZXIta2V5IENBUzwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTUyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzNdIFRpbWVzdGFtcFV0YzwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMiIgeT0iMTc4IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iI2NkZWVlMCI+WzRdIElzRGVsZXRlZDwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMTAiIHk9IjE3OCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMyYmQ2YTAiPnRvbWJzdG9uZTwvdGV4dD4NCiAgICA8dGV4dCB4PSIwIiB5PSIyMTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNWE2YjY1Ij5BY2NvdW50ZWRCeXRlcyAmIzE4MzsgTWF0ZXJpYWxpemVkVmFsdWU8L3RleHQ+DQogICAgPHRleHQgeD0iMCIgeT0iMjMyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzVhNmI2NSI+W0lnbm9yZU1lbWJlcl0gJiM4NTk0OyBzdGF5cyBub2RlLWxvY2FsPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>La mayor parte de lo que Zaris hace bien —replicar una escritura a un backup, poner al día a una réplica rezagada, traspasar una partición a un nuevo propietario— se reduce a una pregunta silenciosa: <em>¿cuál es, exactamente, la unidad de datos que se mueve entre nodos?</em> Si esa unidad está bien, la convergencia es casi aburrida. Si deja caer un campo o lleva el equivocado, obtienes una clave eliminada resucitada, una réplica que no puede distinguir lo obsoleto de lo fresco, o la contabilidad de un nodo filtrándose en otro.</p>
<p>En Zaris esa unidad es un solo tipo —<code>StoredRecord</code>— y vale la pena desmontarlo campo por campo, porque cada decisión en él es portante. Este artículo es la anatomía: qué contiene el registro, cómo se enmarca en el cable, por qué una eliminación viaja <em>como un registro</em> en lugar de como una ausencia, por qué hay dos números de versión distintos, y —igual de importante— qué deliberadamente se deja atrás en cada nodo y nunca cruza el cable en absoluto.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="una-carga-útil-en-todas-partes">Una carga útil, en todas partes<a href="https://clustron.io/es/blog/what-crosses-the-wire/#una-carga-%C3%BAtil-en-todas-partes" class="hash-link" aria-label="Enlace directo al Una carga útil, en todas partes" title="Enlace directo al Una carga útil, en todas partes" translate="no">​</a></h2>
<p>Un almacén clave/valor tiene un número sorprendente de lugares donde "un valor" tiene que describirse con precisión: el mapa en memoria, el stream de replicación, la instantánea de migración, el log de operaciones. Zaris usa la <em>misma</em> forma de registro para todos ellos. La cosa que está en el diccionario del segmento es la misma cosa que recibe una réplica, es la misma cosa que recibe un nuevo propietario durante el cut-over. Esa uniformidad es por qué el <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">log de operaciones puede hacer dos trabajos a la vez</a> y por qué una instantánea de migración y un backfill de réplica no necesitan codificaciones separadas.</p>
<p>Aquí está todo el tipo, despojado a su superficie de cable:</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>Cinco campos cruzan el cable. Dos más viven en el objeto pero están marcados <code>[IgnoreMember]</code> y nunca viajan. El resto de este artículo es por qué cada uno está donde está.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-enmarcado-messagepack-con-claves-posicionales">El enmarcado: MessagePack con claves posicionales<a href="https://clustron.io/es/blog/what-crosses-the-wire/#el-enmarcado-messagepack-con-claves-posicionales" class="hash-link" aria-label="Enlace directo al El enmarcado: MessagePack con claves posicionales" title="Enlace directo al El enmarcado: MessagePack con claves posicionales" translate="no">​</a></h2>
<p>Zaris serializa los registros con <strong>MessagePack</strong>, y los atributos <code>[Key(N)]</code> son la razón por la que el cable se mantiene pequeño. MessagePack puede serializar un objeto de dos formas: como un <em>mapa</em> de nombre-de-campo → valor (autodescriptivo, pero cada mensaje repite cada nombre de campo como una cadena), o como un <em>arreglo</em> posicional donde cada campo se identifica por un índice entero. Los atributos <code>[Key(0)]</code>, <code>[Key(1)]</code>, … seleccionan la forma de arreglo. En el cable un registro es un arreglo corto de valores en un orden fijo —sin nombres de campo, sin puntuación estilo JSON, solo las cargas útiles.</p>
<p>Eso importa porque este registro es la estructura de datos <em>caliente</em> de todo el sistema. Se serializa en cada escritura replicada y cada clave migrada; a cientos de miles de escrituras por segundo la diferencia entre enviar <code>"TimestampUtc"</code> como una cadena un millón de veces y enviar nada más que la posición no es una micro-optimización, es el presupuesto.</p>
<p>El esquema posicional también fija la regla para evolucionar el formato de forma segura: <strong>las claves enteras son solo-de-adición (append-only).</strong> Un nuevo campo recibe el siguiente índice sin usar; los índices existentes nunca cambian de significado. Un nodo más viejo que no conoce el índice 7 simplemente lo ignora; un nodo más nuevo leyendo un registro viejo encuentra el índice 7 ausente y recurre a un valor por defecto. Veremos un campo —las etiquetas (labels) en la clave de mapa 3 dentro de los metadatos— que se apoya exactamente en este comportamiento de "ausente significa por defecto" para no costar nada cuando no se usa.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dentro-de-la-entrada-clave-valor-y-metadatos-no-nulos">Dentro de la entrada: clave, valor y metadatos no nulos<a href="https://clustron.io/es/blog/what-crosses-the-wire/#dentro-de-la-entrada-clave-valor-y-metadatos-no-nulos" class="hash-link" aria-label="Enlace directo al Dentro de la entrada: clave, valor y metadatos no nulos" title="Enlace directo al Dentro de la entrada: clave, valor y metadatos no nulos" translate="no">​</a></h2>
<p><code>ZarisEntry</code> es el campo <code>Entry</code> —la parte que un usuario reconocería como "sus datos":</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>Dos detalles aquí son del tipo que solo aprendes quemándote una vez. Primero, <code>Value</code> está tipado <code>object</code>, no <code>byte[]</code> o <code>string</code> —la forma concreta del valor es lo que sea que el escritor puso ahí, enmarcado dentro de la entrada, que es cómo el mismo tipo de registro lleva una cadena simple, un contador, o toda una <a class="" href="https://clustron.io/es/blog/data-structures-that-replicate/">colección replicada</a> sin que el registro necesite saber cuál.</p>
<p>Segundo, <code>Metadata</code> se sirve a través de un getter que nunca puede devolver null:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">private</span><span class="token plain"> </span><span class="token class-name">EntryMetadata</span><span class="token class-name punctuation" style="color:#393A34">?</span><span class="token plain"> _metadata</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">[</span><span class="token attribute class-name">Key</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">(</span><span class="token attribute attribute-arguments number" style="color:#36acaa">2</span><span class="token attribute attribute-arguments punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">public</span><span class="token plain"> </span><span class="token return-type class-name">EntryMetadata</span><span class="token plain"> Metadata</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">get</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _metadata </span><span class="token operator" style="color:#393A34">??=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">EntryMetadata</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">set</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> _metadata </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">value</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>MessagePack omite el constructor durante la deserialización y llama a los setters directamente, así que un inicializador simple <code>= new()</code> no sobreviviría a un viaje de ida y vuelta —un registro que llegara sin metadatos deserializaría con unos nulos, y cada sitio de llamada downstream necesitaría una comprobación de null. El getter hace la garantía estructural en su lugar: los metadatos están <em>siempre</em> ahí, incluso cuando el cable no llevó ninguno. Un lugar donde imponer el invariante, cero guardas de null en todos los demás.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="metadatos-que-no-cuestan-nada-cuando-están-vacíos">Metadatos que no cuestan nada cuando están vacíos<a href="https://clustron.io/es/blog/what-crosses-the-wire/#metadatos-que-no-cuestan-nada-cuando-est%C3%A1n-vac%C3%ADos" class="hash-link" aria-label="Enlace directo al Metadatos que no cuestan nada cuando están vacíos" title="Enlace directo al Metadatos que no cuestan nada cuando están vacíos" translate="no">​</a></h3>
<p><code>EntryMetadata</code> lleva los atributos opcionales de una clave —<code>CreatedAt</code>, <code>Ttl</code>, <code>ContentType</code>, un handle opcional de arrendamiento o lock, y etiquetas. El campo de etiquetas es una pequeña clase magistral sobre no pagar por lo que no usas. La mayoría de las claves no tienen etiquetas, y un <code>Dictionary</code> ingenuo inicializado por entrada es puro desperdicio —en un almacén real, 390K entradas sin etiquetas sostenían cada una un diccionario vacío de 80 bytes, unos 31 MB de nada.</p>
<p>El arreglo es de dos lados. En el heap, las entradas sin etiquetas comparten un único diccionario vacío estático, así que los lectores aún ven un mapa no nulo (vacío) pero no asignan nada. En el cable, la superficie serializada es null cuando no hay etiquetas:</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>Una entrada sin etiquetas escribe <code>nil</code> en el índice 3 y el lector restaura "sin etiquetas" a partir de ello. Este es el esquema de claves solo-de-adición ganándose el sustento: el caso común cuesta un byte, y la característica está ahí en pleno en el momento en que una clave realmente necesita una etiqueta.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dos-números-dos-trabajos-revision-e-itemversion">Dos números, dos trabajos: Revision e ItemVersion<a href="https://clustron.io/es/blog/what-crosses-the-wire/#dos-n%C3%BAmeros-dos-trabajos-revision-e-itemversion" class="hash-link" aria-label="Enlace directo al Dos números, dos trabajos: Revision e ItemVersion" title="Enlace directo al Dos números, dos trabajos: Revision e ItemVersion" translate="no">​</a></h2>
<p>El punto de confusión más común sobre los registros de Zaris es que llevan <em>dos</em> números que aumentan monótonamente. No son redundantes —responden preguntas distintas, y confundirlos rompería uno de los dos trabajos.</p>
<p><strong><code>ItemVersion</code> es por clave.</strong> Comienza en 1 e incrementa en uno cada vez que <em>esa clave</em> se escribe. Es el número que el cliente ve y el número contra el que las comprobaciones de <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">compare-and-swap</a> optimistas verifican: "confirma solo si esta clave sigue en la versión 7". Cuando el almacén aplica un put, la nueva versión es simplemente la versión de la clave vieja más uno:</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> es por segmento.</strong> Es un único contador compartido por <em>cada</em> clave en la partición, incrementado en cada escritura y cada eliminación:</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>Eso hace de <code>Revision</code> un orden total sobre todas las mutaciones en un segmento, que es exactamente lo que la replicación y la recuperación necesitan. A una réplica no le importa que la clave <code>user:1</code> esté en su 7.ª versión; le importa <em>qué revisiones de todo el segmento ya ha visto</em> para poder pedir todo lo posterior a ese punto. El log de operaciones está indexado por revisión, el protocolo de puesta al día solicita "todo lo posterior a la revisión R", y la resolución de conflictos entre un registro entrante y uno local compara revisiones, no versiones por clave:</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>Así que: <code>ItemVersion</code> es la unidad de <em>concurrencia optimista visible al cliente</em> sobre una clave; <code>Revision</code> es la unidad de <em>orden y convergencia entre nodos</em> a lo largo de todo el segmento. El mismo registro, dos relojes, cada uno haciendo un trabajo que el otro no puede.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="una-eliminación-es-un-registro-no-una-ausencia">Una eliminación es un registro, no una ausencia<a href="https://clustron.io/es/blog/what-crosses-the-wire/#una-eliminaci%C3%B3n-es-un-registro-no-una-ausencia" class="hash-link" aria-label="Enlace directo al Una eliminación es un registro, no una ausencia" title="Enlace directo al Una eliminación es un registro, no una ausencia" translate="no">​</a></h2>
<p>Aquí está el campo que más sorprende a la gente: <code>IsDeleted</code>. Cuando eliminas una clave en Zaris, el almacén no simplemente quita la entrada de su diccionario. Escribe un <code>StoredRecord</code> con <code>IsDeleted = true</code> —una <strong>tombstone</strong>— lo estampa con la siguiente revisión del segmento, y lo añade al log de operaciones como una eliminación:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> revision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">Revision</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Interlocked</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Increment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">ref</span><span class="token plain"> _storeRevision</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">_deleteRevisions</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> revision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">AppendOperationLog</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">revision</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Value</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ReplicationOperation</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Delete</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">StoredRecord</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// ...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    Revision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> revision</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    IsDeleted </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La razón es todo el punto del formato de registro. Si una eliminación fuera meramente una ausencia —la clave desvaneciéndose silenciosamente de un mapa— no habría nada que replicar. Una réplica que se perdiera el momento de la eliminación seguiría sirviendo el valor viejo para siempre, porque "nada" no viaja y no puede ordenarse contra las escrituras a su alrededor. Al hacer de la eliminación un registro de primera clase con su propia revisión, la eliminación fluye por la misma ruta exacta de replicación y puesta al día que una escritura, y encaja en el orden total del segmento en el lugar correcto.</p>
<p>Las tombstones también defienden contra un fallo más sutil: una escritura obsoleta <em>resucitante</em>. Imagina una eliminación en la revisión 50 y un put más viejo para la misma clave, revisión 48, todavía en vuelo a través de la red. Sin una memoria de la eliminación, el put tardío aterrizaría y traería la clave de vuelta de entre los muertos. Zaris registra la revisión de eliminación y rechaza cualquier cosa en o antes de ella:</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>El reverso de las tombstones es que no pueden acumularse para siempre, o una carga de trabajo con muchas eliminaciones filtraría memoria. Zaris recorta las revisiones de eliminación una vez que están con seguridad por debajo de la revisión más vieja que cualquier réplica todavía necesita —la misma lógica de marca de agua baja (low-water-mark) que recorta el log de operaciones— así que la tombstone hace su trabajo de convergencia y luego se recupera. (La disciplina de recorte es el tema del <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">artículo sobre el log de operaciones</a>.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lo-que-se-queda-en-casa-los-campos-ignoremember">Lo que se queda en casa: los campos <code>[IgnoreMember]</code><a href="https://clustron.io/es/blog/what-crosses-the-wire/#lo-que-se-queda-en-casa-los-campos-ignoremember" class="hash-link" aria-label="Enlace directo al lo-que-se-queda-en-casa-los-campos-ignoremember" title="Enlace directo al lo-que-se-queda-en-casa-los-campos-ignoremember" translate="no">​</a></h2>
<p>Los dos campos que el registro lleva pero nunca serializa son tan deliberados como los cinco que sí. Ambos están marcados <code>[IgnoreMember]</code>, y ambos son <em>locales al nodo por diseño</em> —correctos solo para el nodo que los computó.</p>
<p><strong><code>AccountedBytes</code></strong> es la memoria contra la que se cobra este registro en el presupuesto del almacén, estampada una vez en el momento de escritura para que el almacén pueda reportar deltas de bytes sin re-medir el registro anterior. Explícitamente <em>no</em> se serializa, porque cada nodo lo computa a partir de la propia clave y valor del registro bajo sus propias reglas de contabilidad. Si viajara por el cable, un registro replicado desde el nodo A llevaría la estimación de bytes de A a los libros del nodo B —la contabilidad de un nodo distorsionando silenciosamente la de otro. Mantenerlo <code>[IgnoreMember]</code> significa que cada nodo lo recomputa independientemente, y un registro que llega vía replicación o migración lo contabiliza el nodo que ahora lo sostiene.</p>
<p><strong><code>MaterializedValue</code></strong> es una forma en memoria viva, construida perezosamente, de una colección nativa (un hash, lista, set o sorted set) cacheada para que las operaciones por elemento no re-deserialicen todo el valor cada vez. Es pura aceleración local. La forma autoritativa es siempre los bytes del valor enmarcado dentro de <code>Entry</code>; un registro que llega de otro nodo materializa esta caché perezosamente en el primer acceso. Serializarla sería enviar el estado de borrador privado de un nodo como si fuera datos.</p>
<p>La disciplina aquí es limpia y vale la pena enunciarla en voz alta: <strong>el cable lleva la verdad, el nodo lleva las conveniencias.</strong> Cualquier cosa que un nodo pueda recomputar a partir del registro en sí —su costo de memoria, su estructura materializada— se queda local y se marca <code>[IgnoreMember]</code>. Cualquier cosa que sea parte de la <em>identidad y el orden</em> del registro —clave, valor, metadatos, los dos números de versión, el flag de tombstone— cruza el cable. La frontera del atributo es exactamente la frontera verdad/conveniencia, y mantenerla afilada es lo que hace seguro copiar el registro entre nodos sin pensarlo dos veces.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-esto-es-comprobable-y-comprobado">Por qué esto es comprobable, y comprobado<a href="https://clustron.io/es/blog/what-crosses-the-wire/#por-qu%C3%A9-esto-es-comprobable-y-comprobado" class="hash-link" aria-label="Enlace directo al Por qué esto es comprobable, y comprobado" title="Enlace directo al Por qué esto es comprobable, y comprobado" translate="no">​</a></h2>
<p>Como la forma del registro es portante para la convergencia, su comportamiento en el cable está fijado por pruebas en lugar de dejado a inspección. Dos invariantes en particular ganan sus propias aserciones: el flag de tombstone debe sobrevivir a un viaje de ida y vuelta (un <code>IsDeleted</code> dejado caer resucita una clave eliminada en una réplica), y <code>AccountedBytes</code> debe volver como cero tras un viaje de ida y vuelta (probando que nunca viajó):</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>Estas se leen casi como la tesis de todo el artículo escrita como aserciones: las cosas que deben cruzar el cable se demuestran que sobreviven a él, y las cosas que no deben se demuestran que se desvanecen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-registro-es-el-contrato">El registro es el contrato<a href="https://clustron.io/es/blog/what-crosses-the-wire/#el-registro-es-el-contrato" class="hash-link" aria-label="Enlace directo al El registro es el contrato" title="Enlace directo al El registro es el contrato" translate="no">​</a></h2>
<p>Es fácil pensar en los problemas difíciles de un almacén distribuido como si vivieran en las partes vistosas —el <a class="" href="https://clustron.io/es/blog/anatomy-of-a-failover/">protocolo de failover</a>, el <a class="" href="https://clustron.io/es/blog/partition-cutover-protocol/">handshake de cut-over</a>, las <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">pruebas de caos</a>. Lo hacen. Pero esos protocolos son solo tan correctos como la cosa que mueven, y la cosa que mueven es este único registro pequeño. Una tombstone en lugar de una ausencia es por qué una eliminación converge. Dos números de versión son por qué la concurrencia optimista de una clave y el orden de todo un segmento se mantienen desenredados. Las claves posicionales de MessagePack son por qué la ruta caliente se mantiene barata y el formato aún puede crecer. Y un par de campos <code>[IgnoreMember]</code> son por qué la replicación puede copiar un registro entre nodos y confiar en que nada local al nodo vino de polizón.</p>
<p>Acierta la unidad de datos y los grandes protocolos tienen algo sólido sobre lo que apoyarse. Ese es todo el caso para dedicar un artículo de blog a cinco campos y dos que se quedan en casa.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[El techo del pool de workers: cuando un nodo se estanca al 25% de CPU]]></title>
            <link>https://clustron.io/es/blog/worker-pool-ceiling/</link>
            <guid>https://clustron.io/es/blog/worker-pool-ceiling/</guid>
            <pubDate>Sun, 04 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nodo de Zaris que servía lecturas a poco menos de medio millón de ops/seg mientras estaba al cuarto de su CPU no estaba limitado por núcleos ni por el cliente —el pool de workers de despacho de solicitudes era el techo. Aquí está el mecanismo, el arreglo, y cómo afinarlo con honestidad con ZARIS_SERVER_MAX_WORKERS.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Un nodo clavado cerca del 25% de CPU con throughput plano porque el pool de workers de despacho era el techo" 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>Aquí hay un resultado de benchmark que parece una contradicción. Un nodo de Zaris sirviendo una carga de trabajo intensiva en lecturas tope en poco menos de medio millón de ops/seg y se niega a trepar —añade más conexiones de cliente, más generadores de carga, y el número no se mueve. El diagnóstico obvio es "está saturado". Excepto que el gráfico de CPU dice otra cosa: el nodo está al <em>cuarto</em> de su CPU disponible, aproximadamente. Tres núcleos de cada cuatro están ociosos, y el throughput está plano.</p>
<p>Eso no es un nodo limitado por núcleos, y —si lo has descartado con cuidado— tampoco es un benchmark limitado por el cliente. Es una tercera cosa, y es un clásico: el <strong>pool de workers</strong> de despacho de solicitudes era más estrecho de lo que el trabajo quería ser, así que las solicitudes se encolaban mientras los núcleos estaban de brazos cruzados.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="primero-descarta-a-los-sospechosos-habituales">Primero, descarta a los sospechosos habituales<a href="https://clustron.io/es/blog/worker-pool-ceiling/#primero-descarta-a-los-sospechosos-habituales" class="hash-link" aria-label="Enlace directo al Primero, descarta a los sospechosos habituales" title="Enlace directo al Primero, descarta a los sospechosos habituales" translate="no">​</a></h2>
<p>Dos explicaciones tienen que irse antes de que esta esté siquiera sobre la mesa, porque son mucho más comunes.</p>
<p><strong>Limitado por núcleos.</strong> Si el nodo está en o cerca del 100% de CPU y el throughput está plano, genuinamente te has quedado sin CPU —la respuesta es más núcleos o más nodos, y Zaris <a class="" href="https://clustron.io/es/blog/reads-scale-with-cores/">escala las lecturas añadiendo núcleos</a>. Ese no es este caso: aquí la CPU está <em>ociosa</em>.</p>
<p><strong>Limitado por el cliente.</strong> Un solo generador de carga se satura mucho antes que el servidor, y entonces estás midiendo tu cliente, no Zaris. Si el throughput está plano a medida que añades núcleos de <em>servidor</em>, sospecha de tu cliente primero —añade generadores y conexiones hasta que el número del lado del servidor realmente responda. Cubrimos esta trampa en el artículo sobre lecturas, y es la que hay que eliminar antes de seguir adelante.</p>
<p>Lo que queda después de esos dos es el caso interesante: suficiente carga ofrecida para mantener ocupado al nodo, mucho margen de CPU, y el throughput todavía plano. Cuando el cuello de botella está dentro del servidor pero no es la CPU, lo que hay que mirar es la concurrencia —cuántas solicitudes el nodo <em>trabajará a la vez</em>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-pool-de-despacho-es-el-límite-de-concurrencia">El pool de despacho es el límite de concurrencia<a href="https://clustron.io/es/blog/worker-pool-ceiling/#el-pool-de-despacho-es-el-l%C3%ADmite-de-concurrencia" class="hash-link" aria-label="Enlace directo al El pool de despacho es el límite de concurrencia" title="Enlace directo al El pool de despacho es el límite de concurrencia" translate="no">​</a></h2>
<p>Cada solicitud que llega a un nodo de Zaris se entrega a un pool de workers de despacho de solicitudes. Un worker la recoge, la enruta a la partición que posee la clave, ejecuta la operación contra el almacén en proceso de esa partición, y escribe la respuesta de vuelta. Para una lectura, la operación en sí es una búsqueda en memoria a escala de microsegundo —genuinamente diminuta.</p>
<p>El ancho de ese pool —cuántos workers pueden estar a mitad de solicitud simultáneamente— fija un techo duro sobre las solicitudes en vuelo, y ese es un número distinto de "qué tan rápido es una operación". El valor por defecto original derivaba el ancho de un pequeño múltiplo de <code>Environment.ProcessorCount</code>. En un nodo de 4 vCPU, un pool de un par-de-veces-los-núcleos son solo un puñado de workers.</p>
<p>Aquí está la sutileza que hace que el gráfico de CPU parezca equivocado. Si cada worker gastara toda su ranura quemando CPU en la búsqueda, un pool dimensionado a los núcleos sería exactamente correcto y el nodo <em>sí</em> alcanzaría el 100% de CPU. Pero los workers no gastan toda su ranura en CPU. Un worker ocasionalmente se estaciona (parks) —una continuación de <code>await</code>, una escritura de socket que no se completa síncronamente, un breve lock sobre una estructura compartida. Un worker estacionado no quema CPU, pero <strong>aún sostiene su ranura del pool</strong>. Con solo un puñado de ranuras, unos pocos workers estacionados bastan para estancar todo el nodo: las nuevas solicitudes se encolan detrás de ellos, y como los workers ocupados están mayormente esperando en lugar de computando, los núcleos nunca se encienden. Throughput plano, CPU ociosa. El ancho del pool era el techo, no el silicio.</p>
<!-- -->
<p>La señal que distingue esto de un benchmark limitado por el cliente: las solicitudes se <em>encolan en el servidor</em> (la latencia trepa, el throughput está plano) mientras la CPU del <em>servidor</em> se mantiene baja. Limitado por el cliente se ve distinto —el servidor está ocioso porque nada llega lo bastante rápido. Aquí, el trabajo está llegando; el nodo simplemente no acepta suficiente de él a la vez.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-arreglo-dimensiona-el-pool-para-el-trabajo-no-para-el-conteo-de-núcleos">El arreglo: dimensiona el pool para el trabajo, no para el conteo de núcleos<a href="https://clustron.io/es/blog/worker-pool-ceiling/#el-arreglo-dimensiona-el-pool-para-el-trabajo-no-para-el-conteo-de-n%C3%BAcleos" class="hash-link" aria-label="Enlace directo al El arreglo: dimensiona el pool para el trabajo, no para el conteo de núcleos" title="Enlace directo al El arreglo: dimensiona el pool para el trabajo, no para el conteo de núcleos" translate="no">​</a></h2>
<p>La reparación no es exótica. Las lecturas son búsquedas en memoria a escala de microsegundo, así que el pool debería estar fijado <em>ancho</em> por defecto —lo bastante ancho como para que un puñado de workers estacionados no pueda hambrear al nodo— y debería ser ajustable, porque el ancho correcto depende del nodo y la carga de trabajo, no de un múltiplo fijo del conteo de núcleos.</p>
<p>El ancho de pool por defecto ahora es <code>CPU count × 16</code>, acotado a una banda sensata de 64–1024, con un piso de <code>max(4, CPU count × 4)</code>. Dos variables de entorno lo anulan:</p>
<table><thead><tr><th>Variable</th><th>Por defecto</th><th>Qué hace</th></tr></thead><tbody><tr><td><code>ZARIS_SERVER_MAX_WORKERS</code></td><td>CPU count × 16 (acotado 64–1024)</td><td>Anulación dura (1–4096) para el ancho del pool de despacho. Súbelo cuando un nodo tiene CPU de sobra y las solicitudes se encolan.</td></tr><tr><td><code>ZARIS_SERVER_MIN_WORKERS</code></td><td>max(4, CPU count × 4)</td><td>Cota inferior (1–4096) sobre el ancho del pool.</td></tr></tbody></table>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># A node with spare CPU and requests queuing — widen the pool.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">ZARIS_SERVER_MAX_WORKERS</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">512</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">ZARIS_SERVER_MIN_WORKERS</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">64</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  dotnet Clustron.Zaris.Server.dll</span><br></span></code></pre></div></div>
<p>Una propiedad que vale la pena señalar: este es un cambio del <strong>lado del servidor</strong> y nada más. El protocolo de cable no cambia, el SDK del cliente no cambia, y no hay un bump de NuGet que perseguir —afinas un nodo reiniciándolo con un entorno distinto, y cada cliente existente sigue hablando con él sin cambios.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="afínalo-con-honestidad-ensanchar-no-es-gratis">Afínalo con honestidad: ensanchar no es gratis<a href="https://clustron.io/es/blog/worker-pool-ceiling/#af%C3%ADnalo-con-honestidad-ensanchar-no-es-gratis" class="hash-link" aria-label="Enlace directo al Afínalo con honestidad: ensanchar no es gratis" title="Enlace directo al Afínalo con honestidad: ensanchar no es gratis" translate="no">​</a></h2>
<p>El reflejo tras leer esto es subir <code>ZARIS_SERVER_MAX_WORKERS</code> al tope en cada nodo. No lo hagas. Un pool más ancho ayuda en exactamente una situación y no hace nada —o perjudica levemente— en las demás.</p>
<ul>
<li class=""><strong>Ayuda cuando las solicitudes se encolan mientras la CPU está ociosa.</strong> Ese es el caso de todo este artículo: la concurrencia en vuelo es el límite, así que admitir más de ella llena los núcleos. Aquí es donde verás saltar el throughput.</li>
<li class=""><strong>No hace nada cuando ya estás limitado por CPU.</strong> Si el nodo está al 100% de CPU, más workers no conjurarán throughput de la nada —solo añaden sobrecarga de cambio de contexto a una máquina que no tiene ciclos de sobra. Más workers no es más cómputo.</li>
<li class=""><strong>Puede perjudicar si lo llevas a un ancho absurdo.</strong> Miles de workers machacando un nodo de 2 vCPU gastan tiempo real cambiando de contexto en lugar de servir. Por eso hay un acotamiento, y por eso la perilla tope en 4096 en lugar del infinito.</li>
</ul>
<p>El bucle de afinado honesto es: busca la firma —latencia trepando y throughput plano <em>mientras la CPU está muy por debajo de la saturación</em>— sube <code>MAX_WORKERS</code>, y mide. Si la CPU trepa y el throughput la sigue, el pool era tu techo. Si nada se mueve, no lo era, y deberías devolver la perilla y mirar en otro lugar (la ruta de red, el cliente, o genuinamente necesitar más nodos).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mide-la-cpu-donde-es-realmente-cierta">Mide la CPU donde es realmente cierta<a href="https://clustron.io/es/blog/worker-pool-ceiling/#mide-la-cpu-donde-es-realmente-cierta" class="hash-link" aria-label="Enlace directo al Mide la CPU donde es realmente cierta" title="Enlace directo al Mide la CPU donde es realmente cierta" translate="no">​</a></h2>
<p>Toda esta investigación depende de confiar en el número de CPU, y ese número es fácil de obtener mal.</p>
<ul>
<li class=""><strong>Lee la CPU del cgroup, no de una estimación del metrics-server del clúster.</strong> En Kubernetes especialmente, la cifra del metrics-server se retrasa y suaviza —te dirá que un nodo está al 40% cuando la contabilidad del cgroup dice que está clavado, o al revés. Diagnostica un caso de "throughput plano, CPU ociosa" desde el cgroup, o perseguirás un número que no es real.</li>
<li class=""><strong>Los benchmarks locales están limitados por el host y ocultarán el techo.</strong> En un portátil, el generador de carga y el servidor pelean por los mismos núcleos, así que nunca presentas al servidor con carga limpia y suficiente —la meseta que mides es tu plataforma de prueba, no Zaris. El techo real del pool de workers apareció a escala en un clúster gestionado con la carga impulsada desde máquinas separadas, no en una caja de desarrollador. Valida el throughput donde el cliente no pueda ser el cuello de botella.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-conclusión">La conclusión<a href="https://clustron.io/es/blog/worker-pool-ceiling/#la-conclusi%C3%B3n" class="hash-link" aria-label="Enlace directo al La conclusión" title="Enlace directo al La conclusión" translate="no">​</a></h2>
<p>Una curva de throughput plana tiene tres causas muy distintas, y el gráfico de CPU te dice cuál estás mirando. Al 100% de CPU, estás limitado por núcleos —añade nodos. Con el <em>servidor</em> ocioso porque nada llega lo bastante rápido, estás limitado por el cliente —arregla la prueba. Pero un nodo que está plano al cuarto de su CPU, con solicitudes visiblemente encolándose, no es ninguno de los dos: es un límite de concurrencia, y en Zaris ese es el pool de workers de despacho. El valor por defecto ahora es lo bastante ancho como para que la mayoría de las cargas de trabajo nunca lo alcancen, y <code>ZARIS_SERVER_MAX_WORKERS</code> está ahí para las que sí —para subirse deliberadamente, cuando el gráfico de CPU se lo ha ganado.</p>
<p>Para la superficie completa de variables de entorno y los valores por defecto exactos, consulta la <a class="" href="https://clustron.io/es/docs/clustron/zaris/reference/configuration/#environment-variables">referencia de configuración</a>. Si estás dimensionando un clúster para throughput, la <a class="" href="https://clustron.io/es/docs/clustron/zaris/admin-guide/benchmarking/">guía de benchmarking</a> recorre los ajustes que realmente mueven los números, y <a class="" href="https://clustron.io/es/blog/reads-scale-with-cores/">Lecturas que escalan con tus núcleos</a> cubre la otra mitad de la historia —por qué, una vez que el pool está fuera del camino, añadir núcleos añade capacidad de lectura en lugar de chocar con un muro de bucle-de-eventos-único.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Rendimiento</category>
            <category>.NET</category>
            <category>Operaciones</category>
        </item>
        <item>
            <title><![CDATA[Anatomía de una conmutación por error: qué le pasa a una escritura en vuelo cuando el dueño muere]]></title>
            <link>https://clustron.io/es/blog/anatomy-of-a-failover/</link>
            <guid>https://clustron.io/es/blog/anatomy-of-a-failover/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un nodo que tiene la copia activa de tu partición muere mientras le llegan escrituras. Sigue la secuencia exacta que Zaris ejecuta a continuación: detección y confirmación, una promoción de réplica delimitada por épocas (epoch-fenced), una prueba de posesión que decide si el nuevo dueño sirve o sincroniza primero, y el redireccionamiento del cliente que hace que tu reintento aterrice en el nuevo dueño; además de los casos honestos en los que una escritura se pierde genuinamente.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Anatomía de una conmutación por error en 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>Una conmutación por error es el momento en que un almacén distribuido es más honesto sobre lo que realmente es. En estado estable todos los almacenes se ven iguales: una escritura llega a un dueño, se confirma, y sigues adelante. El diseño solo se muestra cuando el dueño <em>desaparece a mitad de frase</em>: un nodo tiene la copia activa de una de tus particiones, le están llegando escrituras ahora mismo, y de pronto ya no está. Eliminado en seco. Particionado por la red. Muerto por OOM. El clúster tiene unos segundos para darse cuenta, acordar un nuevo dueño y hacer que el tráfico vuelva a fluir; y que lo haga correctamente decide si tu escritura confirmada sobrevive o se evapora silenciosamente.</p>
<p>Esta entrada sigue esa secuencia de principio a fin, en el orden en que Zaris realmente la ejecuta, y nombra los casos en los que la respuesta es genuinamente "la escritura se pierde", porque una entrada sobre conmutación por error que solo describe el camino feliz no vale la pena leer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-único-invariante-que-hace-tratable-el-resto">El único invariante que hace tratable el resto<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#el-%C3%BAnico-invariante-que-hace-tratable-el-resto" class="hash-link" aria-label="Enlace directo al El único invariante que hace tratable el resto" title="Enlace directo al El único invariante que hace tratable el resto" translate="no">​</a></h2>
<p>Antes de la línea de tiempo, la única decisión de diseño de la que cuelga todo lo demás: <strong>Zaris separa el <em>mapa</em> de particiones del <em>runtime</em> de particiones.</strong></p>
<p>El mapa de particiones es la asignación lenta y deliberada —<em>qué nodos son responsables de la partición P</em>: un primario preferido y sus réplicas—. Cambia solo ante eventos de topología reales (un nodo se une o se va permanentemente, un escalado horizontal, un rebalanceo) y cambia mediante una regeneración planificada y versionada.</p>
<p>El runtime es la respuesta rápida y en vivo a una pregunta distinta —<em>qué nodo asignado es el dueño activo que sirve P ahora mismo, y con qué salud</em>—. Durante una conmutación por error <strong>el mapa no se mueve en absoluto.</strong> Solo se mueve el activo del runtime. Ese es el invariante declarado en el propio servicio de conmutación por error: una única señal independiente de fallo abre la compuerta de conmutación del runtime, "PartitionMap remains stable and only PartitionRuntimeMap.Active is allowed to move during failover."</p>
<p>Por qué importa: regenerar un mapa de particiones es caro y peligroso; puede reordenar particiones sanas y disparar migración de datos. Que un nodo muera <em>no</em> debería disparar eso. Debería disparar una decisión de runtime barata y local: elegir una réplica superviviente de la misma partición y hacerla el activo. El mapa sigue diciendo "P es propiedad de n1, n2, n3"; el runtime cambia de "activo = n1" a "activo = n2". Mantener estos dos planos separados es lo que permite que la conmutación sea rápida sin ser imprudente.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-1--detectar-pero-no-creerse-el-primer-rumor">Paso 1 — detectar, pero no creerse el primer rumor<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#paso-1--detectar-pero-no-creerse-el-primer-rumor" class="hash-link" aria-label="Enlace directo al Paso 1 — detectar, pero no creerse el primer rumor" title="Enlace directo al Paso 1 — detectar, pero no creerse el primer rumor" translate="no">​</a></h2>
<p>La conmutación por error está coordinada por el líder. Un nodo del clúster es el líder electo, y solo el líder promueve: un no-líder que oye un evento de nodo-salido simplemente lo ignora. Eso evita que dos nodos elijan independientemente dos nuevos dueños distintos para la misma partición.</p>
<p>Cuando el líder recibe una señal <code>NodeLeft</code> para un nodo, <em>no</em> promueve inmediatamente réplicas para cada partición que ese nodo poseía. La promoción se señala en el código como "an irreversible, high-impact action", y está protegida:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">string</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Equals</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">activeOwner</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nodeId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> StringComparison</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ordinal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// A NodeLeft message can race ahead of the peer-table update.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// If the "dead" owner is in fact still reachable, do NOT fail it over.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#d73a49">IsNodeAlive</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">nodeId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        Log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NodeUnavailableRejectedReachable</span><span class="token punctuation" style="color:#393A34">(</span><span class="token comment" style="color:#999988;font-style:italic">/* ... */</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">continue</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">PromoteOrMarkUnavailableAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">partition</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nodeId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> runtime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ct</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>La protección existe porque el <em>mensaje</em> de fallo y la <em>tabla de pares</em> de la capa de membresía se actualizan de forma asíncrona. Un <code>NodeLeft</code> puede llegar un latido antes de que la tabla de pares lo refleje, o estar rotundamente equivocado. Así que antes de la acción de alto impacto, el líder vuelve a comprobar la vitalidad directamente. Un dueño "muerto" todavía alcanzable se deja exactamente donde está.</p>
<p>Hay un segundo camino, más lento, para el caso que el primero no puede cubrir: un dueño activo que se ha quedado en silencio sin un <code>NodeLeft</code> limpio. Un barrido periódico de reconciliación (<code>HandleUnavailableActiveOwnersAsync</code>) recorre cada partición, y para un dueño que no está vivo pero no ha sido confirmado como caído, arranca un <strong>temporizador de sospecha</strong> en lugar de actuar:</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>Solo después de que el dueño haya estado inalcanzable durante toda la ventana de escalada (diez segundos por defecto) el líder lo confirma como caído y procede. El diseño es deliberadamente asimétrico: <em>marcar una partición como degradada por pérdida de réplica es barato y reversible, así que se dispara de inmediato; promover un nuevo activo es irreversible, así que espera a tener pruebas.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-2--elegir-un-superviviente-y-subir-la-época">Paso 2 — elegir un superviviente, y subir la época<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#paso-2--elegir-un-superviviente-y-subir-la-%C3%A9poca" class="hash-link" aria-label="Enlace directo al Paso 2 — elegir un superviviente, y subir la época" title="Enlace directo al Paso 2 — elegir un superviviente, y subir la época" translate="no">​</a></h2>
<p>Una vez confirmado que el dueño está caído, el líder elige una réplica para promover. La selección es <strong>primero la disponibilidad</strong>: entre las réplicas asignadas de la partición, prefiere la que ha sincronizado más segmentos, el nodo con más probabilidades de poder servir pronto.</p>
<p>La promoción en sí es una única difusión que avanza una <strong>época</strong> por partición:</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>La época es el token de delimitación (fencing token), y es el héroe silencioso de todo el mecanismo. Es monótona por partición, y el generador <em>se niega a retroceder</em>: una época propuesta que no sea estrictamente mayor que la más alta ya observada se rechaza de plano:</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>Así que tras la promoción, el activo de la partición es "n2 en la época 8". Si el antiguo dueño n1 estaba meramente particionado (no muerto) y vuelve creyendo que sigue siendo el activo en la época 7, su época obsoleta pierde toda comparación. No puede reclamar la propiedad, y cualquier escritura que intente aceptar como "activo" queda delimitada fuera (fenced). Así es como Zaris evita el split-brain de dos primarios que de otro modo dejaría que un antiguo dueño sanado-pero-confundido aceptara escrituras que nadie volverá a ver jamás.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-3--la-prueba-de-posesión-servir-o-sincronizar-primero">Paso 3 — la prueba de posesión: ¿<em>servir</em> o <em>sincronizar primero</em>?<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#paso-3--la-prueba-de-posesi%C3%B3n-servir-o-sincronizar-primero" class="hash-link" aria-label="Enlace directo al paso-3--la-prueba-de-posesión-servir-o-sincronizar-primero" title="Enlace directo al paso-3--la-prueba-de-posesión-servir-o-sincronizar-primero" translate="no">​</a></h2>
<p>Aquí está la parte sutil, y la parte que requirió depuración real para hacerla bien. Habiendo elegido la réplica más <em>disponible</em>, ¿puede realmente empezar a servir lecturas de inmediato?</p>
<p>No necesariamente, porque "más segmentos sincronizados" no es lo mismo que "tiene todas las claves confirmadas". Una réplica puede estar al día en el <em>recuento</em> de segmentos y aun así ir por detrás de los datos verdaderos en los segmentos que tiene. Promueve tal nodo directamente a un estado de servicio y se convierte en un <strong>activo incompleto</strong>: responde <code>NotFound</code> a claves que fueron confirmadas de forma durable en el dueño muerto y que <em>sí</em> existen en un hermano más completo. Eso es pérdida silenciosa de datos disfrazada de lectura exitosa.</p>
<p>Así que antes de permitir que el nodo promovido sirva, Zaris aplica una <strong>prueba de posesión</strong>. El líder mantiene una tabla fresca y emparejada por término (term-matched) de cuántas claves reporta tener cada nodo para la partición. La regla es una comparación pura:</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>Dos resultados:</p>
<ul>
<li class=""><strong>No existe ningún poseedor vivo estrictamente más completo</strong> — el candidato es el nodo más completo, o un genuino único superviviente sin nada de lo que sincronizar. Se promueve a <strong>Degraded</strong>: un estado de <em>servicio</em>. No tiene nada mejor por lo que esperar, así que acuñarlo como no-servicio solo haría la partición innecesariamente indisponible. Sirve de inmediato.</li>
<li class=""><strong>Sí existe un poseedor vivo estrictamente más completo</strong> — el candidato se promueve a <strong>Recovering</strong>: un estado <em>sin servicio</em>. Sincroniza primero desde esa fuente mejor y solo empieza a servir una vez que se demuestra completo. A los clientes se les dice que la partición todavía está arrancando, en lugar de entregarles un nodo que les mentiría.</li>
</ul>
<p>La distinción entre Degraded y Recovering es exactamente la distinción entre "la redundancia está reducida pero se está sirviendo la verdad" y "no confíes todavía en este dueño para lecturas". Lograr que un único superviviente entre correctamente en Degraded importa tanto como mantener un nodo incompleto en Recovering: una única copia restante nunca debe quedar acuñada como no-servicio para siempre solo porque es la única copia que queda.</p>
<p>Una nota rápida sobre honestidad: esta prueba está <strong>basada en recuentos</strong>, extraída de los reportes de claves-poseídas por nodo con una marca de agua de frescura (un reporte de un nodo que se ha quedado en silencio no cuenta como fuente viva). Es una protección fuerte y barata contra promover a servicio un nodo obviamente rezagado; no es una atestación criptográfica de que dos nodos tienen datos byte a byte idénticos. La convergencia que hace las copias <em>realmente</em> idénticas es el camino de sincronización de réplicas, cubierto más abajo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-4--el-cliente-se-redirecciona-y-tu-reintento-aterriza">Paso 4 — el cliente se redirecciona, y tu reintento aterriza<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#paso-4--el-cliente-se-redirecciona-y-tu-reintento-aterriza" class="hash-link" aria-label="Enlace directo al Paso 4 — el cliente se redirecciona, y tu reintento aterriza" title="Enlace directo al Paso 4 — el cliente se redirecciona, y tu reintento aterriza" translate="no">​</a></h2>
<p>Mientras todo esto ocurre en el servidor, ¿qué ve el cliente que estaba a mitad de escritura?</p>
<p>Ve la verdad, como un estado tipado. Una solicitud que llega al antiguo dueño (ahora delimitado, o simplemente ido) no se cuelga ni lanza un error vago: vuelve como <code>Moved</code> u <code>OwnershipChanging</code>. El cliente de Zaris los trata como señales de <em>redireccionamiento</em> reintentables: refresca su mapa de propiedad, descubre el nuevo activo y reproduce la operación ahí. En el caso común tu <code>PutAsync</code> simplemente tarda unos milisegundos más y devuelve <code>Ok</code>, contra el dueño recién promovido, en la nueva época. El código de la aplicación nunca se entera de que el dueño se movió.</p>
<p>Esa maquinaria del cliente —la taxonomía de estados, el reintento con presupuesto-de-intentos-y-plazo, el refresco del mapa— es una historia en sí misma, contada en <a class="" href="https://clustron.io/es/blog/retries-under-ownership-churn/">Reintentos que no escribes</a>. Para esta entrada el punto es solo que el aumento de época del servidor y el redireccionamiento del cliente son dos mitades de un solo traspaso: el servidor hace al antiguo dueño no-autoritativo, y el cliente deja de hablarle.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-5--converger-luego-sanar-de-vuelta-a-healthy">Paso 5 — converger, luego sanar de vuelta a Healthy<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#paso-5--converger-luego-sanar-de-vuelta-a-healthy" class="hash-link" aria-label="Enlace directo al Paso 5 — converger, luego sanar de vuelta a Healthy" title="Enlace directo al Paso 5 — converger, luego sanar de vuelta a Healthy" translate="no">​</a></h2>
<p>La promoción restaura la <em>disponibilidad</em>; no restaura la <em>redundancia</em>. Justo después de una conmutación por error la partición corre en Degraded con una copia menos de su objetivo. Entonces ocurren dos cosas en segundo plano.</p>
<p>Primero, el nodo promovido (si arrancó en Recovering) y cualquier réplica rezagada o que retorna <strong>se ponen al día reproduciendo el registro de operaciones por segmento</strong> desde una revisión conocida, el mismo sustrato reproducible que impulsa la replicación ordinaria. Un nodo que acaba de heredar el rol activo y una réplica que simplemente se quedó atrás convergen a través del mecanismo idéntico; ese es el tema de <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">Un registro, dos trabajos</a>.</p>
<p>Segundo, una vez que cada participante asignado está vivo de nuevo y sincronizado, el barrido de recuperación del líder transiciona la partición de vuelta a <strong>Healthy</strong>, pero solo mediante la misma difusión que avanza la época, y solo tras un breve enfriamiento para que un nodo oscilante no pueda arrastrar el runtime a través de una tormenta de transiciones. La máquina de estados de salud es por tanto un bucle pequeño y cerrado: <code>Healthy → Degraded/Recovering → Healthy</code>, cada arista un aumento de época, cada promoción delimitada.</p>
<p>Si no sobrevive ninguna réplica elegible en absoluto —todos los nodos asignados a la partición están caídos—, el líder hace lo único honesto: marca la partición como <strong>Unavailable</strong> (sin dueño, sin falsa promoción) e inicia una cuenta regresiva de desalojo que, si nadie retorna a tiempo, deja que una regeneración planificada del mapa reubique la partición. Una partición Unavailable falla lecturas y escrituras ruidosamente en lugar de servir una mentira.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dónde-se-pierde-genuinamente-una-escritura--dicho-con-claridad">Dónde se pierde genuinamente una escritura — dicho con claridad<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#d%C3%B3nde-se-pierde-genuinamente-una-escritura--dicho-con-claridad" class="hash-link" aria-label="Enlace directo al Dónde se pierde genuinamente una escritura — dicho con claridad" title="Enlace directo al Dónde se pierde genuinamente una escritura — dicho con claridad" translate="no">​</a></h2>
<p>Un mecanismo de conmutación por error gana confianza siendo preciso sobre sus bordes, no insinuando que no los hay.</p>
<ul>
<li class=""><strong>Factor de replicación 1.</strong> Una partición con una sola copia y sin réplica no tiene nada que promover. Cuando ese nodo muere la partición queda Unavailable hasta que retorne; los datos no se pierden (están en la memoria de ese nodo) pero están <em>indisponibles</em>, y si el nodo nunca retorna, se han ido. RF1 es no-durable <strong>por diseño</strong>: es una elección deliberada para datos que te puedes permitir perder, no una garantía que el camino de conmutación pueda rescatar.</li>
<li class=""><strong>Confirmación asíncrona.</strong> Zaris permite que una escritura se confirme en el dueño activo antes de que haya llegado a una réplica, por latencia. Ese es el camino rápido, y su coste honesto es una ventana: si el activo muere en el hueco entre confirmarle al cliente y que la operación llegue a cualquier poseedor superviviente, esa escritura confirmada se pierde. La prueba de posesión te protege de promover a servicio un nodo <em>conocido</em> como rezagado; no puede conjurar una operación que nunca salió del nodo muerto. Si necesitas que una escritura sobreviva a la pérdida de su dueño, debe confirmarse solo después de que sea durable en una réplica; el camino síncrono cambia un poco de latencia por exactamente esa garantía. Elige por carga de trabajo, con los ojos abiertos.</li>
<li class=""><strong>La ventana de confirmación misma.</strong> Durante el tiempo de escalada tras que un dueño se queda en silencio, las escrituras de la partición están en el limbo: reintentadas por el cliente, aún sin aterrizar en un nuevo dueño. La corrección se preserva (nada se promueve erróneamente), pero la disponibilidad cae durante esa ventana. Apretar el tiempo de espera cambia una conmutación más rápida por más falsas promociones bajo parpadeos transitorios; es un dial, no un almuerzo gratis.</li>
</ul>
<p>Ninguno de estos son errores que el camino de conmutación pueda diseñar para eliminar: son las genuinas disyuntivas de disponibilidad frente a durabilidad frente a latencia, y el trabajo de Zaris es hacer que la elección <em>segura</em> sea la predeterminada y la elección <em>rápida</em> sea explícita, nunca fingir que la ventana no existe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forma-de-todo-esto">La forma de todo esto<a href="https://clustron.io/es/blog/anatomy-of-a-failover/#la-forma-de-todo-esto" class="hash-link" aria-label="Enlace directo al La forma de todo esto" title="Enlace directo al La forma de todo esto" translate="no">​</a></h2>
<p>Reduce la conmutación por error a lo esencial y son cuatro movimientos sobre un invariante. El invariante: el mapa se queda quieto; solo se mueve el activo del runtime. Los movimientos: <strong>confirmar</strong> la muerte antes de actuar sobre ella, <strong>promover</strong> a un superviviente detrás de una época monótona que delimita al antiguo dueño, <strong>probar la posesión</strong> para que el nuevo dueño sirva la verdad o sincronice antes de servir siquiera, y <strong>redireccionar</strong> al cliente para que su reintento aterrice en el nuevo dueño. Luego converger las copias de vuelta y sanar a Healthy.</p>
<p>Que se sostenga no es una cuestión de argumento: está medido. El mismo camino de promoción, posesión y convergencia corre continuamente bajo caos deliberado de eliminación de nodos durante horas seguidas, con el libro mayor revisado al final en busca de una sola escritura confirmada perdida. Esa certificación es su propia historia: <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">Ninguna escritura confirmada se queda atrás</a>.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Caché lista para usar en ASP.NET Core: IDistributedCache y HybridCache sobre Zaris]]></title>
            <link>https://clustron.io/es/blog/aspnet-core-distributed-cache/</link>
            <guid>https://clustron.io/es/blog/aspnet-core-distributed-cache/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris incluye tres paquetes de integración con ASP.NET Core —un IDistributedCache estándar, un HybridCache de dos niveles con protección contra estampidas e invalidación por etiquetas, y un almacén de sesiones con expiración deslizante— para que una aplicación existente pueda cachear en un clúster Zaris sin tocar el código que usa la caché. Aquí explicamos qué hace cada uno, y los bordes honestos de cada uno.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Caché distribuida lista para usar en ASP.NET Core sobre Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJEcm9wLWluIGRpc3RyaWJ1dGVkIGNhY2hlIGZvciBBU1AuTkVUIENvcmUgb24gWmFyaXMiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5BU1AuTkVUIENPUkU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+RHJvcC1pbiBjYWNoZSw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+dHdvIHRpZXJzLCBvbmUgQVBJPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+SURpc3RyaWJ1dGVkQ2FjaGUgJiMzODsgSHlicmlkQ2FjaGUgb24gWmFyaXM8L3RleHQ+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcyNiwyNTApIj4NCiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI3NSIgeT0iMjkiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj55b3VyIGFwcDwvdGV4dD4NCiAgICA8cGF0aCBkPSJNNzUgNDggTDc1IDg2IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI0Ii8+DQogICAgPHJlY3QgeD0iMCIgeT0iODYiIHdpZHRoPSIxNTAiIGhlaWdodD0iNTYiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iNzUiIHk9IjExMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPkwxIMK3IGluLXByb2M8L3RleHQ+DQogICAgPHRleHQgeD0iNzUiIHk9IjEzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPm5vIG5ldHdvcms8L3RleHQ+DQogICAgPHBhdGggZD0iTTE2MCAxMTQgTDI1MiAxMTQiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtZGFzaGFycmF5PSI2IDYiLz4NCiAgICA8dGV4dCB4PSIyMDYiIHk9IjEwNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm1pc3M8L3RleHQ+DQogICAgPHJlY3QgeD0iMjU4IiB5PSI4NiIgd2lkdGg9IjE2MiIgaGVpZ2h0PSI1NiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzM5IiB5PSIxMTAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5MMiDCtyBjbHVzdGVyPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjMzOSIgeT0iMTMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzVhNmI2NSI+c2hhcmVkLCBkdXJhYmxlPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik0zMzkgMTQyIEwzMzkgMTgwIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWRhc2hhcnJheT0iNiA2Ii8+DQogICAgPHRleHQgeD0iMzM5IiB5PSIyMDAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5mYWN0b3J5IHJ1bnMgb25jZTwvdGV4dD4NCiAgICA8Y2lyY2xlIGN4PSI3NSIgY3k9IjExNCIgcj0iNiIgZmlsbD0iIzJiZDZhMCIvPg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>La mayoría de las razones para recurrir a una caché distribuida en una aplicación .NET nunca mencionan una caché por su nombre. Añades caché de salida (output caching) y quiere un <code>IDistributedCache</code>. Habilitas sesiones y el middleware quiere un almacén de respaldo. Activas el uso compartido de claves de protección de datos entre instancias y, de nuevo, quiere un lugar donde poner bytes que todos los nodos puedan ver. El código de la aplicación está escrito contra una <em>abstracción</em>; la única pregunta es qué implementación registras al arrancar.</p>
<p>Zaris responde a esa pregunta con tres pequeños paquetes NuGet, cada uno apuntando a una costura específica de ASP.NET Core: un <code>IDistributedCache</code> simple, un <code>HybridCache</code> de dos niveles y un almacén de sesiones. Registras una línea al arrancar y el resto de tu aplicación —incluidas las bibliotecas de terceros que solo conocen la abstracción— cachea en un clúster Zaris sin un solo cambio en el código que usa la caché. Esta entrada recorre los tres, y es deliberada sobre dónde se detiene honestamente cada uno.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="los-tres-paquetes">Los tres paquetes<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#los-tres-paquetes" class="hash-link" aria-label="Enlace directo al Los tres paquetes" title="Enlace directo al Los tres paquetes" translate="no">​</a></h2>
<table><thead><tr><th>Paquete</th><th>Implementa</th><th>Úsalo para</th></tr></thead><tbody><tr><td><code>Clustron.Zaris.DistributedCache</code></td><td><code>IDistributedCache</code></td><td>Cualquier biblioteca o código que pida la abstracción estándar de caché distribuida</td></tr><tr><td><code>Clustron.Zaris.HybridCache</code></td><td><code>HybridCache</code> (de <code>Microsoft.Extensions.Caching.Hybrid</code>)</td><td>Un camino caliente que quiere un L1 en proceso delante de la caché compartida, con protección contra estampidas</td></tr><tr><td><code>Clustron.Zaris.AspNetCore.Session</code></td><td><code>ISessionStore</code></td><td>Sesiones de ASP.NET Core con un tiempo de espera de inactividad deslizante</td></tr></tbody></table>
<p>Los tres se apoyan en el mismo cliente .NET de Zaris. No inventan un canal lateral: resuelven un cliente Zaris normal a partir de una cadena de conexión y traducen <code>Get</code>/<code>Set</code>/<code>Remove</code> a los <code>GetAsync</code>/<code>PutAsync</code>/<code>DeleteAsync</code> del cliente. Si has leído <a class="" href="https://clustron.io/es/blog/connection-strings/">Una cadena de conexión, cualquier plataforma</a>, el cableado te resultará familiar: la caché es simplemente otro consumidor de un almacén con nombre.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-el-idistributedcache-estándar">1. El <code>IDistributedCache</code> estándar<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#1-el-idistributedcache-est%C3%A1ndar" class="hash-link" aria-label="Enlace directo al 1-el-idistributedcache-estándar" title="Enlace directo al 1-el-idistributedcache-estándar" translate="no">​</a></h2>
<p>Este es el de usar y listo. <code>IDistributedCache</code> es la interfaz que <code>Microsoft.Extensions.Caching.Abstractions</code> ha expuesto durante años, y una gran parte del ecosistema .NET —sesiones, caché de salida, almacenes de tickets de autenticación, bibliotecas de terceros— depende exactamente de esa interfaz y de nada más. Registra Zaris como la implementación y todo eso queda respaldado por tu clúster.</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZaris</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"app-cache"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"zaris://cache-host:7861/app-cache"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">builder</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Services</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronDistributedCache</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">storeName</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"app-cache"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token named-parameter punctuation" style="color:#393A34">configure</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> o </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">KeyPrefix </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"myapp:"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La primera línea registra un cliente Zaris bajo la clave de DI <code>app-cache</code>; la segunda registra un <code>IDistributedCache</code> que lo usa. A partir de aquí, cualquier cosa que acepte un <code>IDistributedCache</code> —incluido código que no escribiste tú— funciona:</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>Por debajo, <code>GetAsync</code> es un <code>client.GetAsync&lt;byte[]&gt;</code> con el prefijo configurado aplicado, y un fallo de caché (la clave no existe) devuelve <code>null</code>, exactamente el contrato que la interfaz promete. <code>SetAsync</code> con <code>AbsoluteExpirationRelativeToNow</code> mapea esa ventana directamente a un TTL de Zaris, de modo que el almacén expira la entrada por ti.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="dos-bordes-honestos">Dos bordes honestos<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#dos-bordes-honestos" class="hash-link" aria-label="Enlace directo al Dos bordes honestos" title="Enlace directo al Dos bordes honestos" translate="no">​</a></h3>
<p><strong>Solo se mapea la expiración absoluta relativa.</strong> El <code>IDistributedCache</code> de Zaris lee <code>AbsoluteExpirationRelativeToNow</code> y lo convierte en un TTL. Si no estableces expiración, la entrada se escribe sin TTL y vive hasta que se desaloja o se sobrescribe. <code>SlidingExpiration</code> en esta caché de propósito general <strong>no</strong> se respeta; su <code>Refresh</code>/<code>RefreshAsync</code> es una operación nula. Esa es una elección deliberada de alcance, no un descuido: la expiración deslizante es un asunto de sesiones, y el paquete de sesiones de más abajo la implementa correctamente en lugar de implementarla a medias aquí.</p>
<p><strong>Puedes registrar más de uno, y el primero gana por defecto.</strong> Cada llamada a <code>AddClustronDistributedCache</code> registra una instancia de caché con nombre vinculada a un almacén específico. El primero registrado se convierte en el <code>IDistributedCache</code> de toda la aplicación; pasa <code>setAsDefault: true</code> para hacer que un registro posterior sea el predeterminado en su lugar. Si quieres varias cachés en almacenes distintos, resuélvelas explícitamente a través de <code>IClustronCacheProvider.GetCache("other-store")</code> en lugar de depender del predeterminado ambiental.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-hybridcache--un-nivel-en-proceso-delante-del-clúster">2. <code>HybridCache</code> — un nivel en proceso delante del clúster<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#2-hybridcache--un-nivel-en-proceso-delante-del-cl%C3%BAster" class="hash-link" aria-label="Enlace directo al 2-hybridcache--un-nivel-en-proceso-delante-del-clúster" title="Enlace directo al 2-hybridcache--un-nivel-en-proceso-delante-del-clúster" translate="no">​</a></h2>
<p>La abstracción más reciente de .NET, <code>HybridCache</code>, resuelve un problema que <code>IDistributedCache</code> te deja a ti: la combinación de una caché local rápida (L1) y una caché distribuida compartida (L2), más la <em>protección contra estampidas</em>, asegurando que cuando una clave popular expira, una estampida de solicitudes concurrentes no ejecute todas a la vez la costosa fábrica (factory).</p>
<p>Zaris implementa directamente la clase abstracta <code>HybridCache</code>. El giro es que <strong>ambos niveles son almacenes Zaris que tú nombras</strong>: apuntas L1 a un <a class="" href="https://clustron.io/es/blog/embedded-inproc-mode/">almacén embebido</a> en proceso (sin red, sin salto de serialización fuera de la máquina) y L2 al clúster distribuido:</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>El camino de lectura es la clásica búsqueda de dos niveles, y vale la pena verlo completo porque cada garantía de más abajo se desprende de él:</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 se comprueba primero sin bloqueo. Ante un fallo, el llamante toma un <strong>bloqueo por clave</strong>, vuelve a comprobar L1 (por si otro llamante acaba de rellenarlo), luego L2; solo si ambos fallan ejecuta la fábrica, escribe L2 y luego escribe L1. Los TTL vienen de <code>HybridCacheEntryOptions</code> —<code>Expiration</code> para L2, <code>LocalCacheExpiration</code> para L1— y la implementación limita L1 a no ser más largo que L2, de modo que nunca puedes tener una copia local que sobreviva a la compartida. Omite las opciones y ambos tienen por defecto cinco minutos.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="protección-contra-estampidas-medida">Protección contra estampidas, medida<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#protecci%C3%B3n-contra-estampidas-medida" class="hash-link" aria-label="Enlace directo al Protección contra estampidas, medida" title="Enlace directo al Protección contra estampidas, medida" translate="no">​</a></h3>
<p>El bloqueo por clave es la protección contra estampidas. La propia prueba del paquete dispara veinte llamadas concurrentes a <code>GetOrCreateAsync</code> sobre una clave fría, cada una con una fábrica que duerme 100 ms, y comprueba que la fábrica se ejecutó <strong>exactamente una vez</strong>: diecinueve llamantes esperan en el bloqueo, y el que rellenó la caché los sirve a todos. Eso convierte un potencial martilleo de veinte vías sobre tu base de datos en una sola carga.</p>
<p>El borde honesto: este bloqueo vive en un <code>ConcurrentDictionary&lt;string, SemaphoreSlim&gt;</code> <strong>dentro del proceso</strong>. Colapsa la estampida dentro de una instancia de aplicación. Con diez instancias de aplicación todas frías sobre la misma clave en el mismo instante, todavía puedes ver hasta diez ejecuciones de fábrica, una por proceso, no una a nivel de todo el clúster. Esa es la forma normal de la protección contra estampidas del lado del cliente, y para el caso común (un puñado de instancias, una fábrica que es costosa pero no catastrófica de ejecutar unas pocas veces) es exactamente lo que quieres. Si necesitas estrictamente una sola carga en toda la flota, eso es un problema de bloqueo distribuido, no una característica de caché, y un trabajo mejor modelado con el patrón de reclamación por comparación e intercambio (compare-and-swap) de <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">Coordinación sin condiciones de carrera</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="invalidación-por-etiquetas-y-por-qué-es-perezosa">Invalidación por etiquetas, y por qué es perezosa<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#invalidaci%C3%B3n-por-etiquetas-y-por-qu%C3%A9-es-perezosa" class="hash-link" aria-label="Enlace directo al Invalidación por etiquetas, y por qué es perezosa" title="Enlace directo al Invalidación por etiquetas, y por qué es perezosa" translate="no">​</a></h3>
<p><code>HybridCache</code> te permite etiquetar entradas e invalidar un grupo entero de una vez:</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 implementa esto con marcas de tiempo en lugar de un barrido activo. Cada entrada se escribe con dos etiquetas de metadatos: la hora en que se creó y sus etiquetas. <code>RemoveByTagAsync("catalog")</code> simplemente escribe una clave que registra <em>"catalog fue invalidado en el instante T."</em> En la siguiente lectura de una entrada etiquetada, la caché compara: si alguna de las etiquetas de la entrada fue invalidada <em>después</em> de que se creara la entrada, la entrada se trata como un fallo y la fábrica se vuelve a ejecutar. La prueba lo confirma: etiqueta una entrada, invalida la etiqueta, lee de nuevo, y la fábrica se llama por segunda vez.</p>
<p>La disyuntiva de diseño que hay que conocer: la invalidación es <strong>perezosa y disparada por lectura</strong>, no una purga ávida. Llamar a <code>RemoveByTagAsync</code> es O(1) —escribe un marcador y retorna— pero las entradas obsoletas en sí permanecen residentes en el almacén hasta que algo las lee (y obtiene un valor fresco) o su TTL expira. Obtienes una invalidación <em>lógica</em> barata e instantánea; no obtienes una recuperación inmediata de la memoria que esas entradas ocupan. Para datos de caché ese es exactamente el trato correcto. Simplemente no te apoyes en ello como una forma de forzar el borrado de datos por razones de corrección; para eso, <code>RemoveAsync(key)</code> elimina ambos niveles de plano.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-sesiones-con-expiración-deslizante-real">3. Sesiones, con expiración deslizante real<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#3-sesiones-con-expiraci%C3%B3n-deslizante-real" class="hash-link" aria-label="Enlace directo al 3. Sesiones, con expiración deslizante real" title="Enlace directo al 3. Sesiones, con expiración deslizante real" translate="no">​</a></h2>
<p>Las sesiones de ASP.NET Core necesitan un almacén que haga algo que la caché general deliberadamente no hace: <strong>deslizar</strong> la expiración en cada solicitud que toca la sesión, de modo que la sesión de un usuario activo nunca expire por debajo de él mientras que una inactiva eventualmente sí. El paquete de sesiones es un <code>IDistributedCache</code> hecho a propósito que respeta exactamente esa semántica, cableado con una llamada:</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>Dale una cadena de conexión y registra el almacén Zaris de respaldo por ti; omítela y se vincula a un almacén que ya hayas registrado bajo <code>StoreName</code>. En cualquier caso, luego usas <code>HttpContext.Session</code> exactamente como siempre lo has hecho.</p>
<p>La pieza que importa es <code>Refresh</code>. Cuando el middleware de sesión carga una sesión que fue leída pero no modificada, llama a <code>RefreshAsync</code> para mantenerla viva. La caché de sesión de Zaris implementa eso como una lectura-y-luego-reescritura con un TTL fresco igual al tiempo de espera de inactividad, de modo que cada solicitud que toca la sesión desliza la ventana de 30 minutos hacia delante. Una escritura (<code>SetAsync</code>) resuelve su TTL a partir de la expiración deslizante o absoluta de la entrada, recurriendo al tiempo de espera de inactividad configurado. El resultado es el comportamiento estándar de sesiones de ASP.NET Core, respaldado por un clúster Zaris, sobreviviendo a un reinicio de instancia de aplicación porque los datos de sesión viven en el almacén y no en la memoria de la instancia.</p>
<p>Una nota deliberada de implementación tomada del propio código: <code>Refresh</code> es una lectura-más-reescritura en lugar de una llamada nativa de "simplemente reiniciar el TTL". Eso cuesta una ida y vuelta extra, pero mantiene el deslizamiento correcto en <em>cada</em> despliegue de Zaris —incluido el embebido en proceso— sin depender de una primitiva de reinicio-de-TTL del lado del servidor. La corrección en cada topología superó a ahorrar una ida y vuelta en una de ellas.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="elegir-el-correcto">Elegir el correcto<a href="https://clustron.io/es/blog/aspnet-core-distributed-cache/#elegir-el-correcto" class="hash-link" aria-label="Enlace directo al Elegir el correcto" title="Enlace directo al Elegir el correcto" translate="no">​</a></h2>
<ul>
<li class="">Recurre a <strong><code>IDistributedCache</code></strong> cuando algo ya lo pide, o cuando quieres la caché compartida más simple y una biblioteca va a consumirla por ti. Es la costura de máxima compatibilidad.</li>
<li class="">Recurre a <strong><code>HybridCache</code></strong> en un camino de lectura genuinamente caliente donde un L1 en proceso se gana su sitio y una estampida sobre una clave popular fría dolería. Pagas por una pieza móvil extra (dos almacenes, memoria local para L1); obtienes lecturas repetidas de latencia casi nula y una estampida colapsada a una carga por instancia.</li>
<li class="">Recurre a <strong><code>AddZarisSession</code></strong> específicamente para sesiones de ASP.NET Core; es la que tiene expiración deslizante real, y se mantiene al margen del propio <code>IDistributedCache</code> de tu aplicación.</li>
</ul>
<p>Ninguno de los tres te pide aprender una API específica de caché. Se encuentran con el framework allí donde ya define la abstracción, se registran en una línea y ponen los bytes en un clúster que puedes escalar, asegurar y ejecutar en Kubernetes independientemente de la aplicación que tiene delante. La mejor integración es la que el código de tu aplicación nunca tiene que notar, y, con grata frecuencia, esa es exactamente la que obtienes.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>.NET</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Qué primitiva, para qué problema: una guía de campo del modelo de datos de Zaris]]></title>
            <link>https://clustron.io/es/blog/choosing-a-zaris-primitive/</link>
            <guid>https://clustron.io/es/blog/choosing-a-zaris-primitive/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris es un único núcleo clave-valor versionado, particionado y replicado, más un puñado de vistas tipadas y capas de coordinación por encima de él. Lo difícil no es lo que puede hacer, sino elegir la herramienta adecuada para el problema que tienes delante. Esta es una guía de decisión que recorre el KV plano, compare-and-swap, las transacciones multiclave, las colecciones nativas, los streams, watch y los locks, con el encaje honesto y el límite honesto de cada uno.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Elegir la primitiva de Zaris adecuada para el problema" 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>La mayor parte de este blog ha profundizado en una cosa a la vez: <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">compare-and-swap</a>, <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacciones multiclave</a>, <a class="" href="https://clustron.io/es/blog/data-structures-that-replicate/">estructuras de datos nativas</a>, <a class="" href="https://clustron.io/es/blog/redis-streams-native/">streams</a>, <a class="" href="https://clustron.io/es/blog/2026/03/30/watch-changes/">watch</a>, el <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">op-log</a> que hay debajo. Cada artículo responde a "cómo funciona <em>esto</em>". Este responde a una pregunta distinta y más práctica: <strong>cuando tienes un problema delante, ¿cuál eliges?</strong></p>
<p>Esa pregunta importa porque la elección equivocada rara vez es un fallo estrepitoso. Es un sistema que funciona en la demo y luego pierde una actualización bajo carga, o canaliza a mil escritores a través de una sola clave, o usa una notificación de tipo "dispara y olvida" donde hacía falta un log durable. Las primitivas no te impiden usarlas mal. Así que esto es una guía de campo: el modelo mental que las une, una tabla de decisión, un recorrido por cada rama con su encaje honesto y su límite honesto, y un diagrama de flujo al final.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="un-núcleo-muchas-vistas">Un núcleo, muchas vistas<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#un-n%C3%BAcleo-muchas-vistas" class="hash-link" aria-label="Enlace directo al Un núcleo, muchas vistas" title="Enlace directo al Un núcleo, muchas vistas" translate="no">​</a></h2>
<p>Empecemos por aquello sobre lo que se construye todo lo demás. Debajo de cada funcionalidad de este artículo, Zaris es <strong>un único almacén clave-valor particionado y replicado, y cada valor lleva una versión monótona propiedad del almacén.</strong> Eso es todo. Ese es el núcleo.</p>
<p>Todo lo demás es una de dos cosas:</p>
<ul>
<li class=""><strong>Una <em>vista</em> tipada, del lado del servidor, sobre las claves</strong>: un hash, una lista, un conjunto, un conjunto ordenado, un stream. No son blobs que recuperas y mutas en tu proceso; son colecciones reales del lado del servidor que viven en la partición dueña de su clave, y se replican exactamente igual que un valor plano.</li>
<li class=""><strong>Una <em>capa</em> de coordinación sobre la versión</strong>: compare-and-swap, transacciones multiclave, locks, watch. Estas usan la versión (y la maquinaria de propiedad que la rodea) para que muchos clientes se pongan de acuerdo sobre lo que ocurrió sin pisarse unos a otros.</li>
</ul>
<p>Ten presente esa imagen y toda la superficie deja de parecer un cajón de sastre de funcionalidades y empieza a parecer lo que es: un núcleo pequeño y un conjunto de herramientas hechas a medida, cada una de las cuales resuelve una clase concreta de problema por encima de él. Elige según la <em>forma de tu problema</em>, no por el nombre que más se le parezca.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-tabla-de-decisión">La tabla de decisión<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#la-tabla-de-decisi%C3%B3n" class="hash-link" aria-label="Enlace directo al La tabla de decisión" title="Enlace directo al La tabla de decisión" translate="no">​</a></h2>
<table><thead><tr><th>Tu problema</th><th>Usa</th><th>Por qué</th><th>Ten cuidado con</th></tr></thead><tbody><tr><td>Guardar bytes que otros nodos puedan volver a leer</td><td><strong><code>GetAsync</code>/<code>PutAsync</code> planos</strong>, o una <a class="" href="https://clustron.io/es/blog/aspnet-core-distributed-cache/">integración de caché</a></td><td>El núcleo, nada más</td><td>Sin expiración en el momento de escritura: el TTL es una segunda llamada (abajo)</td></tr><tr><td>Un único valor debe cambiar de forma segura bajo concurrencia</td><td><strong><a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">Compare-and-swap</a></strong></td><td>Un único ganador por ronda, sin actualizaciones perdidas, sin mantener un lock</td><td>Una clave única caliente se convierte en un punto de serialización: particiónala</td></tr><tr><td>Un invariante abarca dos o más claves</td><td><strong><a class="" href="https://clustron.io/es/blog/multi-key-transactions/">Transacción multiclave</a></strong></td><td>Todo o nada entre claves <em>y</em> entre nodos</td><td>Más pesada que CAS; es un 2PC, no es gratis</td></tr><tr><td>El dato <em>es</em> una colección: mapa, lista, pertenencia, ranking</td><td><strong><a class="" href="https://clustron.io/es/blog/data-structures-that-replicate/">Estructuras de datos nativas</a></strong></td><td>Operaciones por elemento en el servidor, sin reescribir el valor completo</td><td>Elige la colección que encaje con el patrón de acceso</td></tr><tr><td>Un log de eventos ordenado y reproducible con consumidores at-least-once</td><td><strong><a class="" href="https://clustron.io/es/blog/redis-streams-native/">Streams</a></strong></td><td>Log durable, grupos de consumidores, pending por consumidor + ack</td><td>Es un log, no una cola con prioridades</td></tr><tr><td>Reaccionar a los cambios según ocurren</td><td><strong><a class="" href="https://clustron.io/es/blog/2026/03/30/watch-changes/">Watch</a></strong> (nativo) / <a class="" href="https://clustron.io/es/blog/redis-keyspace-notifications/">notificaciones de keyspace</a> (clientes Redis)</td><td>Replicado, a nivel de clúster, sobrevive al failover</td><td>El pub/sub de tipo "dispara y olvida" <em>no</em> es un feed durable</td></tr><tr><td>Exclusión mutua gruesa en torno a un recurso</td><td><strong><a class="" href="https://clustron.io/es/blog/distributed-locks-explained/">Lock</a></strong> (basado en lease)</td><td>Una compuerta con nombre y acotada por TTL que se libera sola al caer</td><td>Un lock basado en lease no es una barrera de correctitud: ver abajo</td></tr></tbody></table>
<p>El resto de este artículo es esa tabla, a cámara lenta.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kv-y-caché-cuando-un-valor-es-solo-un-valor">KV y caché: cuando un valor es solo un valor<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#kv-y-cach%C3%A9-cuando-un-valor-es-solo-un-valor" class="hash-link" aria-label="Enlace directo al KV y caché: cuando un valor es solo un valor" title="Enlace directo al KV y caché: cuando un valor es solo un valor" translate="no">​</a></h2>
<p>Si lo único que necesitas es poner bytes en algún sitio donde todos los nodos puedan leerlos —un fragmento renderizado, un DTO serializado, una instantánea de configuración— no necesitas una capa de coordinación. <code>PutAsync(key, value)</code> y <code>GetAsync&lt;T&gt;(key)</code> son toda la historia, y en el lado del servidor esos bytes se particionan y replican por ti.</p>
<p>Dos notas honestas. Primera, <strong>la expiración no se fija en el momento de escritura.</strong> No hay un campo de TTL en las opciones de escritura; fijas un tiempo de vida con una llamada aparte —<code>ExpireAsync(key, ttl)</code>— y lo borras con <code>PersistAsync(key)</code>, o lees lo que queda con <code>GetTimeToLiveAsync(key)</code>. Segunda, si tu caso de uso es literalmente "una caché distribuida para mi aplicación ASP.NET Core", no te lo montes a mano: registra uno de los <a class="" href="https://clustron.io/es/blog/aspnet-core-distributed-cache/">paquetes de caché listos para usar</a> y el <code>IDistributedCache</code> del framework (o <code>HybridCache</code>, o el almacén de sesiones) queda respaldado por tu clúster con un cambio de una línea en el arranque y sin código que toque la abstracción de caché.</p>
<p><strong>Usa KV plano cuando</strong> el valor no tiene ningún invariante de concurrencia: que gane el último escritor está bien, o solo hay un escritor. <strong>No lo uses</strong> en cuanto dos escritores puedan competir por la misma clave y te importe que uno sobrescriba al otro en silencio. Ese es el trabajo de la siguiente herramienta.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="compare-and-swap-cuando-un-valor-debe-seguir-siendo-correcto-bajo-concurrencia">Compare-and-swap: cuando un valor debe seguir siendo correcto bajo concurrencia<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#compare-and-swap-cuando-un-valor-debe-seguir-siendo-correcto-bajo-concurrencia" class="hash-link" aria-label="Enlace directo al Compare-and-swap: cuando un valor debe seguir siendo correcto bajo concurrencia" title="Enlace directo al Compare-and-swap: cuando un valor debe seguir siendo correcto bajo concurrencia" translate="no">​</a></h2>
<p>Un contador con un techo, un saldo que no puede quedar negativo, un objeto de configuración que dos administradores editan a la vez. El fallo que intentas evitar es la <em>actualización perdida</em>: leer, leer, escribir, escribir, y la segunda escritura borra la primera. CAS convierte esa pérdida silenciosa en un conflicto detectado y reintentable: lee un valor <em>y su versión</em>, vuelve a escribir solo si la versión no se ha movido, y obtén un <code>Conflict</code> en vez de una sobrescritura si alguien se te adelantó.</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>Ese es todo el mecanismo, y la <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">guía de campo de CAS</a> muestra hasta dónde llega: un limitador de ritmo, una cartera que nunca queda en descubierto bajo mil débitos concurrentes, una reclamación de trabajo con un único ganador (mediante "crear solo si está ausente"), configuración sin actualizaciones perdidas, y el gancho de idempotencia que viene de regalo.</p>
<p><strong>Usa CAS cuando</strong> el invariante vive en <em>una</em> clave y quieres correctitud sin mantener un lock. <strong>No</strong> apuntes a una multitud de escritores a una única clave caliente y esperes que escale: CAS deja ganar deliberadamente a un único escritor por ronda, así que una clave caliente se convierte en un punto de serialización. Particiónala (un bucket por cliente-segundo, un saldo dividido en subsaldos reconciliados) y la contención se disuelve.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="transacciones-multiclave-cuando-el-invariante-abarca-varias-claves">Transacciones multiclave: cuando el invariante abarca varias claves<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#transacciones-multiclave-cuando-el-invariante-abarca-varias-claves" class="hash-link" aria-label="Enlace directo al Transacciones multiclave: cuando el invariante abarca varias claves" title="Enlace directo al Transacciones multiclave: cuando el invariante abarca varias claves" translate="no">​</a></h2>
<p>La línea dura que traza CAS es que es atómico sobre <em>una</em> clave. Mover dinero de la cuenta A a la cuenta B, o mantener un índice secundario en sincronía con el registro al que apunta, y ningún compare-and-swap único cubre ambos. Eso no es motivo para fingirlo con dos escrituras CAS y cruzar los dedos: es el caso exacto para el que existe la <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacción multiclave</a>.</p>
<p>Abres una transacción, preparas lecturas y escrituras dentro de ella y haces commit. Por debajo es un <strong>commit en dos fases coordinado por el cliente sobre versiones optimistas</strong>: las claves pueden vivir en nodos distintos, así que el cliente agrupa el conjunto de escrituras por dueño, cada nodo comprueba que cada clave que leíste <em>sigue</em> en la versión en la que la leíste y toma un lock de corta duración, y solo si todos votan que sí se aplica el conjunto completo. Cualquier versión que se haya movido, y el commit devuelve <code>Conflict</code> sin que nada cambie.</p>
<p><strong>Usa una transacción cuando</strong> dos o más claves deben moverse juntas o no moverse en absoluto. <strong>No</strong> la uses como tu camino de escritura por defecto: es un 2PC de verdad, con viajes de ida y vuelta de prepare/commit y locks de corta duración, más pesado que un único CAS. Si el invariante encaja honestamente en una clave, mantenlo en una clave.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="estructuras-de-datos-nativas-cuando-el-dato-es-una-colección">Estructuras de datos nativas: cuando el dato <em>es</em> una colección<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#estructuras-de-datos-nativas-cuando-el-dato-es-una-colecci%C3%B3n" class="hash-link" aria-label="Enlace directo al estructuras-de-datos-nativas-cuando-el-dato-es-una-colección" title="Enlace directo al estructuras-de-datos-nativas-cuando-el-dato-es-una-colección" translate="no">​</a></h2>
<p>A veces el valor no es un escalar que custodias, sino una forma. Un perfil de usuario es un mapa de campos. Un feed de actividad reciente es una lista. La pertenencia a una etiqueta es un conjunto. Una tabla de clasificación es un ranking. <em>Podrías</em> serializar cada uno como un solo blob y hacerle CAS, pero entonces cada edición de un solo campo reescribe el objeto entero, y has convertido una actualización O(1) en una O(n).</p>
<p>Zaris les da sus propios <a class="" href="https://clustron.io/es/blog/data-structures-that-replicate/">tipos de colección nativos del lado del servidor</a> —hashes, listas, conjuntos, conjuntos ordenados— donde una operación de un solo campo o de un solo elemento toca únicamente ese elemento en el servidor, no el valor entero en tu proceso. Se particionan y replican como cualquier clave, y como el <a class="" href="https://clustron.io/es/blog/redis-protocol-any-language/">front-end RESP</a> mapea los mismos comandos de Redis sobre las mismas colecciones del lado del servidor, un servicio .NET y un servicio Python que usan <code>HSET</code> tocan el mismo hash idéntico.</p>
<p><strong>Usa una colección cuando</strong> tu patrón de acceso es por elemento: fijar un campo, empujar una entrada, comprobar una pertenencia, leer el top N por puntuación. <strong>No</strong> recurras a un conjunto ordenado cuando lo que realmente necesitas es un log de eventos ordenado, confirmado y reproducible. Eso es un stream.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="streams-cuando-necesitas-un-log-ordenado-y-reproducible">Streams: cuando necesitas un log ordenado y reproducible<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#streams-cuando-necesitas-un-log-ordenado-y-reproducible" class="hash-link" aria-label="Enlace directo al Streams: cuando necesitas un log ordenado y reproducible" title="Enlace directo al Streams: cuando necesitas un log ordenado y reproducible" translate="no">​</a></h2>
<p>Una cola y un log se parecen hasta que un consumidor se cae. Los <a class="" href="https://clustron.io/es/blog/redis-streams-native/">streams</a> de Zaris son un log de solo anexado de verdad: las entradas reciben IDs monótonos, puedes leer rangos, y un <strong>grupo de consumidores</strong> reparte el log entre un conjunto de trabajadores, lleva la cuenta del <em>pending</em> de cada trabajador (entregado pero sin confirmar) y permite que otro trabajador reclame lo que uno caído nunca llegó a confirmar. Esa maquinaria de pending y ack es lo que te compra la entrega at-least-once, lo que una notificación de tipo "dispara y olvida" no puede darte.</p>
<p><strong>Usa un stream cuando</strong> necesitas eventos durables, ordenados y reproducibles con confirmación explícita y reparto de trabajo: ingesta de pedidos, un outbox, una traza de auditoría a partir de la cual una proyección se reconstruye. <strong>No</strong> lo uses como cola con prioridades ni como canal de petición/respuesta; es un log, y sus fortalezas son el orden y la reproducción, no el enrutamiento.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="watch-notificaciones-y-la-familia-del-fan-out-cuando-reaccionas-al-cambio">Watch, notificaciones y la familia del fan-out: cuando reaccionas al cambio<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#watch-notificaciones-y-la-familia-del-fan-out-cuando-reaccionas-al-cambio" class="hash-link" aria-label="Enlace directo al Watch, notificaciones y la familia del fan-out: cuando reaccionas al cambio" title="Enlace directo al Watch, notificaciones y la familia del fan-out: cuando reaccionas al cambio" translate="no">​</a></h2>
<p>Hay varias formas de enterarse de que algo cambió, y <em>no</em> son intercambiables: la diferencia está en si la señal es durable.</p>
<p>La que debes usar desde .NET es <strong><a class="" href="https://clustron.io/es/blog/2026/03/30/watch-changes/">Watch</a></strong>: un feed de cambios nativo y a nivel de todo el clúster. Te suscribes a una clave o a un prefijo, opcionalmente incorporas una instantánea inicial para que no haya hueco entre "leer el estado actual" y "suscribirse a los cambios", y recibes eventos a medida que llegan las escrituras:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">var</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">subscription</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> initial</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Watch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">WatchKeyAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"job:42"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">WatchOptions</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> IncludeInitialSnapshot </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    evt </span><span class="token operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">Handle</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">evt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre></div></div>
<p>La propiedad importante es que el watch está <strong>registrado en todo el clúster y sobrevive al failover</strong>: no está atado al único nodo que casualmente procesó una escritura. Esa es también la razón por la que las <a class="" href="https://clustron.io/es/blog/redis-keyspace-notifications/">notificaciones de keyspace de Redis</a> que Zaris habla (<code>__keyevent@0__</code> / <code>__keyspace@0__</code>, un simple <code>PSUBSCRIBE</code> desde cualquier cliente Redis) valen más aquí que en una sola máquina Redis: son un puente delgado sobre ese mismo watch replicado, así que un failover no deja caer en silencio el flujo de eventos.</p>
<p><strong>Usa watch/notificaciones de keyspace cuando</strong> quieras reaccionar a cambios de estado: invalidar una caché, despertar a un trabajador, refrescar una proyección. <strong>No</strong> confundas el fan-out de pub/sub de mejor esfuerzo con un feed durable: si necesitas <em>todos</em> los eventos incluso a través de caídas y reinicios, eso es un stream, no una notificación.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="locks-exclusión-mutua-gruesa-con-una-advertencia-afilada">Locks: exclusión mutua gruesa, con una advertencia afilada<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#locks-exclusi%C3%B3n-mutua-gruesa-con-una-advertencia-afilada" class="hash-link" aria-label="Enlace directo al Locks: exclusión mutua gruesa, con una advertencia afilada" title="Enlace directo al Locks: exclusión mutua gruesa, con una advertencia afilada" translate="no">​</a></h2>
<p>Zaris sí tiene un <strong>lock</strong> distribuido de primera clase: una compuerta con nombre, basada en lease, con un TTL, que se adquiere desde el cliente y se libera al hacer dispose o al expirar por caída:</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>Es genuinamente útil para la exclusión mutua gruesa —evitar que dos planificadores ejecuten el mismo barrido, serializar una tarea de mantenimiento— y el TTL significa que un poseedor caído no bloquea el lock para siempre.</p>
<p>Pero aquí está la advertencia que la versión de brochazo deja fuera, y es la frase más importante de este artículo sobre locks: <strong>un lock basado en lease no es una barrera de correctitud.</strong> Si tu poseedor se pausa —un parón de GC, una llamada al sistema lenta— el tiempo suficiente para que el lease expire, un segundo poseedor puede adquirir el "mismo" lock mientras el primero todavía cree que lo tiene, y ahora dos trabajadores están en la sección crítica a la vez. Para <em>vivacidad y eficiencia</em> (normalmente solo un trabajador ejecuta el trabajo, la contención es baja) un lock de lease es exactamente lo correcto. Para un <em>invariante de correctitud que jamás debe violarse</em>, no confíes en el lock para imponerlo: imponlo sobre los datos con <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">compare-and-swap</a> o una <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacción</a>, de modo que incluso un segundo actor espurio sea rechazado por una comprobación de versión en la escritura.</p>
<p><strong>Usa un lock cuando</strong> quieras exclusión mutua gruesa y de mejor esfuerzo y un solapamiento ocasional sea meramente un desperdicio, no un error. <strong>Usa CAS en su lugar cuando</strong> el solapamiento corrompería datos: deja que sea la versión, no el lock, lo que diga que no.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-parte-transversal-propiedad-durabilidad-consistencia">La parte transversal: propiedad, durabilidad, consistencia<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#la-parte-transversal-propiedad-durabilidad-consistencia" class="hash-link" aria-label="Enlace directo al La parte transversal: propiedad, durabilidad, consistencia" title="Enlace directo al La parte transversal: propiedad, durabilidad, consistencia" translate="no">​</a></h2>
<p>Tres propiedades se cumplen en cada una de las primitivas de arriba, porque son propiedades del núcleo, no de ninguna herramienta en concreto:</p>
<ul>
<li class=""><strong>El dueño cambia, y no tienes que darte cuenta.</strong> Cada clave es propiedad de una partición que puede hacer failover, migrar o volver a su sitio. El cliente .NET convierte ese vaivén en una señal tipada y reintentable y <a class="" href="https://clustron.io/es/blog/retries-under-ownership-churn/">reenruta al nuevo dueño</a>, acotado por un presupuesto de intentos y un plazo de reloj de pared, de modo que un <code>PutAsync</code> durante un failover normalmente solo tarda unos milisegundos más y devuelve éxito.</li>
<li class=""><strong>La durabilidad es por replicación, no por disco.</strong> Zaris mantiene los datos en memoria y los hace durables guardando copias en otros nodos. Por eso <a class="" href="https://clustron.io/es/blog/kubernetes-durability-sizing/">el factor de replicación no es lo mismo que la tolerancia a fallos</a>: el número solo te protege si la topología coloca de verdad una segunda copia en un nodo que pueda sobrevivir a la pérdida del primero. Toda la <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">columna vertebral de propiedad y recuperación</a> existe para asegurar que ninguna escritura confirmada se pierda mientras la propiedad está en movimiento.</li>
<li class=""><strong>Las escrituras y lecturas se enrutan al dueño actual.</strong> La maquinaria de reintento y reenrutado hace que tus operaciones encuentren el nodo que actualmente posee la clave, de modo que una lectura sigue a una escritura confirmada en vez de adelantarse al cambio de propiedad que acaba de ocurrir.</li>
</ul>
<p>Ninguna de estas es una primitiva que elijas. Son el suelo sobre el que se apoya cada elección.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-diagrama-de-flujo">El diagrama de flujo<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#el-diagrama-de-flujo" class="hash-link" aria-label="Enlace directo al El diagrama de flujo" title="Enlace directo al El diagrama de flujo" translate="no">​</a></h2>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-versión-corta">La versión corta<a href="https://clustron.io/es/blog/choosing-a-zaris-primitive/#la-versi%C3%B3n-corta" class="hash-link" aria-label="Enlace directo al La versión corta" title="Enlace directo al La versión corta" translate="no">​</a></h2>
<p>La longitud de la lista de funcionalidades nunca fue el punto. Zaris es un núcleo clave-valor versionado, particionado y replicado, más vistas tipadas del lado del servidor (hash, lista, conjunto, conjunto ordenado, stream) y capas de coordinación sobre la versión (CAS, transacciones, locks, watch). Haz coincidir la herramienta con la <em>forma</em> del problema: un escalar bajo contención quiere CAS; un invariante entre claves quiere una transacción; un dato que <em>es</em> una colección quiere la colección correspondiente; los eventos ordenados y durables quieren un stream; reaccionar al cambio quiere watch; la exclusión mutua gruesa quiere un lock; y cualquier cosa que jamás deba violarse quiere su garantía impuesta sobre los <em>datos</em>, con una comprobación de versión, no sobre un lock que puede caducar.</p>
<p>Elige por forma, apóyate en la versión para la correctitud, y el pequeño núcleo soporta una cantidad notable de sistema.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Arquitectura</category>
            <category>Patrones Distribuidos</category>
        </item>
        <item>
            <title><![CDATA[El plano de control en tu shell: gestionar un clúster de Zaris desde PowerShell]]></title>
            <link>https://clustron.io/es/blog/control-plane-powershell/</link>
            <guid>https://clustron.io/es/blog/control-plane-powershell/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Un recorrido de operador de día uno: crear un workspace, aprovisionar un almacén con New-ZrStore, arrancarlo y leer su estado en vivo con Get-ZrStore, Get-ZrNode y Watch-ZrStoreMetrics — el plano de control de AdminShell, honesto sobre las decisiones de replicación y topología.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Aprovisionar y ejecutar un clúster de Zaris desde PowerShell" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJQcm92aXNpb25pbmcgYW5kIHJ1bm5pbmcgYSBaYXJpcyBjbHVzdGVyIGZyb20gUG93ZXJTaGVsbCI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fQ0KICAgIC5tb25ve2ZvbnQtZmFtaWx5OiJKZXRCcmFpbnMgTW9ubyIsIlNGTW9uby1SZWd1bGFyIixDb25zb2xhcywiTGliZXJhdGlvbiBNb25vIixNZW5sbyxtb25vc3BhY2U7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPk9QRVJBVElPTlM8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+VGhlIGNvbnRyb2wgcGxhbmU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+aW4geW91ciBzaGVsbDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPlByb3Zpc2lvbiBhbmQgcnVuIGEgWmFyaXMgY2x1c3RlciBmcm9tIFBvd2VyU2hlbGw8L3RleHQ+DQoNCiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNzAwLDI1OCkiPg0KICAgIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIyNjAiIGhlaWdodD0iNDQiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8dGV4dCB4PSIxOCIgeT0iMjgiIGNsYXNzPSJtb25vIiBmb250LXNpemU9IjE2IiBmaWxsPSIjMmJkNmEwIj5OZXctWnJXb3Jrc3BhY2U8L3RleHQ+DQogICAgPHBhdGggZD0iTTEzMCA0NCBMMTMwIDY2IiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQoNCiAgICA8cmVjdCB4PSIwIiB5PSI2NiIgd2lkdGg9IjI2MCIgaGVpZ2h0PSI0NCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjIuNSIvPg0KICAgIDx0ZXh0IHg9IjE4IiB5PSI5NCIgY2xhc3M9Im1vbm8iIGZvbnQtc2l6ZT0iMTYiIGZpbGw9IiMyYmQ2YTAiPk5ldy1aclN0b3JlPC90ZXh0Pg0KICAgIDxwYXRoIGQ9Ik0xMzAgMTEwIEwxMzAgMTMyIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMi41Ii8+DQoNCiAgICA8cmVjdCB4PSIwIiB5PSIxMzIiIHdpZHRoPSIyNjAiIGhlaWdodD0iNDQiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyLjUiLz4NCiAgICA8dGV4dCB4PSIxOCIgeT0iMTYwIiBjbGFzcz0ibW9ubyIgZm9udC1zaXplPSIxNiIgZmlsbD0iIzJiZDZhMCI+U3RhcnQtWnJTdG9yZTwvdGV4dD4NCiAgICA8cGF0aCBkPSJNMTMwIDE3NiBMMTMwIDE5NiIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIuNSIvPg0KDQogICAgPGNpcmNsZSBjeD0iNzAiICBjeT0iMjE0IiByPSI5IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iMTEwIiBjeT0iMjE0IiByPSI5IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPGNpcmNsZSBjeD0iMTUwIiBjeT0iMjE0IiByPSI5IiBmaWxsPSIjNWE2YjY1Ii8+DQogICAgPGNpcmNsZSBjeD0iMTkwIiBjeT0iMjE0IiByPSI5IiBmaWxsPSIjMmJkNmEwIi8+DQogICAgPHRleHQgeD0iMTMwIiB5PSIyNDYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zdXBlcnZpc29yIGRyaXZlcyB0aGUgY2x1c3RlcjwvdGV4dD4NCiAgPC9nPg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>La mayor parte de lo que lees sobre un almacén de datos trata del camino de datos: lecturas, escrituras, el protocolo de cable, lo rápido que va. Pero antes de que nada de eso importe, alguien tiene que <em>poner la cosa en pie</em>: decidir cuántas particiones tiene, cuántas copias de cada una, dónde viven los puertos, arrancarla, confirmar que levantó sana y vigilarla mientras corre. Eso es el plano de control, y en Zaris vive en PowerShell.</p>
<p>Este es un recorrido de operador de día uno. Crearemos un workspace, aprovisionaremos un almacén, lo arrancaremos y luego volveremos a leer su estado en vivo, todo desde el módulo <strong>AdminShell</strong>. Sin consola web, nada editado a mano. Y como el sentido de una historia de operador es la confianza, seremos explícitos sobre las decisiones que <code>New-ZrStore</code> pone ante ti y lo que cada una cuesta de verdad.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dos-shells-dos-planos">Dos shells, dos planos<a href="https://clustron.io/es/blog/control-plane-powershell/#dos-shells-dos-planos" class="hash-link" aria-label="Enlace directo al Dos shells, dos planos" title="Enlace directo al Dos shells, dos planos" translate="no">​</a></h2>
<p>Zaris distribuye su superficie de línea de comandos como dos módulos de PowerShell distintos, y la división no es cosmética: refleja los dos planos del sistema:</p>
<ul>
<li class=""><strong>AdminShell</strong> es el <strong>plano de control</strong>. Sus cmdlets aprovisionan, arrancan, detienen, inspeccionan y escalan almacenes. Hablan con el <strong>Servicio de gestión</strong> de Zaris (el "manager"), no con los nodos de datos directamente. Son los verbos <code>New-ZrStore</code>, <code>Get-ZrNode</code>, <code>Watch-ZrStoreMetrics</code>.</li>
<li class=""><strong>ClientShell</strong> es el <strong>plano de datos</strong>. Sus cmdlets —<code>Connect-ZrStore</code>, <code>Get-ZrItem</code>, <code>Set-ZrItem</code>— abren una conexión directa a los nodos del almacén y mueven datos clave-valor. Esa es la superficie que usa una aplicación, y es el tema del artículo sobre <a class="" href="https://clustron.io/es/blog/connection-strings/">cadenas de conexión</a>.</li>
</ul>
<p>Un operador que pone en pie un clúster vive casi por completo en AdminShell. El manager con el que habla es el componente que conoce la <em>forma deseada</em> de cada almacén y conduce los procesos reales hacia ella. Así que lo primero es decirle a tu shell con qué managers hablar.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-0--un-workspace-al-que-apuntar">Paso 0 — un workspace al que apuntar<a href="https://clustron.io/es/blog/control-plane-powershell/#paso-0--un-workspace-al-que-apuntar" class="hash-link" aria-label="Enlace directo al Paso 0 — un workspace al que apuntar" title="Enlace directo al Paso 0 — un workspace al que apuntar" translate="no">​</a></h2>
<p>Un <strong>workspace</strong> es un conjunto con nombre de managers a los que apuntan los cmdlets de administración, persistido bajo <code>~/.clustron</code> para que sobreviva a las sesiones. Creas uno una vez y te cambias a él cada vez que abres un shell:</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Create a workspace pointing at the cluster's managers (does not activate it)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">New-ZrWorkspace</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Name </span><span class="token string" style="color:#e3116c">"prod"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">Managers </span><span class="token string" style="color:#e3116c">"10.0.0.10:7801"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token string" style="color:#e3116c">"10.0.0.11:7801"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Make it the active target for this and future sessions</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Use-ZrWorkspace</span><span class="token plain"> prod</span><br></span></code></pre></div></div>
<p>Cada endpoint de manager es un <em>observador de un clúster</em>: enuméralos todos y los cmdlets de administración agregan el estado de todo el clúster en vez de lo que sepa una máquina cualquiera. Si tu clúster tiene la seguridad activada, registra un token de administración en el workspace (<code>-Token</code>) o autentica la sesión con <code>Connect-ZrManager -Credential (Get-Credential) -Force</code>, que intercambia un nombre de usuario y una contraseña por un token de sesión. (<code>Connect-ZrManager</code> es la conexión ad-hoc más antigua y sigue funcionando, pero los cmdlets de workspace son el camino actual: son a lo que se corresponden los workspaces guardados de la consola web).</p>
<p>Una bifurcación honesta en el camino aquí: el recorrido de abajo es el <strong>modelo supervisor</strong>, donde cada manager bifurca y posee sus procesos locales de Zaris —el despliegue nativo/Windows—. Si tus nodos corren en cambio bajo un orquestador como Kubernetes (el <strong>modelo attach</strong>), el manager no crea procesos; defines el almacén con tu orquestador y lo <em>adoptas</em> con <code>Register-ZrStore</code> en vez de <code>New-ZrStore</code>. <code>New-ZrStore</code> detecta un manager en modo attach y te lo dice en vez de fallar en lo hondo de la pila.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-1--aprovisionar-el-almacén">Paso 1 — aprovisionar el almacén<a href="https://clustron.io/es/blog/control-plane-powershell/#paso-1--aprovisionar-el-almac%C3%A9n" class="hash-link" aria-label="Enlace directo al Paso 1 — aprovisionar el almacén" title="Enlace directo al Paso 1 — aprovisionar el almacén" translate="no">​</a></h2>
<p>Este es el cmdlet con las decisiones de verdad dentro. La forma mínima de producción es corta:</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>Eso crea un almacén llamado <code>orders</code> en cada máquina del workspace, con dos copias de cada partición. Cuatro parámetros llevan el peso, así que seamos precisos con cada uno.</p>
<p><strong><code>-ReplicationFactor</code></strong> es el número total de copias de cada partición: la primaria más las réplicas. Para producción debe ser como mucho el número de máquinas, de modo que cada copia caiga en una máquina <em>distinta</em>; eso es lo que lo convierte en tolerancia a fallos y no en decoración. En una única máquina de desarrollo la restricción se relaja con un aviso: las réplicas compartirán máquina, lo cual está bien para pruebas y no te compra nada en una caída real. El cmdlet lo dice en voz alta en vez de dejarte creer otra cosa.</p>
<p><strong><code>-PartitionsPerNode</code></strong> vale <code>1</code> por defecto, que es la topología de producción. El recuento total de particiones se deriva de la pertenencia al clúster en vivo —<code>PartitionsPerNode × nodos vivos</code>— y se <em>recalcula en cada cambio de pertenencia</em>, así que no es un número que congeles en el momento de creación. Lo subes por encima de 1 principalmente para pruebas locales, donde quieres observar el comportamiento sensible a particiones (enrutamiento de segmentos, rebalanceo al escalar hacia fuera) en una única máquina sin montar un rack.</p>
<p><strong><code>-BaseClusterPort</code> / <code>-BaseClientPort</code></strong> son los primeros puertos de dos rangos: el puerto interno de gossip/clúster y el puerto de cara al cliente, incrementados una vez por proceso en cada máquina. El cmdlet valida que los dos rangos no se solapen y se niega a crear un almacén que colisionaría consigo mismo: una pequeña protección que te ahorra un confuso fallo de primer arranque.</p>
<p>Hay tres más que vale la pena conocer porque codifican una verdadera compensación de durabilidad:</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> es <code>Async</code> por defecto: la primaria escribe localmente y retorna de inmediato, replicando en segundo plano. Esa es la opción más rápida y el valor por defecto correcto para una caché, pero ten claro que una escritura confirmada bajo <code>Async</code> todavía no tiene garantía de estar en una réplica. Elige <code>Sync</code> y la primaria espera la confirmación de la réplica antes de devolver <code>Ok</code>, cambiando latencia por durabilidad.</li>
<li class=""><strong><code>-WriteQuorumPolicy</code></strong> solo muerde bajo <code>Sync</code>: <code>All</code> (toda réplica debe confirmar), <code>Majority</code> (deben confirmar <code>floor(RF/2)+1</code>) o <code>BestEffort</code> (los fallos se registran pero no hacen fallar la escritura).</li>
<li class=""><strong><code>-AllowReplicaReads</code></strong> permite a los clientes leer de nodos réplica para repartir la carga de lectura, con la advertencia honesta de que una lectura de réplica puede devolver datos <em>ligeramente obsoletos</em>, porque una réplica puede quedarse detrás de la primaria. Déjalo desactivado cuando necesites leer-tus-escrituras; actívalo cuando prefieras el rendimiento y puedas tolerar el retardo.</li>
</ul>
<p>Fíjate en lo que dicen los valores por defecto sobre la postura de Zaris: <code>Async</code> + lectura desde la primaria es una caché rápida y correcta por defecto. Los mandos de durabilidad están ahí cuando los necesitas, nombrados sin rodeos, cada uno con un coste que puedes ver.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-2--arrancarlo-y-confirmar-que-levantó">Paso 2 — arrancarlo, y confirmar que levantó<a href="https://clustron.io/es/blog/control-plane-powershell/#paso-2--arrancarlo-y-confirmar-que-levant%C3%B3" class="hash-link" aria-label="Enlace directo al Paso 2 — arrancarlo, y confirmar que levantó" title="Enlace directo al Paso 2 — arrancarlo, y confirmar que levantó" translate="no">​</a></h2>
<p>Crear un almacén define su forma; no ejecuta los procesos. <code>Start-ZrStore</code> enumera las instancias del almacén en cada manager y las arranca, informando del éxito o fallo por instancia:</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>Ahora la parte que separa a un operador de alguien que ejecutó un comando y confió. <code>Get-ZrStore</code> vuelve a leer el estado en vivo del almacén desde los managers: estado general, tiempo activo y el estado de ejecución de cada nodo:</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>El estado del almacén es un resumen: <code>Running</code> cuando cada nodo está arriba, <code>Stopped</code> cuando todos están abajo, y <code>Partial</code> cuando algunos están arriba y otros no, que es exactamente el estado que quieres que salga a la superficie a gritos, porque un almacén <code>Partial</code> es uno que arrancó de forma desigual. El tiempo activo es deliberadamente el <em>tiempo de formación del clúster</em> reportado por el núcleo —el arranque de nodo más temprano que sobrevivió al vaivén— no el proceso en ejecución más antiguo, de modo que un almacén que lleva horas sirviendo no reporte mal "unos minutos" solo porque un nodo se reinició.</p>
<p>Para la siguiente capa más abajo, <code>Get-ZrNode</code> muestra los roles y el mapa de particiones:</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Get-ZrNode</span><span class="token plain"> orders</span><br></span></code></pre></div></div>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">Nodes (4)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">─────────────────────────────────────────────────────────────────────────</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.10-n0   10.0.0.10:7861   Running   Primary   v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.10-n1   10.0.0.10:7862   Running   Replica   v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.11-n0   10.0.0.11:7861   Running   Primary   v7</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ● 10.0.0.11-n1   10.0.0.11:7862   Running   Replica   v7</span><br></span></code></pre></div></div>
<p>La columna <code>Primary</code>/<code>Replica</code> es quién posee actualmente cada partición, y el <code>v7</code> es la <strong>versión del mapa de particiones</strong>, el número de generación del mapa de propiedad. Cuando todos los nodos coinciden en la misma versión de mapa, la propiedad ha convergido; un nodo atascado en una versión más antigua es un nodo que no se ha puesto al día con el último reparto. Pedir detalle a un único nodo (<code>Get-ZrNode orders 10.0.0.10-n0</code>) expande esto hasta los IDs exactos de particiones primarias y réplicas que posee y cuándo cambió su rol por última vez: la vista a la que recurres cuando acaba de ocurrir un failover y quieres saber dónde aterrizó la propiedad.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-3--vigilarlo-mientras-corre">Paso 3 — vigilarlo mientras corre<a href="https://clustron.io/es/blog/control-plane-powershell/#paso-3--vigilarlo-mientras-corre" class="hash-link" aria-label="Enlace directo al Paso 3 — vigilarlo mientras corre" title="Enlace directo al Paso 3 — vigilarlo mientras corre" translate="no">​</a></h2>
<p>Una instantánea te dice que el almacén está arriba. Una vista en vivo te dice lo que está <em>haciendo</em>. <code>Watch-ZrStoreMetrics</code> sondea los managers y redibuja una tabla por nodo a intervalos hasta que lo cancelas:</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>Coloca primero los contadores del día a día —tasa de peticiones, gets, puts, deletes, recuento de elementos, uso de memoria— y empuja al fondo los diagnósticos avanzados de replicación y sincronización, de modo que los números que miras de un vistazo estén arriba. Los contadores de errores se ponen rojos en cuanto su tasa sube por encima de cero; el rendimiento se tiñe de verde a medida que sube. También señala los cambios de rol en línea: si un failover voltea una primaria a réplica mientras miras, se imprime una línea <code>[ROLE CHANGE]</code> con una marca de tiempo —la señal en vivo de que una partición acaba de cambiar de manos.</p>
<p>Un detalle deliberado que vale la pena destacar, porque es de esas cosas que hacen que una herramienta sea usable a las 2 de la madrugada: la vista obtiene el siguiente fotograma <em>antes</em> de limpiar la pantalla, y un sondeo vacío transitorio mantiene el último buen fotograma en vez de quedarse en blanco. Obtienes una tabla estable, no una parpadeante, por muy lento que sea un manager en responder.</p>
<p>Para redondear el conjunto de solo lectura, <code>Get-ZrInfo</code> reporta la versión instalada, los directorios de instalación y de datos, la ubicación de ambos módulos, el runtime del host, y sondea cada manager en busca de alcanzabilidad. Un manager asegurado que responde <code>401</code> se reporta como <em>alcanzable pero necesitando autenticación</em>, no como "caído", de modo que puedas distinguir de un vistazo un problema de red de un token ausente.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="paso-4--detenerlo-y-la-forma-que-subyace-a-todo">Paso 4 — detenerlo, y la forma que subyace a todo<a href="https://clustron.io/es/blog/control-plane-powershell/#paso-4--detenerlo-y-la-forma-que-subyace-a-todo" class="hash-link" aria-label="Enlace directo al Paso 4 — detenerlo, y la forma que subyace a todo" title="Enlace directo al Paso 4 — detenerlo, y la forma que subyace a todo" translate="no">​</a></h2>
<p>Bajar un almacén es el espejo del ciclo de vida de arrancarlo:</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>El valor por defecto es un hard kill rápido, que es el comportamiento correcto cuando el almacén <em>entero</em> se va a bajar de golpe: no hay clúster superviviente al que un nodo que se marcha pueda entregar el relevo, así que drenar no compra nada. Usa <code>-Graceful</code> cuando quieras que cada nodo drene y deje el clúster limpiamente, lo cual importa cuando estás deteniendo un subconjunto en vez de todo.</p>
<p>Debajo de cada cmdlet de este artículo hay una idea: un almacén de Zaris tiene una <strong>forma deseada</strong> —una definición canónica de los servidores y particiones que se supone que debe tener— y el manager corre bajo un <strong>modelo supervisor</strong> que conduce continuamente los procesos en vivo hacia ella. <code>New-ZrStore</code> escribe esa forma; <code>Start-ZrStore</code> la realiza; <code>Get-ZrStore</code> y <code>Get-ZrNode</code> informan de cuán cerca está la realidad de ella. El mismo modelo es lo que convierte el redimensionado elástico en una línea en vez de un runbook: cambiar la forma deseada con <code>Add-ZrServer</code> / <code>Remove-ZrServer</code> y dejar que el supervisor reconcilie, que es su propia historia de <a class="" href="https://clustron.io/es/blog/elastic-scaling/">escalado elástico</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-resumen-honesto">El resumen honesto<a href="https://clustron.io/es/blog/control-plane-powershell/#el-resumen-honesto" class="hash-link" aria-label="Enlace directo al El resumen honesto" title="Enlace directo al El resumen honesto" translate="no">​</a></h2>
<p>El ciclo de vida completo de día uno son cinco comandos y dos lecturas:</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>Lo que hace de esto un plano de control y no solo un montón de scripts es que los cmdlets reportan <em>verdad en vivo y agregada</em> —no lo que pediste, sino lo que el clúster realmente es— y que las decisiones con consecuencias reales se nombran a plena vista: el factor de replicación frente al recuento de máquinas, <code>Async</code> frente a <code>Sync</code>, las lecturas de réplica frente a la obsolescencia. Pon un almacén en pie, confirma que convergió y mantenlo vigilado, todo sin salir del shell.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Operaciones</category>
            <category>.NET</category>
        </item>
        <item>
            <title><![CDATA[El mismo código, con red o sin ella: el modo embebido de Zaris]]></title>
            <link>https://clustron.io/es/blog/embedded-inproc-mode/</link>
            <guid>https://clustron.io/es/blog/embedded-inproc-mode/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris se ejecuta como un clúster distribuido sobre un socket y, desde la misma superficie de cliente .NET, como un almacén embebido en proceso sin red alguna. Cambias entre los dos modificando una única cadena de conexión. Aquí está cómo funciona el modo embebido, para qué sirve y la línea donde honestamente se detiene.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="El modo embebido en proceso de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJaYXJpcyBlbWJlZGRlZCBpbi1wcm9jZXNzIG1vZGU6IHRoZSBzYW1lIGNvZGUgd2l0aCBvciB3aXRob3V0IGEgbmV0d29yayI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPkVNQkVEREVEIE1PREU8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+VGhlIHNhbWUgY29kZSw8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+d2l0aCBvciB3aXRob3V0IGEgbmV0d29yazwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPk9uZSBJWmFyaXMgc3VyZmFjZSAmIzE4MzsgaW4tcHJvY2VzcyBmb3IgdGVzdHMsIGEgY2x1c3RlciBpbiBwcm9kdWN0aW9uPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MjAsMjYyKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjIzMCIgaGVpZ2h0PSI1MiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjMiLz4NCiAgICA8dGV4dCB4PSIxMTUiIHk9IjMyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+eW91ciBhcHAgJiMxODM7IElaYXJpcyBjbGllbnQ8L3RleHQ+DQogICAgPHBhdGggZD0iTTcwIDUyIEwzNiAxMTAiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiLz4NCiAgICA8cGF0aCBkPSJNMTYwIDUyIEwxOTYgMTEwIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWRhc2hhcnJheT0iNiA2Ii8+DQogICAgPHJlY3QgeD0iLTcwIiB5PSIxMTQiIHdpZHRoPSIxODYiIGhlaWdodD0iNjQiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iMjMiIHk9IjE0MiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPnphcmlzOi8vaW5wcm9jL29yZGVyczwvdGV4dD4NCiAgICA8dGV4dCB4PSIyMyIgeT0iMTY0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzJiZDZhMCI+ZW1iZWRkZWQgJiMxODM7IG5vIHNvY2tldDwvdGV4dD4NCiAgICA8cmVjdCB4PSIxMjQiIHk9IjExNCIgd2lkdGg9IjE4NiIgaGVpZ2h0PSI2NCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjE3IiB5PSIxNDIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj56YXJpc3M6Ly9ob3N0Ojc4NjM8L3RleHQ+DQogICAgPHRleHQgeD0iMjE3IiB5PSIxNjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5jbHVzdGVyICYjMTgzOyBvdmVyIFRMUzwvdGV4dD4NCiAgICA8dGV4dCB4PSIxMjAiIHk9IjIwMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm9ubHkgdGhlIGNvbm5lY3Rpb24gc3RyaW5nIGNoYW5nZXM8L3RleHQ+DQogIDwvZz4NCiAgPHRleHQgeD0iODAiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMyYmQ2YTAiPlphcmlzPC90ZXh0Pg0KICA8dGV4dCB4PSIxNDYiIHk9IjQzMCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM1YTZiNjUiPmJ5IENsdXN0cm9uPC90ZXh0Pg0KPC9zdmc+DQo=" width="1200" height="470" class="img_ev3q"></p>
<p>La mayoría de los almacenes de datos distribuidos te hacen elegir, pronto y de forma permanente, entre dos mundos. Está el almacén "de verdad": un clúster que despliegas, al que apuntas un cliente por la red y con el que hablas a través de un protocolo de cable. Y está el almacén "falso" al que recurres en las pruebas unitarias: un diccionario en memoria, un mock, un stub hecho a mano que implementa lo justo de la interfaz para que la prueba compile. Los dos no comparten nada. Tu prueba ejercita el comportamiento del stub, no el del almacén, y el hueco entre ellos es exactamente donde se esconden los bugs.</p>
<p>Zaris colapsa esa elección. El mismo cliente tipado de .NET que habla con un clúster distribuido sobre TLS también ejecuta el almacén <strong>embebido dentro de tu proceso</strong>, sin socket, sin mapa de particiones y sin despliegue, y es el <em>mismo motor de almacén</em>, no un sustituto. Seleccionas cuál obtienes con una única cadena de conexión. Este artículo trata de cómo funciona eso, los casos para los que es genuinamente bueno y la línea dura donde el modo embebido se detiene y empieza un clúster.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="una-cadena-es-todo-el-interruptor">Una cadena es todo el interruptor<a href="https://clustron.io/es/blog/embedded-inproc-mode/#una-cadena-es-todo-el-interruptor" class="hash-link" aria-label="Enlace directo al Una cadena es todo el interruptor" title="Enlace directo al Una cadena es todo el interruptor" translate="no">​</a></h2>
<p>Un cliente de Zaris siempre se configura a partir de una <a class="" href="https://clustron.io/es/blog/connection-strings/">cadena de conexión</a>. La cadena lleva el transporte, los endpoints semilla, el nombre del almacén y las opciones. Hay dos formas, y la diferencia entre ellas es la diferencia entre un clúster y un almacén embebido:</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>El token de host <code>inproc</code> está <strong>reservado</strong>. Cuando el cliente lo ve, no hay endpoints que marcar, ni transporte que negociar, ni handshake de TLS, ni token: el almacén se sirve desde la memoria del proceso actual. Todo lo demás de tu código permanece idéntico:</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>En ASP.NET Core es la misma historia a través de la inyección de dependencias: la cadena de conexión suele venir de la configuración, así que el cambio es un valor de configuración, no un cambio de código:</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>Pon <code>zaris://inproc/orders</code> en <code>appsettings.Development.json</code> y <code>zariss://.../orders</code> en producción, y el mismo binario corre embebido en el portátil de un desarrollador y en clúster en la nube, sin nada recompilado.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-el-mismo-cliente-es-el-punto">Por qué "el mismo cliente" es el punto<a href="https://clustron.io/es/blog/embedded-inproc-mode/#por-qu%C3%A9-el-mismo-cliente-es-el-punto" class="hash-link" aria-label="Enlace directo al Por qué &quot;el mismo cliente&quot; es el punto" title="Enlace directo al Por qué &quot;el mismo cliente&quot; es el punto" translate="no">​</a></h2>
<p>La razón por la que esto importa está en el tipo que recibes de vuelta. <code>ConnectAsync</code> devuelve un <code>IZarisClient</code> —o, para toda la superficie de funcionalidades, <code>IZaris</code>— sin importar el modo. El modo embebido lo sirve una clase llamada <code>ZarisInProcClient</code>, y implementa exactamente las mismas interfaces que el cliente de clúster:</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>Cada una de esas superficies está presente en modo embebido, respaldada por el mismo motor de almacén que usa el clúster. Clave/valor <code>Get</code>/<code>Put</code>/<code>Delete</code>, bulk y batch, <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacciones multielemento</a>, TTL y expiración, <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">compare-and-swap optimista</a>, <a class="" href="https://clustron.io/es/blog/2026/03/30/watch-changes/">Watch para lecturas reactivas</a>, <a class="" href="https://clustron.io/es/blog/redis-streams-native/">Streams nativos</a> con grupos de consumidores, las <a class="" href="https://clustron.io/es/blog/data-structures-that-replicate/">estructuras de datos replicadas</a>: todo ello se resuelve contra el almacén embebido, no contra un mock.</p>
<!-- -->
<p>Ese motor compartido es lo que hace del almacén embebido un objetivo de prueba fiel. Un bucle de compare-and-swap que depende de que un conflicto se reporte con precisión, una suscripción <code>Watch</code> que debe dispararse cuando una clave cambia, una transacción que debe hacer commit de forma atómica o no hacerlo en absoluto: cada uno se comporta embebido tal como se comporta en clúster, porque es literalmente el mismo camino de código evaluándolo. Un <code>KvStatus.Conflict</code> embebido significa lo que significa en producción. Un stub de diccionario escrito a mano no puede prometer eso.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-es-realmente-el-host-embebido">Qué es realmente el host embebido<a href="https://clustron.io/es/blog/embedded-inproc-mode/#qu%C3%A9-es-realmente-el-host-embebido" class="hash-link" aria-label="Enlace directo al Qué es realmente el host embebido" title="Enlace directo al Qué es realmente el host embebido" translate="no">​</a></h2>
<p>Vale la pena ser preciso sobre qué levanta <code>zaris://inproc</code>, porque los límites honestos caen directamente de ello. El host embebido es una <strong>topología fija de un único nodo que posee cada segmento</strong>. No hay mapa de particiones, porque solo hay un participante; no hay replicación, porque no hay una segunda copia; no hay transporte, porque no hay un segundo proceso al que llegar.</p>
<p>Lo que <em>sí</em> corre es la maquinaria real que querrías en una prueba. El almacén aprovisiona su segmento de antemano, conecta el servicio de leases, el servicio de watch y las métricas locales, y arranca el <strong>barredor de TTL</strong> a su cadencia normal, de modo que una clave a la que haces <code>ExpireAsync</code> con un TTL de 50 ms de verdad desaparece, con un temporizador, exactamente como lo haría en un clúster. La expiración, la contabilidad de desalojo, el versionado y las notificaciones de watch están todos activos. Es un almacén completo que resulta tener un tamaño de clúster de uno.</p>
<p>Dentro de un único proceso, los clientes se <strong>comparten por nombre de almacén</strong>. Dos llamadas <code>ConnectAsync("zaris://inproc/orders")</code> en el mismo proceso se resuelven al <em>mismo</em> almacén embebido, así que un valor que uno escribe, el otro lo lee, útil cuando tu prueba levanta un productor y un consumidor y espera que se vean mutuamente. Nombres de almacén distintos obtienen almacenes distintos; procesos distintos obtienen almacenes embebidos enteramente separados (más sobre ese límite abajo).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dónde-el-modo-embebido-se-gana-su-sitio">Dónde el modo embebido se gana su sitio<a href="https://clustron.io/es/blog/embedded-inproc-mode/#d%C3%B3nde-el-modo-embebido-se-gana-su-sitio" class="hash-link" aria-label="Enlace directo al Dónde el modo embebido se gana su sitio" title="Enlace directo al Dónde el modo embebido se gana su sitio" translate="no">​</a></h2>
<p><strong>Pruebas unitarias e de integración que ejercitan el almacén real.</strong> Este es el uso estrella. Tu repositorio, tu capa de caché, tu coordinador de sagas: pruébalos contra <code>zaris://inproc/test-{Guid}</code>, obtén un almacén aislado y fresco por prueba, y ejercita la semántica genuina: versionado real para tus bucles CAS, expiración de TTL real con un temporizador real, atomicidad de transacción real, eventos de watch reales. Sin contenedor Docker que arrancar en CI, sin puerto que asignar, sin fragilidad de teardown. El almacén se crea en la primera conexión y muere con el proceso.</p>
<p><strong>Aplicaciones de un solo proceso que pueden crecer más adelante.</strong> Una aplicación de escritorio, un trabajador en segundo plano, una herramienta de CLI o un servicio pequeño que necesita un almacén estructurado rápido hoy pero podría convertirse en un despliegue en clúster mañana. Empieza embebido. El día que necesites compartir esos datos entre instancias, cambias la cadena de conexión para que apunte a un clúster, y como programaste contra <code>IZaris</code> todo el tiempo, nada más se mueve.</p>
<p><strong>Integraciones que se distribuyen embebidas por defecto.</strong> El proveedor de sesiones de Zaris para ASP.NET Core es un ejemplo concreto: conectar Zaris como tu almacén de sesiones es una llamada, y en su ejemplo corre enteramente embebido —</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>— lo que significa que un desarrollador puede hacer <code>F5</code> en la aplicación y tener sesiones funcionando con cero infraestructura, y luego moverse a <code>zariss://.../sessions</code> para un despliegue multiinstancia sin tocar el código de sesión.</p>
<p>Para usar el modo embebido, referencia el paquete <code>Clustron.Zaris.InProc</code> junto al cliente (tiene como objetivo .NET 8). El paquete se autorregistra: el cliente descubre y carga la factoría en proceso en el primer uso, así que simplemente referenciarlo es suficiente. En un ensamblado de pruebas donde prefieras ser explícito, <code>new InProcBootstrap().Register();</code> lo conecta de forma determinista.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dónde-se-detiene-honestamente">Dónde se detiene honestamente<a href="https://clustron.io/es/blog/embedded-inproc-mode/#d%C3%B3nde-se-detiene-honestamente" class="hash-link" aria-label="Enlace directo al Dónde se detiene honestamente" title="Enlace directo al Dónde se detiene honestamente" translate="no">​</a></h2>
<p>El modo embebido es un almacén real, pero es un almacén <em>de un único nodo, en memoria</em>, y fingir lo contrario sería la pelusa de marketing que este blog intenta evitar. Tres límites duros, directos del diseño:</p>
<p><strong>Sin durabilidad.</strong> El almacén embebido mantiene los datos en la memoria del proceso y en ningún otro sitio. No hay replicación ni log en disco en este modo: cuando el proceso termina, los datos se han ido. Eso es exactamente lo que quieres para una prueba (pizarra limpia en cada ejecución) y exactamente aquello en lo que no debes confiar para nada que necesites que sobreviva a un reinicio. El modo embebido no es una base de datos con la que respaldes tus datos de producción.</p>
<p><strong>Sin alta disponibilidad, sin escalado hacia fuera.</strong> Un clúster de uno tolera la pérdida de cero nodos y escala a la memoria y la CPU de un único proceso. Todas las razones por las que recurrirías a Zaris distribuido —sobrevivir a un fallo de nodo sin pérdida de datos, <a class="" href="https://clustron.io/es/blog/kubernetes-durability-sizing/">dimensionar réplicas para durabilidad</a>, <a class="" href="https://clustron.io/es/blog/elastic-scaling/">escalar el rendimiento entre núcleos y máquinas</a>— simplemente no aplican a un host embebido, porque solo hay uno de él. Cuando necesitas esas propiedades, necesitas el clúster.</p>
<p><strong>Sin compartición entre procesos.</strong> El registro en proceso comparte un almacén por nombre <em>dentro de un proceso</em>. Dos procesos distintos que ambos abren <code>zaris://inproc/orders</code> obtienen dos almacenes independientes que no saben nada el uno del otro: no hay socket entre ellos, por definición. En el momento en que tus datos necesiten ser visibles para un segundo proceso o una segunda máquina, el modo embebido es la herramienta equivocada y un clúster es la correcta.</p>
<p>Ninguno de estos es un bug que haya que arreglar; son la definición de "embebido". El valor no es que el almacén en proceso sea secretamente distribuido, sino que el día que necesites distribución, lo <em>único</em> que cambias es la cadena que dice dónde vive el almacén.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-misma-promesa-cumplida-dos-veces">La misma promesa, cumplida dos veces<a href="https://clustron.io/es/blog/embedded-inproc-mode/#la-misma-promesa-cumplida-dos-veces" class="hash-link" aria-label="Enlace directo al La misma promesa, cumplida dos veces" title="Enlace directo al La misma promesa, cumplida dos veces" translate="no">​</a></h2>
<p>La propuesta es pequeña y concreta. Escribes tu aplicación contra una única superficie de cliente. En una prueba, o en un portátil, o dentro de una herramienta de un solo proceso, esa superficie la sirve un almacén de Zaris real corriendo en tu propia memoria: rápido de crear, fiel en su semántica, y desaparecido cuando el proceso termina. En producción, el código idéntico habla con un clúster distribuido sobre TLS. El comportamiento que tus pruebas fijaron es el comportamiento que envías, porque nunca fue una implementación distinta, solo un lugar distinto para que el almacén viviera.</p>
<p>Esa es toda la idea detrás de <code>zaris://inproc</code>: el hueco entre "el almacén en mi prueba" y "el almacén en producción" es del ancho de una cadena de conexión, y tú decides, por entorno, en qué lado de él estás.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>.NET</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Leer tus propias escrituras: el modelo de consistencia de Zaris, explicado con honestidad]]></title>
            <link>https://clustron.io/es/blog/reading-your-own-writes/</link>
            <guid>https://clustron.io/es/blog/reading-your-own-writes/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[¿Qué garantiza realmente una lectura en Zaris? Las lecturas se enrutan al único propietario activo de una partición, de modo que lees tus propias escrituras dentro de esa partición, pero no hay linealizabilidad global, las lecturas entre particiones no tienen un orden compartido y 'confirmado' significa comprometido en el propietario, todavía no en una réplica. Aquí está el modelo completo, con los ajustes y las salvedades expuestos con claridad.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Leer tus propias escrituras — un propietario por partición" 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>Hay una pregunta que todo desarrollador acaba por hacerle a un almacén distribuido, normalmente justo después de un informe de error confuso: <em>si escribo un valor y luego lo leo de inmediato, ¿tengo la garantía de ver lo que escribí?</em> Suena como si debiera tener una respuesta de una sola palabra. No la tiene — ni para ningún almacén particionado y replicado, ni para Zaris. La respuesta honesta es "sí, dentro de una partición, porque las lecturas van al mismo propietario único al que fue tu escritura" — y lo interesante es todo lo que se esconde en esa salvedad.</p>
<p>Esta entrada reúne en un solo lugar el modelo de consistencia de Zaris. Hemos escrito sobre las piezas por separado — <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">compare-and-swap optimista</a>, <a class="" href="https://clustron.io/es/blog/retries-under-ownership-churn/">reintentos transparentes ante cambios de propiedad</a>, el <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">op-log reproducible</a> que alimenta a las réplicas, <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacciones multiclave</a>. Aquí las enlazamos desde el lado del lector y respondemos a la única pregunta que importa frente a un teclado: <strong>cuando leo, ¿qué se me promete en realidad?</strong> Incluyendo, sobre todo, dónde se detiene la promesa.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-modelo-en-una-frase">El modelo en una frase<a href="https://clustron.io/es/blog/reading-your-own-writes/#el-modelo-en-una-frase" class="hash-link" aria-label="Enlace directo al El modelo en una frase" title="Enlace directo al El modelo en una frase" translate="no">​</a></h2>
<p>Zaris es un <strong>almacén con tendencia AP cuyo límite de consistencia se traza en torno a la propiedad, no en torno a cada escritura.</strong> El plano de datos se mantiene disponible — un nodo solitario sigue sirviendo los segmentos que posee — y la disciplina de escritor único que mantiene las lecturas coherentes se aplica en el plano de <em>propiedad</em>: <strong>hay exactamente un propietario activo por partición en cada instante.</strong> Las escrituras a una partición se serializan a través de ese único nodo. Deliberadamente <strong>no hay quórum ni consenso en la ruta de escritura</strong>, y Zaris <strong>no</strong> afirma tener linealizabilidad estricta global.</p>
<p>Casi todo lo que observa un lector se desprende de ese único hecho estructural, así que conviene hacer explícita la cadena:</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>Una clave se asigna determinísticamente por hash a un segmento; un segmento pertenece a una partición; una partición tiene exactamente un propietario activo que acepta sus escrituras <em>y sirve sus lecturas</em>. La propiedad es un <strong>arrendamiento (lease)</strong> que viaja en el mapa de particiones como <code>(LeaderTerm, OwnershipGeneration)</code> — otorgado por el líder del clúster, duradero entre reinicios y propagado a cada nodo. Ese arrendamiento es lo que garantiza el "exactamente uno" — reemplazó a un esquema anterior de "gana la época más alta" que podía reiniciarse al arrancar y producir brevemente dos nodos creyendo cada uno que poseía la misma partición.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-las-lecturas-enrutadas-al-propietario-te-dan-leer-tus-escrituras">Por qué las lecturas enrutadas al propietario te dan leer-tus-escrituras<a href="https://clustron.io/es/blog/reading-your-own-writes/#por-qu%C3%A9-las-lecturas-enrutadas-al-propietario-te-dan-leer-tus-escrituras" class="hash-link" aria-label="Enlace directo al Por qué las lecturas enrutadas al propietario te dan leer-tus-escrituras" title="Enlace directo al Por qué las lecturas enrutadas al propietario te dan leer-tus-escrituras" translate="no">​</a></h2>
<p>Aquí está la parte que responde a la pregunta inicial. En Zaris, <strong>las lecturas se enrutan al propietario activo, el mismo nodo al que fue tu escritura.</strong> Los clientes no leen de las réplicas por defecto. Así que cuando haces <code>PutAsync</code> de una clave y luego <code>GetAsync</code> de ella, ambas solicitudes se resuelven — a través de la misma tabla de enrutamiento, construida a partir del mapa de particiones más la instantánea de propiedad en tiempo de ejecución — al mismo nodo propietario, y ese nodo ya tiene tu escritura en su almacén local.</p>
<!-- -->
<p>Ese es todo el mecanismo. Leer-tus-escrituras no está añadido como una garantía de sesión ni como una caché del lado del cliente; se desprende del enrutamiento. Lees tus escrituras <strong>dentro de una partición</strong> porque un único nodo es la autoridad tanto para las lecturas como para las escrituras de esa partición, y las escrituras a él se serializan de una en una. La réplica de ese diagrama podría ir retrasada unos milisegundos respecto al propietario — la replicación tiene una cola asíncrona — pero por defecto <em>nunca lees la réplica</em>, así que ese retraso te resulta invisible.</p>
<p>La salvedad "dentro de una partición" es determinante, y volveremos a lo que excluye.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-versión-que-se-te-permite-ver">La versión que se te permite ver<a href="https://clustron.io/es/blog/reading-your-own-writes/#la-versi%C3%B3n-que-se-te-permite-ver" class="hash-link" aria-label="Enlace directo al La versión que se te permite ver" title="Enlace directo al La versión que se te permite ver" translate="no">​</a></h2>
<p>Cada valor que sirve el propietario lleva dos números monótonos, y son los que hacen de "el mismo valor" una afirmación precisa en lugar de una sensación:</p>
<ul>
<li class="">una <strong><code>Revision</code></strong> propiedad del almacén — un número de secuencia estrictamente creciente que el propietario estampa en cada mutación en orden de confirmación, y</li>
<li class="">un <strong><code>ItemVersion</code></strong> por elemento — la versión de esa clave concreta, que es lo que <code>IfMatchVersion</code> protege en una <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">escritura compare-and-swap</a>.</li>
</ul>
<p>Como el propietario los asigna en orden de confirmación y sirve las lecturas desde ese mismo estado confirmado, las lecturas sucesivas de una clave desde el propietario avanzan la versión <strong>hacia adelante, nunca hacia atrás.</strong> No leerás la versión 42 y luego la versión 40 un momento después. Esa monotonía es exactamente lo que permite que el bucle CAS sea correcto: un lector ve una versión, calcula a partir de ella y vuelve a escribir condicionado a que siga siendo esa versión. Si el modelo permitiera que las lecturas oscilaran entre versiones, la concurrencia optimista estaría construida sobre arena. Un propietario, revisiones en orden de confirmación, lecturas servidas por el propietario — ese es el fundamento sobre el que se apoya toda la historia de coordinación de la <a class="" href="https://clustron.io/es/blog/race-free-coordination-cas/">entrada sobre CAS</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-ventana-en-la-que-una-lectura-dice-honestamente-todavía-no">La ventana en la que una lectura dice honestamente "todavía no"<a href="https://clustron.io/es/blog/reading-your-own-writes/#la-ventana-en-la-que-una-lectura-dice-honestamente-todav%C3%ADa-no" class="hash-link" aria-label="Enlace directo al La ventana en la que una lectura dice honestamente &quot;todavía no&quot;" title="Enlace directo al La ventana en la que una lectura dice honestamente &quot;todavía no&quot;" translate="no">​</a></h2>
<p>Las lecturas enrutadas al propietario son limpias en estado estable. Los momentos interesantes son las transiciones — una conmutación por error (failover), una migración, un primario preferido recuperándose — porque durante un traspaso el nodo al que enrutarías podría no ser la autoridad en ese momento. La elección de Zaris aquí es la honesta: <strong>en lugar de servir una lectura desde un nodo que no es el propietario asentado, devuelve un estado reintentable.</strong> Una lectura (o escritura) durante una ventana así puede volver como:</p>
<ul>
<li class=""><strong><code>Moved</code></strong> — tu tabla de enrutamiento está obsoleta; la clave vive ahora en un nodo distinto.</li>
<li class=""><strong><code>OwnershipChanging</code></strong> — la propiedad de ese segmento está en transición activa ahora mismo; es un traspaso, no un error de enrutamiento asentado.</li>
<li class=""><strong><code>Migrating</code></strong> — el segmento está en plena migración a un nuevo propietario.</li>
<li class=""><strong><code>ReplicaSyncPending</code></strong> — este nodo está poniendo al día la copia de un segmento y todavía no servirá una lectura posiblemente incompleta desde él.</li>
<li class=""><strong><code>Unavailable</code></strong> — la partición no tiene un propietario alcanzable en este instante.</li>
</ul>
<p>Ninguno de estos es una excepción que tengas que extraer de un rastreo de pila — son resultados legibles por máquina, y el cliente .NET los trata como transitorios y <a class="" href="https://clustron.io/es/blog/retries-under-ownership-churn/">reintenta reenrutando</a>, refrescando su mapa de clúster y dirigiéndose a quien el mapa ahora nombre como propietario. El efecto es que tu <code>GetAsync</code> simplemente tarda unos milisegundos más y luego devuelve el valor correcto. La decisión de diseño que hay debajo vale la pena enunciarla con claridad: <strong>Zaris prefiere hacerte esperar un instante que entregarte una lectura de la que no está seguro de que sea autoritativa.</strong> Un "todavía no" reintentable es mejor respuesta que una errónea con aplomo.</p>
<p>Por esto también sobrevive leer-tus-escrituras a una conmutación por error: el trabajo de vallado de escritura (write-fence) implica que una escritura confirmada no se queda atrás en la ventana de traspaso (llegaremos abajo al borde honesto de eso), y la promoción solo convierte a un nodo en propietario una vez que es elegible — de modo que la lectura que se reenruta tras una conmutación por error se reenruta a un nodo que tiene tus datos.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-ajuste-que-puedes-girar--y-lo-que-realmente-hace-hoy">El ajuste que puedes girar — y lo que realmente hace hoy<a href="https://clustron.io/es/blog/reading-your-own-writes/#el-ajuste-que-puedes-girar--y-lo-que-realmente-hace-hoy" class="hash-link" aria-label="Enlace directo al El ajuste que puedes girar — y lo que realmente hace hoy" title="Enlace directo al El ajuste que puedes girar — y lo que realmente hace hoy" translate="no">​</a></h2>
<p>Zaris expone una superficie de consistencia de lectura. <code>GetOptions</code> lleva un <code>ReadConsistency</code> de <code>Leader</code>, <code>Quorum</code> o <code>Any</code>, y un almacén tiene una bandera <code>AllowReplicaReads</code> (por defecto <strong>desactivada</strong>). La intención es el compromiso clásico: dejar que las lecturas se sirvan desde réplicas para repartir la carga de lectura, a cambio de aceptar que una réplica pueda ir retrasada respecto al propietario — una obsolescencia acotada que eliges deliberadamente.</p>
<p>Aquí está el estado actual honesto, porque es exactamente el tipo de cosa que el texto de marketing difuminaría: <strong>hoy, las lecturas son solo del propietario.</strong> <code>AllowReplicaReads</code> y <code>ReadConsistency.Any</code> son una superficie de configuración y de modelado de solicitudes que existe y fluye a través de la definición del almacén, pero todavía no hay una ruta de código en el servidor que realmente sirva una lectura desde una réplica. En la práctica eso significa que ahora mismo obtienes el extremo <em>fuerte</em> del ajuste — cada lectura es una lectura del propietario, así que leer-tus-escrituras se sostiene — y <strong>no</strong> obtienes actualmente el comportamiento de lectura-de-réplica-obsoleta, aunque actives la bandera.</p>
<p>¿Por qué mencionar un ajuste que en efecto está fijado en un solo valor? Porque te dice qué <em>permitirá</em> intercambiar el contrato de consistencia, y porque la asimetría importa cuando razonas sobre obsolescencia. Cuando lleguen las lecturas servidas por réplica, activar <code>AllowReplicaReads</code> será el momento en que dejarías de tener garantizado leer-tus-escrituras: una lectura enrutada a una réplica puede observar una versión por detrás de la del propietario, durante todo el tiempo que la cola de replicación asíncrona vaya rezagada. Hasta entonces, la garantía es más simple y más fuerte de lo que sugiere el mando. Preferimos que lo sepas a que supongas que ya estás balanceando lecturas entre réplicas cuando no es así.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-significa-confirmado-para-el-siguiente-lector">Qué significa "confirmado" para el <em>siguiente</em> lector<a href="https://clustron.io/es/blog/reading-your-own-writes/#qu%C3%A9-significa-confirmado-para-el-siguiente-lector" class="hash-link" aria-label="Enlace directo al qué-significa-confirmado-para-el-siguiente-lector" title="Enlace directo al qué-significa-confirmado-para-el-siguiente-lector" translate="no">​</a></h2>
<p>Leer-tus-escrituras trata de que <em>tú</em> leas <em>tu propia</em> escritura en el propietario. Una pregunta distinta y más afilada es: una vez que el propietario te dice que una escritura está confirmada, ¿está esa escritura a salvo si el propietario muere después, de modo que el lector que se reenruta al nuevo propietario siga viéndola?</p>
<p>El contrato honesto de confirmación (ack) hoy: <strong>una escritura se confirma cuando aterriza en el almacén local del propietario activo.</strong> La replicación a las RF−1 réplicas viaja en una cola asíncrona por detrás de esa confirmación. Vale la pena planificar en torno a las consecuencias en lugar de pasarlas por alto:</p>
<ul>
<li class="">Con <strong>redundancia 1</strong> — sin réplica sincronizada — una escritura es genuinamente de mejor esfuerzo. No debe tratarse como duradera ante un solo fallo, porque hay exactamente una copia.</li>
<li class="">Incluso con réplicas, si se pierde el único propietario activo en la estrecha ventana <em>después</em> de que confirmara una escritura pero <em>antes</em> de que esa escritura llegara a una réplica, la escritura confirmada puede perderse.</li>
</ul>
<p>La propiedad más fuerte — <em>confirmar solo una vez que la escritura esté en al menos una réplica sincronizada</em>, de modo que una escritura confirmada sobreviva a la pérdida de cualquier nodo — es el <strong>objetivo de diseño (la compuerta de confirmación "W1"), todavía no la semántica de confirmación entregada.</strong> Es la dirección hacia la que converge el <a class="" href="https://clustron.io/es/blog/zero-data-loss-under-chaos/">trabajo del backbone de durabilidad</a>, y esa entrada es cuidadosa con la misma distinción: la afirmación fuerte que valida está acotada a <em>escrituras confirmadas, cuando existe una réplica sincronizada, bajo las clases de fallo que induce la ejecución de caos</em> — no un "nada puede perderse nunca" incondicional. Los <a class="" href="https://clustron.io/es/blog/op-log-replayable-substrate/">mecanismos de op-log y migración</a> son cómo una réplica o un nuevo propietario se pone al día con el estado confirmado del propietario en primer lugar; la compuerta de confirmación trata de <em>cuándo</em> se le permite al propietario decir "hecho".</p>
<p>Así que la afirmación precisa de cara al lector es: leer-tus-escrituras en el propietario es sólido; la durabilidad ante fallos cruzados de una escritura confirmada es fuerte-y-cada-vez-más-fuerte, con la salvedad actual expuesta arriba, y un suelo duro en RF1 que ningún almacén puede sortear por ingeniería — una copia es una copia.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="entre-particiones-no-hay-reloj-compartido">Entre particiones, no hay reloj compartido<a href="https://clustron.io/es/blog/reading-your-own-writes/#entre-particiones-no-hay-reloj-compartido" class="hash-link" aria-label="Enlace directo al Entre particiones, no hay reloj compartido" title="Enlace directo al Entre particiones, no hay reloj compartido" translate="no">​</a></h2>
<p>El último lugar donde muerde la salvedad "dentro de una partición": <strong>no hay ordenamiento ni instantánea global entre particiones.</strong> Cada partición se serializa a través de su propio propietario; dos particiones distintas son dos propietarios distintos con dos secuencias de revisión independientes. Si escribes la clave <code>A</code> (partición 1) y luego la clave <code>B</code> (partición 2), un lector concurrente puede observar el nuevo valor de <code>B</code> y el viejo de <code>A</code>, o viceversa — no hay un instante a nivel de clúster en el que "ambas ocurrieron". Las operaciones en bloque (bulk) y por lotes (batch) son por clave por debajo; son cómodas, no atómicas.</p>
<p>Cuando genuinamente necesitas que un invariante abarque varias claves — mover un saldo de <code>A</code> a <code>B</code>, mantener un índice sincronizado con el registro al que apunta — eso es exactamente para lo que existen las <a class="" href="https://clustron.io/es/blog/multi-key-transactions/">transacciones multiclave</a>. Proporcionan aislamiento <code>Serializable</code> sobre una confirmación coordinada por el cliente a través de versiones optimistas, y son la <em>única</em> superficie de atomicidad entre claves. Las lecturas y escrituras multiclave ordinarias no son una transacción y no deben confundirse con una.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-se-garantiza-y-qué-no">Qué se garantiza y qué no<a href="https://clustron.io/es/blog/reading-your-own-writes/#qu%C3%A9-se-garantiza-y-qu%C3%A9-no" class="hash-link" aria-label="Enlace directo al Qué se garantiza y qué no" title="Enlace directo al Qué se garantiza y qué no" translate="no">​</a></h2>
<p>Dicho con claridad, para que puedas abarcar el modelo entero de una vez:</p>
<table><thead><tr><th>Propiedad</th><th>Zaris hoy</th></tr></thead><tbody><tr><td>Propietario por partición</td><td>Exactamente un propietario activo, mantenido como arrendamiento duradero.</td></tr><tr><td>Ruta de lectura</td><td>Se enruta al propietario activo; las réplicas no se leen por defecto.</td></tr><tr><td>Leer-tus-escrituras</td><td><strong>Sí, dentro de una partición</strong> — el mismo propietario sirve tu escritura y tu lectura.</td></tr><tr><td>Lecturas monótonas de una clave</td><td>Sí — el propietario sirve revisiones en orden de confirmación; las versiones avanzan.</td></tr><tr><td>Lecturas durante el traspaso</td><td>Estado reintentable (<code>Moved</code> / <code>OwnershipChanging</code> / <code>Migrating</code> / <code>ReplicaSyncPending</code>); el cliente reintenta y reenruta.</td></tr><tr><td>Ajuste de lecturas de réplica / obsolescencia</td><td><code>AllowReplicaReads</code> + <code>ReadConsistency.Any</code> existen como superficie; <strong>solo del propietario en la práctica hoy</strong> (aún no hay ruta de servicio desde réplica).</td></tr><tr><td>Durabilidad de confirmación</td><td>Local al propietario hoy; duradera-ante-un-fallo-con-réplica-sincronizada es el objetivo (compuerta de confirmación W1).</td></tr><tr><td>Orden entre particiones</td><td><strong>Ninguno</strong> — sin instantánea global; usa transacciones para atomicidad entre claves.</td></tr><tr><td>Linealizabilidad global</td><td><strong>No se afirma.</strong></td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forma-honesta-de-todo-esto">La forma honesta de todo esto<a href="https://clustron.io/es/blog/reading-your-own-writes/#la-forma-honesta-de-todo-esto" class="hash-link" aria-label="Enlace directo al La forma honesta de todo esto" title="Enlace directo al La forma honesta de todo esto" translate="no">​</a></h2>
<p>Un buen modelo de consistencia no es el que tiene la lista más larga de garantías; es aquel cuyas garantías puedes nombrar de verdad y cuyos bordes puedes ver de verdad. Zaris traza su límite en torno a la propiedad y es franco sobre el compromiso: obtienes leer-tus-escrituras y lecturas monótonas <em>dentro de una partición</em> gratis, porque un propietario sirve ambos lados y te entrega versiones en orden de confirmación; obtienes un "todavía no" reintentable en lugar de una respuesta errónea con aplomo durante un traspaso; y renuncias a la linealizabilidad global y al orden entre particiones, que recuperas explícitamente con una transacción cuando un invariante concreto lo necesita.</p>
<p>Las dos salvedades que vale la pena llevar en la cabeza son las que esta entrada expuso en lugar de suavizar: el ajuste de lectura de réplica está, hoy, en efecto fijado en el valor fuerte, así que eres más consistente de lo que el mando implica; y "confirmado" significa actualmente <em>en el propietario</em>, con la durabilidad completa ante un solo fallo como objetivo que el trabajo de durabilidad está aterrizando, y un suelo genuino en redundancia 1. Conoce esas dos cosas y el resto del modelo es exactamente tan simple como parece: encuentra al propietario, lee tus escrituras.</p>
<p>Para el diseño más profundo, consulta <a class="" href="https://clustron.io/es/docs/clustron/zaris/core-concepts/consistency/">Consistencia</a> y <a class="" href="https://clustron.io/es/docs/clustron/zaris/architecture/partitioning-replication/">Particionado y replicación</a> en la documentación.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Sistemas Distribuidos</category>
            <category>Arquitectura</category>
        </item>
        <item>
            <title><![CDATA[Notificaciones de keyspace que sobreviven a la conmutación por error]]></title>
            <link>https://clustron.io/es/blog/redis-keyspace-notifications/</link>
            <guid>https://clustron.io/es/blog/redis-keyspace-notifications/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris habla las notificaciones de keyspace de Redis — CONFIG SET notify-keyspace-events, los canales __keyspace@0__ / __keyevent@0__, un simple PSUBSCRIBE desde cualquier cliente de Redis. Pero por debajo, cada evento viaja sobre el watch nativo de Zaris a nivel de todo el clúster: replicado entre los miembros y entregado por pub/sub de todo el clúster, de modo que las notificaciones no se desvanecen cuando lo hace el nodo que las produjo.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Notificaciones de keyspace de Redis sobre el watch nativo replicado de Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJSZWRpcyBrZXlzcGFjZSBub3RpZmljYXRpb25zIG9uIFphcmlzJ3MgcmVwbGljYXRlZCBuYXRpdmUgd2F0Y2giPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5SRURJUyBDT01QQVRJQklMSVRZPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPktleXNwYWNlIG5vdGlmaWNhdGlvbnM8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxODAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+dGhhdCBzdXJ2aXZlIGZhaWxvdmVyPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMjI0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzdmZTNiZiI+UmVkaXMga2V5c3BhY2UgZXZlbnRzIG92ZXIgYSByZXBsaWNhdGVkIG5hdGl2ZSB3YXRjaDwvdGV4dD4NCg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDAsMjY2KSI+DQogICAgPCEtLSB3cml0ZSAtLT4NCiAgICA8cmVjdCB4PSIwIiB5PSI0MCIgd2lkdGg9IjEyOCIgaGVpZ2h0PSI1MCIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iNjQiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+U0VUIHVzZXI6NDI8L3RleHQ+DQogICAgPHBhdGggZD0iTTEzNCA2NSBMMTg2IDY1IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPCEtLSByZXBsaWNhdGVkIHdhdGNoIC0tPg0KICAgIDxyZWN0IHg9IjE5MCIgeT0iMTgiIHdpZHRoPSIxNDgiIGhlaWdodD0iOTQiIHJ4PSIxMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMyIvPg0KICAgIDx0ZXh0IHg9IjI2NCIgeT0iNTAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmZmZmIj5uYXRpdmUgd2F0Y2g8L3RleHQ+DQogICAgPHRleHQgeD0iMjY0IiB5PSI3NCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIuNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzJiZDZhMCI+cmVwbGljYXRlZDwvdGV4dD4NCiAgICA8dGV4dCB4PSIyNjQiIHk9Ijk0IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMi41IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMmJkNmEwIj5yZS1ob21lZDwvdGV4dD4NCiAgICA8cGF0aCBkPSJNMzQ0IDY1IEwzOTYgNjUiIHN0cm9rZT0idXJsKCNhY2NlbnQpIiBzdHJva2Utd2lkdGg9IjUiLz4NCiAgICA8IS0tIGNoYW5uZWwgLS0+DQogICAgPHJlY3QgeD0iNDAwIiB5PSI0MCIgd2lkdGg9IjU2IiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI0MjgiIHk9IjYzIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzdmZTNiZiI+X19rZXk8L3RleHQ+DQogICAgPHRleHQgeD0iNDI4IiB5PSI3OSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPmV2ZW50QDBfXzwvdGV4dD4NCiAgICA8dGV4dCB4PSIyNjQiIHk9IjE1MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPmFueSBSZWRpcyBjbGllbnQgUFNVQlNDUklCRXM8L3RleHQ+DQogIDwvZz4NCg0KICA8dGV4dCB4PSI4MCIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzJiZDZhMCI+WmFyaXM8L3RleHQ+DQogIDx0ZXh0IHg9IjE0NiIgeT0iNDMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9IjQwMCIgZmlsbD0iIzVhNmI2NSI+YnkgQ2x1c3Ryb248L3RleHQ+DQo8L3N2Zz4NCg==" width="1200" height="470" class="img_ev3q"></p>
<p>Las notificaciones de keyspace son una de las funciones más útiles de Redis y una de las más silenciosamente frágiles. La idea es preciosa: cada vez que se escribe, borra o expira una clave, el servidor publica un evento en un canal pub/sub bien conocido, y cualquiera a quien le interese puede hacer <code>PSUBSCRIBE</code> y reaccionar — invalidar una caché, despertar a un worker, refrescar una proyección. Sin sondeo, sin un <code>GET</code> en bucle preguntando "¿ya ha cambiado?".</p>
<p>La fragilidad está en la letra pequeña. Las notificaciones de keyspace de Redis son <strong>pub/sub de disparar-y-olvidar</strong>, emitidas por el nodo que procesó el comando, a los suscriptores conectados a ese mismo nodo. En un despliegue en clúster eso significa que un suscriptor solo se entera de las escrituras que aterrizaron en el nodo al que está conectado, y si ese nodo sufre una conmutación por error, la suscripción — y el flujo de eventos que transportaba — se va con él. La función funciona de maravilla en una sola máquina y se vuelve sutil rápidamente en cuanto hay más de una.</p>
<p>Zaris habla exactamente el mismo protocolo — <code>CONFIG SET notify-keyspace-events</code>, los canales <code>__keyspace@0__</code> y <code>__keyevent@0__</code>, un <code>PSUBSCRIBE</code> corriente desde cualquier cliente de Redis en cualquier lenguaje. Pero la fontanería de debajo no es una reimplementación del notificador de Redis. Es un puente fino hacia una función que Zaris ya tenía: un <strong>watch nativo, a nivel de todo el clúster, replicado.</strong> Eso cambia lo que valen las notificaciones.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="activarlo--exactamente-al-estilo-de-redis">Activarlo — exactamente al estilo de Redis<a href="https://clustron.io/es/blog/redis-keyspace-notifications/#activarlo--exactamente-al-estilo-de-redis" class="hash-link" aria-label="Enlace directo al Activarlo — exactamente al estilo de Redis" title="Enlace directo al Activarlo — exactamente al estilo de Redis" translate="no">​</a></h2>
<p>No hay nada específico de Zaris en cómo activas o consumes la función. Apunta cualquier cliente de Redis al front-end RESP y usa los comandos que ya conoces:</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>Desde un cliente tipado — aquí StackExchange.Redis — tiene la misma forma, sin ningún SDK de Zaris de por medio:</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>El punto que vale la pena subrayar: <strong>ningún cambio de aplicación.</strong> El código escrito contra las notificaciones de keyspace de Redis corre contra Zaris sin modificar. Esa es toda la promesa del front-end RESP — tus clientes no necesitan saber qué hay detrás del socket.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="las-dos-familias-de-canales">Las dos familias de canales<a href="https://clustron.io/es/blog/redis-keyspace-notifications/#las-dos-familias-de-canales" class="hash-link" aria-label="Enlace directo al Las dos familias de canales" title="Enlace directo al Las dos familias de canales" translate="no">​</a></h2>
<p>Zaris publica las dos familias de canales estándar de Redis, con la convención de carga útil (payload) estándar — a cuál te suscribas depende de si te orientas por <em>la clave</em> o por <em>el evento</em>:</p>
<table><thead><tr><th>Canal</th><th>Publicado cuando la bandera…</th><th>Carga útil</th><th>Úsalo cuando te importa…</th></tr></thead><tbody><tr><td><code>__keyspace@0__:&lt;key&gt;</code></td><td><code>K</code> está activada</td><td>el nombre del evento (<code>set</code> / <code>del</code> / <code>expired</code>)</td><td>una <strong>clave específica</strong> — "dime todo lo que le pasa a <code>session:abc</code>"</td></tr><tr><td><code>__keyevent@0__:&lt;event&gt;</code></td><td><code>E</code> está activada</td><td>la clave que cambió</td><td>una <strong>clase de eventos</strong> — "dime cada clave que acaba de expirar"</td></tr></tbody></table>
<p>El <code>@0</code> es el índice de base de datos, y en Zaris es siempre <code>0</code> — Zaris no divide el keyspace en bases de datos numeradas, así que hay un único espacio de nombres de notificaciones, plano, en lugar de dieciséis. Los tres nombres de evento se corresponden con las tres cosas que le pueden pasar a una clave:</p>
<ul>
<li class=""><strong><code>set</code></strong> — la clave fue escrita (un <code>SET</code>, un <code>HSET</code>, un <code>LPUSH</code>, cualquier mutación)</li>
<li class=""><strong><code>del</code></strong> — la clave fue eliminada</li>
<li class=""><strong><code>expired</code></strong> — la clave fue desalojada porque su TTL transcurrió</li>
</ul>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="qué-ocurre-realmente-por-debajo">Qué ocurre realmente por debajo<a href="https://clustron.io/es/blog/redis-keyspace-notifications/#qu%C3%A9-ocurre-realmente-por-debajo" class="hash-link" aria-label="Enlace directo al Qué ocurre realmente por debajo" title="Enlace directo al Qué ocurre realmente por debajo" translate="no">​</a></h2>
<p>Cuando envías <code>CONFIG SET notify-keyspace-events KEA</code>, Zaris no enciende un emisor de eventos de Redis a medida cableado en la ruta de comandos. Registra <strong>un watch de prefijo sobre todo el keyspace</strong> en el propio cliente de enrutamiento del nodo — la misma primitiva <code>Watch</code> que el cliente nativo de .NET expone para <a class="" href="https://clustron.io/es/blog/2026/03/30/watch-changes/">lecturas reactivas sin sondeo</a>. A partir de ese momento, la única lógica que vive en la capa RESP es un mapa de nombres: llega un evento de watch nativo, y el puente lo convierte en el nombre de canal de Redis y la carga útil correctos.</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>Esa llamada a <code>Publish</code> es el pub/sub nativo — la misma maquinaria de suscribir/entregar, con su propio fan-out a nivel de todo el clúster, que el almacén ya ejecuta. El front-end RESP posee el <em>vocabulario</em> (nombres de canal, convención de carga útil); el clúster posee la <em>entrega</em>. Este es un patrón que mantenemos deliberadamente a lo largo de todo el producto: <a class="" href="https://clustron.io/es/blog/redis-protocol-any-language/">cada función es primero una capacidad nativa, y RESP es una piel fina sobre ella</a> — nunca una implementación paralela con sus propios errores.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-el-watch-es-nativo-es-todo-el-asunto">Por qué "el watch es nativo" es todo el asunto<a href="https://clustron.io/es/blog/redis-keyspace-notifications/#por-qu%C3%A9-el-watch-es-nativo-es-todo-el-asunto" class="hash-link" aria-label="Enlace directo al Por qué &quot;el watch es nativo&quot; es todo el asunto" title="Enlace directo al Por qué &quot;el watch es nativo&quot; es todo el asunto" translate="no">​</a></h2>
<p>Aquí es donde divergen los dos sistemas, y no es cosmético.</p>
<p>Una notificación de keyspace de Redis se produce en el nodo que ejecutó el comando y se entrega a los suscriptores conectados a <em>ese</em> nodo. Nada de la suscripción se comparte con el resto del clúster, y nada de ella es duradero. Es pub/sub en sentido estricto — si no estabas escuchando en el nodo correcto en el instante correcto, el evento simplemente no existió para ti.</p>
<p>Una notificación de keyspace de Zaris viaja sobre un watch nativo, y un watch nativo es un objeto a nivel de todo el clúster:</p>
<ul>
<li class=""><strong>La suscripción se replica entre los miembros.</strong> <code>WatchSyncService</code> mantiene las suscripciones de watch sincronizadas entre nodos — el líder empuja el conjunto actual a un nodo que se une, y un nodo que arranca pide al líder lo que está activo. La suscripción no es un hecho conocido solo por un proceso; es estado del clúster.</li>
<li class=""><strong>Se reubica a través de los cambios (churn).</strong> Cuando las particiones se mueven — conmutación por error, migración, un nodo que se va — el watch se mueve con los datos que vigila, igual que la propiedad de las claves. La escritura sigue produciendo un evento una vez que se asienta el polvo, porque el vigilante siguió a la partición.</li>
<li class=""><strong>La entrega se distribuye a nivel de todo el clúster.</strong> Como la publicación va a través del pub/sub nativo, un cliente conectado a <em>cualquier</em> nodo recibe eventos de escrituras que ocurrieron en <em>cualquier</em> nodo. No estás suscrito silenciosamente a la actividad de un solo shard.</li>
</ul>
<p>La consecuencia práctica: un suscriptor que vigila <code>__keyevent@0__:expired</code> para impulsar la invalidación de caché sigue funcionando a través de un fallo de nodo. En Redis Cluster ese mismo suscriptor está conectado a un nodo y ve las expiraciones de un nodo; la mitigación honesta allí es abrir una suscripción a cada nodo y desduplicar. En Zaris es una suscripción, completa a nivel de clúster, y sobrevive a la conmutación por error que la habría cortado en silencio.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="los-bordes-honestos">Los bordes honestos<a href="https://clustron.io/es/blog/redis-keyspace-notifications/#los-bordes-honestos" class="hash-link" aria-label="Enlace directo al Los bordes honestos" title="Enlace directo al Los bordes honestos" translate="no">​</a></h2>
<p>Esto es un puente sobre pub/sub de mejor esfuerzo, no una actualización mágica de fiabilidad. Tres cosas que conviene tener claras:</p>
<p><strong>La entrega sigue siendo de mejor esfuerzo, no un registro garantizado.</strong> El puente publica cada evento de disparar-y-olvidar: un fallo al entregar a un suscriptor nunca debe atascar la tubería del watch para todos los demás, así que una notificación a un cliente momentáneamente desconectado se descarta, no se almacena en búfer. Esto coincide con el propio contrato de Redis — las notificaciones de keyspace nunca fueron un registro de eventos exactamente-una-vez, y no deberías tratar las de Zaris como tal. Si un evento perdido corrompiera tu estado, las notificaciones son el <em>aviso</em>, no la <em>fuente de verdad</em>: ante el evento, ve y lee el valor autoritativo.</p>
<p><strong>Sin relleno retroactivo (backfill).</strong> El watch se arma sin instantánea inicial, así que obtienes los cambios desde el momento en que se activan las notificaciones en adelante — nunca una reproducción de lo que el keyspace ya contenía. Activa las notificaciones y <em>luego</em> empieza a razonar sobre los deltas.</p>
<p><strong>La granularidad de las banderas es gruesa.</strong> Redis te permite seleccionar <em>clases</em> de eventos con una cadena de banderas densa (<code>g</code>, <code>$</code>, <code>l</code>, <code>s</code>, <code>h</code>, <code>z</code>, <code>x</code>, <code>e</code>, …). Zaris lee las dos banderas que deciden <em>dónde</em> publicar — <code>K</code> para los canales de keyspace, <code>E</code> para los canales de keyevent (y <code>A</code> como el atajo habitual de "todas las clases") — y puentea los tres tipos de evento nativos siempre que cualquiera de las dos esté activada. No filtra hasta suscripciones por clase. En la práctica te suscribes a los canales y patrones que te importan y dejas que la coincidencia de patrones haga el estrechamiento, que es como se escribe de todos modos la mayoría del código de notificaciones de keyspace.</p>
<p>Si necesitas notificaciones acotadas a una parte del keyspace en lugar de a todo él, configura <code>ZARIS_RESP_KEYSPACE_PREFIX</code> en el nodo y el watch subyacente se arma sobre ese prefijo en lugar de sobre todo el almacén — una forma barata de evitar que un keyspace ocupado publique eventos a los que nadie se suscribe.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cuando-quieres-un-registro-en-lugar-de-un-timbre">Cuando quieres un registro en lugar de un timbre<a href="https://clustron.io/es/blog/redis-keyspace-notifications/#cuando-quieres-un-registro-en-lugar-de-un-timbre" class="hash-link" aria-label="Enlace directo al Cuando quieres un registro en lugar de un timbre" title="Enlace directo al Cuando quieres un registro en lugar de un timbre" translate="no">​</a></h2>
<p>Las notificaciones de keyspace son un timbre: te dicen que <em>pasó algo</em>, ahora mismo, a quien esté escuchando. Cuando lo que realmente necesitas es un <strong>registro reproducible y duradero</strong> — cada evento, en orden, legible por un consumidor que se unió tarde o se cayó y volvió — eso no es una notificación, es un stream. Zaris tiene <a class="" href="https://clustron.io/es/blog/redis-streams-native/">Redis Streams nativos</a> exactamente para eso: <code>XADD</code> para añadir, <code>XREADGROUP</code> con grupos de consumidores para procesamiento al-menos-una-vez con confirmaciones, lecturas bloqueantes para baja latencia. Echa mano de las notificaciones cuando un evento perdido solo signifique "vuelve a comprobar la próxima vez"; echa mano de Streams cuando un evento perdido sea un error.</p>
<p>Usada por lo que es — un aviso de baja latencia y sin sondeo que, en Zaris, resulta ser completo a nivel de clúster y duradero ante conmutaciones por error — la notificación de keyspace es una de las cosas más agradables que puedes activar con un solo <code>CONFIG SET</code>. La diferencia solo es visible el día en que un nodo desaparece, que es precisamente el día en que habrías descubierto los límites de la versión de un solo nodo por las malas.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Redis</category>
            <category>Patrones Distribuidos</category>
        </item>
        <item>
            <title><![CDATA[Sin servidor de autenticación: tokens verificables sin conexión, roles y ámbitos en Zaris]]></title>
            <link>https://clustron.io/es/blog/token-auth-roles-scopes/</link>
            <guid>https://clustron.io/es/blog/token-auth-roles-scopes/</guid>
            <pubDate>Sat, 03 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Zaris asegura un clúster con una sola clave de firma RS256 y JWT autocontenidos. Cualquier manager emite un token; cada nodo lo verifica sin conexión con la mitad pública — sin servidor central de autenticación, sin base de datos, sin ida y vuelta por operación. Aquí está el modelo de tokens, el catálogo de roles/ámbitos, los dos planos de aplicación y los bordes operativos honestos.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Tokens verificables sin conexión, roles y ámbitos en Zaris" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJPZmZsaW5lLXZlcmlmaWFibGUgdG9rZW5zLCByb2xlcywgYW5kIHNjb3BlcyBpbiBaYXJpcyI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iYmciIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBhMTUxMiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMGUyMTFiIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8cmFkaWFsR3JhZGllbnQgaWQ9Imdsb3ciIGN4PSIwLjgyIiBjeT0iMC4yIiByPSIwLjciPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMTJiNTdlIiBzdG9wLW9wYWNpdHk9IjAuMzAiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImFjY2VudCIgeDE9IjAiIHkxPSIxIiB4Mj0iMCIgeTI9IjAiPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMGVhNjcyIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyYmQ2YTAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxzdHlsZT4uc2Fuc3tmb250LWZhbWlseToiU3BhY2UgR3JvdGVzayIsLWFwcGxlLXN5c3RlbSxCbGlua01hY1N5c3RlbUZvbnQsIlNlZ29lIFVJIixSb2JvdG8sSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWY7fTwvc3R5bGU+DQogIDwvZGVmcz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2JnKSIvPg0KICA8cmVjdCB3aWR0aD0iMTIwMCIgaGVpZ2h0PSI0NzAiIGZpbGw9InVybCgjZ2xvdykiLz4NCiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2FjY2VudCkiLz4NCiAgPHRleHQgeD0iODAiIHk9IjcwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNyIgbGV0dGVyLXNwYWNpbmc9IjQiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiM3ZmUzYmYiPlNFQ1VSSVRZPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTI2IiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPk5vIGF1dGggc2VydmVyPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPm9uIHRoZSBob3QgcGF0aDwvdGV4dD4NCiAgPHRleHQgeD0iODAiIHk9IjIyNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI0MDAiIGZpbGw9IiM3ZmUzYmYiPk9mZmxpbmUtdmVyaWZpYWJsZSB0b2tlbnMsIHJvbGVzLCBhbmQgc2NvcGVzPC90ZXh0Pg0KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDAsMjUwKSI+DQogICAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjE5MCIgaGVpZ2h0PSI3OCIgcng9IjEwIiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIzIi8+DQogICAgPHRleHQgeD0iOTUiIHk9IjMwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZmZmZiI+SldUICYjMTgzOyBSUzI1NjwvdGV4dD4NCiAgICA8dGV4dCB4PSI5NSIgeT0iNTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5zdWJqZWN0ICsgcm9sZUBzY29wZTwvdGV4dD4NCiAgICA8dGV4dCB4PSI5NSIgeT0iNzEiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5zaWduZWQgYnkgY2x1c3RlciBrZXk8L3RleHQ+DQogICAgPHBhdGggZD0iTTIwMCAyMCBMMzAwIDIwIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPHBhdGggZD0iTTIwMCA1OCBMMzAwIDU4IiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSI1Ii8+DQogICAgPHJlY3QgeD0iMzA2IiB5PSItOCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSI1MiIgcng9IjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzkxIiB5PSIxNiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNmZmZmZmYiPmNvbnRyb2wgcGxhbmU8L3RleHQ+DQogICAgPHRleHQgeD0iMzkxIiB5PSIzNCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1YTZiNjUiPm1hbmFnZXIgJiMxODM7IDo3ODAxPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjMwNiIgeT0iNTIiIHdpZHRoPSIxNzAiIGhlaWdodD0iNTIiIHJ4PSI4IiBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjM5MSIgeT0iNzYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmZmZmIj5kYXRhIHBsYW5lPC90ZXh0Pg0KICAgIDx0ZXh0IHg9IjM5MSIgeT0iOTQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNWE2YjY1Ij5ub2RlcyAmIzE4MzsgdmVyaWZ5IEAgSGVsbG88L3RleHQ+DQogICAgPHRleHQgeD0iOTUiIHk9IjExMiIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMyYmQ2YTAiPnZlcmlmaWVkIHdpdGggdGhlIHB1YmxpYyBrZXkgJiM4NTk0OyBubyByb3VuZC10cmlwPC90ZXh0Pg0KICA8L2c+DQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>La mayoría de las formas de poner autenticación delante de un almacén de datos distribuido acaban por añadir un segundo sistema distribuido para hacerla funcionar: un servicio de autenticación al que llamar, una base de datos de sesiones que replicar, un endpoint de introspección de tokens que cada nodo tiene que alcanzar en la ruta caliente. Aseguraste el almacén y te compraste una nueva dependencia de disponibilidad y una nueva fuente de latencia.</p>
<p>Zaris toma una ruta distinta, y es una deliberadamente aburrida: <strong>una clave de firma por clúster, y tokens que cualquier nodo puede verificar por sí mismo.</strong> Un token es un JWT firmado con la clave privada del clúster; cada manager y cada nodo tiene la mitad pública y comprueba la firma <strong>sin conexión</strong> — sin llamada a un servidor de autenticación, sin quórum, sin consulta a una base de datos. La seguridad está desactivada por defecto, y un clúster con ella desactivada se comporta exactamente como uno no asegurado. Esta entrada trata de qué se activa cuando lo enciendes: la forma del token, el modelo de roles-y-ámbitos, los dos planos donde realmente ocurre la aplicación y — porque fingir lo contrario es cómo la gente adquiere una falsa sensación de seguridad — los bordes operativos que es fácil equivocar.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="el-token-es-toda-la-historia">El token es toda la historia<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#el-token-es-toda-la-historia" class="hash-link" aria-label="Enlace directo al El token es toda la historia" title="Enlace directo al El token es toda la historia" translate="no">​</a></h2>
<p>Cada token de Zaris es un JWT. Nombra un <strong>sujeto</strong> — un usuario o una aplicación — y lleva una o más concesiones <strong><code>role @ scope</code></strong>. Está firmado por la clave privada RS256 del clúster, que vive solo en los managers; todo lo que necesita <em>comprobar</em> un token tiene solo la clave pública.</p>
<p>Esa única decisión de diseño es lo que elimina el servidor de autenticación del cuadro:</p>
<ul>
<li class=""><strong>Cualquier manager puede emitir un token.</strong> No hay nodo de autenticación primario, ni elección, ni estado de sesión mutable compartido que coordinar. Emitir es una operación de firma local.</li>
<li class=""><strong>Cada manager y cada nodo puede verificar un token</strong> con solo la clave pública. La verificación es una comprobación de firma más una comprobación de expiración y de ámbito — CPU puro, sin red. El token es <strong>autocontenido</strong>: el rol y el ámbito están embebidos <em>en</em> él, así que un nodo que autoriza una conexión no necesita nada más que la clave pública. No hay base de datos de políticas en el nodo que mantener sincronizada.</li>
</ul>
<p>La recompensa práctica aparece en la ruta de datos. Un cliente presenta su token <strong>una vez, cuando se establece la conexión</strong> — el "Hello". El nodo lo verifica, autoriza la conexión para el almacén y luego confía en esa conexión durante toda su vida. <strong>Los tokens nunca se vuelven a comprobar en la ruta caliente por operación</strong>, así que activar la seguridad no te cuesta nada por cada <code>Get</code> o <code>Set</code>. Pagas la autenticación en el momento de la conexión, una vez, y nunca más para esa conexión.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="roles-y-ámbitos">Roles y ámbitos<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#roles-y-%C3%A1mbitos" class="hash-link" aria-label="Enlace directo al Roles y ámbitos" title="Enlace directo al Roles y ámbitos" translate="no">​</a></h2>
<p>Los permisos son un pequeño catálogo preestablecido de roles, cada uno concedido <em>en un ámbito</em> que dice <strong>dónde</strong> se aplica.</p>
<table><thead><tr><th>Rol</th><th>Nivel de ámbito</th><th>Qué concede</th></tr></thead><tbody><tr><td><code>ClusterAdmin</code></td><td>clúster</td><td>todo — todos los permisos del plano de control y del plano de datos</td></tr><tr><td><code>StoreOperator</code></td><td>almacén</td><td>iniciar/detener el almacén, editar config, iniciar/detener nodos, diagnósticos, métricas</td></tr><tr><td><code>Observer</code></td><td>almacén</td><td>diagnósticos y métricas, solo lectura</td></tr><tr><td><code>DataWriter</code></td><td>almacén</td><td>lectura / escritura / borrado / escaneo de datos</td></tr><tr><td><code>DataReader</code></td><td>almacén</td><td>lectura / escaneo de datos</td></tr></tbody></table>
<p>Un <strong>ámbito</strong> es jerárquico, y un ámbito más amplio cubre todo lo que hay por debajo de él:</p>
<ul>
<li class=""><code>zaris:cluster</code> — todo el clúster, para la administración a nivel de clúster.</li>
<li class=""><code>zaris:store:&lt;name&gt;</code> — un único almacén, p. ej. <code>zaris:store:orders</code>.</li>
<li class=""><em>root</em> (el ámbito vacío) — cubre todo; reservado para el administrador de arranque (bootstrap).</li>
</ul>
<p>La contención es <code>root ⊃ cluster ⊃ store</code>, de modo que un <code>DataWriter</code> concedido en <code>zaris:cluster</code> puede escribir en todos los almacenes, mientras que uno concedido en <code>zaris:store:orders</code> solo puede escribir allí. Compones el mínimo privilegio a partir de estos dos ejes: elige el rol más pequeño que haga el trabajo, y fíjalo al ámbito más estrecho que la identidad necesite.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="activarlo-sin-dejarte-fuera">Activarlo sin dejarte fuera<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#activarlo-sin-dejarte-fuera" class="hash-link" aria-label="Enlace directo al Activarlo sin dejarte fuera" title="Enlace directo al Activarlo sin dejarte fuera" translate="no">​</a></h2>
<p>Lo único que un sistema de seguridad jamás debe hacer es dejarte varado fuera de tu propio clúster. Zaris lo maneja haciendo de <strong>inicializar</strong> y <strong>activar</strong> un único paso: la aplicación solo entra en vigor <em>una vez que existe el primer administrador</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>El administrador es una <strong>identidad de inicio de sesión real</strong> — un nombre de usuario y una contraseña, como una cuenta root en la nube — no un token flotante que tengas que custodiar para siempre. Después de esto, los operadores inician sesión con credenciales y obtienen un token de sesión de corta duración. La propia aplicación es un <strong>conmutador en tiempo de ejecución</strong>, persistido por manager, que se activa sin reinicio ni edición de config:</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">Enable-ZrSecurity</span><span class="token plain">     </span><span class="token comment" style="color:#999988;font-style:italic"># control-plane enforcement ON  (requires an initialized admin)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">Disable-ZrSecurity</span><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># OFF — keeps the key and policy, so you can re-enable instantly</span><br></span></code></pre></div></div>
<p>Ambos se distribuyen a cada manager del workspace, de modo que el clúster se mantiene consistente.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dos-tipos-de-identidad-personas-y-aplicaciones">Dos tipos de identidad: personas y aplicaciones<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#dos-tipos-de-identidad-personas-y-aplicaciones" class="hash-link" aria-label="Enlace directo al Dos tipos de identidad: personas y aplicaciones" title="Enlace directo al Dos tipos de identidad: personas y aplicaciones" translate="no">​</a></h2>
<p>Siguiendo el mismo patrón que AWS IAM, Kubernetes y Vault, <strong>cada token pertenece a una identidad registrada</strong> — no puedes acuñar un token para un nombre arbitrario. Hay exactamente dos tipos, más el único administrador de arranque.</p>
<p><strong>Los usuarios son personas.</strong> Se crean con una contraseña (almacenada solo como un hash PBKDF2 con sal en <code>users.json</code> en cada manager — nunca en texto plano, sin base de datos externa) e <em>inician sesión</em> para obtener un token de sesión:</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>Las cuentas de servicio son aplicaciones.</strong> Sin contraseña; crear una te entrega un token autocontenido que la aplicación presenta y renueva:</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> sigue existiendo para emitir o rotar un token, pero el sujeto <strong>ya debe ser un usuario o cuenta de servicio registrado</strong>. No hay tokens ad hoc para nombres que no existen — esa es la propiedad que hace de "¿quién puede tocar este almacén?" una pregunta con respuesta.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="usar-un-token">Usar un token<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#usar-un-token" class="hash-link" aria-label="Enlace directo al Usar un token" title="Enlace directo al Usar un token" translate="no">​</a></h2>
<p>En el plano de datos, el token se integra directamente en la <a class="" href="https://clustron.io/es/blog/connection-strings/">cadena de conexión</a> — y nunca tienes que incrustar un secreto literal en ella, porque la indirección <code>env:</code> y <code>file:</code> es de primera clase:</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>Desde .NET es la misma cadena, o un <code>TokenProvider</code> en código cuando quieres rotación:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> services </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token constructor-invocation class-name">ServiceCollection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddClustronZarisConnectionString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token string" style="color:#e3116c">"zaris://node-a:7861,node-b:7861/orders?token=env:ZARIS_TOKEN"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">BuildServiceProvider</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token class-name keyword" style="color:#00009f">var</span><span class="token plain"> client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> services</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token generic-method function" style="color:#d73a49">GetRequiredService</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&lt;</span><span class="token generic-method generic class-name">IZarisClientProvider</span><span class="token generic-method generic class-name punctuation" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GetAsync</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"orders"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// token presented once, at the connection Hello</span><br></span></code></pre></div></div>
<p>Si el token falta, está expirado, es inválido o no tiene ámbito para este almacén, la conexión se rechaza con un estado <em>Unauthorized</em> — te enteras al conectar, no tres operaciones después dentro de una transacción. Si prefieres no acuñar y pegar un token a mano, <code>Connect-ZrStore</code> también puede intercambiar una credencial de usuario/contraseña por uno contra el Management Service y renovarlo en segundo plano.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="renovación-que-nunca-corta-una-conexión">Renovación que nunca corta una conexión<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#renovaci%C3%B3n-que-nunca-corta-una-conexi%C3%B3n" class="hash-link" aria-label="Enlace directo al Renovación que nunca corta una conexión" title="Enlace directo al Renovación que nunca corta una conexión" translate="no">​</a></h2>
<p>Aquí es donde "verificado solo al conectar" rinde el doble. Como un nodo comprueba el token <strong>solo cuando se abre la conexión</strong>, una conexión <em>en vuelo</em> nunca se interrumpe cuando su token expira. La renovación solo tiene que asegurarse de que el cliente tenga un token <em>actualmente válido</em> la próxima vez que abra una conexión — en una reconexión, un nodo nuevo, un reinicio de nodo.</p>
<p>Así que el cliente lee su token a través de un <strong>provider</strong> en cada (re)conexión. Mantén ese provider devolviendo tokens frescos y la renovación es completamente transparente:</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>El endpoint de renovación (<code>POST /security/token/renew</code>) es <strong>sin estado</strong> — verifica tu token actual sin conexión contra la clave compartida y devuelve uno nuevo con el mismo sujeto y roles y una expiración fresca, de modo que <em>cualquier</em> manager puede servirlo y sobrevive a reinicios. Un token expirado no se puede renovar (renueva antes de la expiración — el provider integrado lo hace), y un token o sujeto revocado se rechaza. La orientación que se desprende de esto es simple: prefiere <strong>vidas cortas con renovación automática</strong> a un solo token de larga duración. Obtienes la misma fluidez con mucha menos exposición.</p>
<p>Si no quieres código de cliente en el bucle en absoluto, apunta el provider a una variable de entorno o a un archivo y deja que un rotador externo — un gestor de secretos, un sidecar, un cron que reacuña — lo actualice fuera de banda:</p>
<div class="language-csharp codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-csharp codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">options</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TokenProvider </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> ZarisTokenProviders</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">FromEnvironment</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ZARIS_TOKEN"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// or ZarisTokenProviders.FromFile("/etc/zaris/token");</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-parte-honesta-dos-planos-significan-dos-conmutadores">La parte honesta: dos planos significan dos conmutadores<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#la-parte-honesta-dos-planos-significan-dos-conmutadores" class="hash-link" aria-label="Enlace directo al La parte honesta: dos planos significan dos conmutadores" title="Enlace directo al La parte honesta: dos planos significan dos conmutadores" translate="no">​</a></h2>
<p>Este es el detalle que muerde a la gente, así que tiene su propia sección. "La seguridad está activada" no es un hecho — son <strong>dos</strong>, porque hay dos puntos de aplicación.</p>
<p>El <strong>plano de control</strong> — el Management Service en el puerto 7801, donde creas almacenes, inicias y detienes nodos y gestionas concesiones — aplica en cuanto has inicializado y activado la seguridad. Ese es el conmutador que acciona <code>Enable-ZrSecurity</code>.</p>
<p>El <strong>plano de datos</strong> — los nodos de Zaris reales que sirven tus claves — es un conmutador <em>separado</em>. Un nodo solo aplica tokens una vez que tiene la clave pública compartida en su configuración, y eso lo aprovisionas por almacén:</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>Hasta que hagas esto, los nodos del almacén ejecutan un autenticador <code>AllowAll</code> y aceptan cada conexión — con token o sin él.</strong> Esto es intencional: te permite activar primero el plano de control y desplegar la aplicación en el plano de datos almacén por almacén, con un reinicio planificado, en lugar de accionar todo el clúster de una vez. Pero significa que <em>activar el plano de control no asegura, por sí mismo, tus datos.</em> Las dos trampas correspondientes:</p>
<ul>
<li class=""><strong><code>Get-ZrSecurityStatus</code> informa del plano de control.</strong> Sus campos <code>Enabled</code> / <code>Initialized</code> / <code>Issuer</code> describen la aplicación del plano de gestión. Un <code>True</code> ahí <strong>no</strong> demuestra que los nodos de un almacén dado estén verificando tokens — ese es el trabajo de <code>Enable-ZrStoreSecurity</code>, confirmado por separado. Si un almacén "asegurado" sigue aceptando clientes sin token, la config del nodo carece de la clave o no se reinició.</li>
<li class=""><strong>La revocación no es instantánea en la ruta de datos.</strong> Como los tokens de datos son autocontenidos y se comprueban solo al conectar, <code>Revoke-ZrToken</code> es honrado de inmediato por el plano de control y por la <em>renovación</em> (un sujeto revocado no puede refrescar), pero una conexión de datos ya abierta sigue funcionando hasta que se cierra. La forma limpia de cortar a una aplicación en el plano de datos es emitir tokens de datos con un <code>-LifetimeMinutes</code> modesto y dejar que el actual expire, o reiniciar sus conexiones tras revocar. Si necesitas un corte casi instantáneo, las vidas cortas son la palanca — no una expectativa de que la ruta caliente vuelva a comprobar.</li>
</ul>
<p>Ninguna de estas es un error; son las consecuencias directas de "verificar sin conexión, una vez, sin servidor de autenticación". Son simplemente la parte que tienes que operar deliberadamente. El despliegue recomendado se desprende directamente de ellas: inicializa y activa el plano de control, verifica con <code>Get-ZrSecurityStatus</code>, emite tokens de operador y de aplicación y confirma que funcionan, luego <code>Enable-ZrStoreSecurity</code> por almacén con un reinicio planificado, y finalmente mueve las aplicaciones a tokens de corta duración con renovación automática.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dónde-se-sitúa-en-la-pila">Dónde se sitúa en la pila<a href="https://clustron.io/es/blog/token-auth-roles-scopes/#d%C3%B3nde-se-sit%C3%BAa-en-la-pila" class="hash-link" aria-label="Enlace directo al Dónde se sitúa en la pila" title="Enlace directo al Dónde se sitúa en la pila" translate="no">​</a></h2>
<p>La autenticación por tokens responde a <em>quién puede tocar este almacén y qué puede hacer</em>. Se compone con las otras dos mitades de un Zaris blindado y no reemplaza a ninguna:</p>
<ul>
<li class=""><strong><a class="" href="https://clustron.io/es/blog/tls-ca-modes/">Confianza TLS enchufable</a></strong> cifra el cable y autentica los <em>extremos</em> — es el transporte <code>zariss://</code> bajo el token, ortogonal a quién sostiene el token.</li>
<li class=""><strong><a class="" href="https://clustron.io/es/blog/helm-secure-e2e/">El tutorial seguro de Helm</a></strong> reúne ambos en Kubernetes de principio a fin, que es donde los detalles de la cadena de conexión y del certificado realmente se ejercitan.</li>
</ul>
<p>Lo que vale la pena retener es la forma de la idea central. Una clave RS256, JWT que embeben su propia autoridad, verificación que es CPU puro sin conexión, y un token comprobado exactamente una vez por conexión. Eso es lo que te compra autenticación y autorización con rol y ámbito a través de un clúster entero sin levantar un servidor de autenticación, un almacén de sesiones ni un solo salto de red extra en la ruta que tu aplicación realmente ejecuta. El coste es la disciplina a la que esta entrada dedicó su segunda mitad: recuerda que el plano de datos es su propio conmutador, y mantén tus tokens de corta duración. Haz eso, y "asegurado" se convierte en una propiedad que puedes afirmar con precisión en lugar de esperar que lo esté.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Seguridad</category>
            <category>Operaciones</category>
            <category>Sistemas Distribuidos</category>
        </item>
        <item>
            <title><![CDATA[El factor de replicación no es tolerancia a fallos: dimensionar un clúster de Zaris para durabilidad en Kubernetes]]></title>
            <link>https://clustron.io/es/blog/kubernetes-durability-sizing/</link>
            <guid>https://clustron.io/es/blog/kubernetes-durability-sizing/</guid>
            <pubDate>Fri, 02 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En Kubernetes, Zaris mantiene la data en memoria y durable por replicación, no por disco. Pero fijar replicationFactor=2 no sobrevive por sí solo a la pérdida de un pod — el número de pods tiene que ser un múltiplo de RF, o una partición sobrante corre con una sola copia y pierde sus claves cuando ese pod muere. Aquí está la aritmética de ubicación, por qué un número impar de pods muerde, y cómo el chart te protege.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Dimensionar un clúster de Zaris para durabilidad en Kubernetes" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ3MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJTaXppbmcgYSBaYXJpcyBjbHVzdGVyIGZvciBkdXJhYmlsaXR5IG9uIEt1YmVybmV0ZXMiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+DQogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwYTE1MTIiLz4NCiAgICAgIDxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzBlMjExYiIvPg0KICAgIDwvbGluZWFyR3JhZGllbnQ+DQogICAgPHJhZGlhbEdyYWRpZW50IGlkPSJnbG93IiBjeD0iMC44MiIgY3k9IjAuMiIgcj0iMC43Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzEyYjU3ZSIgc3RvcC1vcGFjaXR5PSIwLjMwIi8+DQogICAgICA8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxMmI1N2UiIHN0b3Atb3BhY2l0eT0iMCIvPg0KICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPGxpbmVhckdyYWRpZW50IGlkPSJhY2NlbnQiIHgxPSIwIiB5MT0iMSIgeDI9IjAiIHkyPSIwIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzBlYTY3MiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMmJkNmEwIi8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgICA8c3R5bGU+LnNhbnN7Zm9udC1mYW1pbHk6IlNwYWNlIEdyb3Rlc2siLC1hcHBsZS1zeXN0ZW0sQmxpbmtNYWNTeXN0ZW1Gb250LCJTZWdvZSBVSSIsUm9ib3RvLEhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmO308L3N0eWxlPg0KICA8L2RlZnM+DQogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNiZykiLz4NCiAgPHJlY3Qgd2lkdGg9IjEyMDAiIGhlaWdodD0iNDcwIiBmaWxsPSJ1cmwoI2dsb3cpIi8+DQogIDxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSI2IiBoZWlnaHQ9IjQ3MCIgZmlsbD0idXJsKCNhY2NlbnQpIi8+DQogIDx0ZXh0IHg9IjgwIiB5PSI3MCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTciIGxldHRlci1zcGFjaW5nPSI0IiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjN2ZlM2JmIj5LVUJFUk5FVEVTIMK3IERVUkFCSUxJVFk8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIxMjYiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjQ0IiBmb250LXdlaWdodD0iODAwIiBmaWxsPSIjZmZmZmZmIiBsZXR0ZXItc3BhY2luZz0iLTAuNSI+UmVwbGljYXRpb24gZmFjdG9yPC90ZXh0Pg0KICA8dGV4dCB4PSI4MCIgeT0iMTgwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSI0NCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iI2ZmZmZmZiIgbGV0dGVyLXNwYWNpbmc9Ii0wLjUiPmlzIG5vdCBmYXVsdCB0b2xlcmFuY2U8L3RleHQ+DQogIDx0ZXh0IHg9IjgwIiB5PSIyMjQiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjN2ZlM2JmIj5TaXppbmcgYSBaYXJpcyBjbHVzdGVyIHNvIGEgcG9kIGxvc3MgbmV2ZXIgbG9zZXMgZGF0YTwvdGV4dD4NCg0KICA8IS0tIFJpZ2h0IGRpYWdyYW06IDQgcG9kcyAtPiAyIGZ1bGx5LXJlcGxpY2F0ZWQgcGFydGl0aW9ucyAoZHVyYWJsZSkgLS0+DQogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDcyMCwyNjYpIj4NCiAgICA8dGV4dCB4PSIwIiB5PSIwIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgbGV0dGVyLXNwYWNpbmc9IjIiIGZpbGw9IiM1YTZiNjUiPjQgUE9EUyDCtyBSRiAyIMK3IDIgUEFSVElUSU9OUzwvdGV4dD4NCg0KICAgIDwhLS0gUGFydGl0aW9uIEEgLS0+DQogICAgPHJlY3QgeD0iMCIgeT0iMTYiIHdpZHRoPSIyMDAiIGhlaWdodD0iNjYiIHJ4PSIxMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI2FjY2VudCkiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjEyIiB5PSIzOCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMyYmQ2YTAiPnBhcnRpdGlvbiBBPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjEyIiB5PSI0NiIgd2lkdGg9IjgwIiBoZWlnaHQ9IjI4IiByeD0iNiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiLz4NCiAgICA8dGV4dCB4PSI1MiIgeT0iNjUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5wb2QgMDwvdGV4dD4NCiAgICA8cmVjdCB4PSIxMDgiIHk9IjQ2IiB3aWR0aD0iODAiIGhlaWdodD0iMjgiIHJ4PSI2IiBmaWxsPSJub25lIiBzdHJva2U9IiM1YTZiNjUiIHN0cm9rZS13aWR0aD0iMiIvPg0KICAgIDx0ZXh0IHg9IjE0OCIgeT0iNjUiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjN2ZlM2JmIj5wb2QgMTwvdGV4dD4NCg0KICAgIDwhLS0gUGFydGl0aW9uIEIgLS0+DQogICAgPHJlY3QgeD0iMjIwIiB5PSIxNiIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI2NiIgcng9IjEwIiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjYWNjZW50KSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjMyIiB5PSIzOCIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMyYmQ2YTAiPnBhcnRpdGlvbiBCPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjIzMiIgeT0iNDYiIHdpZHRoPSI4MCIgaGVpZ2h0PSIyOCIgcng9IjYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMjcyIiB5PSI2NSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnBvZCAyPC90ZXh0Pg0KICAgIDxyZWN0IHg9IjMyOCIgeT0iNDYiIHdpZHRoPSI4MCIgaGVpZ2h0PSIyOCIgcng9IjYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzVhNmI2NSIgc3Ryb2tlLXdpZHRoPSIyIi8+DQogICAgPHRleHQgeD0iMzY4IiB5PSI2NSIgY2xhc3M9InNhbnMiIGZvbnQtc2l6ZT0iMTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM3ZmUzYmYiPnBvZCAzPC90ZXh0Pg0KDQogICAgPHRleHQgeD0iMCIgeT0iMTEyIiBjbGFzcz0ic2FucyIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzJiZDZhMCI+c3Vydml2ZXMgb25lIHBvZCBsb3NzICYjMTAwMDM7PC90ZXh0Pg0KDQogICAgPCEtLSBvZGQgcG9kIHdhcm5pbmcgLS0+DQogICAgPHJlY3QgeD0iMCIgeT0iMTMwIiB3aWR0aD0iNDIwIiBoZWlnaHQ9IjM0IiByeD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNWE2YjY1IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNiIvPg0KICAgIDx0ZXh0IHg9IjE2IiB5PSIxNTIiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjEzIiBmaWxsPSIjOWZiM2FkIj5wb2RzIG1vZCBSRiAmIzg4MDA7IDAgJiM4NTk0OyBhIGxlZnRvdmVyIHBhcnRpdGlvbiBpcyBzaW5nbGUtY29weTwvdGV4dD4NCiAgPC9nPg0KDQogIDx0ZXh0IHg9IjgwIiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMmJkNmEwIj5aYXJpczwvdGV4dD4NCiAgPHRleHQgeD0iMTQ2IiB5PSI0MzAiIGNsYXNzPSJzYW5zIiBmb250LXNpemU9IjIwIiBmb250LXdlaWdodD0iNDAwIiBmaWxsPSIjNWE2YjY1Ij5ieSBDbHVzdHJvbjwvdGV4dD4NCjwvc3ZnPg0K" width="1200" height="470" class="img_ev3q"></p>
<p>Hay una línea de razonamiento tentadora cuando despliegas un almacén replicado en Kubernetes: <em>fijé <code>replicationFactor: 2</code>, así que cada partición tiene un backup, así que sobrevivo a la pérdida de un nodo.</em> Suena hermético. También es, para un tamaño de clúster sorprendentemente común, incorrecto — e incorrecto de la forma silenciosa que solo aparece la primera vez que un pod realmente muere.</p>
<p>Zaris en Kubernetes almacena la data <strong>en memoria</strong> y la hace durable por <strong>replicación, no por disco</strong>: el volumen de data del nodo es un <code>emptyDir</code>, y un pod reprogramado reconstruye a partir de la copia que tiene otro pod. Ese diseño es sólido — pero se apoya enteramente en que <em>haya</em> una copia en otro pod. Que esa copia exista no lo decide <code>replicationFactor</code> por sí solo. Lo decide la aritmética entre tu número de pods y tu factor de replicación, y si te equivocas en esa aritmética el almacén correrá perfectamente, servirá cada lectura y escritura, y en silencio sostendrá una partición que no tiene a dónde replicarse.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-forma-del-despliegue">La forma del despliegue<a href="https://clustron.io/es/blog/kubernetes-durability-sizing/#la-forma-del-despliegue" class="hash-link" aria-label="Enlace directo al La forma del despliegue" title="Enlace directo al La forma del despliegue" translate="no">​</a></h2>
<p>El chart de Helm de Zaris despliega la capa de nodos como un <strong>StatefulSet</strong>, una instancia de nodo por pod (<code>zaris-0</code>, <code>zaris-1</code>, …), cada una un propietario de shard en memoria. Un Service headless le da a cada pod una identidad de DNS estable, la readiness se condiciona a la convergencia del clúster, y un PodDisruptionBudget más una ventana de terminación con drenado elegante protegen el conjunto durante las interrupciones voluntarias.</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>Las propias notas del chart son contundentes sobre de dónde viene la durabilidad:</p>
<blockquote>
<p><strong>La data del nodo es emptyDir.</strong> La durabilidad en memoria es vía replicación (RF), no disco; un nodo reprogramado reconstruye a partir de las réplicas.</p>
</blockquote>
<p>Así que un pod de nodo puede ser matado y reprogramado todo el día sin pérdida de data — <em>siempre que</em> sus particiones tengan una réplica viva en un pod diferente del cual reconstruir. Esa condición es todo el partido.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cómo-aterrizan-las-particiones-en-los-pods">Cómo aterrizan las particiones en los pods<a href="https://clustron.io/es/blog/kubernetes-durability-sizing/#c%C3%B3mo-aterrizan-las-particiones-en-los-pods" class="hash-link" aria-label="Enlace directo al Cómo aterrizan las particiones en los pods" title="Enlace directo al Cómo aterrizan las particiones en los pods" translate="no">​</a></h2>
<p>Una partición en Zaris la sostienen <strong>RF instancias de nodo distintas</strong> — un propietario activo más <code>RF−1</code> réplicas — y las réplicas siempre se colocan en pods <em>diferentes</em> (una réplica en el mismo pod que su activo no sería protección alguna). El runtime coloca las particiones de forma codiciosa:</p>
<blockquote>
<p>El runtime llena una partición hasta su RF completo antes de formar la siguiente.</p>
</blockquote>
<p>Esa única frase es la clave de toda la historia del dimensionamiento. Las instancias se consumen de RF en RF. Así que el número de particiones <strong>completamente replicadas</strong> que obtienes es:</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>y cualquier instancia sobrante — <code>instances mod RF</code> de ellas — forma una partición de <strong>una sola copia</strong> sin par al cual replicarse. Es una partición real, que sirve. Simplemente no tiene backup.</p>
<!-- -->
<p>Con cuatro pods a RF 2 obtienes dos particiones, cada una con dos copias en pods distintos — pierde cualquier pod y la copia superviviente continúa mientras la partición se re-replica en el pod reprogramado. Con tres pods a RF 2, los pods 0 y 1 forman la partición A a RF completo, pero el pod 2 queda sobrante y forma la partición B con una sola copia. Añade un cuarto pod y se convierte en la réplica de B. Hasta entonces, B es un pasivo.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="la-regla-de-dimensionamiento-como-tabla">La regla de dimensionamiento, como tabla<a href="https://clustron.io/es/blog/kubernetes-durability-sizing/#la-regla-de-dimensionamiento-como-tabla" class="hash-link" aria-label="Enlace directo al La regla de dimensionamiento, como tabla" title="Enlace directo al La regla de dimensionamiento, como tabla" translate="no">​</a></h2>
<p>Como cada partición necesita RF pods distintos y Kubernetes corre una instancia por pod, la regla es simplemente:</p>
<blockquote>
<p><strong>Corre un número de pods que sea múltiplo de RF.</strong> Con RF 2, usa un número par de pods.</p>
</blockquote>
<table><thead><tr><th>Pods (1 instancia cada uno)</th><th>RF</th><th>Particiones completamente replicadas</th><th>¿Sobrevive la pérdida de un pod?</th></tr></thead><tbody><tr><td>2</td><td>2</td><td>1</td><td>Sí</td></tr><tr><td><strong>3</strong></td><td>2</td><td>1 (+ <strong>1 de una sola copia</strong>)</td><td><strong>No</strong> — perder el pod de una sola copia pierde esa partición</td></tr><tr><td>4</td><td>2</td><td>2</td><td>Sí</td></tr><tr><td>6</td><td>2</td><td>3</td><td>Sí</td></tr></tbody></table>
<p>El patrón se generaliza: <code>N</code> particiones a RF 2 necesitan <code>2N</code> pods; a RF 3 necesitan <code>3N</code>. Escalar hacia afuera significa añadir pods de un RF completo a la vez si quieres que cada nueva partición nazca durable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="por-qué-tres-pods-con-rf-2-es-la-trampa">Por qué tres pods con RF 2 es la trampa<a href="https://clustron.io/es/blog/kubernetes-durability-sizing/#por-qu%C3%A9-tres-pods-con-rf-2-es-la-trampa" class="hash-link" aria-label="Enlace directo al Por qué tres pods con RF 2 es la trampa" title="Enlace directo al Por qué tres pods con RF 2 es la trampa" translate="no">​</a></h2>
<p>Tres es el número que muerde, porque tres pods <em>parecen</em> un clúster sano y de alta disponibilidad. Pasa la readiness. Sirve tráfico. <code>kubectl get pods</code> muestra <code>zaris-0..2</code> todos <code>1/1 Ready</code>. Nada está obviamente mal.</p>
<p>Lo que está mal es invisible hasta que el pod de una sola copia — el que sostiene la única copia de la partición B — es el pod que muere. En ese instante las claves de B se han ido, permanentemente, porque nunca hubo una segunda copia de la cual reconstruir. Esto no es un bug; es la misma advertencia que correr una sola máquina, solo que acotada a una partición en lugar de a todo el almacén. El mapa solo se regenera cuando se pierden <em>todas</em> las copias de una partición, y para una partición de una sola copia "todas las copias" es un pod.</p>
<p>Esto se midió, no se asumió. En un clúster <code>kind</code>:</p>
<ul>
<li class="">Con <strong><code>replicas=4</code></strong> (2 particiones × RF 2), matar el pod propietario-activo de una partición perdió <strong>0 de 200 claves</strong> — la réplica tomó el relevo y el RF se autorrestauró en el pod reprogramado.</li>
<li class="">Con <strong><code>replicas=3</code></strong>, matar el pod de una sola copia perdió las claves de esa partición por completo.</li>
</ul>
<p>Mismo almacén, mismo ajuste de RF, mismo caos. La única variable fue si el número de pods era múltiplo de RF.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="las-barandillas-que-el-chart-te-da">Las barandillas que el chart te da<a href="https://clustron.io/es/blog/kubernetes-durability-sizing/#las-barandillas-que-el-chart-te-da" class="hash-link" aria-label="Enlace directo al Las barandillas que el chart te da" title="Enlace directo al Las barandillas que el chart te da" translate="no">​</a></h2>
<p>El chart no deja esto al folclore. Instala un clúster cuyo número de pods no sea múltiplo de RF y <code>helm install</code> imprime una advertencia de durabilidad en sus notas:</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>Más allá de la advertencia, varios defaults se fijan para que un clúster <em>correctamente dimensionado</em> se comporte bien bajo interrupción:</p>
<ul>
<li class=""><strong>Readiness condicionada a la convergencia.</strong> <code>/readyz</code> pasa solo una vez que el mapa de particiones y la propiedad están vivos, de modo que un pod <code>Ready</code> es uno enrutable — Kubernetes no enviará tráfico a un nodo que aún no ha tomado la propiedad.</li>
<li class=""><strong>PodDisruptionBudget.</strong> Por defecto, a lo sumo un nodo se interrumpe voluntariamente a la vez, de modo que un drenado o una actualización rodante no puede sacar ambas copias de una partición a la vez.</li>
<li class=""><strong>Terminación con drenado elegante.</strong> Una ventana de gracia de terminación deja que un nodo saliente haga su entrega limpiamente en lugar de ser derribado en pleno vuelo.</li>
<li class=""><strong>Escalado hacia afuera en caliente.</strong> Con el descubrimiento de Kubernetes activado (el default), el roster se sintetiza a partir de los ordinales de pod y el DNS headless, de modo que añadir pods es un simple <code>kubectl scale statefulset</code> — los nuevos pods se unen y el mapa de particiones crece sin reiniciar los existentes. Añádelos de un múltiplo de RF a la vez y cada nueva partición es durable desde su nacimiento.</li>
</ul>
<p>Nada de esto ayuda a la partición de una sola copia, sin embargo. Un PDB que mantiene todos-menos-uno de los pods arriba no es protección cuando uno de esos pods es el <em>único</em> hogar de una partición. Las barandillas asumen que dimensionaste el clúster correctamente; no rescatan a un clúster que no lo estaba.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reglas-prácticas">Reglas prácticas<a href="https://clustron.io/es/blog/kubernetes-durability-sizing/#reglas-pr%C3%A1cticas" class="hash-link" aria-label="Enlace directo al Reglas prácticas" title="Enlace directo al Reglas prácticas" translate="no">​</a></h2>
<p>Todo el tema se comprime en unos pocos hábitos:</p>
<ol>
<li class=""><strong>Elige RF primero, luego haz que el número de pods sea múltiplo de él.</strong> RF 2 → números pares de pods; RF 3 → múltiplos de tres. El chart por defecto entrega 4 pods a RF 2 precisamente porque es la forma tolerante a fallos más pequeña.</li>
<li class=""><strong>Trata un número impar de pods como solo-evaluación.</strong> Tres pods a RF 2 está bien para una demo en <code>kind</code>; no es una postura de durabilidad de producción, y el chart te lo dice en el momento de la instalación.</li>
<li class=""><strong>Escala por RF.</strong> Añadir un pod a un clúster durable crea temporalmente una partición de una sola copia hasta que llega el siguiente pod. Si creces por durabilidad, crece un conjunto de replicación completo a la vez.</li>
<li class=""><strong>Recuerda que el volumen es emptyDir.</strong> No hay disco al que recurrir. La replicación <em>es</em> tu durabilidad, que es exactamente por qué la copia tiene que existir realmente en otro pod.</li>
</ol>
<p>El factor de replicación te dice cuántas copias <em>quiere</em> una partición. El número de pods decide si puede <em>tenerlas</em>. Alinea esos dos números y la pérdida de un pod es un no-evento — la réplica sirve, el pod reprogramado reconstruye, y no se pierde ni una clave. Deja que se desalineen y has construido un clúster que es de alta disponibilidad justo hasta el momento en que importa.</p>]]></content:encoded>
            <category>Clustron</category>
            <category>Kubernetes</category>
            <category>Operaciones</category>
        </item>
    </channel>
</rss>