彻底弄懂负载均衡

我们都知道,在分布式系统中,业务流程的执行会涉及到多个服务实例之间的协同工作。那么为了提高系统的整体效率和吞吐量,就必须要能最大程度地发挥每个节点的作用,而负载均衡就是保证系统运行效率的关键技术,这也正是我们今天所要探讨的主要话题。

那么接下来,我们就先通过一个具体的案例场景,来理解下负载均衡的主要作用吧。

负载均衡是如何保证软件系统的生产部署的

假如你要设计一个分布式服务系统,这个系统中存在一批能够独立运行的服务,并且在部署上采用了集群模式,也就是把多个服务实例集中在一起,对外提供同一业务功能,这样用户任意的访问请求都可以由集群中的某一个服务实例进行响应,从而防止出现单点故障,以此实现高可用。如下图所示:

不过这样问题也就来了:用户的一次访问请求应该由哪个服务实例来响应最为合适呢?这个问题看上去很简单,实际上却要复杂得多,因为它涉及到服务请求的路由机制。

负载均衡就是最为常见的路由机制实现方案,能够根据请求来选择合适的服务实例,也就是可以把请求的流量转发到集群中的各个服务实例中,从而达到流量分摊效果。而支持这个实现过程的技术组件,就叫做负载均衡器。

那么现在,我们来看下负载均衡器的基本结构:

你可以看到,来自客户端的请求通过中间的负载均衡器被分发到了各个服务实例当中,然后根据分发策略的不同,会产生不同的分发结果。

这里你可以先停下来考虑一个基础性的问题:负载均衡器想要实现请求分发的前提是什么?

很显然,那就是负载均衡器需要掌握当前各个服务实例的运行时状态,也就是说,它需要持有当前的服务实例列表信息。

负载均衡分发策略

那么,当负载均衡器拥有了当前的服务实例列表信息后,具体会怎么分发呢?

请求由谁来分发?

首先要明确的是,请求是由谁来分发?针对这个问题,我们根据服务实例地址列表所存放的位置不同,可以把负载均衡器分成两大类,一类是服务器端负载均衡器,一类是客户端负载均衡器。

服务器端负载均衡器

我们先来看服务器端负载均衡的结构:

可以看到,在客户端与服务实例集群之间,存在一个独立的负载均衡服务器,这台负载均衡服务器就负责将接收到的各个请求转发到运行中的某个服务实例上。

提供服务器端负载均衡的工具有很多,比如常见的Apache、Nginx、HAProxy等,都实现了基于HTTP协议或TCP协议的负载均衡模块。基于服务器端的负载均衡机制的实现也比较简单,只需要在客户端与各个服务实例之间,架设集中式的负载均衡器即可。

此外,负载均衡器与各个服务实例之间还需要实现服务诊断以及状态监控,通过动态获取各个服务实例的运行时信息,来决定负载均衡的目标服务。如果负载均衡器检测到某个服务实例已经不可用,就会自动移除该服务实例。

这么一分析,我们就能知道,服务器端的负载均衡器是运行在一台独立的服务器上,它会充当代理(Proxy)的作用,所有的请求都需要通过负载均衡器的转发才能实现服务调用。这是它的特点,但也是一个问题。

因为当服务请求量越来越大时,服务器端负载均衡器就会成为系统的瓶颈。同时,一旦负载均衡器自身出现异常,整个服务的调用过程都会失败。

因此,在分布式架构中,为了避免服务器端这种集中式的负载均衡所带来的这种问题,我们可以采用客户端负载均衡的请求分发模式。

客户端负载均衡

和服务器端负载均衡器部署在服务集群前端的形式不同,在客户端负载均衡器中,服务实例信息是保存在各个客户端的内部。这时候,目标服务实例地址是由客户端自身通过一定的调度算法来决定的,结构如下:

相比服务器端负载均衡,客户端负载均衡机制的主要优势就是不会出现集中式负载均衡所产生的瓶颈问题,因为每个客户端都有自己的负载均衡器,即使单个负载均衡器失效,也不会造成严重的后果。

不过客户端负载均衡也同样存在一个缺陷,就是由于所有服务实例运行时信息,都需要在多个负载均衡器之间进行传递,因此会在一定程度上加重网络流量负载。

好了,现在你应该就清楚"请求由谁来分发"这个问题的答案了吧。这个核心问题实际上也引出了一个新的概念,就是用于执行负载均衡的调度算法,这也是我们要讨论的第二个问题:"请求分发到哪去?"

请求分发到哪去

要知道,无论是使用服务器端负载均衡还是客户端负载均衡,运行时的分发策略都决定了负载均衡的最终效果。

分发策略在软件负载均衡中的实现形式是一组调度算法,我们俗称为"负载均衡算法"。

负载均衡算法有静态和动态之分,它们之间的区别就在于是否依赖于当前服务的运行时状态,这些状态信息包括服务过去一段时间的平均调用时延和所承接的连接数等。

静态负载均衡算法

我们先来看静态负载均衡算法,这类算法中具有代表性的是各种随机(Random)和轮询(Round Robin)算法。

所谓的随机算法,就是在集群中随机选择一个服务实例,它的特点是负载均衡的结果相对比较平均。随机算法的实现也比较简单,我们使用JDK自带的Random工具类,就可以来指定服务实例的地址。

举个例子,这是来自客户端的9个请求,分别被分发到了服务A集群中的3个服务实例当中,它的分发策略就是随机选择的,而不是一个固定的规则。

你可能也会用到随机算法的升级版:加权随机(Weight Random)算法。

在集群中可能存在部分性能较优的服务器,为了使这些服务器响应更多请求,就可以通过加权随机算法提升这些服务器的权重。比如这张图(图8)中实例2的权重设置的最大,所以对应的处理请求数可能也就最多。

讲完随机算法,我们再来看看轮询算法。

所谓轮询,就是按一定顺序循环遍历整个服务实例列表,并在循环过程中为请求指定某一个服务实例。如果循环过程到达上限,那么就从零开始继续顺序循环,直到找到下一个服务实例。

我还是给你举个例子。

可以看到在这个示意图中,第一个请求被分发到了实例1,第二个请求被分发到了实例2,第三个请求被分发到了实例3,然后第四个请求再次被分发到了实例1,以此类推。当然,轮询的时候每个服务实例也可以具有特定的权重,构成加权轮询算法。

动态负载均衡算法

事实上,所有涉及到权重的静态算法都可以转变为动态算法,因为权重可以在运行过程中动态更新。比如在动态轮询算法中,权重值会基于对各个服务器的持续监控并不断更新。另外,基于服务器的实时性能分配请求也是常见的动态策略。

典型的动态算法包括最少连接数算法、服务调用时延算法等。

最少连接数(Least Connection)算法,是指根据集群中各个服务的当前打开连接数,来确定目标服务实例,连接数最少的服务实例会优先响应请求。当执行分发策略时,系统会根据在某一个特定的时间点下服务实例的最新连接数,来判断是否执行客户端请求。而在下一个时间点时,服务实例的连接数一般都会发生相应的变化,对应的请求处理也会做相应的调整。

与最少连接数类似,服务调用时延(Service Invoke Delay)算法是根据服务实例的调用时延来决定负载均衡的结果,服务器也可以根据服务调用和平均时延的差值动态来调整权重。

另外,在现实中,有时候我们也会使用源地址哈希(Source IP Hash)算法,这个算法的具体实现是请求IP粘滞(Sticky)连接,以此尽可能地让来自同一IP的地址访问到同一个服务实例。

这是一种有状态机制,因而我们也可以把它归为动态负载均衡算法。

在Spring Cloud中实现的核心负载均衡算法,包括了随机、轮询、加权响应时间、并发量最小优先等,而Dubbo中则提供了随机、轮询、最少活跃调用数和一致性哈希等算法。

所以你可以发现,这两个框架中都实现了前面介绍的一些主流算法,但它们也根据框架自身的特点,提供了一些比较有特色的策略。当然,这两个框架也都提供了开发入口,供开发人员实现自定义的负载均衡算法。

相关推荐
丘山子1 小时前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
CopyLower1 小时前
在 Spring Boot 中实现 WebSockets
spring boot·后端·iphone
prinrf('千寻)2 小时前
nacos设置权重进行负载均衡不生效
运维·负载均衡
.生产的驴2 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
景天科技苑2 小时前
【Rust】Rust中的枚举与模式匹配,原理解析与应用实战
开发语言·后端·rust·match·enum·枚举与模式匹配·rust枚举与模式匹配
追逐时光者3 小时前
MongoDB从入门到实战之Docker快速安装MongoDB
后端·mongodb
方圆想当图灵3 小时前
深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入
后端·maven
豌豆花下猫3 小时前
Python 潮流周刊#99:如何在生产环境中运行 Python?(摘要)
后端·python·ai
嘻嘻嘻嘻嘻嘻ys3 小时前
《Spring Boot 3 + Java 17:响应式云原生架构深度实践与范式革新》
前端·后端
异常君3 小时前
线程池隐患解析:为何阿里巴巴拒绝 Executors
java·后端·代码规范