The default for this post's gate went on at 1.35.0, off at 1.35.4 and on again at 1.37, so whether the field the post tells you to enable works at all is a question about your patch number, and the generated half of the reference still reports it off, along with thirty-five other gates¶
Post — Kubernetes 1.24: Maximum Unavailable Replicas for StatefulSet, 2022-05-27.
149 lines, 8,883 bytes, one author from one vendor — Mayank Kumar, Salesforce — against a 2022 median of 8,081 bytes, so an ordinary post by every measure the manifest records. It is the fifth of the year's thirteen walked posts and the fifth 1.24 announcement in a row: every walked post of 2022 so far has been about a gate or a graduation in the same release.
As written
The argument is short and good. A StatefulSet with podManagementPolicy: OrderedReady updates one
Pod at a time, and the post gives two cases where that is too slow: a cache-backed application whose
containers need a long time to warm, and a leader-plus-followers application whose followers can all
go down at once. Both want a batch size larger than one, and neither wants to give up per-Pod
identity, which is the only reason they are StatefulSets at all.
Then the instruction, at :31-33:
In order to support such scenarios, Kubernetes 1.24 includes a new alpha feature to help. Before you can use the new feature you must enable the
MaxUnavailableStatefulSetfeature flag. Once you enable that, you can specify a new field calledmaxUnavailable, part of thespecfor a StatefulSet.
The manifest that follows sets replicas: 5, maxUnavailable: 2, partition: 0, and carries an
inline comment on the pod management policy — podManagementPolicy: OrderedReady # you must set
OrderedReady (:42). The demonstration changes the image and prints forty lines of kubectl get
pods --watch, annotated in the right margin: 4 and 3 terminate together, then 2 and 1 only after
both 4 and 3 are running, then 0 alone. The narration at :117-127 states the guarantee precisely —
ordering holds between batches and not inside one, so replica 3 may become ready before replica 4,
and an application that cannot survive that must not set the field above 1.
The post closes by admitting two things it has not covered (:137-139):
So, now you may have a lot of questions about:- - What is the behavior when you set
podManagementPolicy: Parallel? - What is the behavior whenpartitionto a value other than0?
It has in fact half-answered the first, four lines earlier at :133-135: under Parallel the
terminations and the creations both happen maxUnavailable at a time, "This is called bursting."
Then it asks for bug reports, and links the documentation section, the KEP, the implementation pull
request and the tracking issue.
As it runs now
The feature exists, the field is beta, and the one sentence in it you cannot check without looking
at your own cluster is "you must enable the MaxUnavailableStatefulSet feature flag". Its truth
has changed four times, and one of those changes landed inside a release rather than between two:
alpha and off from 1.24 through 1.34, beta and on from 1.35.0, beta and off again from 1.35.4
through 1.36, beta and on once more at 1.37. Two of those four boundaries are patch numbers. This
repo's lab clusters run v1.35, which means the post's instruction is correct on some of them and
wrong on others, decided by a number that most people never read off kubectl version.
The documentation the post links still lands. ### Maximum unavailable Pods sits at
statefulset.md:357, so the #maximum-unavailable-pods fragment at :146 resolves, and the
section underneath carries the post's arithmetic almost unchanged — percentage rounded up, the field
cannot be 0, the default is 1. It also carries a hand-written stamp, {{< feature-state
for_k8s_version="v1.35" state="beta" >}} (:359), and a note (:372): "The maxUnavailable
field is in Beta stage and it is enabled by default." Neither names the releases in which that was
false.
And the reference disagrees with itself. The feature-gate page for this gate is hand-maintained YAML
and says beta, default true, from v1.37. The component pages — kube-apiserver.md,
kube-controller-manager.md and kube-scheduler.md, all three carrying auto_generated: true and
built from the components' own Go source — print kube:MaxUnavailableStatefulSet=true|false (BETA -
default=false). That is not a typo and it is not specific to this gate: the generated pages at this
pin were built from v1.36 binaries.
What this exercise does not cover, and where it lives
The gate's own four-row ladder is printed, with four sibling StatefulSet and DaemonSet gates, in
The two sentences this post is most emphatic
about, which also shows that the {{<
feature-state >}} stamp above the section is written by hand rather than driven by the gate name,
and that the partition rules contradict each other once .spec.ordinals.start is non-zero. This
exercise does not repeat any of that. It takes the other cut: why the default moved, how far the two
halves of the reference have drifted apart, and what the rollback looks like on a running cluster.
The arithmetic of maxSurge and maxUnavailable on a Deployment — how many Pods exist at each
step, and why a rollout with maxSurge: 0 stalls — belongs to Rolling update
predictions and to the Deployment objects
post. StatefulSet identity, ordinals and claim
retention belong to the StatefulSet
announcement and to the
PVC auto-deletion post. Nothing here re-derives them.
The diff, and why
Broke, and then unbroke, and then broke again. "Before you can use the new feature you must
enable the MaxUnavailableStatefulSet feature flag" is not stale. It is intermittently correct.
Run it against a cluster and the answer depends on the minor release and, for one minor release, on
the patch: true for 1.24–1.34, false for 1.35.0–1.35.3, true again for 1.35.4–1.36, false at 1.37. A
reader who checks it once and writes the answer down has recorded a fact with an expiry date and no
way to tell when it expired. The usual repair for a stale instruction — read it, note the version it
stopped being true, move on — does not work here, because it never stopped being true; it
alternates.
Wrong when it was published, by four lines. The manifest comment at :42 says
podManagementPolicy: OrderedReady # you must set OrderedReady. The post's own body at :133-135
describes what happens when you set Parallel instead, and gives the behaviour a name. Both cannot
be right, and it is the comment that is wrong: maxUnavailable has always applied under both
policies. The documentation now says so in a sentence the post could have written itself
(statefulset.md:305-307), and uses the post's word: "the StatefulSet controller terminates and
creates up to maxUnavailable Pods simultaneously (also known as \"bursting\")".
Still right, in the part that took the most work. Everything the post says about semantics
survives: the batch boundary, the guarantee that batch n+1 waits for batch n, the explicit
warning that replica 3 may become ready before replica 4 and that this is the application's problem,
the percentage rounding up, the default of 1, and the observation that Kubernetes calls them
replicas while the operator does not. Four years on, statefulset.md:361-373 says the same things
in fewer words. The post is a correct description of a mechanism whose configuration story went
sideways.
Retired by being agreed with, in one of its two open questions. "What is the behavior when you
set podManagementPolicy: Parallel?" is now a documented paragraph under #### Parallel Pod
Management, cross-linked to the maxUnavailable section, with the post's own coinage in it. The
second question — partition set to something other than 0 — has no such paragraph, and where the
documentation does discuss partition it contradicts itself. One question was answered by being
absorbed. The other is still open, four years and thirteen releases later, and the post is still
where it is put most plainly.
The ladder
The gate's own stages are laddered next door and are not reprinted here. What belongs here is the population those stages sit in, because every number is small enough to check and each one says something different about how rare this post's gate is.
Of the 487 feature-gate files at the pin, exactly two record a stage boundary at a patch release
rather than a minor: this post's gate, and DisableNodeKubeProxyVersion, which went beta at 1.31.0
and deprecated at 1.31.1 — one patch later, which reads as a mistake caught immediately. Ten gates
have a defaultValue that goes from true back to false at some point, and five of those ten do
it without changing stage. Two of the five are already deprecated when it happens, so the flip is
part of switching something off for good. The other three are in beta, where a default going
backwards means the project shipped an on-by-default feature and then took it back:
MaxUnavailableStatefulSet, SchedulerQueueingHints and WatchList — and all three were later
turned back on. And 401 gates have an alpha stage at all; 23 of those spent eleven or more releases
on it, and four spent exactly eleven, this one among them.
The drift between the two halves of the reference is the number worth writing down. The component
pages list 150 gates in their --feature-gates help text; 136 of those have a hand-maintained gate
file to compare against. Thirty-six of the 136 disagree with that file about stage or default at
v1.37. Five disagree at v1.36. The generated pages were built one minor release before the gate
files were updated, and nothing on either page says so:
gate gate file, at v1.37 binaries, as generated
MaxUnavailableStatefulSet beta true BETA default=false
MemoryQoS beta true ALPHA default=false
HPAScaleToZero beta true ALPHA default=false
SELinuxMount stable true BETA default=false
PodCertificateRequest stable true BETA default=false
DRAExtendedResource stable true BETA default=true
... 30 more at v1.37, 5 at v1.36
For thirty-five of those thirty-six the drift is the ordinary lag of a generated artifact. For this
one it is worse, because the thing the generated page is a release behind on is precisely the flag
flip: default=false was the answer for 1.35.4 through 1.36 and is the answer the binaries' help
text still gives. A reader who checks the component reference gets the correct answer for the wrong
release, and has no way to tell from the page which release it is.
Topology
solo: one node at 10.10.10.180, 4096MB, 4 cores, 25G.
Five Pods on one kubelet, so the only variable in the timing is the controller's batching — a second
node would add image-pull and scheduling skew to a measurement that is entirely about when the
controller decides to delete. The single node is also the control plane, which this exercise needs,
because step 3 may have to turn the gate on the way the post says: by editing two static Pod
manifests. Bring it up with the strand's provisioning
run if it is not already there.
Do
- Find out which row of the ladder this cluster is standing on, before touching anything. The patch number matters here and almost nowhere else, so read it explicitly rather than trusting the minor:
kubectl version -o json | python3 -c 'import json, sys
v = json.load(sys.stdin)["serverVersion"]
mi = int(v["minor"].rstrip("+"))
pa = int(v["gitVersion"].lstrip("v").split(".")[2].split("-")[0])
if mi <= 34: row = "alpha, default false - you must enable it"
elif mi == 35 and pa <= 3: row = "beta, default TRUE - already on"
elif mi == 35 or mi == 36: row = "beta, default FALSE - you must enable it"
else: row = "beta, default TRUE - already on"
print("server ", v["gitVersion"])
print("ladder ", row)'
kubectl get node -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
ssh zain@10.10.10.180 "sudo grep -n feature-gates \
/etc/kubernetes/manifests/kube-apiserver.yaml \
/etc/kubernetes/manifests/kube-controller-manager.yaml || echo 'no --feature-gates set'"
- Ask the API server what it thinks, and ask the schema whether the field is there at all. The first tells you what was compiled in; the second tells you nothing about the gate, which is the point:
kubectl get --raw /metrics | grep '^kubernetes_feature_enabled' \
| grep -i 'maxunavailable\|watchlist\|queueinghints' || echo "no matching gate reported"
kubectl explain statefulset.spec.updateStrategy.rollingUpdate.maxUnavailable
- Now the only test that settles it: write the field and read it back. An API server with the gate off drops the field silently on the way in. If it comes back missing, enable the gate on both components the post's instruction implies — the API server that stores the field and the controller manager that acts on it — and wait for the static Pods to restart:
kubectl create ns sts
kubectl -n sts apply -f - <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: web
podManagementPolicy: OrderedReady
replicas: 5
selector:
matchLabels: {app: web}
template:
metadata:
labels: {app: web}
spec:
terminationGracePeriodSeconds: 1
containers:
- name: a
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
readinessProbe:
tcpSocket: {port: 8080}
periodSeconds: 1
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 2
partition: 0
YAML
kubectl -n sts get sts web -o jsonpath='{.spec.updateStrategy.rollingUpdate}{"\n"}'
- If and only if step 3 came back without
maxUnavailable, do what the post tells you to do. Keep the backups; the teardown needs them:
ssh zain@10.10.10.180 'set -e
cd /etc/kubernetes/manifests
sudo cp kube-apiserver.yaml kube-controller-manager.yaml /root/
sudo sed -i "/- kube-apiserver/a\\ - --feature-gates=MaxUnavailableStatefulSet=true" kube-apiserver.yaml
sudo sed -i "/- kube-controller-manager/a\\ - --feature-gates=MaxUnavailableStatefulSet=true" kube-controller-manager.yaml'
sleep 45
kubectl -n sts patch sts web --type=merge \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"maxUnavailable":2}}}}'
kubectl -n sts get sts web -o jsonpath='{.spec.updateStrategy.rollingUpdate}{"\n"}'
kubectl get --raw /metrics | grep '^kubernetes_feature_enabled' | grep -i maxunavailable
- Reproduce the post's own demonstration, but measure it instead of reading it. The watch output in the post is forty lines of interleaved events; what you actually want is how many Pods are terminating at once, sampled once a second and collapsed:
cat > /tmp/roll.py <<'PY'
import json, subprocess, sys, time
ns = sys.argv[1]
for _ in range(int(sys.argv[2])):
ps = json.loads(subprocess.check_output(
["kubectl", "-n", ns, "get", "pod", "-o", "json"]))["items"]
term = sorted(p["metadata"]["name"] for p in ps
if p["metadata"].get("deletionTimestamp"))
ready = [p for p in ps
if any(c.get("ready") for c in p["status"].get("containerStatuses", []))]
print("pods %2d terminating %-22s ready %d"
% (len(ps), ",".join(term) or "-", len(ready)))
time.sleep(1)
PY
kubectl -n sts rollout status sts/web --timeout=180s
kubectl -n sts set env sts/web ROLL=2
python3 /tmp/roll.py sts 120 | uniq -c
- Answer the post's first open question with the cluster rather than the documentation.
podManagementPolicycannot be patched on a live StatefulSet, so this needs a second one, in a namespace of its own so the sampler sees only it. Watch the creations as well as the deletions — that is what makes it bursting:
kubectl create ns burst
kubectl -n burst apply -f - <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: burst
spec:
serviceName: burst
podManagementPolicy: Parallel
replicas: 5
selector:
matchLabels: {app: burst}
template:
metadata:
labels: {app: burst}
spec:
terminationGracePeriodSeconds: 1
containers:
- name: a
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
readinessProbe:
tcpSocket: {port: 8080}
periodSeconds: 1
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 2
partition: 0
YAML
kubectl -n burst rollout status sts/burst --timeout=180s
kubectl -n burst set env sts/burst ROLL=2
python3 /tmp/roll.py burst 120 | uniq -c
- Now the bug the project turned this gate off for. The v1.37 release note describes it exactly: a faulty initial revision whose Pod never becomes ready, after which the controller fails to move that Pod to the corrected revision. Build that, then correct it, and see whether your cluster has the fix:
kubectl create ns bug
kubectl -n bug apply -f - <<'YAML'
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: broke
spec:
serviceName: broke
podManagementPolicy: OrderedReady
replicas: 3
selector:
matchLabels: {app: broke}
template:
metadata:
labels: {app: broke}
spec:
terminationGracePeriodSeconds: 1
containers:
- name: a
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
readinessProbe:
tcpSocket: {port: 9999}
periodSeconds: 1
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 2
YAML
sleep 60
kubectl -n bug get pod -o wide
kubectl -n bug patch sts broke --type=json \
-p '[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/tcpSocket/port","value":8080}]'
python3 /tmp/roll.py bug 120 | uniq -c
kubectl -n bug get pod -o custom-columns=NAME:.metadata.name,REV:.metadata.labels.controller-revision-hash,READY:.status.containerStatuses[0].ready
kubectl -n bug get controllerrevision -o custom-columns=NAME:.metadata.name,REV:.revision
- The second open question, which the documentation still does not answer. Set a partition and a batch size at the same time and find out which one wins at the boundary:
kubectl -n sts patch sts web --type=merge \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3,"maxUnavailable":2}}}}'
kubectl -n sts set env sts/web ROLL=3
python3 /tmp/roll.py sts 90 | uniq -c
kubectl -n sts get pod \
-o custom-columns=NAME:.metadata.name,REV:.metadata.labels.controller-revision-hash | sort
- Offline now, in the pinned checkout. Count how rare each part of this gate's shape is, against the whole population of gate files rather than against memory:
cd /path/to/kubernetes/website/content/en/docs/reference/command-line-tools-reference/feature-gates
cat MaxUnavailableStatefulSet.md
grep -lE '(from|to)Version: "[0-9]+\.[0-9]+\.[0-9]+"' *.md
python3 - <<'PY'
import os, yaml
g = {}
for fn in sorted(os.listdir(".")):
if fn.endswith(".md") and fn != "index.md":
g[fn[:-3]] = yaml.safe_load(open(fn).read().split("---")[1])
print("gate files", len(g))
back = [(k, a["stage"], b["stage"])
for k, m in g.items()
for a, b in zip(m.get("stages", []), m.get("stages", [])[1:])
if a.get("defaultValue") is True and b.get("defaultValue") is False]
print("default true -> false:", len(back))
print(" same stage:", sorted(k for k, s1, s2 in back if s1 == s2))
mi = lambda v: int(str(v).split(".")[1])
span = {}
for k, m in g.items():
a = [s for s in m.get("stages", []) if s["stage"] == "alpha"]
if a:
span[k] = (max(mi(s["toVersion"]) if s.get("toVersion") else 37 for s in a)
- min(mi(s["fromVersion"]) for s in a) + 1)
print("with an alpha stage:", len(span))
print("eleven or more:", sum(1 for v in span.values() if v >= 11))
print("exactly eleven:", sorted(k for k, v in span.items() if v == 11))
PY
-
And the drift between the two halves of the reference, which is the measurement that explains why step 2 and step 9 can both be right and still disagree. Compare every gate the binaries name against the gate file of the same name, at v1.37 and then at v1.36:
cd /path/to/kubernetes/website/content/en python3 - <<'PY' import os, re, yaml D = "docs/reference/command-line-tools-reference" g = {} for fn in sorted(os.listdir(D + "/feature-gates")): if fn.endswith(".md") and fn != "index.md": g[fn[:-3]] = yaml.safe_load( open(D + "/feature-gates/" + fn).read().split("---")[1]) gen = {m[0]: (m[1], m[2]) for m in re.findall( r'kube:([A-Za-z0-9]+)=true\|false \((ALPHA|BETA|GA|DEPRECATED) - default=(true|false)\)', open(D + "/kube-apiserver.md").read())} def at(name, minor): best = None for s in g[name].get("stages", []): lo = int(str(s["fromVersion"]).split(".")[1]) hi = int(str(s["toVersion"]).split(".")[1]) if s.get("toVersion") else 999 if lo <= minor <= hi: best = s return best and (best["stage"].upper(), str(best.get("defaultValue")).lower()) both = [k for k in gen if k in g] print("named by the binaries", len(gen), " with a gate file", len(both)) for minor in (37, 36): bad = [k for k in both if at(k, minor) != gen[k]] print("v1.%d disagreements: %d" % (minor, len(bad)), "MaxUnavailableStatefulSet" in bad) PY grep -n 'you must set OrderedReady' blog/_posts/2022/maxunavailable-for-statefulset.md sed -n '305,307p' docs/concepts/workloads/controllers/statefulset.md sed -n '607,616p' blog/_posts/2026/kubernetes-v1-37-release/index.md
Expect
Step 1 prints v1.35.x on a cluster provisioned from this repo's strand, and the ladder row it maps
to is the whole exercise in one line. On 1.35 and nowhere else the patch number decides: 3 or lower
and the gate is already on, so the post's instruction is wrong for you; 4 or higher and the gate is
off, so the instruction is right for you, four years after it stopped being right for anyone running
1.35.0. The node should carry no NoSchedule taint — solo is a single node that has to run
workloads — and neither static Pod manifest should mention --feature-gates, because nothing in the
provisioning run sets one.
Step 2 reports kubernetes_feature_enabled{name="MaxUnavailableStatefulSet",stage="BETA"} with a
value of 0 or 1 matching step 1's row, alongside WatchList and, if your build exposes it,
SchedulerQueueingHints — the two other gates in the archive whose default went off and on again
inside a single stage. kubectl explain describes the field in full either way. That is worth
pausing on: the schema always has the field, the gate only decides whether a write survives, so the
documentation you get from the cluster cannot tell you whether the feature works.
Step 3 is the test that matters. With the gate on, the read-back prints
{"maxUnavailable":2,"partition":0}. With it off, it prints {"partition":0} and nothing warns you
— no error, no event, no field on the status. The StatefulSet is accepted, the rollout will be
correct, and it will update one Pod at a time forever. This is the failure mode the post's
instruction exists to prevent, and it is silent.
Step 4 restarts both static Pods, so expect kubectl to fail for perhaps twenty seconds in the
middle. Afterwards the read-back has maxUnavailable in it and the metric reads 1. If the API
server does not come back, the manifest edit is the first suspect: sed inserted the flag after the
- kube-apiserver line, and a component that already had a --feature-gates flag would now have
two, which it rejects at startup. Step 1 checked for exactly that.
Step 5 is the post's kubectl get pods --watch block, compressed. Expect the collapsed output to
show a run with web-3,web-4 terminating together, then a run with web-1,web-2 together, then
web-0 alone: three batches for five Pods, which is the arithmetic the post narrates at :117-119.
Watch the ready count as well — it should never fall below 3, because maxUnavailable: 2 is a floor
on availability and not just a batch size. With maxUnavailable absent — which is the state step 3
leaves you in when the gate is off — the same rollout is five batches of one, and that is the
comparison the post never had to make.
Step 6 answers the first of the post's two closing questions the way the post half-answered it.
Under Parallel the terminations still come two at a time, but so do the creations: the Pod count
stays at 5 and two Pods are simultaneously terminating and being replaced, rather than the count
dipping while the controller waits. The post named this bursting at :135 and the documentation
now uses the same word at statefulset.md:307. Note what the second manifest changes and what it
does not — the name, the namespace, the policy and the label move; replicas, maxUnavailable,
partition and the container are identical to step 3, because they are the variables being held
constant.
Step 7 is the one that may fail, and failing is the result. Under OrderedReady only broke-0 is
ever created, because the controller will not start broke-1 until its predecessor is ready, so the
three replicas you asked for never appear — that alone is worth seeing. On a cluster with the fix,
broke-0 is recreated on the corrected revision within a few seconds of the patch and becomes
ready. On a cluster without it, broke-0 stays on the first controllerrevision indefinitely while
a second revision sits in the list unused — the controller has counted a Pod that was never
available against maxUnavailable, and so never takes it. Compare the revision hash on the Pod
against the two controllerrevision objects. Whichever you get, you have reproduced the reasoning
behind the patch-level rollback: this is the shape of bug that is worth turning a beta default off
for, because the workload it strands is the one that was already broken.
Step 8 should update web-3 and web-4 together and leave web-0 through web-2 on the old
revision, which is partition and maxUnavailable agreeing rather than competing: the partition
chooses the set, the batch size chooses the rate. The interesting case is not this one. It is the
one where .spec.ordinals.start is non-zero, and that case is argued next door rather than here.
Step 9 prints the gate file with its four stages and two patch-level boundaries, then finds exactly
two files in 487 that use a three-part version — this gate and DisableNodeKubeProxyVersion. The
default-regression count should be 10 and the same-stage list should have five names on it, two of
them deprecated gates being switched off for good and three of them beta gates that were taken back:
MaxUnavailableStatefulSet, SchedulerQueueingHints, WatchList. Then 401 gates with an alpha
stage, 23 of them eleven releases or longer, and four at exactly eleven —
BalanceAttachedNodeVolumes, CloudControllerManagerWebhook, ServiceNodeExclusion and this one.
Ten out of 487 is not rare enough to be remarkable on its own; three out of 487 is, and two out of
487 is close to unique.
Step 10 should report 150 gates named by the binaries, 136 with a gate file, 36 disagreements at
v1.37 and 5 at v1.36 — and MaxUnavailableStatefulSet in the v1.37 list. That ratio is the finding:
a sevenfold jump between the two releases is not a documentation quality problem, it is a build
date. The generated reference at this pin is a v1.36 artifact sitting inside a v1.37 tree. Then the
three greps: the post contradicting itself four lines apart, the documentation answering the first
of its two questions in the post's own vocabulary, and the release note naming the bug that moved
the default back. Read that note against the gate file once more. The note says the bug was observed
in v1.36; the gate file records the default going false at 1.35.4. The rollback reached the 1.35
patch stream as well, and only the gate file says so.
Read on
-
The two sentences this post is most emphatic about — the gate's ladder in full, next to four sibling gates, plus the
partitioncontradiction this exercise deliberately stops short of. Read it after step 8, when you have seen the easy case work. -
Rolling update predictions — the same question on a Deployment, where
maxUnavailablehas never been gated, has a percentage default, and comes withmaxSurge. Predicting Pod counts there and then here is the fastest way to feel what ordering costs. -
Two halves of one Service CIDR — the neighbouring row, on a gate that took four releases from alpha to deleted. Put its span against this one's eleven-release alpha and you have the two ends of the same distribution, measured on the same 487 files four days apart.
-
kube-apiserver.mdin the pinned tree, specifically the--feature-gatesrow — 150 gates in one table cell, generated from the binary's help text, and the single best source at the pin for what a component actually shipped with. The index page next to it lists stage and default for every gate too, but it is built from the gate files rather than from a binary, which is exactly why the two can drift and step 10 can measure by how much. -
Unanswerable from the pin: whether your cluster carries the fix for the bug in step 7. The release note names the issue and the release that fixed it, and the gate file records the default moving, but no page at the pin lists the patch releases of 1.35 or 1.36 that the fix was backported to — if it was. Step 7 measures your own cluster; it cannot tell you what any other cluster does.
Teardown
kubectl delete ns sts burst bug
ssh zain@10.10.10.180 'sudo test -f /root/kube-apiserver.yaml \
&& sudo cp /root/kube-apiserver.yaml /root/kube-controller-manager.yaml \
/etc/kubernetes/manifests/ \
|| echo "step 4 was not needed; nothing to put back"'
sleep 45
kubectl get --raw /metrics | grep '^kubernetes_feature_enabled' | grep -i maxunavailable
rm -f /tmp/roll.py
Put the manifests back even if you plan to keep the guest. A hand-set --feature-gates on two
static Pods is exactly the kind of state that makes the next exercise on this cluster measure the
wrong thing, and this one is worse than most because the flag it sets is the flag whose default
moves. Confirm the metric has gone back to the value step 2 printed before you walk away.