在 openFeign 未出现前,Spring 提供了 RestTemplate 作为远程服务调用的客户端,提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。由于文章内容会使用到 RestTemplate,所以这里就简单说下。
一讲到服务调用,我们肯定会联想到服务的路由与负载均衡,那么我们接下来就先介绍两种客户端的服务负载均衡组件:LoadBalancerClient 与 Ribbon
一、SpringCloud的客户端负载均衡:
1、客户端负载均衡:
负载均衡分为客户端负载均衡和服务端负载均衡,它们之间的区别在于:服务清单所在的位置。
我们通常说的负载均衡都是服务端的负载均衡,其中可以分为硬件的负载均衡和软件负载均衡:硬件的负载均衡就是在服务器节点之间安装用于负载均衡的设备,比如F5;软件负载均衡则是在服务器上安装一些具有负载均衡功能的模块或软件来完成请求分发的工作,比如nginx。服务端的负载均衡会在服务端维护一个服务清单,然后通过心跳检测来剔除故障节点以保证服务清单中的节点都正常可用。
客户端负载均衡指客户端都维护着自己要访问的服务端实例清单,而这些服务端清单来自服务注册中心。客户端负载均衡也需要心跳检测维护清单服务的健康性,只不过这个工作要和服务注册中心配合完成。
2、LoadBalancerClient:
LoadBalancerClient 是 SpringCloud 提供的一种负载均衡客户端,LoadBalancerClient 在初始化时会通过 Eureka Client 向 Eureka 服务端获取所有服务实例的注册信息并缓存在本地,并且每10秒向 EurekaClient 发送“ ping ”,来判断服务的可用性。如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或者重新拉取。最后,在得到服务注册列表信息后,ILoadBalancer 根据 IRule 的策略进行负载均衡(默认策略为轮询)。
当使用 LoadBalancerClient 进行远程调用的负载均衡时,LoadBalancerClient 先通过目标服务名在本地服务注册清单中获取服务提供方的某一个实例,比如订单服务需要访问商品服务,商品服务有3个节点,LoadBalancerClient 会通过 choose() 方法获取到3个节点中的一个服务,拿到服务的信息之后取出服务IP信息,就可以得到完整的想要访问的IP地址和接口,最后通过 RestTempate 访问商品服务。
2.1、springboot + LoadBalancerClient 负载均衡调用:
2.1.1、服务提供方代码:
//nacos注册中心的服务名:cloud-producer-server
//两数求和
@PostMapping ("getSum")
public String getSum(@RequestParam (value = "num1") Integer num1, @RequestParam (value = "num2") Integer num2)
{
return "两数求和结果=" + (num1 + num2);
}
2.1.2、服务消费方代码:
(1)指定服务,通过 LoadBalancerClient 自动获取某个服务实例与请求地址
@Component
public class LoadBalancerUtil
{
// 注入LoadBalancerClient
@Autowired
LoadBalancerClient loadBalancerClient;
/**
* 通过 LoadBalancer 获取提供服务的host与ip
*/
public String getService(String serviceId)
{
//获取实例服务中的某一个服务
ServiceInstance instance = loadBalancerClient.choose(serviceId);
//获取服务的ip地址和端口号
String host = instance.getHost();
int port = instance.getPort();
//格式化最终的访问地址
return String.format("http://%s:%s", host, port);
}
}
(2)通过 RestTemplate 请求远程服务地址并接收返回值:
@RestController
@RequestMapping (value = "api/invoke")
public class InvokeController
{
@Autowired
private LoadBalancerUtil loadBalancerUtil;
/**
* 使用 SpringCloud 的负载均衡策略组件 LoadBalancerClient 进行远程服务调用
*/
@GetMapping ("getByLoadBalancer")
public String getByLoadBalancer(Integer num1, Integer num2)
{
String hostAndIp = loadBalancerUtil.getService("cloud-producer-server");
//打印服务的请求地址与端口,方便测试负载功能
System.out.println(hostAndIp);
String url = hostAndIp + "/cloud-producer-server/getSum";
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("num1", num1);
params.add("num2", num2);
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.postForObject(url, params, String.class);
return result;
}
}
多次访问服务消费方的 api/invoke/getByLoadBalancer 接口,并且通过打印出来的 hostAndIp 信息,可以看出 LoadBalancerClient 是轮询调用服务提供方的,这也是 LoadBalancerClient 的默认负载均衡策略
2.2、LoadBalancerClient 原理:
3、ribbon:
Ribbon 负载组件的内部就是集成了 LoadBalancerClient 负载均衡客户端,所以 Ribbon 负载均衡的原理本质也跟上面介绍的 LoadBalancerClient 原理一致,负载均衡器 Ribbon 默认会通过 Eureka Client 向 Eureka 服务端的服务注册列表中获取服务的信息,并缓存一份在本地 JVM 中,根据缓存的服务注册列表信息,可以通过 LoadBalancerClient 来选择不同的服务实例,从而实现负载均衡。
基本用法就是注入一个 RestTemplate,并使用 @LoadBalance 注解标注 RestTemplate,从而使 RestTemplate 具备负载均衡的能力。当 Spring 容器启动时,使用 @LoadBalanced 注解修饰的 RestTemplate 会被添加拦截器 LoadBalancerInterceptor,拦截器会拦截 RestTemplate 发送的请求,转而执行 LoadBalancerInterceptor 中的 intercept() 方法,并在 intercept() 方法中使用 LoadBalancerClient 处理请求,从而达到负载均衡的目的。
那么 RestTemplate 添加 @LoadBalanced 注解后,为什么会被拦截呢?这是因为 LoadBalancerAutoConfiguration 类维护了一个被 @LoadBalanced 修饰的 RestTemplate 列表,在初始化过程中,通过调用 customizer.customize(restTemplate) 方法为 RestTemplate 添加了 LoadBalancerInterceptor 拦截器,该拦截器中的方法将远程服务调用的方法交给了 LoadBalancerClient 去处理,从而达到了负载均衡的目的。
3.1、springboot + Ribbon 负载均衡调用:
通过 Spring Cloud Ribbon 的封装,我们在微服务架构中使用客户端负载均衡非常简单,只需要两步:
- ① 服务提供者启动服务实例并注册到服务注册中心
- ② 服务消费者直接使用被 @LoadBalanced 注解修饰的 RestTemplate 来实现面向服务的接口调用
3.1.1、服务提供方代码:
//nacos注册中心的服务名:cloud-producer-server
//两数求和
@PostMapping ("getSum")
public String getSum(@RequestParam (value = "num1") Integer num1, @RequestParam (value = "num2") Integer num2)
{
return "两数求和结果=" + (num1 + num2);
}
3.1.2、服务消费方代码:
(1)使用 @LoadBalanced 注解修饰的 RestTemplate:
@LoadBalanced 注解用于开启负载均衡,标记 RestTemplate 使用 LoadBalancerClient 配置
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestConfig
{
/**
* 创建restTemplate对象。
* LoadBalanced注解表示赋予restTemplate使用Ribbon的负载均衡的能力(一定要加上注解,否则无法远程调用)
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
(2)通过 RestTemplate 请求远程服务地址并接收返回值
@RestController
@RequestMapping (value = "api/invoke")
public class InvokeController
{
@Autowired
private RestTemplate restTemplate;
/**
* 使用 RestTemplate 进行远程服务调用,并且使用 Ribbon 进行负载均衡
*/
@ApiOperation (value = "RestTemplate", notes = "使用RestTemplate进行远程服务调用,并使用Ribbon进行负载均衡")
@GetMapping ("getByRestTemplate")
public String getByRestTemplate(Integer num1, Integer num2)
{
//第一个cloud-producer-server代表在nacos注册中心中的服务名,第二个cloud-producer-server代表contextPath配置的项目路径
String url = "http://cloud-producer-server/cloud-producer-server/getSum";
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("num1", num1);
params.add("num2", num2);
//通过服务名的方式调用远程服务(非ip端口)
return restTemplate.postForObject(url, params, String.class);
}
}
默认情况下,Ribbon 也是使用轮询作为负载均衡策略,那么处理轮询策略,Ribbon 还有哪些负载均衡策略呢?
3.2、Ribbon 的七种负载均衡策略:
我们打开 com.netflix.loadbalancer.IRule 接口,该接口的实现类主要用于定义负载均衡策略,我们找到它所有的实现类,如下:
(1)随机策略 RandomRule:随机数选择服务列表中的服务节点Server,如果当前节点不可用,则进入下一轮随机策略,直到选到可用服务节点为止
(2)轮询策略 RoundRobinRule:按照接收的请求顺序,逐一分配到不同的后端服务器
(3)重试策略 RetryRule:在选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用 subRule 的方式选择一个可用的server;
(4)可用过滤策略 PredicateBaseRule:过滤掉连接失败 和 高并发连接 的服务节点,然后从健康的服务节点中以线性轮询的方式选出一个节点返回
(5)响应时间权重策略 WeightedRespinseTimeRule:根据服务器的响应时间分配一个权重weight,响应时间越长,weight越小,被选中的可能性越低。主要通过后台线程定期地从 status 里面读取平均响应时间,为每个 server 计算一个 weight
(6)并发量最小可用策略 BestAvailableRule:选择一个并发量最小的服务节点 server。ServerStats 的 activeRequestCount 属性记录了 server 的并发量,轮询所有的server,选择其中 activeRequestCount 最小的那个server,就是并发量最小的服务节点。该策略的优点是可以充分考虑每台服务节点的负载,把请求打到负载压力最小的服务节点上。但是缺点是需要轮询所有的服务节点,如果集群数量太大,那么就会比较耗时。
(7)区域权重策略 ZoneAvoidanceRule:综合判断 server 所在区域的性能 和 server 的可用性,使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate 用于过滤掉连接数过多的Server。
二、什么是openFeign:
微服务架构中,由于对服务粒度的拆分致使服务数量变多,而作为 Web 服务的调用端方,除了需要熟悉各种 Http 客户端,比如 okHttp、HttpClient 组件的使用,而且还要显式地序列化和反序列化请求和响应内容,从而导致出现很多样板代码,开发起来很痛苦。为了解决这个问题,Feign 诞生了,那么 Feign 是什么呢?
Feign 就是一个 Http 客户端的模板,目标是减少 HTTP API 的复杂性,希望能将 HTTP 远程服务调用做到像 RPC 一样易用。Feign 集成 RestTemplate、Ribbon 实现了客户端的负载均衡的 Http 调用,并对原调用方式进行了封装,使得开发者不必手动使用 RestTemplate 调用服务,而是声明一个接口,并在这个接口中标注一个注解即可完成服务调用,这样更加符合面向接口编程的宗旨,客户端在调用服务端时也不需要再关注请求的方式、地址以及是 forObject 还是 forEntity,结构更加明了,耦合也更低,简化了开发。但 Feign 已经停止迭代了,所以本篇文章我们也不过多的介绍,而在 Feign 的基础上,又衍生出了 openFeign,那么 openFeign 又是什么呢?
openFeign 在 Feign 的基础上支持了 SpringMVC 的注解,如 @RequestMapping 等。OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
总的就是,openFeign 作为微服务架构下服务间调用的解决方案,是一种声明式、模板化的 HTTP 的模板,使 HTTP 请求就像调用本地方法一样,通过 openFeign 可以替代基于 RestTemplate 的远程服务调用,并且默认集成了 Ribbon 进行负载均衡。
三、Springboot 整合 openFeign:
1、创建服务提供者 provider:
(1)项目配置:
# 服务在nacos中的服务名
spring.application.name = openFeign-provider
# nacos注册中心配置
nacos.url = 120.76.129.106:80
nacos.namespace = 856a40d7-6548-4494-bdb9-c44491865f63
spring.cloud.nacos.discovery.server-addr = ${nacos.url}
spring.cloud.nacos.discovery.namespace = ${nacos.namespace}
spring.cloud.nacos.discovery.register-enabled = true
2、创建服务消费者 consumer:
(1)引入 openFeign 相关依赖
<!-- 引入openFeign进行远程服务调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)开启 openFeign 功能:
在 Springboot 应用的主启动类上使用注解 @EnableFeignClients 开启 openFeign 功能,如下:
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* SpringBoot启动类:
* EnableFeignClients:启动OpenFeign客户端
* EnableDiscoveryClient:启动服务发现
*/
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan(basePackages = "com.eebbk.*.dao")
public class ConsumerApplication
{
public static void main(String[] args)
{
SpringApplication.run(ConsumerApplication.class, args);
}
}
(3)新建 FeignClient 接口:
新建一个 openFeign 接口,使用 @FeignClient 注解标注,如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
}
(4)新建 Controller 调试:
新建一个controller用来调试接口,直接调用openFeign的接口,如下:
@RestController
@RequestMapping("/openfeign")
public class OpenFeignController {
}
好了,至此一个openFeign的微服务就搭建好了,并未实现具体的功能,下面一点点实现。
3、openFeign 的传参:
开发中接口传参的方式有很多,但是在 openFeign 中的传参是有一定规则的,下面详细介绍四种常见的传参方式。
3.1、传递JSON数据:
provider 接口中 JSON 传参方法如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order2")
public Order createOrder2(@RequestBody Order order){
return order;
}
}
consumer消费者openFeign代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 参数默认是@RequestBody标注的,这里的@RequestBody可以不填
* 方法名称任意
*/
@PostMapping("/openfeign/provider/order2")
Order createOrder2(@RequestBody Order order);
}
注意:openFeign 默认的传参方式就是JSON传参(@RequestBody),因此定义接口的时候可以不用@RequestBody注解标注,不过为了规范,一般都填上。
3.2、POJO表单传参:
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/order1")
public Order createOrder1(Order order){
return order;
}
}
consumer消费者openFeign代码如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 如果通过POJO表单传参的,使用@SpringQueryMap标注
*/
@PostMapping("/openfeign/provider/order1")
Order createOrder1(@SpringQueryMap Order order);
}
3.3、URL中携带参数:
此种方式针对restful方式中的GET请求,也是比较常用请求方式。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@GetMapping("/test/{id}")
public String test(@PathVariable("id")Integer id){
return "accept one msg id="+id;
}
consumer消费者openFeign接口如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
@GetMapping("/openfeign/provider/test/{id}")
String get(@PathVariable("id")Integer id);
}
使用注解 @PathVariable 接收url中的占位符,这种方式很好理解。
3.4、普通表单参数:
此种方式传参不建议使用,但是也有很多开发在用。
provider服务提供者代码如下:
@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
@PostMapping("/test2")
public String test2(String id,String name){
return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}
}
consumer消费者openFeign接口传参如下:
@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
/**
* 必须要@RequestParam注解标注,且value属性必须填上参数名
* 方法参数名可以任意,但是@RequestParam注解中的value属性必须和provider中的参数名相同
*/
@PostMapping("/openfeign/provider/test2")
String test(@RequestParam("id") String arg1,@RequestParam("name") String arg2);
}
4、设置超时时间:
想要理解超时处理,先看一个例子:我将provider服务接口睡眠3秒钟,接口如下:
@PostMapping("/test2")
public String test2(String id,String name) throws InterruptedException {
Thread.sleep(3000);
return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}
此时,我们调用consumer的openFeign接口返回结果如下图的超时异常:
openFeign 其实是有默认的超时时间的,默认分别是连接超时时间 10秒、读超时时间 60秒,源码在 feign.Request.Options#Options() 这个方法中,如下图:
那么为什么我们只设置了睡眠3秒就报超时呢?其实 openFeign 集成了 Ribbon,Ribbon 的默认超时连接时间、读超时时间都是是1秒,源码在 org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute() 方法中,如下图:
源码大致意思:如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。理解了超时设置的原理,由之产生两种方案也是很明了了,如下:
- 设置openFeign的超时时间
- 设置Ribbon的超时时间
4.1、设置Ribbon的超时时间(不推荐)
ribbon:
# 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
ReadTimeout: 5000
# 指的是建立链接后从服务器读取可用资源所用的时间
ConectTimeout: 5000
4.2、设置Ribbon的超时时间
openFeign设置超时时间非常简单,只需要在配置文件中配置,如下:
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
default设置的是全局超时时间,对所有的openFeign接口服务都生效,但是正常的业务逻辑中可能有其实 openFeign 接口的调用需要单独配置一个超时时间,比如下面我们就单独给 serviceC 这个服务单独配置了一个超时时间,单个配置的超时时间将会覆盖全局配置:
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
## 为serviceC这个服务单独配置超时时间
serviceC:
connectTimeout: 30000
readTimeout: 30000
5、替换的 HTTP 客户端:
openFeign 默认使用的是 JDK 原生的 URLConnection 发送 HTTP 请求,没有连接池,但是对每个地址会保持一个长连接,即利用 HTTP 的 persistence connection。在生产环境中,通常不使用默认的 http client,通常有两种选择:使用 ApacheHttpClient 或者 OkHttp,两者各有千秋,下面我们演示下如何使用 ApacheHttpClient 替换原生的 http client
5.1、添加ApacheHttpClient依赖:
<!-- 使用 Apache HttpClient 替换 Feign原生httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
为什么要添加上面的依赖呢?从源码中不难看出,请看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration 这个类,代码如下:
上述红色框中的生成条件,其中的 @ConditionalOnClass(ApacheHttpClient.class),必须要有 ApacheHttpClient 这个类才会生效,并且 feign.httpclient.enabled 这个配置要设置为 true。
5.2、配置文件中开启:
在配置文件中要配置开启,代码如下:
feign:
client:
httpclient:
# 开启 Http Client
enabled: true
5.3、如何验证?
其实很简单,在 feign.SynchronousMethodHandler#executeAndDecode() 这个方法中可以清楚的看出调用哪个client,如下图:
上图中可以看到最终调用的是 ApacheHttpClient。
6、开启日志增强:
openFeign 虽然提供了日志增强功能,但默认是不显示任何日志的,不过开发者在调试阶段可以自己配置日志的级别。
openFeign 的日志级别如下:
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
- FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
配置起来也很简单,步骤如下:
6.1、配置类中配置日志级别
需要自定义一个配置类,在其中设置日志级别,如下:
6.2、yaml文件中设置接口日志级别:
logging:
level:
cn.myjszl.service: debug
这里的 cn.myjszl.service 是 openFeign 接口所在的包名,当然你也可以配置一个特定的openFeign接口。
6.3、效果演示
上述步骤将日志设置成了 FULL,此时发出请求,日志效果如下图:
日志中详细的打印出了请求头、请求体的内容。
7、通讯优化:
在讲如何优化之前先来看一下GZIP 压缩算法
7.1、GZIP压缩算法:
gzip是一种数据格式,采用deflate算法压缩数据;当GZIP算法压缩到一个纯文本数据时,效果是非常明显的,大约可以减少70%以上的数据大小。
网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快的好处不言而喻,除了节省流量,改善用户的浏览体验外,另一个潜在的好处是GZIP与搜索引擎的抓取工具有着更好的关系。例如 Google就可以通过直接读取GZIP文件来比普通手工抓取更快地检索网页。
GZIP压缩传输的原理如下图:
按照上图拆解出的步骤如下:
- 客户端向服务器请求头中带有:Accept-Encoding:gzip,deflate 字段,向服务器表示,客户端支持的压缩格式(gzip或者deflate),如果不发送该消息头,服务器是不会压缩的。
- 服务端在收到请求之后,如果发现请求头中含有 Accept-Encoding 字段,并且支持该类型的压缩,就对响应报文压缩之后返回给客户端,并且携带 Content-Encoding:gzip 消息头,表示响应报文是根据该格式压缩过的。
- 客户端接收到响应之后,先判断是否有 Content-Encoding 消息头,如果有,按该格式解压报文。否则按正常报文处理。
7.2、openFeign开启GZIP压缩:
openFeign支持请求/响应开启GZIP压缩,整体的流程如下图:
上图中涉及到GZIP传输的只有两块,分别是 Application client -> Application Service、 Application Service->Application client。
注意:openFeign支持的GZIP仅仅是在openFeign接口的请求和响应,即openFeign消费者调用服务提供者的接口。
openFeign开启GZIP步骤也是很简单,只需要在配置文件中开启如下配置:
feign:
## 开启压缩
compression:
request:
enabled: true
## 开启压缩的阈值,单位字节,默认2048,即是2k,这里为了演示效果设置成10字节
min-request-size: 10
mime-types: text/xml,application/xml,application/json
response:
enabled: true
上述配置完成之后,发出请求,可以清楚看到请求头中已经携带了GZIP压缩,如下图:
四、OpenFeign 的原理:
1、@FeignClient 如何根据接口生成实现(代理)类的?
2、生成的实现(代理)类是如何适配各种HTTP组件的?
3、生成的实现(代理)类如何实现HTTP请求应答序列化和反序列化的?
4、生成的实现(代理)类是如何注入到Spring容器中的?
参考文章:
openFeign夺命连环9问,这谁受得了?