Repository navigation
Add opt-in per-server dial backoff #8
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
Draft
vikram-t-verkada
wants to merge
1
commit into
master
Choose a base branch
from
vikram/backoff-failing-nodes
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+147
−0
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package memcache | ||
|
|
||
| import ( | ||
| "crypto/rand" | ||
| "encoding/binary" | ||
| "net" | ||
| "sync" | ||
| "time" | ||
| ) | ||
|
|
||
| // nodeBackoff tracks consecutive failed dials to one server. Once failures | ||
| // reach the threshold, dials are skipped until blockedUntil; after that a | ||
| // single dial is let through to test the server while other callers keep | ||
| // failing fast, so a dead server sees one dial per backoff period per client | ||
| // instead of one per request. | ||
| type nodeBackoff struct { | ||
| threshold int | ||
| baseBackoff time.Duration | ||
| maxBackoff time.Duration | ||
|
|
||
| mu sync.Mutex | ||
| failures int | ||
| backoff time.Duration | ||
| blockedUntil time.Time | ||
| retryInFlight bool | ||
| } | ||
|
|
||
| // allow reports whether a dial may be attempted. Every allow() that returns | ||
| // true must be followed by exactly one record(), or the test dial is never | ||
| // released and the server stays backed off. | ||
| func (n *nodeBackoff) allow() bool { | ||
| n.mu.Lock() | ||
| defer n.mu.Unlock() | ||
|
|
||
| if n.failures < n.threshold { | ||
| return true | ||
| } | ||
| if time.Now().Before(n.blockedUntil) || n.retryInFlight { | ||
| return false | ||
| } | ||
| n.retryInFlight = true | ||
| return true | ||
| } | ||
|
|
||
| // record feeds a dial's outcome back into the tracker. | ||
| func (n *nodeBackoff) record(failed bool) { | ||
| n.mu.Lock() | ||
| defer n.mu.Unlock() | ||
|
|
||
| n.retryInFlight = false | ||
| if !failed { | ||
| n.failures = 0 | ||
| n.backoff = n.baseBackoff | ||
| return | ||
| } | ||
|
|
||
| n.failures++ | ||
| // Dials started before the block keep failing in after it; only the | ||
| // failure that crosses the threshold or a failed test dial extends it. | ||
| if n.failures < n.threshold || time.Now().Before(n.blockedUntil) { | ||
| return | ||
| } | ||
| n.blockedUntil = time.Now().Add(withJitter(n.backoff)) | ||
| n.backoff *= 2 | ||
| if n.backoff > n.maxBackoff { | ||
| n.backoff = n.maxBackoff | ||
| } | ||
| } | ||
|
|
||
| // withJitter returns a random duration in [d/2, 3d/2) so clients that backed | ||
| // off a server at the same moment don't all retry it at the same moment. | ||
| func withJitter(d time.Duration) time.Duration { | ||
| var buf [8]byte | ||
| if _, err := rand.Read(buf[:]); err != nil { | ||
| return d | ||
| } | ||
| frac := float64(binary.LittleEndian.Uint64(buf[:])>>11) / (1 << 53) | ||
| return d/2 + time.Duration(frac*float64(d)) | ||
| } | ||
|
|
||
| // nodeBackoff returns the tracker for addr, creating it on first use. | ||
| func (c *Client) nodeBackoff(addr net.Addr) *nodeBackoff { | ||
| c.lk.Lock() | ||
| defer c.lk.Unlock() | ||
| if c.backoffs == nil { | ||
| c.backoffs = make(map[string]*nodeBackoff) | ||
| } | ||
| n, ok := c.backoffs[addr.String()] | ||
| if !ok { | ||
| n = &nodeBackoff{ | ||
| threshold: c.FailureThreshold, | ||
| baseBackoff: c.baseBackoff(), | ||
| maxBackoff: c.maxBackoff(), | ||
| backoff: c.baseBackoff(), | ||
| } | ||
| c.backoffs[addr.String()] = n | ||
| } | ||
| return n | ||
| } | ||
|
|
||
| func (c *Client) baseBackoff() time.Duration { | ||
| if c.BaseBackoff > 0 { | ||
| return c.BaseBackoff | ||
| } | ||
| return DefaultBaseBackoff | ||
| } | ||
|
|
||
| func (c *Client) maxBackoff() time.Duration { | ||
| if c.MaxBackoff > 0 { | ||
| return c.MaxBackoff | ||
| } | ||
| return DefaultMaxBackoff | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,10 @@ var ( | |
|
|
||
| // ErrNoServers is returned when no servers are configured or available. | ||
| ErrNoServers = errors.New("memcache: no servers configured or available") | ||
|
|
||
| // ErrConnectionBroken is returned without dialing when the server has | ||
| // failed FailureThreshold dials in a row and is being backed off. | ||
| ErrConnectionBroken = errors.New("memcache: connection to server is broken, backing off") | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -70,6 +74,12 @@ const ( | |
| // DefaultMaxIdleConns is the default maximum number of idle connections | ||
| // kept for any single address. | ||
| DefaultMaxIdleConns = 2 | ||
|
|
||
| // DefaultBaseBackoff is the default first backoff for a failing server. | ||
| DefaultBaseBackoff = 1 * time.Second | ||
|
|
||
| // DefaultMaxBackoff is the default cap on a failing server's backoff. | ||
| DefaultMaxBackoff = 60 * time.Second | ||
|
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. 60s max (up to 90s with jitter) is long for a cache: every key on a recovered node misses until the next test dial. Fine as a library default since it's configurable; vaccel should probably set something like 10–15s. |
||
| ) | ||
|
|
||
| const buffered = 8 // arbitrary buffered channel size, for readability | ||
|
|
@@ -149,10 +159,24 @@ type Client struct { | |
| // be set to a number higher than your peak parallel requests. | ||
| MaxIdleConns int | ||
|
|
||
| // FailureThreshold is the number of consecutive failed dials to a server | ||
| // after which new dials to it are skipped (returning ErrConnectionBroken) | ||
| // for a backoff period. If zero, backoff is disabled. | ||
| FailureThreshold int | ||
|
|
||
| // BaseBackoff is the first backoff for a failing server; each further | ||
| // failed dial doubles it. If zero, DefaultBaseBackoff is used. | ||
| BaseBackoff time.Duration | ||
|
|
||
| // MaxBackoff caps a failing server's backoff. If zero, DefaultMaxBackoff | ||
| // is used. | ||
| MaxBackoff time.Duration | ||
|
|
||
| selector ServerSelector | ||
|
|
||
| lk sync.Mutex | ||
| freeconn map[string][]*conn | ||
| backoffs map[string]*nodeBackoff | ||
| TlsConfig *tls.Config | ||
| } | ||
|
|
||
|
|
@@ -299,7 +323,17 @@ func (c *Client) getConn(addr net.Addr) (*conn, error) { | |
| cn.extendDeadline() | ||
| return cn, nil | ||
| } | ||
| var backoff *nodeBackoff | ||
| if c.FailureThreshold > 0 { | ||
| backoff = c.nodeBackoff(addr) | ||
| if !backoff.allow() { | ||
| return nil, ErrConnectionBroken | ||
| } | ||
| } | ||
| nc, err := c.dial(addr) | ||
| if backoff != nil { | ||
| backoff.record(err != nil) | ||
| } | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
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.
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.
recordclearsretryInFlightfor every dial, but only the test dial should. WithDialTimeout(3s in vaccel prod) >BaseBackoff, dials started before the block finish after it expires, release the test-dial slot so more test dials get through, and each of those failures re-extends and doubles the block (line 60). Suggestallow()return whether the caller is the test dial, andrecord(probe, failed)only clearretryInFlight/ extend the block whenprobeis true. Worth a test for exactly this late-finishing-dial case.