Skip to content

Commit 1995d92

Browse files
Add FULL_SCAN selection mode to least request LB (#31507)
Add FULL_SCAN mode to least request load balancer. By default, the least request load balancer returns the host with the fewest active requests from a set of N randomly selected hosts. This introduces a new "full scan" selection method that returns the host with the fewest number of active requests from all hosts. If multiple hosts are tied for "least", one of the tied hosts is randomly chosen. Added selection_method option to the least request load balancer. If set to FULL_SCAN, Envoy will select the host with the fewest active requests from the entire host set rather than choice_count random choices. Risk Level: low, existing code path unchanged Testing: unit tests add Docs Changes: protobuf docs Release Notes: added Signed-off-by: Jared Kirschner <jkirschner@hashicorp.com> Signed-off-by: Leonardo da Mata <ldamata@spotify.com> Co-authored-by: Leonardo da Mata <barroca@gmail.com>
1 parent ff68dcb commit 1995d92

File tree

8 files changed

+203
-7
lines changed

8 files changed

+203
-7
lines changed

api/envoy/extensions/load_balancing_policies/least_request/v3/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2
66

77
api_proto_package(
88
deps = [
9+
"//envoy/annotations:pkg",
910
"//envoy/config/core/v3:pkg",
1011
"//envoy/extensions/load_balancing_policies/common/v3:pkg",
1112
"@com_github_cncf_xds//udpa/annotations:pkg",

api/envoy/extensions/load_balancing_policies/least_request/v3/least_request.proto

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "envoy/extensions/load_balancing_policies/common/v3/common.proto";
77

88
import "google/protobuf/wrappers.proto";
99

10+
import "envoy/annotations/deprecation.proto";
1011
import "udpa/annotations/status.proto";
1112
import "validate/validate.proto";
1213

@@ -22,10 +23,34 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
2223
// This configuration allows the built-in LEAST_REQUEST LB policy to be configured via the LB policy
2324
// extension point. See the :ref:`load balancing architecture overview
2425
// <arch_overview_load_balancing_types>` for more information.
25-
// [#next-free-field: 6]
26+
// [#next-free-field: 7]
2627
message LeastRequest {
28+
// Available methods for selecting the host set from which to return the host with the
29+
// fewest active requests.
30+
enum SelectionMethod {
31+
// Return host with fewest requests from a set of ``choice_count`` randomly selected hosts.
32+
// Best selection method for most scenarios.
33+
N_CHOICES = 0;
34+
35+
// Return host with fewest requests from all hosts.
36+
// Useful in some niche use cases involving low request rates and one of:
37+
// (example 1) low request limits on workloads, or (example 2) few hosts.
38+
//
39+
// Example 1: Consider a workload type that can only accept one connection at a time.
40+
// If such workloads are deployed across many hosts, only a small percentage of those
41+
// workloads have zero connections at any given time, and the rate of new connections is low,
42+
// the ``FULL_SCAN`` method is more likely to select a suitable host than ``N_CHOICES``.
43+
//
44+
// Example 2: Consider a workload type that is only deployed on 2 hosts. With default settings,
45+
// the ``N_CHOICES`` method will return the host with more active requests 25% of the time.
46+
// If the request rate is sufficiently low, the behavior of always selecting the host with least
47+
// requests as of the last metrics refresh may be preferable.
48+
FULL_SCAN = 1;
49+
}
50+
2751
// The number of random healthy hosts from which the host with the fewest active requests will
2852
// be chosen. Defaults to 2 so that we perform two-choice selection if the field is not set.
53+
// Only applies to the ``N_CHOICES`` selection method.
2954
google.protobuf.UInt32Value choice_count = 1 [(validate.rules).uint32 = {gte: 2}];
3055

3156
// The following formula is used to calculate the dynamic weights when hosts have different load
@@ -61,8 +86,12 @@ message LeastRequest {
6186
common.v3.LocalityLbConfig locality_lb_config = 4;
6287

6388
// [#not-implemented-hide:]
64-
// Configuration for performing full scan on the list of hosts.
65-
// If this configuration is set, when selecting the host a full scan on the list hosts will be
66-
// used to select the one with least requests instead of using random choices.
67-
google.protobuf.BoolValue enable_full_scan = 5;
89+
// Unused. Replaced by the `selection_method` enum for better extensibility.
90+
google.protobuf.BoolValue enable_full_scan = 5
91+
[deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"];
92+
93+
// Method for selecting the host set from which to return the host with the fewest active requests.
94+
//
95+
// Defaults to ``N_CHOICES``.
96+
SelectionMethod selection_method = 6 [(validate.rules).enum = {defined_only: true}];
6897
}

changelogs/current.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,12 @@ new_features:
122122
Added :ref:`uri_template<envoy_v3_api_field_config.rbac.v3.Permission.uri_template>` which uses existing
123123
:ref:`UriTemplateMatchConfig<envoy_v3_api_msg_extensions.path.match.uri_template.v3.UriTemplateMatchConfig>`
124124
to allow use of glob patterns for URI path matching in RBAC.
125+
- area: upstream
126+
change: |
127+
Added :ref:`selection_method <envoy_v3_api_msg_extensions.load_balancing_policies.least_request.v3.LeastRequest>`
128+
option to the least request load balancer. If set to ``FULL_SCAN``,
129+
Envoy will select the host with the fewest active requests from the entire host set rather than
130+
:ref:`choice_count <envoy_v3_api_msg_extensions.load_balancing_policies.least_request.v3.LeastRequest>`
131+
random choices.
125132
126133
deprecated:

source/common/upstream/load_balancer_impl.cc

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1298,19 +1298,80 @@ HostConstSharedPtr LeastRequestLoadBalancer::unweightedHostPick(const HostVector
12981298
const HostsSource&) {
12991299
HostSharedPtr candidate_host = nullptr;
13001300

1301+
switch (selection_method_) {
1302+
case envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest::FULL_SCAN:
1303+
candidate_host = unweightedHostPickFullScan(hosts_to_use);
1304+
break;
1305+
case envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest::N_CHOICES:
1306+
candidate_host = unweightedHostPickNChoices(hosts_to_use);
1307+
break;
1308+
default:
1309+
IS_ENVOY_BUG("unknown selection method specified for least request load balancer");
1310+
}
1311+
1312+
return candidate_host;
1313+
}
1314+
1315+
HostSharedPtr LeastRequestLoadBalancer::unweightedHostPickFullScan(const HostVector& hosts_to_use) {
1316+
HostSharedPtr candidate_host = nullptr;
1317+
1318+
size_t num_hosts_known_tied_for_least = 0;
1319+
1320+
const size_t num_hosts = hosts_to_use.size();
1321+
1322+
for (size_t i = 0; i < num_hosts; ++i) {
1323+
const HostSharedPtr& sampled_host = hosts_to_use[i];
1324+
1325+
if (candidate_host == nullptr) {
1326+
// Make a first choice to start the comparisons.
1327+
num_hosts_known_tied_for_least = 1;
1328+
candidate_host = sampled_host;
1329+
continue;
1330+
}
1331+
1332+
const auto candidate_active_rq = candidate_host->stats().rq_active_.value();
1333+
const auto sampled_active_rq = sampled_host->stats().rq_active_.value();
1334+
1335+
if (sampled_active_rq < candidate_active_rq) {
1336+
// Reset the count of known tied hosts.
1337+
num_hosts_known_tied_for_least = 1;
1338+
candidate_host = sampled_host;
1339+
} else if (sampled_active_rq == candidate_active_rq) {
1340+
++num_hosts_known_tied_for_least;
1341+
1342+
// Use reservoir sampling to select 1 unique sample from the total number of hosts N
1343+
// that will tie for least requests after processing the full hosts array.
1344+
//
1345+
// Upon each new tie encountered, replace candidate_host with sampled_host
1346+
// with probability (1 / num_hosts_known_tied_for_least percent).
1347+
// The end result is that each tied host has an equal 1 / N chance of being the
1348+
// candidate_host returned by this function.
1349+
const size_t random_tied_host_index = random_.random() % num_hosts_known_tied_for_least;
1350+
if (random_tied_host_index == 0) {
1351+
candidate_host = sampled_host;
1352+
}
1353+
}
1354+
}
1355+
1356+
return candidate_host;
1357+
}
1358+
1359+
HostSharedPtr LeastRequestLoadBalancer::unweightedHostPickNChoices(const HostVector& hosts_to_use) {
1360+
HostSharedPtr candidate_host = nullptr;
1361+
13011362
for (uint32_t choice_idx = 0; choice_idx < choice_count_; ++choice_idx) {
13021363
const int rand_idx = random_.random() % hosts_to_use.size();
13031364
const HostSharedPtr& sampled_host = hosts_to_use[rand_idx];
13041365

13051366
if (candidate_host == nullptr) {
1306-
13071367
// Make a first choice to start the comparisons.
13081368
candidate_host = sampled_host;
13091369
continue;
13101370
}
13111371

13121372
const auto candidate_active_rq = candidate_host->stats().rq_active_.value();
13131373
const auto sampled_active_rq = sampled_host->stats().rq_active_.value();
1374+
13141375
if (sampled_active_rq < candidate_active_rq) {
13151376
candidate_host = sampled_host;
13161377
}

source/common/upstream/load_balancer_impl.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,8 @@ class LeastRequestLoadBalancer : public EdfLoadBalancerBase {
710710
least_request_config.has_active_request_bias()
711711
? absl::optional<Runtime::Double>(
712712
{least_request_config.active_request_bias(), runtime})
713-
: absl::nullopt) {
713+
: absl::nullopt),
714+
selection_method_(least_request_config.selection_method()) {
714715
initialize();
715716
}
716717

@@ -737,6 +738,8 @@ class LeastRequestLoadBalancer : public EdfLoadBalancerBase {
737738
const HostsSource& source) override;
738739
HostConstSharedPtr unweightedHostPick(const HostVector& hosts_to_use,
739740
const HostsSource& source) override;
741+
HostSharedPtr unweightedHostPickFullScan(const HostVector& hosts_to_use);
742+
HostSharedPtr unweightedHostPickNChoices(const HostVector& hosts_to_use);
740743

741744
const uint32_t choice_count_;
742745

@@ -746,6 +749,8 @@ class LeastRequestLoadBalancer : public EdfLoadBalancerBase {
746749
double active_request_bias_{};
747750

748751
const absl::optional<Runtime::Double> active_request_bias_runtime_;
752+
const envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest::SelectionMethod
753+
selection_method_{};
749754
};
750755

751756
/**

test/common/upstream/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ envoy_cc_test(
268268
srcs = ["load_balancer_impl_test.cc"],
269269
deps = [
270270
":utility_lib",
271+
"//source/common/common:random_generator_lib",
271272
"//source/common/network:utility_lib",
272273
"//source/common/upstream:load_balancer_lib",
273274
"//source/common/upstream:upstream_includes",

test/common/upstream/load_balancer_impl_test.cc

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "envoy/config/core/v3/base.pb.h"
1212
#include "envoy/config/core/v3/health_check.pb.h"
1313

14+
#include "source/common/common/random_generator.h"
1415
#include "source/common/network/utility.h"
1516
#include "source/common/upstream/load_balancer_impl.h"
1617
#include "source/common/upstream/upstream_impl.h"
@@ -2880,6 +2881,96 @@ TEST_P(LeastRequestLoadBalancerTest, PNC) {
28802881
EXPECT_EQ(hostSet().healthy_hosts_[3], lb_5.chooseHost(nullptr));
28812882
}
28822883

2884+
TEST_P(LeastRequestLoadBalancerTest, DefaultSelectionMethod) {
2885+
envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest lr_lb_config;
2886+
EXPECT_EQ(lr_lb_config.selection_method(),
2887+
envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest::N_CHOICES);
2888+
}
2889+
2890+
TEST_P(LeastRequestLoadBalancerTest, FullScanOneHostWithLeastRequests) {
2891+
hostSet().healthy_hosts_ = {makeTestHost(info_, "tcp://127.0.0.1:80", simTime()),
2892+
makeTestHost(info_, "tcp://127.0.0.1:81", simTime()),
2893+
makeTestHost(info_, "tcp://127.0.0.1:82", simTime()),
2894+
makeTestHost(info_, "tcp://127.0.0.1:83", simTime()),
2895+
makeTestHost(info_, "tcp://127.0.0.1:84", simTime())};
2896+
hostSet().hosts_ = hostSet().healthy_hosts_;
2897+
hostSet().runCallbacks({}, {}); // Trigger callbacks. The added/removed lists are not relevant.
2898+
2899+
hostSet().healthy_hosts_[0]->stats().rq_active_.set(4);
2900+
hostSet().healthy_hosts_[1]->stats().rq_active_.set(3);
2901+
hostSet().healthy_hosts_[2]->stats().rq_active_.set(2);
2902+
hostSet().healthy_hosts_[3]->stats().rq_active_.set(1);
2903+
hostSet().healthy_hosts_[4]->stats().rq_active_.set(5);
2904+
2905+
envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest lr_lb_config;
2906+
2907+
// Enable FULL_SCAN on hosts.
2908+
lr_lb_config.set_selection_method(
2909+
envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest::FULL_SCAN);
2910+
2911+
LeastRequestLoadBalancer lb{priority_set_, nullptr, stats_, runtime_,
2912+
random_, 1, lr_lb_config, simTime()};
2913+
2914+
// With FULL_SCAN we will always choose the host with least number of active requests.
2915+
EXPECT_EQ(hostSet().healthy_hosts_[3], lb.chooseHost(nullptr));
2916+
}
2917+
2918+
TEST_P(LeastRequestLoadBalancerTest, FullScanMultipleHostsWithLeastRequests) {
2919+
hostSet().healthy_hosts_ = {makeTestHost(info_, "tcp://127.0.0.1:80", simTime()),
2920+
makeTestHost(info_, "tcp://127.0.0.1:81", simTime()),
2921+
makeTestHost(info_, "tcp://127.0.0.1:82", simTime()),
2922+
makeTestHost(info_, "tcp://127.0.0.1:83", simTime()),
2923+
makeTestHost(info_, "tcp://127.0.0.1:84", simTime())};
2924+
hostSet().hosts_ = hostSet().healthy_hosts_;
2925+
hostSet().runCallbacks({}, {}); // Trigger callbacks. The added/removed lists are not relevant.
2926+
2927+
hostSet().healthy_hosts_[0]->stats().rq_active_.set(3);
2928+
hostSet().healthy_hosts_[1]->stats().rq_active_.set(3);
2929+
hostSet().healthy_hosts_[2]->stats().rq_active_.set(1);
2930+
hostSet().healthy_hosts_[3]->stats().rq_active_.set(1);
2931+
hostSet().healthy_hosts_[4]->stats().rq_active_.set(1);
2932+
2933+
envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest lr_lb_config;
2934+
2935+
// Enable FULL_SCAN on hosts.
2936+
lr_lb_config.set_selection_method(
2937+
envoy::extensions::load_balancing_policies::least_request::v3::LeastRequest::FULL_SCAN);
2938+
2939+
auto random = Random::RandomGeneratorImpl();
2940+
2941+
LeastRequestLoadBalancer lb{priority_set_, nullptr, stats_, runtime_,
2942+
random, 1, lr_lb_config, simTime()};
2943+
2944+
// Make 1 million selections. Then, check that the selection probability is
2945+
// approximately equal among the 3 hosts tied for least requests.
2946+
// Accept a +/-0.5% deviation from the expected selection probability (33.3..%).
2947+
size_t num_selections = 1000000;
2948+
size_t expected_approx_selections_per_tied_host = num_selections / 3;
2949+
size_t abs_error = 5000;
2950+
2951+
size_t host_2_counts = 0;
2952+
size_t host_3_counts = 0;
2953+
size_t host_4_counts = 0;
2954+
2955+
for (size_t i = 0; i < num_selections; ++i) {
2956+
auto selected_host = lb.chooseHost(nullptr);
2957+
2958+
if (selected_host == hostSet().healthy_hosts_[2]) {
2959+
++host_2_counts;
2960+
} else if (selected_host == hostSet().healthy_hosts_[3]) {
2961+
++host_3_counts;
2962+
} else if (selected_host == hostSet().healthy_hosts_[4]) {
2963+
++host_4_counts;
2964+
} else {
2965+
FAIL() << "Must only select hosts with least requests";
2966+
}
2967+
}
2968+
2969+
EXPECT_NEAR(expected_approx_selections_per_tied_host, host_2_counts, abs_error);
2970+
EXPECT_NEAR(expected_approx_selections_per_tied_host, host_3_counts, abs_error);
2971+
EXPECT_NEAR(expected_approx_selections_per_tied_host, host_4_counts, abs_error);
2972+
}
2973+
28832974
TEST_P(LeastRequestLoadBalancerTest, WeightImbalance) {
28842975
hostSet().healthy_hosts_ = {makeTestHost(info_, "tcp://127.0.0.1:80", simTime(), 1),
28852976
makeTestHost(info_, "tcp://127.0.0.1:81", simTime(), 2)};

tools/spelling/spelling_dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@ exe
747747
execlp
748748
exprfor
749749
expectable
750+
extensibility
750751
extrahelp
751752
faceplant
752753
facto

0 commit comments

Comments
 (0)