负载均衡介绍

  • 将请求根据负载均衡策略分摊到不同的服务器上。

负载均衡方案主要有2种模式:

1、服务端进行负载,如 Nginx,F5(硬件负载均衡器)

2、客户端自行负载均衡,如在服务上自行引入Ribbon实现负载均衡

1)服务端负载均衡

服务端负载均衡

当请求进入Nginx后,由Nginx进行负载均衡算法,决定发送给哪个服务器。

2)客户端负载均衡

客户端负载均衡

例如Spring Cloud的Ribbon负载均衡器,客户端本地会通过Eureka、Nacos获取可用的服务列表,然后通过Ribbon进行请求,Ribbon会根据负载均衡算法,选择出最优的节点进行访问。

3)常见的负载均衡算法

  • 随机,通过随机算 选择服务进行访问
  • 轮询,负载均衡的默认实现方式
  • 权重轮询,我们可以根据给高配置的服务器加权重,那么轮询的时候会优先轮询权重高的服务器
  • 地址hash,通过对服务器的地址进行hash 取模后找到对应的服务器,然后把请求转发过去
  • 最小连接数,比较合理且用的多的方案,根据服务器的情况,如 连接数量,请求积压情况,把请求分配到压力最小的服务器上

Ribbon负载均衡器

1)Ribbon介绍

Spring Cloud Ribbon是基于Netflix Ribbon 实现的一套客户端的负载均衡工具,Ribbon客户端组件提供一系列的完善的配置,如超时,重试等。通过Load Balancer获取到服务提供的所有机器实例,Ribbon会自动基于某种规则(轮询,随机)去调用这些服务。Ribbon也可以实现我们自己的负载均衡算法。

2)Spring Cloud Alibaba整合Ribbon

创建2个模块,xingmall-order 订单模块,xingmall-user 用户模块,我们在用户模块上通过ribbon负载均衡的使用RestTemplate请求xingmall-order的接口。

实验使用的源码:https://files.javaxing.com/Java%08Demo/NacosServer.zip

2.1 引入ribbon依赖

1
2
3
4
5
<!--添加ribbon的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

如果我们的微服务引入了nacos,就不需要再引入ribbon了,因为nacos-discovery依赖了ribbon。

nacos

2.2 创建微服务模块

image-20230401175301940

2.3 编写controller代码

user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.javaxing.springbootnacos.contrller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserContrller {

@Autowired
private RestTemplate restTemplate;

@RequestMapping(value = "/testRibbon")
public String testRibbon() {
// ribbon实现,restTemplate需要添加@LoadBalanced注解
String url = "http://xingmall-order/queryOrder";
String result = restTemplate.getForObject(url,String.class);
return result;
}

}

order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.javaxing.xingmallorder.contrller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class OrderContrller {


@GetMapping("/queryOrder")
public String queryOrder(){
System.out.println("成功请求到该接口");
return "Query Success";
}
}

2.4 配置RestTemplate负载均衡

我们在user 用户模块上,创建RestTemplate配置文件 并添加 添加@LoadBalanced注解,让RestTemplate支持 负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.javaxing.springbootnacos.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestConfig {
@Bean
@LoadBalanced //开启负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

2.5 发起请求测试

我们访问4次user模块的接口:http://localhost:8080/testRibbon ,他会通过restTemplate去访问order模块接口。

image-20230401175611291

服务端的日志显示,4次请求都平均分散到了order服务器上。

image-20230401175804096image-20230401175751420

3)Ribbon @LoadBalanced原理

参考源码: LoadBalancerAutoConfiguration。

@LoadBalanced使用了@Qualifier,spring中@Qualifier用于筛选限定注入Bean。@LoadBalanced利用@Qualifier作为restTemplates注入的筛选条件,筛选出具有负载均衡标识的RestTemplate。

image-20230401180354764

被@LoadBalanced注解的restTemplate会被拦截,添加LoadBalancerInterceptor拦截器。

image-20230401181004165

image-20230401180602227

注意:SmartInitializingSingleton所有的bean都实例化完成之后才会调用的,所以在bean的实例化期间使用@LoadBalanced修饰的restTemplate是不具备负载均衡作用的,一定要等bean都实例化完成后再来调用。

如果不使用@LoadBalanced注解,也可以通过添加LoadBalancerInterceptor拦截器让restTemplate起到负载均衡器的作用。

1
2
3
4
5
6
7
@Bean
public RestTemplate restTemplate(LoadBalancerInterceptor loadBalancerInterceptor) {
RestTemplate restTemplate = new RestTemplate();
//注入loadBalancerInterceptor拦截器
restTemplate.setInterceptors(Arrays.asList(loadBalancerInterceptor));
return restTemplate;
}

Ribbon扩展功能

1)Ribbon相关接口

参考: org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration

IClientConfig:Ribbon的客户端配置,默认采用DefaultClientConfigImpl实现。

IRule:Ribbon的负载均衡策略,默认采用ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例进行访问。

IPing:Ribbon的实例检查策略,默认采用DummyPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服务实例都是可用的。

ServerList:服务实例清单的维护机制,默认采用ConfigurationBasedServerList实现。

ServerListFilter:服务实例清单过滤机制,默认采ZonePreferenceServerListFilter,该策略能够优先过滤出与请求方处于同区域的服务实例。

ILoadBalancer:负载均衡器,默认采用ZoneAwareLoadBalancer实现,它具备了区域感知的能力。

2)Ribbon 负载均衡策略

Ribbon IRule实现方法

  1. RandomRule: 随机选择一个Server。
  2. RetryRule: 对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的server。
  3. RoundRobinRule: 轮询选择, 轮询index,选择index对应位置的Server。
  4. AvailabilityFilteringRule: 过滤掉一直连接失败的被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就是检查status里记录的各个Server的运行状态。
  5. BestAvailableRule: 选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。
  6. WeightedResponseTimeRule: 根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低。
  7. ZoneAvoidanceRule默认的负载均衡策略,即复合判断Server所在区域的性能和Server的可用性选择Server,在没有区域的环境下,类似于轮询
  8. NacosRule: 优先调用同一集群的实例,基于随机权重

3)修改默认负载均衡策略

修改负载均衡策略的话,有两种方式,一种是直接全局配置,一种是局部配置。

1.全局配置

该服务下所有的请求都会根据这个策略进行负载均衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.javaxing.springbootnacos.config;

import com.alibaba.cloud.nacos.ribbon.NacosRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfig {

/**
* 全局配置
* 指定负载均衡策略
* @return
*/
@Bean
public IRule ribbonRule() {
// 指定使用Nacos提供的负载均衡策略(优先调用同一集群的实例,基于随机权重)
return new NacosRule();
}
}

我们这里使用Nacos提供的负载均衡策略,优先调用同一集群的实例,基于随机权重。

2.局部配置

该服务下的指定微服务模块才会根据该策略进行负载均衡,其他模块均以默认的策略进行负载均衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8080
spring:
application:
name: xingmall-user
cloud:
nacos:
discovery:
server-addr: 10.211.55.12:8848
namespace: 37731914-6c28-4265-a090-7a9391ed843d
cluster-name: shanghai
# 被调用的微服务名
xingmall-order:
ribbon:
# 指定使用Nacos提供的负载均衡策略(优先调用同一集群的实例,基于随机&权重)
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule

3.验证Nacos负载均衡策略

我们把xingmall-order2个服务进行区域划分,一个服务划分到北京,一个服务划分到上海,而xingmall-user划分到上海,那么如果我们请求user的接口时,user会去访问order服务,他会优先访问 同一个集群(区域)的服务,如果访问不到才会去访问其他集群的服务。

xingmall-order服务

image-20230401210154882

xingmall-user

image-20230401210318883

访问user接口测试

接口:http://localhost:8080/testRibbon

image-20230401212149347image-20230401212202210

我们请求了9次,有6次落到了上海集服务器上,有3次落到了北京集群服务器上。

由此可以看出,现在已经并非是默认的负载均衡策略了,而是优先调用同一集群的实例。

注意:在实际测试过程中,发现一旦实现全局配置后,Nacos的代码会报错,因为缺少一个StringUtils依赖,所以我们要在user项目中引入该依赖。

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
</dependencies>

image-20230401212432472

4.Nacos负载均衡策略源码分析

负载均衡策略入口:NacosRule ->choose

image-20230402100643437

4)自定义负载均衡策略

我们可以通过IRule接口可以自定义负载均衡策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.javaxing.springbootnacos.rule;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.Server;
import org.springframework.beans.factory.annotation.Autowired;

/**
* Nacos 基于权重的算法 自定义策略
*/
public class NacosRandomWithWeightRule extends AbstractLoadBalancerRule {

@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;

@Override
public Server choose(Object key) {
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
String serviceName = loadBalancer.getName();
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
try {
//nacos基于权重的算法
Instance instance = namingService.selectOneHealthyInstance(serviceName);
return new NacosServer(instance);
} catch (NacosException e) {
e.printStackTrace();
}
return null;
}

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {

}
}

1.全局配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.javaxing.springbootnacos.config;

import com.alibaba.cloud.nacos.ribbon.NacosRule;
import com.javaxing.springbootnacos.rule.NacosRandomWithWeightRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfig {

/**
* 全局配置
* 指定负载均衡策略
* @return
*/
@Bean
public IRule ribbonRule() {
return new NacosRandomWithWeightRule();
}
}

2.局部配置

1
2
3
4
5
# 被调用的微服务名
xingmall-order:
ribbon:
# 自定义的负载均衡策略(基于随机&权重)
NFLoadBalancerRuleClassName: com.javaxing.springbootnacos.rule.NacosRandomWithWeightRule

3.验证自定义负载均衡策略

订单服务有2个服务端,其中8082端口的服务权重=10,8081权重=1,那么我们进行调用的时候,请求应该更多的落在了8082服务端。

image-20230402111018196

请求访问

image-20230402111240104image-20230402111219442

如图所示,8082 因为权重高,所以10个请求有9个落到了8082,1个落到了8081 。

5)饥饿加载

如果服务器是刚启动的话,第一次调用会比较慢,因为需要加载很多组件,需要去获取实例列表,并根据负载均衡策略进行请求。

image-20230402113348082

image-20230402113014880

1.优化方法

我们可以使用饥饿加载,顾名思义,就是让SpringBoot在启动时,他会自动加载LoadBalancer并去拉去Nacos中的可用服务实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 8080
spring:
application:
name: xingmall-user
cloud:
nacos:
discovery:
server-addr: 10.211.55.12:8848
namespace: 37731914-6c28-4265-a090-7a9391ed843d
cluster-name: shanghai
# 被调用的微服务名
xingmall-order:
ribbon:
# 自定义的负载均衡策略(基于随机&权重)
NFLoadBalancerRuleClassName: com.javaxing.springbootnacos.rule.NacosRandomWithWeightRule

# 饥饿加载
ribbon:
eager-load:
# 开启饥饿加载
enabled: true
# 指定需要饥饿加载的服务名,也就是你需要调用的服务,如果有多个服务,则用逗号隔开
clients: xingmall-order

开启饥饿加载后,当我们启动微服务时候,他会自动加载LoadBalancer并去拉去Nacos中的可用服务实例。

image-20230402114105441

当我们再次去调用时,明显看到延迟响应降低了很多,但是因为 第一次进行远程调用涉及到一些组件,所以还是会有所损耗,但是比刚才好多了,第二次在调用的时候,不需要再次加载组件,延迟可以降低到30ms。

image-20230402114204409image-20230402114414648

LoadBalancer 负载均衡器

Spring Cloud LoadBalancer是Spring Cloud官方自己提供的客户端负载均衡器, 用来替代Ribbon

但是实际上截止到2023年,生产环境中大部分用的还是Ribbon,而并非LoadBalancer,但是Ribbon已经在2019年停止维护了,只能说LoadBalancer未来可期,但不适合现在。

1)RestTemplate整合LoadBalancer

在RestTemplate使用LoadBalancer的话,需要引入依赖,然后使用@LoadBalanced注解修饰RestTemplate,其他的和Ribbon没有区别。

1.1 引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- 提供了RestTemplate支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- nacos服务注册与发现 移除ribbon支持-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>

注意: 由于nacos-discovery中引入了ribbon,**需要移除ribbon的包,如果不移除,也可以在yml中配置不使用ribbon。

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: mall-user-loadbalancer-demo
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 不使用ribbon,使用loadbalancer
loadbalancer:
ribbon:
enabled: false

1.2 使用@LoadBalanced注解

使用@LoadBalanced注解修饰RestTemplate,开启客户端负载均衡功能

1
2
3
4
5
6
7
8
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

1.3 使用RestTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.javaxing.springbootnacos.contrller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserContrller {

@Autowired
private RestTemplate restTemplate;

@RequestMapping(value = "/testRibbon")
public String testRibbon() {
// ribbon实现,restTemplate需要添加@LoadBalanced注解
String url = "http://xingmall-order/queryOrder";
String result = restTemplate.getForObject(url,String.class);
return result;
}
}