一般情况下,在计算密集型服务中,即使处理单个请求也需要使用到服务器的所有 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 方式:
作者介绍:
罗茂林,携程国际机票后台研发总监,主要负责国际机票引擎的研发工作。致力于系统性能优化和研发效率提升。
本文转载自公众号携程技术(ID:ctriptech)。
原文链接:
https://mp.weixin.qq.com/s/XsyXkaOIJ3m8sdCt8Pl5Ig
评论