Skip to content

Commit b9fa1c9

Browse files
committed
Add bounded-load consistent hashing load balancer (c_murmurhash_bl)
Classic consistent hashing routes a hot key to one server with no relief valve: that server saturates while its ring neighbors idle. c_murmurhash_bl implements "Consistent Hashing with Bounded Loads" (Mirrokni et al., CACM 2017) on the same murmurhash ring: each server accepts at most ceil(load_factor * average in-flight) requests, and an at-capacity server overflows clockwise to the next server with spare capacity, so spilled requests always land on the same ring successors. load_factor defaults to -chash_bounded_load_factor(1.25, validated > 1) and is overridable per channel: c_murmurhash_bl:load_factor=1.5. In-flight accounting uses relaxed per-server counters shared by both DoublyBufferedData buffers, incremented at selection and decremented in Feedback(); the total is restored even if the server was removed in between, and the counter map is resynced from the ring on membership changes. The ring code is reused by subclassing ConsistentHashingLoadBalancer via a per-key SetParameter() hook. The existing `replicas' parameter is now documented for all CH schemes, settling the old "TODO: or 160?" on the replica count. Includes unit tests (cap under hot-key load, overflow to ring successor, factor validation, feedback decrement, removal consistency, replicas parameter) and docs in cn/en client.md.
1 parent 4a429cd commit b9fa1c9

6 files changed

Lines changed: 641 additions & 13 deletions

File tree

docs/cn/client.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,16 @@ locality-aware,优先选择延时低的下游,直到其延时高于其他机
277277

278278
注意甄别请求中的“主键”部分和“属性”部分,不要为了偷懒或通用,就把请求的所有内容一股脑儿计算出哈希值,属性的变化会使请求的目的地发生剧烈的变化。另外也要注意padding问题,比如struct Foo { int32_t a; int64_t b; }在64位机器上a和b之间有4个字节的空隙,内容未定义,如果像hash(&foo, sizeof(foo))这样计算哈希值,结果就是未定义的,得把内容紧密排列或序列化后再算。
279279

280+
每台服务器的虚拟节点数默认由-chash_num_replicas控制(默认100),可按channel覆盖:`c_murmurhash:replicas=300`
281+
280282
实现原理请查看[Consistent Hashing](consistent_hashing.md)
281283

282284
其他lb不需要设置Controller.set_request_code(),如果调用了request_code也不会被lb使用,例如:lb=rr调用了Controller.set_request_code(),即使所有RPC的request_code都相同,也依然是rr。
283285

286+
### c_murmurhash_bl
287+
288+
即带负载上限的一致性哈希("Consistent Hashing with Bounded Loads",Mirrokni等,CACM 2017)。哈希环与`c_murmurhash`完全相同,但每台服务器额外有容量上限`ceil(load_factor * 平均在途请求数)`。当哈希命中的服务器已达上限时,请求沿哈希环顺时针溢出到下一台有余量的服务器,因此热点key不再压垮单台服务器,且溢出请求总是落到环上固定的后继节点,对cache仍然友好。系数默认来自-chash_bounded_load_factor(默认1.25,必须大于1),可按channel覆盖:`c_murmurhash_bl:load_factor=1.5``replicas`参数与`c_murmurhash`相同。
289+
284290
### 从集群宕机后恢复时的客户端限流
285291

286292
集群宕机指的是集群中所有server都处于不可用的状态。由于健康检查机制,当集群恢复正常后,server会间隔性地上线。当某一个server上线后,所有的流量会发送过去,可能导致服务再次过载。若熔断开启,则可能导致其它server上线前该server再次熔断,集群永远无法恢复。作为解决方案,brpc提供了在集群宕机后恢复时的限流机制:当集群中没有可用server时,集群进入恢复状态,假设正好能服务所有请求的server数量为min_working_instances,当前集群可用的server数量为q,则在恢复状态时,client接受请求的概率为q/min_working_instances,否则丢弃;若一段时间hold_seconds内q保持不变,则把流量重新发送全部可用的server上,并离开恢复状态。在恢复阶段时,可以通过判断controller.ErrorCode()是否等于brpc::ERJECT来判断该次请求是否被拒绝,被拒绝的请求不会被框架重试。

docs/en/client.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,16 @@ Need to set Controller.set_request_code() before RPC otherwise the RPC will fail
278278

279279
Do distinguish "key" and "attributes" of the request. Don't compute request_code by full content of the request just for quick. Minor change in attributes may result in totally different hash code and change destination dramatically. Another cause is padding, for example: `struct Foo { int32_t a; int64_t b; }` has a 4-byte undefined gap between `a` and `b` on 64-bit machines, result of `hash(&foo, sizeof(foo))` is undefined. Fields need to be packed or serialized before hashing.
280280
281+
Number of virtual nodes per server defaults to -chash_num_replicas(default 100) and can be overridden per channel: `c_murmurhash:replicas=300`.
282+
281283
Check out [Consistent Hashing](consistent_hashing.md) for more details.
282284
283285
Other kind of lb does not need to set Controller.set_request_code(). If request code is set, it will not be used by lb. For example, lb=rr, and call Controller.set_request_code(), even if request_code is the same for every request, lb will balance the requests using the rr policy.
284286
287+
### c_murmurhash_bl
288+
289+
which is consistent hashing with bounded loads("Consistent Hashing with Bounded Loads", Mirrokni et al., CACM 2017). The hash ring is identical to `c_murmurhash`, but each server additionally has a capacity of `ceil(load_factor * average in-flight requests)`. When the hashed-to server is at capacity, the request overflows clockwise to the next server on the ring with spare capacity, so a hot key no longer saturates a single server while overflowed requests always land on the same ring successors, which keeps caches effective. The default factor comes from -chash_bounded_load_factor(default 1.25, must be > 1) and can be overridden per channel: `c_murmurhash_bl:load_factor=1.5`. The `replicas` parameter is supported as in `c_murmurhash`.
290+
285291
### Client-side throttling for recovery from cluster downtime
286292
287293
Cluster downtime refers to the state in which all servers in the cluster are unavailable. Due to the health check mechanism, when the cluster returns to normal, server will go online one by one. When a server is online, all traffic will be sent to it, which may cause the service to be overloaded again. If circuit breaker is enabled, server may be offline again before the other servers go online, and the cluster can never be recovered. As a solution, brpc provides a client-side throttling mechanism for recovery after cluster downtime. When no server is available in the cluster, the cluster enters recovery state. Assuming that the minimum number of servers that can serve all requests is min_working_instances, current number of servers available in the cluster is q, then in recovery state, the probability of client accepting the request is q/min_working_instances, otherwise it is discarded. If q remains unchanged for a period of time(hold_seconds), the traffic is resent to all available servers and leaves recovery state. Whether the request is rejected in recovery state is indicated by whether controller.ErrorCode() is equal to brpc::ERJECT, and the rejected request will not be retried by the framework.

src/brpc/global.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ struct GlobalExtensions {
138138
, ch_mh_lb(CONS_HASH_LB_MURMUR3)
139139
, ch_md5_lb(CONS_HASH_LB_MD5)
140140
, ch_ketama_lb(CONS_HASH_LB_KETAMA)
141+
, ch_mh_bl_lb(CONS_HASH_LB_MURMUR3)
141142
, constant_cl(0) {
142143
}
143144

@@ -163,6 +164,7 @@ struct GlobalExtensions {
163164
ConsistentHashingLoadBalancer ch_mh_lb;
164165
ConsistentHashingLoadBalancer ch_md5_lb;
165166
ConsistentHashingLoadBalancer ch_ketama_lb;
167+
ConsistentHashingBoundedLoadBalancer ch_mh_bl_lb;
166168
DynPartLoadBalancer dynpart_lb;
167169

168170
AutoConcurrencyLimiter auto_cl;
@@ -411,6 +413,7 @@ static void GlobalInitializeOrDieImpl() {
411413
LoadBalancerExtension()->RegisterOrDie("c_murmurhash", &g_ext->ch_mh_lb);
412414
LoadBalancerExtension()->RegisterOrDie("c_md5", &g_ext->ch_md5_lb);
413415
LoadBalancerExtension()->RegisterOrDie("c_ketama", &g_ext->ch_ketama_lb);
416+
LoadBalancerExtension()->RegisterOrDie("c_murmurhash_bl", &g_ext->ch_mh_bl_lb);
414417
LoadBalancerExtension()->RegisterOrDie("_dynpart", &g_ext->dynpart_lb);
415418

416419
// Compress Handlers

src/brpc/policy/consistent_hashing_load_balancer.cpp

Lines changed: 229 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,36 @@
1818

1919
#include <algorithm> // std::set_union
2020
#include <array>
21+
#include <cmath> // std::ceil
22+
#include <limits> // numeric_limits
2123
#include <gflags/gflags.h>
2224
#include <openssl/md5.h>
2325
#include "butil/containers/flat_map.h"
2426
#include "butil/errno.h"
2527
#include "butil/strings/string_number_conversions.h"
2628
#include "brpc/socket.h"
29+
#include "brpc/reloadable_flags.h"
2730
#include "brpc/policy/consistent_hashing_load_balancer.h"
2831
#include "brpc/policy/hasher.h"
2932

3033
namespace brpc {
3134
namespace policy {
3235

33-
// TODO: or 160?
34-
DEFINE_int32(chash_num_replicas, 100,
35-
"default number of replicas per server in chash");
36-
DEFINE_bool(consistent_hashing_enable_server_tag, false,
36+
DEFINE_int32(chash_num_replicas, 100,
37+
"default number of replicas per server in chash, "
38+
"overridable per channel with the `replicas' parameter");
39+
DEFINE_bool(consistent_hashing_enable_server_tag, false,
3740
"if consistent hashing enable server with tag");
41+
DEFINE_double(chash_bounded_load_factor, 1.25,
42+
"default capacity factor of bounded-load consistent hashing"
43+
"(c_*_bl): a server takes at most ceil(factor * average "
44+
"in-flight) requests before overflowing to its ring successor, "
45+
"overridable per channel with the `load_factor' parameter");
46+
47+
static bool ValidateLoadFactor(const char*, double factor) {
48+
return factor > 1.0;
49+
}
50+
BRPC_VALIDATE_GFLAG(chash_bounded_load_factor, ValidateLoadFactor);
3851

3952
// Defined in hasher.cpp.
4053
const char* GetHashName(HashFunc hasher);
@@ -395,16 +408,223 @@ bool ConsistentHashingLoadBalancer::SetParameters(const butil::StringPiece& para
395408
LOG(ERROR) << "Empty value for " << sp.key() << " in lb parameter";
396409
return false;
397410
}
398-
if (sp.key() == "replicas") {
399-
if (!butil::StringToSizeT(sp.value(), &_num_replicas)) {
400-
return false;
411+
if (!SetParameter(sp.key(), sp.value())) {
412+
return false;
413+
}
414+
}
415+
return true;
416+
}
417+
418+
bool ConsistentHashingLoadBalancer::SetParameter(
419+
const butil::StringPiece& key, const butil::StringPiece& value) {
420+
if (key == "replicas") {
421+
return butil::StringToSizeT(value, &_num_replicas);
422+
}
423+
LOG(ERROR) << "Failed to set this unknown parameters " << key << '=' << value;
424+
return true;
425+
}
426+
427+
ConsistentHashingBoundedLoadBalancer::ConsistentHashingBoundedLoadBalancer(
428+
ConsistentHashingLoadBalancerType type)
429+
: ConsistentHashingLoadBalancer(type)
430+
, _load_factor(FLAGS_chash_bounded_load_factor)
431+
, _total_inflight(0) {}
432+
433+
size_t ConsistentHashingBoundedLoadBalancer::ResetLoads(
434+
LoadMap& bg, const LoadMap& fg, const std::vector<SocketId>& ids) {
435+
bg.clear();
436+
for (size_t i = 0; i < ids.size(); ++i) {
437+
const std::shared_ptr<ServerLoad>* fg_load = fg.seek(ids[i]);
438+
bg[ids[i]] = (fg_load != nullptr)
439+
? *fg_load : std::make_shared<ServerLoad>();
440+
}
441+
// Non-zero so that both buffers are always rebuilt.
442+
return 1;
443+
}
444+
445+
void ConsistentHashingBoundedLoadBalancer::SyncLoadMap() {
446+
std::vector<SocketId> ids;
447+
{
448+
butil::DoublyBufferedData<std::vector<Node> >::ScopedPtr s;
449+
if (_db_hash_ring.Read(&s) != 0) {
450+
return;
451+
}
452+
butil::FlatSet<SocketId> id_set;
453+
ids.reserve(s->size() / std::max(_num_replicas, (size_t)1));
454+
for (size_t i = 0; i < s->size(); ++i) {
455+
const SocketId id = (*s)[i].server_sock.id;
456+
if (id_set.seek(id) == nullptr && id_set.insert(id) != nullptr) {
457+
ids.push_back(id);
401458
}
402-
continue;
403459
}
404-
LOG(ERROR) << "Failed to set this unknown parameters " << sp.key_and_value();
405460
}
461+
_db_load_map.ModifyWithForeground(ResetLoads, ids);
462+
}
463+
464+
bool ConsistentHashingBoundedLoadBalancer::AddServer(const ServerId& server) {
465+
if (!ConsistentHashingLoadBalancer::AddServer(server)) {
466+
return false;
467+
}
468+
SyncLoadMap();
406469
return true;
407470
}
408471

472+
bool ConsistentHashingBoundedLoadBalancer::RemoveServer(const ServerId& server) {
473+
if (!ConsistentHashingLoadBalancer::RemoveServer(server)) {
474+
return false;
475+
}
476+
SyncLoadMap();
477+
return true;
478+
}
479+
480+
size_t ConsistentHashingBoundedLoadBalancer::AddServersInBatch(
481+
const std::vector<ServerId>& servers) {
482+
const size_t n = ConsistentHashingLoadBalancer::AddServersInBatch(servers);
483+
if (n != 0) {
484+
SyncLoadMap();
485+
}
486+
return n;
487+
}
488+
489+
size_t ConsistentHashingBoundedLoadBalancer::RemoveServersInBatch(
490+
const std::vector<ServerId>& servers) {
491+
const size_t n = ConsistentHashingLoadBalancer::RemoveServersInBatch(servers);
492+
if (n != 0) {
493+
SyncLoadMap();
494+
}
495+
return n;
496+
}
497+
498+
LoadBalancer* ConsistentHashingBoundedLoadBalancer::New(
499+
const butil::StringPiece& params) const {
500+
ConsistentHashingBoundedLoadBalancer* lb =
501+
new (std::nothrow) ConsistentHashingBoundedLoadBalancer(_type);
502+
if (lb && !lb->SetParameters(params)) {
503+
delete lb;
504+
lb = nullptr;
505+
}
506+
return lb;
507+
}
508+
509+
bool ConsistentHashingBoundedLoadBalancer::SetParameter(
510+
const butil::StringPiece& key, const butil::StringPiece& value) {
511+
if (key == "load_factor") {
512+
double factor = 0.0;
513+
if (!butil::StringToDouble(value.as_string(), &factor) ||
514+
factor <= 1.0) {
515+
LOG(ERROR) << "Invalid load_factor=`" << value
516+
<< "', must be a number > 1";
517+
return false;
518+
}
519+
_load_factor = factor;
520+
return true;
521+
}
522+
return ConsistentHashingLoadBalancer::SetParameter(key, value);
523+
}
524+
525+
int ConsistentHashingBoundedLoadBalancer::SelectServer(
526+
const SelectIn& in, SelectOut* out) {
527+
if (!in.has_request_code) {
528+
LOG(ERROR) << "Controller.set_request_code() is required";
529+
return EINVAL;
530+
}
531+
if (in.request_code > UINT_MAX) {
532+
LOG(ERROR) << "request_code must be 32-bit currently";
533+
return EINVAL;
534+
}
535+
butil::DoublyBufferedData<std::vector<Node> >::ScopedPtr s;
536+
if (_db_hash_ring.Read(&s) != 0) {
537+
return ENOMEM;
538+
}
539+
if (s->empty()) {
540+
return ENODATA;
541+
}
542+
butil::DoublyBufferedData<LoadMap>::ScopedPtr lm;
543+
if (_db_load_map.Read(&lm) != 0) {
544+
return ENOMEM;
545+
}
546+
int64_t capacity = std::numeric_limits<int64_t>::max();
547+
if (!lm->empty()) {
548+
const int64_t total = _total_inflight.load(butil::memory_order_relaxed);
549+
capacity = (int64_t)std::ceil(
550+
_load_factor * (double)(total + 1) / (double)lm->size());
551+
}
552+
std::vector<Node>::const_iterator choice =
553+
std::lower_bound(s->begin(), s->end(), (uint32_t)in.request_code);
554+
if (choice == s->end()) {
555+
choice = s->begin();
556+
}
557+
// Walk clockwise from the hashed-to node and take the first server under
558+
// capacity. With load_factor > 1 at least one server is below the average
559+
// whenever counters are consistent, so the walk finds one; the first
560+
// acceptable server is kept as a fallback to guard against transient
561+
// inconsistency of the relaxed counters.
562+
SocketUniquePtr fallback_ptr;
563+
ServerLoad* fallback_load = nullptr;
564+
ServerLoad* selected_load = nullptr;
565+
for (size_t i = 0; i < s->size(); ++i) {
566+
SocketUniquePtr ptr;
567+
if (((i + 1) == s->size() // always take last chance
568+
|| !ExcludedServers::IsExcluded(in.excluded, choice->server_sock.id))
569+
&& IsServerAvailable(choice->server_sock.id, &ptr)) {
570+
const std::shared_ptr<ServerLoad>* pload =
571+
lm->seek(choice->server_sock.id);
572+
ServerLoad* load = (pload != nullptr) ? pload->get() : nullptr;
573+
const int32_t inflight = (load != nullptr)
574+
? load->inflight.load(butil::memory_order_relaxed) : 0;
575+
if (inflight < capacity) {
576+
selected_load = load;
577+
out->ptr->swap(ptr);
578+
break;
579+
}
580+
if (fallback_ptr.get() == nullptr) {
581+
fallback_load = load;
582+
fallback_ptr.swap(ptr);
583+
}
584+
}
585+
if (++choice == s->end()) {
586+
choice = s->begin();
587+
}
588+
}
589+
if (out->ptr->get() == nullptr) {
590+
if (fallback_ptr.get() == nullptr) {
591+
return EHOSTDOWN;
592+
}
593+
selected_load = fallback_load;
594+
out->ptr->swap(fallback_ptr);
595+
}
596+
if (in.changable_weights && selected_load != nullptr) {
597+
selected_load->inflight.fetch_add(1, butil::memory_order_relaxed);
598+
_total_inflight.fetch_add(1, butil::memory_order_relaxed);
599+
out->need_feedback = true;
600+
}
601+
return 0;
602+
}
603+
604+
void ConsistentHashingBoundedLoadBalancer::Feedback(const CallInfo& info) {
605+
_total_inflight.fetch_sub(1, butil::memory_order_relaxed);
606+
butil::DoublyBufferedData<LoadMap>::ScopedPtr lm;
607+
if (_db_load_map.Read(&lm) != 0) {
608+
return;
609+
}
610+
const std::shared_ptr<ServerLoad>* pload = lm->seek(info.server_id);
611+
if (pload != nullptr) {
612+
// If the server was removed after selection, its counter is already
613+
// gone and only the total needs restoring.
614+
(*pload)->inflight.fetch_sub(1, butil::memory_order_relaxed);
615+
}
616+
}
617+
618+
void ConsistentHashingBoundedLoadBalancer::Describe(
619+
std::ostream& os, const DescribeOptions& options) {
620+
if (!options.verbose) {
621+
os << "c_hash_bl";
622+
return;
623+
}
624+
os << "BoundedLoad{load_factor=" << _load_factor << " total_inflight="
625+
<< _total_inflight.load(butil::memory_order_relaxed) << "}\n";
626+
ConsistentHashingLoadBalancer::Describe(os, options);
627+
}
628+
409629
} // namespace policy
410630
} // namespace brpc

0 commit comments

Comments
 (0)