Skip to content
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

Zone aware routing #91

Merged
merged 14 commits into from
Sep 26, 2016
13 changes: 13 additions & 0 deletions docs/configuration/cluster_manager/cluster_runtime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,16 @@ upstream.use_http2
upstream.weight_enabled
Binary switch to turn on or off weighted load balancing. If set to non 0, weighted load balancing
is enabled. Defaults to enabled.

upstream.zone_routing.enabled
% of requests that will be routed to the same upstream zone. Defaults to 100% of requests.

upstream.zone_routing.percent_diff
Zone aware routing will be used only if the percent of upstream hosts in the same zone is within
percent_diff of expected. Expected is calculated as 100 / number_of_zones. This prevents Envoy
from using same zone routing if the zones are not balanced well. Defaults to 3% allowed deviation.

upstream.zone_routing.healthy_panic_threshold
Defines the :ref:`zone healthy panic threshold <arch_overview_load_balancing_zone_panic_threshold>`
percentage. Defaults to 80%. If the % of healthy hosts in the current zone falls below this %
all healthy hosts will be used for routing.
10 changes: 10 additions & 0 deletions docs/intro/arch_overview/load_balancing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,13 @@ health status and balance amongst all hosts. This is known as the *panic thresho
panic threshold is 50%. This is :ref:`configurable <config_cluster_manager_cluster_runtime>` via
runtime. The panic threshold is used to avoid a situation in which host failures cascade throughout
the cluster as load increases.

.. _arch_overview_load_balancing_zone_panic_threshold:

Zone aware routing and local zone panic threshold
-------------------------------------------------

By default Envoy performs zone aware routing where it will send traffic to the same upstream zone.
This is only done if the zones are well balanced (defaults to 3% allowed deviation) and if there
are enough healthy hosts in the local zone (the *panic threshold* which defaults to 80%). These are
:ref:`configurable <config_cluster_manager_cluster_runtime>` via runtime.
6 changes: 5 additions & 1 deletion include/envoy/upstream/upstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ class HostSet {
COUNTER(update_attempt) \
COUNTER(update_success) \
COUNTER(update_failure) \
GAUGE (max_host_weight)
GAUGE (max_host_weight) \
GAUGE (upstream_zone_count) \
COUNTER(upstream_zone_above_threshold) \
COUNTER(upstream_zone_healthy_panic) \
COUNTER(upstream_zone_within_threshold)
// clang-format on

/**
Expand Down
41 changes: 36 additions & 5 deletions source/common/upstream/load_balancer_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,49 @@ const std::vector<HostPtr>& LoadBalancerBase::hostsToUse() {
return host_set_.hosts();
}

uint64_t threshold =
uint64_t global_panic_threshold =
std::min(100UL, runtime_.snapshot().getInteger("upstream.healthy_panic_threshold", 50));
double healthy_percent =
(host_set_.healthyHosts().size() / static_cast<double>(host_set_.hosts().size())) * 100;
double healthy_percent = 100.0 * host_set_.healthyHosts().size() / host_set_.hosts().size();

// If the % of healthy hosts in the cluster is less than our panic threshold, we use all hosts.
if (healthy_percent < threshold) {
if (healthy_percent < global_panic_threshold) {
stats_.upstream_rq_lb_healthy_panic_.inc();
return host_set_.hosts();
} else {
}

// Early exit if we cannot perform zone aware routing.
if (stats_.upstream_zone_count_.value() < 2 || host_set_.localZoneHealthyHosts().empty() ||
!runtime_.snapshot().featureEnabled("upstream.zone_routing.enabled", 100)) {
return host_set_.healthyHosts();
}

double zone_to_all_percent =
100.0 * host_set_.localZoneHealthyHosts().size() / host_set_.healthyHosts().size();
double expected_percent = 100.0 / stats_.upstream_zone_count_.value();

uint64_t zone_percent_diff =
runtime_.snapshot().getInteger("upstream.zone_routing.percent_diff", 3);

// Hosts should be roughly equally distributed between zones.
if (std::abs(zone_to_all_percent - expected_percent) > zone_percent_diff) {
stats_.upstream_zone_above_threshold_.inc();

return host_set_.healthyHosts();
}

stats_.upstream_zone_within_threshold_.inc();

uint64_t zone_panic_threshold =
runtime_.snapshot().getInteger("upstream.zone_routing.healthy_panic_threshold", 80);
double zone_healthy_percent =
100.0 * host_set_.localZoneHealthyHosts().size() / host_set_.localZoneHosts().size();
if (zone_healthy_percent < zone_panic_threshold) {
stats_.upstream_zone_healthy_panic_.inc();

return host_set_.healthyHosts();
}

return host_set_.localZoneHealthyHosts();
}

ConstHostPtr RoundRobinLoadBalancer::chooseHost() {
Expand Down
5 changes: 5 additions & 0 deletions source/common/upstream/upstream_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const std::vector<HostPtr>& n
std::vector<HostPtr>& hosts_removed,
bool depend_on_hc) {
uint64_t max_host_weight = 1;
std::unordered_set<std::string> zones;

// Go through and see if the list we have is different from what we just got. If it is, we
// make a new host list and raise a change notification. This uses an N^2 search given that
Expand All @@ -188,6 +189,7 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const std::vector<HostPtr>& n
// If we find a host matched based on URL, we keep it. However we do change weight inline so
// do that here.
if ((*i)->url() == host->url()) {
zones.insert((*i)->zone());
if (host->weight() > max_host_weight) {
max_host_weight = host->weight();
}
Expand All @@ -205,6 +207,7 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const std::vector<HostPtr>& n
if (host->weight() > max_host_weight) {
max_host_weight = host->weight();
}
zones.insert(host->zone());

final_hosts.push_back(host);
hosts_added.push_back(host);
Expand All @@ -221,6 +224,7 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const std::vector<HostPtr>& n
if ((*i)->weight() > max_host_weight) {
max_host_weight = (*i)->weight();
}
zones.insert((*i)->zone());

final_hosts.push_back(*i);
i = current_hosts.erase(i);
Expand All @@ -231,6 +235,7 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const std::vector<HostPtr>& n
}

stats_.max_host_weight_.set(max_host_weight);
stats_.upstream_zone_count_.set(zones.size());

if (!hosts_added.empty() || !current_hosts.empty()) {
hosts_removed = std::move(current_hosts);
Expand Down
108 changes: 107 additions & 1 deletion test/common/upstream/load_balancer_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,112 @@ TEST_F(RoundRobinLoadBalancerTest, MaxUnhealthyPanic) {
EXPECT_EQ(3UL, stats_.upstream_rq_lb_healthy_panic_.value());
}

TEST_F(RoundRobinLoadBalancerTest, ZoneAwareRoutingDone) {
cluster_.healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:82")};
cluster_.hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:82")};
cluster_.local_zone_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81")};
cluster_.local_zone_healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81")};
stats_.upstream_zone_count_.set(3UL);

EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.zone_routing.enabled", 100))
.WillRepeatedly(Return(true));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.healthy_panic_threshold", 80))
.WillRepeatedly(Return(80));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.percent_diff", 3))
.WillRepeatedly(Return(2));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.healthy_panic_threshold", 50))
.WillRepeatedly(Return(50));

// There is only one host in the given zone for zone aware routing.
EXPECT_EQ(cluster_.local_zone_healthy_hosts_[0], lb_.chooseHost());
EXPECT_EQ(1UL, stats_.upstream_zone_within_threshold_.value());

EXPECT_EQ(cluster_.local_zone_healthy_hosts_[0], lb_.chooseHost());
EXPECT_EQ(2UL, stats_.upstream_zone_within_threshold_.value());

// Disable runtime global zone routing.
EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.zone_routing.enabled", 100))
.WillRepeatedly(Return(false));
EXPECT_EQ(cluster_.healthy_hosts_[2], lb_.chooseHost());
EXPECT_EQ(2UL, stats_.upstream_zone_within_threshold_.value());
}

TEST_F(RoundRobinLoadBalancerTest, NoZoneAwareRoutingOneZone) {
cluster_.healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80")};
cluster_.hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80")};
cluster_.local_zone_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80")};
cluster_.local_zone_healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80")};
stats_.upstream_zone_count_.set(1UL);

EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.zone_routing.enabled", 100)).Times(0);
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.healthy_panic_threshold", 80))
.Times(0);
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.percent_diff", 3)).Times(0);
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.healthy_panic_threshold", 50))
.WillRepeatedly(Return(50));

EXPECT_EQ(cluster_.healthy_hosts_[0], lb_.chooseHost());
EXPECT_EQ(0UL, stats_.upstream_zone_within_threshold_.value());
EXPECT_EQ(0UL, stats_.upstream_zone_above_threshold_.value());
}

TEST_F(RoundRobinLoadBalancerTest, ZoneAwareRoutingNotHealthy) {
cluster_.healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:82")};
cluster_.hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:82")};
cluster_.local_zone_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81")};
cluster_.local_zone_healthy_hosts_ = {};
stats_.upstream_zone_count_.set(3UL);

EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.zone_routing.enabled", 100))
.WillRepeatedly(Return(true));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.healthy_panic_threshold", 50))
.WillRepeatedly(Return(50));

// Should not be called due to early exit.
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.healthy_panic_threshold", 80))
.Times(0);
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.percent_diff", 3)).Times(0);

// local zone has no healthy hosts, take from the all healthy hosts.
EXPECT_EQ(cluster_.healthy_hosts_[0], lb_.chooseHost());
EXPECT_EQ(cluster_.healthy_hosts_[1], lb_.chooseHost());
}

TEST_F(RoundRobinLoadBalancerTest, ZoneAwareRoutingNotEnoughHealthy) {
cluster_.healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:82")};
cluster_.hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:80"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81"),
newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:82")};
cluster_.local_zone_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81")};
cluster_.local_zone_healthy_hosts_ = {newTestHost(Upstream::MockCluster{}, "tcp://127.0.0.1:81")};
stats_.upstream_zone_count_.set(2UL);

EXPECT_CALL(runtime_.snapshot_, featureEnabled("upstream.zone_routing.enabled", 100))
.WillRepeatedly(Return(true));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.healthy_panic_threshold", 50))
.WillRepeatedly(Return(50));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.healthy_panic_threshold", 80))
.WillRepeatedly(Return(80));
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.zone_routing.percent_diff", 3))
.WillRepeatedly(Return(3));

// Not enough healthy hosts in local zone.
EXPECT_EQ(cluster_.healthy_hosts_[0], lb_.chooseHost());
EXPECT_EQ(1UL, stats_.upstream_zone_above_threshold_.value());
EXPECT_EQ(cluster_.healthy_hosts_[1], lb_.chooseHost());
EXPECT_EQ(2UL, stats_.upstream_zone_above_threshold_.value());
}

class LeastRequestLoadBalancerTest : public testing::Test {
public:
LeastRequestLoadBalancerTest() : stats_(ClusterImplBase::generateStats("", stats_store_)) {}
Expand Down Expand Up @@ -135,7 +241,7 @@ TEST_F(LeastRequestLoadBalancerTest, Normal) {
EXPECT_EQ(cluster_.healthy_hosts_[1], lb_.chooseHost());
}

TEST_F(LeastRequestLoadBalancerTest, WeightImbalanceRuntimOff) {
TEST_F(LeastRequestLoadBalancerTest, WeightImbalanceRuntimeOff) {
// Disable weight balancing.
EXPECT_CALL(runtime_.snapshot_, getInteger("upstream.weight_enabled", 1))
.WillRepeatedly(Return(0));
Expand Down
4 changes: 4 additions & 0 deletions test/common/upstream/sds_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ TEST_F(SdsTest, NoHealthChecker) {
EXPECT_EQ("us-east-1d", canary_host->zone());
EXPECT_EQ(1U, canary_host->weight());
EXPECT_EQ(1UL, cluster_->stats().max_host_weight_.value());
EXPECT_EQ(3UL, cluster_->stats().upstream_zone_count_.value());

// Test response with weight change. We should still have the same host.
setupRequest();
Expand All @@ -147,6 +148,7 @@ TEST_F(SdsTest, NoHealthChecker) {
EXPECT_EQ("us-east-1d", canary_host->zone());
EXPECT_EQ(50U, canary_host->weight());
EXPECT_EQ(50UL, cluster_->stats().max_host_weight_.value());
EXPECT_EQ(3UL, cluster_->stats().upstream_zone_count_.value());

// Now test the failure case, our cluster size should not change.
setupRequest();
Expand All @@ -157,6 +159,7 @@ TEST_F(SdsTest, NoHealthChecker) {
EXPECT_EQ(13UL, cluster_->hosts().size());
EXPECT_EQ(50U, canary_host->weight());
EXPECT_EQ(50UL, cluster_->stats().max_host_weight_.value());
EXPECT_EQ(3UL, cluster_->stats().upstream_zone_count_.value());

// 503 response.
setupRequest();
Expand All @@ -169,6 +172,7 @@ TEST_F(SdsTest, NoHealthChecker) {
EXPECT_EQ(13UL, cluster_->hosts().size());
EXPECT_EQ(50U, canary_host->weight());
EXPECT_EQ(50UL, cluster_->stats().max_host_weight_.value());
EXPECT_EQ(3UL, cluster_->stats().upstream_zone_count_.value());
}

TEST_F(SdsTest, HealthChecker) {
Expand Down
2 changes: 2 additions & 0 deletions test/mocks/upstream/mocks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ MockCluster::MockCluster()
ON_CALL(*this, connectTimeout()).WillByDefault(Return(std::chrono::milliseconds(1)));
ON_CALL(*this, hosts()).WillByDefault(ReturnRef(hosts_));
ON_CALL(*this, healthyHosts()).WillByDefault(ReturnRef(healthy_hosts_));
ON_CALL(*this, localZoneHosts()).WillByDefault(ReturnRef(local_zone_hosts_));
ON_CALL(*this, localZoneHealthyHosts()).WillByDefault(ReturnRef(local_zone_healthy_hosts_));
ON_CALL(*this, name()).WillByDefault(ReturnRef(name_));
ON_CALL(*this, altStatName()).WillByDefault(ReturnRef(alt_stat_name_));
ON_CALL(*this, lbType()).WillByDefault(Return(Upstream::LoadBalancerType::RoundRobin));
Expand Down
2 changes: 2 additions & 0 deletions test/mocks/upstream/mocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class MockCluster : public Cluster {

std::vector<HostPtr> hosts_;
std::vector<HostPtr> healthy_hosts_;
std::vector<HostPtr> local_zone_hosts_;
std::vector<HostPtr> local_zone_healthy_hosts_;
std::string name_{"fake_cluster"};
std::string alt_stat_name_{"fake_alt_cluster"};
std::list<MemberUpdateCb> callbacks_;
Expand Down