429 lines
15 KiB
C++
429 lines
15 KiB
C++
// 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 <cstdlib>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <vector>
|
|
#include <gflags/gflags.h>
|
|
#include <gtest/gtest.h>
|
|
#include <pthread.h>
|
|
#include <unistd.h>
|
|
#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<size_t> 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<brpc::ServerId> 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<brpc::ServerId>(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<brpc::ServerId>(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<brpc::SocketId, size_t> 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<brpc::SocketId, size_t> 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<brpc::SocketId, size_t> 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<brpc::SocketId, size_t> 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<bool> stop;
|
|
butil::atomic<size_t> nselected;
|
|
};
|
|
|
|
void* SelectAndFeedback(void* void_arg) {
|
|
ChurnArg* arg = static_cast<ChurnArg*>(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<brpc::ServerId> 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<bool> stop;
|
|
butil::atomic<size_t> nfeedback;
|
|
};
|
|
|
|
void* FeedbackHammer(void* void_arg) {
|
|
FeedbackBenchArg* arg = static_cast<FeedbackBenchArg*>(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
|