了解Ribbon
本书使用的Spring Cloud 默认支持的Riibbon 版本是2.3.0
1.负载均衡介绍
负载均衡是分布式项目架构的重点,负载均衡决定着整个分布式服务集群的性能和稳定,前面讲解了Eureka集群的使用,本节我们讲解Spring Cloud 如何为我们的微服务提供负载均衡的方案。
2.Ribbon 介绍
Ribbon
是Netflix下的负载均衡项目,通过Spring Cloud的封装的工具Spring Cloud Ribbon
,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。主要提供了以下特性:
- 负载均衡器,可支持插拔式的负载均衡原则
- 对多种协议提供支持,例如HTTP、TCP、UDP等
- 集成了负载均衡功能的客户端
它不需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要
2.1 Ribbon子模块
- ribbon-core:ribbon的核心,主要包含负载均衡器、负载均衡接口、客户端接口、内置负载均衡实现API
- ribbon-httpclient:为负载均衡提供了REST客户端
- ribbon-loadbalancer:负载均衡模块,可以独立使用,也可和其它模块一起使用
ribbon:在 ribbon 模块和 Hystrix 基础之上,集成了 负载均衡、容错处理、缓存/批处理 的 API
2.2 Ribbon 负载均衡器主键
Ribbon 的负载均衡器主要与集群中的各个服务进行通信,负载均衡器主要提供已下功能:
维护服务器的IP、DNS的信息
- 根据特定的逻辑在服务列表中循环
为了实现负载均衡的基础功能,RIbbon的负载均衡器提供了已下三大模块: - Rule: Ribbon提供的逻辑主键,决定了服从服务器列表中返回那个服务器实例
- Ping: 主要使用定时器来确保服务器的网络可以连接
ServerList: 服务器列表,可以静态的配置服务器列表,也可以动态指定服务器列表
2.3 Ribbon提供的负载均衡算法
RoundRobinRule(轮询算法)
- RandomRule(随机算法)
- AvailabilityFilteringRule():会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
- WeightedResponseTimeRule():根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够会切换到WeightedResponseTimeRule
- RetryRule():先按照RoundRobinRule的策略获取服务,如果获取失败则在制定时间内进行重试,获取可用的服务。
- BestAviableRule():会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
3. 总结
通过上面的介绍,我们对Ribbon 已经有了了解,知道Ribbon 在微服务中是干什么的。下面我们通过程序让大家对Ribbon 有更进一步的认识。Ribbon 服务提供者
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
本节将通过一个简单的程序展示Ribbon API 的使用,本节的程序结构图如下:
先建服务端fw-cloud-ribbon-server
提供一个RESTFUL的接口,然后在创建Ribbon客户端调动,并将负载的效果显示出来。接下来开始
1. ribbon服务端模块
1.1 maven 包引入
<dependencies>
<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.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
spring-boot-starter-actuator
包不清楚的可以看下,前面章节“Eureka服务端健康检查”,使用spring-cloud-starter-netflix-eureka-client
是想通过用Eureka的注册中心发现注册的服务列表。
1.2 新建启动类
/**
*
* @Author xuyisu
* @Date 2019/12/7
*/
@EnableDiscoveryClient
@SpringBootApplication
public class FwRibbonServerApplication {
public static void main(String[] args) {
SpringApplication.run(FwRibbonServerApplication.class, args);
}
}
1.3 添加配置
server:
port: 8773
spring:
application:
name: fw-cloud-ribbon-server
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
这里为了更好的演示RESTFUL接口,我创建了一个User类
1.4 新建实体
/**
* @author xuyisu
* @description 用户
* @date 2019/12/11
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
/**
* 主键
*/
private long id;
/**
* 用户名
*/
private String username;
/**
* 真实姓名
*/
private String realname;
/**
* 邮箱
*/
private String email;
/**
* 备注
*/
private String remark;
}
1.5 新建Service 接口
/**
* @author xuyisu
* @description 用户service
* @date 2019/12/11
*/
public interface UserService {
/**
* 模拟数据库获取所有用户
* @return
*/
List<User> getUsers();
/**
* 模拟数据库根据id获取用户
* @return
*/
User getUserById(long id);
}
1.6 新建 UserServiceImpl 实现类
这里我模拟了数据库构造数据,同时使用了java8 新特性lambda表达式查询用户信息,想要熟悉更多的java8 特性,可以看一本“Java 8 实战”的书。
/**
* @author xuyisu
* @description User接口实现
* @date 2019/12/11
*/
@Service
public class UserServiceImpl implements UserService {
@Override
public List<User> getUsers() {
return initUser();
}
@Override
public User getUserById(long id) {
List<User> userList = getUsers().stream().filter(user -> user.getId() == id).collect(Collectors.toList());
if(CollectionUtils.isEmpty(userList)){
return new User(0,null,null,null,"这位顾客先拿一下腰牌!");
}
return userList.get(0);
}
/**
* 模拟数据库初始化数据
* @return
*/
private List<User> initUser(){
List<User> userList =new ArrayList<>();
User user1=new User(1,"113445","刘备","liubei@gmail.com","汉室刘皇叔,蜀国大佬");
User user2=new User(2,"123456","关羽","guanyu@gmail.com","人称关二爷,蜀国五虎上将");
User user3=new User(3,"147258","张飞","zhangfei@gmail.com","此人性格暴躁,蜀国五虎上将");
userList.add(user1);
userList.add(user2);
userList.add(user3);
return userList;
}
}
1.7 接下来编写RESTFUL 接口
这里我们增加了一个log日志,我们将当前提供服务的是url地址打印出来,这样方便我们看到演示效果。
/**
* @author xuyisu
* @description ribbon demo接口演示
* @date 2019/12/11
*/
@RestController
@RequestMapping("user")
public class RibbonController {
@Autowired
private UserService userService;
@GetMapping("/{id:\\d+}")
public User getUserById(@PathVariable Long id, HttpServletRequest req){
String url = req.getRequestURL().toString();
User user = userService.getUserById(id);
user.setRemark(user.getRemark()+":提供服务的是:"+url);
return user;
}
}
1.8 启动项目
启动之前记得先启动Eureka服务端
通过postman 验证没问题
下面我们启动8774 端口的服务,跟之前一样,我们需要修改一下配置,允许并行启动
然后修改端口为8774之后再启动
server:
port: 8774
2.总结
本节我们了解了Ribbon 的请求结构及服务端的搭建,下面我们再创建Ribbon客户端,看看Ribbon 客户端如何跟服务端发生亲密的接触。
Ribbon客户端
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client
下面我们开始创建Ribbon客户端的项目,本节我们会讲到Ribbon调用的原理和自定义规则。
1.新建客户端
新建模块fw-cloud-ribbon
,注意是New Module
,我们创建的是一个聚合工程
1.1 maven 配置
这里我们使用Spring Cloud 官方提供的spring-cloud-starter-netflix-ribbon
<dependencies>
<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.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
</dependencies>
1.2 新建启动类
/**
*
* @Author xuyisu
* @Date 2019/12/6
*/
@EnableDiscoveryClient
@SpringBootApplication
public class FwRibbonApplication {
public static void main(String[] args) {
SpringApplication.run(FwRibbonApplication.class, args);
}
}
1.3 项目配置
server:
port: 8772
spring:
application:
name: fw-feign
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
因为服务端是接口返回的User 类,所以接收端也需要同样的User 类
1.4 新建User 类
注意
@NoArgsConstructor
如果不添加反序列化的时候回报错
/**
* @author xuyisu
* @description 用户
* @date 2019/12/11
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
/**
* 主键
*/
private long id;
/**
* 用户名
*/
private String username;
/**
* 真实姓名
*/
private String realname;
/**
* 邮箱
*/
private String email;
/**
* 备注
*/
private String remark;
}
1.5 添加 Ribbon 的配置
/**
* @author xuyisu
* @description ribbon配置
* @date 2019/12/10
*/
@Configuration
public class EurekaRibbonConfig {
@Bean
@LoadBalanced // 实现负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
1.5.1 @LoadBalanced 原理
在 RestTemplate 上加了一个 @LoadBalanced 注解就可以负载均衡。这是因为 Spring Cloud 做了大量的底层封装,做了很多简化。
内部的主要逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行了替换,或者根据具体的负载策略选择服务地址,然后去调用,这就是 @LoadBalanced 的原理。
Spring Web 为 HttpClient 提供了 Request 拦载器 ClientHttpRequestInterceptor,位于 spring-web jar 包下。
在 spring-cloud-commons 包中提供了负载均衡自动配置类 LoadBalancerAutoConfiguration ,里面维护了一个 @LoadBalanced 注解的 RestTemplate 列表,里面的静态类 LoadBalancerInterceptorConfig 注册了 负载均衡拦截器 LoadBalancerInterceptor,RestTemplateCustomizer 来添加拦截器列表。
负载均衡拦截器 LoadBalancerInterceptor 实现了 ClientHttpRequestInterceptor,主要逻辑在 intercept() 方法中,执行交给了 LoadBalancerClient,通过 LoadBalancerRequestFactory 来构建一个 LoadBalancerRequest 对象,createRequest 方法中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,核心是通过 reconstructURI() 方法实现,该方法的业务实现是在 RibbonLoadBalancerClient 类中 。
1.6 创建Ribbon Service
/**
* @author xuyisu
* @description
* @date 2019/12/10
*/
@Service
@Slf4j
public class EurekaRibbonService {
@Autowired
RestTemplate restTemplate;
public User findUserById(Long id) {
// http://服务提供者的serviceId/url
return restTemplate.getForObject("http://fw-cloud-ribbon-server/user/" + id, User.class);
}
}
restTemplate.getForObject("http://fw-cloud-feign-server/user/" + id, User.class)
这里需要添加正确的服务名称,RestTemplate 这样请求的好处就是不需要关心应用的IP,通过Eureka 注册中心发现是此服务名的服务提供者就会去请求。非常方便应用的水平拓展。
1.7 Ribbon RESTFUL 请求层
/**
* @author xuyisu
* @description controller
* @date 2019/12/10
*/
@RestController
public class EurekaRibbonController {
@Resource
private EurekaRibbonService eurekaRibbonService;
/**
* 根据id获取用户信息
* @param id
* @return
*/
@GetMapping("/user/{id:\\d+}")
public User findUserById(@PathVariable long id){
return eurekaRibbonService.findUserById(id);
}
}
\\d+
是一个正则表达式,这个正则表达式的含义就是请求的id只能是正整数
1.8 启动应用
通过postman 验证发现,分别输入localhost:8771/user/1
结果如下
通过返回结果可以看到8773和8774一直在变化,轮询着变化,原因是什么呢?下面我们说一下负载均衡的策略。
1.9 负载均衡策略
Ribbon 默认的负载策略是轮询,同时也提供了很多其他的策略能够让用户根据业务需求来选择。负载均衡策略的根接口是 com.netflix.loadbalancer.IRule
- BestAvailableRule
选择最小并发请求的服务器,每个客户端都会获得一个随机的服务器列表,如果服务器被标记为错误,则跳过。 - AvailabilityFilteringRule
用于过滤连接一直失败或读取失败而被标记为 circuit breaker tripped 状态的服务;或持有超过可配置限制的活动连接(默认值为 Integer.MAX_VALUE),即过滤掉高并发的后端服务。实际就是检查获取到的服务列表里,各个 Server 的 Status 。 - ZoneAvoidanceRule
根据区域和可用性来过滤服务器。使用 ZoneAvoidancePredicate 来判断 Zone 的使用是否达到阀值,过滤出最差 Zone 中的所有服务器; AvailabilityPredicate 用于过滤出并发连接过多的服务。 - RandomRule
随机分配流量,即随机选择一个 Server。 - RetryRule
向现有负载均衡策略添加重试机制。 - ResponseTimeWeightedRule
该策略已过期,同见 WeightedResponseTimeRule。 - WeightedResponseTimeRule
根据响应时间为每个服务器动态分配权重(Weight)分,然后台加权循环的方式使用该策略。响应时间越长,权重越低,被选中可能性越低。
以上是Ribbon 自带的负载策略,如果我们想改变策略怎么改?我们可以自定义策略吗? 请见下一节 Ribbon 自定义负载规则
Ribbon自定义操作
1. Ribbon修改请求策略
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client
前面已经说,Ribbon 默认的请求策略是轮询,如何调整为其他策略呢?
1.1 修改EurekaRibbonConfig
路径在fw-cloud-ribbon
中的com.yisu.ribbon.config.EurekaRibbonConfig
中
我们只需要定义一个bean,比如替换Ribbon 自带的随机策略
@Bean
public IRule ribbonRule() {
//自定义成随机
return new RandomRule();
}
1.2 重启客户端
修改好之后启动客户端,postman 重新请求localhost:8771/user/1
多请求几次,会发现会有连续请求几次的接口是8773或8774服务提供的。
2. 如何自定义请求策略
下面我们自定义负载请求的策略规则,新定义规则需要实现IRule
并实现里面的方法,其中规则策略的实现是在choose(Object o)
中。比如我们现在实现一个只取第一个注册到Eureka服务中的提供者实例。
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client
2.1 新建规则类
/**
* @author xuyisu
* @description 自定义负载规则
* @date 2019/12/22
*/
@Slf4j
public class MyRule implements IRule {
private ILoadBalancer loadBalancer;
@Override
public Server choose(Object o) {
List<Server> allServers = loadBalancer.getAllServers();
//输出一遍提供者实例
allServers.stream().forEach(server -> System.out.println(server.getHostPort()));
if(CollectionUtils.isEmpty(allServers)){
log.info("当前不存在负载调用的提供者实例");
return null;
}
return allServers.get(0);
}
@Override
public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
this.loadBalancer=iLoadBalancer;
}
@Override
public ILoadBalancer getLoadBalancer() {
return this.loadBalancer;
}
}
2.2 修改EurekaRibbonConfig
路径在fw-cloud-ribbon
中的com.yisu.ribbon.config.EurekaRibbonConfig
中
我们只需要替换我们自己定义的负载规则
@Bean
public IRule ribbonRule() {
//自定义规则
return new MyRule();
}
2.3 重启客户端
通过postman 重新请求localhost:8771/user/1
多请求几次,会发现返回的结构永远是第一次注册到Eureka服务上的提供者
3. 如何不再Eureka环境下运行
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-client
3.1 修改配置
在无Eureka 环境下如何运行Spring Cloud RIbbon 提供的负载均衡?修改一下application.yml配置
ribbon:
eureka:
enabled: false #禁用eurake
# 禁用 Eureka 后手动配置服务地址
fw-cloud-ribbon-server: #服务名称
ribbon:
listOfServers: localhost:8773,localhost:8774
这里需要将Eureka设置为false,就是不启用从Eureka获取服务实例,但是需要将已经启动的服务端实例的地址设置在调用服务的后面,规则如下
服务名称(spring.application.name):
ribbon:
listOfServers: ip:port,ip:port
3.2 重新启动
通过postman 重新请求localhost:8771/user/1
并且控制台会把配置的实例地址输出来
2019-12-22 15:03:55.411 INFO 19300 --- [nio-8772-exec-2] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client fw-cloud-ribbon-server initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=fw-cloud-ribbon-server,current list of Servers=[localhost:8773, localhost:8774],Load balancer stats=Zone stats: {unknown=[Zone:unknown; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:localhost:8774; Zone:UNKNOWN; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
, [Server:localhost:8773; Zone:UNKNOWN; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:com.netflix.loadbalancer.ConfigurationBasedServerList@d0606e7
localhost:8773
localhost:8774