基于 QPS 与资源利用率转换的过载应对

QPS(Query Per Second):每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。我们常常使用 QPS值来衡量一个服务器的性能。

$$ QPS = 并发数 / 平均响应时间 $$

$$ 并发数 = QPS * 平均响应时间 $$

一个系统吞吐量通常由QPS、并发数两个因素决定,每套系统的这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降。所谓吞吐量这里可以理解为每秒能处理请求的次数。

对于时延敏感的服务,当外部请求超过系统处理能力,如果系统没有做相应保护,可能导致历史累计的超时请求达到一定规模,像雪球一样形成恶性循环。由于系统处理的每个请求都因为超时而无效,系统对外呈现的服务能力为 0,且这种情况下不能自动恢复。极端过载情况下,该服务甚至可能连这种降级回复都无法生成和发送。

不同的请求可能需要数最迥异的资源来处理。某个请求的成本可能由各种各样的因素决 定,例如客户端代码的不同(有的服务有很多种客户端实现),或者甚至是当时的现实时间(家庭用户和企业用户,交互型请求和批量请求)。

按照 QPS 来规划服务容量,或者是按照某种静态属性(认为其能指代处理所消耗的资源:例如某个请求所需要读取的键值数量)一般是错误的选择。就算这个指标在某一个时间段内看起来运作作还算良好,但早晚也会发生变化。有些变动是逐渐发生的,有些则是非常突然的(例如某个软件的新版本突然使得某些请求 消耗的资源大幅减少)。这种不断变动的目机,使得设计和实现良好的负载均衡策略使用起来非常困难。

更好的办法是使用可用资源来衡量可用容量。比如预留了 100 个 CPU 和 1 TB 的内存的话,可以用这种资源的数字来描绘处理请求的成本。比如我们发现在有 GC 的情况下,内存压力会增大 CPU 压力,那么可以将某个请求的成本定义为 CPU 的消耗时间(因为 CPU 消耗的资源包含了内存 GC 的成本,与内存占用成正比关系。)。

GC即垃圾回收,是指 runtime vm 用于释放那些不再使用的对象所占用的内存。在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。

那么我们可以把 QPS 的峰值转为需要的 CPU 资源、内存资源等等进行规划,那么对服务的限制的 CPU、内存等等就会直接等同于最大支持的 QPS。

那么这个时候我们对于服务的保护就变成基于资源利用率来实现的,大多数情况下,资源利用率指目前的某种动态资源的消耗程度,如 IO 密集型的以 CPU 消耗程度为准(目前 CPU 使用量),辅以内存使用率等等。如内存型的则优先以内存的消耗程度为主,其它为辅。

这里就不得不提到几个常见的过载保护的方式:

  • 熔断

熔断就是在系统濒临崩溃的时候,立即中断服务,从而保障系统稳定避免崩溃。  它类似于电器中的“保险丝”或“断路器”,当电流过大的时候,“保险丝”会先被烧掉,断开电流,以免电路过热烧毁电器引起火灾。软件系统中的熔断也是如此,当系统满足某个判断条件时,就会拒绝处理请求,避免系统压力过大被压垮。

  • 限流

限流的原理跟熔断有点类似,都是通过判断某个条件来确定是否执行某个策略。比如,如果并发请求达到 1000 QPS,那么系统会拒绝或者延迟处理后续请求。它的目的是确保系统高效、稳定地运行,确保请求能够快速处理的同时,保障系统不被流量压垮。

  • 降级

降级是指自己的待遇下降了,从RPC调用环节来讲,就是去访问一个本地的伪装者而不是真实的服务。

当双11活动时,把无关交易的服务统统降级,如查看蚂蚁深林,查看历史订单,商品历史评论,只显示最后100条等等。

那么我们依据 QPS 转换的资源占用率及剩余的资源的量可以反推承受的最高 QPS,为了以免万一我们会在计算时将 QPS 往上 + 20% 来给予更多的资源,避免突然暴增。同理我们在计算熔断、限流、降级时可以将最高 QPS 降低 20% 来设计过载保护的值。

比如 QPS 最高为 1000(可以通过极限压力测试得到),可以在设计限流时限制 800。为什么呢?

资源耗尽可能导致软件服务器崩溃。例如,这些服务器可能会由于内存超标而崩溃。一但几个服务由干过载而崩溃,其他软件服务的负载可能会上升,从而使它们也崩溃。这种问题经常如同滚雪球一样越来越严重,不多久全部服务器就会进入崩溃循环。这种场景经常很难恢发,因为一旦某个软件服务器恢复正常,它就会接收到大量请求的轰炸,几乎立即再次崩溃。

例如,如果某个服务在 1000 QPS 的水平下,正常服务,但是当 1100 QPS 的时候进入连锁故障模式,降低负裁到 900 QPS 通常也无法恢复。这是因为这时该服务仍然处于资源容易不足的状态;通常仅仅只有一小部分的软件服务可以正常处理请求。

而服务要恢复正常取决于重启速度、进入正常工作的实际、新启动的服务能承受过载请求的时间。那么在上面这个情况下,如果只有 10% 的任务可以正常处理的话,就要把 QPS 降到 100 才能够使系统恢复文档。所以建议留出一个上限空间,避免像滚雪球一样导致问题加剧。

还有种情况下是客户端遇到了请求服务器时超时、过载等情况,如果疯狂重试也会导致越来越难处理掉这么多的请求。建议客户端限制每个请求只能重试三次或是只有非常重要的请求才进行重试。其次是将重试的间隔时间取一个区间的随机数来减缓服务器的负担。

如果是服务和服务之间调用的话,一定要设置超时时间,以及也要约束重试次数和重试间隔时间。还有种办法是设置一个服务与另一个服务的全局超时时间,比如 A 需要调用 B 服务,那么 A 服务中可能有一个关于调用 B 服务的全局重试时间,当数量超过时,会在一段时间内不会再访问 B 服务,而是直接认为是超时。

在设计超时时间时,我们可以设置为 400 ms(理论上服务应该在 200 ms 内出结果,给予两倍的冗余)。

总结来说,我们将 QPS 与资源利用率的相互转换来更合理的去评估一个服务可承载的最大的量,同时我们通过以所需最大资源 + 20% 来给予资源,以所需最大资源对应的 QPS -20% 来设计限制流量从而避免雪崩效应。在这种情况下至少保证了 40% 的冗余空间,同时还剩下 20% 的可以调整的空间,比如在某些情况下可能调整限流就可以解决掉目前的流量,那么可以直接调整限流,同时仍然保证了无论什么情况都拥有 20% 的冗余。