1、Ribbon是什么
Ribbon是Netflix发布的开源项目,Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的框架。
2、Ribbon能干什么
LB负载均衡(Load Balance)是什么?简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,硬件 F5等。
3.负载均衡使用场景
现在Java非常流行微服务,也就是所谓的面向服务开发,将一个项目拆分成了多个项目,其优点有很多,其中一个优点就是:将服务拆分成一个一个微服务后,我们很容易的来针对性的进行集群部署。例如订单模块用的人比较多,我就可以将这个模块多部署几台机器,来分担单个服务器的压力。
这时候有个问题来了,前端页面请求的时候到底请求集群当中的哪一台?既然是降低单个服务器的压力,所以肯定全部机器都要利用起来,而不是说一台用着,其他空余着。这时候就需要用负载均衡了,像这种前端页面调用后端请求的,要做负载均衡的话,常用的就是Nginx。
4.Ribbon和Nginx负载均衡区别
- 当后端服务是集群的情况下,前端页面调用后端请求,要做负载均衡的话,常用的就是Nginx。
Ribbon主要是在服务端内做负载均衡,举例:订单后端服务 要调用 支付后端服务,这属于后端之间的服务调用,压根根本不经过页面,而支付后端服务是集群,这时候订单服务就需要做负载均衡来调用支付服务,记住是订单服务做负载均衡 来调用 支付服务。
5.负载均衡分类
- 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
- 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon负载均衡
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程。
举例:微服务经常会涉及到A服务调用B服务的接口,这时候就需要用HTTP远程调用框架,常见的有Feign、RestTemplate、HttpClient,假如B服务只有一个节点,这时候我们可以在调用的时候写固定ip来进行调用,假如B服务的节点存在多个(也就是集群),那A服务究竟调用B服务的哪个节点呢,这时候可以通过负载均衡框架来计算出调用哪个,比如轮询调用B服务的多个节点,总不可能一直调用人家的一个服务,这样B服务的集群有什么意义呢?或者也可以随机调用任意节点,总之负载均衡的作用就是避免一直调用一个节点。
大概的流程:RestTemplate或者Feign可以通过注册中心拿到服务提供方的IP+端口,假如提供者有多个,那他就会拿到多个地址,有了这些地址就差访问的时候访问哪个地址的服务了,而Ribbon可以很好的和RestTemplate或者Feign进行集成,来决定调用哪个服务,具体是负载均衡还是随机Ribbon都可以设置。
项目处于维护状态 ,已经一年多没有更新过了。
6.Ribbion架构说明
首先通过上图一定要明白一点:ribbon一定是用在消费方,而不是服务的提供方!
Ribbon在工作时分成两步(这里以Eureka为例,consul和zk同样道理):
第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server.
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
之前写样例时候没有引入spring-cloud-starter-ribbon也可以使用ribbon,这是为什么?
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
猜测spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用,证明如下: 可以看到spring-cloud-starter-netflix-eureka-client 确实引入了Ribbon(zk和consul注册中心同样是如此)
7 .Ribbon负载均衡策略
Ribbon负载均衡策略是 IRule接口定义的,每一个子接口都是一种策略。下面是常见负载均衡算法
1.负载均衡种类
负载均衡类 | 规则 |
---|---|
RoundRobinRule | 轮询选择服务器。Ribbon默认负载均衡算法。 |
AvailabilitvFilteringRule | (1) 默认情况,服务器连接3次失败,服务器设置为短路状态。短路状态持续30秒,如果再连接失败,短路状态持续时间就会几何级增长。 (2)并发数高的服务器。可配置该规则会忽略并发数高的服务器。并发数上限可以配置。 |
WeightedResponseTimeRule | 给服务器赋予权重。响应时间越长,权重越小。权重影响服务器的选择。 |
ZoneAvoidanceRule | 使用Zone对服务器进行分类。Zone可以是一个机房、一个机架等。然后对Zone内的服务进行轮询。例如:配置Zone是曹县机房,然后服务提供者会优先选择曹县机房的服务器。 |
BestAvailableRule | 忽略短路的服务器并选择并发数低的服务器。 |
RandomRule | 随机选择一个可用服务器。 |
RetryRule | 重试机制的选择逻辑。 |
8.置负载均衡
8.1.RestTemplate远程调用配置负载均衡
@Autowiredprivate RestTemplate restTemplate;public Order queryOrderById(Long orderId) {// 根据订单id查询订单Order order = orderMapper.findById(orderId);// 利用RestTemplate发起http请求,根据用户id查询用户// 把localhost改为服务名称这里这里 String url = "http://userservice/user/" + order.getUserId();//String url = "http://localhost:8081/user/" + order.getUserId();// 发送http请求,实现远程调用,现在是get请求类型User user = restTemplate.getForObject(url, User.class);// 封装user到Orderorder.setUser(user);// 返回值return order;}
注意: Feign集成了Ribbon,不用去特别配置负载均衡。
2.RestTemplate配置类加上注解
/*** 创建RestTemplate并注入Spring容器*/@Bean@LoadBalanced //负载均衡注解public RestTemplate restTemplate() {return new RestTemplate();}
8.2 配置负载均衡
(1) 代码类型
注意:这种配置是全局的,订单服务不光调用用户服务是随机的,调用其他的服务也是随机的
//注入新的负载均衡算法 在启动类内注入也可以自定义配置类
@Bean
public IRule randomRule() {//随机选取一个可用服务器return new RandomRule();
}
(2) yml中配置
注意:这种局部配置,只针对用户服务去选择负载均衡的算法
userservice: # 服务名称ribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.RandomRule # 负载均衡规则
(3) Ribbon的饥饿加载配置
Ribbon默认是懒加载: 第一次加载才会创建 LoadBalancerClient ,请求时间会很长。
饥饿加载: 在项目启动时创建,降低第一次访问的耗时。
在Spring Cloud中,Ribbon的饥饿加载是指在启动时就加载并初始化相关的服务调用客户端配置,而不是等到第一次请求时再进行加载。这样做可以提高系统的响应速度,因为它避免了第一次请求的延迟。要配置Ribbon的饥饿加载,你需要在你的应用程序中设置spring.cloud.load-balancer.ribbon.enabled为true,默认情况下它是开启的。如果你需要关闭它,可以设置为false
(1) 单个服务配置饥饿加载(在yml文件配置)
spring:cloud:load-balancer:ribbon:eager-load:enabled: true # 开启Ribbon的饥饿加载clients: # 指定需要进行饥饿加载的服务列表- service-id-1- service-id-2
(1) 多个服务配置饥饿加载(在yml文件配置)
spring:cloud:load-balancer:ribbon:eager-load:enabled: true # 开启饥饿加载clients: # 指定饥饿加载的服务名称- userservice # 用户服务- cangkuservice # 仓库服务- zhifuservice # 支付服务
在这个配置中,service-id-1和service-id-2是你希望在启动时就加载并初始化的服务的ID。
注意:开启Ribbon的饥饿加载可能会稍微增加启动时间,因为它会预先初始化与服务相关的客户端。但是,这对于提高系统的整体响应速度是有益的。
九 Ribbon实现负载均衡的原理
Ctrl + N 快速查找类:原理是 LoadBalancerInterceptor 实现了 ClientHttpRequestInterceptor 接口。
客户端http请求拦截器
@FunctionalInterface
public interface ClientHttpRequestInterceptor {ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
}
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {重写ClientHttpRequestInterceptor接口的方法@Overridepublic ClientHttpResponse intercept(final HttpRequest request, final byte[] body,final ClientHttpRequestExecution execution) throws IOException {// 1.获取http的请求地址final URI originalUri = request.getURI();// 2.获取服务的名称String serviceName = originalUri.getHost();Assert.state(serviceName != null,"Request URI does not contain a valid hostname: " + originalUri);//3.我们进去execute方法:就是为了从Eureka-Server拉取信息return this.loadBalancer.execute(serviceName,this.requestFactory.createRequest(request, body, execution));}
}
public class RibbonLoadBalancerClient implements LoadBalancerClient {//4.进入此方法public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {//调用下面的execute方法return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);}public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {//5.根据服务名称获取对应的服务列表,loadBalancer里面有List存有服务的地址ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);//6.根据算法选择要使用的服务,会进入IRule接口里面,默认是轮询算法Server server = this.getServer(loadBalancer, hint);if (server == null) {throw new IllegalStateException("No instances available for " + serviceId);} else {RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);}}
}
十 Ribbon单独使用,配置自动重试,实现负载均衡和高可用
JDK 1.8,SpringCloud Greenwich.SR2,SpringBoot 2.1.3.RELEASE
本文示例,CONSUMER-SERVICE服务调用PRODUCER-SERVICE服务。在进行以下步骤前,请先启动两个普通的SpringBoot服务PRODUCER-SERVICE。
2.1 pom依赖
因为这里独立使用Ribbon,所以CONSUMER-SERVICE只需要spring-cloud-starter-netflix-ribbon,启动主类也无需更多的注解,如
@EnableEurekaClient、@EnableDiscoveryClient、@EnableCircuitBreaker等,只需保留@SpringBootApplication即可。
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency>
</dependencies>
<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Greenwich.SR2</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>
2.2 RestTemplate配置
Ribbon是对RestTemplate的加强,需要为RestTemplate添加注解@LoadBalanced,使之具有负载均衡能力。如下:
@Bean
@LoadBalanced
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {RestTemplate restTemplate = new RestTemplate(factory);restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));return restTemplate;
}
2.3 Ribbon配置
注意:网上和书上很多教程都没有提到ribbon.restclient.enabled这一配置,导致再怎么尝试都无法成功自动重试。
spring.application.name=CONSUMER-SERVICE
server.port=8801ribbon.restclient.enabled=true
#开启重试机制
spring.cloud.loadbalancer.retry.enabled=true
#请求连接的超时时间
PRODUCER-SERVICE.ribbon.ConnectTimeout=250
#请求处理的超时时间
PRODUCER-SERVICE.ribbon.ReadTimeout=1000
#对所有操作请求都进行重试,默认false,只有GET请求会重试;这是防止POST等对数据有影响的请求在重试后因为接口未做幂等性导致数据异常,影响较大
PRODUCER-SERVICE.ribbon.OkToRetryOnAllOperations=true
#指定请求重试开关,经调试源码该属性未被使用,疑似bug,导致不论怎么设置,都是只有服务提供者的Get请求可以被自动重试
#PRODUCER-SERVICE.ribbon.RequestSpecificRetryOn=true
#切换实例的重试次数
PRODUCER-SERVICE.ribbon.MaxAutoRetriesNextServer=2
#对当前实例的重试次数
PRODUCER-SERVICE.ribbon.MaxAutoRetries=1#服务PRODUCER-SERVICE的地址
PRODUCER-SERVICE.ribbon.listOfServers=localhost:8080,localhost:8083
2.4 Ribbon调用服务
在Controller中,注入RestTemplate,使用服务名(即spring.application.name)的方式,调用PRODUCER-SERVICE服务的GET接口,如下:
@Controller
@RequestMapping("consumer/api")
public class ConsumerController {@Autowiredprivate RestTemplate restTemplate;@GetMapping("/test")@ResponseBodypublic String test() {ResponseEntity<String> entity = restTemplate.getForEntity("http://PRODUCER-SERVICE/outer/data?res=3&msgKey=token123", String.class);return entity.getBody();}
}
三、测试运行
3.1 负载均衡测试
启动一个消费者服务CONSUMER-SERVICE,多次访问/consumer/api/test,可以通过给PRODECER-SERVICE服务的/outer/data接口添加调试日志的打印,来确认默认使用了轮询的负载均衡策略。
3.2 高可用测试
停止其中一个PRODECER-SERVICE服务实例,确认轮询到已停止的服务时,可以成功地在未停止的服务上自动重试请求。
四、无法成功自动重试的几种情况
本人在单独使用Ribbon的过程中,碰到以下几种无法自动重试其他服务节点的情况:
4.1 ribbon.restclient.enabled
遇到Ribbon的问题,网上一搜,千篇一律,也不知道作者们是否亲自实践证明可用,就随意发篇文章。言归正传,若不设置ribbon.restclient.enabled=true,在本人的实验环境中是无法自动重试的。
4.2 Maven打包有警告
在SpringCloud生态的开发中,各组件往往自动依赖了很多其他的jar包,如果向Maven本地仓库下载的过程中,网络不好,就会下载到一份不完整的jar包或pom文件,最终可能会导致打包出错。下面是在使用Maven打包项目时,比较常见下面的警告:
[WARNING] The POM for com.sun.jersey:jersey-core:jar:1.19.1 is
invalid, transitive dependencies (if any) will not be available
[WARNING] The POM for com.sun.jersey:jersey-client:jar:1.19.1 is
invalid, transitive dependencies (if any) will not be available
可以看出来,提示我们传递依赖将失效,所以有可能整个打包过程是SUCCESS的,但是最后启动jar包时,却可能报NoClassDef等缺少jar包的错误。因此,我们需要在打包时加上参数 -X 以查看具体原因:
[WARNING] The POM for com.sun.jersey:jersey-core:jar:1.19.1 is
invalid, transitive dependencies (if any) will not be available: 1
problem was encountered while building the effective model for
com.sun.jersey:jersey-core:[unknown-version] [FATAL] Non-parseable POM
E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom:
processing instruction can not have PITarget with reserved xml name
(position: END_TAG seen …\n\n\n<?xml …
@627:7) @
E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom,
line 627, column 7 [WARNING] The POM for
com.sun.jersey:jersey-client:jar:1.19.1 is invalid, transitive
dependencies (if any) will not be available: 1 problem was encountered
while building the effective model for
com.sun.jersey:jersey-client:[unknown-version] [FATAL] Non-parseable
POM
E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom:
processing instruction can not have PITarget with reserved xml name
(position: END_TAG seen …\n\n\n<?xml …
@627:7) @
E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom,
line 627, column 7 [WARNING] The POM for
com.sun.jersey.contribs:jersey-apache-client4:jar:1.19.1 is invalid,
transitive dependencies (if any) will not be available: 1 problem was
encountered while building the effective model for
com.sun.jersey.contribs:jersey-apache-client4:[unknown-version]
[FATAL] Non-parseable POM
E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom:
processing instruction can not have PITarget with reserved xml name
(position: END_TAG seen …\n\n\n<?xml …
@627:7) @
E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom,
line 627, column 7
特别注意FATAL这个最严重的日志级别的信息, 日志提醒我们本地仓库的pom文件E:\maven\repository\com\sun\jersey\jersey-project\1.19.1\jersey-project-1.19.1.pom解析出错,然后我们去此目录下,可以发现这个文件未被下载完整。最后的解决方式是删除此文件的父目录,重新打包,则会自动下载一份完整的pom文件,警告消失,打包成功。
4.3 OkToRetryOnAllOperations和RequestSpecificRetryOn失效
在上文的例子中,消费者服务调用的生产者服务接口是GET类型,自动重试没有任何问题。接着,本人尝试让消费者调用生产者服务的POST接口,同时仍然设置了ribbon.OkToRetryOnAllOperations=true,结果无法成功重试,然后去调试ribbon源码,查看自动重试机制,经查,OkToRetryOnAllOperations和RequestSpecificRetryOn属性可以成功获取到,但RequestSpecificRetryOn并未被获取出来使用,疑似Ribbon的bug。这里记录一下调试的重要部分,
(1)Ribbon配置属性类com.netflix.client.config.CommonClientConfigKey;
(2)Ribbon判断是否重试:
所以,单独使用Ribbon需谨慎!
十一、Spring Cloud Ribbon配置详解
常用配置
1. 禁用 Eureka
当我们在 RestTemplate 上添加 @LoadBalanced 注解后,就可以用服务名称来调用接口了,当有多个服务的时候,还能做负载均衡。
这是因为 Eureka 中的服务信息已经被拉取到了客户端本地,如果我们不想和 Eureka 集成,可以通过下面的配置方法将其禁用。
# 禁用 Eureka
ribbon.eureka.enabled=false
当我们禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。
2. 配置接口地址列表
上面我们讲了可以禁用 Eureka,禁用之后就需要手动配置调用的服务地址了,配置如下:
# 禁用 Eureka 后手动配置服务地址
ribbon-config-demo.ribbon.listOfServers=localhost:8081,localhost:8083
这个配置是针对具体服务的,前缀就是服务名称,配置完之后就可以和之前一样使用服务名称来调用接口了。
3. 配置负载均衡策略
Ribbon 默认的策略是轮询,从我们前面讲解的例子输出的结果就可以看出来,Ribbon 中提供了很多的策略,这个在后面会进行讲解。我们通过配置可以指定服务使用哪种策略来进行负载操作。
4. 超时时间
Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
5. 并发参数
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500
代码配置 Ribbon
配置 Ribbon 最简单的方式就是通过配置文件实现。当然我们也可以通过代码的方式来配置。
通过代码方式来配置之前自定义的负载策略,首先需要创建一个配置类,初始化自定义的策略,代码如下所示。
@Configuration
public class BeanConfiguration {
@Bean
public MyRule rule() {
return new MyRule();
}
}
创建一个 Ribbon 客户端的配置类,关联 BeanConfiguration,用 name 来指定调用的服务名称,代码如下所示。
@RibbonClient(name = "ribbon-config-demo", configuration = BeanConfiguration.class)
public class RibbonClientConfig {
}
可以去掉之前配置文件中的策略配置,然后重启服务,访问接口即可看到和之前一样的效果。
配置文件方式配置 Ribbon
除了使用代码进行 Ribbon 的配置,我们还可以通过配置文件的方式来为 Ribbon 指定对应的配置:
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer(负载均衡器操作接口)
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule(负载均衡算法)
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing(服务可用性检查)
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList(服务列表获取)
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter(服务列表的过滤)
重试机制
在集群环境中,用多个节点来提供服务,难免会有某个节点出现故障。用 Nginx 做负载均衡的时候,如果你的应用是无状态的、可以滚动发布的,也就是需要一台台去重启应用,这样对用户的影响其实是比较小的,因为 Nginx 在转发请求失败后会重新将该请求转发到别的实例上去。
由于 Eureka 是基于 AP 原则构建的,牺牲了数据的一致性,每个 Eureka 服务都会保存注册的服务信息,当注册的客户端与 Eureka 的心跳无法保持时,有可能是网络原因,也有可能是服务挂掉了。
在这种情况下,Eureka 中还会在一段时间内保存注册信息。这个时候客户端就有可能拿到已经挂掉了的服务信息,故 Ribbon 就有可能拿到已经失效了的服务信息,这样就会导致发生失败的请求。
这种问题我们可以利用重试机制来避免。重试机制就是当 Ribbon 发现请求的服务不可到达时,重新请求另外的服务。
1. RetryRule 重试
解决上述问题,最简单的方法就是利用 Ribbon 自带的重试策略进行重试,此时只需要指定某个服务的负载策略为重试策略即可:
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
2. Spring Retry 重试
除了使用 Ribbon 自带的重试策略,我们还可以通过集成 Spring Retry 来进行重试操作。
在 pom.xml 中添加 Spring Retry 的依赖,代码如下所示。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
配置重试次数等信息:
# 对当前实例的重试次数
ribbon.maxAutoRetries=1
# 切换实例的重试次数
ribbon.maxAutoRetriesNextServer=3
# 对所有操作请求都进行重试
ribbon.okToRetryOnAllOperations=true
# 对Http响应码进行重试
ribbon.retryableStatusCodes=500,404,502