Repository navigation
CKS: change the node settings of a cluster and restart its nodes with them, a few at a time - #14616
Draft
bhouse-nexthop wants to merge 3 commits into
Draft
bhouse-nexthop wants to merge 3 commits into
bhouse-nexthop wants to merge 3 commits into
Conversation
…tates A Kubernetes cluster's state was written by id alone, so two operations started on the same cluster at once both got past their first transition. Each then ran its steps against VMs the other was changing, and whichever finished last decided the final state. The status scanner could also overwrite the state of an operation that started after it read the cluster. The state is now compared and set in one update, as the VM and volume states are. Each operation moves the cluster into its own state before it changes anything, and fails without touching the cluster when another operation got there first. From then on it only moves the cluster while the cluster is in a state the operation moved it to. Starting a cluster in Alert now moves it to Starting like any other start. A start from Error, a stop from Created or Error, and a retried destroy still go on without a state change, as they did before; the start and stop then leave the state alone when they end. From any other state with no transition for it, an operation is now refused instead of running alongside the one that holds the cluster. The scanner only completes a stop or a recovery that it started itself. A management server that stopped during an operation left the cluster in that operation's state. Nothing moved it on, and every later operation on it was refused until the database was edited by hand. The scanner now fails the operation of a cluster in an operation's state when no job is in progress for it on any management server. The existing Alert reconciliation then returns a healthy cluster to Running. To find those jobs, the start, stop, scale and upgrade jobs now record their cluster, as the create, delete and node jobs already did. A cluster named by a job in progress on a management server of an older version, which does not record it, is left alone. This replaces the scanner's first-run handling of Starting clusters, which tried a transition that does not exist. No schema or API change. Signed-off-by: Brad House <bhouse@nexthop.ai> (cherry picked from commit fee9834)
…resumable Scale-down and upgrade both drain a node before touching it, each with its own copy of the drain loop, and neither works well: - Scale-down gives up at once when kubectl drain exits non-zero (a PodDisruptionBudget that does not allow an eviction yet, say), and when it does retry after an exception it counts the attempt twice. The node is left cordoned, and the cluster goes to Alert although nothing has been removed and every node still works. - Upgrade retries straight away, with no wait for the eviction to become possible, and on the last failure leaves the node cordoned. - In both, kubectl drain has no --timeout, so it is cut off by a fixed 60s SSH timeout rather than a deadline of its own. Both workers now use one drain helper in KubernetesClusterActionWorker: one count per attempt, a retry (after a wait) on a non-zero exit or an exception, kubectl drain --timeout from the new setting cloud.kubernetes.cluster.node.drain.timeout (300s), cut down so that no attempt runs past the operation's deadline, with the SSH wait derived from it, and the end of the drain output in the error. The caller uncordons the node, and only if it cordoned it. Scale-down cordons every node it is going to remove (leaving alone the ones that were cordoned already), drains them, and if one cannot be drained uncordons them all and puts the cluster back in the state it was in, with an error that names the node, instead of Alert; the same happens if the scale times out before every node is drained. Once they are drained, the removal is not stopped part way by the timeout any more. If removing a node fails, the nodes still in the cluster are uncordoned. The upgrade had two more problems: - It only waited for the first control node to be Ready, so the next node could be drained while the previous one was still coming back. It now waits for every node, after it has been uncordoned. - It reached node i on sshPort + i. The port-forwarding rules are not created in the order of the upgrade's VM list (manually upgraded nodes are left out of that list, and nodes added later get the next free port), so the script could run on the wrong node. It now looks up the node's own SSH port-forwarding rule, as the remove worker already does. A failed upgrade left the cluster in Alert with no way to finish it, as upgradeKubernetesCluster only accepts a Running cluster. An upgrade now records the version it is upgrading to as a cluster detail (an existing key/value table) and removes it once the cluster is Running or Stopped again. upgradeKubernetesCluster accepts a cluster in Alert (Alert -> Upgrading) when that detail is the requested version (or another one with the same semantic version), which is a real upgrade of the cluster's own version, and all of the cluster's VMs are running, and refuses any other Alert cluster with a clear message. For an upgrade that failed before this change, a cluster still at the old version with nodes recorded at the target version (kubernetesnodeversion, which is set as each node is done) is accepted too. Nodes that are already at the target version, both as recorded and as reported by kubectl get node, and that are Ready, are skipped. The first control node is still upgraded first; if it is already done, kubeadm upgrade apply is not run again. The node version check now compares versions exactly, so 1.28.1 no longer matches 1.28.10, line by line, as the SSH output can hold stderr too, and ignoring semver build metadata. Failure messages name the node the upgrade stopped on. The upgrade stops if it can't move the cluster to Upgrading, and the Alert state scanner no longer marks a cluster Running when it couldn't move it to Recovering, and a failed upgrade detaches its ISO before going to Alert, so a retry and the scanner or the failing job don't undo each other's work. No schema change. Signed-off-by: Brad House <bhouse@nexthop.ai> (cherry picked from commit 0dcaa8f)
…a time A CloudManaged Kubernetes cluster could get a new service offering only through a scale, which changes every node at once with no drain, its affinity groups only while stopped, and its root disk size not at all. updateKubernetesCluster sets, for each node type (control, worker, etcd), the service offering, the root disk size and the affinity groups. The nodes created from then on get them. A cluster created before its node types had settings of their own first gets the service offering of the cluster copied to them; templates are left as they are. With rollout=true, the nodes that differ from the settings of their type are restarted with them: the external etcd nodes, then the control nodes, one at a time, then the worker nodes, maxunavailable at a time (1 by default, and always leaving a worker running). For each batch: - every other node must be running and Ready, and a control or etcd node is only taken down if the others keep a quorum - each Kubernetes node is annotated with its boot ID, unless someone else cordoned it, then cordoned and drained - its VM is stopped, given the offering, a larger root disk and the affinity groups, and started, which places it by its new groups; cloud-init grows the root partition and file system at boot - once it is Ready with another boot ID, and etcd is healthy, it is uncordoned and the annotation removed A control or etcd node type with too few nodes to lose one (a single control node, a 2-node stacked control plane, 2 etcd nodes) can not be rolled: a rollout=true that would make one of its nodes differ from the settings is refused, and its nodes that differ anyway are left out and reported in lasterror, while the other types are still rolled. Nodes are restarted rather than changed live, which CloudStack can not do reliably for a KVM VM, and which would leave the guest and kubelet unaware of the new resources. Nothing else is recorded: the nodes that still differ, are stopped or carry the annotation are the work left, and go first when the rollout is run again, which is how one that failed is resumed. However a rollout ends, it uncordons the nodes that are back, leaving the others cordoned, counts the CPU cores and memory of the cluster from its VMs, and moves the cluster from the new Updating state to Running, or to Alert if it is not healthy. Why it stopped is shown as lasterror. cancelrollout=true stops a rollout between two batches. Other operations are refused while a cluster is Updating, and the state scanner moves a cluster left Updating by a management server that stopped to Alert. The response also shows the number of nodes of each type that differ from the settings of their type. Signed-off-by: Brad House <bhouse@nexthop.ai>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #14616 +/- ##
============================================
+ Coverage 20.78% 20.91% +0.12%
- Complexity 21931 22269 +338
============================================
Files 6496 6498 +2
Lines 591657 592878 +1221
Branches 72592 72837 +245
============================================
+ Hits 122966 123981 +1015
- Misses 455346 455438 +92
- Partials 13345 13459 +114
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Lets
updateKubernetesClusterchange the service offering, root disk size and affinity groups of each node type of a CKS cluster. Withrollout=true, it then restarts the existing nodes that differ with the new settings, a few at a time, cordoning and draining each one first.Fixes #14615
Depends on #14609 and #14611. Their commits are the first two on this branch, and they drop out once 4.22 is merged forward into main. Please review only the last commit, "CKS: restart the nodes of a cluster with new node settings, a few at a time". It uses #14609's compare-and-set state changes (
transitToOperationState) and #14611's drain helper (drainKubernetesClusterNode).This targets main only, because it adds three columns.
Nodes are restarted, not changed live
Each node is restarted. This PR doesn't scale VMs live, migrate them live, or resize disks online:
A stop and start applies all three changes the same way on every hypervisor, and the node is drained first, so the pods move before it goes down.
API
updateKubernetesClustergets these parameters:nodeofferings,noderootdisksizes(GB) andnodeaffinitygroups, set per node type (control,worker,etcd).rollout(default false).maxunavailable: the number of workers restarted at a time, default 1, only withrollout=true.cancelrollout.With
rollout=false, the settings are only stored, and nodes created later get them. This works in any state except while another operation is running on the cluster or it is being destroyed.A root disk size is refused with
rollout=trueif it would shrink an existing node's disk, or if the disk's offering has a strictly fixed size. Withrollout=falseit is accepted, ascreateKubernetesClusteraccepts it.The response adds:
controlnodesoutofdate,workernodesoutofdateandetcdnodesoutofdate, the number of nodes of each type that differ from its settings;lasterror: why the last rollout stopped, or which control or etcd nodes it could not restart.Rollout
rollout=truemoves the cluster from Running or Alert to a new transient state,Updating, then stores the settings. It restarts the out-of-date nodes in this order:maxunavailableat a time. This is clamped tomin(maxunavailable, workers - 1), and is always at least 1.A node is out of date when its service offering differs, its root disk is smaller than the type's size, or its affinity groups differ from those recorded for the type. Explicit dedication groups, which CKS adds by itself, are not compared and are kept. A type with no recorded groups leaves the nodes' groups as they are.
nodeaffinitygroupsrefuses explicit dedication groups. Stopped nodes, and nodes still carrying the annotation from a rollout that stopped, go first.Each batch goes through these steps:
rollout=truethat would change such a type's settings is refused at the API. If such a type is out of date for another reason, its nodes are skipped and reported inlasterror, and the other types are still rolled.cloudstack.apache.org/rollout-boot-id=<bootID>, then cordoned. A node someone else has already cordoned is left without the annotation, and stays cordoned. Each node is then drained with CKS: fix node drain on scale-down and upgrade, make a failed upgrade resumable #14611's helper.upgradeVirtualMachinesets the offering,resizeVolumegrows the root disk (offline, grow only), and the affinity groups are updated. The VM is then started, and the deployment planner places it by its new groups.cloud.kubernetes.cluster.update.node.timeoutseconds (default 1800).cancelrollout=truestops the rollout between two batches.However a rollout ends, a single idempotent
release()runs. It:lasterror.Nothing else is stored. The nodes that still differ, are stopped or still carry the annotation are the work left to do. A rollout that stopped is resumed by calling
rollout=trueagain, which also uncordons any node that came back after the last run.While a cluster is
Updating, the other mutating APIs are refused. If the management server running a rollout stops, the state scanner moves the cluster fromUpdatingto Alert and recounts its capacity. It doesn't reach into the nodes. The next rollout picks them up.Data model
The schema change adds
control_node_root_disk_size,worker_node_root_disk_sizeandetcd_node_root_disk_sizetokubernetes_cluster, idempotently, inschema-42300to2400.sql. Nodes created later use these sizes. Cancel requests and last errors are cluster details (rollout.cancel,rollout.lasterror). There are no new tables.Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
How Has This Been Tested?
Unit tests only. Nothing here has been run against a live cluster yet.
mvn -o test -pl plugins/integrations/kubernetes-servicepasses (233 tests).engine/schemacompiles. The new tests are:KubernetesClusterRollingUpdateWorkerTestruns the worker against a simulated cluster (VMs, kubectl output, boot IDs). It covers:maxunavailableclamp;release()after success, a drain failure, a start failure, a cancel and an early exception, each run twice;KubernetesClusterManagerImplUpdateTestcovers:rollout=false;maxunavailable, disk sizes, shrink and strict offerings, node types, dedication groups, control settings that can't be rolled);Updatingbefore the settings are stored, and going back to the previous state, with the last error kept, if storing fails or the settings are refused;Updatingguards on the other APIs.UpdateKubernetesClusterCmdTestcovers parsing of the per-type maps.Not tested yet, and the main risks in a live test:
etcdctl endpoint healthon an external etcd node, andcurl http://127.0.0.1:2381/health, the metrics address kubeadm sets up, on a stacked control node. The cluster-wide check iskubectl get --raw=/readyz/etcd. All of these were checked against the CKS scripts, not against a running cluster.admin.confpoints at the first control node.