Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(下)
目录
博主整理的SpringCloud系列目录:>>戳这里<<
内容关联篇(建议先看):
Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(上)
一、Spring Cloud 中使用 Ribbon
在上篇,我们介绍了 Ribbon 组件的作用,以及 Ribbon 中的基本组件,如 ILoadBalancer、IPing、IRule、ServerList、ServerListUpdater。
本篇呢,将介绍如何在 Spring Cloud 中集成使用 Ribbon,结合 Eureka,实现客户端的负载均衡。
前面我们使用的 RestTemplate(被@LoadBalanced修饰),还有后面将介绍的 Feign,都已经拥有了负载均衡功能。这里将以 RestTemplate 为基础,介绍 Eureka 中的 Ribbon 配置。
这里再挖一个坑,后面博主还会讲讲如何在不使用 Eureka 支持的情况下,利用 Ribbon 各组件来保障集群服务的高可用性(主要是针对传统 Web 项目微服务化的支持)。
1.1、本例架构图
本例将会运行一个Eureka服务器实例、两个服务提供者实例、一个服务调用者实例,然后服务调用者请求服务。
-
cloud-ribbon-server
新建Eureka服务端项目,命名为
cloud-ribbon-server
,端口为8761
,Gitee上代码目录为Spring-Cloud-Study/02/ribbon02/cloud-ribbon-server
-
cloud-ribbon-provider
新建Eureka服务提供者项目,命名为
cloud-ribbon-provider
,Gitee上代码目录为Spring-Cloud-Study/02/ribbon02/cloud-ribbon-provider
该项目主要进行一下工作:- 在控制器中发布一个 REST 服务,地址为
/user/{userId}
,请求后返回 UserInfo 实例(包含服务端口信息)。 - 服务提供者需启动两次,分别发布
8081
和8082
两个端口。
- 在控制器中发布一个 REST 服务,地址为
-
cloud-ribbon-invoker
新建Eureka服务调用者项目,命名为
cloud-ribbon-invoker
,端口为9000
,Gitee上代码目录为Spring-Cloud-Study/02/ribbon02/cloud-ribbon-invoker
。本例的负载均衡配置主要针对服务调用者。
由于博主前面的文章中已经讲解过如何快速搭建一个 Eureka 服务,服务提供者也只是发布一个简单的 REST 服务,这里就不再赘述了,下面主要讲讲服务调用者,Ribbon 的使用。
1.2、使用代码配置 Ribbon
上篇讲述了负载规则 IRule 以及 Ping 机制,在 Spring Cloud 中,可将自定义的负载规则以及 Ping 类放到服务调用者中查看效果。新建自定义的 IRule 和 IPing,代码如下:
package com.swotxu.ribboninvoker.ribbon;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* @Date: 2020/9/19 19:31
* @Author: swotXu
*/
@Slf4j
public class MyRule implements IRule {
private ILoadBalancer lb;
@Override
public Server choose(Object o) {
List<Server> servers = lb.getAllServers();
log.info("这是自定义服务器规则类,输出服务器信息:");
for (Server s : servers) {
log.info("-> {}", s.getHostPort());
}
log.info("最后选择的服务器为:");
return servers.get(0);
}
@Override
public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
this.lb = iLoadBalancer;
}
@Override
public ILoadBalancer getLoadBalancer() {
return lb;
}
}
package com.swotxu.ribboninvoker.ribbon;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
/**
* @Date: 2020/9/19 19:30
* @Author: swotXu
*/
@Slf4j
public class MyPing implements IPing {
@Override
public boolean isAlive(Server server) {
log.info("这是自定义Ping实现类:{}", server.getHostPort());
return true;
}
}
根据两个自定义 IRule 和 IPing 类可知,服务选择规则只返回集合中的第一个实例,IPing 的实现仅是输出日志信息。下面新建配置类:
package com.swotxu.ribboninvoker.config;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.swotxu.ribboninvoker.ribbon.MyPing;
import com.swotxu.ribboninvoker.ribbon.MyRule;
import org.springframework.context.annotation.Bean;
/**
* @Date: 2020/9/19 19:35
* @Author: swotXu
*/
public class MyConfig {
@Bean
public IRule getRule(){
return new MyRule();
}
@Bean
public IPing getPing(){
return new MyPing();
}
}
package com.swotxu.ribboninvoker.config;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
/**
* 设置 cloud-ribbon-provider 服务的 Ribbon 规则
*
* @Date: 2020/9/19 19:38
* @Author: swotXu
*/
@RibbonClient(name = "cloud-ribbon-provider", configuration = MyConfig.class)
public class CloudProviderConfig {}
CloudProviderConfig
配置类使用@RibbonClient
注解,配置了 RibbonClient 的名称为 cloud-ribbon-provider,对应的配置类为 MyConfig,也就是名称为 cloud-ribbon-provider 的客户端将使用 MyRule 和 MyPing 两个类。
下面,我们编写一个对外的服务,服务中通过 RestTemplate 调用服务提供者。
package com.swotxu.ribboninvoker;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @Date: 2020/9/19 19:09
* @Author: swotXu
*/
@Slf4j
@Configuration
@RestController
public class lnvokerController {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
@RequestMapping(value = "/router", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String router(){
RestTemplate restTemplate = getRestTemplate();
String json = restTemplate.getForObject("http://cloud-ribbon-provider/user/1", String.class);
log.info("result: {}", json);
return json;
}
}
关于 RestTemplate 的原理,将在后面讲述。进行以下操作,查看本例效果:
- 启动一个 Eureka 服务器(cloud-ribbon-server)
- 启动两个 Eureka 服务提供者(cloud-ribbon-provider)端口为8081和8082
- 启动一个 Eureka 服务调用者(cloud-ribbon-invoker)
- 打开浏览器访问
http://127.0.0.1:9000/router
,可以看到返回的JSON字符串,不管刷新多少次,最终都只会访问其中一个端口。
1.3、使用配置文件设置 Ribbon
前面我们使用代码配置类的方式,来设置使用 Ribbon。同样,我们可以通过配置文件的方式使用,在 application.yml 中添加以下配置:
cloud-ribbon-provider:
ribbon:
NFLoadBalancerRuleClassName: com.swotxu.ribboninvoker.ribbon.MyRule
NFLoadBalancerPingClassName: com.swotxu.ribboninvoker.ribbon.MyPing
NFLoadBalancerPinglnterval: 2
listOfServers: http://localhost:8081/,http://localhost:8082/
如上配置,以相同的方式运行此例子,可以看到同样的效果。
1.4、Spring 使用 Ribbon 的 API
Spring Cloud 对 Ribbon 进行封装,例如像负载客户端、负载均衡器等,我们可以直接使用 Spring 的 LoadBalancerClient 来处理请求及服务选择。如下
// 代码位置:02/ribbon02/cloud-ribbon-invoker/src/main/java/com/swotxu/ribboninvoker/lnvokerController.java
@Autowired
private LoadBalancerClient loadBalanced;
@RequestMapping(value = "/uselb", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ServiceInstance uselb(){
// 查找服务器实例
ServiceInstance si = loadBalanced.choose(SERVICE_NAME_PROVIDER);
log.info("ServiceInstance: {}", si);
return si;
}
除了使用 Spring 封装的负载客户端外,还可以直接使用 Ribbon 的 API,直接获取 Spring Cloud 默认环境中各个 Ribbon 的实现类,代码如下:
// 代码位置:02/ribbon02/cloud-ribbon-invoker/src/main/java/com/swotxu/ribboninvoker/lnvokerController.java
@Autowired
private SpringClientFactory factory;
@RequestMapping(value = "/usefactory", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String usefactory(){
log.info("========== 输出 default 配置 ==========");
printlb(factory.getLoadBalancer("default"));
log.info("========== 输出 {} 配置 ==========", SERVICE_NAME_PROVIDER);
printlb(factory.getLoadBalancer(SERVICE_NAME_PROVIDER));
return "OK!";
}
private void printlb(ILoadBalancer lb){
ZoneAwareLoadBalancer zalb = (ZoneAwareLoadBalancer) lb;
log.info("ILoadBalancer: {}", lb.getClass().getName());
log.info("IRule: {}", zalb.getRule().getClass().getName());
log.info("IPing: {}", zalb.getPing().getClass().getName());
log.info("PingInterval: {}", zalb.getPingInterval());
log.info("ClientConfig: {}", zalb.getClientConfig().getClass().getName());
log.info("ServerListFilter: {}", zalb.getFilter().getClass().getName());
log.info("ServerListImpl: {}", zalb.getServerListImpl().getClass().getName());
log.info("ServerListUpdater: {}", zalb.getServerListUpdater().getClass().getName());
}
在浏览器中访问地址 http://localhost:9000/usefactory
,可看到控制台输出如下:
========== 输出 default 配置 ==========
ILoadBalancer: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: com.netflix.loadbalancer.ZoneAvoidanceRule
IPing: com.netflix.niws.loadbalancer.NIWSDiscoveryPing
PingInterval: 30
ClientConfig: com.netflix.client.config.DefaultClientConfigImpl
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ServerListImpl: org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
ServerListUpdater: com.netflix.loadbalancer.PollingServerListUpdater
========== 输出 cloud-ribbon-provider 配置 ==========
ILoadBalancer: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: com.swotxu.ribboninvoker.ribbon.MyRule
IPing: com.swotxu.ribboninvoker.ribbon.MyPing
PingInterval: 30
ClientConfig: com.netflix.client.config.DefaultClientConfigImpl
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ServerListImpl: org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
ServerListUpdater: com.netflix.loadbalancer.PollingServerListUpdater
根据输出可知, cloud-ribbon-provider 客户端使用的负载规则类以及 Ping 类,是我们自定义的实现类。
下面,我们讲讲 RestTemplate 进行负载均衡的原理。
二、RestTemplate 负载均衡
2.1、@LoadBalanced 注解概述
RestTemplate 本是 spring-web 项目中的一个 REST 客户端,它遵 REST 的设计原则,提供简单的 API 让我们去调用 HTTP 服务。RestTemplate 本身不具有负载均衡的功能,该类也与 Spring Cloud 没有关系,但为何加入 @LoadBalanced 注解后,一个 RestTemplate 实例就具有负载均衡的功能了呢?
实际上这要得益于 RestTemplate 的拦截器功能。
在 Spring Cloud 中,使用 @LoadBalanced 修饰的 RestTemplate,在 Spring 容器启动时,会为这些被修饰过的 RestTemplate 添加拦截器 ,拦截器中使用了 LoadBalancerClient 来处理请求,LoadBalancerClient 本来就是 Spring 封装的负载均衡客户端,通过这样间接处理,使得 RestTemplate 拥有了负载均衡的功能。
下面我们将模仿拦截器机制,实现一个简单的 RestTemplate,以便让大家更了解 @LoadBalanced 以及 RestTemplate 的原理。
此案例需新建一个 Spring Boot 项目,仅依赖了spring-boot-starter-web
模块.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.2、编写自定义注解及拦截器
先模仿 @LoadBalanced 注解,编写一个自定义注解,如下
package com.swotxu.rttest.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 模仿 @LoadBalanced 自定义负载均衡注解
*
* @Date: 2020/9/22 22:55
* @Author: swotXu
*/
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyLoadBalanced {
}
注意,这里使用了@Qualifier
限定注解。下面编写自定义拦截器
package com.swotxu.rttest.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
/**
* 自定义拦截器,将原始 HttpRequest 替换为自定义的 MyHttpRequest
*
* @Date: 2020/9/22 23:00
* @Author: swotXu
*/
@Slf4j
public class MyInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
log.info("===== 进入自定义拦截器实现 =====");
log.info(" oldUri: {}", httpRequest.getURI());
MyHttpRequest myHttpRequest = new MyHttpRequest(httpRequest);
log.info(" newUri: {}", myHttpRequest.getURI());
return clientHttpRequestExecution.execute(myHttpRequest, bytes);
}
}
在拦截器中,将原始 HttpRequest 替换为自定义的 MyHttpRequest。MyHttpRequest 代码如下:
package com.swotxu.rttest.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import java.net.URI;
/**
* HttpRequest 封装类
*
* @Date: 2020/9/22 23:01
* @Author: swotXu
*/
@Slf4j
public class MyHttpRequest implements HttpRequest {
private HttpRequest sourceRequest;
public MyHttpRequest(HttpRequest httpRequest) {
this.sourceRequest = httpRequest;
}
@Override
public HttpMethod getMethod() {
return sourceRequest.getMethod();
}
@Override
public String getMethodValue() {
return sourceRequest.getMethodValue();
}
@Override
public HttpHeaders getHeaders() {
return sourceRequest.getHeaders();
}
/**
* 转换 URI
* @return
*/
@Override
public URI getURI() {
try {
return new URI("http://localhost:8080/hello");
} catch (Exception e) {
log.warn("URI地址转换失败!", e);
}
return sourceRequest.getURI();
}
}
在 MyHttpRequest 类中,会将原来请求的 URI 进行改写,只要使用了这个对象,所有的请求都会被转发到 http://localhost:8080/hello
这个地址。
Spring Cloud 在对 RestTemplate 进行拦截时也做了同样的事情,只不过b并没有像我们这个固定了 URI,而是对“源请求”进行了更加灵活的处理。
下面,我们来使用自定义注解及拦截器
2.3、使用自定义注解及拦截器
编写一个 Spring 的配置类,在初始化的 Bean 中为容器中的 RestTemplate 实例设置自定义拦截器,如下:
package com.swotxu.rttest.config;
import com.swotxu.rttest.annotation.MyLoadBalanced;
import com.swotxu.rttest.interceptor.MyInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @Date: 2020/9/22 23:09
* @Author: swotXu
*/
@Slf4j
@Configuration
public class MyAutoConfiguration {
@Autowired(required = false)
@MyLoadBalanced
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer(){
log.info("===== 容器初始化注入 =====");
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate myRt : restTemplates) {
// 获取 RestTemplate 原来所有的拦截器
List<ClientHttpRequestInterceptor> list = new ArrayList(myRt.getInterceptors());
// 增加自定义拦截器
list.add(new MyInterceptor());
// 重新设置到 RestTemplate
myRt.setInterceptors(list);
}
}
};
}
}
在配置类中定义了 RestTemplate 集合,并且使用 @MyLoadBalanced
以及 @Autowired
注解修饰,@MyLoadBalanced
中含有 @Qualifier
注解。简单来说,就是在 Spring 容器中使用了 @MyLoadBalanced
修饰的 RestTemplate 实例,将会被加入配置类的 RestTemplate 集合中。
在容器初始化时,Spring 会向容器中注入 SmartInitializingSingleton 实例,该 Bean 在初始化完成后,会遍历 RestTemplate 集合并为它们设置“自定义拦截器”。
下面我们在控制器中使用@MyLoadBalanced
。
2.4、在控制器中使用RestTemplate
package com.swotxu.rttest.controller;
import com.swotxu.rttest.annotation.MyLoadBalanced;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @Date: 2020/9/22 23:18
* @Author: swotXu
*/
@Slf4j
@RestController
@Configuration
public class InvokerController {
@Bean
@MyLoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
/**
* 浏览器访问此请求
*
* @return
*/
@RequestMapping(value = "/router", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String router(){
RestTemplate restTemplate = getRestTemplate();
String json = restTemplate.getForObject("http://my-server/hello", String.class);
log.info("result: {}", json);
return json;
}
/**
* 最终会转发到这个服务
* @return
*/
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello(){
return "Hello World";
}
}
上述代码中,我们将/router
的请求全部转到/hello
服务上,我们实现的注解和 Spring 提供的 @LoadBalanced 注解使用一致。
打开浏览器,访问http://localhost:8080/router
,可以看到实际上调用了 hello 的服务。
Spring Cloud 对 RestTemplate 的拦截实现更加复杂,并且在拦截器中使用 LoadBalancerClient 来实现请求的负载均衡功能。
三、项目下载
1、项目完整结构图
2、源码下载
码云Gitee仓库地址:https://gitee.com/swotxu/Spring-Cloud-Study.git
>>戳这里<<
项目路径:Spring-Cloud-Study/02/ribbon02
这篇,我们讲了在 Spring 中使用 Ribbon 负载均衡,同时,我们也深入介绍了 @LoadBalanced 注解如何使得 RestTemplate 具备负载均衡功能的。
下篇,我们将讲解 Spring Cloud 全家桶中的 Feign
别忘了点赞关注收藏~
上一篇: 1. 蓝桥杯—入门训练
下一篇: jQuery中的100个技巧汇总