feat(stalwart): Pods einer Knotengruppe verteilen und ein PodDisruptionBudget je Gruppe #96

Closed
opened 2026-10-07 11:13:05 +02:00 by jlab-bot · 3 comments
Member

Der stalwart-Chart kann eine Mehr-Knoten-Installation nicht hochverfügbar machen: Es fehlt jede Möglichkeit, die Pods einer Knotengruppe über Knoten zu verteilen, und ein PodDisruptionBudget gibt es auch nicht. Beides zusammen entscheidet darüber, ob eine zweite Replica etwas gegen den Ausfall bringt, für den sie hinzugefügt wurde.

Anlass

In jk-cluster laufen seit dem 05.10.2026 zwei Knotengruppen (full und mx, je eine Replica, #250). Der nächste Schritt wäre je zwei. Die Lage dort macht das Problem konkret:

jk-worker-1   CPU 97% angefordert
jk-worker-2   CPU 73%
jk-worker-3   CPU 55%

Drei Arbeitsknoten, vier Stalwart-Pods — ohne Verteilungsregel landen beide Replicas einer Gruppe möglicherweise auf demselben Knoten, und die Redundanz ist nominell.

Gemessener Stand (Chart 1.0.1+upv0.16.24)

  • Kein affinity, kein topologySpreadConstraints, kein nodeSelector, kein tolerations — weder in der values.yaml noch in einem Template.
  • Kein PDB-Template. Neun Templates, keines davon.
  • Das StatefulSet setzt weder updateStrategy noch podManagementPolicy. Das ist in Ordnung und soll so bleiben: Die Kubernetes-Vorgaben sind RollingUpdate mit partition: 0 und OrderedReady, ein Update läuft also ohnehin strikt ein Pod zur Zeit und wartet auf Ready. Hier ist nichts hinzuzufügen — der Hinweis steht nur da, damit niemand redundante Schalter einbaut.

Teil 1: Verteilung über Knoten

Vorschlag, zwei Ebenen:

  1. Ein bequemer Schalter je Gruppe, bei dem der Chart den Selektor selbst füllt:

    nodes:
      full:
        spreadAcrossNodes: true
    

    Gerendert als topologySpreadConstraints auf topologyKey: kubernetes.io/hostname, maxSkew: 1, mit einem labelSelector aus den Labels, die der Chart ohnehin setzt (stalwart.jk-dev.de/release und stalwart.jk-dev.de/node-group).

  2. Generische Durchreichung je Gruppe für alles andere — affinity, nodeSelector, tolerations, topologySpreadConstraints —, mit toYaml | nindent.

Warum beides und nicht nur die Durchreichung: Ein topologySpreadConstraints, dessen labelSelector die Pods nicht trifft, scheitert still. Die Bedingung passt dann auf null Pods, ist damit trivial erfüllt, und verteilt wird nichts — bei grünem Rollout und ohne eine einzige Meldung. Der Chart kennt seine Labels, der Benutzer muss sie nicht abschreiben.

Und whenUnsatisfiable gehört begründet statt vorgegeben. Mit DoNotSchedule bleibt eine zweite Replica bei knapper Kapazität dauerhaft Pending — und ein Pending-Pod ohne Scheduling-Ereignis liest sich wie ein anderes Problem, nicht wie eine Verteilungsregel (dieselbe Klasse Irrweg wie beim inotify-Fall der Actions-Runner). Vorschlag: ScheduleAnyway als Vorgabe, mit einem Kommentar, der sagt, was das kostet — Verteilung nach bestem Bemühen, keine Zusage.

Teil 2: PodDisruptionBudget

Hier gibt es einen Präzedenzfall im selben Repository, und der sollte übernommen werden: keycloak.pdb.yaml mit keycloak.podDisruptionBudget.{enabled, minAvailable}. Dessen Kommentar nennt auch den Grund, warum die Bedingung wichtiger ist als das Objekt:

minAvailable: 1 with exactly ONE replica forbids every voluntary eviction, so kubectl drain blocks forever on a pod nothing is allowed to move. The message then names the PDB and not the replica count, which sends the reader to the wrong file.

Für Stalwart kommt eine Besonderheit dazu: Es gibt Knotengruppen. Also ein PDB je Gruppe mit der Replica-Zahl dieser Gruppe, und die Bedingung „erst ab zwei" gilt entsprechend pro Gruppe — bei full: 2 und mx: 1 entsteht genau ein PDB. Unter scaleDown: true keines, aus demselben Grund wie bei Keycloak.

minAvailable statt maxUnavailable, analog zu Keycloak: Die Zusage betrifft den Dienst, nicht die Störung.

Warum das nicht im Konsumenten-Repository gelöst werden kann

Das PDB allein ginge als eigenes Manifest-Bundle in cluster-config. Die Verteilung nicht: Sie steht im Pod-Template des StatefulSets, und das von außen zu patchen wäre genau der Drift, den correctDrift: unset bis zur nächsten Chart-Änderung festschreibt. Außerdem müsste der Selektor des PDB die Chart-Labels nachbauen und würde bei einer Umbenennung still falsch.

Abnahme

  • helm template mit nodes.full.replicas: 2 und spreadAcrossNodes: true rendert eine topologySpreadConstraints mit einem Selektor, der auf die Labels des eigenen Pod-Templates passt (am gerenderten Ergebnis abgleichen, nicht nach Augenmaß).
  • Bei replicas: 1 wird kein PDB gerendert; bei 2 genau eines je Gruppe; bei scaleDown: true keines.
  • Die generische Durchreichung erlaubt zusätzlich ein affinity ohne den Schalter.
  • README und NOTES.txt sagen, welcher der Fälle eingetreten ist — bei Keycloak ist das schon so gelöst.

Dringlichkeit

Mittel. Es blockiert nichts, was heute läuft: Mit je einer Replica ist weder ein PDB noch eine Verteilungsregel sinnvoll. Es blockiert aber den nächsten Schritt — und zwar auf die unangenehme Art, weil eine zweite Replica ohne Verteilung aussieht wie Redundanz und keine ist.

Der `stalwart`-Chart kann eine Mehr-Knoten-Installation nicht hochverfügbar machen: Es fehlt jede Möglichkeit, die Pods einer Knotengruppe über Knoten zu **verteilen**, und ein **PodDisruptionBudget** gibt es auch nicht. Beides zusammen entscheidet darüber, ob eine zweite Replica etwas gegen den Ausfall bringt, für den sie hinzugefügt wurde. ## Anlass In `jk-cluster` laufen seit dem 05.10.2026 zwei Knotengruppen (`full` und `mx`, je eine Replica, #250). Der nächste Schritt wäre je **zwei**. Die Lage dort macht das Problem konkret: ``` jk-worker-1 CPU 97% angefordert jk-worker-2 CPU 73% jk-worker-3 CPU 55% ``` Drei Arbeitsknoten, vier Stalwart-Pods — ohne Verteilungsregel landen beide Replicas einer Gruppe möglicherweise auf **demselben** Knoten, und die Redundanz ist nominell. ## Gemessener Stand (Chart `1.0.1+upv0.16.24`) - **Kein `affinity`, kein `topologySpreadConstraints`, kein `nodeSelector`, kein `tolerations`** — weder in der `values.yaml` noch in einem Template. - **Kein PDB-Template.** Neun Templates, keines davon. - Das StatefulSet setzt **weder `updateStrategy` noch `podManagementPolicy`**. Das ist in Ordnung und soll so bleiben: Die Kubernetes-Vorgaben sind `RollingUpdate` mit `partition: 0` und `OrderedReady`, ein Update läuft also ohnehin strikt ein Pod zur Zeit und wartet auf `Ready`. **Hier ist nichts hinzuzufügen** — der Hinweis steht nur da, damit niemand redundante Schalter einbaut. ## Teil 1: Verteilung über Knoten Vorschlag, zwei Ebenen: 1. **Ein bequemer Schalter je Gruppe**, bei dem der Chart den Selektor selbst füllt: ```yaml nodes: full: spreadAcrossNodes: true ``` Gerendert als `topologySpreadConstraints` auf `topologyKey: kubernetes.io/hostname`, `maxSkew: 1`, mit einem `labelSelector` aus den Labels, die der Chart ohnehin setzt (`stalwart.jk-dev.de/release` und `stalwart.jk-dev.de/node-group`). 2. **Generische Durchreichung je Gruppe** für alles andere — `affinity`, `nodeSelector`, `tolerations`, `topologySpreadConstraints` —, mit `toYaml | nindent`. **Warum beides und nicht nur die Durchreichung:** Ein `topologySpreadConstraints`, dessen `labelSelector` die Pods nicht trifft, **scheitert still**. Die Bedingung passt dann auf null Pods, ist damit trivial erfüllt, und verteilt wird nichts — bei grünem Rollout und ohne eine einzige Meldung. Der Chart kennt seine Labels, der Benutzer muss sie nicht abschreiben. **Und `whenUnsatisfiable` gehört begründet statt vorgegeben.** Mit `DoNotSchedule` bleibt eine zweite Replica bei knapper Kapazität dauerhaft `Pending` — und ein `Pending`-Pod ohne Scheduling-Ereignis liest sich wie ein anderes Problem, nicht wie eine Verteilungsregel (dieselbe Klasse Irrweg wie beim inotify-Fall der Actions-Runner). Vorschlag: **`ScheduleAnyway` als Vorgabe**, mit einem Kommentar, der sagt, was das kostet — Verteilung nach bestem Bemühen, keine Zusage. ## Teil 2: PodDisruptionBudget Hier gibt es einen Präzedenzfall im selben Repository, und der sollte übernommen werden: `keycloak.pdb.yaml` mit `keycloak.podDisruptionBudget.{enabled, minAvailable}`. Dessen Kommentar nennt auch den Grund, warum die Bedingung wichtiger ist als das Objekt: > `minAvailable: 1` with exactly ONE replica forbids every voluntary eviction, so `kubectl drain` blocks forever on a pod nothing is allowed to move. The message then names the PDB and not the replica count, which sends the reader to the wrong file. Für Stalwart kommt eine Besonderheit dazu: **Es gibt Knotengruppen.** Also ein PDB **je Gruppe** mit der Replica-Zahl *dieser* Gruppe, und die Bedingung „erst ab zwei" gilt entsprechend pro Gruppe — bei `full: 2` und `mx: 1` entsteht genau ein PDB. Unter `scaleDown: true` keines, aus demselben Grund wie bei Keycloak. `minAvailable` statt `maxUnavailable`, analog zu Keycloak: Die Zusage betrifft den Dienst, nicht die Störung. ## Warum das nicht im Konsumenten-Repository gelöst werden kann Das PDB allein ginge als eigenes Manifest-Bundle in `cluster-config`. Die **Verteilung** nicht: Sie steht im Pod-Template des StatefulSets, und das von außen zu patchen wäre genau der Drift, den `correctDrift: unset` bis zur nächsten Chart-Änderung festschreibt. Außerdem müsste der Selektor des PDB die Chart-Labels nachbauen und würde bei einer Umbenennung still falsch. ## Abnahme - `helm template` mit `nodes.full.replicas: 2` und `spreadAcrossNodes: true` rendert eine `topologySpreadConstraints` mit einem Selektor, der auf die Labels des eigenen Pod-Templates passt (am gerenderten Ergebnis abgleichen, nicht nach Augenmaß). - Bei `replicas: 1` wird **kein** PDB gerendert; bei `2` genau eines je Gruppe; bei `scaleDown: true` keines. - Die generische Durchreichung erlaubt zusätzlich ein `affinity` ohne den Schalter. - README und NOTES.txt sagen, welcher der Fälle eingetreten ist — bei Keycloak ist das schon so gelöst. ## Dringlichkeit **Mittel.** Es blockiert nichts, was heute läuft: Mit je einer Replica ist weder ein PDB noch eine Verteilungsregel sinnvoll. Es blockiert aber den nächsten Schritt — und zwar auf die unangenehme Art, weil eine zweite Replica ohne Verteilung *aussieht* wie Redundanz und keine ist.
jlab-bot added the inhouse-chart label 2026-10-07 11:13:05 +02:00
Author
Member

Ergänzung: Entscheidungen und vier Punkte aus der Durchsicht

Die gemessenen Aussagen des Issues sind nachgeprüft und stimmen alle: kein affinity, kein topologySpreadConstraints, kein nodeSelector, keine tolerations — nirgends in stalwart/; neun Templates, kein PDB; weder updateStrategy noch podManagementPolicy gesetzt. Und entscheidend für den Vorschlag: die Labels stalwart.jk-dev.de/release und stalwart.jk-dev.de/node-group stehen im Pod-Template (Zeilen 54–55), nicht nur in der StatefulSet-Metadata. Ein labelSelector trifft Pods, der Entwurf funktioniert also.

Entscheidung 1: whenUnsatisfiable: DoNotSchedule

Der Issue-Vorschlag war ScheduleAnyway. Der Betreuer hat die Anforderung als Zusage formuliert — full-Pods laufen auf verschiedenen Knoten, mx-Pods auf verschiedenen Knoten —, und ScheduleAnyway gibt keine Zusage: bei 97 % angeforderter CPU auf jk-worker-1 kann der Scheduler beide full-Replicas auf jk-worker-3 legen, regelkonform und wirkungslos.

Also DoNotSchedule. Auf dem Zielcluster ist das erfüllbar: drei Arbeitsknoten, je zwei Replicas pro Gruppe, maxSkew: 1 geht als 1+1 auf.

Der Preis gehört an den Wert und in NOTES.txt, und das Issue benennt ihn selbst richtig: lässt sich eine Replica nicht platzieren, bleibt sie dauerhaft Pending — und ein Pending-Pod ohne Scheduling-Ereignis liest sich wie ein anderes Problem, nicht wie eine Verteilungsregel. Konkret tritt das ein, wenn

  • zwei der drei Arbeitsknoten ausfallen (die zweite Replica läuft dann nicht beim Überlebenden mit), oder
  • jemand über die generische Durchreichung ein nodeSelector oder tolerations setzt, die weniger Knoten zulassen als die Gruppe Replicas hat.

Der zweite Fall ist der wahrscheinlichere und gehört ausdrücklich dazu: DoNotSchedule plus eine Knoteneinschränkung ist eine Kombination, die sich selbst blockiert.

Entscheidung 2: Verteilung nur innerhalb einer Gruppe

Bestätigt wie im Issue entworfen — labelSelector auf Release und Knotengruppe. Ein mx und ein full auf demselben Knoten ist ausdrücklich in Ordnung. Keine zusätzliche Bedingung, die nur auf release selektiert und damit alle Stalwart-Pods gegeneinander verteilt; die Frage war gestellt und ist mit nein beantwortet.

Punkt 3: Teil 1 hängt an einem offenen Messpunkt von #65

Der Grund, warum die Verteilung überhaupt wirken kann, ist dass die StatefulSet keine volumeClaimTemplates hat — ein Pod ist nicht durch ein topologie-gebundenes Volume an einen Knoten genagelt.

Offener Messpunkt 1 auf #65 lautet: muss unter /var/lib/stalwart etwas einen Neustart überleben? Wenn ja, braucht es doch eine PVC je Knotengruppe. Fällt diese Antwort auf ja, entsteht eine PVC — und mit einer knoten-lokalen oder topologie-gebundenen Storage-Klasse wird der Pod dorthin gebunden, wo sein Volume zuerst angelegt wurde. Die Verteilung entscheidet sich dann beim ersten Provisionieren, nicht mehr über die Regel, und DoNotSchedule kann einen wiederkehrenden Pod sogar dauerhaft Pending lassen, wenn sein Volume auf einem Knoten liegt, der die Skew-Bedingung verletzt.

Keine Blockade für dieses Issue, aber es gehört in die Chart-Dokumentation: diese Verteilungsregel setzt voraus, dass der Chart PVC-frei bleibt.

Punkt 4: die Selektor-Prüfung soll eine CI-Zusicherung werden

Das Abnahmekriterium sagt richtig „am gerenderten Ergebnis abgleichen, nicht nach Augenmaß". Der Fehlerfall kehrt aber zurück, sobald jemand ein Label umbenennt — und er kehrt still zurück: der Selektor trifft null Pods, die Bedingung ist trivial erfüllt, verteilt wird nichts, alles grün. Bei DoNotSchedule ist das noch unangenehmer, weil die Regel dann scheinbar eine Zusage trägt.

Vorbild im Repo: ci/run-quantity-render-test.sh ist eine Zusicherung auf Render-Ebene. Dieselbe Form hier — den labelSelector aus der gerenderten Spread-Bedingung und dem PDB extrahieren und gegen die Labels des gerenderten Pod-Templates halten — macht aus einer einmaligen Prüfung eine dauerhafte. Das halte ich für die wertvollste Ergänzung dieses Durchgangs.

Nebenbefund, außerhalb dieses Issues

Beim Nachprüfen fiel zweierlei auf, das die Objektwahl des Charts betrifft und hier nur festgehalten wird, weil es #96 nicht berührt — topologySpreadConstraints und PDB wirken auf StatefulSet und Deployment identisch:

  1. Der Headless-Service je Gruppe hat keinen Verbraucher. Seine einzige Referenz im ganzen Chart ist serviceName: an der StatefulSet; niemand löst <pod>.<gruppe>-headless auf, weil die Koordination über Valkey-Pub/Sub läuft und nicht über DNS. Er existiert für einen Zweck, den nichts in Anspruch nimmt.
  2. Der aufgeschriebene Grund für die StatefulSet trägt weniger weit als er klingt. Das Template sagt, nur eine StatefulSet gebe einem Pod einen stabilen Hostnamen — das ist wahr, belegt aber nur, dass sie Stabilität liefert, nicht dass Stalwart sie braucht. keycloak in diesem Repo ist derselbe Fall (zustandslos, externe Datenbank, geclusterter eingebetteter Cache, Knotenname je Pod) und holt ihn genauso aus fieldRef: metadata.name — in einem Deployment. Dort genügt, dass der Name eindeutig ist; stabil muss er nicht sein.

Ob Stalwarts Node-ID-Lease einen wechselnden Hostnamen verträgt, ist ungemessen. Leases laufen ab, und Rollen werden über STALWART_ROLE zugewiesen statt über die Knoten-Identität — eine stehengebliebene Lease verdoppelt also keine Aufgabe. Das klingt tragbar, ist aber nicht belegt, und ein Wechsel wäre ein Major-Bump. Eigenes Issue, falls es verfolgt werden soll.

# Ergänzung: Entscheidungen und vier Punkte aus der Durchsicht Die gemessenen Aussagen des Issues sind nachgeprüft und stimmen alle: kein `affinity`, kein `topologySpreadConstraints`, kein `nodeSelector`, keine `tolerations` — nirgends in `stalwart/`; neun Templates, kein PDB; weder `updateStrategy` noch `podManagementPolicy` gesetzt. Und entscheidend für den Vorschlag: die Labels `stalwart.jk-dev.de/release` und `stalwart.jk-dev.de/node-group` stehen **im Pod-Template** (Zeilen 54–55), nicht nur in der StatefulSet-Metadata. Ein `labelSelector` trifft Pods, der Entwurf funktioniert also. ## Entscheidung 1: `whenUnsatisfiable: DoNotSchedule` Der Issue-Vorschlag war `ScheduleAnyway`. Der Betreuer hat die Anforderung als **Zusage** formuliert — `full`-Pods laufen auf verschiedenen Knoten, `mx`-Pods auf verschiedenen Knoten —, und `ScheduleAnyway` gibt keine Zusage: bei 97 % angeforderter CPU auf `jk-worker-1` kann der Scheduler beide `full`-Replicas auf `jk-worker-3` legen, regelkonform und wirkungslos. **Also `DoNotSchedule`.** Auf dem Zielcluster ist das erfüllbar: drei Arbeitsknoten, je zwei Replicas pro Gruppe, `maxSkew: 1` geht als 1+1 auf. **Der Preis gehört an den Wert und in `NOTES.txt`**, und das Issue benennt ihn selbst richtig: lässt sich eine Replica nicht platzieren, bleibt sie **dauerhaft `Pending`** — und ein `Pending`-Pod ohne Scheduling-Ereignis liest sich wie ein anderes Problem, nicht wie eine Verteilungsregel. Konkret tritt das ein, wenn - zwei der drei Arbeitsknoten ausfallen (die zweite Replica läuft dann **nicht** beim Überlebenden mit), oder - jemand über die generische Durchreichung ein `nodeSelector` oder `tolerations` setzt, die weniger Knoten zulassen als die Gruppe Replicas hat. Der zweite Fall ist der wahrscheinlichere und gehört ausdrücklich dazu: `DoNotSchedule` plus eine Knoteneinschränkung ist eine Kombination, die sich selbst blockiert. ## Entscheidung 2: Verteilung nur innerhalb einer Gruppe Bestätigt wie im Issue entworfen — `labelSelector` auf **Release und Knotengruppe**. Ein `mx` und ein `full` auf demselben Knoten ist ausdrücklich in Ordnung. **Keine** zusätzliche Bedingung, die nur auf `release` selektiert und damit alle Stalwart-Pods gegeneinander verteilt; die Frage war gestellt und ist mit nein beantwortet. ## Punkt 3: Teil 1 hängt an einem offenen Messpunkt von #65 Der Grund, warum die Verteilung überhaupt wirken *kann*, ist dass die StatefulSet **keine `volumeClaimTemplates`** hat — ein Pod ist nicht durch ein topologie-gebundenes Volume an einen Knoten genagelt. Offener Messpunkt 1 auf #65 lautet: *muss unter `/var/lib/stalwart` etwas einen Neustart überleben? Wenn ja, braucht es doch eine PVC je Knotengruppe.* Fällt diese Antwort auf ja, entsteht eine PVC — und mit einer knoten-lokalen oder topologie-gebundenen Storage-Klasse wird der Pod dorthin gebunden, wo sein Volume zuerst angelegt wurde. Die Verteilung entscheidet sich dann beim ersten Provisionieren, nicht mehr über die Regel, und `DoNotSchedule` kann einen wiederkehrenden Pod sogar dauerhaft `Pending` lassen, wenn sein Volume auf einem Knoten liegt, der die Skew-Bedingung verletzt. **Keine Blockade für dieses Issue**, aber es gehört in die Chart-Dokumentation: diese Verteilungsregel setzt voraus, dass der Chart PVC-frei bleibt. ## Punkt 4: die Selektor-Prüfung soll eine CI-Zusicherung werden Das Abnahmekriterium sagt richtig „am gerenderten Ergebnis abgleichen, nicht nach Augenmaß". Der Fehlerfall kehrt aber zurück, sobald jemand ein Label umbenennt — und er kehrt **still** zurück: der Selektor trifft null Pods, die Bedingung ist trivial erfüllt, verteilt wird nichts, alles grün. Bei `DoNotSchedule` ist das noch unangenehmer, weil die Regel dann *scheinbar* eine Zusage trägt. Vorbild im Repo: `ci/run-quantity-render-test.sh` ist eine Zusicherung auf Render-Ebene. Dieselbe Form hier — den `labelSelector` aus der gerenderten Spread-Bedingung und dem PDB extrahieren und gegen die Labels des gerenderten Pod-Templates halten — macht aus einer einmaligen Prüfung eine dauerhafte. **Das halte ich für die wertvollste Ergänzung dieses Durchgangs.** ## Nebenbefund, außerhalb dieses Issues Beim Nachprüfen fiel zweierlei auf, das die **Objektwahl** des Charts betrifft und hier nur festgehalten wird, weil es #96 nicht berührt — `topologySpreadConstraints` und PDB wirken auf StatefulSet und Deployment identisch: 1. **Der Headless-Service je Gruppe hat keinen Verbraucher.** Seine einzige Referenz im ganzen Chart ist `serviceName:` an der StatefulSet; niemand löst `<pod>.<gruppe>-headless` auf, weil die Koordination über Valkey-Pub/Sub läuft und nicht über DNS. Er existiert für einen Zweck, den nichts in Anspruch nimmt. 2. **Der aufgeschriebene Grund für die StatefulSet trägt weniger weit als er klingt.** Das Template sagt, nur eine StatefulSet gebe einem Pod einen stabilen Hostnamen — das ist wahr, belegt aber nur, dass sie Stabilität *liefert*, nicht dass Stalwart sie *braucht*. `keycloak` in diesem Repo ist derselbe Fall (zustandslos, externe Datenbank, geclusterter eingebetteter Cache, Knotenname je Pod) und holt ihn genauso aus `fieldRef: metadata.name` — **in einem Deployment**. Dort genügt, dass der Name eindeutig ist; stabil muss er nicht sein. Ob Stalwarts Node-ID-**Lease** einen wechselnden Hostnamen verträgt, ist **ungemessen**. Leases laufen ab, und Rollen werden über `STALWART_ROLE` zugewiesen statt über die Knoten-Identität — eine stehengebliebene Lease verdoppelt also keine Aufgabe. Das klingt tragbar, ist aber nicht belegt, und ein Wechsel wäre ein Major-Bump. Eigenes Issue, falls es verfolgt werden soll.
Author
Member

Korrektur meiner Ergänzung, und die offenen Punkte entschieden

Der zweite Preis-Fall meiner Ergänzung war falsch, und zwar in die unangenehme Richtung. Nachgeprüft an der Kubernetes-API-Definition, beide Vorgaben wörtlich:

// nodeAffinityPolicy …
// - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations.
// If this value is nil, the behavior is equivalent to the Honor policy.

// nodeTaintsPolicy …
// - Ignore: node taints are ignored. All nodes are included.
// If this value is nil, the behavior is equivalent to the Ignore policy.

Daraus folgt die Asymmetrie, die meine Ergänzung nicht nannte:

Einschränkung Wirkung mit DoNotSchedule, maxSkew: 1, Vorgaben
nodeSelector lässt nur einen Knoten zu, Gruppe hat 2 Replicas eine zulässige Domäne, beide Pods darin → Skew 0 → beide laufen, ko-lokiert. Kein Pending, kein Ereignis, keine Zusage.
Untolerierter Taint auf zwei von drei Knoten die zwei zählen weiter als Domänen mit 0 Pods → Minimum 0 → zweiter Pod ergäbe Skew 2 → Pending

Mein Satz „jemand setzt nodeSelector oder tolerations … → dauerhaft Pending, der zweite Fall ist der wahrscheinlichere" stimmt also nur für tolerations. Für nodeSelector verspricht er ein Symptom, das nicht eintritt, während der Schaden unsichtbar bleibt — genau die stille Klasse, gegen die der ganze Entwurf gebaut ist. Und die CI-Zusicherung fängt es nicht: der Selektor ist dann richtig, die Knotenmenge ist falsch.

Entscheidung: nodeAffinityPolicy: Ignore, ausdrücklich gesetzt

Die Behebung ist keine der beiden zur Wahl gestellten Varianten — kein Render-Abbruch und nicht nur Dokumentation, sondern ein Feld an der chart-eigenen Bedingung.

Mit nodeAffinityPolicy: Ignore zählen alle Knoten in die Skew-Rechnung. Ein nodeSelector, der auf einen Knoten verengt, ergibt dann 2,0,0 → Skew 2 → Pending. Die stille Hälfte wird zur lauten.

Durchgerechnet, damit es keine zulässige Konfiguration verbietet (genau der Grund gegen einen Abbruch):

Lage Verteilung Skew Ergebnis
kein nodeSelector, 3 Knoten, 2 Replicas 1,1,0 1 zulässig
nodeSelector über 5 Knoten, 2 Replicas 1,1,0,0,0 1 zulässig
nodeSelector über 1 Knoten, 2 Replicas 2,0,0 2 Pending — gewollt
beide Replicas auf einem Knoten 2,0,0 2 abgelehnt — gewollt

Ein Render-Abbruch hätte Zeile 2 mitverboten. Das Feld trifft genau Zeile 3.

Entscheidung: nodeTaintsPolicy bleibt auf der Vorgabe Ignore

Konsequent zur DoNotSchedule-Wahl: die Zusage lässt sich nicht durch das Wegfallen von Domänen aufweichen. Zwei Folgen, die ausdrücklich dokumentiert gehören — in values.yaml am Schalter, in NOTES.txt und auf der Wiki-Seite:

  1. Eine Obergrenze. Bei drei Workern plus drei Control-Plane-Knoten zählen sechs Domänen. Mit maxSkew: 1 kann eine Gruppe damit nie mehr als drei Replicas planen — die vierte ergäbe 2,1,1,0,0,0, also Skew 2. Für den Zielzustand von zwei je Gruppe ohne Belang, aber es ist eine Grenze, die später überrascht.
  2. Verhalten im Ausfall. Fallen zwei Worker aus, bleibt die zweite Replica Pending statt beim Überlebenden mitzulaufen — solange die Node-Objekte noch existieren (NotReady zählt wegen Ignore als leere Domäne). Werden die Knoten gelöscht, verschwinden die Domänen und der Pod läuft mit. Das ist derselbe Mechanismus, ein Satz Unterschied, und beide Hälften gehören hin.

Entscheidung zu Befund 2: anhängen, bei Kollision abbrechen

Übernommen wie vorgeschlagen. Andere topologyKeys werden angehängt und wirklich mit UND verknüpft. Bei Kollision auf (topologyKey, whenUnsatisfiable) — also kubernetes.io/hostname mit leerem oder DoNotSchedule — Render-Abbruch, der nodes.<gruppe>.spreadAcrossNodes und nodes.<gruppe>.topologySpreadConstraints beide namentlich nennt.

Die Begründung trägt: bei create lehnt die API-Validierung zwei Einträge mit gleichem Schlüsselpaar ab, mit einer Meldung auf einen Listenindex statt auf einen Wert; und auf einem Apply-Pfad verschmelzen sie über patchMergeKey: topologyKey still zu einem, wobei ein Selektor wegfällt und das Ergebnis grün ist. Beides ist schlechter als ein benannter Abbruch.

Befund 3 übernommen

Der 97-%-Knoten gehört an den dritten Replica, nicht an den zweiten: bei je zwei auf drei Workern ist 1,1,0 → Skew 1 → zulässig, die knappe CPU erzeugt dort noch kein Pending. Erst ab drei Replicas je Gruppe muss jeder Knoten einen annehmen. Die Begründungskette im Issue-Anlass gehört entsprechend verschoben, sonst wird aus der Vorsorge eine falsche Warnung.

Die vorbereiteten Punkte, bestätigt

Vorgaben und Versionssprung: podDisruptionBudget.enabled: true und spreadAcrossNodes: true, beide hinter der berechneten Replica-Zahl >= 2 gesperrt, damit scaleDown mitgefangen ist. Eine einfache Installation (full: 1, mx: 0) rendert damit dieselben Objekte wie vorher → Minor, 1.1.0+upv0.16.24. Zu belegen mit einem helm template-Diff gegen origin/main, nicht mit Gefühl. Die Gegenrechnung gehört in values.yaml: ein Hochskalieren von 1 auf 2 ändert das Pod-Template, der bestehende Pod wird in derselben Rollout-Runde ersetzt.

Die CI-Zusicherung generisch über alle Charts, wie vorgeschlagen — Selektoren aus PDB und Spread-Bedingung gegen die Labels jedes gerenderten Pod-Templates desselben Renders. Dass die Runner kein pyyaml und kein yq haben, ist gemessen; Helm selbst als YAML-Parser zu benutzen statt einen eigenen zu schreiben ist die richtige Antwort, weil ein selbstgeschriebener Parser den Test still vakuum-grün machen könnte. podAffinity/podAntiAffinity bleiben draußen, Selektoren mit ausschließlich matchExpressions werden als „nicht geprüft" gemeldet statt als bestanden gezählt. Und das Overlay, damit die Zusicherung bei stalwart nicht vakuum-grün ist, gehört dazu.

Schlüsselwort: Fixes #96. Die Abnahmeliste dieses Issues fordert ausschließlich Render-Aussagen, und die sind belegbar. Ein Part of gegen Kriterien, die das Issue nicht enthält, würde es auf Dauer offen halten — das ist die Buchführung, die hier schon dreimal zu „Issue heißt neuer Chart und wartet auf eine Browser-Prüfung" geführt hat. Dafür gehört in den PR-Text ausdrücklich, was ungemessen bleibt: die tatsächliche Scheduling-Wirkung, das Verhalten des PDB unter kubectl drain, und die Obergrenze aus nodeTaintsPolicy. Der Install-Test fährt eine Replica je Gruppe und rendert PDB und Spread-Bedingung also nicht — das gehört ebenfalls hinein.

# Korrektur meiner Ergänzung, und die offenen Punkte entschieden Der zweite Preis-Fall meiner Ergänzung war **falsch**, und zwar in die unangenehme Richtung. Nachgeprüft an der Kubernetes-API-Definition, beide Vorgaben wörtlich: ```go // nodeAffinityPolicy … // - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. // If this value is nil, the behavior is equivalent to the Honor policy. // nodeTaintsPolicy … // - Ignore: node taints are ignored. All nodes are included. // If this value is nil, the behavior is equivalent to the Ignore policy. ``` Daraus folgt die Asymmetrie, die meine Ergänzung nicht nannte: | Einschränkung | Wirkung mit `DoNotSchedule`, `maxSkew: 1`, Vorgaben | | --- | --- | | `nodeSelector` lässt nur einen Knoten zu, Gruppe hat 2 Replicas | eine zulässige Domäne, beide Pods darin → Skew 0 → **beide laufen, ko-lokiert. Kein `Pending`, kein Ereignis, keine Zusage.** | | Untolerierter Taint auf zwei von drei Knoten | die zwei zählen weiter als Domänen mit 0 Pods → Minimum 0 → zweiter Pod ergäbe Skew 2 → **`Pending`** | Mein Satz „jemand setzt `nodeSelector` oder `tolerations` … → dauerhaft `Pending`, der zweite Fall ist der wahrscheinlichere" stimmt also nur für `tolerations`. Für `nodeSelector` verspricht er ein Symptom, das nicht eintritt, während der Schaden unsichtbar bleibt — genau die stille Klasse, gegen die der ganze Entwurf gebaut ist. Und die CI-Zusicherung fängt es nicht: der Selektor ist dann richtig, die Knotenmenge ist falsch. ## Entscheidung: `nodeAffinityPolicy: Ignore`, ausdrücklich gesetzt Die Behebung ist **keine** der beiden zur Wahl gestellten Varianten — kein Render-Abbruch und nicht nur Dokumentation, sondern **ein Feld** an der chart-eigenen Bedingung. Mit `nodeAffinityPolicy: Ignore` zählen alle Knoten in die Skew-Rechnung. Ein `nodeSelector`, der auf einen Knoten verengt, ergibt dann `2,0,0` → Skew 2 → **`Pending`**. Die stille Hälfte wird zur lauten. Durchgerechnet, damit es keine zulässige Konfiguration verbietet (genau der Grund gegen einen Abbruch): | Lage | Verteilung | Skew | Ergebnis | | --- | --- | --- | --- | | kein `nodeSelector`, 3 Knoten, 2 Replicas | 1,1,0 | 1 | zulässig | | `nodeSelector` über 5 Knoten, 2 Replicas | 1,1,0,0,0 | 1 | zulässig | | `nodeSelector` über **1** Knoten, 2 Replicas | 2,0,0 | 2 | **`Pending`** — gewollt | | beide Replicas auf einem Knoten | 2,0,0 | 2 | **abgelehnt** — gewollt | Ein Render-Abbruch hätte Zeile 2 mitverboten. Das Feld trifft genau Zeile 3. ## Entscheidung: `nodeTaintsPolicy` bleibt auf der Vorgabe `Ignore` Konsequent zur `DoNotSchedule`-Wahl: die Zusage lässt sich nicht durch das Wegfallen von Domänen aufweichen. **Zwei Folgen, die ausdrücklich dokumentiert gehören** — in `values.yaml` am Schalter, in `NOTES.txt` und auf der Wiki-Seite: 1. **Eine Obergrenze.** Bei drei Workern plus drei Control-Plane-Knoten zählen sechs Domänen. Mit `maxSkew: 1` kann eine Gruppe damit nie mehr als **drei** Replicas planen — die vierte ergäbe `2,1,1,0,0,0`, also Skew 2. Für den Zielzustand von zwei je Gruppe ohne Belang, aber es ist eine Grenze, die später überrascht. 2. **Verhalten im Ausfall.** Fallen zwei Worker aus, bleibt die zweite Replica `Pending` statt beim Überlebenden mitzulaufen — **solange die `Node`-Objekte noch existieren** (`NotReady` zählt wegen `Ignore` als leere Domäne). Werden die Knoten *gelöscht*, verschwinden die Domänen und der Pod läuft mit. Das ist derselbe Mechanismus, ein Satz Unterschied, und beide Hälften gehören hin. ## Entscheidung zu Befund 2: anhängen, bei Kollision abbrechen Übernommen wie vorgeschlagen. Andere `topologyKey`s werden angehängt und wirklich mit UND verknüpft. Bei Kollision auf `(topologyKey, whenUnsatisfiable)` — also `kubernetes.io/hostname` mit leerem oder `DoNotSchedule` — **Render-Abbruch, der `nodes.<gruppe>.spreadAcrossNodes` und `nodes.<gruppe>.topologySpreadConstraints` beide namentlich nennt.** Die Begründung trägt: bei `create` lehnt die API-Validierung zwei Einträge mit gleichem Schlüsselpaar ab, mit einer Meldung auf einen Listenindex statt auf einen Wert; und auf einem Apply-Pfad verschmelzen sie über `patchMergeKey: topologyKey` **still** zu einem, wobei ein Selektor wegfällt und das Ergebnis grün ist. Beides ist schlechter als ein benannter Abbruch. ## Befund 3 übernommen Der 97-%-Knoten gehört an den **dritten** Replica, nicht an den zweiten: bei je zwei auf drei Workern ist `1,1,0` → Skew 1 → zulässig, die knappe CPU erzeugt dort noch kein `Pending`. Erst ab drei Replicas je Gruppe muss jeder Knoten einen annehmen. Die Begründungskette im Issue-Anlass gehört entsprechend verschoben, sonst wird aus der Vorsorge eine falsche Warnung. ## Die vorbereiteten Punkte, bestätigt **Vorgaben und Versionssprung:** `podDisruptionBudget.enabled: true` und `spreadAcrossNodes: true`, beide hinter der **berechneten** Replica-Zahl `>= 2` gesperrt, damit `scaleDown` mitgefangen ist. Eine einfache Installation (`full: 1`, `mx: 0`) rendert damit dieselben Objekte wie vorher → **Minor**, `1.1.0+upv0.16.24`. Zu belegen mit einem `helm template`-Diff gegen `origin/main`, nicht mit Gefühl. Die Gegenrechnung gehört in `values.yaml`: ein Hochskalieren von 1 auf 2 ändert das Pod-Template, der bestehende Pod wird in derselben Rollout-Runde ersetzt. **Die CI-Zusicherung generisch über alle Charts**, wie vorgeschlagen — Selektoren aus PDB und Spread-Bedingung gegen die Labels jedes gerenderten Pod-Templates desselben Renders. Dass die Runner kein `pyyaml` und kein `yq` haben, ist gemessen; Helm selbst als YAML-Parser zu benutzen statt einen eigenen zu schreiben ist die richtige Antwort, weil ein selbstgeschriebener Parser den Test still vakuum-grün machen könnte. `podAffinity`/`podAntiAffinity` bleiben draußen, Selektoren mit ausschließlich `matchExpressions` werden als „nicht geprüft" **gemeldet** statt als bestanden gezählt. Und das Overlay, damit die Zusicherung bei `stalwart` nicht vakuum-grün ist, gehört dazu. **Schlüsselwort: `Fixes #96`.** Die Abnahmeliste dieses Issues fordert ausschließlich Render-Aussagen, und die sind belegbar. Ein `Part of` gegen Kriterien, die das Issue nicht enthält, würde es auf Dauer offen halten — das ist die Buchführung, die hier schon dreimal zu „Issue heißt neuer Chart und wartet auf eine Browser-Prüfung" geführt hat. **Dafür gehört in den PR-Text ausdrücklich, was ungemessen bleibt:** die tatsächliche Scheduling-Wirkung, das Verhalten des PDB unter `kubectl drain`, und die Obergrenze aus `nodeTaintsPolicy`. Der Install-Test fährt eine Replica je Gruppe und rendert PDB und Spread-Bedingung also nicht — das gehört ebenfalls hinein.
Author
Member

Drei Präzisierungen zur Entscheidung, eine davon neu

Die Rechnung aus dem Kommentar davor ist unabhängig nachgerechnet und bestätigt — alle vier Fälle, und die Obergrenze. Drei Dinge gehören davon präzisiert in die Dokumentation, und der erste Punkt ist neu.

1. nodeAffinityPolicy kann still verworfen werden — gehört an den Wert

Das Feld ist seit Kubernetes 1.26 Beta und erst mit dem Gate NodeInclusionPolicyInPodTopologySpread GA geworden. Auf einem älteren API-Server wird es stillschweigend verworfen — und dann kehrt genau die stille nodeSelector-Hälfte zurück, gegen die es gesetzt wurde: eine Verengung auf einen Knoten ergibt wieder Skew 0 und damit Ko-Lokation, ohne Meldung.

Das ist dieselbe Gattung wie der Fehler in meiner ersten Ergänzung, nur eine Ebene tiefer: der Schutz ist dann weg, ohne dass etwas davon berichtet. Der Satz gehört an den Wert, nicht als bekannt vorausgesetzt — mit der Mindestversion, gegen die der Chart die Zusage überhaupt geben kann.

2. Die Obergrenze ist keine Zahl, sondern eine Regel

Mein Kommentar sprach von „nie mehr als drei Replicas". Das ist der heutige Wert, nicht der Mechanismus. Richtig ist: so viele Replicas je Gruppe, wie es planbare Worker gibt. Die Control-Plane-Domänen halten das Minimum dauerhaft bei 0, und daraus folgt die Grenze — kommt ein Worker hinzu, steigt sie mit; wird einer entfernt, fällt sie.

So gehört es dokumentiert, sonst steht in values.yaml eine Zahl, die beim nächsten Cluster-Umbau falsch ist und niemand nachzieht.

3. Die beiden Felder sind kein Paket

Die Obergrenze entsteht aus nodeTaintsPolicy ≙ Ignore und bestand damit schon vor diesem Entwurf — nodeAffinityPolicy: Ignore fügt sie nicht hinzu, es schließt nur die stille nodeSelector-Lücke.

In values.yaml sollten die beiden deshalb getrennt begründet stehen und nicht als ein Block erscheinen, sonst liest der nächste die Obergrenze als Preis einer Entscheidung, die sie nicht verursacht hat.

Zum Verfahren

Der vorige Durchgang hat nichts implementiert: der isolierte Worktree des Agenten wurde ihm zum fünften Mal mitten in der Arbeit entzogen, und zwar wieder beim Fortsetzen, nicht beim Start. Er hat angehalten statt im Worktree der Hauptsitzung zu arbeiten — richtig, und die Auflage stand so in seinem Auftrag. Kein Branch, kein Commit, keine PR, kein Wiki-Edit; im fremden Worktree ausschließlich Lesevorgänge.

Ein Hinweis, der dabei auffiel und jede künftige Sitzung betrifft: in der gemeinsamen Arbeitskopie steht main auf afa1efb, origin/main dagegen auf 6e196c2. Der lokale main-Ref ist also nicht origin/main — genau die Blindstelle, die CLAUDE.md beschreibt. Jeder Worktree ist von origin/main zu schneiden.

# Drei Präzisierungen zur Entscheidung, eine davon neu Die Rechnung aus dem Kommentar davor ist unabhängig nachgerechnet und bestätigt — alle vier Fälle, und die Obergrenze. Drei Dinge gehören davon präzisiert in die Dokumentation, und der erste Punkt ist neu. ## 1. `nodeAffinityPolicy` kann still verworfen werden — gehört an den Wert Das Feld ist seit Kubernetes **1.26 Beta** und erst mit dem Gate `NodeInclusionPolicyInPodTopologySpread` GA geworden. Auf einem **älteren API-Server wird es stillschweigend verworfen** — und dann kehrt genau die stille `nodeSelector`-Hälfte zurück, gegen die es gesetzt wurde: eine Verengung auf einen Knoten ergibt wieder Skew 0 und damit Ko-Lokation, ohne Meldung. Das ist dieselbe Gattung wie der Fehler in meiner ersten Ergänzung, nur eine Ebene tiefer: der Schutz ist dann weg, ohne dass etwas davon berichtet. **Der Satz gehört an den Wert**, nicht als bekannt vorausgesetzt — mit der Mindestversion, gegen die der Chart die Zusage überhaupt geben kann. ## 2. Die Obergrenze ist keine Zahl, sondern eine Regel Mein Kommentar sprach von „nie mehr als **drei** Replicas". Das ist der heutige Wert, nicht der Mechanismus. Richtig ist: **so viele Replicas je Gruppe, wie es planbare Worker gibt.** Die Control-Plane-Domänen halten das Minimum dauerhaft bei 0, und daraus folgt die Grenze — kommt ein Worker hinzu, steigt sie mit; wird einer entfernt, fällt sie. So gehört es dokumentiert, sonst steht in `values.yaml` eine Zahl, die beim nächsten Cluster-Umbau falsch ist und niemand nachzieht. ## 3. Die beiden Felder sind kein Paket Die Obergrenze entsteht aus **`nodeTaintsPolicy ≙ Ignore`** und bestand damit schon vor diesem Entwurf — `nodeAffinityPolicy: Ignore` fügt sie **nicht** hinzu, es schließt nur die stille `nodeSelector`-Lücke. In `values.yaml` sollten die beiden deshalb getrennt begründet stehen und nicht als ein Block erscheinen, sonst liest der nächste die Obergrenze als Preis einer Entscheidung, die sie nicht verursacht hat. ## Zum Verfahren Der vorige Durchgang hat nichts implementiert: der isolierte Worktree des Agenten wurde ihm **zum fünften Mal** mitten in der Arbeit entzogen, und zwar wieder beim Fortsetzen, nicht beim Start. Er hat angehalten statt im Worktree der Hauptsitzung zu arbeiten — richtig, und die Auflage stand so in seinem Auftrag. Kein Branch, kein Commit, keine PR, kein Wiki-Edit; im fremden Worktree ausschließlich Lesevorgänge. Ein Hinweis, der dabei auffiel und jede künftige Sitzung betrifft: in der gemeinsamen Arbeitskopie steht `main` auf `afa1efb`, `origin/main` dagegen auf `6e196c2`. Der lokale `main`-Ref ist also nicht `origin/main` — genau die Blindstelle, die CLAUDE.md beschreibt. Jeder Worktree ist von `origin/main` zu schneiden.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jlab-cluster/helm-charts#96