blob: e3098b9e9d172682f965bd7a56c20ebead7d7f23 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <map>
#include <sstream>
#include <vector>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include "butil/macros.h"
#include "brpc/socket.h"
#include "brpc/excluded_servers.h"
#include "brpc/policy/consistent_hashing_load_balancer.h"
#include "brpc/policy/hasher.h"
namespace {
brpc::ServerId CreateServer(const char* addr, const char* tag = "") {
butil::EndPoint point;
EXPECT_EQ(0, str2endpoint(addr, &point));
brpc::ServerId id(8888);
brpc::SocketOptions options;
options.remote_side = point;
EXPECT_EQ(0, brpc::Socket::Create(options, &id.id));
id.tag = tag;
return id;
}
void DestroyServers(const std::vector<brpc::ServerId>& ids) {
for (size_t i = 0; i < ids.size(); ++i) {
brpc::Socket::SetFailed(ids[i].id);
}
}
brpc::LoadBalancer::SelectIn MakeInput(uint64_t code,
bool changable_weights = true) {
brpc::LoadBalancer::SelectIn in = { 0, changable_weights, true, code, nullptr };
return in;
}
int64_t TotalInflightOf(brpc::LoadBalancer* lb) {
std::ostringstream os;
brpc::DescribeOptions opt;
opt.verbose = true;
lb->Describe(os, opt);
const std::string desc = os.str();
const size_t pos = desc.find("total_inflight=");
EXPECT_NE(std::string::npos, pos) << desc;
return strtoll(desc.c_str() + pos + strlen("total_inflight="), nullptr, 10);
}
class CHBoundedLoadTest : public testing::Test {};
TEST_F(CHBoundedLoadTest, load_factor_validation) {
brpc::policy::ConsistentHashingBoundedLoadBalancer lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
ASSERT_EQ(nullptr, lb.New("load_factor=1.0"));
ASSERT_EQ(nullptr, lb.New("load_factor=0.5"));
ASSERT_EQ(nullptr, lb.New("load_factor=abc"));
brpc::LoadBalancer* valid = lb.New("load_factor=1.5");
ASSERT_TRUE(valid != nullptr);
valid->Destroy();
ASSERT_EQ("", GFLAGS_NAMESPACE::SetCommandLineOption(
"chash_bounded_load_factor", "0.9"));
ASSERT_EQ("", GFLAGS_NAMESPACE::SetCommandLineOption(
"chash_bounded_load_factor", "1.0"));
ASSERT_NE("", GFLAGS_NAMESPACE::SetCommandLineOption(
"chash_bounded_load_factor", "1.25"));
}
TEST_F(CHBoundedLoadTest, replicas_parameter) {
brpc::policy::ConsistentHashingLoadBalancer classic_lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
brpc::policy::ConsistentHashingBoundedLoadBalancer bounded_lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
const brpc::LoadBalancer* lbs[] = { &classic_lb, &bounded_lb };
for (size_t i = 0; i < arraysize(lbs); ++i) {
ASSERT_EQ(nullptr, lbs[i]->New("replicas=abc"));
brpc::LoadBalancer* lb = lbs[i]->New("replicas=300");
ASSERT_TRUE(lb != nullptr);
std::ostringstream os;
brpc::DescribeOptions opt;
opt.verbose = true;
lb->Describe(os, opt);
ASSERT_NE(std::string::npos,
os.str().find("replica per host: 300")) << os.str();
lb->Destroy();
}
brpc::LoadBalancer* lb = bounded_lb.New("replicas=200 load_factor=2");
ASSERT_TRUE(lb != nullptr);
lb->Destroy();
}
TEST_F(CHBoundedLoadTest, hot_key_is_capped) {
const size_t N = 8;
const size_t K = 200;
const double FACTOR = 1.25;
std::vector<brpc::ServerId> ids;
brpc::policy::ConsistentHashingBoundedLoadBalancer lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
brpc::policy::ConsistentHashingLoadBalancer classic_lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
for (size_t i = 0; i < N; ++i) {
char addr[32];
snprintf(addr, sizeof(addr), "192.168.1.%d:8080", (int)i);
ids.push_back(CreateServer(addr));
}
ASSERT_EQ(N, lb.AddServersInBatch(ids));
ASSERT_EQ(N, classic_lb.AddServersInBatch(ids));
const std::string hot_key = "hot_key";
brpc::LoadBalancer::SelectIn in =
MakeInput(brpc::policy::MurmurHash32(hot_key.data(), hot_key.size()));
// Classic CH sends every request for the key to one server.
std::map<brpc::SocketId, size_t> classic_counts;
for (size_t i = 0; i < K; ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, classic_lb.SelectServer(in, &out));
++classic_counts[ptr->id()];
}
ASSERT_EQ(1UL, classic_counts.size());
const brpc::SocketId primary = classic_counts.begin()->first;
// Bounded-load CH spreads the same hot key once the primary server hits
// its capacity: no server exceeds ceil(FACTOR * K / N) outstanding
// requests(the cap of the last selection, when total load is largest).
std::map<brpc::SocketId, size_t> counts;
for (size_t i = 0; i < K; ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
ASSERT_TRUE(out.need_feedback);
++counts[ptr->id()];
}
const size_t cap = (size_t)std::ceil(FACTOR * K / N);
size_t max_count = 0;
for (std::map<brpc::SocketId, size_t>::iterator it = counts.begin();
it != counts.end(); ++it) {
max_count = std::max(max_count, it->second);
}
ASSERT_LE(max_count, cap);
ASSERT_GT(counts.size(), 1UL);
// The first selection matches classic CH: locality is unchanged until
// the primary server is at capacity.
ASSERT_GT(counts[primary], 0UL);
ASSERT_EQ((int64_t)K, TotalInflightOf(&lb));
DestroyServers(ids);
}
TEST_F(CHBoundedLoadTest, overflow_walks_to_ring_successor) {
const size_t N = 8;
const size_t K = 200;
std::vector<brpc::ServerId> ids;
brpc::policy::ConsistentHashingBoundedLoadBalancer lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
brpc::policy::ConsistentHashingLoadBalancer classic_lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
std::map<brpc::SocketId, brpc::ServerId> id_map;
for (size_t i = 0; i < N; ++i) {
char addr[32];
snprintf(addr, sizeof(addr), "192.168.1.%d:8080", (int)i);
ids.push_back(CreateServer(addr));
id_map[ids.back().id] = ids.back();
}
ASSERT_EQ(N, lb.AddServersInBatch(ids));
ASSERT_EQ(N, classic_lb.AddServersInBatch(ids));
const std::string hot_key = "another_hot_key";
brpc::LoadBalancer::SelectIn in =
MakeInput(brpc::policy::MurmurHash32(hot_key.data(), hot_key.size()));
// First-use order of servers under overflow.
std::vector<brpc::SocketId> bounded_order;
std::map<brpc::SocketId, size_t> seen;
for (size_t i = 0; i < K; ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
if (++seen[ptr->id()] == 1) {
bounded_order.push_back(ptr->id());
}
}
ASSERT_GT(bounded_order.size(), 1UL);
// Expected overflow targets are the ring successors: what classic CH
// picks as servers are removed one by one.
for (size_t i = 0; i < bounded_order.size(); ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, classic_lb.SelectServer(in, &out));
ASSERT_EQ(bounded_order[i], ptr->id()) << "i=" << i;
ASSERT_TRUE(classic_lb.RemoveServer(id_map[ptr->id()]));
}
DestroyServers(ids);
}
TEST_F(CHBoundedLoadTest, feedback_decrements_load) {
const size_t N = 3;
std::vector<brpc::ServerId> ids;
brpc::policy::ConsistentHashingBoundedLoadBalancer lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
for (size_t i = 0; i < N; ++i) {
char addr[32];
snprintf(addr, sizeof(addr), "192.168.1.%d:8080", (int)i);
ids.push_back(CreateServer(addr));
}
ASSERT_EQ(N, lb.AddServersInBatch(ids));
const std::string key = "some_key";
brpc::LoadBalancer::SelectIn in =
MakeInput(brpc::policy::MurmurHash32(key.data(), key.size()));
// With capacity ceil(1.25 * 1 / 3) = 1 an idle ring always routes the
// key to its primary server, so select+feedback staying on one server
// for many rounds proves the counters return to zero every round.
brpc::SocketId primary = 0;
for (size_t i = 0; i < 100; ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
ASSERT_TRUE(out.need_feedback);
if (i == 0) {
primary = ptr->id();
} else {
ASSERT_EQ(primary, ptr->id()) << "i=" << i;
}
const brpc::LoadBalancer::CallInfo info = { 0, ptr->id(), 0, nullptr };
lb.Feedback(info);
}
ASSERT_EQ(0, TotalInflightOf(&lb));
// Saturate the primary without feedback: the key overflows...
std::vector<brpc::SocketId> outstanding;
bool overflowed = false;
for (size_t i = 0; i < 20; ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
outstanding.push_back(ptr->id());
overflowed |= (ptr->id() != primary);
}
ASSERT_TRUE(overflowed);
// ...and returns to the primary once the load drains.
for (size_t i = 0; i < outstanding.size(); ++i) {
const brpc::LoadBalancer::CallInfo info = { 0, outstanding[i], 0, nullptr };
lb.Feedback(info);
}
ASSERT_EQ(0, TotalInflightOf(&lb));
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
ASSERT_EQ(primary, ptr->id());
const brpc::LoadBalancer::CallInfo info = { 0, ptr->id(), 0, nullptr };
lb.Feedback(info);
DestroyServers(ids);
}
TEST_F(CHBoundedLoadTest, feedback_after_server_removed_keeps_total_consistent) {
const size_t N = 3;
std::vector<brpc::ServerId> ids;
brpc::policy::ConsistentHashingBoundedLoadBalancer lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
for (size_t i = 0; i < N; ++i) {
char addr[32];
snprintf(addr, sizeof(addr), "192.168.1.%d:8080", (int)i);
ids.push_back(CreateServer(addr));
}
ASSERT_EQ(N, lb.AddServersInBatch(ids));
const std::string key = "some_key";
brpc::LoadBalancer::SelectIn in =
MakeInput(brpc::policy::MurmurHash32(key.data(), key.size()));
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
ASSERT_EQ(1, TotalInflightOf(&lb));
brpc::ServerId selected;
for (size_t i = 0; i < ids.size(); ++i) {
if (ids[i].id == ptr->id()) {
selected = ids[i];
}
}
ASSERT_TRUE(lb.RemoveServer(selected));
const brpc::LoadBalancer::CallInfo info = { 0, ptr->id(), 0, nullptr };
lb.Feedback(info);
ASSERT_EQ(0, TotalInflightOf(&lb));
ASSERT_TRUE(lb.AddServer(selected));
DestroyServers(ids);
}
TEST_F(CHBoundedLoadTest, no_accounting_without_changable_weights) {
const size_t N = 4;
std::vector<brpc::ServerId> ids;
brpc::policy::ConsistentHashingBoundedLoadBalancer lb(
brpc::policy::CONS_HASH_LB_MURMUR3);
for (size_t i = 0; i < N; ++i) {
char addr[32];
snprintf(addr, sizeof(addr), "192.168.1.%d:8080", (int)i);
ids.push_back(CreateServer(addr));
}
ASSERT_EQ(N, lb.AddServersInBatch(ids));
const std::string key = "some_key";
brpc::LoadBalancer::SelectIn in = MakeInput(
brpc::policy::MurmurHash32(key.data(), key.size()), false);
brpc::SocketId first = 0;
for (size_t i = 0; i < 50; ++i) {
brpc::SocketUniquePtr ptr;
brpc::LoadBalancer::SelectOut out(&ptr);
ASSERT_EQ(0, lb.SelectServer(in, &out));
ASSERT_FALSE(out.need_feedback);
if (i == 0) {
first = ptr->id();
} else {
ASSERT_EQ(first, ptr->id());
}
}
ASSERT_EQ(0, TotalInflightOf(&lb));
DestroyServers(ids);
}
} // namespace