文章目录
Ribbon
目前主流的负载均衡分为两种,一种是集中式负载均衡, 在消费者和服务提供方中间使用的代理方式进行负载,有硬件(比如F5),也有软件的(比如Nginx)。另一种则是客户端自己做负载均衡,根据自己的请求情况做负载,Ribbon就属于客户端自己做负载。 如果用一句话介绍,那就是Ribbon是Netflix开源的一款用于客户端负载均衡的工具软件。
GitHub地址:https://github.com/Netflix/ribbon
什么是负载均衡?
负载均衡,是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。或者这么说:负载均衡就是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。
ribbon能干嘛?
LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。
负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
常见的负载均衡软件有Nginx,Lvs等等
dubbo、SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义
负载均衡简单分类:
集中式LB:
即在服务的消费方和提供方之间使用独立的LB设施,如Nginx:反向代理服务器!由该设施负责把访问请求通过某种策略转发至服务的提供方!
进程式LB:
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选出一个合适的服务器。
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址!
1)、Ribbon模块
Ribbon模块如下:
ribbon-loadbalancer负载均衡模块,可独立使用,也可以和别的模块一起使用。Ribbon内置的负载均衡算法都实现在其中。
ribbon-eureka:基于Eureka封装的模块,能够快速、方便地集成Eureka。
ribbon-transport:基于Netty实现多协议的支持,比如HTTP、Tcp、Udp等。
ribbon-httpclient:基于Apache HttpClient封装的REST客户端,集成了负载均衡模块,可以直接在项目中使用来调用接口。
ribbon-example:Ribbon使用代码示例。
ribbon-core:一些比较核心且具有通用性的代码,客户端API的一些配置和其他API的定义。
Ribbon使用
接下来我们使用Ribbon来实现一个最简单的负载均衡调用功能,接口就用在Eureka注册中心文章中的服务提供者的接口/user/hello,需要启动两个服务,一个是8081的端口,一个是8083的端口。然后我们创建一个新的Maven项目ribbon-native-demo,在项目中集成Ribbon,在pom.xml中添加依赖:
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.0.10</version>
</dependency>
然后编写一个普通的接口:
public class Main {
public static void main(String[] args) {
//服务列表
List<Server> serverList = Arrays.asList(
new Server("localhost", 8081),
new Server("localhost", 8083)
);
//构建负载实例
ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
.buildFixedServerListLoadBalancer(serverList);
//调用5次来测试效果
for (int i = 0; i < 5; i++) {
String result = LoadBalancerCommand.<String>builder()
.withLoadBalancer(loadBalancer)
.build()
.submit(new ServerOperation<String>() {
@Override
public Observable<String> call(Server server) {
try {
String addr = "http://" + server.getHost() + ":" +
server.getPort() + "/user/hello";
System.out.println("调用地址:" + addr);
URL url = new URL(addr);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
InputStream in = conn.getInputStream();
byte[] data = new byte[in.available()];
in.read(data);
return Observable.just(new String(data));
} catch (Exception e) {
return Observable.error(e);
}
}
}).toBlocking().first();
System.out.println("调用结果:" + result);
}
}
}
这个例子主要演示了Ribbon如何去做负载操作,调用接口用的最底层的HttpURLConnection。启动服务提供者的接口/user/hello的服务,分别使用不同端口,怕有小伙伴不会同一服务启动两次,来!上教程:
首先要勾起这个:
然后启动一次服务,挂着,再然后去application.properties里更换端口:
在点击启动,这样便可以同时启动8081和8083服务,启动服务提供者的这两个服务前,记得要先启动服务中心:
运行一下这个程序可以在控制台看到:
从输出的结果中可以看到,负载起作用了。
2)、RestTemplate结合Ribbon使用
刚才我们便简单的使用了Ribbon进行了负载的一个调用,这意味着Ribbon是可以单独使用的。
那我们应该如何使用RestTemplate与整合Ribbon呢?
I、使用RestTemplate
首先我们得先会使用RestTemplate,在Eureka的学习中就使用过RestTemplate,项目便讲解具体的使用方法:
配置好依赖pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
首先来看看GET请求的使用方式:创建一个新项目spring-rest-template,配置好RestTemplate:
config/BeanConfiguration.java:
@Configuration
public class BeanConfiguration {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HouseInfo {
private Long id;
private String city;
private String region;
private String name;
}
新建一个HouseController,并增加两个接口,一个通过@RequestParam来传递参数,返回一个对象信息;另一个通过@PathVariable来传递参数,返回一个字符串。
消费接口定义:
@RestController
public class HouseController {
@GetMapping("/house/data")
public HouseInfo getData(@RequestParam("name") String name) {
return new HouseInfo(1L, "广东", "广州", "增城区");
}
@GetMapping("/house/data/{name}")
public String getData2(@PathVariable("name") String name) {
return name;
}
}
新建一个HouseClientController用于测试,使用RestTemplate来调用我们刚刚定义的两个接口:
调用接口:
public class HouseClientController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
return restTemplate.getForObject("http://localhost:8081/house/data?name=" +
name, HouseInfo.class);
}
@GetMapping("/call/data/{name}")
public String getData2(@PathVariable("name") String name) {
return restTemplate.getForObject("http://localhost:8081/house/data/{name}",
String.class, name);
}
}
获取数据结果可通过RestTemplate的getForObject方法来实现,此方法有三个重载的实现:
url: 请求的API地址,有两种方式,其中一种是字符串,另一种是URL形式。
responseType: 返回值的类型。
uriVariables: PathVariable参数,有两种方式,其中一种是可变参数,另外一种是Map形式。
可以看看getForObject方法的源码:
除了getForObject,我们还可以使用getEntity来获取数据;
getForEntity使用:
HouseClientController.java:
@GetMapping("/call/dataEntity")
public HouseInfo getData1(@RequestParam("name") String name) {
ResponseEntity<HouseInfo> responseEntity = restTemplate.getForEntity(
"http://localhost:8081/house/data?name=" + name, HouseInfo.class
);
if (responseEntity.getStatusCodeValue() == 200) {
return responseEntity.getBody();
}
return null;
}
getForEntity中可以获取返回的状态码、请求头等信息,通过getBody获取响应的内容。其余的和getForObject一样,也是有3个重载的实现,可以看看源码:
那么接下来就看看怎么使用POST方法调用接口。在HouseController中增加一个save方法用来接收HouseInfo数据:
POST接口定义:
HouseController.java:
@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
System.out.println(houseInfo.getName());
return 1001L;
}
接着写调用代码,用postForObject来调用:
HouseClientController.java:
@GetMapping("/call/save")
public Long add() {
HouseInfo houseInfo = new HouseInfo();
houseInfo.setCity("广东");
houseInfo.setRegion("广州");
houseInfo.setName("XXX");
Long id = restTemplate.postForObject(
"http://localhost:8081/house/save", houseInfo, Long.class);
return id;
}
postForObject同样有3个重载的实现。除了postForObject还可以使用postForEntity方法,用法都一样,来看源码:
除了get和post对应的方法之外,RestTemplate还提供put、delete等操作,还有一个比较实用就是exchange方法。exchange可以执行get、post、put、delete这4种请求方式。
II、整合Ribbon
在Spring Cloud项目中集成Ribbon只需要在pom.xml中加入下面的依赖即可,其实也可以不用配置,因为Eureka中已经引用了Ribbon。
Ribbon Maven配置:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
RestTemplate负载均衡示例:
前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate可以结合Eureka来动态发现服务并进行负载均衡的调用。
修改RestTemplate的配置,增加能够让RestTemplate具备负载均衡能力的注解@LoadBalanced。
RestTemplate负载配置:
@Configuration
public class BeanConfiguration {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
修改接口调用的代码,将IP+PORT改成服务名称,也就是注册到Eureka中的名称。
服务名称方式调用:
@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
return restTemplate.getForObject(
"http://ribbon-eureka-demo/house/data?name=" + name, HouseInfo.class
);
}
接口调用的时候,框架内部会将服务名称替换成具体的服务IP信息,然后进行调用。
相信在配置类中大家都看见了 @LoadBalanced,那这个注解的原理是什么呢?
相信大家一定有一个疑问:为什么在RestTemplate上加了一个@LoadBalanced之后,RestTemplate就能够跟Eureka结合了,不但可以使用服务名称去调用接口,还可以负载均衡?
应该归功于Spring Cloud给我们做了大量的底层工作,因为它将这些都封装好了,我们用起来才会那么简单。框架就是为了简化代码,提高效率而产生的。
这里主要的逻辑就是给RestTemplate 增加拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务地址,然后再去调用,这就是@LoadBalanced的原理。
接下来我们便看看 @LoadBalanced的工作原理源码是一个怎样的一个逻辑:
首先看配置类,如何为RestTemplate设置拦截器,代码在spring-cloud-commons.jar中的org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration类里面通过查看LoadBalancerAutoConfiguration的源码,可以看到这里也是维护一个@LoadBalanced的RestTemplate列表:
LoadBalanced拦截注入源码(一)
通过查看拦截器的配置可以知道,拦截器用的是LoadBalancerInterceptor,RestTemplateCustomizer用来添加拦截器
LoadBalanced拦截注入源码:
拦截器的代码在org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor中
LoadBalanced拦截注入源码:
主要的逻辑在intercept中,执行交给了LoadBalancerClient来处理,通过LoadBalancerRequestFactory来构建一个LoadBalancerRequest对象
LoadBalancerRequest拦截注入源码:
createRequest中通过ServiceRequestWrapper来执行替换URI的逻辑,ServiceRequestWrapper中将URI的获取交给了org.springframework.cloud.client.loadbalancer.LoadBalancerClient#reconstructURI方法。
以上就是整个RestTemplate结合@LoadBalanced的执行流程。
Ribbon API使用:
当我们有一些特殊需求,想通过Ribbon获取对应的服务信息时,可以使用Load-Balancer Client来获取,比如想获取一个ribbon-eureka-demo服务的服务地址,可以通过LoadBalancerClient的choose方法来选择一个:
Ribbon Client使用:
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/choose")
public Object choose() {
ServiceInstance instance = loadBalancerClient.choose("ribbon-eureka-demo");
return instance;
}
访问接口,可以看到返回的信息。
Ribbon饥饿加载
在进行服务调用的时候,如果网络情况不好,第一次调用会超时?有很多大神对此提出了解决方案,比如把超时时间改长一点、禁止超时等。而Spring Cloud随着版本的更新,提供了最优的解决方案。Ribbon的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化Client的时间再加上请求接口的时间,就会导致第一次请求超时。
而下面将介绍Finchley.SR2版本提供的一种针对上述问题的解决方法,那就是eager-load方式。通过配置eager-load来提前初始化客户端就解决这个问题。
ribbon.eager-load.enabled=true
ribbon.eager-load.clients=ribbon-eureka-demo
ribbon.eager-load.enabled:开启Ribbon的饥饿加载模式。
ribbon.eager-load.clients:指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。
怎么进行验证呢?网络情况确实不太好的模拟,不过通过调试源码的方式即可验证,在org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration中找到对应的代码:
Ribbon饥饿加载配置源码:
在return这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了。
3)、负载均衡策略介绍
Ribbon作为一款客户端负载均衡框架,默认的负载策略是轮询,同时也提供了很多其他的策略,能够让用户根据自身的业务需求进行选择。
整体策略代码实现类:
图中:
BestAvailableRule:选择一个最小的并发请求的Server,逐个考察Server,如果Server被标记为错误,则跳过,然后再选择ActiveRequestCount中最小的Server。
AvibliliteringRul:过滤掉那些一直连接失败的且被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤Server的逻辑。其实就是检查Status里记录的各个Server的运行状态。
ZoneAvoidanceRule:使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个Server,前一个判断判定个Zone 的运行性能是否可用, 剔除不可用的 Zone (的所有Server),AvailabilityPredicate 用于过滤掉连接数过多的Server。
RandomRule:随机选择一个Server。
RoundRobinRule::轮询选择,轮询index,选择index对应位置的Server。
RetryRule:对选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择Server不成功,则一直尝试使用subRule的方式选择一个可用的Server。
ResponseTimeWeightedRule:作用同WeightedResponseTimeRule,ResponseTimeWeightedRule后来改名为WeightedResponseTimeRule。
WeightedResponseTimeRule:根据响应时间分配一个Weight (权重),响应时间越长,Weight越小,被选中的可能性越低。
4)、自定义负载策略
通过实现IRule接口可以自定义负载策略,主要的选择服务逻辑在choose方法中。
自定义负载策略:
public class MyRule implements IRule {
private ILoadBalancer lb;
@Override
public Server choose(Object key) {
List<Server> servers = lb.getAllServers();
for (Server server : servers) {
System.out.println(server.getHostPort());
}
return servers.get(0);
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer() {
return lb;
}
}
在Spring Cloud中,可通过配置的方式使用自定义的负载策略,ribbon-config-demo是调用的服务名称。
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.springresttemplate.ribbon_eureka_demo.rule.MyRule
重启服务,访问调用了其他服务的接口,可以看到控制台的输出信息中已经有了我们自定义策略中输出的服务信息,并且每次都是调用第一次服务。这跟我们的逻辑是相匹配的。
5)、配置详情
I、常用配置
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中提供了很多的策略,这个在后面会进行讲解。我们通过配置可以指定服务使用哪种策略来进行负载操作。
#配置负载均衡策略
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com. netflix. loadbalancer.RandomRule
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
II、代码配置Ribbon
配置Ribbon最简单的方式就是通过配置文件实现。当然我们也可以通过代码的方式来配置。
通过代码方式来配置之前自定义的负载策略,首先需要创建一个配置类,初始化自定义的策略:
自定义负载策略配置:
@Configuration
public class BeanConfiguration {
@Bean
public MyRule rule() {
return new MyRule();
}
}
创建一个Ribbon客户端的配置类,关联BeanConfiguration,用name来指定调用的服务名称,代码如下:
Ribbon配置使用:
@RibbonClient(name = "ribbon-config-demo", configuration = BeanConfiguration.class)
public class RibbonClientConfig {
}
可以去掉之前配置文件中的策略配置,然后重启服务,访问接口即可看到和之前一样的效果。
III、配置文件方式配置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 ServerList-Filter(服务列表的过滤)
6)、重试机制
在集群环境中,用多个节点来提供服务,难免会有某个节点出现故障。用Nginx做负载均衡的时候,如果你的应用是无状态的、可以滚动发布的,也就是需要一台台去重启应用,这样对用户的影响其实是比较小的,因为Nginx在转发请求失败后会重新将该请求转发到别的实例上去。
由于Eureka是基于AP原则构建的,牺牲了数据的一致性,每个Eureka服务都会保存注册的服务信息,当注册的客户端与Eureka的心跳无法保持时,有可能是网络原因,也有可能是服务挂掉了。在这种情况下,Eureka中还会在一段时间内保存注册信息。这个时候客户端就有可能拿到已经挂掉了的服务信息,故Ribbon就有可能拿到已经失效了的服务信息,这样就会导致发生失败的请求。
这种问题我们可以利用重试机制来避免。重试机制就是当Ribbon发现请求的服务不可到达时,重新请求另外的服务。
I、RetryRule重试
解决上述问题,最简单的方法就是利用Ribbon自带的重试策略进行重试,此时只需要指定某个服务的负载策略为重试策略即可:
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
II、Spring Retry重试
除了使用Ribbon自带的重试策略,我们还可以通过集成Spring Retry来进行重试操作。在pom.xml中添加Spring Retry的依赖:
Spring Retry Maven配置:
<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
至此关于Ribbon的学习就到这里,相信大家看完后明白Ribbon是一款非常优秀的客户端负载均衡组件,在Spring Cloud中集成Ribbon可以让我们的服务调用具备负载均衡的能力。
在Spring Cloud中结合RestTemplate使用Ribbon。用RestTemplate调用接口还是比较麻烦的,所以下一篇文章将学习如何通过Feign去优雅地调用服务中的接口(~ ̄▽ ̄)~