为什么要有调度算法?
所有服务器,都有一个能处理请求的qps上限,超过这个上限就会有丢包的风险,这个时候我们必须对服务器进行扩容。
扩容有两种方法,一种是增加服务器的硬件资源(scale up纵向扩容),这种方法比较简单,插块卡就行了,但是如果要增加计算网络资源的话,可能需要重启服务器,而且卡槽总有插满的一天。另一种方法是增加服务器的个数(scale out横向扩容),目的是为了让请求均匀的分配到多台服务器上,减少单台服务器的压力,并且结合健康监测可以避免单机故障。scale out的扩展能力明显要比scale up要好,理论上可以无限往上扩展。
那怎么让请求可以均分到每一台服务器上呢,就得靠我们熟知的负载均衡器了。我们比较常见的负载均衡器有lvs、nginx等,作为服务器的前端,流量首先经过负载均衡器,然后负载均衡器的调度器根据对应的调度算法,将请求转发给合适的服务器。
常用负载均衡算法
rr-轮询算法
轮询调度算法,顾名思义,就是将请求轮训的调度到每一台服务器上,是最简单的一种调度算法。这个算法的目的就是想做到一个“绝对的公平”。
wrr-加权轮询算法
轮询算法把请求平均分配给后端服务器,所以每个服务器的负载都是一样的,如果后端服务器的性能不一样,就有可能出现性能比较差的服务器挂掉的情况。所以就出现了加权轮询算法,权重值其实就是服务器的处理能力,权重值越高的机器处理的请求就越多。
wlc-加权最小连接算法
前面的加权轮询算法是一种无状态的算法,它不会去管服务器的真实负载情况。最小连接算法会记录所有服务器的连接建立情况,调度器总是把请求调度给连接数最少的服务器上。wlc尽可能让服务器的已有连接数和权重值成比例。
hash-哈希算法
hash算法首先对每台服务器的ip+port进行hash,然后根据请求的一个特定元组来计算出请求的hash值,最后将这个请求发送给hash值更接近的服务器。这个特定的元组可以自己设定,可以是源地址、源端口、目的地址、目的端口、协议中的任意几项。哈希算法可以保证同一个客户端或者同一个四元组(会话)的请求永远只被同一台服务器处理,如果四元组足够散列,那请求理论上也是均分到各个服务器上的。
一致性hash算法
在实际运维过程中,服务器变更(增删服务器)的操作十分频繁,对普通的hash算法来说,每次变更服务器,调度器都会重新计算服务器的hash值,这样即使是同一个四元组(会话),在变更前后,也会调度到不通的服务器上。一致性hash就是为了在服务器变更后,也尽可能的让同一个会话调度到同一个rs上,保持连接的一致性。比较常用的一致性hash算法有hash环和maglev hash,具体内容可以查看这篇文章:https://writings.sh/post/consistent-hashing-algorithms-part-4-maglev-consistent-hash。
为什么会出现调度不均的情况?
可以看到调度算法本质上的目的都是为了让请求均匀的分配到每台服务器上,但是算法都是有缺陷的,在实际生产过程中,经常会遇到调度不均的情况。主要有以下几个原因:
- wrr-原生wrr算法算法有一个bug。调度算法每次出现健康检查抖动的时候,都会重置调度器,重新从头开始调度。rr算法加入了一个随机因子,每次重置后会随机选择一个服务器,而wrr算法没有加入这个随机因子,所以如果健康检查抖动非常频繁的话,有可能排在前面的服务器上的请求比后面的服务器多很多。
- rr wrr-客户端使用长连接。如果客户端使用了很多长连接请求,某个服务器挂掉重启或者新添加了服务器,其他服务器上的长连接依然存在,在分配新连接的时候,调度器仍然根据权重来分配,这样机会导致重启的服务器或者新添加的服务器上的连接比其他服务器上的少很多。
- wrr wlc-多核调度。我们现在的负载均衡器往往是采用的一个多核的架构,每个核的调度算法都是独立的,而且所有 worker 以相同的方式初始化,所以每个 worker 会以相同的顺序选取 RS。比如,对于轮询(rr)调度,所有 worker 上的第一个连接都会选择 RS 列表中的第一台服务器。下图给出了 8 个 worker, 5 个 RS 的调度情况:假设 RSS HASH 算法是平衡的,则很可能前 8 个用户连接分别哈希到 8 个不同 worker 上,而 8 个 worker 独立调度,将 8 个用户流量全都转发到第一个 RS 上,而其余 4 个 RS 没有用户连接,使得负载在 RS 上分布很不均衡。
- wlc-并发创建连接。假设短时间内有一批syn冲过来(同时并发创建一批连接),必然有一个服务器先建立第一个active的连接,在第二个服务器也建立第一个active连接之前,后面的连接都会发给第二个服务器,那么最终会看到第二个服务器的连接远大于第一个服务器,这样就导致了最终连接数的负载不均衡。服务器到负载均衡器之间的时延差异会放大这个不均衡。
- hash-客户端ip+port不够散列。这个很好理解,毕竟是hash算法,如果客户端的ip+port不够散列,就更容易出现hash碰撞。
怎么选择合适的调度算法
针对上章节的第一个和第三个问题,我们可以通过修改代码或者使用hash算法的方法来解决。
比较有意思的是第二个问题,这个问题的本质原因是wrr算法是无状态的,没有记录后端连接情况,那看起来可以用wlc算法来完美解决该问题。但是wlc算法在生产环境中却很少使用,因为它有一个非常非常严重的问题:强依赖于健康检查。假设我们有一台服务器挂掉了,这台服务器上的连接全部断掉,那这台服务器就一定是连接最少的。从服务器挂掉到负载均衡器监测出异常的这段时间,所有的请求都会打到这台有问题的服务器上,那这段时间所有的请求都会是失败的。负载均衡器的健康检查是需要时间的,在几十秒内服务完全不可用是很难接受的,而且万一健康检查没检测出来异常怎么办?
- 尽可能不使用wlc算法,原因如上。
- 如果需要保证连接的一致性,那只能选择一致性hash算法。
- 如果客户端比较固定,比如内网场景,只有一两个固定的客户端,不够散列,建议不使用hash算法,使用wrr算法。
- hash算法会占用比较多的内存,后端服务器越多,占用的内存也就越多,hash算法支持的后端服务器数量一定有个上限值,如果超过这个上限值,负载均衡器会crash。所以如果服务器特别多,建议不使用hash算法,使用wrr算法。
- 由于wrr的bug比较多,所以其他情况下可以使用hash算法。