feign的组成
1.Hystri 将每一个fegin请求封装成一个命令 通过执行命令来控制请求hystrixCommand.execute()
2.Feign通过动态代理把最终请求的执行放在了SynchronousMethodHandler.invoke(同步的方法执行器)
3.Feign请求通过ribbon负载均衡,来获取注册在eureka上的服务的IP+端口
4.默认通过java自带的HttpURLConnection来发送http请求
逻辑上 Hystri + ribbon + HttpURLConnection = Feign
看一下调用的链路
feign的配置
我们已 <spring-cloud.version>Hoxton.SR1</spring-cloud.version>版本为例来看一下feign的一些常用的配置。
#开启feign的熔断降级 默认为false是不开启熔断的
#feign.hystrix.enabled =
#hystrix熔断的超时时间(等待该时长未得到返回就执行降级的策略)
#hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds =
#ribbon的读取超时时间
#ribbon.ReadTimeout =
#ribbon的连接超时时间
#ribbon.ConnectTimeout =
#feign的连接超时时间
#feign.client.config.default.connectTimeout =
#feign的读取超时时间
#feign.client.config.default.readTimeout =
feign时间的默认配置Request.Options 连接超时10s 读取超时60s
Ribbon的默认配置类 DefaultClientConfigImpl 读取超时时间5s 连接超时时间2s
我们来测试一下这个需要准备两个服务和一个注册中心,我只把两个服务里面的两个方法贴出来 A服务通过feign来调用B服务
A服务方法
@Autowired
private FeignClientProperties feignClientProperties;
@ApiOperation(value = "降级测试", httpMethod = "GET")
@RequestMapping("/normal")
public Object normal() {
long start = System.currentTimeMillis();
System.out.println("进入方法=====");
Result result = invoiceService.queryCloudEasyOrderInfo2("");
long end = System.currentTimeMillis();
System.out.println("花费的时间"+(end-start));
System.out.println("调用发票服务返回结果 result="+result);
return result;
}
B服务的方法 post请求(ribbon的重试自动屏蔽掉post请求)
public Result<OrderHistory> queryCloudEasyOrderInfo(String orderNo) {
try {
System.out.println("请求来了=====================");
//方法处理耗时是1s钟
Thread.sleep(1000);
}catch (Exception e){
return null;
}
}
1.fegin的所有配置都是默认的都不做配置(配置文件未做配置,代码里面配置了Retryer)
A服务日志
B服务的日志
因为有重试机制所以总共调用了5次。每次调用的的时间差大概就是1s多。
思考:1.方法耗时1s钟 feign读取超时时5s为什么还是没有成功呢?
2.调用失败重试了5次,每次这个时间间隔时哪里设置的(这个1s时谁的默认)?
答案:feign的读取超时时间时针对整个调用过程
每次的http请求,建立socket连接,等待响应 这个过程在HttpURLConnection内完成 的,它的默认读取超时时间时1s。所以只要1s内没有响应那么这一次的调用就是失败的。但是整个feign调用还没有结束,会继续重试。
2.只配置这两个值
feign.client.config.default.connectTimeout = 3000
feign.client.config.default.readTimeout = 3000
A服务日志
进入方法=====2021-04-16T15:45:41.658
2021-04-16 15:45:57.886 [http-nio-8081-exec-3] ERROR o.a.c.c.C.[.[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.RetryableException: Read timed out executing POST http://broker-invoice-service/api/cloudeasy/order/operation/queryCloudEasyOrderInfo?orderNo] with root cause
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
feign整个调用的时间是 3s*5=15s
B服务的日志
[broker-invoice-service] [TID: N/A] >> 2021-04-16 15:45:41.662 ERROR [,eaba7ae3a40b96b1,eaba7ae3a40b96b1,false] 29516 --- [nio-8857-exec-7] c.c.b.c.i.u.interceptor.RequestAop required
请求来了=====================时间2021-04-16T15:45:41.663
[broker-invoice-service] [TID: N/A] >> 2021-04-16 15:45:44.812 ERROR [,85234dd79c50cf69,85234dd79c50cf69,false] 29516 --- [nio-8857-exec-2] c.c.b.c.i.u.interceptor.RequestAop required
请求来了=====================时间2021-04-16T15:45:44.812
[broker-invoice-service] [TID: N/A] >> 2021-04-16 15:45:48.038 ERROR [,eef74f3d48abc37c,eef74f3d48abc37c,false] 29516 --- [nio-8857-exec-8] c.c.b.c.i.u.interceptor.RequestAop required
请求来了=====================时间2021-04-16T15:45:48.038
[broker-invoice-service] [TID: N/A] >> 2021-04-16 15:45:51.377 ERROR [,1eea8198257057a1,1eea8198257057a1,false] 29516 --- [nio-8857-exec-4] c.c.b.c.i.u.interceptor.RequestAop required
请求来了=====================时间2021-04-16T15:45:51.378
[broker-invoice-service] [TID: N/A] >> 2021-04-16 15:45:54.884 ERROR [,394f79e805f3602f,394f79e805f3602f,false] 29516 --- [io-8857-exec-10] c.c.b.c.i.u.interceptor.RequestAop required
请求来了=====================时间2021-04-16T15:45:54.884
每次重试的时间间隔是3s
重试机制
feign的重试机制默认试关闭的(在上面的demo我们试配置了feign的重试的)。
// feign 默认的重试
Retryer NEVER_RETRY = new Retryer() {
@Override
public void continueOrPropagate(RetryableException e) {
throw e;
}
@Override
public Retryer clone() {
return this;
}
};
feign的重试底层需要依赖spring-retry 开启需要做如下配置
引入依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.4.RELEASE</version>
</dependency>
配置feign的重试
@Bean
public Retryer feignRetryer() {
return new Retryer.Default();
}
feign重试的部分代码
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
//初始化重试的机制
Retryer retryer = this.retryer.clone();
while (true) {
try {
//不断的重试执行这个方法
return executeAndDecode(template);
} catch (RetryableException e) {
try {
//如果发生异常就执行continueOrPropagate方法
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
public void continueOrPropagate(RetryableException e) {
//判断重试的次数是否到达上限
if (attempt++ >= maxAttempts) {
throw e;
}
long interval;
if (e.retryAfter() != null) {
interval = e.retryAfter().getTime() - currentTimeMillis();
//判断时间间隔
if (interval > maxPeriod) {
interval = maxPeriod;
}
if (interval < 0) {
return;
}
} else {
interval = nextMaxInterval();
}
try {
Thread.sleep(interval);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
throw e;
}
sleptForMillis += interval;
}
在上面的测试超时时间的时候我们feign的重试是配置了的。
ribbon的重试和feign的重试是完全分离开的,可以理解成feign是外面一层的重试,ribbon是里面一层的重试,ribbon的重试当前版本只对get请求有效默认开启。我们先看一下ribbon重试的几个配置
#对下一个实例的重试次数 默认1
ribbon.MaxAutoRetriesNextServer=1
#对当前实例的重试次数 默认0
ribbon.MaxAutoRetries=0
#对所有操作都重试(这个配置是无效的)
ribbon.OKToRetryOnAllOperations=true
我们把B服务的请求改成GET请求 所有的配置都是默认(不做任何配置)A服务请求B服务
B服务如果只有一个实例该请求在该实例只执行一次。
如果B服务有两个实例该请求在两个实例分别执行一次。
只配置这个
ribbon.MaxAutoRetries=1 查看两个实例的日志 实例a
实例b
两个实例各执行了两次。要理解这个配置ribbon.MaxAutoRetries 首先要理解当前实例
请求调用实例a当前实例就是a,第一次调用失败后就会有重试(重试的前提是调用失败),对当前的a实例重试1次,默认调用下一个实例1次,调用实例b,当前实例就是b,调用失败执行重试的策略,对当前实例的重试1次即对b重试一次。
为什么说 ribbon.OKToRetryOnAllOperations=true
这个配置是无效
DefaultLoadBalancerRetryHandler下面的方法就是用到我们配置的方法
public DefaultLoadBalancerRetryHandler(IClientConfig clientConfig) {
this.retrySameServer = clientConfig.get(CommonClientConfigKey.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
this.retryNextServer = clientConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
// OkToRetryOnAllOperations 属性没有从配置里面取,直接默认false
this.retryEnabled = clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false);
}
RibbonLoadBalancedRetryPolicy重试的策略
public boolean canRetry(LoadBalancedRetryContext context) {
HttpMethod method = context.getRequest().getMethod();
//GET方法 或者OkToRetryOnAllOperations为true
return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations();
}
@Override
public boolean canRetrySameServer(LoadBalancedRetryContext context) {
return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer()
&& canRetry(context);
}
@Override
public boolean canRetryNextServer(LoadBalancedRetryContext context) {
// this will be called after a failure occurs and we increment the counter
// so we check that the count is less than or equals to too make sure
// we try the next server the right number of times
return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer()
&& canRetry(context);
}
断点验证
我们的配置
前两个都生效了只有OKToRetryOnAllOperations还是false.
RetryableFeignLoadBalancer
@Override
public RibbonResponse execute(final RibbonRequest request,
IClientConfig configOverride) throws IOException {
final Request.Options options;
if (configOverride != null) {
RibbonProperties ribbon = RibbonProperties.from(configOverride);
options = new Request.Options(ribbon.connectTimeout(this.connectTimeout),
ribbon.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
final LoadBalancedRetryPolicy retryPolicy = this.loadBalancedRetryFactory
.createRetryPolicy(this.getClientName(), this);
RetryTemplate retryTemplate = new RetryTemplate();
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory
.createBackOffPolicy(this.getClientName());
retryTemplate.setBackOffPolicy(
backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
RetryListener[] retryListeners = this.loadBalancedRetryFactory
.createRetryListeners(this.getClientName());
if (retryListeners != null && retryListeners.length != 0) {
retryTemplate.setListeners(retryListeners);
}
retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy()
: new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this,
this.getClientName()));
return retryTemplate.execute(new RetryCallback<RibbonResponse, IOException>() {
//这个方法的重试就是ribbon的重试
@Override
public RibbonResponse doWithRetry(RetryContext retryContext)
throws IOException {
Request feignRequest = null;
// on retries the policy will choose the server and set it in the context
// extract the server and update the request being made
if (retryContext instanceof LoadBalancedRetryContext) {
ServiceInstance service = ((LoadBalancedRetryContext) retryContext)
.getServiceInstance();
if (service != null) {
feignRequest = ((RibbonRequest) request
.replaceUri(reconstructURIWithServer(
new Server(service.getHost(), service.getPort()),
request.getUri()))).toRequest();
}
}
if (feignRequest == null) {
feignRequest = request.toRequest();
}
Response response = request.client().execute(feignRequest, options);
if (retryPolicy != null
&& retryPolicy.retryableStatusCode(response.status())) {
byte[] byteArray = response.body() == null ? new byte[] {}
: StreamUtils
.copyToByteArray(response.body().asInputStream());
response.close();
throw new RibbonResponseStatusCodeException(
RetryableFeignLoadBalancer.this.clientName, response,
byteArray, request.getUri());
}
return new RibbonResponse(request.getUri(), response);
}
}, new LoadBalancedRecoveryCallback<RibbonResponse, Response>() {
@Override
protected RibbonResponse createResponse(Response response, URI uri) {
return new RibbonResponse(uri, response);
}
});
}