Repository navigation
feat(keeper): hand leadership over before restarting the leader - #347
melancholictheory wants to merge 1 commit into
Conversation
cceca92 to
d49999f
Compare
|
The first push failed Dropped that part. The change is now only the |
|
The PR handles the leader yield problem during shut down initiated by the operator only. However multiple native kubernetes components can gracefully initiate a pod shut down. Examples are :
So the solution to the problem should be a generic solution that handles all graceful shutdowns of a pod, ideally using a native kuberntes construct. I suggest using the container's Currently our CRD does not allow for setting the Lifecycle.PreStop and terminationGracePeriodSeconds, so I suggest that we should modify the CRD to allow setting the container's Lifecycle.PreStop and terminationGracePeriodSeconds, with default that executes the suggested bash script and reasonable graceful termination period. We need to allow users who run distroless containers to override these default because their images don't contain bash. |
d0be9af to
d903e2b
Compare
|
You are right, and I measured a couple of things before changing course. The operator-side handover only covers restarts the operator itself issues. A descheduler eviction or a node drain never goes through the reconcile loop, so that version of the PR left the window open for exactly the cases you list. Dropped it. What replaced it is a On timing: the kubelet sends SIGTERM as soon as the hook returns, so I checked whether the transfer survives that. On a 3-node 26.7.5.10, Two details on the CRD side. The one gate is the Keeper version: the commands API is there from 26.3 and missing on 25.8, which the compat matrix still includes, so the hook is only added when One thing I want to be upfront about: the kubelet bounds a hook by the grace period, so a Keeper that has stopped answering on |
A leader restart leaves the ensemble without a leader until the election completes, which ClickHouse#346 measured at 5 to 6 seconds with INSERT p95 rising from about 150 ms to 6.3 s. The operator's rollout is only one source of such restarts; evictions by the descheduler, the cluster autoscaler or a node drain stop pods without the operator in the loop. Add a preStop hook to the Keeper container that sends ydld through the http_control HTTP commands API (GET /api/v1/commands?command=ydld), so any graceful termination hands leadership to a peer first. Going through HTTP keeps it working on distroless images, and the endpoint is the same port the readiness probe already uses. Keeper serves that API from 26.3; on older servers the request 404s, the kubelet records FailedPreStopHook and the container stops as before. The hook is not gated on Status.Version on purpose: Keeper has no version probe, the value comes from running replicas, and a template that depends on it rolls every freshly created cluster once the version is observed. Measured on a 3-node Keeper 26.7.5.10: ydld followed by an immediate SIGTERM gives the ensemble a new leader in 0.04 s, against 1.36 s for a plain SIGTERM. On a lone replica the command is a no-op. Fixes ClickHouse#346
d903e2b to
a599380
Compare
|
The previous push failed The cause was the version gate. Keeper has no version probe; Dropped the gate: the hook is always set. On Keeper older than 26.3 the request returns 404, the kubelet records |
|
The timing in your test is not always guaranteed. The current leader keeps acting as leader for whatever was already in its log/pipeline or This is why I suggested a bash script to make sure that the new leader election finished before we let k8s send a sigterm. |
|
You are right, and I have numbers now rather than an idle-cluster measurement. I read the NuRaft path first. Then a 3-node 26.7.5.10 under write load from a Go
Under the heavy load the old leader still answered So the hook has to wait, which means h() { exec 3<>/dev/tcp/127.0.0.1/9123; printf 'GET %s HTTP/1.0\r\n\r\n' "$1" >&3; cat <&3; exec 3<&-; }
h '/api/v1/commands?command=ydld' >/dev/null 2>&1
for i in $(seq 1 100); do h /ready 2>/dev/null | grep -q '"role":"leader"' || exit 0; sleep 0.1; doneIt talks to The trade-off is the one you named: on a distroless image Longer term the right place for the wait is Keeper itself. A blocking form of the command on the HTTP API would make the shell-free hook correct on any image, and that is a ClickHouse issue rather than an operator one. I will hold the PR in its current state until a maintainer weighs in on which shape they want, exec with the distroless caveat, or the best-effort |
|
Closing in favour of #351, which is the exec-with-wait shape from the measurements above, plus the The load measurements and the NuRaft notes in the thread above stay relevant to #351, so I will leave them here rather than repeat them. |
Why
When a Keeper leader pod stops, the ensemble has no leader until the election finishes. #346 measured that window at 5 to 6 seconds, with ClickHouse logging session expirations and INSERT p95 going from about 150 ms to 6.3 s while it lasted.
The operator's own rolling update is only one way a leader pod stops. The descheduler, the cluster autoscaler, VPA and a node drain all terminate pods without the operator in the loop, so a handover implemented in the reconcile loop covers a fraction of the cases (raised by @mu-soliman below).
What
The Keeper container gets a
preStophook that asks the server to hand Raft leadership to a peer before it stops:It goes through the
http_controlport the readiness probe already uses, via Keeper's HTTP commands API, which executes four letter words from thecommandquery parameter. That keeps it shell-free, so it works on the distroless images too, and it runs on every graceful termination regardless of who initiated it.Keeper serves the commands API from 26.3 (checked on 26.3.22.7, 26.5.7.64, 26.6.3.62 and 26.7.5.10) and not on 25.8.32.4, which the compat matrix still covers. On the older servers the request returns 404, the kubelet records a
FailedPreStopHookevent, and the container stops exactly as before, so the cost there is one warning event per termination.The hook is deliberately not gated on
status.version. Keeper has no version probe; that value comes frommntron running replicas, so it is empty when the StatefulSets are created and arrives afterwards. A template that depends on it changes after startup and rolls every new cluster once, which is what the first push of this PR did in e2e. A unit test now pins thattemplateContaineroutput does not vary withstatus.version.No CRD change.
terminationGracePeriodSecondsis already exposed onpodTemplate;lifecycleis not exposed oncontainerTemplateand this PR does not add it, since the default needs no override to work on distroless. If someone needs to disable or replace the hook, exposinglifecycleis a small follow-up.One cost to be aware of: the kubelet bounds a
preStophook by the pod's grace period, so a server that has stopped answering onhttp_controlentirely holds termination open for up toterminationGracePeriodSeconds(30 s by default) before SIGTERM goes out. A healthy server answers in milliseconds, and a server that answers with an error does not delay anything, only a hung one does. The readiness probe already depends on the same port, so a hung server is already failing readiness by then.Measured on a 3-node Keeper 26.7.5.10
ydldover HTTP, thenmntrreports follower after 0.085 sydldover HTTP followed by an immediate SIGTERM, which is what the kubelet does right after the hook returns: a surviving node reports itself leader after 0.04 sstandalone, so single-node clusters are unaffectedThe response body is
Sent yield leadership request to leader.even when the command lands on a follower, so nothing in the hook depends on it.Testing
Unit tests on
templateContainer: the hook is present with the expected path, port and scheme, and the output is identical for an empty, a 25.8 and a 26.7status.version.Related Issues
Fixes #346