我们都对高可用有一个基本的认识,其中负载均衡是高可用的核心工作。本文将通过如下几个方面,让你妥妥的吃透“负载均衡”。
- 负载均衡是什么
- 常用负载均衡策略图解
- 常用负载均衡策略优缺点和适用场景
- 用健康探测来保障高可用
- 结语
负载均衡是什么
正如上图所示的这样,由一个独立的统一入口来收敛流量,再做二次分发的过程就是负载均衡,它的本质和分布式系统一样,是分治。
如果大家习惯了开车的时候用一些导航软件,我们会发现,导航软件的推荐路线方案会有一个数量的上限,比如 3 条、5 条。
因此,其实本质上它也起到了一个类似负载均衡的作用,因为如果只能取 Top3 的通畅路线,自然拥堵严重的路线就无法推荐给你了,使得车流的压力被分摊到了相对空闲的路线上。
在软件系统中也是一样的道理,为了避免流量分摊不均,造成局部节点负载过大(如 CPU 吃紧等),所以引入一个独立的统一入口来做类似上面的“导航”的工作。
但是,软件系统中的负载均衡与导航的不同在于:导航是一个柔性策略,最终还是需要使用者做选择,而前者则不同。
怎么均衡的背后是策略在起作用,而策略的背后是由某些算法或者说逻辑来组成的。
比如,导航中的算法属于路径规划范畴,在这个范畴内又细分为静态路径规划和动态路径规划,并且,在不同的分支下还有各种具体计算的算法实现,如 Dijikstra、A* 等。
同样的,在软件系统中的负载均衡,也有很多算法或者说逻辑在支撑着这些策略,巧的是也有静态和动态之分。
常用负载均衡策略图解
下面来罗列一下日常工作中最常见的 5 种策略。
轮询
这是最常用也最简单策略,平均分配,人人都有、一人一次。大致的代码如下:
- int globalIndex = 0; //注意是全局变量,不是局部变量。
- try
- {
- return servers[globalIndex];
- }
- finally
- {
- globalIndex++;
- if (globalIndex == 3)
- globalIndex = 0;
- }
加权轮询
在轮询的基础上,增加了一个权重的概念。权重是一个泛化后的概念,可以用任意方式来体现,本质上是一个能者多劳思想。
比如,可以根据宿主的性能差异配置不同的权重。大致的代码如下:
- int matchedIndex = -1;
- int total = 0;
- for (int i = 0; i < servers.Length; i++)
- {
- servers[i].cur_weight += servers[i].weight;//①每次循环的时候做自增(步长=权重值)
- total += servers[i].weight;//②将每个节点的权重值累加到汇总值中
- if (matchedIndex == -1 || servers[matchedIndex].cur_weight < servers[i].cur_weight) //③如果 当前节点的自增数 > 当前待返回节点的自增数,则覆盖。
- {
- matchedIndex = i;
- }
- }
- servers[matchedIndex].cur_weight -= total;//④被选取的节点减去②的汇总值,以降低下一次被选举时的初始权重值。
- return servers[matchedIndex];
这段代码的过程如下图的表格。"()"中的数字就是自增数,即代码中的 cur_weight。
值得注意的是,加权轮询本身还有不同的实现方式,虽说最终的比例都是 2:1:2。
但是在请求送达的先后顺序上可以有所不同。比如「5-4,3,2-1」和上面的案例相比,最终比例是一样的,但是效果不同。
「5-4,3,2-1」更容易产生并发问题,导致服务端拥塞,且这个问题随着权重数字越大越严重。
例子:10:5:3 的结果是「18-17-16-15-14-13-12-11-10-9,8-7-6-5-4,3-2-1」
最少连接数
这是一种根据实时的负载情况,进行动态负载均衡的方式。维护好活动中的连接数量,然后取最小的返回即可。大致的代码如下:
- var matchedServer = servers.orderBy(e => e.active_conns).first();
- matchedServer.active_conns += 1;
- return matchedServer;
- //在连接关闭时还需对active_conns做减1的动作。
最快响应
这也是一种动态负载均衡策略,它的本质是根据每个节点对过去一段时间内的响应情况来分配,响应越快分配的越多。
具体的运作方式也有很多,上图的这种可以理解为,将最近一段时间的请求耗时的平均值记录下来,结合前面的加权轮询来处理,所以等价于 2:1:3 的加权轮询。
题外话:一般来说,同机房下的延迟基本没什么差异,响应时间的差异主要在服务的处理能力上。
如果在跨地域(例:浙江->上海,还是浙江->北京)的一些请求处理中运用,大多数情况会使用定时「Ping」的方式来获取延迟情况,因为是 OSI 的 L3 转发,数据更干净,准确性更高。
Hash 法
Hash 法的负载均衡与之前的几种不同在于,它的结果是由客户端决定的。通过客户端带来的某个标识经过一个标准化的散列函数进行打散分摊。上图中的散列函数运用的是最简单粗暴的取余法。
题外话:散列函数除了取余之外,还有诸如变基、折叠、平方取中法等等,此处不做展开,有兴趣的小伙伴可自行查阅资料。
另外,被求余的参数其实可以是任意的,只要最终转化成一个整数参与运算即可。
最常用的应该是用来源 IP 地址作为参数,这样可以确保相同的客户端请求尽可能落在同一台服务器上。
常用负载均衡策略优缺点和适用场景
我们知道,没有完美的事物,负载均衡策略也是一样。上面列举的这些最常用的策略也有各自的优缺点和适用场景,我稍作了整理,如下。
这些负载均衡算法之所以常用也是因为简单,想要更优的效果,必然就需要更高的复杂度。
比如,可以将简单的策略组合使用、或者通过更多维度的数据采样来综合评估、甚至是基于进行数据挖掘后的预测算法来做。
用健康探测来保障高可用
不管是什么样的策略,难免会遇到机器故障或者程序故障的情况。所以要确保负载均衡能更好的起到效果,还需要结合一些健康探测机制。定时的去探测服务端是不是还能连上,响应是不是超出预期的慢。
如果节点属于“不可用”的状态的话,需要将这个节点临时从待选取列表中移除,以提高可用性。一般常用的健康探测方式有 3 种。
HTTP 探测
使用 Get/Post 的方式请求服务端的某个固定的 URL,判断返回的内容是否符合预期。一般使用 HTTP 状态码、Response 中的内容来判断。
TCP 探测
基于 TCP 的三次握手机制来探测指定的 IP + 端口。最佳实践可以借鉴阿里云的 SLB 机制,如下图:
▲图片来源于阿里云,版权归原作者所有
值得注意的是,为了尽早释放连接,在三次握手结束后立马跟上 RST 来中断 TCP 连接。
UDP 探测
可能有部分应用使用的是 UDP 协议。在此协议下可以通过报文来进行探测指定的 IP + 端口。最佳实践同样可以借鉴阿里云的 SLB 机制,如下图:
▲图片来源于阿里云,版权归原作者所有
结果的判定方式是:在服务端没有返回任何信息的情况下,默认是正常状态。否则会返回一个 ICMP 的报错信息。
结语
用一句话来概括负载均衡的本质是:将请求或者说流量,以期望的规则分摊到多个操作单元上进行执行。
通过它可以实现横向扩展(scale out),将冗余的作用发挥为高可用。另外,还可以物尽其用,提升资源使用率。