负载均衡

微服务架构在生成环境中,各个微服务会部署多个实例,负载均衡就是将服务消费者的请求分摊到多个服务提供者实例的过程,它可以增加系统的可用性和扩展性。

当我们使用RestTemplate来调用其他服务时,Ribbon可以方便的是实现负载均衡,版本迭代之后由LoadBalancer替代。

LoadBalancer的优势主要在于支持响应式编程方式异步访问客户端,依赖Spring Web Flux实现客户端负载均衡调用。

RestTemplate-HTTP客户端

RestTemplate是一个HTTP客户端,使用它我们可以方便的调用HTTP接口,支持GET、POST、PUT、DELETE等方法。

这里以Get方法为例,说明RestTemplate远程调用,具体可查看RestOperations接口的方法声明。

GET请求方法

image-20220531145524762

getForObject方法

返回对象为响应体中数据转化成对象

image-20220531145953530

getForEntity方法

返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。

image-20220531155322989

调试运行可以看到ResponseEntity对象里的具体信息

image-20220531160107761

POST请求方法

image-20220531161914683

PUT请求方法

image-20220531161923943

DELETE请求方法

image-20220531161936372

使用Ribbon实现客户端侧负载均衡

Ribbon简介

Spring Cloud Ribbon 是Spring Cloud Netflix 子项目的核心组件之一,主要给服务间调用及API网关转发提供负载均衡的功能,有助于控制HTTP和TCP客户端的行为。在服务消费者拉取服务提供者地址列表后,Ribbon基于某种负载均衡算法,自动帮助服务消费者发起请求。

Ribbon提供了很多负载均衡算法,例如轮询、随机等,还支持自定义的负载均衡算法。

在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。

image-20220531144624058

服务消费者整合Ribbon

由于之前的项目使用的Spring Cloud Netflix版本已经弃用Ribbon,所有我们需要修改响应的版本信息,这里就不做过多的赘述。

项目引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

文件配置

主要配置端口、注册中心地址以及服务调用路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
port: 8301
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloudstudy_borrow
username: root
password: 13851176590++
application:
name: borrow-service
mybatis:
mapper-locations:
- classpath:mapper/*.xml
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://root:123456@replica1:8005/eureka,
http://root:123456@replica2:8006/eureka
service-url:
user-service: http://user-service
book-service: http://book-service

在RestTemplate注入方法上添加@LoadBalanced注解

添加@LoadBalanced注解,就可为RestTemplate整合Ribbom,使其具备负载均衡的能力。

1
2
3
4
5
6
/*注册RestTemplate对象*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

接口实现负载均衡的远程调用

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
@Service
public class BorrowServiceImpl implements BorrowService {
@Resource
BorrowMapper borrowMapper;
@Autowired
RestTemplate restTemplate;
@Value("${service-url.user-service}")
private String userServiceUrl;
@Value("${service-url.book-service}")
private String bookServiceUrl;

@Override
public UserBorrowDetail getBorrowByUser(Long uid) {
List<Borrow> borrows = borrowMapper.getBorrow(uid,null);
User user = restTemplate.getForObject(userServiceUrl + "/user/" + uid, User.class);
List<Book> books = borrows.stream().map(borrow -> restTemplate.getForObject(bookServiceUrl + "/book/" + borrow.getBid(), Book.class)).collect(Collectors.toList());
return new UserBorrowDetail(user, books);
}

@Override
public BookBorrowDetail getBorrowByBook(Long bid) {
List<Borrow> borrows = borrowMapper.getBorrow(null,bid);
List<User> users = borrows.stream().map(borrow -> restTemplate.getForObject(userServiceUrl + "/user/" + borrow.getUid(), User.class)).collect(Collectors.toList());
ResponseEntity<Book> entity = restTemplate.getForEntity(bookServiceUrl + "/book/"+bid, Book.class);
if (entity.getStatusCode().is2xxSuccessful()) {
return new BookBorrowDetail(users, entity.getBody());
} else {
return new BookBorrowDetail(users, null);
}
}

@Override
public BorrowDetail getBorrow(Long uid, Long bid) {
List<Borrow> borrows = borrowMapper.getBorrow(uid, bid);
List<User> users = borrows.stream().map(borrow -> restTemplate.getForObject(userServiceUrl + "/user/" + borrow.getUid(), User.class)).collect(Collectors.toList());
List<Book> books = borrows.stream().map(borrow -> restTemplate.getForObject(bookServiceUrl + "/book/" + borrow.getBid(), Book.class)).collect(Collectors.toList());
return new BorrowDetail(users, books);
}
}

控制层

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
@RestController
public class BorrowController {
@Resource
BorrowService borrowService;
@Autowired
private LoadBalancerClient loadBalancerClient;

private static final Logger logger = LoggerFactory.getLogger(BorrowController.class);

@RequestMapping("/borrow/user/{uid}")
public UserBorrowDetail getBorrowByUser(@PathVariable("uid") Long uid){
return borrowService.getBorrowByUser(uid);
}

@RequestMapping("/borrow/book/{bid}")
public BookBorrowDetail getBorrowByBook(@PathVariable("bid") Long bid){
return borrowService.getBorrowByBook(bid);
}

@RequestMapping("/borrow")
public BorrowDetail getBorrow(@RequestParam(value = "uid", required = true) Long uid,
@RequestParam(value = "bid", required = true) Long bid){
return borrowService.getBorrow(uid, bid);
}

@GetMapping("/log-instance")
public void logUserInstance() {
ServiceInstance serviceInstance = this.loadBalancerClient.choose("user-service");
BorrowController.logger.info("{}:{}:{}", serviceInstance.getServiceId(), serviceInstance.getHost(), serviceInstance.getPort());
}
}

Ribbon原理

负载均衡拦截器在获取用户的请求地址后,会获取微服务的虚拟主机名,再从注册中心拉取微服务的网络地址列表,在服务列表进行负载均衡。

LoadBalancerInterceptor方法

image-20220604011042764

ClientHttpRequestInterceptor接口

image-20220604011204139

接口方法具体实现

image-20220604011815218

负载均衡接口

image-20220604011927175

RibbonLoadBalancerClient

image-20220604012252580

image-20220604013037564

image-20220604020317394

Ribbon负载均衡策略

Ribbon的负载均衡规则是由IRule的接口定义,每一个子接口对应一种规则。

image-20220604021231385

  • com.netflix.loadbalancer.RandomRule:从提供服务的实例中以随机的方式;
  • com.netflix.loadbalancer.RoundRobinRule:以线性轮询的方式,就是维护一个计数器,从提供服务的实例中按顺序选取,第一次选第一个,第二次选第二个,以此类推,到最后一个以后再从头来过;
  • com.netflix.loadbalancer.RetryRule:在RoundRobinRule的基础上添加重试机制,即在指定的重试时间内,反复使用线性轮询策略来选择可用实例;
  • com.netflix.loadbalancer.WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择;
  • com.netflix.loadbalancer.BestAvailableRule:选择并发较小的实例;
  • com.netflix.loadbalancer.AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例;
  • com.netflix.loadbalancer.ZoneAwareLoadBalancer:采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。

通过自定义定义IRule实现可以修改负载均衡规则,由以下两种方式实现:

  • 配置类IRule注入(全局配置)
1
2
3
4
@Bean
public IRule randomRule(){
return new RandomRule();
}
  • 配置文件方式(服务绑定)
1
2
3
user-service:
ribbon:
NFLoadBalanverRuleClassName: com.netflix.loadbalancer.RandomRule

Ribbon饥饿加载

Ribbon默认是采用懒加载,即第一次访问时会去创建LoadBalanceClient,请求时间很长

使用Ribbon的饥饿加载会在项目开启时创建,降低第一次访问的耗时

开启饥饿加载的方式,配置文件:

1
2
3
4
5
6
ribbon:
eager-load:
enable: true
clients:
-user-service
-book-service

使用LoadBalancer实现客户端侧负载均衡

服务消费者整合LoadBalancer

整合步骤和Ribbon的整合步骤基本一致,只是依赖版本不同且无需引入Ribbon

image-20220604031524461

LoadBalancer原理

img

工作流程

img

@LoadBalanced注解开启负载均衡

image-20220531102500703

客户端HTTP请求拦截器接口

image-20220531102426240

负载均衡拦截器实现接口

image-20220531102336211

负载均衡客户端中execute接口方法

image-20220604031737840

BlockingLoadBalancerClient实现execute方法

image-20220604033125528

choose具体实现

image-20220604034427604

ReactiveLoadBalancer被不同规则的负载均衡器实现

image-20220604034601307

默认由RoundRobinLoadBalancer即轮询负载均衡实现

image-20220604035134969

自定义负载均衡策略

创建负载均衡配置类

image-20220604041507416

1
2
3
4
5
6
7
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer (Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name)/*随机负载均衡策略*/;
}
}

为指定服务指定负载均衡策略

image-20220604125902447

1
2
3
4
5
6
@LoadBalancerClients(
value = {
@LoadBalancerClient(value = "user-service", configuration = LoadBalancerConfig.class),
@LoadBalancerClient(value = "book-service", configuration = LoadBalancerConfig.class)
}, defaultConfiguration = LoadBalancerClientConfiguration.class
)

我们发现远程调用user-service服务不在是通过轮询的方式,而是通过随机的负载均衡策略实现

image-20220604130204288