开端
目前主流的负载方案一般为两种:一种是集中式的负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件(F5),也有软件(Nginx)。另一种则是客户端自己做负载均衡。我觉得使用Nginx也是一个不错的方式,但是这样我们还要令开一块专题去学习,既然在学Spring Cloud,我们就来试试使用Netflix开源的Ribbon做负载。
另外ribbon的内容不是特别多,我们在此还会学习另一个知识点,声明式REST客户端Feign。
Ribbon
开始学习Ribbon。
了解
首先还是来了解一下Ribbon的基本模块:
- ribbon-loadbalancer:负载均衡模块,内置的负载均衡算法都实现在其中。
- ribbon-eureka:基于Eureka封装的模块,能够快速集成。
- ribbon-transport:基于Netty实现多协议的支持,比如HTTP、TCP、UDP等。
- ribbon-httpclient:基于Apache HttpClient封装的REST客户端,集成了负载均衡模块,可以直接在项目中调用接口。
- ribbon-example:代码示例,供学习。
- ribbon-core:核心且通用的代码,客户端API的一些配置和其他API的定义。
使用
浅尝
结束初步的了解,咱们直接开始上手。关于我们之前Eureka的项目文件,大家可以放在一起,右键每一个pom文件后使用add as maven project功能构建。如此:
这样一来就方便大家管理项目,以及统一启动和终止。我们首先复制一份eureka-client项目,创建一个消费者集群。启动类、控制层、配置文件、Maven文件的修改这里就不写了,都是些小细节。我将这个项目的端口设置为8084,然后启动,回到注册中心就可以看到集群成功。
现在我们需要右键外面的大项目文件夹,重新创建一个ribbon-native-demo项目,创建方式同client。为pom添加依赖:
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
</dependency>
复制一份client的yml,修改应用名为ribbon-native-demo,端口为8083。然后写一个测试类调用hello接口。
public static void main(String[] args) {
List<Server> serverList = Lists.newArrayList(
new Server("localhost", 8081),
new Server("localhost", 8084)
);
ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
.buildFixedServerListLoadBalancer(serverList);
for (int i = 0; i < 6; 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() + "/test/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);
}
}
跑起来之后我们看到结果:
循环六次,三次分配给了8081端口,另外三次分配给了8084端口,实现了负载均衡。这时候,各位同学有没有想起来在上一篇中我们添加了服务消费者,使用了RestTemplate的那个,还记不记得我们在调用接口时并没有指明端口号,而是直接使用服务的名字来调用,这时候负载的作用就体现出来了。仔细领悟,RestTemplate的具体用法我就不展开了。
值得一提的是,如果想要脱离Eureka使用Ribbon,请加入下列pom,由于我们测试的时候是使用了Eureka的,所以没有配置:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-atarter-netflix-ribbon</artifactId>
</dependency>
辄止
说到底,我们在使用RestTemplate调用接口时,最关键的步骤在于@LoadBalanced
这一个注解。多亏Spring Cloud为我们做了大量的底层工作,才得以如此方便的使用。其实它的原理莫过于拦截请求-替换地址-选择均衡策略-调用,这几步。
由于源码像裹脚布一样又臭又长,难以理解,这里就不做分析,大体知道这个逻辑即可。
拓展
接下来从我们方便接触的角度尝试一下Ribbon的一些拓展功能。
Ribbon API
有时候面对一些特殊的需求,可能需要通过Ribbon获取对应的服务信息,可以使用LoadBalancerClient来获取,比如你想获取一个ribbon-eureka-demo服务的服务地址,可以通过LoadBalancerClient的choose方法来实现。
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/choose")
public Object chooseUrl() {
return loadBalancerClient.choose("eureka-client");
}
这样我们就可以获取到eureka-client这一个服务的信息,我们使用Postman测试结果如下:
{
"metadata": {
"management.port": "8081"
},
"secure": false,
"scheme": "http",
"host": "xxx.xxx.xx.xx",
"port": 8081,
"uri": "http://xxx.xxx.xx.xx:8081",
"serviceId": "EUREKA-CLIENT",
"instanceInfo": {
"instanceId": "eureka-client:xxx.xxx.xx.xx:8081",
"app": "EUREKA-CLIENT",
"appGroupName": null,
"ipAddr": "192.168.43.87",
"sid": "na",
"homePageUrl": "http://xxx.xxx.xx.xx:8081/",
"statusPageUrl": "http://xxx.xxx.xx.xx:8081/actuator/info",
"healthCheckUrl": "http://xxx.xxx.xx.xx:8081/actuator/health",
"secureHealthCheckUrl": null,
"vipAddress": "eureka-client",
"secureVipAddress": "eureka-client",
"countryId": 1,
"dataCenterInfo": {
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name": "MyOwn"
},
"hostName": "xxx.xxx.xx.xx",
"status": "UP",
"overriddenStatus": "UNKNOWN",
"leaseInfo": {
"renewalIntervalInSecs": 5,
"durationInSecs": 5,
"registrationTimestamp": 1616034670752,
"lastRenewalTimestamp": 1616035593582,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1616034670754
},
"isCoordinatingDiscoveryServer": false,
"metadata": {
"management.port": "8081"
},
"lastUpdatedTimestamp": 1616034670755,
"lastDirtyTimestamp": 1616034670568,
"actionType": "ADDED",
"asgName": null
},
"instanceId": "eureka-client:xxx.xxx.xx.xx:8081"
}
Eager Load
加入网络不好,往往会令Ribbon的第一次调用超时,我们可以将超时时间改长,也可以禁用超时,但是最新版的Spring Cloud中提供了饥饿加载的策略解决这一问题。要开启饥饿加载,往yml中加入如下配置:
ribbon:
eager-load:
enabled: true
clients: ribbon-native-demo
自定义
差不多也算是Ribbon入门了,我们接下来了解一下他最核心的负载均衡策略。Ribbon默认的负载策略是轮询,也就是和我们之前测试中的结果一样,每次调用都依次分配给集群中的服务,谁都能吃到这一口瓜。但是实际的线上高并发状态,并不是这么简单就能解决的,因此Ribbon还提供了如下的几个策略:
- BestAvailable:选择一个最小并发请求的Server,逐个考察,若被标记为错误则跳过。
- AvailabilityFilteringRule:检查Status里记录的各个Server的运行状态,过滤掉高并发的后端Server。
- ZoneAvoidanceRule:判断Zone的运行性能是否可用,剔除掉不可用的并且过滤掉连接数过多的。
- RandomRule:顾名思义,随机选择,可能有惊喜,可能。
- RoundRobinRule:即默认的轮询方式。
- RetryRule:重试机制策略,若选择Server不成功则一直尝试使用subRule的方式选择一个可用Server。
- WeightedResponseTimeRule:根据响应时间分配一个权重,时间越长权重越小,被选中的概率也越小。
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如此设置策略为随机。
重试
集群环境中,用多个节点提供服务,难免会遇到故障。由于Eureka是基于AP原则构建的,牺牲了数据的一致性,每个Eureka服务都会保存注册的服务信息,当心跳无法接受到时,仍会保存信息,这个时候Ribbon就可能拿到已经挂掉的服务信息,导致请求失败。我们可以为其配置重试机制。
首先在pom中添加依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
总结
总的来说,Ribbon是一款非常优秀的客户端负载均衡组件,但是在Spring Cloud里结合RestTemplate使用Ribbon还是相对麻烦的,所以接下来我们看看如何优雅的使用Feign去调用服务中的接口。
Feign
开始学习Feign。
了解
Feign是一个声明式的REST客户端,让接口的调用更加简单,它提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。
而Feign则会完全代理HTTP请求,只需要像调用方法一样调用它就可以完成服务请求及相关处理。
集成
首先依旧是添加依赖:
<!--Feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在启动来加上注解@EnableFeignClients
。如果接口定义和启动类不再同一个包名下,还需要指定扫描的包名@EnableFeignClients(basePackages = "com.ky.kevin")
。
然后定义一个Feign的客户端,以接口的形式存在。
package com.ky.kevin.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "eureka-client")
public interface UserRemoteClient {
@GetMapping("/test/hello")
String hello();
}
主要就是一个注解,value填写对应的eureka服务提供者。将提供的所有服务都单独抽出来,使用注解标记它的url即可。接下来回到控制层,之前写的大段大段测试语句可以删除了,创建的RestTemplate也可以删除了,写上下面的内容。
package com.ky.kevin.controller;
import com.ky.kevin.feign.UserRemoteClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/article")
public class HelloController {
@Autowired
private UserRemoteClient userRemoteClient;
@GetMapping("/callHello")
private String callHello() {
return userRemoteClient.hello();
}
}
接下来启动项目,多次访问路径,如果添加了输出信息可以发现访问成功而且自动完成了负载均衡。
如果初步看这些操作,可能会觉得多次一局,多嵌套了一层,到时候修改接口会复杂不少。但是这样调用接口确实非常优雅,相当于我们之前每次调用都需要写上服务的名称(一开始甚至要写主机号和端口号),现在我们把这一块内容抽出来作为接口的注解,极大的简化了调用,并且这样一来也更加方便我们的视觉上的审阅。
自定义
讲讲自定义的配置方式。
日志
好像都是老生常谈的内容了,还是同一个问题,遇到Bug怎么办,那就定义一个配置类,把请求信息输出。
package com.ky.kevin.configuration;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class FeignConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
接着回到FeignClient,将注解修改为@FeignClient(value = "eureka-client", configuration = FeignConfiguration.class)
,表示使用指定的配置类。最后在yml中写上日志级别的显示如下:
logging:
level:
com.ky.kevin.feign.UserRemoteClient: DEBUG
再次启动程序,调用接口我们可以看到DEBUG信息:
超时
修改连接超时时间和读取超时时间。
@Bean
public Request.Options options() {
return new Request.Options(5000, 10000);
}
压缩
开启压缩以有效节约网络资源,提升接口性能,我们可以配置GZIP来压缩数据,在yml中添加一下配置:
feign:
compression:
request:
enabled: true
mime-types:
- text/xml
- application/xml
- application/json
min-request-size: 2048
response:
enabled: true
总结
通过本节,我们已经对Feign有了一个初步的了解,通过Feign简化调用接口的方式,还可以和Eureka何Ribbon轻松集成,所以还是非常值得学习的,相信接下来的项目实战中我们还有很多的机会可以接触这一块内容。
大总结
本文一共学习了Ribbon和Feign两块知识。我们现在已经可以自己创建注册中心集群,创建服务提供者和消费者集群。使用Ribbon实现有效的负载均衡策略,最后再利用Feign优化接口调用。从一开始的迷茫,无从下手,到现在雾霾的驱散,前方的道路越来越清晰。虽然这部分的内容在实战当中肯定还是经不起考验的,但是毕竟毕竟我们也只是初步的一个了解,最终肯定是由实践来检验这些真理。
本来想试试Hystrix来保护高并发下的服务安全,但是目前我们手上的小项目好像并不会涉及到这方面的内容,所以放到最后有时间我们再回来,接下来的内容又是一块比较重要比较难的--Zuul网关,敬请期待。
2 comments
ribbon 已被标记为 deprecated
参照 https://spring.io/guides/gs/client-side-load-balancing/
考虑用 spring-cloud-loadbalancer 替换
参照 https://spring.io/guides/gs/spring-cloud-loadbalancer/
feign 只是改了个名字变成了 open-feign,实际使用区别不大
实际上相当多原本属于 netflix 的开源组件都正在被陆续替换
大佬大佬,学到了OωO