Spring Cloud Ribbon 负载均衡调用服务

Posted by 暮夏有五 on 2021-02-05
Estimated Reading Time 10 Minutes
Words 2.2k In Total
Viewed Times

Ribbon 简介


Spring Cloud Ribbon 是 Spring Cloud Netflix 子项目的核心组件之一,主要给服务间调用及 API 网关转发提供负载均衡的功能,本文将对其用法进行详细介绍。

在微服务架构中,很多服务都会部署多个,其他服务去调用该服务的时候,如何保证负载均衡是个不得不去考虑的问题。负载均衡可以增加系统的可用性和扩展性,当我们使用RestTemplate来调用其他服务时,Ribbon 可以很方便的实现负载均衡功能。

RestTemplate 的使用


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

GET 请求方法

1
2
3
4
5
6
7
8
9
10
11
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables);

<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables);

<T> T getForObject(URI url, Class<T> responseType);

<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables);

<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables);

<T> ResponseEntity<T> getForEntity(URI var1, Class<T> responseType);

getForObject 方法

返回对象为响应体中数据转化成的对象,举例如下:

1
2
3
4
@GetMapping("/{id}")
public CommonResult getUser(@PathVariable Long id) {
return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
}

getForEntity 方法

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

1
2
3
4
5
6
7
8
9
@GetMapping("/getEntityByUsername")
public CommonResult getEntityByUsername(@RequestParam String username) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(userServiceUrl + "/user/getByUsername?username={1}", CommonResult.class, username);
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult("操作失败", 500);
}
}

POST 请求方法

1
2
3
4
5
6
7
8
9
10
11
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);

<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);

<T> T postForObject(URI url, @Nullable Object request, Class<T> responseType);

<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);

<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);

<T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType);

postForObject 示例

1
2
3
4
@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/create", user, CommonResult.class);
}

postForEntity 示例

1
2
3
4
@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
return restTemplate.postForEntity(userServiceUrl + "/user/create", user, CommonResult.class).getBody();
}

PUT 请求方法

1
2
3
4
5
void put(String url, @Nullable Object request, Object... uriVariables);

void put(String url, @Nullable Object request, Map<String, ?> uriVariables);

void put(URI url, @Nullable Object request);

PUT 请求示例

1
2
3
4
5
@PutMapping("/update")
public CommonResult update(@RequestBody User user) {
restTemplate.put(userServiceUrl + "/user/update", user);
return new CommonResult("操作成功",200);
}

DELETE 请求方法

1
2
3
4
5
void delete(String url, Object... uriVariables);

void delete(String url, Map<String, ?> uriVariables);

void delete(URI url);

DELETE 请求示例

1
2
3
4
5
@DeleteMapping("/delete/{id}")
public CommonResult delete(@PathVariable Long id) {
restTemplate.delete(userServiceUrl + "/user/delete/{1}", null, id);
return new CommonResult("操作成功",200);
}

创建服务提供者


首先我们创建一个 user-service,用于给 Ribbon 提供服务调用。

引入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

相关配置

主要是配置了端口和注册中心地址。

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8201
spring:
application:
name: user-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8001/eureka/

创建 Controller

UserController 类定义了对 User 对象常见的 CRUD 接口。

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
42
43
44
45
46
47
@RestController
@RequestMapping("/user")
public class UserController {

private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

@Autowired
private UserService userService;

@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
userService.create(user);
return new CommonResult("操作成功", 200);
}

@GetMapping("/{id}")
public CommonResult<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
LOGGER.info("根据id获取用户信息,用户名称为:{}",user.getUsername());
return new CommonResult<>(user);
}

@GetMapping("/getUserByIds")
public CommonResult<List<User>> getUserByIds(@RequestParam List<Long> ids) {
List<User> userList= userService.getUserByIds(ids);
LOGGER.info("根据ids获取用户信息,用户列表为:{}",userList);
return new CommonResult<>(userList);
}

@GetMapping("/getByUsername")
public CommonResult<User> getByUsername(@RequestParam String username) {
User user = userService.getByUsername(username);
return new CommonResult<>(user);
}

@PostMapping("/update")
public CommonResult update(@RequestBody User user) {
userService.update(user);
return new CommonResult("操作成功", 200);
}

@PostMapping("/delete/{id}")
public CommonResult delete(@PathVariable Long id) {
userService.delete(id);
return new CommonResult("操作成功", 200);
}
}

创建服务消费者

这里我们创建一个 ribbon-service 模块来调用 user-service 模块演示负载均衡的服务调用。

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

相关配置

主要是配置了端口、注册中心地址及 user-service 的调用路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8301
spring:
application:
name: ribbon-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8001/eureka/
service-url:
user-service: http://user-service

使用 @LoadBalanced 注解赋予 RestTemplate 负载均衡的能力。使用 Ribbon 的负载均衡功能非常简单,和直接使用 RestTemplate 没什么两样,只需给 RestTemplate 添加一个 @LoadBalanced 即可。

1
2
3
4
5
6
7
8
9
@Configuration
public class RibbonConfig {

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

创建 Controller

注入 RestTemplate,使用其调用 user-service 中提供的相关接口,这里对 GET 和 POST 调用进行了演示,其他方法调用均可参考。

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
42
43
@RestController
@RequestMapping("/user")
public class UserRibbonController {
@Autowired
private RestTemplate restTemplate;
@Value("${service-url.user-service}")
private String userServiceUrl;

@GetMapping("/{id}")
public CommonResult getUser(@PathVariable Long id) {
return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
}

@GetMapping("/getByUsername")
public CommonResult getByUsername(@RequestParam String username) {
return restTemplate.getForObject(userServiceUrl + "/user/getByUsername?username={1}", CommonResult.class, username);
}

@GetMapping("/getEntityByUsername")
public CommonResult getEntityByUsername(@RequestParam String username) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(userServiceUrl + "/user/getByUsername?username={1}", CommonResult.class, username);
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult("操作失败", 500);
}
}

@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/create", user, CommonResult.class);
}

@PostMapping("/update")
public CommonResult update(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/update", user, CommonResult.class);
}

@PostMapping("/delete/{id}")
public CommonResult delete(@PathVariable Long id) {
return restTemplate.postForObject(userServiceUrl + "/user/delete/{1}", null, CommonResult.class, id);
}
}

负载均衡功能演示

  • 启动 eureka-server 于 8001 端口

  • 启动 user-service 于 8201 端口

  • 启动另一个 user-service 于 8202 端口,可以通过修改 IDEA 中的 Spring Boot 的启动配置实现

  • 此时运行中的服务如下

  • 调用接口进行测试:http://localhost:8301/user/1

  • 可以发现运行在 8201 和 8202 的 user-service 控制台交替打印如下信息

Ribbon的常用配置


全局配置

1
2
3
4
5
6
7
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法

指定服务进行配置

与全局配置的区别就是ribbon节点挂在服务名称下面,如下是对ribbon-service调用user-service时的单独配置。

1
2
3
4
5
6
7
8
user-service:
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法

Ribbon 的负载均衡策略


所谓的负载均衡策略,就是当A服务调用B服务时,此时B服务有多个实例,这时A服务以何种方式来选择调用的B实例,Ribbon 可以选择以下几种负载均衡策略。

  • 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:采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。

更多干货请移步:https://antoniopeng.com


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !