retry extensions: implement "other priority" extension#4529
retry extensions: implement "other priority" extension#4529alyssawilk merged 16 commits intoenvoyproxy:masterfrom
Conversation
Implements a RetryPriority which will keep track of attempted priorities and attempt to route retry requests to other priorities. The update frequency is configurable, allowing multiple requests to hit each priority if desired. As a fallback, when no healthy priorities remain, the list of attempted priorities will be reset and a host will selected again using the original priority load. Signed-off-by: Snow Pettersen <snowp@squareup.com>
64081ff to
f91c5ce
Compare
|
@alyssawilk do you mind taking this one on? |
| message OtherPriorityConfig { | ||
| // How often the priority load should be updated based on previously attempted priorities. Useful | ||
| // to allow each priorities to receive more than one request before being excluded. | ||
| int32 update_frequency = 1; |
There was a problem hiding this comment.
There might be some context I'm missing here but it's not obvious to me what the units of this frequency are.
There was a problem hiding this comment.
The unit here is "number of attempts". I'll try to clarify the comment
There was a problem hiding this comment.
The comment definitely helps but reading it I misread and thought this was number_of_requests_using_default_priority. Can we extend it just a bit to say that the fourth and fifth will then use the priority load excluding priorities for the frirst 4 attempts?
We should also comment what happens when we run out of priority levels
| std::vector<uint32_t>& per_priority_health_); | ||
|
|
||
| // The percentage load (0-100) for each priority level | ||
| std::vector<uint32_t> per_priority_load_; |
There was a problem hiding this comment.
Did you meant to make these vectors public?
| void recalculatePerPriorityState(uint32_t priority); | ||
| void static recalculatePerPriorityState(uint32_t priority, const PrioritySet& priority_set, | ||
| PriorityLoad& priority_load, | ||
| std::vector<uint32_t>& per_priority_health_); |
There was a problem hiding this comment.
If this becomes a static function that doesn't access the members, I don't think the argument names want the _ suffix (and the comment should also probably be updated).
| } | ||
|
|
||
| const Upstream::PriorityLoad& | ||
| determinePriorityLoad(const Upstream::PrioritySet& priority_set, |
There was a problem hiding this comment.
This function persist the address of priority_set after exiting. I'm not as familiar with this area of the code, so I don't know if this is an expected/safe thing to do. But I think there should be a comment about the ownership semantics here if this is indeed what we want to do.
There was a problem hiding this comment.
Thinking about this some more I don't think this is actually safe, since the set is owned by the LB which is owned by the cluster. If a cluster is removed during routing, we'd run into issues accessing the set. I'll try to figure out a better way to keep the priority load in sync with membership updates, or perhaps not handle that at all and document the limitation.
There was a problem hiding this comment.
Changed it to no longer persist the priority_set address and instead documented the limitation. While watching cluster membership is possible it gets tricky due to the possibility of cluster being removed, so I'd rather tackle that separately to adding this basic implementation
| priority, *priority_set_, per_priority_load_, per_priority_health_); | ||
| } | ||
|
|
||
| // Distributes priority load between priorities that should be consider after |
| // excluding attempted priorities. | ||
| void adjustForAttemptedPriorities(); | ||
|
|
||
| uint32_t update_frequency_; |
| */ | ||
| class RetryPriorityNameValues { | ||
| public: | ||
| // Previous host predicate. Rejects hosts that have already been tried. |
There was a problem hiding this comment.
This description doesn't seem to match.
| if (attempted_priorites_.size() < update_frequency_) { | ||
| return original_priority_load; | ||
| } else if (attempted_priorites_.size() % update_frequency_ == 0) { | ||
| for (auto priority : attempted_priorites_) { |
| } | ||
| } | ||
|
|
||
| per_priority_load_ = per_priority_load; |
There was a problem hiding this comment.
It's unclear to me why this doesn't adjust the per_priority_load in place. If it can't be adjusted in place, this might be worth refactoring out so the call here looks more like:
per_priority_load_ = refactoredRebalancingFunction(adjusted_per_priority_health)
There was a problem hiding this comment.
I think it was a remnant of a previous iteration where I short circuiting during the update to fall back to the original values. I'll update it to update in place
|
Alyssa asked me to take a look at this - hopefully some comments to get you started. |
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
alyssawilk
left a comment
There was a problem hiding this comment.
Very cool to have a good example of our new retry framework. I'm going to have to do at least 2 passes to page back in all the health stuff but looks great so far!
| // to allow each priorities to receive more than one request before being excluded. | ||
| // For example, by setting this to 2, then the first two attempts (initial attempt and one retry) | ||
| // will use the unmodified priority load. The third and fourth attempt will use priority load which | ||
| // excludes the priorities routed to with the first two attempts. |
There was a problem hiding this comment.
I think it would be useful to have a more detailed explanation either here in the proto or in the load balancing docs* detailing about which priorities will be selected on retry and why.
*https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/load_balancing#priority-levels
| message OtherPriorityConfig { | ||
| // How often the priority load should be updated based on previously attempted priorities. Useful | ||
| // to allow each priorities to receive more than one request before being excluded. | ||
| int32 update_frequency = 1; |
There was a problem hiding this comment.
The comment definitely helps but reading it I misread and thought this was number_of_requests_using_default_priority. Can we extend it just a bit to say that the fourth and fifth will then use the priority load excluding priorities for the frirst 4 attempts?
We should also comment what happens when we run out of priority levels
| // Initialize our local priority_load_ and priority_health_, | ||
| // keeping them in sync with the member update cb. | ||
| for (auto& host_set : priority_set.hostSetsPerPriority()) { | ||
| recalculatePerPriorityState(host_set->priority(), priority_set); |
There was a problem hiding this comment.
I think this is fine for 2-3 priority levels but this might get expensive fast if one has a larger number of host sets and priorities. I think by nature of this code it's always going to have a lot of O(#priorities) work per pick - maybe we should have a warning comment about the scaling constraint here? I suspect very few people will have N>3 but still better to not surprise anyone :-P
There was a problem hiding this comment.
We use something like 6-8 priorities so I totally share that concern. I'll add a note about potential perf issues
| void adjustForAttemptedPriorities(); | ||
|
|
||
| const uint32_t update_frequency_; | ||
| std::vector<uint32_t> attempted_priorites_; |
There was a problem hiding this comment.
attempted_priorites_ -> attempted_priorites_
| } | ||
| attempted_priorites_.clear(); | ||
|
|
||
| adjustForAttemptedPriorities(); |
There was a problem hiding this comment.
I'd prefer we avoid recursion in tricky code like this - I think it's too easy to have a refactor result in a loop.
Given we;ve already verified that we aren't in the state where everything is unhealthy, what if we just factored out the prior section into a helper function and rerun the helper if we hit this case?
There was a problem hiding this comment.
Yeah that sounds better, I was being a bit too cute about it
| // | ||
| // Note that changes made to the cluster during retries will not be reflected in the priority | ||
| // load of retries, so care should be taken when using this with long running requests that | ||
| // might retry. |
There was a problem hiding this comment.
Can we add more detail about what care they should take?
I had a moment of being convinced this would cause crashes due to the priority set being resized in a way not reflected by attempted_priorities before I recalled that priority_set_ always grows but never shrinks :-P
There was a problem hiding this comment.
Realizing that the limitation I had in mind is due to me having the if (!initialized_) piece where I only read from the PrioritySet on the first retry, so changes to host health wouldn't be picked up on subsequent attempts. I can avoid this altogether by running recalculatePerPriorityState on each attempt to pick up changes to PrioritySet, although it would come at additional runtime cost. Any thoughts on this trade off?
There was a problem hiding this comment.
Thinking about it some I think I prefer the slower approach as it's less surprising, so I'll update the comment and code to use that
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
|
@alyssawilk Wanna give this another look? I believe I've addressed the feedback |
alyssawilk
left a comment
There was a problem hiding this comment.
Looking good - just 2 little nits left!
| // Attempt 4: P2 | ||
| // | ||
| // Using this PriorityFilter requires rebuilding the priority load, which runs in O(# of | ||
| // priorities), which might incur significant overhead for clusters with many priorities. |
There was a problem hiding this comment.
This is fantastic. 5***** would read again :-)
| std::min<uint32_t>(total_load, per_priority_health[i] * 100 / total_health); | ||
| total_load -= per_priority_load[i]; | ||
| } | ||
|
|
There was a problem hiding this comment.
Oh nice improvement. Mind adding both a load balancer test and adding this to the commit notes?
There was a problem hiding this comment.
Huh this was actually added in #4533, not sure how this ended up in this diff. I'll see if I can fix it
There was a problem hiding this comment.
Haha, the joys of git :-P
There was a problem hiding this comment.
I merged in a bunch of unrelated commits and the diff went away /shrug
| } | ||
| } | ||
|
|
||
| std::pair<std::vector<uint32_t>, uint32_t> OtherPriorityRetryPriority::adjustedHealth() const { |
There was a problem hiding this comment.
Can we get your comment back?
// create an adjusted health view of the priorities, where attempted priorities are
// given a zero weight.
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Implements a RetryPriority which will keep track of attempted priorities and attempt to route retry requests to other priorities. The update frequency is configurable, allowing multiple requests to hit each priority if desired. As a fallback, when no healthy priorities remain, the list of attempted priorities will be reset and a host will selected again using the original priority load. Extracts out the recalculatePerPriorityState from LoadBalancerBase to recompute the priority load with the same code used by the LB. Signed-off-by: Snow Pettersen snowp@squareup.com Risk Level: Medium, new extension Testing: unit tests Docs Changes: n/a Release Notes: n/a Signed-off-by: Snow Pettersen <snowp@squareup.com> Signed-off-by: Aaltan Ahmad <aa@stripe.com>
Implements a RetryPriority which will keep track of attempted
priorities and attempt to route retry requests to other priorities. The
update frequency is configurable, allowing multiple requests to hit each
priority if desired.
As a fallback, when no healthy priorities remain, the list of attempted
priorities will be reset and a host will selected again using the
original priority load.
Extracts out the
recalculatePerPriorityStatefromLoadBalancerBasetorecompute the priority load with the same code used by the LB.
Signed-off-by: Snow Pettersen snowp@squareup.com
Risk Level: Medium, new extension
Testing: UTs
Docs Changes: n/a
Release Notes: n/a