Zum Hauptinhalt springen

Die Speicherobergrenze: Warum ein Zaris-Node nie unbegrenzt wächst

· 10 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Ein Zaris-Node hält die Speichernutzung in einem ruhigen Band unterhalb seiner Obergrenze und führt bei 80 % eine Eviction bis hinunter auf 70 % durch, sodass Schreibzugriffe das Limit nie überschreiten

Hier ist eine Frage, die einfach klingt und es nicht ist: Was passiert, wenn einem In-Memory-Store der Speicher ausgeht?

Die naive Antwort lautet „er stürzt ab" — der Prozess wächst, bis das Betriebssystem ihn beendet. Die etwas weniger naive Antwort lautet „er lehnt Schreibzugriffe ab". Zaris tut standardmäßig keines von beidem. Ein Zaris-Node ist ein In-Memory-Store, aber bewusst kein unbegrenzter: Jeder Node ist ein begrenzter Puffer mit einer harten Obergrenze, und wenn sich die Daten, die er besitzt, dieser Obergrenze nähern, entfernt der Node seine kältesten Daten, um Platz zu schaffen. Schreibzugriffe gelingen weiter; das Limit hält; der Prozess bläht sich nicht auf. Der Node verhält sich wie ein Cache, denn darunter ist er einer.

Jeder Node ist ein begrenzter Puffer​

Ein Zaris-Node wächst nicht ohne Grenze. Jeder Node bilanziert fortlaufend die Größe der Daten, die er aktiv besitzt, und hält diese laufende Summe unter einer konfigurierten Obergrenze. Diese Bilanzierung ist nie optional — ein Node ist immer ein begrenzter Puffer. Konfigurierbar sind nur, wie groß die Obergrenze ist und im Prinzip, was der Node tut, wenn er sie erreicht.

Die Standardobergrenze beträgt 1 GiB aktiv besessener Daten pro Node. Eine Einstellung „unbegrenzt" gibt es bewusst nicht. Setzen Sie keine Obergrenze, erhalten Sie den Default — Sie bekommen nie versehentlich Unendlich.

Ein Detail ist wichtig, um den Footprint eines Clusters zu beurteilen: Die Obergrenze deckt die Daten ab, die ein Node aktiv besitzt — die Primärkopien, die er Clients bereitstellt. Replica-Kopien, die ein Node für andere Partitionen hält, werden separat erfasst und zählen nicht gegen die Obergrenze für aktive Owner. Der tatsächlich belegte Speicher eines Nodes ist also sein besessenes Working Set plus alle Replica-Kopien, die er für seine Peers trägt; die Obergrenze regelt Ersteres, nicht die Summe.

Zwei Wege, die Linie zu halten​

Ein begrenzter Puffer kann genau zwei Dinge tun, wenn ein Schreibzugriff ihn über seine Obergrenze triebe: etwas Altes verwerfen, um Platz zu machen, oder den neuen Schreibzugriff ablehnen.

ModusVerhalten an der ObergrenzeEin Schreibzugriff, der sie überschreiten würde
Eviction aktiviert (Default)Entfernt ältere Daten, um Platz zu schaffenZugelassen — kältere Daten werden verworfen
Eviction deaktiviertLehnt neue Daten abAbgelehnt mit KvStatus.CapacityExceeded

Bei aktivierter Eviction ist ein Node ein Cache: Unter Druck verwirft er die am wenigsten wertvollen Daten, damit Schreibzugriffe weiterfließen. Bei deaktivierter Eviction ist er ein Store, der bei voller Kapazität ablehnt und bestehende Daten schützt, indem er neue Schreibzugriffe abweist.

Hier kommt der ehrliche Teil. In der aktuellen Version läuft ein Node immer mit aktivierter Eviction und der LRU-Policy. Der Modus ohne Eviction und die alternativen Policies (LFU, TTL-bewusst) existieren in der Engine, sind aber noch nicht per Konfiguration wählbar. Praktische Folge: CapacityExceeded — der Status für Ablehnung bei voller Kapazität — ist heute im Wesentlichen nicht erreichbar: Ein voller Node entfernt Daten, statt abzulehnen. Es ist ein echter, typisierter Status, gegen den Sie Code schreiben können, aber unter der stets aktiven LRU-Policy werden Sie ihn normalerweise nicht sehen. Der eine Speicher-Regler, den Sie derzeit tatsächlich steuern, ist die Größe der Obergrenze selbst, memory.maxSizeBytes.

Der Eviction-Lauf bewegt sich in einem ruhigen Band​

Das Interessante an der Ingenieursarbeit ist nicht, dass ein Node Daten entfernt — sondern wie, denn ein naives Design „bei jedem Schreibzugriff entfernen, sobald voll" flattert kläglich. Stellen Sie sich einen Node vor, der genau an seiner Obergrenze sitzt: Jeder einzelne eingehende Schreibzugriff würde eine Eviction auslösen, um sich selbst Platz zu machen, und Sie würden diese Kosten für immer auf dem Hot Path zahlen.

Zaris vermeidet das mit zwei Maßnahmen. Erstens wird die Eviction von einer Hintergrundschleife getrieben, nicht vom einzelnen Schreibzugriff — der Schreibzugriff, der einen Node über die Kante kippt, wird zugelassen, und die Schleife schafft kurz darauf Platz, abseits des Request-Pfads. Zweitens nutzt die Schleife Hysterese: einen hohen Auslöser und ein niedrigeres Ziel, sodass sie in Schüben Platz schafft und dann ruhig wird, statt am Rand kontinuierlich zu feuern.

  • Auslöser: Ein Lauf startet, wenn die aktive Nutzung über 80 % der Obergrenze steigt.
  • Ziel: Einmal gestartet, entfernt der Lauf so lange Daten, bis wieder etwa 30 % der Obergrenze frei sind — bis die Nutzung auf etwa 70 % zurückgefallen ist.
  • Taktung: Die Schleife wacht in einem festen Intervall auf (standardmäßig eine Sekunde) und zieht in jedem Zyklus eine begrenzte Stichprobe von Kandidaten.

Da das Ziel nach der Eviction (70 %) strikt unter dem Auslöser (80 %) liegt, senkt jeder Lauf die Nutzung in ein ruhiges Band, statt genau an dem Punkt zu stoppen, der ihn sofort wieder auslösen würde. Das ist der Sägezahn im Titelbild: Die Nutzung steigt auf 80 %, ein Lauf bringt sie zurück auf 70 %, und sie steigt erneut — nie an die Obergrenze gedrückt, nie mit einer Eviction pro Schreibzugriff flatternd.

Die Eviction-Policy selbst ist LRU — Least Recently Used — und sie arbeitet per Stichprobe, nicht strikt. Statt eine global geordnete Liste jedes Keys zu führen (was Speicher und Lock-Contention auf dem Hot Path kosten würde), stempelt Zaris auf dem Lesepfad ungefähre Aktualitätssignale an Ort und Stelle ein und zieht, wenn ein Lauf feuert, eine kleine Menge von Kandidaten und verwirft die mit der niedrigsten Bewertung. Es ist derselbe Stichprobenansatz, den Redis verwendet, und derselbe Kompromiss: Sie geben die perfekte Reihenfolge „den einen ältesten Key entfernen" auf und erhalten dafür eine Eviction, die fast nichts in der Pflege kostet und nie einen separaten Index braucht.

Die andere Obergrenze: die Größe pro Item​

Die gesamte Speicherobergrenze ist ein Limit für die Summe. Es gibt ein zweites, unabhängiges Limit für jedes einzelne Item: Standardmäßig darf ein Wert 4 MiB nicht überschreiten. Das wird bei jedem Schreibzugriff unabhängig vom Eviction-Modus geprüft, und hier trifft ein Schreibzugriff auf eine tatsächlich erreichbare Ablehnung.

Die Unterscheidung ist wichtig, weil die beiden Fehler entgegengesetzte Dinge bedeuten und entgegengesetzte Reaktionen verlangen:

var result = await client.PutAsync("report:2026-07", payload);

if (!result.IsSuccess)
{
switch (result.Status)
{
case KvStatus.ItemTooLarge:
// This one value is too big. No amount of eviction helps —
// split it, or store a reference to it elsewhere.
break;

case KvStatus.CapacityExceeded:
// The node is full and eviction is disabled (not the default).
// Retry later or shed load.
break;
}
}

ItemTooLarge ist in jedem Modus ein Live-, erreichbares Ergebnis — Eviction kann für einen Wert über dem Limit pro Item keinen Platz schaffen, denn bei dem Limit geht es nicht um freien Platz, sondern um einen einzelnen pathologischen Wert. CapacityExceeded erscheint, wie oben besprochen, nur im Modus ohne Eviction, den Sie noch nicht wählen können. Beide gleich zu behandeln ist ein Bug, der nur darauf wartet, aufzutreten: Einen ItemTooLarge-Schreibzugriff endlos zu wiederholen, wird nie gelingen.

Eine verwandte Eigenheit liegt an der Grenze zwischen diesem Limit pro Item und der .NET-Runtime: Werte ab etwa 85.000 Bytes landen auf dem Large Object Heap, daher ist das Anheben von maxItemSizeBytes, um große Werte zuzulassen, eine bewusste Entscheidung mit GC-Folgen und kein Regler zum Nulltarif. Wenn Sie große einfache Werte per Hot-Update ändern, ist das Zerlegen in Stücke unterhalb der LOH-Grenze der Mechanismus, der den LOH vom Churn bewahrt — ein eigenes Feature mit eigenen Kompromissen, das wir in Große Objekte speichern, ohne den Heap zu belasten aufgeschlüsselt haben.

Was Sie konfigurieren und was nicht​

Die Speicherobergrenze steht in der Store-Konfiguration im Abschnitt memory:

{
"memory": {
"maxSizeBytes": 2147483648
}
}

Das sind 2 GiB. Setzen Sie maxSizeBytes auf null oder lassen Sie es weg, erhalten Sie den Default von 1 GiB. In der aktuellen Version ist das die Speichereinstellung, die Sie steuern — die LRU-Policy, die Hysterese-Schwellen 80 %/70 % und die Taktung von 1 Sekunde sind feste Defaults, keine Regler. Es ist wichtig, hier nüchtern zu sein: Die Blogbeiträge und die Engine beschreiben einen reichhaltigeren Policy-Raum, als die Konfigurationsoberfläche derzeit bereitstellt, und dieser Beitrag beschreibt, was Sie heute tatsächlich einstellen können.

Was das für die Dimensionierung eines Clusters bedeutet​

Da jeder Node ein begrenzter Puffer ist, entspricht die Gesamtkapazität eines Stores grob der Obergrenze pro Node multipliziert mit der Anzahl der Partitionen, die ein Node besitzt, angepasst um den Replikationsfaktor (Replicas halten zusätzliche Kopien, die außerhalb der Obergrenze für aktive Owner liegen). Daraus ergeben sich zwei praktische Konsequenzen:

  • Dimensionieren Sie die Obergrenze nach Ihrem Working Set plus Eviction-Spielraum, nicht nach Ihren Gesamtdaten. Ein Node betreibt heute einen stets aktiven LRU-Cache, sodass kalte Daten unter Druck verworfen statt abgelehnt werden. Passt Ihr Working Set unter etwa 70 % der Obergrenze, mit Raum für das Band zum Atmen, bleibt die Eviction ruhig und das Hot Set resident. Dimensionieren Sie die Obergrenze unter Ihrem Working Set, haben Sie einen Cache gebaut, der ständig Daten verwirft, die er gleich wieder braucht — korrekt, aber langsam, und aus dem Sägezahn wird ein Mahlen.
  • Vergessen Sie den Replica-Footprint nicht. Die Obergrenze regelt besessene Daten, aber der tatsächliche RAM eines Nodes besteht aus besessenen Daten plus Replica-Kopien. Planen Sie beim Provisionieren von Speicher pro Pod oder pro Maschine beides ein, sonst läuft ein Node, der „auf seine Obergrenze" dimensioniert ist, immer noch heißer, als die Obergrenze vermuten lässt.

Für die Kapazitätsrechnung im Zusammenhang mit Partitionen, Replikationsfaktor und Pod-Anzahl rechnet Einen Zaris-Cluster für Dauerhaftigkeit unter Kubernetes dimensionieren die vollständige Berechnung durch.

Das Fazit​

Ein In-Memory-Store, der ohne Grenze wächst, ist kein Feature — er ist ein Ausfall, der auf eine Traffic-Spitze wartet. Zaris behandelt jeden Node als begrenzten Puffer mit einer echten Obergrenze (standardmäßig 1 GiB, keine unbegrenzte Option), einem stets aktiven Sampled-LRU-Cache darunter und einem Hysterese-Band, das Platz in ruhigen Schüben zurückgewinnt, statt am Rand mit einer Eviction pro Schreibzugriff zu flattern. Die Gesamtobergrenze und das Limit pro Item sind zwei verschiedene Limits mit zwei verschiedenen typisierten Status, und der, der heute tatsächlich erreichbar ist — ItemTooLarge — verlangt eine andere Reaktion als eine Retry-Schleife. Dimensionieren Sie die Obergrenze nach Ihrem Working Set, planen Sie Replica-Kopien obendrauf ein, und der Node hält seine Linie, ohne dass Sie ihn beobachten müssen.

Das vollständige Modell, einschließlich der Policies, die die Engine mitbringt, aber noch nicht bereitstellt, steht in Speicher und Eviction; die genauen Felder und Defaults stehen in der Konfigurationsreferenz, und jeder Kapazitätsstatus ist in der Status- und Exception-Referenz katalogisiert.