Jitter kubernetes dial backoff so builders do not retry in lockstep - #3995
Jitter kubernetes dial backoff so builders do not retry in lockstep#39951991santhu wants to merge 1 commit into
Conversation
|
Corrected the direction of the jitter in my own patch. It was equal jitter, I ran the numbers rather than just asserting the problem. 100 builders hitting the same CSR lag: without jitter all 100 dial inside the same 100ms window on every attempt, with it about 28 on the first retry and 7 by the fourth. Simulated arrival times, not something I've measured on a real cluster.
|
crazy-max
left a comment
There was a problem hiding this comment.
The direction makes sense to me.
One small wording nit: this is additive jitter, [d, 2d] capped by maxDelay, not equal jitter anymore. That behavior is fine if preserving the current exponential delay as the floor is intentional, but the description/comment should call it that directly.
Also, please squash the commits before merge.
| return d | ||
| } | ||
|
|
||
| return d + time.Duration(rand.Int63n(int64(extra)+1)) |
There was a problem hiding this comment.
The new math/rand call should use the same gosec suppression pattern as podchooser.RandomPodChooser, since retry jitter doesn't need crypto randomness:
return d + time.Duration(rand.Int63n(int64(extra)+1)) // #nosec G404 -- no strong randomness required for retry jitter…n lockstep calculateBackoff was a pure function of the attempt number, so every builder retrying the same condition waited exactly the same durations. That matters here because CSR approval lagging node readiness is a cluster-wide event: concurrent builds scheduled onto newly-ready nodes hit the transient TLS error at the same moment, then retry in unison against an API server already working through the approval backlog. Add jitter drawn from [d, 2d], capped by maxDelay, where d is the exponential value for the attempt. The exponential value is the floor rather than the midpoint, so a retry is never issued sooner than the schedule would have on its own. Centring it would let the first retry fire at baseDelay/2, which undercuts a configured minimum at exactly the wrong moment. With maxRetries=5 and baseDelay=500ms the delays used are 500ms through 4s, so the 10s cap is never reached in practice. Marked the math/rand call with the same #nosec pattern podchooser uses. Signed-off-by: Santhosh Kumar Somarapu <somarapu.santhosh91@gmail.com>
|
Thanks, all three done. Switched to the same pattern as return d + time.Duration(rand.Int63n(int64(extra)+1)) // #nosec G404 -- no strong randomness required for retry jitterYou're right about the wording. The doc comment said "backoff and jitter" without saying which kind, which is worse than useless here since the whole point of the change is that the jitter is additive rather than centred. It now says so directly: additive jitter drawing from Squashed to one commit. Verified with golangci-lint v2.8.0 built from source against Go 1.26, matching how the lint job builds it: 0 issues. |
dfa4bf9 to
c1195f0
Compare
The problem
calculateBackoffis a pure function of the attempt number:so every builder retrying the same condition waits for exactly 500ms, 1s, 2s, 4s, 8s.
That matters specifically because of the condition this retry exists to handle. From the comment on
tryWithBackoff:CSR approval lag is a cluster-wide, time-correlated event rather than a per-pod accident. When a node group comes up, every build scheduled onto those nodes hits the transient TLS error at roughly the same moment — and then retries at identical instants. In CI, where parallel builds are the normal case, that concentrates retries on the API server precisely while it is still working through the approval backlog.
The backoff limits how often each builder retries. It does not stop builders retrying together.
The change
Equal jitter — retain half the computed interval as a floor, randomise the remainder:
Half is kept rather than using full jitter so a retry is never issued immediately after a TLS failure. The result never exceeds
maxDelay, so no builder waits longer than it does today — the change only removes the alignment.Testing
go test -count=2 ./driver/kubernetes/...passes,go vetclean.The package had no test file, so this adds one covering: the result stays within
[d/2, d]for attempts 0–5 across 500 samples each; a large attempt count stays capped atmaxDelay; and successive calls vary, which fails if the implementation regresses to deterministic.Note
math/randrather thancrypto/rand— this is load spreading, not a security boundary. Happy to change if the project prefers otherwise. Context for the original retry: #2668.