Skip to content

[Backport release-1.6] fix(etcd): require an even spread of members over nodes by default - #4988

Open
europrinter (yankawai) wants to merge 1 commit into
cozystack:release-1.6from
yankawai:backport/etcd-strict-member-spread-1.6
Open

europrinter (yankawai) wants to merge 1 commit into
cozystack:release-1.6from
yankawai:backport/etcd-strict-member-spread-1.6

Conversation

@yankawai

@yankawai europrinter (yankawai) commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Backport of #4986 to release-1.6. The fix commit is cherry-picked from f1f30b3 with its cherry-pick reference and applies without conflicts. On this branch etcd-cluster.yaml has no affinity block from #3962, and the spread block and tests/topology-spread_test.yaml are the same as on main before the fix. The docs commit of #4986 rewrites the affinity description and the comment above that block, so it has nothing to change here.

The default spread of tenant etcd members becomes DoNotSchedule with maxSkew: 1 on kubernetes.io/hostname and nodeTaintsPolicy: Ignore, and the globalAppTopologySpreadConstraints branch stays as it is. The cluster where two members of a new tenant etcd started on one node runs v1.6.4, which has the same template and the same etcd-operator v0.5.5 as v1.6.5, see #4986. In v0.5.5, and in v0.5.6 that release-1.6 ships now, the operator copies the constraints into an EtcdMember only when it creates one and builds the Pod from that copy, so on upgrade no member is recreated and only members created later get the new constraint. The two cases the release note declares and the way out of each are confirmed against that source in #4986.

$ docker run --rm -v "$PWD":/apps -w /apps/packages/extra/etcd helmunittest/helm-unittest:4.2.3-1.1.2 .
Charts:      1 passed, 1 total
Test Suites: 8 passed, 8 total
Tests:       24 passed, 24 total

On release-1.6 the same command gives 23 tests in 8 suites, and against its template the updated suite fails only the default case, 1 failed and 23 passed. With default values helm template differs from release-1.6 only in the two lines, and with a global policy set it renders the same. The constraint renders the same as with #4986 on main.

Screenshots

Not a UI change.

Downstream repositories

Same diff as #4986, so no trigger map entry applies.

Release note

fix(etcd): tenant etcd members now spread over nodes with whenUnsatisfiable DoNotSchedule by default, so two members of one etcd no longer start on the same node while another node has none. Members that exist are not moved, only members created after the upgrade get the new constraint.

An etcd created while the cluster had fewer nodes than members puts two or three members on one node. Once a node is added, such a member whose Pod is recreated (drain, eviction, graceful node shutdown) stays Pending, because maxSkew rejects its own node and its node-local volume rules out the others. With two nodes grown to three the etcd runs two of three members and its PodDisruptionBudget blocks draining the other nodes. With one node grown to three a reboot of that node brings back one member of three and quorum is lost. A stranded member is replaced by deleting its EtcdMember while the other members keep quorum. Its Pod and PVC go with it, and the replacement gets a new Pod and PVC on the node without a member. Right after adding nodes, doing this for one co-located member at a time while all members are Ready avoids the case.

With nodeTaintsPolicy Ignore a node the etcd Pods cannot tolerate, such as a tainted control plane, counts as a node with zero members, so no node can take a second member. On a cluster with such a node and fewer schedulable nodes than members, a new etcd starts only as many members as there are schedulable nodes, one of three on a single schedulable node. An existing etcd there keeps its members, but a member it replaces stays Pending the same way.

Either case is avoided by setting scheduling.globalAppTopologySpreadConstraints in spec.components.platform.values of the cozystack.cozystack-platform Package to a topologySpreadConstraints entry with maxSkew 1, topologyKey kubernetes.io/hostname and whenUnsatisfiable ScheduleAnyway before the upgrade. Every app chart that reads this value applies it, two members of a new etcd can share a node again, and the operator gives it only to members created after it adopts the changed EtcdCluster spec into status.observed. Once status.observed.topologySpreadConstraints of an EtcdCluster shows ScheduleAnyway, a member of it that stays Pending is replaced by deleting its EtcdMember.

The default spread of the etcd members was ScheduleAnyway, which only
adds to the score of a node, and other scores can outweigh it. A new
etcd on a three-node cluster got its first two members on one node.
On a node-local StorageClass a member cannot leave its node, so losing
that node takes two of the three members and the quorum.

DoNotSchedule with maxSkew 1 still lets an etcd with more members than
nodes start, because the skew is counted between the nodes that exist.
One node takes all three members and two nodes take two and one.
nodeTaintsPolicy is Ignore, so a cordoned node still counts and a
member waits for it instead of landing next to another member.

The operator copies the constraints into an EtcdMember when it creates
it and builds the Pod from that copy, so members that exist keep the
old setting and only members created later get the new one.

Assisted-by: LLM
Signed-off-by: Yan Bondarenko <[email protected]>
(cherry picked from commit f1f30b3)
@github-actions github-actions Bot added area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) kind/bug Categorizes issue or PR as related to a bug labels Oct 20, 2026
@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: cozystack/cozystack/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3cbb9129-2c84-4694-8480-51075e19b4c8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/M This PR changes 30-99 lines, ignoring generated files label Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/database Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse) area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) kind/bug Categorizes issue or PR as related to a bug size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant