| // 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 |