Netflix开源软件套件(Netflix Open Source Software,简称Netflix OSS)是Netflix公司开源的一系列优秀的软件工具和框架,用于构建高性能、可扩展、弹性和可靠的分布式系统。提供了丰富的工具和解决方案,如Eureka、Feign、Ribbon、Hystrix、Zuul等。
这里我们借助Feign、Ribbon和Hystrix组件,用于实现微服务中的容错设计。Spring Cloud中提供了对这些组件的集成,在spring-cloud-starter-openfeign包中。
引入如下依赖即可使用feign
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Feign
Feign是一个声明式的HTTP调用客户端,可以通过接口定义的方式进行服务调用。传统的服务调用,需要进行Request封装、创建连接、解析结果等,十分繁琐。而使用Feign的话,一下就简洁了不少。
使用示例
- 在SpringBootApplication类上添加注解@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class WarehouseApplication {
public static void main(String[] args) {
SpringApplication.run(WarehouseApplication.class, args);
}
}
- 创建FeignCLient客户端,定义接口服务
// 在没有集成服务发现时,需要通过url配置服务具体连接信息
@FeignClient(value = "product", contextId = "product-info", url="127.0.0.1:8089")
public interface ProductServiceClient {
@GetMapping(value = "/restful/products", consumes = MediaType.APPLICATION_JSON)
Product[] getProducts();
}
- 使用FeignClient接口
public class warehouseService {
@Autowired
private ProductServiceClient serviceClient;
public Product[] listProduct() {
Product[] products = serviceClient.getProducts();
return products;
}
}
高级配置
请求拦截器
Feign发送请求前需要生成一个请求对象,我们可以往这个对象的请求头中添加一些数据,例如Auth信息。Feign中提供了这样的机制,暴露出一个RequestInterceptor接口,接口方法会在发送请求之前执行。
创建一个RequestInterceptor的实现类,根据实际情况重写apply方法。
@Configuration
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
HttpServletRequest request = getHttpServletRequest();
if (Objects.isNull(request)) {
return;
}
Map<String, String> headers = getHeaders(request);
// 把当前线程中请求对象中信息,复制到feign创建的reqeust请求中
for (Map.Entry<String, String> entry : headers.entrySet()) {
requestTemplate.header(entry.getKey(), entry.getValue());
}
}
}
执行日志
默认情况下,Feign不会记录访问日志,为了便于调试定位问题,我们可以更改配置,使其记录部分日志。Feign提供了一个Logger接口,里面定义了四种日志策略,如下
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
这里我们选择HEADERS
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: HEADERS
因为Feign中打印的日志级别为DEBUG,所以需要将项目中Feignclient接口所在包的日志级别设置为BEBUG。
logging:
level:
com.long.clients: debug
Http客户端
Feign中发送请求时,会使用一个Client来执行。默认的CLient实现是为每一个请求都新建一个连接HttpURLConnection,这导致其性能较差,详见feign.Client.Default类。
除了默认的Client实现之外,Feign还支持高性能的HttpClient和OkHttp,只要导入相关依赖,就可以集成使用。HttpClient和OkHttp内部都使用了连接池,可以将已建立的连接缓存起来复用,避免了重复建立连接的时间花销,大幅提升了性能。
HttpClient
使用apache httpclient,添加依赖后无需进行额外配置,有关配置项是feign.httpclient.enabled,默认为true。相关实现类为HttpClientFeignLoadBalancedConfiguration和FeignRibbonClientAutoConfiguration。
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
Okhttp
使用okhttp,添加依赖后,还需要手动添加配置项。
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
feign:
okhttp:
enabled: true
okhttp中的连接池
public final class RealConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
private static final Executor executor= new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));
}
重试器
Feign中有一个重试机制(Retryer接口),在发送请求执行过程中,如果抛出了可重试异常(例如超时,异常为RetryableException),会进行重试操作。这类异常一般与网络IO相关,例如断连,有关的代码逻辑见SynchronousMethodHandler.invoke()。
超时配置
超时有关的配置对象是Request.Options,其中配置有两个部分,一个连接超时,二是读取超时,默认的超时配置分别为10秒和60秒。
public Options() {
this(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
}
public static class Options {
private final long connectTimeout;
private final TimeUnit connectTimeoutUnit;
private final long readTimeout;
private final TimeUnit readTimeoutUnit;
private final boolean followRedirects;
}
导致的问题
重试机制存在的问题是,有可能导致重复调用。例如调用接口后已经执行成功,但在返回数据的时候发生了IO错误,这时的重试,就是重复调用。
发生重复调用时,如果调用的接口不是幂等的,就会有异常行为,假设为扣款操作,会进行两次扣费。所以,如果服务不是幂等的,需要关闭重试操作。
配置重试器
默认的策略是会进行重试操作,见Retryer.Default,最大重试次数5,最大间隔时间1S。我们也可以根据自己的需要实现Retryer接口或者自定义Default,例如不进行重试,直接抛出异常。
- 创建Retryer
public class FeignConfig {
@Bean
public Retryer retryer(){
// 初始重试间隔时间 (默认按照1.5倍递增)
// 最大重试间隔时间
// 最大尝试次数
return new Retryer.Default(100,1000,1);
}
}
- 绑定到feignclient.configuration
@FeignClient(value = "product", contextId = "product-info", configuration = {FeignConfig.class})
public interface ProductServiceClient {
}
自定义配置
默认配置
feign中有一个配置类FeignClientsConfiguration,在其中定义了相关组件的默认实现。在没有进行自定义配置时,会使用FeignClientsConfiguration中创建的对象。
作用域
feign中进行自定义配置时,有两个维度,一个是针对单client,一个是针对default。在client没有进行单独配置时,使用default配置;单个client配置只作用于自己。
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: HEADERS
connectTimeout: 1000
readTimeout: 1000
product: # 只针对product服务
loggerLevel: HEADERS
connectTimeout: 5000
readTimeout: 5000
配置方式
有两种配置方式,一是在配置文件中,二是在代码中。
在代码中进行配置时,需要先创建配置类,并在配置类中创建相关的Bean。
@Slf4j
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.HEADERS;
}
@Bean
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
log.info("feign 请求前添加请求头");
requestTemplate.header("traceId",traceId);
}
};
}
@Bean
public Retryer retryer(){
// 初始重试间隔时间 (默认按照1.5倍递增)
// 最大重试间隔时间
// 最大重试次数
return new Retryer.Default(100,1000,5);
}
@Bean
public Request.Options options(){
// 链接服务端超时时间ms
// 接受服务端响应时间ms
// 是否支持重定向
return new Request.Options(1000,1000,true);
}
}
配置类创建好后,可以将其作为default配置,也可以是针对某个client。
default
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
public class WarehouseApplication {
public static void main(String[] args) {
SpringApplication.run(WarehouseApplication.class, args);
}
}
client
@FeignClient(value = "product", contextId = "product-info", configuration = {FeignConfig.class})
public interface ProductServiceClient {
}
集成Ribbon
openfeign中默认集成了ribbon,在starter openfeign引入了spring-cloud-starter-netflix-ribbon依赖,所以可以直接使用ribbon,不用额外引入依赖。
配置
要使用负载均衡,首先得搭建一个服务注册中心,如Eureka或者Nacos。
然后在项目中引入Eureka或者Nacos相关的依赖,并在配置文件中配置服务注册中心地址。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: fc44d960-44bf-4a41-b718-af4b061b2da3
最后启用服务发现,在Application类上配置如下注解。
@EnableDiscoveryClient
public class WarehouseApplication {
public static void main(String[] args) {
SpringApplication.run(WarehouseApplication.class, args);
}
}
使用
在@feignclient注解中设置服务Id,如auth-api
@FeignClient(value = "auth-api", contextId = "user-info")
public interface AuthClient {
}
在进行请求时,ribbon通过服务发现接口,获取该服务的可用地址列表。如果该服务存在多个实例,ribbon然后根据自己的负载均衡策略(如轮询、随机),选择一个地址。服务发现接口由Eureka、Nacos等中间件实现。
集成Hystrix
配置
feign可以通过feign-hystrix与Hystrix进行无缝集成,只需加入相关依赖和配置。
由于Hystrix已经不再更新,在Hoxton以上的spring cloud版本,可以集成其它的服务治理框架来进行熔断保护,例如Resilience4j或Sentinel。
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
集成时还需要feign-hystrix依赖,但是starter-openfeign中已经引入,就不需要再次引入了。
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
</dependency>
在openfeign中,需要通过配置手动启用Hystrix。
启用Hystrix之后,本质上是对openfeign中请求发送模块再进行一次封装,记录每次请求的结果进行监测,在达到熔断条件时,进行服务熔断。服务熔断后,针对相应的请求,走服务降级逻辑。
feign:
hystrix:
enabled: true
使用
在@FeignClient注解中,新增fallback配置,fallback是一个降级回调。
在feign抛出异常时,hystrix会捕获feign中抛出的连接超时或者读取超时异常,转而调用fallback中的方法。
在服务熔断后,hystrix会拦截对其的调用,转而调用fallback中的方法。
@FeignClient(name = "warehouse", fallback = ProductServiceFallback.class)
public interface ProductServiceClient {
}
ProductServiceFallback实现该接口,并重写相应方法
@Component
public class ProductServiceFallback implements ProductServiceClient{
@Override
public Product[] getProducts() {
return new Product[0];
}
}
在FeignClient接口中的方法上添加@HystrixCommand注解,并配置相关的commandKey和threadPoolKey。
@FeignClient(name = "warehouse", fallback = ProductServiceFallback.class)
public interface ProductServiceClient {
@GetMapping(value = "/restful/products", consumes = MediaType.APPLICATION_JSON)
@HystrixCommand(commandKey = "simplecmd", threadPoolKey = "simpleThreadPool")
Product[] getProducts();
}
Hystrix配置
我们在feign集成hystrix时,需要将@HystrixCommand注解设置在接口方法上,并对其进行了commandKey、threadPoolKey等配置。
- commandKey表示对应的HystrixCommand对象(hystrix中的核心类),我们可以对这个对象进行配置,如果没配置,则使用Hystrix中默认的配置。
- threadPoolKey表示对应的线程池名称,如果我们没有创建对应的线程池,则使用Hystrix中默认的线程池。
command
command可以配置多个,可以一个接口服务使用一个command,也可以多个接口服务使用同一个command。
command中主要进行execution和circuitBreake配置,详见HystrixCommandProperties。
- execution,执行配置,使用默认的线程池隔离,可配置超时时间。
- circuitBreaker,断路器配置,设置触发熔断的条件,如最小请求数和请求失败概率。
关联配置为metrics.rollingStats.timeInMilliseconds,用于设置Metrics数据的滚动时间窗口,指定了数据统计的时间范围。默认为10,表示一个时间窗口为10S。在这个时间窗口内,Hystrix会统计并更新各种指标,如请求数、失败数、超时数等,用于评估熔断器和线程池的运行状况。
以simplecmd为例,10秒内超过10个请求,并有50%的请求都失败时,就达到熔断的条件了。熔断后该服务示例休眠十秒钟,即十秒内,不调用该服务实例。
hystrix:
command:
# 对单独的hystrix command进行设置,命令名默认为方法名,可以使用注解的commandkey属性进行设置
# 不同服务应该设置不同conmandKey,否则一个断路器断开后,会影响所有的服务
simplecmd:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 # 设置超时时间
circuitBreaker: # 断路器配置
requestVolumeThreshold: 10 # 触发熔断的最小请求数量
errorThresholdPercentage: 50 # 触发熔断的错误百分比
sleepWindowInMilliseconds: 10000# 熔断后的休眠时间
# 对所有的hytrtix command进行设置,优先级不如单独设置
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1500 # 设置超时时间
circuitBreaker: # 断路器配置
requestVolumeThreshold: 5 # 触发熔断的最小请求数量
errorThresholdPercentage: 50 # 触发熔断的错误百分比
sleepWindowInMilliseconds: 5000 # 熔断后的休眠时间
threadPool
在execution中配置使用线程池进行隔离后,如果不为其设置threadPoolKey,将使用默认的线程池。多个服务都使用一个线程池时,一个服务阻塞会耗尽线程池中的线程,这会导致其它服务也被阻塞。所以,有必要为不同服务设置不同的线程池。
hystrix:
# 线程池配置
threadpool:
# 单独配置
simpleThreadPool:
# 核心线程数量
coreSize: 10
# 最大线程数量
maximumSize: 15
# 是否允许线程池扩展到最大线程池数量
allowMaximumSizeToDivergeFromCoreSize: false
# 多余线程被回收前的存活时间
keepAliveTimeMinutes: 1
# 任务队列最大长度
maxQueueSize: 1000
# 队列内任务长度达到该值后,即使队列未满,也会拒绝新的请求,可用于动态调整队列长度
queueSizeRejectionThreshold: 600
# 默认配置
default:
# 默认10
coreSize: 10
maximumSize: 10
标签:实战,feign,请求,openfeign,配置,class,容错,线程,public
From: https://www.cnblogs.com/cd-along/p/18213949