计算密集型服务的负载均衡策略

2020-05-25 00:00:00 队列 服务器 请求 策略 响应时间

一般情况下,在计算密集型服务中,即使处理单个请求也需要使用到服务器的所有CPU。如果单台服务器连续接收到两个请求,要么两个请求互相争抢CPU,要么后来的请求排在前面的后面等待处理。终,会导致平均处理时间变长。常规的负载均衡策略(如轮询、随机等)下,负载均衡器不关心服务器的负载情况,这就很容易造成服务器同时收到多个请求,从而使服务器的服务质量下降。

一、背景

有一天,携程国际机票查询引擎经过一次改造后,虽然平均响应时间得到了提升,但是响应时间也有非常大的波动。从监控图上看,非常明显的尖刺持续存在。如下图:

经过分析,我们发现这次改造深度优化了服务的并行计算能力,使得引擎成为了一个完全的计算密集型服务,它的大并发处理能力为1。然而,我们却没有相应的修改负载均衡策略,而是继续使用的轮询策略。

对于计算密集型服务,如果使用轮询策略,有如下三种情况:

服务器架构

通常情况下,由于请求的到达普遍服从泊松分布,如果使用轮询、随机等负载均衡策略,单机的请求也服从泊松分布,即连续两个请求间总会存在间隔或者重叠,导致服务器资源空闲或者请求响应时间上升。

在极端情况下,如果某个请求的处理时间特别长,后续的一大串请求将产生积压,终导致这些请求的响应时间也变得特别长,甚至超时。

我们发现,引擎的响应时间尖刺是由极端情况的case造成的。引擎有一类请求A,它qps不高,但是却需要CPU满负荷运转长达几秒甚至10秒才能算出结果。另有一类请求B,它qps非常高,只需要CPU满负荷运转几十毫秒就能算出结果。

当一台服务器正在处理一个A类请求时,在接下来的几秒内,它将继续收到几十个B类请求,而且所有的B类请求都要排队,直到A类请求完成。这就导致大批B类请求的响应时间由应该的几十毫秒升高到几秒,从而造成了严重的尖刺。

二、pooling

为了解决这个问题,我们使用了一种新的负载均衡策略,在这种策略下,服务器不再被动的接收请求,而是主动的去获取请求,这种方式非常容易做到服务器同一时刻只处理一个请求。在我们内部,这种方式被称为pooling(它和线程池类似,可以叫做服务器池)。

在pooling模式中,有三个主要角色:submitor、queue、worker。

  • submitor

submitor一方面用于接收请求方的调用,它收到请求后,不直接处理请求,而是把这个请求提交给queue。

另一方面,submitor接收worker的回调,submitor收到worker的结果后,直接把它转发给请求方。

  • queue

pooling的关键是引入了一个queue,queue是一个全局队列,用于暂时缓冲请求。

我们使用了redis的list结构来实现queue。入队操作为lpush,出队操作为brpop。brpop是阻塞式的操作,当队列为空时,brpop会阻塞直到队列非空。队列非空时,如果有该队列有多个brpop操作阻塞,只有其中一个会被唤醒并且返回数据。

  • worker

worker是实际的请求处理者。在旧的模式下,worker是被动接收请求。在pooling模式下,worker要主动去queue获取请求。worker启动时,要创建一个线程,这个线程启动后,便进入一个无限循环,循环的主要内容为:

1)从queue获取一个请求,当queue没有请求时,worker被阻塞。

2)worker处理这个请求。

3)把结果返回给submitor。

如此往复。可以看到,worker要么正在处理一个请求,要么正在等待一个请求。

三、效果

国际机票查询引擎的负载均衡策略由轮询改为pooling后,效果非常好。系统的平均响应时间降低了大约20%,并且完全消除了响应时间尖刺。

轮询方式:

pooling方式:

【作者简介】罗茂林,携程国际机票后台研发总监,主要负责国际机票引擎的研发工作。致力于系统性能优化和研发效率提升。

更多携程技术人一手干货,欢迎搜索关注“携程技术中心”微信公众号~

相关文章