一、前言

Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡器,它本身不属于 Spring Cloud Alibaba 提供的组件,而是 Spring Cloud 将其封装成 starter 供微服务使用。另外,笔者在之前的 文章 中也做过 Ribbon 相关的知识介绍,故本篇章只作为对 Ribbon 内容的补充。

二、RestTemplete 介绍

2.1 RestTemplete 请求模板

Spring Cloud 底层对 Ribbon 做了二次封装,可以让我们使用 RestTemplate 的服务请求,同时搭配 @LoadBalanced 注解使用,从而实现客户端负载均衡的服务调用。

RestTemplate 提供两种方法 getForObjectgetForEntity 去请求调用服务端的数据,接下来笔者将介绍 RestTemplate 基于 REST 的常用的 2 种请求方式。

2.1.1 GET 请求

无参情况:

1
2
3
4
5
6
7
## 第一个参数:微服务接口地址,第二个参数:返回值类型
ResponseEntity<User> responseEntity = restTemplate.getForEntity("xxx", User.class);

User user = responseEntity.getBody();
HttpStatus statusCode = responseEntity.getStatusCode();
int statusCodeValue = responseEntity.getStatusCodeValue();
HttpHeaders headers = responseEntity.getHeaders();

有参情况:

1
2
3
4
5
6
7
8
9
10
11
12
String[] paramArr = {"1000", "张三"};

## 第一个参数:服务接口地址,第二个参数:返回值类型,第三个参数:入参数组
ResponseEntity<User> responseEntity = restTemplate.getForEntity("xxx?id={0}&name={1}", User.class, paramArr);


Map<String, Object> paramMap = new HashMap<>();
paramMap.put("id", 1000);
paramMap.put("name", "张三");

## 第一个参数:服务接口地址,第二个参数:返回值类型,第三个参数:入参 Map
ResponseEntity<User> responseEntity = restTemplate.getForEntity("xxx?id={id}&name={name}", User.class, paramMap);

2.1.2 POST 请求

方式一:使用 Map 传参

1
2
3
4
5
6
7
# 注意,使用的是 MultiValueMap 类型
MultiValueMap<String, Object> dataMap = new LinkedMultiValueMap<>();
dataMap.add("id", "1000");
dataMap.add("name", "张三");

# 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型
ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx", dataMap, User.class);

注意:如果使用上边的方式传参,接口提供方使用 @RequestParam("id") Integer id 形式接收参数。

方式二:使用实体传参

1
2
3
4
5
6
User user = new User();
user.setId("1000");
user.setName("张三");

# 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型
ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx", user, User.class);

注意:如果使用上边的方式传参,接口提供方使用 @RequestBody User user 形式接收参数。

方式三:使用 JSON 传参

1
2
3
4
5
6
7
String userJson = "{\"id\": 4, \"name\": \"张三\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(userJson, headers);

# 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型
ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx", entity, User.class);

注意:如果使用上边的方式传参,接口提供方使用 @RequestBody User user 形式接收参数。

方式四:已封装参数,URL 仍需添加额外参数

1
2
3
4
String token = "abc123";

# 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型,第四个参数:配对 url 后边的参数
ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx?token={token}", user, User.class, token);

2.2 负载均衡

要使用 Ribbon 的负载均衡,只需要在 RestTemplateBean 上添加 @LoadBalanced 即可,如下:

使用 Ribbon 需要添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
9
@Configuration
public class RestConfiguration {

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

2.2.1 常规负载均衡策略

Ribbon 的负载均衡策略是由 IRule 接口定义, 该接口有如下实现:

从图中可知,Ribbon 已经默认实现了多种负载均衡策略,我们可以根据自己的实际情况替换这些策略:

负载均衡实现类策略
RandomRule随机
RoundRobinRule轮询
AvailabilityFilteringRule先过滤掉由于多次访问故障的服务,以及并发连接数超过阈值的服务,然后对剩下的服务按照轮询策略进行访问
WeightedResponseTimeRule根据平均响应时间计算所有服务的权重,响应时间越快服务权重就越大被选中的概率即 越高,如果服务刚启动时统计信息不足,则使用RoundRobinRule策略,待统计信息足够会切换到该WeightedResponseTimeRule策略
RetryRule先按照RoundRobinRule策略分发,如果分发到的服务不能访问,则在指定时间内进行重 试,然后分发其他可用的服务
BestAvailableRule先过滤掉由于多次访问故障的服务,然后选择一个并发量最小的服务
ZoneAvoidanceRule (默认)综合判断服务节点所在区域的性能和服务节点的可用性,来决定选择哪个服务

如果想替换默认的负载均衡策略,操作非常简单,只需重新创建 IRule 接口的实现即可,我们拿 RoundRobinRule 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class RestConfiguration {

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

@Bean
public IRule testRule() {
return new RoundRobinRule();
}
}

这样,启动项目后,Ribbon 就会使用该策略进行接口调用了。

2.2.2 细粒度策略配置

假设现有用户微服务和订单微服务,我们希望用户微服务依然使用默认的轮询策略,订单微服务使用随机策略,应该怎么处理呢?

很简单,共有两种方式进行处理(修改订单微服务):

方式一:文件配置(推荐,配置简单且优先级高)

1
2
3
order-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

其中,order-service 为订单微服务的应用名。

方式二:java 配置

1
2
3
4
@RibbonClient(name = "order-service", configuration = RibbonConfiguration.class)
public class OrderServiceRibbonRule {

}
1
2
3
4
5
6
7
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}

注意:RibbonConfiguration 类必须放在启动类所在的包之外,否则 Spring 父子容器都扫描 RibbonConfiguration 类无法实现细粒度配置的效果。

2.2.3 自定义负载均衡策略

如果上述的策略不满足自身要求,我们还可以自定义负载均衡策略,需要操作 2 个步骤:

第一步,实现 AbstractLoadBalancerRule 接口,例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyNacosRule extends AbstractLoadBalancerRule {

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
//基本上不需要实现
}

@Override
public Server choose(Object key) {
//实现该方法
}
}

第二步,配置文件(application.properties 或 application.yml)配置负载均衡策略:

1
2
3
xxx:
ribbon:
NFLoadBalancerRuleClassName: com.light.ribbon.MyNacosRule

其中,xxx表示远程服务的名称。

三、OpenFeign 补充

先前也介绍过 Feign 的使用,至 Spring Cloud F 及F版本以上 Spring Boot 2.0 以上基本上使用 OpenFeign,本小节作为补充内容讲解。

3.1 区别

  • Feign: Spring Cloud 组件中的一个轻量级 Restful 的 HTTP 服务客户端,Feign 内置了 Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
  • OpenFeignSpring CloudFeign 的基础上支持了 SpringMVC 的注解,如 @RequestMapping 等等。OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

3.2 实战演练

项目名称端口描述
feign-test-pom 项目,父工厂
feign-common-jar 项目,通用 api 项目,包含 model,feign
user-service9001用户微服务,依赖 feign-common,服务注册到 nacos
order-service9002订单微服务,依赖 feign-common,服务注册到 nacos

不熟悉 Nacos 的读者可以先打开 传送门 浏览相关文章。

测试流程:请求订单接口返回订单信息和订单关联的用户信息。其调用链:order-service -> user-service

现在开始搭建项目:

  1. feign-test 为父工程,pom 项目,只负责配置依赖:
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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>

<dependencyManagement>
<dependencies>
<!-- spring cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- spring cloud alibaba 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
  1. feign-common 为通用工程,管理 model 和 feign 的 API。

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

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

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

</dependencies>

OpenFeign 类:

1
2
3
4
5
6
@FeignClient(value="user-service")
public interface UserServiceFeign {

@RequestMapping("/user/findById/{id}")
public User findById(@PathVariable("id") Integer id);
}

注意:@FeignClient 注解中的 values 对应用户微服务的应用名, UserServiceFeign 中声明的接口与用户微服务公开的接口保持一致

  1. user-service 为用户微服务,提供用户相关接口, 依赖 feign-common 项目:

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9001

spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos

接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/user")
public class UserController {

private static Map<Integer, User> userMap;

static {
userMap = new HashMap<>();
userMap.put(1, new User(1, "张三"));
userMap.put(2, new User(2, "李四"));
userMap.put(3, new User(3, "王五"));
}

@RequestMapping("/findById/{id}")
public User findById(@PathVariable("id") Integer id) {
// 为了测试方便,用此方式模拟用户查询
return userMap.get(id);
}
}

在用户微服务启动类上添加 @EnableFeignClients(basePackages={"com.light.feign"}) 注解。

  1. order-service 为订单微服务,提供订单相关接口, 依赖 feign-common 项目:

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9002

spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private UserServiceFeign userServiceFeign;

// @Autowired
// private RestTemplate restTemplate;

private static Map<Integer, Order> orderMap;

static {
orderMap = new HashMap<>();
orderMap.put(1, new Order(1, 1, 10.0));
orderMap.put(2, new Order(2, 2, 20.0));
orderMap.put(3, new Order(3, 3, 30.0));
}

@RequestMapping("/getOrderInfo/{orderId}")
public Map<String, Object> getOrderInfo(@PathVariable Integer orderId) {
Map<String, Object> result = new HashMap<>();
// 模拟数据库查询
Order order = this.orderMap.get(orderId);
if (order != null) {
Integer userId = order.getUserId();

// http://服务提供者的应用名称/接口名称/参数
// ResponseEntity<User> userEntity = this.restTemplate.getForEntity("http://user-service/user/findById/" + userId, User.class);
// User user = userEntity.getBody();

// 使用 openfeign 请求用户接口
User user = this.userServiceFeign.findById(userId);

// 订单信息
result.put("order", order);
// 用户信息
result.put("user", user);
}
return result;
}
}

在订单微服务启动类上添加 @EnableFeignClients(basePackages={"com.light.feign"}) 注解。

依次启动 Nacos 服务,用户微服务,订单微服务。打开浏览器访问: http://localhost:9002/order/getOrderInfo/1 ,结果如下图:

成功返回数据,OpenFeign 整合成功。

3.3 性能优化

OpenFeign 底层默认使用 URLConnection 进行连接请求,性能仅为 RestTemplate 的 50%。为了更高效的请求,我们需要修改底层请求组件,改用 HttpClient,性能可提高 15%。

只需如下 2 步操作:

  1. 添加依赖:
1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
  1. 修改配置文件:
1
2
3
4
5
feign:
httpclient:
enabled: true
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径最大连接数