This is an automated email from the ASF dual-hosted git repository.
chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new 32c95225 Add Power-of-Two-Choices Peak-EWMA load balancer (p2c) (#3367)
32c95225 is described below
commit 32c95225e83b88d8037560ba33c43977b4641508
Author: rajvarun77 <[email protected]>
AuthorDate: Sun Jul 12 10:41:54 2026 -0400
Add Power-of-Two-Choices Peak-EWMA load balancer (p2c) (#3367)
* Add Power-of-Two-Choices Peak-EWMA load balancer (p2c)
Implements the p2c load balancing policy proposed in #3340: each
selection samples two random servers (configurable via choices=N) and
routes to the lower peak-EWMA latency * (inflight+1) / weight score.
Upward latency spikes take effect immediately while recovery decays
over tau_ms (default 10s), so a degraded server is shed within one
observation at O(1) selection cost.
Uses only existing LoadBalancer hooks (SelectServer/Feedback,
need_feedback) with DoublyBufferedData membership like rr/la; per-node
stats are shared_ptr-owned by both buffers. Registered as "p2c" in
global.cpp. Includes unit tests (functional, weighted, exclusion,
error punishment, concurrency churn) and docs in cn/en client.md.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Reuse SelectIn.begin_time_us to avoid extra clock read per selection
Co-Authored-By: Claude Fable 5 <[email protected]>
* Address review: gflags for tunables, cap error punishment, drop FlatMap
init
- p2c_default_choices/p2c_default_tau_ms gflags as channel-overridable
defaults
- p2c_max_punish_ms(default 30s) caps the doubled-EWMA error punishment so a
persistently failing server recovers in bounded time after turning healthy
- Servers::server_map relies on FlatMap small-map auto-init
- tests: error_punish_is_capped + feedback_lock_overhead benchmark
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: rajvarun77 <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
---
docs/cn/client.md | 4 +
docs/cn/rpc_press.md | 2 +-
docs/en/client.md | 4 +
src/brpc/global.cpp | 3 +
src/brpc/policy/p2c_ewma_load_balancer.cpp | 389 +++++++++++++++++++++++
src/brpc/policy/p2c_ewma_load_balancer.h | 94 ++++++
test/brpc_p2c_ewma_load_balancer_unittest.cpp | 428 ++++++++++++++++++++++++++
tools/rpc_press/rpc_press.cpp | 2 +-
8 files changed, 924 insertions(+), 2 deletions(-)
diff --git a/docs/cn/client.md b/docs/cn/client.md
index 609becc0..bb16eefb 100755
--- a/docs/cn/client.md
+++ b/docs/cn/client.md
@@ -260,6 +260,10 @@ int main() {
locality-aware,优先选择延时低的下游,直到其延时高于其他机器,无需其他设置。实现原理请查看[Locality-aware load
balancing](lalb.md)。
+### p2c
+
+即power-of-two-choices加peak-EWMA延时评分。每次选择随机采样两台服务器,把请求发给`延时 * (inflight + 1) /
权重`得分较低的那台。其中延时是对尖峰敏感的滑动平均:延时上升立即生效,恢复则按`tau_ms`(默认10秒)衰减。变慢或出错的服务器在一次观察内即被避开,且选择开销与集群规模无关,为O(1)。权重取自实例tag(同wrr,默认为1)。可选参数:`p2c:choices=4`(每次比较4台采样服务器,适合多台机器同时劣化的场景)、`p2c:tau_ms=5000`。
+
### c_murmurhash or c_md5
一致性哈希,与简单hash的不同之处在于增加或删除机器时不会使分桶结果剧烈变化,特别适合cache类服务。
diff --git a/docs/cn/rpc_press.md b/docs/cn/rpc_press.md
index 32572846..357340ae 100644
--- a/docs/cn/rpc_press.md
+++ b/docs/cn/rpc_press.md
@@ -33,7 +33,7 @@ json也可以写在文件中,假如./input.json包含了上述两个请求,-
可选参数:
- -inc:
包含被import的proto文件的路径。rpc_press默认支持import目录下的其他proto文件,但如果proto文件在其他目录,就要通过这个参数指定,多个路径用分号(;)分隔。
-- -lb_policy: 指定负载均衡算法,默认为空,可选项为: rr random la c_murmurhash
c_md5,具体见[负载均衡](client.md#负载均衡)。
+- -lb_policy: 指定负载均衡算法,默认为空,可选项为: rr random la p2c c_murmurhash
c_md5,具体见[负载均衡](client.md#负载均衡)。
- -timeout_ms: 设定超时,单位是毫秒(milliseconds),默认是1000(1秒)
- -max_retry: 最大的重试次数,默认是3, 一般无需修改. brpc的重试行为具体请见[这里](client.md#重试).
- -protocol: 连接server使用的协议,可选项见[协议](client.md#协议), 默认是baidu_std
diff --git a/docs/en/client.md b/docs/en/client.md
index 60c458b6..8da0c6b1 100644
--- a/docs/en/client.md
+++ b/docs/en/client.md
@@ -258,6 +258,10 @@ Requirements of instance tag is the same as wrr.
which is locality-aware. Perfer servers with lower latencies, until the
latency is higher than others, no other settings. Check out [Locality-aware
load balancing](lalb.md) for more details.
+### p2c
+
+which is power-of-two-choices with peak-EWMA latency scoring. Each selection
samples two random servers and routes to the one with the lower `latency *
(inflight + 1) / weight` score, where the latency is a peak-sensitive moving
average: an upward spike takes effect immediately while recovery decays over
`tau_ms`(default 10s). A slow or failing server is shed within one observation
and selection cost is O(1) regardless of cluster size. Weight comes from the
instance tag as in wrr(default [...]
+
### c_murmurhash or c_md5
which is consistent hashing. Adding or removing servers does not make
destinations of requests change as dramatically as in simple hashing. It's
especially suitable for caching services.
diff --git a/src/brpc/global.cpp b/src/brpc/global.cpp
index c1bfbcdd..e9402512 100644
--- a/src/brpc/global.cpp
+++ b/src/brpc/global.cpp
@@ -47,6 +47,7 @@
#include "brpc/policy/randomized_load_balancer.h"
#include "brpc/policy/weighted_randomized_load_balancer.h"
#include "brpc/policy/locality_aware_load_balancer.h"
+#include "brpc/policy/p2c_ewma_load_balancer.h"
#include "brpc/policy/consistent_hashing_load_balancer.h"
#include "brpc/policy/hasher.h"
#include "brpc/policy/dynpart_load_balancer.h"
@@ -157,6 +158,7 @@ struct GlobalExtensions {
RandomizedLoadBalancer randomized_lb;
WeightedRandomizedLoadBalancer wr_lb;
LocalityAwareLoadBalancer la_lb;
+ P2CEwmaLoadBalancer p2c_ewma_lb;
ConsistentHashingLoadBalancer ch_mh_lb;
ConsistentHashingLoadBalancer ch_md5_lb;
ConsistentHashingLoadBalancer ch_ketama_lb;
@@ -407,6 +409,7 @@ static void GlobalInitializeOrDieImpl() {
LoadBalancerExtension()->RegisterOrDie("random", &g_ext->randomized_lb);
LoadBalancerExtension()->RegisterOrDie("wr", &g_ext->wr_lb);
LoadBalancerExtension()->RegisterOrDie("la", &g_ext->la_lb);
+ LoadBalancerExtension()->RegisterOrDie("p2c", &g_ext->p2c_ewma_lb);
LoadBalancerExtension()->RegisterOrDie("c_murmurhash", &g_ext->ch_mh_lb);
LoadBalancerExtension()->RegisterOrDie("c_md5", &g_ext->ch_md5_lb);
LoadBalancerExtension()->RegisterOrDie("c_ketama", &g_ext->ch_ketama_lb);
diff --git a/src/brpc/policy/p2c_ewma_load_balancer.cpp
b/src/brpc/policy/p2c_ewma_load_balancer.cpp
new file mode 100644
index 00000000..2b3f5bb5
--- /dev/null
+++ b/src/brpc/policy/p2c_ewma_load_balancer.cpp
@@ -0,0 +1,389 @@
+// 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> // std::exp
+#include <gflags/gflags.h>
+#include "butil/fast_rand.h" // fast_rand_less_than
+#include "butil/time.h" // gettimeofday_us
+#include "butil/string_splitter.h" // KeyValuePairsSplitter
+#include "butil/strings/string_number_conversions.h" // StringToUint
+#include "brpc/socket.h"
+#include "brpc/controller.h"
+#include "brpc/policy/p2c_ewma_load_balancer.h"
+
+namespace brpc {
+namespace policy {
+
+DEFINE_uint32(p2c_default_choices, 2,
+ "Default number of servers sampled per selection in p2c, "
+ "overridable per channel with the `choices' parameter");
+DEFINE_int64(p2c_default_tau_ms, 10000,
+ "Default decay time(ms) of the peak-EWMA latency in p2c "
+ "(Finagle's PeakEwma default), overridable per channel with "
+ "the `tau_ms' parameter");
+DEFINE_int64(p2c_max_punish_ms, 30000,
+ "Cap(ms) on the punished latency recorded for a failed call in "
+ "p2c, bounding how long a persistently failing server takes to "
+ "recover after it turns healthy. 0 means no cap");
+
+namespace {
+// Upper bound of the `choices' parameter. Compile-time because it sizes the
+// sampling array on SelectServer's stack.
+const uint32_t MAX_CHOICES = 64;
+// Floor of the latency term so that in-flight counts break ties between
+// servers that have no latency observation yet.
+const double MIN_LATENCY_TERM_US = 1.0;
+
+uint32_t WeightOfTag(const std::string& tag) {
+ if (tag.empty()) {
+ return 1;
+ }
+ uint32_t weight = 0;
+ if (!butil::StringToUint(tag, &weight) || weight == 0) {
+ LOG(WARNING) << "Invalid weight tag=`" << tag << "', use weight=1";
+ return 1;
+ }
+ return weight;
+}
+} // namespace
+
+P2CEwmaLoadBalancer::P2CEwmaLoadBalancer()
+ // Clamp so that broken flag values can not disable comparison or decay.
+ : _choices(std::min(std::max(FLAGS_p2c_default_choices, 2u), MAX_CHOICES))
+ , _tau_us(std::max<int64_t>(FLAGS_p2c_default_tau_ms, 1) * 1000L) {}
+
+bool P2CEwmaLoadBalancer::Add(Servers& bg, const Servers& fg,
+ const ServerId& id) {
+ if (bg.server_list.capacity() < 128) {
+ bg.server_list.reserve(128);
+ }
+ if (bg.server_map.seek(id.id) != NULL) {
+ return false;
+ }
+ ServerInfo info = { id.id, WeightOfTag(id.tag), NULL };
+ const size_t* pindex = fg.server_map.seek(id.id);
+ if (pindex == NULL) {
+ // Both buffers do not have the server. Create the stat structure
+ // which will be shared by both buffers.
+ info.stat = std::make_shared<NodeStat>();
+ } else {
+ // Already added to the other buffer, share its stat.
+ info.stat = fg.server_list[*pindex].stat;
+ }
+ bg.server_map[id.id] = bg.server_list.size();
+ bg.server_list.push_back(info);
+ return true;
+}
+
+bool P2CEwmaLoadBalancer::Remove(Servers& bg, const ServerId& id) {
+ size_t* pindex = bg.server_map.seek(id.id);
+ if (pindex == NULL) {
+ return false;
+ }
+ const size_t index = *pindex;
+ bg.server_list[index] = bg.server_list.back();
+ bg.server_map[bg.server_list[index].id] = index;
+ bg.server_list.pop_back();
+ bg.server_map.erase(id.id);
+ return true;
+}
+
+size_t P2CEwmaLoadBalancer::BatchAdd(
+ Servers& bg, const Servers& fg, const std::vector<ServerId>& servers) {
+ size_t count = 0;
+ for (size_t i = 0; i < servers.size(); ++i) {
+ count += !!Add(bg, fg, servers[i]);
+ }
+ return count;
+}
+
+size_t P2CEwmaLoadBalancer::BatchRemove(
+ Servers& bg, const std::vector<ServerId>& servers) {
+ size_t count = 0;
+ for (size_t i = 0; i < servers.size(); ++i) {
+ count += !!Remove(bg, servers[i]);
+ }
+ return count;
+}
+
+bool P2CEwmaLoadBalancer::AddServer(const ServerId& id) {
+ return _db_servers.ModifyWithForeground(Add, id);
+}
+
+bool P2CEwmaLoadBalancer::RemoveServer(const ServerId& id) {
+ return _db_servers.Modify(Remove, id);
+}
+
+size_t P2CEwmaLoadBalancer::AddServersInBatch(
+ const std::vector<ServerId>& servers) {
+ const size_t n = _db_servers.ModifyWithForeground(BatchAdd, servers);
+ LOG_IF(ERROR, n != servers.size())
+ << "Fail to AddServersInBatch, expected " << servers.size()
+ << " actually " << n;
+ return n;
+}
+
+size_t P2CEwmaLoadBalancer::RemoveServersInBatch(
+ const std::vector<ServerId>& servers) {
+ return _db_servers.Modify(BatchRemove, servers);
+}
+
+double P2CEwmaLoadBalancer::Score(
+ const ServerInfo& info, int64_t now_us) const {
+ const int64_t ewma_us =
+ info.stat->ewma_us.load(butil::memory_order_relaxed);
+ const int32_t inflight =
+ info.stat->inflight.load(butil::memory_order_relaxed);
+ double latency_term = MIN_LATENCY_TERM_US;
+ if (ewma_us > 0) {
+ // Decay the (possibly stale) EWMA at read time so that a server
+ // penalized long ago regains traffic and gets re-observed.
+ const int64_t stamp_us =
+ info.stat->stamp_us.load(butil::memory_order_relaxed);
+ const int64_t elapsed_us = now_us - stamp_us;
+ double decayed = (double)ewma_us;
+ if (elapsed_us > 0) {
+ decayed *= std::exp(-(double)elapsed_us / (double)_tau_us);
+ }
+ latency_term = std::max(decayed, MIN_LATENCY_TERM_US);
+ }
+ // Clamp so that a transiently negative counter can not invert routing.
+ const int32_t load = std::max(inflight + 1, 1);
+ return latency_term * (double)load / (double)info.weight;
+}
+
+int P2CEwmaLoadBalancer::SelectServer(const SelectIn& in, SelectOut* out) {
+ butil::DoublyBufferedData<Servers>::ScopedPtr s;
+ if (_db_servers.Read(&s) != 0) {
+ return ENOMEM;
+ }
+ const size_t n = s->server_list.size();
+ if (n == 0) {
+ return ENODATA;
+ }
+ // Reuse the caller-provided timestamp to avoid an extra clock read per
+ // selection; only Channel::CheckHealth passes 0.
+ const int64_t now_us = in.begin_time_us > 0
+ ? in.begin_time_us : butil::gettimeofday_us();
+
+ const ServerInfo* best = NULL;
+ double best_score = 0;
+ SocketUniquePtr best_ptr;
+ // Score the server at `index' and keep it if it beats the current best.
+ // Excluded and unavailable servers are skipped.
+ auto consider = [&](size_t index) {
+ const ServerInfo& info = s->server_list[index];
+ if (ExcludedServers::IsExcluded(in.excluded, info.id)) {
+ return;
+ }
+ SocketUniquePtr ptr;
+ if (!IsServerAvailable(info.id, &ptr)) {
+ return;
+ }
+ const double score = Score(info, now_us);
+ if (best == NULL || score < best_score) {
+ best = &info;
+ best_score = score;
+ best_ptr.swap(ptr);
+ }
+ };
+
+ const size_t choices = std::min((size_t)_choices, n);
+ if (choices >= n) {
+ // Scan from a random offset so that equal scores do not herd all
+ // clients onto the lowest-indexed server.
+ const size_t start = butil::fast_rand_less_than(n);
+ for (size_t i = 0; i < n; ++i) {
+ consider((start + i) % n);
+ }
+ } else {
+ // Sample `choices' distinct random servers. Attempts are bounded so
+ // that duplicated draws never loop for long.
+ size_t chosen[MAX_CHOICES];
+ size_t nchosen = 0;
+ const size_t max_attempts = 4 * choices + 8;
+ for (size_t attempt = 0;
+ attempt < max_attempts && nchosen < choices; ++attempt) {
+ const size_t index = butil::fast_rand_less_than(n);
+ bool duplicated = false;
+ for (size_t i = 0; i < nchosen; ++i) {
+ if (chosen[i] == index) {
+ duplicated = true;
+ break;
+ }
+ }
+ if (duplicated) {
+ continue;
+ }
+ chosen[nchosen++] = index;
+ consider(index);
+ }
+ if (best == NULL) {
+ // All sampled servers were excluded or unavailable, fall back
+ // to scoring the whole list before violating exclusion below.
+ for (size_t i = 0; i < n; ++i) {
+ consider(i);
+ }
+ }
+ }
+
+ if (best == NULL) {
+ // Always take last chance: all servers are excluded, send to any
+ // available one as rr/random do.
+ for (size_t i = 0; i < n; ++i) {
+ if (IsServerAvailable(s->server_list[i].id, &best_ptr)) {
+ best = &s->server_list[i];
+ break;
+ }
+ }
+ if (best == NULL) {
+ return EHOSTDOWN;
+ }
+ }
+ if (in.changable_weights) {
+ best->stat->inflight.fetch_add(1, butil::memory_order_relaxed);
+ out->need_feedback = true;
+ }
+ out->ptr->swap(best_ptr);
+ return 0;
+}
+
+void P2CEwmaLoadBalancer::Feedback(const CallInfo& info) {
+ butil::DoublyBufferedData<Servers>::ScopedPtr s;
+ if (_db_servers.Read(&s) != 0) {
+ return;
+ }
+ const size_t* pindex = s->server_map.seek(info.server_id);
+ if (pindex == NULL) {
+ // The server was removed after selection, its stat is gone with it.
+ return;
+ }
+ NodeStat* stat = s->server_list[*pindex].stat.get();
+ stat->inflight.fetch_sub(1, butil::memory_order_relaxed);
+
+ const int64_t now_us = butil::gettimeofday_us();
+ int64_t latency_us = now_us - info.begin_time_us;
+ if (latency_us <= 0) {
+ // time skews, ignore the sample.
+ return;
+ }
+ if (info.error_code != 0) {
+ // Punish failures with at least the timeout(if any) and twice the
+ // current average, so that a failing server keeps losing comparisons
+ // even when it fails fast.
+ const int64_t timeout_us = info.controller->timeout_ms() * 1000L;
+ latency_us = std::max(latency_us, timeout_us);
+ latency_us = std::max(
+ latency_us, 2 * stat->ewma_us.load(butil::memory_order_relaxed));
+ // Cap the punishment: consecutive failures double the EWMA each
+ // time, which otherwise grows without bound and delays recovery
+ // after the server turns healthy.
+ const int64_t max_punish_us = FLAGS_p2c_max_punish_ms * 1000L;
+ if (max_punish_us > 0 && latency_us > max_punish_us) {
+ latency_us = max_punish_us;
+ }
+ }
+
+ BAIDU_SCOPED_LOCK(stat->update_mutex);
+ const int64_t ewma_us = stat->ewma_us.load(butil::memory_order_relaxed);
+ int64_t new_ewma_us = latency_us;
+ if (ewma_us > 0 && latency_us < ewma_us) {
+ // Downward samples decay the average while upward spikes(handled by
+ // the branch above) replace it immediately.
+ const int64_t elapsed_us =
+ now_us - stat->stamp_us.load(butil::memory_order_relaxed);
+ const double w = elapsed_us > 0
+ ? std::exp(-(double)elapsed_us / (double)_tau_us) : 1.0;
+ new_ewma_us = (int64_t)(ewma_us * w + latency_us * (1.0 - w));
+ }
+ stat->ewma_us.store(new_ewma_us, butil::memory_order_relaxed);
+ stat->stamp_us.store(now_us, butil::memory_order_relaxed);
+}
+
+P2CEwmaLoadBalancer* P2CEwmaLoadBalancer::New(
+ const butil::StringPiece& params) const {
+ P2CEwmaLoadBalancer* lb = new (std::nothrow) P2CEwmaLoadBalancer;
+ if (lb != NULL && !lb->SetParameters(params)) {
+ delete lb;
+ lb = NULL;
+ }
+ return lb;
+}
+
+bool P2CEwmaLoadBalancer::SetParameters(const butil::StringPiece& params) {
+ for (butil::KeyValuePairsSplitter sp(params.begin(), params.end(), ' ',
'=');
+ sp; ++sp) {
+ if (sp.value().empty()) {
+ LOG(ERROR) << "Empty value for " << sp.key() << " in lb parameter";
+ return false;
+ }
+ if (sp.key() == "choices") {
+ unsigned choices = 0;
+ if (!butil::StringToUint(sp.value().as_string(), &choices) ||
+ choices < 2 || choices > MAX_CHOICES) {
+ LOG(ERROR) << "Invalid choices=`" << sp.value() << "'";
+ return false;
+ }
+ _choices = choices;
+ } else if (sp.key() == "tau_ms") {
+ int64_t tau_ms = 0;
+ if (!butil::StringToInt64(sp.value(), &tau_ms) || tau_ms <= 0) {
+ LOG(ERROR) << "Invalid tau_ms=`" << sp.value() << "'";
+ return false;
+ }
+ _tau_us = tau_ms * 1000L;
+ } else {
+ LOG(ERROR) << "Unknown parameter " << sp.key_and_value();
+ return false;
+ }
+ }
+ return true;
+}
+
+void P2CEwmaLoadBalancer::Destroy() {
+ delete this;
+}
+
+void P2CEwmaLoadBalancer::Describe(
+ std::ostream& os, const DescribeOptions& options) {
+ if (!options.verbose) {
+ os << "p2c";
+ return;
+ }
+ os << "P2CEwma{choices=" << _choices << " tau_ms=" << _tau_us / 1000;
+ butil::DoublyBufferedData<Servers>::ScopedPtr s;
+ if (_db_servers.Read(&s) != 0) {
+ os << " fail to read _db_servers";
+ } else {
+ const int64_t now_us = butil::gettimeofday_us();
+ os << " n=" << s->server_list.size() << ':';
+ for (size_t i = 0; i < s->server_list.size(); ++i) {
+ const ServerInfo& info = s->server_list[i];
+ os << ' ' << info.id << '(' << "w=" << info.weight
+ << " ewma_us="
+ << info.stat->ewma_us.load(butil::memory_order_relaxed)
+ << " inflight="
+ << info.stat->inflight.load(butil::memory_order_relaxed)
+ << " score=" << Score(info, now_us) << ')';
+ }
+ }
+ os << '}';
+}
+
+} // namespace policy
+} // namespace brpc
diff --git a/src/brpc/policy/p2c_ewma_load_balancer.h
b/src/brpc/policy/p2c_ewma_load_balancer.h
new file mode 100644
index 00000000..1594f159
--- /dev/null
+++ b/src/brpc/policy/p2c_ewma_load_balancer.h
@@ -0,0 +1,94 @@
+// 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.
+
+
+#ifndef BRPC_POLICY_P2C_EWMA_LOAD_BALANCER_H
+#define BRPC_POLICY_P2C_EWMA_LOAD_BALANCER_H
+
+#include <memory> // std::shared_ptr
+#include <vector> // std::vector
+#include "butil/containers/flat_map.h" // FlatMap
+#include "butil/containers/doubly_buffered_data.h" // DoublyBufferedData
+#include "brpc/load_balancer.h"
+
+namespace brpc {
+namespace policy {
+
+// Power-of-Two-Choices with Peak-EWMA latency scoring ("p2c").
+// Each selection samples `choices'(default 2) distinct servers and routes
+// to the one with the lower load score:
+// score = peak_ewma_latency_us * (inflight + 1) / weight
+// The latency EWMA is peak-sensitive: an upward spike replaces the average
+// immediately while recovery decays with time constant `tau_ms'(default 10s),
+// so a degraded server is shed within one observation. Selection is O(1)
+// regardless of fleet size. Weight is got from tag of ServerId(default 1).
+class P2CEwmaLoadBalancer : public LoadBalancer {
+public:
+ P2CEwmaLoadBalancer();
+ bool AddServer(const ServerId& id) override;
+ bool RemoveServer(const ServerId& id) override;
+ size_t AddServersInBatch(const std::vector<ServerId>& servers) override;
+ size_t RemoveServersInBatch(const std::vector<ServerId>& servers) override;
+ int SelectServer(const SelectIn& in, SelectOut* out) override;
+ void Feedback(const CallInfo& info) override;
+ P2CEwmaLoadBalancer* New(const butil::StringPiece& params) const override;
+ void Destroy() override;
+ void Describe(std::ostream& os, const DescribeOptions&) override;
+
+private:
+ // Mutable per-server load state. Allocated once when the server is first
+ // added and shared by both buffers of _db_servers, so a stable pointer
+ // can be used from SelectServer()/Feedback() without copying.
+ struct NodeStat {
+ NodeStat() : inflight(0), ewma_us(0), stamp_us(0) {}
+ butil::atomic<int32_t> inflight;
+ // Peak-sensitive EWMA of latency in us. 0 means no observation yet.
+ butil::atomic<int64_t> ewma_us;
+ // Time of the last EWMA update.
+ butil::atomic<int64_t> stamp_us;
+ butil::Mutex update_mutex;
+ };
+ struct ServerInfo {
+ SocketId id;
+ uint32_t weight;
+ std::shared_ptr<NodeStat> stat;
+ };
+ struct Servers {
+ std::vector<ServerInfo> server_list;
+ // Maps SocketId to index in server_list.
+ butil::FlatMap<SocketId, size_t> server_map;
+ };
+
+ static bool Add(Servers& bg, const Servers& fg, const ServerId& id);
+ static bool Remove(Servers& bg, const ServerId& id);
+ static size_t BatchAdd(Servers& bg, const Servers& fg,
+ const std::vector<ServerId>& servers);
+ static size_t BatchRemove(Servers& bg, const std::vector<ServerId>&
servers);
+
+ bool SetParameters(const butil::StringPiece& params);
+ // Load score of the server at `now_us'. Smaller is better.
+ double Score(const ServerInfo& info, int64_t now_us) const;
+
+ butil::DoublyBufferedData<Servers> _db_servers;
+ uint32_t _choices;
+ int64_t _tau_us;
+};
+
+} // namespace policy
+} // namespace brpc
+
+#endif // BRPC_POLICY_P2C_EWMA_LOAD_BALANCER_H
diff --git a/test/brpc_p2c_ewma_load_balancer_unittest.cpp
b/test/brpc_p2c_ewma_load_balancer_unittest.cpp
new file mode 100644
index 00000000..922bfc39
--- /dev/null
+++ b/test/brpc_p2c_ewma_load_balancer_unittest.cpp
@@ -0,0 +1,428 @@
+// 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
diff --git a/tools/rpc_press/rpc_press.cpp b/tools/rpc_press/rpc_press.cpp
index cb69ccfc..a9a98d60 100644
--- a/tools/rpc_press/rpc_press.cpp
+++ b/tools/rpc_press/rpc_press.cpp
@@ -31,7 +31,7 @@ DEFINE_string(method, "example.EchoService.Echo", "The full
method name");
DEFINE_string(server, "0.0.0.0:8002", "ip:port of the server when
-load_balancer is empty, the naming service otherwise");
DEFINE_string(input, "", "The file containing requests in json format");
DEFINE_string(output, "", "The file containing responses in json format");
-DEFINE_string(lb_policy, "", "The load balancer algorithm: rr, random, la,
c_murmurhash, c_md5");
+DEFINE_string(lb_policy, "", "The load balancer algorithm: rr, random, la,
p2c, c_murmurhash, c_md5");
DEFINE_int32(thread_num, 0, "Number of threads to send requests. 0:
automatically chosen according to -qps");
DEFINE_string(protocol, "baidu_std", "baidu_std hulu_pbrpc sofa_pbrpc http
public_pbrpc nova_pbrpc ubrpc_compack...");
DEFINE_string(connection_type, "", "Type of connections: single, pooled,
short");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]