You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
实现技巧。要统计精准的窗口内错误开销必须用队列,但这种与每个连接绑定且高频触发的逻辑用队列是不经济的,更适合用近似算法。一种常见的方法是EMA,具体来说,若es指代累积错误开销,当错误进来时es = es * p + latency-of-error * (1-p), 当正常回复进来时,es = es * p。p一般在[0.5-1.0)间。EMA相比平均值更偏重时间点近的值,在很多场景中反而是有意义的。由于错误的延时变化剧烈,可以在错误进来时不对es和latency-of-error加权平均,而是直接累加,即错误进来时es = es + latency-of-error,正常回复进来时es = es * p (非常类似于reno流控算法,就像是在计算error的"吞吐")。
由于一个链接上可能同时发起多个rpc,所以这部分的数据计算是要多线程安全的,且每个rpc都要做。一种可能不用锁的方案是在brpc::Socket中加一个butil::atomic<int64_t> _acc_error_cost代表累积错误开销,更新伪码如下:
// EPSILON is a chosen small constant, like 0.1// Variables prefixed with FLAGS_ are specified by users.// when errorous response entersconstint64_t before_add = _acc_error_cost.fetch_add(latency-of-error);
if (before_add + latency-of-error > average-latency * FLAGS_window_size * FLAGS_percentage * (1 + EPSILON)){
// break the circuit
the-related-socket->SetFailed(...)
}
// when ordinary reponse entersint64_t val = _acc_error_cost.load(butil::memory_order_relaxed);
do {
if (val == 0) { // most commonreturn;
} elseif (val < MIN_ERROR_COST) {
if (_acc_error_cost.compare_exchange_strong(val, 0, butil::memory_order_relaxed)) {
return;
}
} else {
// value of p is approximated by pow(EPSILON, 1.0/FLAGS_window_size)if (_acc_error_cost.compare_exchange_strong(val, val * p, butil::memory_order_relaxed)) {
return;
}
}
} while (true)
颠簸问题。健康检查允许一些假阳性(client认为server挂了,但server没挂),一旦出现则会很快被tcp连上从而恢复正常。但如果假阳性出现频率很高,则可能出现大量无谓的后台检查。由于熔断条件会tcp连接召回更多的server,所以会引入更多的假阳性,颠簸出现的概率就更高了。另一个产生颠簸的因素是熔断的恢复条件,很可能出现满足熔断条件但server能通过健康检查的情况,从而触发颠簸。一个方法是健康检查前sleep一段时间,此时间可以固定,也可以指数增加。这部分的改动主要在 Make naming service from sync to async #319 做,会先于熔断功能合入。
在没有熔断时,client会依据指定的负载均衡策略把流量分给不同的server,当和某个server的连接出现问题时会隔离server(无法被负载均衡选中)并触发健康检查:每过一段时间连一下server直到 1.连上则恢复(停止隔离)2. server被移出名字服务,不属于集群无需再连。
但在一些业务场景中,即使连接完好也不意味着server能正常服务,如server返回大量严重错误时,即使client有重试,错误访问花费的时间也浪费掉了,此时最好也隔离掉server。从这个角度来说,熔断是比连上tcp更严格的“健康检查”条件。
从另一个角度,一些负载均衡算法已经惩罚了错误,一个错误频出的server只会分到很少的流量。之前想让负载均衡算法自己做这块,但目前看起来抽取出来被所有负载均衡共享是更好的。熔断扮演分流门槛的角色:一旦错误严重程度超过熔断阈值,则隔离一会儿,否则只是被负载均衡算法降权。这样可以进一步减少用于试错的流量比例。
熔断有以下实现难点:
通用性。若得配“针对哪些错误码”以及“错误码的严重程度”的话,维护起来还是非常困难的,且容易给后人埋坑。所以熔断得做到无场景相关的参数。和其他问题类似,以错误耗费的延时作为开销是一种通用方法。对于不同的方法可除以正常的延时以均一化。既要允许某个server在短时间内抖动,又要剔除掉长期持续出错的server,则要同时统计长短两个窗口内的错误,长窗口阈值低,短窗口阈值高,超过任一阈值触发熔断。长窗口可以基于短窗口的聚合数据做二次聚合以节省开销。
实现技巧。要统计精准的窗口内错误开销必须用队列,但这种与每个连接绑定且高频触发的逻辑用队列是不经济的,更适合用近似算法。一种常见的方法是EMA,具体来说,若es指代累积错误开销,当错误进来时es = es * p + latency-of-error * (1-p), 当正常回复进来时,es = es * p。p一般在[0.5-1.0)间。EMA相比平均值更偏重时间点近的值,在很多场景中反而是有意义的。由于错误的延时变化剧烈,可以在错误进来时不对es和latency-of-error加权平均,而是直接累加,即错误进来时es = es + latency-of-error,正常回复进来时es = es * p (非常类似于reno流控算法,就像是在计算error的"吞吐")。
由于一个链接上可能同时发起多个rpc,所以这部分的数据计算是要多线程安全的,且每个rpc都要做。一种可能不用锁的方案是在brpc::Socket中加一个butil::atomic<int64_t> _acc_error_cost代表累积错误开销,更新伪码如下:
具体实现还有很多细节,后续更新或在IM沟通。
恢复条件
试图复制或回放请求来检查一个server是很困难的,由于分库、时效性、频控等等因素,请求未必可以想发哪就发哪,至少在框架层面上来看,不太通用。
可行的方案要么是访问/health服务是否成功,要么是tcp连接是否成功。这两个都属于健康检查的范畴,与熔断是独立的。
颠簸问题。健康检查允许一些假阳性(client认为server挂了,但server没挂),一旦出现则会很快被tcp连上从而恢复正常。但如果假阳性出现频率很高,则可能出现大量无谓的后台检查。由于熔断条件会tcp连接召回更多的server,所以会引入更多的假阳性,颠簸出现的概率就更高了。另一个产生颠簸的因素是熔断的恢复条件,很可能出现满足熔断条件但server能通过健康检查的情况,从而触发颠簸。一个方法是健康检查前sleep一段时间,此时间可以固定,也可以指数增加。这部分的改动主要在 Make naming service from sync to async #319 做,会先于熔断功能合入。