在微服务中某个服务的请求突然升高会造成服务的过载,服务一旦过载进入异常状态之后会对服务的上下游都造成一定的影响。当服务出现过载的时候常见的处理方法是调用方进行熔断,服务方进行降级、流量抛弃等。

资源耗尽

假设一个服务最多能处理1000QPS的请求,当QPS超过1000之后随着请求量的增多会发生什么:

  1. CPU不足以应对这么多的请求则所有请求的响应时间变慢
  2. 响应时间变慢进而导致积压的in-flight请求变多
  3. in-flight请求增多会导致内存、活跃线程数(在每个请求一个线程的模式下)、文件描述符等资源的消耗增加
  4. 请求队列填满,所有请求都要排队进一步增加响应时间同时会使用更多的内存
  5. 如果跑的是带有垃圾回收的runtime程序内存使用率上升会导致GC触发的次数增多,进而导致CPU资源进一步减少
  6. 内存的过多消耗会导致内存中的缓存命中率降低,未命中缓存的请求发送给下游获取数据进而导致下游服务的连锁反应

当发生过载的情况时往往会伴随着高延迟、高错误率或者低质量的响应发生

应对过载

客户端

当服务发生过载错误率升高的时候对于调用方可以进行熔断容错处理,常见的是Netflix的Hystrix框架。当检测到错误率达到一定阈值之后调用方进行熔断,不再向服务方发送请求同时要对此进行容错处理,过一段时间之后再尝试向服务方发送一部分流量探测服务方是否正常,如果正常则恢复请求,如果不正常则继续熔断。

该熔断策略不好之处在于“一刀切”,熔断之后所有请求都不发送给服务方,调用方不能及时的感知到服务方的状态,动态调整的能力较弱。Google提出的一种自适应的机制可动态调整请求的发送量,客户端通过滑动窗口记录过去两分钟的请求数量(requests)和被服务端正常接受的请求数量(accepts)。

正常情况下这两个数值是相等的,当后端开始出现请求超时、返回错误等拒绝一部分请求时,accepts的值将降低,直到超过requests = K*accepts时客户端开始自行节流熔断,请求将以一定的概率被客户端直接拒绝,概率计算如下:$$ drop=max(0, \frac {requests -K * accetps} {requests+1})$$ K通常设置为2,开始时requests=accetps,drop值为0。当后端开始拒绝服务导致requests > K*accepts此时客户端开始拒绝请求,requests会继续上升导致拒绝的概率变大。当后端服务恢复之后accepts值增大
且由于K的存在K*accepts的增速比requests快因此会逐渐降低drop值最终恢复正常的状态。

服务端

服务端应对过载常见的做法有以下几种:

  1. 流量抛弃,当请求速率超过线程处理速率时会产生排队,此时线程池和队列都会饱和,当出现队列饱和时可以尽早的拒绝请求。对于一些排队很久的请求可以不处理因为客户端可能已经发起重试了,对这种请求进行处理没有意义。如果请求有优先级可对某些低优先级的请求进行拒绝,进一步降低资源消耗
  2. 降级,通过降低响应的质量来减少资源的消耗,例如:推荐系统可以减少召回数量或者直接返回静态数据等
  3. 传递和判断请求的截止时间,在多层级的RPC请求中传递截止时间,如果已经超过截止时间则不再向下传递请求
  4. 为预防过载服务端针对不同的客户端设置一定配额的限流,防止某个客户端的流量突增导致整个服务不可用进而影响其他客户端

小心负载均衡和重试

当某个集群实例发生过载而崩溃时负载均衡会把请求转发到其他的集群,这会导致其他的集群也相继过载从而导致整个服务过载,如同滚雪球一样整个服务进入循环崩溃且比较难以恢复,一旦某个实例恢复就会被流量立即打垮。

对于重试应注意以下几点:

  1. 区分可重试与不可重试的错误处理,对于服务端明确返回的不可重试错误不应进行重试
  2. 限制请求重试次数,不能无限重试
  3. 客户端设置全局重试次数
  4. 在较长的调用链路中避免多级重试,进而放大重试次数
  5. 使用随机的、指数避让式的重试,避免同一时刻发起大量重试

最后

服务一旦出现滚雪球式的过载往往对整个系统造成较大影响,需要在平时把容量估算、水位监控、限流、降级等工作做好,才能防患未然。