十四、流量控制

当资源成为瓶颈时,服务框架需要对消费者做限流,启动流控保护机制。

1 静态流控

主要针对客户端访问速率进行控制,它通常根据服务质量等级协定(SLA)中约定的 QPS做全局流量控制,例如订单服务的静态流控阈值为 100QPS,则无论集群有多少个订单服务实例,它们总的处理速率之和不能超过 100QPS。

1.1 传统静态流控设计方案

在软件安装时,根据集群服务节点个数和静态流控阈值,计算每个服务及诶单分摊的 QPS阈值,系统运行时,各个服务节点按照自己分配的阈值进行流控,对于超出流控阈值的请求则拒绝访问。

服务框架启动时,将本节点的静态流控阈值加载到内存中,服务框架通过 Handler拦截器咋服务调用前做拦截计数,当计数器在指定周期 T到达 QPS上限时,启动流控,拒绝信的请求消息接入。注意:

  1. 服务实例通常由多线程执行,因此计数时需要考虑线程并发安全,可以使用 Atomic原子类进行原子操作。
  2. 达到流控阈值之后拒绝新的请求消息接入,不能拒绝后续的应答消息,否则这会导致客户端超时或者触发 FailOver,增加服务端的负载。

1.2 传统方案的缺点

  1. 云端服务的弹性伸缩性使服务节点数处于动态变化过程中,预分配方案行不通。
  2. 服务节点宕机,或者有新的服务节点动态加入,导致服务节点数发生变化,静态分配的 QPS需要实时动态调整,否则会导致流控不准。

当应用和服务迁移到云上之后, PaaS 平台的一个重要功能就是支持应用和服务的弹性伸缩,在云上,资源都是动态分配和调整的,静态分配阈值方案无法适应服务迁移到云上。

1.3 动态配额分配制

原理:由服务注册中心以流控周期 T为单位,动态推送每个节点分配的流控阈值 QPS。当服务节点发生变更时,会触发服务注册中心重新计算每个节点的配额,然后进行推送,这样无论是新增还是减少服务节点数,都能够在下一个流控周期内被识别和处理。

而在生产环境中,每台机器/VM 的配置可能不同,如果每个服务节点采用流控总阈值/服务节点数这种平均主义,可能会发生性能高、处理快的节点配额很快用完,但是性能差的节点配额有剩余的情况,这会导致总的配额没用完,但是系统却发生了静态流控的问题。
解决方案一:根据各个服务节点的性能 KPI数据(例如服务调用平均时延)做加权。
解决方案二:配额指标返还和重新申请,每个服务节点根据自身分配的指标值、处理速率做预测,如果计算结果表明指标会有剩余,则把多余的返还给服务注册中心;对于配额已经使用完的服务节点,重新主动去服务注册中心申请配额,如果连续 N次都申请不到新的配额指标,则对于新接入的请求消息做流控。

结合负载均衡进行静态流控,才能够实现更精确的调度和控制。消费者根据各服务节点的负载情况做加权路由,性能差的节点路由到的消息更少,这样保证了系统的负载均衡和配额的合理分配。

1.4 动态配额申请制

尽管动态配额分配制可以解决节点变化引起的流控不准问题,也能改善平均主义配额分配缺点如下:

  1. 如果流控周期 T比较大,各服务节点的负载情况变化比较快,服务节点的负载反馈到注册中心,统一计算后再做配额均衡,误差会比较大。
  2. 如果流控周期 T比较小,服务注册中心需要实时获取各服务节点的性能 KPI数据并计算负载情况,经过性能数据采集、上报、汇总和计算之后会有一定的时延,这会导致流控滞后产生误差。
  3. 如果采用配额返还和重新申请方式,则会增加交互次数,同时也会存在时序误差。
  4. 扩展性差,负载的汇总、计算和配额分配、下发都由服务注册中心完成,如果服务注册中心管理的节点数非常多,则服务注册中心的计算压力就非常大,随着服务节点数的增加服务注册中心配额分配效率会急速下降、系统不具备平滑扩展能力。

而动态配额申请制,工作原理如下:

  1. 系统部署的时候,根据服务节点数和静态流控 QPS阈值,拿出一定比例的配额做初始分配,剩余的配额放在配额资源池中。
  2. 哪个服务节点使用完了配额,就主动向服务注册中心申请配额。配额的申请策略是,如果流控周期为 T,则将周期 T分成更小的周期 T/N(N为经验值,默认值为 10),当前的服务节点数为 M个,则申请的配额为(总 QPS配额-已经分配的 QPS)/ M * T / N。
  3. 总的配额如果被申请完,则返回 0 配额给各个申请配额的服务节点,服务节点对新接入的请求消息进行流控。

动态配额申请制的优点:

  1. 各个服务节点最清楚自己的负载情况,性能 KPI数据在本地内存中计算获得,实时性高。
  2. 由各个服务节点根据自身负载情况去申请配额,保证性能高的节点有更高的配额,性能差的自然配额就少,实现合理资源,流控的精确性。

2 动态流控

动态流控的最终目标是为了保命,并不是对流量或者访问速度做精确控制。
触发动态流控的因子是资源,资源又分为系统资源和应用资源两大类,根据不同的资源负载情况,动态流控又分为多个级别,每个级别流控系数都不同,也就是被拒绝掉的消息比例不同。每个级别都有相应的流控阈值,这个阈值通常支持在线动态调整。

2.1 动态流控因子

动态流控因子包括系统资源和应用资源两大类,常见的系统资源包括:

  1. 应用进程所在主机/VM 的 CPU使用率。
  2. 应用进程所在主机/VM 的 内存使用率。

使用 java.lang.Process 执行 top、sar 等外部命令获取系统资源使用情况。
常用的应用资源:

  1. JVM 堆内存使用率
  2. 消息队列积压率
  3. 会话积压率

具体实现策略是系统启动时拉起一个管理线程,定时采集应用资源的使用率,并刷新动态流控的应用资源阈值。

2.2 分级流控

不同级别拒掉的消息比例不同,例如一级流控拒绝掉 1/8 的消息;发生二级流控时,拒绝掉 1/4 消息。
为了防止系统波动导致的偶发性流控,无论是进入流控状态还是从流控状态恢复,都需要连续采集 N次并计算平均值,如果连续 N次平均值大于流控阈值,则进入流控状态。
而在一个流控周期内,不会发生流控级别的跳变。

3 并发控制

并发控制针对线程的并发执行数进行控制,它的本质是限制对某个服务或者服务的方法过度消息,耗用过多的资源而影响其它的服务的正常运行。有两种形式:

  1. 针对服务提供者的全局控制。
  2. 针对服务消费者的局部控制。

4 连接控制

通常分布式服务框架服务提供者和消费者之间采用长连接私有协议,为了防止因为消费者连接数过多导致服务端负载压力过大,系统需要针对连接数进行流控。

5 并发和连接控制算法

并发连接的控制算法原理如下图:

基于服务调用 Pipeline 机制,可以对请求消息接收和发送、应答消息接收和发送、异常消息等做切面拦截(类似 Spring 的 AOP 机制,但是没采用反射机制,性能更高),利用 Pipeline 拦截切面接口,对请求消息做服务调用前的拦截和计数,根据计数器做流控,服务端的算法如下:

  1. 获取流控阈值。
  2. 从全局 RPC上下文中获取当前的并发执行数,与流控阈值对比,如果小于流控阈值,则对当前的计数器做原子自增。
  3. 如果等于或者大于流控阈值,则抛出 RPC流控异常给客户端。
  4. 服务调用执行完成之后,获取 RPC上下文中的并发执行数,做原子自减。

客户端的算法如下:

  1. 获取流控阈值。
  2. 从全局 RPC上下文中获取当前的并发执行数,与流控阈值对比,如果小于流控阈值,则对当前的计数器做原子自增。
  3. 如果等于或大于流控阈值,则当前线程进入 wait状态, wait超时时间为服务调用的超时时间。
  4. 如果有其它线程服务调用完成,调用计数器自减,则并发执行数小于阈值,线程被 notify,退出 wait,继续执行。

6 个人总结

流量控制是保证服务 SLA(Sevice-Level Agreement)的重要措施,也是业务高峰期故障预防和恢复的有效手段,分布式服务框架需要支持流控阈值、策略的在线调整,不需要重启应用即可生效。