深入理解Feign的负载均衡 失败重试 熔断

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);
			}
		});
	}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值