Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于NetFlix Ribbon实现。在经过Spring Cloud封装后,可以快速结合Rest模板请求自动完成客户端的负载均衡调用。
Spring Cloud Ribbon是一个工具类框架,但是与服务注册中心,配置中心,Api网关需要独立部署有所区别,他几乎存在于每一个SpringCloud的微服务中,因为服务之间的调用,Api网关的请求转发等操作,实际上都是用过Ribbon来实现的。
接下来使用Ribbon实现一个简单的负载均衡的功能,这里需要使用到之前Eureka笔记中创建的两个项目。
Eureka学习笔记
然后创建一个新的Maven项目ribbon-native-demo,在项目中继承Ribbon,pom.xml添加如下依赖:
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbonartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbon-coreartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbon-loadbalancerartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>io.reactivexgroupId>
<artifactId>rxjavaartifactId>
<version>1.0.10version>
dependency>
接下来就是创建一个客户端来调用接口并实现负载均衡:
// 服务列表
List<Server> serverList = Lists.newArrayList(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>() {
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);
}
上述例子中使用HttpURLConnection,当然也可以直接使用RibbonClient进行请求,执行程序后输出如下:
从以上结果可以看到,负载均衡器还是起到作用的,两个服务都能接收到请求。
在上述例子中,我们单独使用了Ribbon
进行了负载均衡的调用,SpringCloud
在原来的Ribbon
上进行了一层封装,使得Ribbon
的使用更加简单。
首先尝试一下Get
的请求方式,创建一个微服务spring-rest-template
,并配置RestTemplate
:
@Configuration
public class BeanConfiguration {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
新建一个HouseController
,创建两个请求接口,一个通过@RequestParam
传递参数,返回一个对象,另一个通过@PathVariable
传递参数,返回字符串,代码如下:
@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
来调用我们刚刚定义的两个接口,代码如下:
@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);
}
除了 getForObject
,我们还可以使用 getForEntity
来获取数据,代码如下所示:
@GetMapping("/call/dataEntity")
public HouseInfo getData(@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
获取响应的内容。
接下来看看怎么使用 POST
方式调用接口。在 HouseController
中增加一个 save
方法用来接收 HouseInfo
数据,代码如下:
@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
System.out.println(houseInfo.getName());
return 1001L;
}
接着写调用代码,用 postForObject
来调用,代码如下:
@GetMapping("/call/save")
public Long add() {
HouseInfo houseInfo = new HouseInfo();
houseInfo.setCity("上海");
houseInfo.setRegion("虹口");
houseInfo.setName("×××");
Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
return id;
}
除了 get
和 post
对应的方法之外,RestTemplate
还提供了 put
、delete
等操作方法,还有一个比较实用的就是 exchange
方法。exchange
可以执行 get
、post
、put
、delete
这 4 种请求方式。
在 Spring Cloud
项目中集成 Ribbon
只需要在 pom.xml
中加入下面的依赖即可,其实也可以不用配置,因为 Eureka
中已经引用了 Ribbon
,代码如下:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate
可以结合 Eureka
来动态发现服务并进行负载均衡的调用。修改 RestTemplate
的配置,增加能够让 RestTemplate
具备负载均衡能力的注解 @LoadBalanced
。代码如下:
@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
新增一个拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务器地址,然后再去调用。
下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。代码如下:
public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
System.out.println("进入自定义的请求拦截器中" + serviceName);
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
拦截器设置好了之后,我们再定义一个注解,并复制@LoadBalanced
的代码,改个名称就可以了,代码如下:
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}
然后定义一个配置类,给 RestTemplate
注入拦截器,代码如下:
@Configuration
public class MyLoadBalancerAutoConfiguration {
@MyLoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
return new MyLoadBalancerInterceptor();
}
@Bean
public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
list.add(myLoad BalancerInterceptor());
restTemplate.setInterceptors(list);
}
}
};
}
}
维护一个 @MyLoadBalanced
的 RestTemplate
列表,在 SmartInitializingSingleton
中对 RestTemplate
进行拦截器设置。然后改造我们之前的 RestTemplate
配置,将 @LoadBalanced
改成我们自定义的 @MyLoadBalanced
,代码如下:
@Bean
//@LoadBalanced
@MyLoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
重启服务,访问服务中的接口就可以看到控制台的输出了,这证明在接口调用的时候会进入该拦截器,输出如下:
进入自定义的请求拦截器中 ribbon-eureka-demo
当你有一些特殊的需求,想通过 Ribbon
获取对应的服务信息时,可以使用 Load-Balancer Client
来获取,比如你想获取一个 ribbon-eureka-demo
服务的服务地址,可以通过 LoadBalancerClient
的 choose
方法来选择一个。
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping("/choose")
public Object chooseUrl() {
ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
return instance;
}
访问接口,可以看到返回的信息如下:
{
serviceId: "ribbon-eureka-demo",
server: {
host: "localhost",
port: 8081,
id: "localhost:8081",
zone: "UNKNOWN",
readyToServe: true,
alive: true,
hostPort: "localhost:8081",
metaInfo: {
serverGroup: null,
serviceIdForDiscovery: null, instanceId: "localhost:8081",
appName: null
}
},
secure: false, metadata: { }, host: "localhost", port: 8081,
uri: "http://localhost:8081"
}
Ribbon
的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client
的时间再加上请求接口的时间,就会导致第一次请求超时。
针对这种情况可以通过配置 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
中找到对应的代码,代码如下所示:
@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
}
在 return
这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了。
Ribbon
作为一款负载均衡框架,默认的负载均衡策略是轮询
,但是同时也提供了很多其他的策略,可以提供使用者根据不同的业务自行配置。
Ribbon
中实现的策略代码结构如下图所示:
ActiveRequestCount
最小的服务。circuit tripped
的后端 服务,并过滤掉那些高并发的后端 服务 或者使用一个AvailabilityPredicate
来包含过滤 服务 的逻辑。其实就是检查Status
里记录的各个服务的运行状态。ZoneAvoidancePredicate
和 AvailabilityPredicate
来判断是否选择某个服务,前一个判断判定一个Zone
的运行性能是否可用,剔除不可用的Zone
的所有服务,AvailabilityPredicate
用于过滤掉连接数过多的服务。index
,选择index
对应位置的服务。subRule
的方式选择一个可用的服务。Weight
(权重),响应时间越长,Weight
越小,被选中的可能性越低。 通过实现 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=net.biancheng.ribbon_eureka_demo.rule.MyRule
重启服务,访问调用了其他服务的接口,可以看到控制台的输出信息中已经有了我们自定义策略中输出的服务信息,并且每次都是调用第一个服务。这跟我们的逻辑是相匹配的。
# 禁用 Eureka
ribbon.eureka.enabled=false
当我们禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。
# 禁用 Eureka 后手动配置服务地址
ribbon-config-demo.ribbon.listOfServers=localhost:8081,localhost:8083
这个配置是针对具体服务的,前缀就是服务名称,配置完之后就可以和之前一样使用服务名称来调用接口了。
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500
通过代码的方式配置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 {
}
去掉配置文件中之前配置的策略配置,重启服务后调用,发现调用结果与MyRule之中的配置一样。
我们还可以通过如下方式对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发现请求不可达时,重新请求另外的服务。
最简单的重试方法就是利用Ribbon自带的重试机制策略,只需要指定某个服务的负载均衡策略为重试策略即可,配置如下:
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
我们还可以通过集成Spring Retry来进行重试操作。在pom.xml中添加Spring Retry依赖,如下:
<dependency>
<groupId>org.springframework.retrygroupId>
<artifactId>spring-retryartifactId>
dependency>
在配置文件中配置相关信息:
# 对当前实例的重试次数
ribbon.maxAutoRetries=1
# 切换实例的重试次数
ribbon.maxAutoRetriesNextServer=3
# 对所有操作请求都进行重试
ribbon.okToRetryOnAllOperations=true
# 对Http响应码进行重试
ribbon.retryableStatusCodes=500,404,502
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于NetFlix Ribbon实现。在经过Spring Cloud封装后,可以快速结合Rest模板请求自动完成客户端的负载均衡调用。
Spring Cloud Ribbon是一个工具类框架,但是与服务注册中心,配置中心,Api网关需要独立部署有所区别,他几乎存在于每一个SpringCloud的微服务中,因为服务之间的调用,Api网关的请求转发等操作,实际上都是用过Ribbon来实现的。
接下来使用Ribbon实现一个简单的负载均衡的功能,这里需要使用到之前Eureka笔记中创建的两个项目。
Eureka学习笔记
然后创建一个新的Maven项目ribbon-native-demo,在项目中继承Ribbon,pom.xml添加如下依赖:
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbonartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbon-coreartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbon-loadbalancerartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>io.reactivexgroupId>
<artifactId>rxjavaartifactId>
<version>1.0.10version>
dependency>
接下来就是创建一个客户端来调用接口并实现负载均衡:
// 服务列表
List<Server> serverList = Lists.newArrayList(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>() {
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);
}
上述例子中使用HttpURLConnection,当然也可以直接使用RibbonClient进行请求,执行程序后输出如下:
从以上结果可以看到,负载均衡器还是起到作用的,两个服务都能接收到请求。
在上述例子中,我们单独使用了Ribbon
进行了负载均衡的调用,SpringCloud
在原来的Ribbon
上进行了一层封装,使得Ribbon
的使用更加简单。
首先尝试一下Get
的请求方式,创建一个微服务spring-rest-template
,并配置RestTemplate
:
@Configuration
public class BeanConfiguration {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
新建一个HouseController
,创建两个请求接口,一个通过@RequestParam
传递参数,返回一个对象,另一个通过@PathVariable
传递参数,返回字符串,代码如下:
@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
来调用我们刚刚定义的两个接口,代码如下:
@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);
}
除了 getForObject
,我们还可以使用 getForEntity
来获取数据,代码如下所示:
@GetMapping("/call/dataEntity")
public HouseInfo getData(@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
获取响应的内容。
接下来看看怎么使用 POST
方式调用接口。在 HouseController
中增加一个 save
方法用来接收 HouseInfo
数据,代码如下:
@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
System.out.println(houseInfo.getName());
return 1001L;
}
接着写调用代码,用 postForObject
来调用,代码如下:
@GetMapping("/call/save")
public Long add() {
HouseInfo houseInfo = new HouseInfo();
houseInfo.setCity("上海");
houseInfo.setRegion("虹口");
houseInfo.setName("×××");
Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
return id;
}
除了 get
和 post
对应的方法之外,RestTemplate
还提供了 put
、delete
等操作方法,还有一个比较实用的就是 exchange
方法。exchange
可以执行 get
、post
、put
、delete
这 4 种请求方式。
在 Spring Cloud
项目中集成 Ribbon
只需要在 pom.xml
中加入下面的依赖即可,其实也可以不用配置,因为 Eureka
中已经引用了 Ribbon
,代码如下:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate
可以结合 Eureka
来动态发现服务并进行负载均衡的调用。修改 RestTemplate
的配置,增加能够让 RestTemplate
具备负载均衡能力的注解 @LoadBalanced
。代码如下:
@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
新增一个拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务器地址,然后再去调用。
下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。代码如下:
public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
System.out.println("进入自定义的请求拦截器中" + serviceName);
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
拦截器设置好了之后,我们再定义一个注解,并复制@LoadBalanced
的代码,改个名称就可以了,代码如下:
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}
然后定义一个配置类,给 RestTemplate
注入拦截器,代码如下:
@Configuration
public class MyLoadBalancerAutoConfiguration {
@MyLoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
return new MyLoadBalancerInterceptor();
}
@Bean
public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
list.add(myLoad BalancerInterceptor());
restTemplate.setInterceptors(list);
}
}
};
}
}
维护一个 @MyLoadBalanced
的 RestTemplate
列表,在 SmartInitializingSingleton
中对 RestTemplate
进行拦截器设置。然后改造我们之前的 RestTemplate
配置,将 @LoadBalanced
改成我们自定义的 @MyLoadBalanced
,代码如下:
@Bean
//@LoadBalanced
@MyLoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
重启服务,访问服务中的接口就可以看到控制台的输出了,这证明在接口调用的时候会进入该拦截器,输出如下:
进入自定义的请求拦截器中 ribbon-eureka-demo
当你有一些特殊的需求,想通过 Ribbon
获取对应的服务信息时,可以使用 Load-Balancer Client
来获取,比如你想获取一个 ribbon-eureka-demo
服务的服务地址,可以通过 LoadBalancerClient
的 choose
方法来选择一个。
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping("/choose")
public Object chooseUrl() {
ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
return instance;
}
访问接口,可以看到返回的信息如下:
{
serviceId: "ribbon-eureka-demo",
server: {
host: "localhost",
port: 8081,
id: "localhost:8081",
zone: "UNKNOWN",
readyToServe: true,
alive: true,
hostPort: "localhost:8081",
metaInfo: {
serverGroup: null,
serviceIdForDiscovery: null, instanceId: "localhost:8081",
appName: null
}
},
secure: false, metadata: { }, host: "localhost", port: 8081,
uri: "http://localhost:8081"
}
Ribbon
的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client
的时间再加上请求接口的时间,就会导致第一次请求超时。
针对这种情况可以通过配置 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
中找到对应的代码,代码如下所示:
@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
}
在 return
这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了。
Ribbon
作为一款负载均衡框架,默认的负载均衡策略是轮询
,但是同时也提供了很多其他的策略,可以提供使用者根据不同的业务自行配置。
Ribbon
中实现的策略代码结构如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dr9Eijdy-1606194193364)(SpringCloud.assets/5-1ZR1155050B0.png)]
ActiveRequestCount
最小的服务。circuit tripped
的后端 服务,并过滤掉那些高并发的后端 服务 或者使用一个AvailabilityPredicate
来包含过滤 服务 的逻辑。其实就是检查Status
里记录的各个服务的运行状态。ZoneAvoidancePredicate
和 AvailabilityPredicate
来判断是否选择某个服务,前一个判断判定一个Zone
的运行性能是否可用,剔除不可用的Zone
的所有服务,AvailabilityPredicate
用于过滤掉连接数过多的服务。index
,选择index
对应位置的服务。subRule
的方式选择一个可用的服务。Weight
(权重),响应时间越长,Weight
越小,被选中的可能性越低。 通过实现 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=net.biancheng.ribbon_eureka_demo.rule.MyRule
重启服务,访问调用了其他服务的接口,可以看到控制台的输出信息中已经有了我们自定义策略中输出的服务信息,并且每次都是调用第一个服务。这跟我们的逻辑是相匹配的。
# 禁用 Eureka
ribbon.eureka.enabled=false
当我们禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。
# 禁用 Eureka 后手动配置服务地址
ribbon-config-demo.ribbon.listOfServers=localhost:8081,localhost:8083
这个配置是针对具体服务的,前缀就是服务名称,配置完之后就可以和之前一样使用服务名称来调用接口了。
# 请求连接的超时时间
ribbon.ConnectTimeout=2000
# 请求处理的超时时间
ribbon.ReadTimeout=5000
# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
# 最大连接数
ribbon.MaxTotalConnections=500
# 每个host最大连接数
ribbon.MaxConnectionsPerHost=500
通过代码的方式配置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 {
}
去掉配置文件中之前配置的策略配置,重启服务后调用,发现调用结果与MyRule之中的配置一样。
我们还可以通过如下方式对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发现请求不可达时,重新请求另外的服务。
最简单的重试方法就是利用Ribbon自带的重试机制策略,只需要指定某个服务的负载均衡策略为重试策略即可,配置如下:
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
我们还可以通过集成Spring Retry来进行重试操作。在pom.xml中添加Spring Retry依赖,如下:
<dependency>
<groupId>org.springframework.retrygroupId>
<artifactId>spring-retryartifactId>
dependency>
在配置文件中配置相关信息:
# 对当前实例的重试次数
ribbon.maxAutoRetries=1
# 切换实例的重试次数
ribbon.maxAutoRetriesNextServer=3
# 对所有操作请求都进行重试
ribbon.okToRetryOnAllOperations=true
# 对Http响应码进行重试
ribbon.retryableStatusCodes=500,404,502