Skip to content

熔断 #387

@jamesge

Description

@jamesge

在没有熔断时,client会依据指定的负载均衡策略把流量分给不同的server,当和某个server的连接出现问题时会隔离server(无法被负载均衡选中)并触发健康检查:每过一段时间连一下server直到 1.连上则恢复(停止隔离)2. server被移出名字服务,不属于集群无需再连。

但在一些业务场景中,即使连接完好也不意味着server能正常服务,如server返回大量严重错误时,即使client有重试,错误访问花费的时间也浪费掉了,此时最好也隔离掉server。从这个角度来说,熔断是比连上tcp更严格的“健康检查”条件。

从另一个角度,一些负载均衡算法已经惩罚了错误,一个错误频出的server只会分到很少的流量。之前想让负载均衡算法自己做这块,但目前看起来抽取出来被所有负载均衡共享是更好的。熔断扮演分流门槛的角色:一旦错误严重程度超过熔断阈值,则隔离一会儿,否则只是被负载均衡算法降权。这样可以进一步减少用于试错的流量比例。

熔断有以下实现难点:

  1. 通用性。若得配“针对哪些错误码”以及“错误码的严重程度”的话,维护起来还是非常困难的,且容易给后人埋坑。所以熔断得做到无场景相关的参数。和其他问题类似,以错误耗费的延时作为开销是一种通用方法。对于不同的方法可除以正常的延时以均一化。既要允许某个server在短时间内抖动,又要剔除掉长期持续出错的server,则要同时统计长短两个窗口内的错误,长窗口阈值低,短窗口阈值高,超过任一阈值触发熔断。长窗口可以基于短窗口的聚合数据做二次聚合以节省开销。

  2. 实现技巧。要统计精准的窗口内错误开销必须用队列,但这种与每个连接绑定且高频触发的逻辑用队列是不经济的,更适合用近似算法。一种常见的方法是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 enters
const int64_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 enters
int64_t val = _acc_error_cost.load(butil::memory_order_relaxed);
do {
  if (val == 0) {  // most common
    return;
  } else if (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)

具体实现还有很多细节,后续更新或在IM沟通。

  1. 恢复条件
    试图复制或回放请求来检查一个server是很困难的,由于分库、时效性、频控等等因素,请求未必可以想发哪就发哪,至少在框架层面上来看,不太通用。
    可行的方案要么是访问/health服务是否成功,要么是tcp连接是否成功。这两个都属于健康检查的范畴,与熔断是独立的。

  2. 颠簸问题。健康检查允许一些假阳性(client认为server挂了,但server没挂),一旦出现则会很快被tcp连上从而恢复正常。但如果假阳性出现频率很高,则可能出现大量无谓的后台检查。由于熔断条件会tcp连接召回更多的server,所以会引入更多的假阳性,颠簸出现的概率就更高了。另一个产生颠簸的因素是熔断的恢复条件,很可能出现满足熔断条件但server能通过健康检查的情况,从而触发颠簸。一个方法是健康检查前sleep一段时间,此时间可以固定,也可以指数增加。这部分的改动主要在 Make naming service from sync to async #319 做,会先于熔断功能合入。

Metadata

Metadata

Assignees

No one assigned

    Labels

    officialcreated by brpc authors

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions