对微信《Scalable Overload Control for Large-scale Microservice Architecture》论文的解读

这几年微服务大行其道,随之而来也伴随着很多问题:

  • 服务注册和发现(常见解决方案如Consul, ZK, etcd)
  • 熔断和降级 (如Hystrix)

在服务过载时, 一个常见的解决方案是熔断。以前的熔断方案基本都是各个服务独自处理的, 即如果一个服务无法处理所有的请求, 那么就抛弃一部分请求。这种粗暴的处理方式并不能缓解整个系统的负载, 并且浪费了很多计算资源,比如以下场景:

假设一个客户端请求依赖N个下游服务, 这N个下游服务都处于过载状态, 以p的概率随机拒绝服务, 那么这个客户端请求最后被成功处理的概率就是(1-p)^N。 如果P和N很大, 那么这个请求很可能没有被成功处理,但是却浪费了很多计算资源(比如N-1个服务都处理了这个请求,但是1个服务拒绝了)。

微信是怎么解决这个问题的呢? 主要是以下两点:

  1. 面向业务的准入控制
  2. 面向用户的准入控制

下面就来详细说说这两点分别是什么意思。

面向业务的准入控制

微信根据业务重要性和用户体验,对一些常见的业务定义了优先级:

比如两个同样依赖红包服务的业务:发红包和查看红包历史记录, 他们的业务优先级是不一样的, 发红包明显要高于查看历史记录。 因此,微信在入口服务层通过预定义的hash表, 获取当前请求的优先级,并透传给下游业务。 于是下游业务在过载的时候,就不用随机拒绝请求,而是可以根据业务优先级来处理。

业务优先级的hash表很少变化,如果在表中没有配置,可以认为是默认优先级(最低)。

面向用户的准入控制

面向业务的准入控制,是根据业务优先级做出拒绝请求的决定。 比如当前服务过载时, 调整为只处理业务优先级t以上的请求, 如果这时候不过载了, 再尝试处理优先级t+1 的请求。 这会出现业务优先级抖动的情况: t+1过载, 调整为t; t不过载,调整为t+1, 然后循环往复……

因此,我们必须做到能够部分丢弃业务优先级是t+1的请求。 但是如果随机丢弃,则又会面临上述浪费资源、系统整体成功率低下的问题。

对于这种情况,微信提出了面向用户的准入控制: 每个入口服务中, 都有一个hash函数, 根据userid和这个hash函数,生成请求的用户优先级。 下游服务除了用业务优先级以外,还会用到这个用户优先级来做拒绝服务的判断。

hash函数每隔一段时间会更新一次(这个时间可以根据经验来设定),这样做有以下好处:

  1. 单个用户的优先级在这段时间内是固定的,能够让用户接受的服务有一定的持续性
  2. 对于所有用户而言,维持了一定的公平性, 大家的优先级是轮着来的

整体工作流程

解释了两种准入控制的方法,我们来看看这个框架整体的工作流程:
对微信《Scalable Overload Control for Large-scale Microservice Architecture》论文的解读

  1. 用户请求到达入口服务,入口服务为这个请求设置业务优先级和用户优先级, 并透传给下游服务
  2. 下游服务收到请求, 根据当前的服务准入级别, 执行准入控制。 服务的准入级别是根据当前服务的负载而定的。(微信用每个请求在队列中的平均等待时间来判定是否过载)
  3. 服务在发送后续请求给下游服务时, 会根据本地存储的下游服务的准入级别来决定是否要发送
  4. 下游服务向上游服务返回响应,并且带上服务当前的准入级别
  5. 上游服务收到响应后,更新本地存储的下游服务的准入级别