服务通信与调用-Feign
1. 通过Feign进行远程调用的分析
先来看一下之前的调用方式
Eureka:http://ip:port/path
Ribbon:http://serviceName/path
1.1. Feign的内部调用方式
-
引入Feign依赖的同时也引入两个组件
- Ribbon:利用负载均衡器选定目标机器
- Hystrix:根据熔断的开启状态,决定是否发起此次调用
-
动态代理
Feign是通过一个代理接口进行远程调用,这一步就是为了构造接口的动态代理对象,用来代理远程服务的真实调用,这样就能像调用本地方法一样调用HTTP请求,不需要像Ribbon或Eureka那样在方法调用的地方提供服务名。在Feign中动态代理是通过Feign.build返回的构造器来装配相关参数,这里就是应用的Builder设计模式。
-
Contract协议
Feign有着自己的一套协议的规范,Contract协议会构造复杂的元数据对象MethodMetadata。
1.2. Feign发起调用的过程
- 拦截器:是spring处理网络请求的经典方案,Feign也沿用了这个做法,通过一系列的拦截器多Request和Response对象进行装饰,构造请求头,装饰完毕后就正式发起调用
- 发起请求
- 重试:Feign这里借助Ribbon的配置重试机制实现重试操作,可以指定对当前服务节点发起重试,也可用让Feign换一个服务节点重试
- 降级:Feign接口在声明的时候指定Hystrix的降级策略实现类,如果达到了Hystrix的超市判定,或者得到异常情况,将执行指定的降级逻辑
2. Feign远程调用实例
添加POM依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
实现启动类
package com.michael.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignConsumerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FeignConsumerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建调用的Service接口引用实现
package com.michael.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
//eureka-client是服务提供者的servie-name
//这个注解的意思是IService这个接口的抵用都发到eureka-client这个服务的提供者上
@FeignClient("eureka-client")
public interface IService {
@GetMapping("/sayhello")
public String sayHello();
}
controller的实现
package com.michael.springcloud.controller;
import com.michael.springcloud.service.IService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeignController {
@Autowired
private IService service;
@GetMapping("/sayhi")
public String sayHi(){
return service.sayHello();
}
}
创建配置properties
spring.application.name=feign-consumer
server.port=20081
eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/
3. 理想的Feign风格的项目结构
Feign的调用都是基于HTTP协议方案的
3.1. 抽取一个公共接口层
在feign目录下创建一个feign-client-intf
POM里仅保持最低限度的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
建立接口层
package com.michael.springcloud.service;
import com.michael.springcloud.pojo.PortInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
//这里不能再使用eureka-client了,要使用自己的服务提供方
@FeignClient("feign-client")
public interface IService {
@GetMapping("/sayhello")
String sayHello();
@PostMapping("/sayhello")
PortInfo sayPortInfo(@RequestBody PortInfo portInfo);
}
pojo实体对象
package com.michael.springcloud.pojo;
import lombok.Data;
@Data
public class PortInfo {
private String name;
private String port;
}
3.2. 新建服务提供者
在feign目录中创建项目feign-client
POM依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.michael</groupId>
<artifactId>feign-client-intf</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
创建启动类
package com.michael.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class FeignClientApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FeignClientApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
在controller里实现feign-client-intf的IService
package com.michael.springcloud.controller;
import com.michael.springcloud.pojo.PortInfo;
import com.michael.springcloud.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class FeignController implements IService {
@Value("${server.port}")
private String port;
@GetMapping("/sayhello")
public String sayHello(){
return "my port is "+port;
}
@PostMapping("/sayhello")
public PortInfo sayPortInfo(@RequestBody PortInfo portInfo){
log.info("you are "+portInfo.getName());
portInfo.setPort(port);
return portInfo;
}
}
配置properties
spring.application.name=feign-client
server.port=20082
eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/
3.3. 改进版的消费者
创建feign-consumer-advanced
设置POM文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-learn</artifactId>
<groupId>com.michael</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>feign-consumer-advanced</artifactId>
<name>feign-consumer-advanced</name>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.michael</groupId>
<artifactId>feign-client-intf</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
创建启动类
package com.michael.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
//这个地方要注意IService所在的包路径,默认是扫com.michael.springcloud
//如果接口不在同一个包下,就需要把包扫进来
//@EnableFeignClients(basePackages"com.michael.*")
@EnableFeignClients
public class FeignConsumerAdvApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FeignConsumerAdvApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
controller实现
package com.michael.springcloud.controller;
import com.michael.springcloud.pojo.PortInfo;
import com.michael.springcloud.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class FeignController {
@Autowired
private IService service;
@GetMapping("/sayhi")
public String sayHi(){
return service.sayHello();
}
@PostMapping("/sayhi")
public PortInfo sayHello(@RequestBody PortInfo portInfo){
return service.sayPortInfo(portInfo);
}
}
properties配置
spring.application.name=feign-consumer-advanced
server.port=20083
eureka.client.serviceUrl.defaultZone=http://localhost:10080/eureka/
# 配置允许注解的重载
spring.main.allow-bean-definition-overriding=true
4. 服务调用超时重试机制
# Feign接口的超时重试机制由Ribbon提供
feign-service-provider.ribbon.OkToRetryOnAllOperations=true
feign-service-provider.ribbon.ConnectTimeout=1000
feign-service-provider.ribbon.ReadTimeout=2000
feign-service-provider.ribbon.MaxAutoRetries=2
feign-service-provider.ribbon.MaxAutoRetriesNextServer=2
以上的参数设置都是对feign-service-provider这个微服务的超时重试策略
- OkToRetryOnAllOperations:这个是指定了什么HTTP Method可以进行retry,这里设置的true的意思是无论GET还是POST都可以进行重试,一般来讲,在实际环境中往往只是GET请求才会允许重试,如果接口实现了幂等性后就可以进行重试了
- ConnectTimeout:超时判定的第一个参数,单位是ms,创建会话的连接时间,注意这个不是服务的响应时间,而是本机和服务建立的一个Connection花费的时间,如果连接超时则直接进行重试
- ReadTimeout:超时判断的第二个参数,服务响应时间。当连接建立后,如果对方服务没有在规定时间内返回,则直接进行重试
- MaxAutoRetries:这里配置重试次数为2,那么在首次调用超时后,会再次向一个服务节点发起最多2次重试(总共向当前节点发起了1+2=3次请求)
- MaxAutoRetriesNextServer:这里配置的2相当于换2个节点重试,在当前机器调用超时后,Feign将最多换N台机器发起调用(注意,这个参数将和MaxAutoRetries共同作用,也就是说,在新机器上超时后,会继续重试MaxAutoRetries+1次)
按照上面配置的参数,最大超时时间是多少?
(1000+2000)x(1+2)x(1+2)= 27000ms
极值函数的公式如下
MAX(Response Time) =(ConnectTimeout + ReadTimeout)*(MaxAutoRetries + 1)*(MaxAutoRetriesNextServer + 1)
5. 配置超时和重试机制
在feign-consumer-advanced里进行配置即可
# 调用feign-client这个服务
# 每台机器最大的重试次数
feign-client.ribbon.MaxAutoRetries=2
# 可以重试的机器数量
feign-client.ribbon.MaxAutoRetriesNextServer=2
# 连接的请求超时时长
feign-client.ribbon.ConnectTimeout=1000
# 业务处理的时长
feign-client.ribbon.ReadTimeout=2000
# HTTP Method的重试开关,可以配置GET、POST、PUT、DELETE
feign-client.ribbon.OkToRetryOnAllOperations=true
在feign-client-intf里增加一个测试接口
@GetMapping("/retry")
String retry(@RequestParam(name="timeout") int timeout);
在feign-client-advanced里实现这个接口
@Override
public String retry(@RequestParam(name="timeout") int timeout) {
try{
while (timeout-- >0){
Thread.sleep(1000);
}
}catch (Exception ex){
ex.printStackTrace();
}
log.info("retry is "+port);
return port;
}
在feign-consumer-advanced的controller里实现
@GetMapping("/retry")
public String retry(@RequestParam(name="timeout") int timeout){
return service.retry(timeout);
}
启动顺序:eureka-server,3个feign-client-advanced,1个feign-consumer-advanced
调用:http://localhost:20083/retry?timeout=2