佳佳的博客
Menu
首页
Feign Retry 重试
Posted by
佳佳
on 2019-01-22
IT
Feign
Spring Cloud
在 [为Spring Cloud Ribbon配置请求重试(Camden.SR2+)](https://www.jianshu.com/p/7af2814db7e9) 里说是通过 `spring.cloud.loadbalancer.retry.enabled` 参数来开启重试机制,但是经过测试发现是通过 `ribbon.OkToRetryOnAllOperations` 设置为 `true` 来开启重试,该属性默认值为 `false`。 - `ribbon.ConnectTimeout`:请求连接的超时时间 - `ribbon.ReadTimeout`:请求处理的超时时间 - `ribbon.OkToRetryOnAllOperations`:对所有操作请求都进行重试(默认值为 `false`) - `ribbon.MaxAutoRetriesNextServer`:切换实例的重试次数(默认值为 1) - `ribbon.MaxAutoRetries`:对当前实例的重试次数(默认值为 0) #### 调用次数 **调用次数 = `(ribbon.MaxAutoRetriesNextServer + 1) * (ribbon.MaxAutoRetries + 1)`** | ribbon.MaxAutoRetriesNextServer | ribbon.MaxAutoRetries | 调用次数 | | --- | --- | --- | | 0 | 0 | 1 | | 1 | 0 | 2 | | 1 | 1 | 4 | | 1 | 2 | 6 | | 2 | 0 | 3 | | 2 | 1 | 6 | | 2 | 2 | 9 | #### 配置文件 *application.yml* 可以在配置文件中使用如下方式配置全局的重试次数或者某个服务的重试次数,也可以单独将某个服务禁用重试机制。 ```yaml # Ribbon ribbon: ReadTimeout: 2000 # 请求处理的超时时间 ConnectTimeout: 10000 # 请求连接的超时时间 #MaxAutoRetries: 0 #OkToRetryOnAllOperations: false #Whether all operations can be retried for this client OkToRetryOnAllOperations: true MaxAutoRetries: 0 MaxAutoRetriesNextServer: 1 SERVICE-TEST: ribbon: OkToRetryOnAllOperations: true MaxAutoRetries: 1 MaxAutoRetriesNextServer: 1 SERVICE-NO-RETRYING: ribbon: OkToRetryOnAllOperations: false ``` #### `FeignClientsConfiguration` 也可以通过代码来实现上述效果。 ```java package com.octopus.middle.api.config; import feign.Retryer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Created by liujiajia on 2019/1/22. */ @Configuration public class FeignRetryConfig { @Bean public Retryer feignRetryer() { return new Retryer.Default(); } } ``` 下面是 `Retryer.Default` 的源码,可以看出默认是最多重试5次(包含首次调用),重试的间隔时间是动态变化的(参照源码 `nextMaxInterval` 方法),越往后间隔时间越长,但最长不会超过设置的最大间隔(`maxPeriod`)。 可以 `Retryer.Default` 的一个重载来指定间隔时间和最大重试次数。 ```java /* * Copyright 2013 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package feign; import static java.util.concurrent.TimeUnit.SECONDS; /** * Cloned for each invocation to {@link Client#execute(Request, feign.Request.Options)}. * Implementations may keep state to determine if retry operations should continue or not. */ public interface Retryer extends Cloneable { /** * if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception. */ void continueOrPropagate(RetryableException e); Retryer clone(); public static class Default implements Retryer { private final int maxAttempts; private final long period; private final long maxPeriod; int attempt; long sleptForMillis; public Default() { this(100, SECONDS.toMillis(1), 5); } public Default(long period, long maxPeriod, int maxAttempts) { this.period = period; this.maxPeriod = maxPeriod; this.maxAttempts = maxAttempts; this.attempt = 1; } // visible for testing; protected long currentTimeMillis() { return System.currentTimeMillis(); } 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(); } sleptForMillis += interval; } /** * Calculates the time interval to a retry attempt. <br> The interval increases exponentially * with each attempt, at a rate of nextInterval *= 1.5 (where 1.5 is the backoff factor), to the * maximum interval. * * @return time in nanoseconds from now until the next attempt. */ long nextMaxInterval() { long interval = (long) (period * Math.pow(1.5, attempt - 1)); return interval > maxPeriod ? maxPeriod : interval; } @Override public Retryer clone() { return new Default(period, maxPeriod, maxAttempts); } } /** * Implementation that never retries request. It propagates the RetryableException. */ Retryer NEVER_RETRY = new Retryer() { @Override public void continueOrPropagate(RetryableException e) { throw e; } @Override public Retryer clone() { return this; } }; } ``` 通过代码禁用重试: ```java package com.octopus.middle.api.config; import feign.Retryer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Created by liujiajia on 2019/1/22. */ @Configuration public class FeignNoRetryingConfig { @Bean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; } } ``` #### `@FeignClient.configuration` 上面的配置是作用全局或者某个服务的全局的,如果需要某些接口可以重试,某些不允许重试,则需要使用 `@FeignClient` 注解的 `configuration` 参数来实现。 下面是全局禁用重试,仅 `TestServiceInterface` 允许重试的示例。 *FeignNoRetryingConfig.java* ```java package com.octopus.middle.api.config; import feign.Retryer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Created by liujiajia on 2019/1/22. */ @Configuration public class FeignNoRetryingConfig { @Bean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; } } ``` *FeignRetryableConfig.java* ```java package com.octopus.middle.api.config; import feign.Retryer; import org.springframework.context.annotation.Bean; /** * Created by liujiajia on 2019/1/22. */ public class FeignRetryableConfig { @Bean public Retryer feignRetryer() { return new Retryer.Default(); } } ``` *TestServiceInterface.java* ```java package com.octopus.middle.api.services.test; /** * Created by liujiajia on 2019/1/21. */ import com.octopus.middle.api.config.FeignRetryableConfig; import com.octopus.middle.api.model.base.Parameter; import com.octopus.middle.api.result.base.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; /** * Test Service */ @FeignClient(name = "${service.test}", configuration = FeignRetryableConfig.class ,fallback = TestService.class) public interface TestServiceInterface { /** * Not Found * * @param parameter * @return */ @RequestMapping(value = "api/test/not-found", method = RequestMethod.POST) Result notFound(Parameter parameter); /** * Time Out * * @param parameter * @return */ @RequestMapping(value = "api/test/time-out", method = RequestMethod.POST) Result timeOut(Parameter parameter); } ``` #### 配置文件和代码同时使用 单独使用代码方式来设置重试时,重试次数貌似是在实例之间平均分配的,比如默认5次时,一台调用了2次,一台调用了3次。 ~~但若是代码和配置文件都做了设置,调用的次数不确定是如何计算的。~~ ~~比如 `ribbon.MaxAutoRetries` 设置为2,代码使用 `Retryer.Default()` 时,本以为代码的重试次数会覆盖配置文件的设置,但结果调用很多次(日志太乱,不知道数的对不对,总共调用了约30次)。~~ **2019/01/23 追记** 配置文件和代码同时使用时,经过几次测试,调用次数应该等于 单独使用配置文件时的调用次数 再乘以 代码中设置的重试次数。 **调用次数 = `(ribbon.MaxAutoRetriesNextServer + 1) * (ribbon.MaxAutoRetries + 1) * Retryer.Default.maxAttempts`** 使用两个实例(假设为A和B)时的测试结果: | ribbon.MaxAutoRetriesNextServer | ribbon.MaxAutoRetries | Retryer.Default.maxAttempts| 调用次数(A) | 调用次数(B) | | --- | --- | --- | --- | --- | | 0 | 0 | 5 | 2 | 3 | | 1 | 0 | 5 | 5 | 5 | | 1 | 1 | 5 | 10 | 10 | | 1 | 2 | 5 | 18 | 12 | | 2 | 0 | 5 | 9 | 6 | | 2 | 1 | 5 | 12 | 18 | | 2 | 2 | 5 | 24 | 21 | 调用次数是符合上面的公式的,但是具体每个服务的实例调用的次数就不确定是如何分配的了。 ribbon 设置都为 0 的时候,应该仅在某台服务实例上重试,但结果是分别调用 2 次和 3 次,没有严格遵守 `ribbon.MaxAutoRetriesNextServer` 为 0 的设定。 #### 参考 1. [为Spring Cloud Ribbon配置请求重试(Camden.SR2+)](https://www.jianshu.com/p/7af2814db7e9)
版权声明:原创文章,未经允许不得转载。
https://www.liujiajia.me/2019/1/22/feign-retry
“Buy me a nongfu spring”
« SAXParseException : 文档根元素 "beans" 必须匹配 DOCTYPE 根 "null"
未能加载文件或程序集“Newtonsoft.Json”或它的某一个依赖项 »
昵称
*
电子邮箱
*
回复内容
*
(回复审核后才会显示)
提交
目录
AUTHOR
刘佳佳
江苏 - 苏州
软件工程师
梦嘉集团