// 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 #include #include #include #include #include #include #include #include "butil/macros.h" #include "butil/time.h" #include "brpc/socket.h" #include "brpc/controller.h" #include "brpc/excluded_servers.h" #include "brpc/policy/p2c_ewma_load_balancer.h" namespace brpc { namespace policy { DECLARE_int64(p2c_max_punish_ms); } } namespace { butil::atomic nrecycle(0); class SaveRecycle : public brpc::SocketUser { void BeforeRecycle(brpc::Socket* s) { nrecycle.fetch_add(1, butil::memory_order_relaxed); delete this; } }; 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; options.user = new SaveRecycle; EXPECT_EQ(0, brpc::Socket::Create(options, &id.id)); id.tag = tag; return id; } // Report a call that took `latency_us' back to the load balancer. void FeedbackLatency(brpc::LoadBalancer* lb, brpc::SocketId server_id, int64_t latency_us, int error_code = 0, const brpc::Controller* cntl = NULL) { brpc::LoadBalancer::CallInfo info; info.begin_time_us = butil::gettimeofday_us() - latency_us; info.server_id = server_id; info.error_code = error_code; info.controller = cntl; lb->Feedback(info); } class P2CEwmaLoadBalancerTest : public ::testing::Test { protected: void SetUp() override { _lb = new brpc::policy::P2CEwmaLoadBalancer; } void TearDown() override { _lb->Destroy(); } int Select(brpc::SocketUniquePtr* ptr, bool* need_feedback = NULL, const brpc::ExcludedServers* excluded = NULL) { brpc::LoadBalancer::SelectIn in = { butil::gettimeofday_us(), true, false, 0u, excluded }; brpc::LoadBalancer::SelectOut out(ptr); const int rc = _lb->SelectServer(in, &out); if (need_feedback != NULL) { *need_feedback = out.need_feedback; } return rc; } brpc::policy::P2CEwmaLoadBalancer* _lb; }; TEST_F(P2CEwmaLoadBalancerTest, add_remove_servers) { brpc::SocketUniquePtr ptr; ASSERT_EQ(ENODATA, Select(&ptr)); std::vector ids; ids.push_back(CreateServer("127.0.0.1:7777")); ids.push_back(CreateServer("127.0.0.1:7778")); ids.push_back(CreateServer("127.0.0.1:7779")); ASSERT_TRUE(_lb->AddServer(ids[0])); // Duplicated server is rejected. ASSERT_FALSE(_lb->AddServer(ids[0])); ASSERT_EQ(2u, _lb->AddServersInBatch( std::vector(ids.begin() + 1, ids.end()))); ASSERT_EQ(0, Select(&ptr)); ASSERT_TRUE(_lb->RemoveServer(ids[0])); ASSERT_FALSE(_lb->RemoveServer(ids[0])); ASSERT_EQ(2u, _lb->RemoveServersInBatch( std::vector(ids.begin() + 1, ids.end()))); ASSERT_EQ(ENODATA, Select(&ptr)); } TEST_F(P2CEwmaLoadBalancerTest, single_server) { const brpc::ServerId id = CreateServer("127.0.0.1:7780"); ASSERT_TRUE(_lb->AddServer(id)); for (int i = 0; i < 10; ++i) { brpc::SocketUniquePtr ptr; bool need_feedback = false; ASSERT_EQ(0, Select(&ptr, &need_feedback)); ASSERT_EQ(id.id, ptr->id()); ASSERT_TRUE(need_feedback); FeedbackLatency(_lb, id.id, 1000); } } TEST_F(P2CEwmaLoadBalancerTest, prefers_lower_latency) { const brpc::ServerId fast = CreateServer("127.0.0.1:7781"); const brpc::ServerId slow = CreateServer("127.0.0.1:7782"); ASSERT_TRUE(_lb->AddServer(fast)); ASSERT_TRUE(_lb->AddServer(slow)); // Servers respond with their characteristic latency; both get observed // within the first rounds because unobserved servers score best. std::map counts; const int kRounds = 1000; for (int i = 0; i < kRounds; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr)); ++counts[ptr->id()]; // Selected server responds with its own characteristic latency. FeedbackLatency(_lb, ptr->id(), ptr->id() == fast.id ? 1000 : 50000); } LOG(INFO) << "fast=" << counts[fast.id] << " slow=" << counts[slow.id]; // The fast server should receive almost all traffic. ASSERT_GE(counts[fast.id], (size_t)(0.9 * kRounds)); } TEST_F(P2CEwmaLoadBalancerTest, sheds_degraded_server) { const brpc::ServerId a = CreateServer("127.0.0.1:7783"); const brpc::ServerId b = CreateServer("127.0.0.1:7784"); ASSERT_TRUE(_lb->AddServer(a)); ASSERT_TRUE(_lb->AddServer(b)); // Warm up with `b' slightly faster so it is the preferred server. for (int i = 0; i < 20; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr)); FeedbackLatency(_lb, ptr->id(), ptr->id() == a.id ? 1000 : 500); } // `b' degrades: its first slow response must shift traffic to `a' // immediately(peak-sensitivity), long before an averaging window would. std::map counts; for (int i = 0; i < 100; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr)); ++counts[ptr->id()]; FeedbackLatency(_lb, ptr->id(), ptr->id() == a.id ? 1000 : 100000); } ASSERT_GE(counts[a.id], 90u); } TEST_F(P2CEwmaLoadBalancerTest, inflight_breaks_ties) { const brpc::ServerId a = CreateServer("127.0.0.1:7785"); const brpc::ServerId b = CreateServer("127.0.0.1:7786"); ASSERT_TRUE(_lb->AddServer(a)); ASSERT_TRUE(_lb->AddServer(b)); // With no latency observations, scores only differ by in-flight count, // so two selections without feedback must go to different servers. brpc::SocketUniquePtr ptr1; brpc::SocketUniquePtr ptr2; ASSERT_EQ(0, Select(&ptr1)); ASSERT_EQ(0, Select(&ptr2)); ASSERT_NE(ptr1->id(), ptr2->id()); // After feedback(decrementing in-flight), both are tied again and the // third selection must not crash or fail. FeedbackLatency(_lb, ptr1->id(), 1000); FeedbackLatency(_lb, ptr2->id(), 1000); brpc::SocketUniquePtr ptr3; ASSERT_EQ(0, Select(&ptr3)); } TEST_F(P2CEwmaLoadBalancerTest, weighted_split_by_inflight) { // With equal latency, steady-state in-flight counts converge to the // 1:2:4 weight ratio because the score is (inflight+1)/weight. const brpc::ServerId w1 = CreateServer("127.0.0.1:7787", "1"); const brpc::ServerId w2 = CreateServer("127.0.0.1:7788", "2"); const brpc::ServerId w4 = CreateServer("127.0.0.1:7789", "4"); brpc::policy::P2CEwmaLoadBalancer* lb = _lb->New(butil::StringPiece("choices=3")); ASSERT_TRUE(lb != NULL); ASSERT_TRUE(lb->AddServer(w1)); ASSERT_TRUE(lb->AddServer(w2)); ASSERT_TRUE(lb->AddServer(w4)); std::map counts; const int kRounds = 700; for (int i = 0; i < kRounds; ++i) { brpc::SocketUniquePtr ptr; brpc::LoadBalancer::SelectIn in = { butil::gettimeofday_us(), true, false, 0u, NULL }; brpc::LoadBalancer::SelectOut out(&ptr); ASSERT_EQ(0, lb->SelectServer(in, &out)); ++counts[ptr->id()]; // No feedback: requests stay in flight. } LOG(INFO) << "w1=" << counts[w1.id] << " w2=" << counts[w2.id] << " w4=" << counts[w4.id]; const double share1 = counts[w1.id] / (double)kRounds; const double share2 = counts[w2.id] / (double)kRounds; const double share4 = counts[w4.id] / (double)kRounds; ASSERT_NEAR(share1, 1.0 / 7, 0.05); ASSERT_NEAR(share2, 2.0 / 7, 0.05); ASSERT_NEAR(share4, 4.0 / 7, 0.05); lb->Destroy(); } TEST_F(P2CEwmaLoadBalancerTest, error_feedback_punishes_server) { const brpc::ServerId good = CreateServer("127.0.0.1:7790"); const brpc::ServerId bad = CreateServer("127.0.0.1:7791"); ASSERT_TRUE(_lb->AddServer(good)); ASSERT_TRUE(_lb->AddServer(bad)); // Warm up with `bad' slightly faster so it is the preferred server. for (int i = 0; i < 20; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr)); FeedbackLatency(_lb, ptr->id(), ptr->id() == good.id ? 1000 : 500); } // `bad' starts failing fast(1ms), but failures are punished with at // least the RPC timeout so it keeps losing selections. brpc::Controller cntl; cntl.set_timeout_ms(100); std::map counts; for (int i = 0; i < 100; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr)); ++counts[ptr->id()]; if (ptr->id() == good.id) { FeedbackLatency(_lb, good.id, 1000); } else { FeedbackLatency(_lb, bad.id, 1000, ETIMEDOUT, &cntl); } } ASSERT_GE(counts[good.id], 90u); } TEST_F(P2CEwmaLoadBalancerTest, excluded_servers) { const brpc::ServerId a = CreateServer("127.0.0.1:7792"); const brpc::ServerId b = CreateServer("127.0.0.1:7793"); ASSERT_TRUE(_lb->AddServer(a)); ASSERT_TRUE(_lb->AddServer(b)); brpc::ExcludedServers* excluded = brpc::ExcludedServers::Create(2); excluded->Add(a.id); for (int i = 0; i < 20; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr, NULL, excluded)); ASSERT_EQ(b.id, ptr->id()); FeedbackLatency(_lb, b.id, 1000); } // All servers excluded: still take the last chance instead of failing. excluded->Add(b.id); brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr, NULL, excluded)); brpc::ExcludedServers::Destroy(excluded); } TEST_F(P2CEwmaLoadBalancerTest, invalid_parameters) { brpc::LoadBalancer* lb = _lb->New(butil::StringPiece("")); ASSERT_TRUE(lb != NULL); lb->Destroy(); lb = _lb->New(butil::StringPiece("choices=4 tau_ms=5000")); ASSERT_TRUE(lb != NULL); lb->Destroy(); ASSERT_TRUE(_lb->New(butil::StringPiece("choices=1")) == NULL); ASSERT_TRUE(_lb->New(butil::StringPiece("choices=abc")) == NULL); ASSERT_TRUE(_lb->New(butil::StringPiece("tau_ms=0")) == NULL); ASSERT_TRUE(_lb->New(butil::StringPiece("unknown=1")) == NULL); } struct ChurnArg { brpc::policy::P2CEwmaLoadBalancer* lb; butil::atomic stop; butil::atomic nselected; }; void* SelectAndFeedback(void* void_arg) { ChurnArg* arg = static_cast(void_arg); while (!arg->stop.load(butil::memory_order_relaxed)) { brpc::SocketUniquePtr ptr; brpc::LoadBalancer::SelectIn in = { butil::gettimeofday_us(), true, false, 0u, NULL }; brpc::LoadBalancer::SelectOut out(&ptr); if (arg->lb->SelectServer(in, &out) == 0) { arg->nselected.fetch_add(1, butil::memory_order_relaxed); if (out.need_feedback) { FeedbackLatency(arg->lb, ptr->id(), 1000); } } } return NULL; } TEST_F(P2CEwmaLoadBalancerTest, concurrent_select_with_churn) { std::vector ids; for (int i = 0; i < 8; ++i) { char addr[32]; snprintf(addr, sizeof(addr), "127.0.0.1:%d", 7800 + i); ids.push_back(CreateServer(addr)); ASSERT_TRUE(_lb->AddServer(ids.back())); } ChurnArg arg; arg.lb = _lb; arg.stop.store(false); arg.nselected.store(0); pthread_t threads[4]; for (size_t i = 0; i < ARRAY_SIZE(threads); ++i) { ASSERT_EQ(0, pthread_create( &threads[i], NULL, SelectAndFeedback, &arg)); } // Churn membership while selections are running. const int64_t stop_at_us = butil::gettimeofday_us() + 1000000L; while (butil::gettimeofday_us() < stop_at_us) { ASSERT_TRUE(_lb->RemoveServer(ids[0])); ASSERT_TRUE(_lb->AddServer(ids[0])); } arg.stop.store(true); for (size_t i = 0; i < ARRAY_SIZE(threads); ++i) { ASSERT_EQ(0, pthread_join(threads[i], NULL)); } LOG(INFO) << "selected " << arg.nselected.load() << " times"; ASSERT_GT(arg.nselected.load(), 0u); } TEST_F(P2CEwmaLoadBalancerTest, error_punish_is_capped) { const brpc::ServerId id = CreateServer("127.0.0.1:7900"); ASSERT_TRUE(_lb->AddServer(id)); brpc::Controller cntl; cntl.set_timeout_ms(100); // Persistent failures double the punished EWMA per sample; without the // cap 64 rounds would push it past 2^64. for (int i = 0; i < 64; ++i) { brpc::SocketUniquePtr ptr; ASSERT_EQ(0, Select(&ptr)); FeedbackLatency(_lb, id.id, 1000, ETIMEDOUT, &cntl); } std::ostringstream os; brpc::DescribeOptions options; options.verbose = true; _lb->Describe(os, options); const std::string desc = os.str(); const size_t pos = desc.find("ewma_us="); ASSERT_NE(std::string::npos, pos) << desc; const int64_t ewma_us = strtoll(desc.c_str() + pos + 8, NULL, 10); ASSERT_LE(ewma_us, brpc::policy::FLAGS_p2c_max_punish_ms * 1000L) << desc; } struct FeedbackBenchArg { brpc::policy::P2CEwmaLoadBalancer* lb; brpc::SocketId server_id; butil::atomic stop; butil::atomic nfeedback; }; void* FeedbackHammer(void* void_arg) { FeedbackBenchArg* arg = static_cast(void_arg); size_t n = 0; while (!arg->stop.load(butil::memory_order_relaxed)) { FeedbackLatency(arg->lb, arg->server_id, 1000); ++n; } arg->nfeedback.fetch_add(n, butil::memory_order_relaxed); return NULL; } TEST_F(P2CEwmaLoadBalancerTest, feedback_lock_overhead) { // All threads feed back to a single server so update_mutex sees // worst-case contention. const brpc::ServerId id = CreateServer("127.0.0.1:7901"); ASSERT_TRUE(_lb->AddServer(id)); for (size_t nthread = 1; nthread <= 4; nthread *= 2) { FeedbackBenchArg arg; arg.lb = _lb; arg.server_id = id.id; arg.stop.store(false); arg.nfeedback.store(0); pthread_t threads[4]; const int64_t begin_us = butil::gettimeofday_us(); for (size_t i = 0; i < nthread; ++i) { ASSERT_EQ(0, pthread_create( &threads[i], NULL, FeedbackHammer, &arg)); } usleep(500 * 1000); arg.stop.store(true); for (size_t i = 0; i < nthread; ++i) { ASSERT_EQ(0, pthread_join(threads[i], NULL)); } const int64_t elapsed_us = butil::gettimeofday_us() - begin_us; const size_t n = arg.nfeedback.load(); ASSERT_GT(n, 0u); LOG(INFO) << "Feedback on 1 shared server, " << nthread << " thread(s): " << (elapsed_us * 1000L * nthread) / n << "ns/op (" << n << " ops in " << elapsed_us / 1000 << "ms)"; } } } // namespace