-
Notifications
You must be signed in to change notification settings - Fork 675
kubernetes: jitter dial backoff so builders do not retry in lockstep #3995
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||
| stderrors "errors" | ||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||
| "math/rand" | ||||||||||||||||||||||||||||||||||||||||
| "net" | ||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||
| "syscall" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -389,9 +390,37 @@ func isTransientConnectionError(err error) bool { | |||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // calculateBackoff calculates the delay for the given attempt with exponential backoff. | ||||||||||||||||||||||||||||||||||||||||
| // calculateBackoff calculates the delay for the given attempt with exponential | ||||||||||||||||||||||||||||||||||||||||
| // backoff and additive jitter, drawing 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. | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // The exponential component alone is a pure function of the attempt number, so | ||||||||||||||||||||||||||||||||||||||||
| // every builder retrying the same condition waits for exactly the same durations. | ||||||||||||||||||||||||||||||||||||||||
| // That matters for the case this backoff exists to handle: CSR approval lagging | ||||||||||||||||||||||||||||||||||||||||
| // node readiness is a cluster-wide event, so concurrent builds scheduled onto | ||||||||||||||||||||||||||||||||||||||||
| // newly-ready nodes hit the transient TLS error at the same moment and would | ||||||||||||||||||||||||||||||||||||||||
| // then retry in unison, concentrating load on the API server while it is already | ||||||||||||||||||||||||||||||||||||||||
| // working through the approval backlog. | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+401
to
+405
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. duplicates |
||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // The jitter is added to the interval rather than centred on it, so a retry is | ||||||||||||||||||||||||||||||||||||||||
| // never issued sooner than the exponential schedule intended. Centring it would | ||||||||||||||||||||||||||||||||||||||||
| // let the first retry fire after baseDelay/2, undercutting a configured minimum | ||||||||||||||||||||||||||||||||||||||||
| // while the API server is still working through the CSR backlog. The extra is | ||||||||||||||||||||||||||||||||||||||||
| // bounded by the remaining headroom so the result never exceeds maxDelay. | ||||||||||||||||||||||||||||||||||||||||
| func calculateBackoff(attempt int, baseDelay, maxDelay time.Duration) time.Duration { | ||||||||||||||||||||||||||||||||||||||||
| return min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay) | ||||||||||||||||||||||||||||||||||||||||
| d := min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| extra := d | ||||||||||||||||||||||||||||||||||||||||
| if headroom := maxDelay - d; headroom < extra { | ||||||||||||||||||||||||||||||||||||||||
| extra = headroom | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if extra <= 0 { | ||||||||||||||||||||||||||||||||||||||||
| return d | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return d + time.Duration(rand.Int63n(int64(extra)+1)) // #nosec G404 -- no strong randomness required for retry jitter | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+413
to
+423
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: took me some time to understand the function, I think we could simplify the code and use more explicit names to improve readability:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.Client, error) { | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||
| package kubernetes | ||||||
|
|
||||||
| import ( | ||||||
| "testing" | ||||||
| "time" | ||||||
|
|
||||||
| "github.com/stretchr/testify/require" | ||||||
| ) | ||||||
|
|
||||||
| func TestCalculateBackoffNeverShorterThanSchedule(t *testing.T) { | ||||||
| const ( | ||||||
| baseDelay = 500 * time.Millisecond | ||||||
| maxDelay = 10 * time.Second | ||||||
| ) | ||||||
|
|
||||||
| for attempt := range 6 { | ||||||
| schedule := min(time.Duration(1<<uint(attempt))*baseDelay, maxDelay) | ||||||
| ceiling := min(2*schedule, maxDelay) | ||||||
|
|
||||||
| for range 500 { | ||||||
| got := calculateBackoff(attempt, baseDelay, maxDelay) | ||||||
| require.GreaterOrEqual(t, got, schedule, | ||||||
| "attempt %d must never wait less than the exponential schedule", attempt) | ||||||
| require.LessOrEqual(t, got, ceiling, | ||||||
| "attempt %d must not exceed twice the schedule, nor maxDelay", attempt) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestCalculateBackoffRespectsMaxDelay(t *testing.T) { | ||||||
| const ( | ||||||
| baseDelay = 500 * time.Millisecond | ||||||
| maxDelay = 10 * time.Second | ||||||
| ) | ||||||
|
|
||||||
| for range 500 { | ||||||
| require.LessOrEqual(t, calculateBackoff(20, baseDelay, maxDelay), maxDelay, | ||||||
| "a large attempt count must stay capped at maxDelay") | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func TestCalculateBackoffVaries(t *testing.T) { | ||||||
| const ( | ||||||
| baseDelay = 500 * time.Millisecond | ||||||
| maxDelay = 10 * time.Second | ||||||
| ) | ||||||
|
|
||||||
| seen := make(map[time.Duration]struct{}) | ||||||
| for range 500 { | ||||||
| seen[calculateBackoff(3, baseDelay, maxDelay)] = struct{}{} | ||||||
| } | ||||||
|
|
||||||
| // A deterministic implementation returns a single value. The range at | ||||||
| // attempt 3 is two seconds wide, so one distinct value across 500 draws | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: at attempt 3, d is 4s, so the additive-jitter range is [4s, 8s], four seconds wide not two. I think this comment is left over from the earlier equal-jitter implementation. The test itself looks correct.
Suggested change
|
||||||
| // would not be chance. | ||||||
| require.Greater(t, len(seen), 1, | ||||||
| "backoff must vary so concurrent builders do not retry in lockstep") | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: doc comment is much longer than this package's convention and duplicates the caller's documentation. I'd leave the rationale behind the retries to
tryWithBackoffand focus oncalculateBackoffcontract and non-obvious implementation choice (floor vs. midpoint).See http://go.dev/doc/comment
(and remove everything below this to keep it to 2 lines)