由于高并发和海量数据访问,一般系统都会从单体架构升级为集群架构,不可避免都会使用RPC框架。
但是,由于网络抖动等外部不可控因素,系统调用会偶发失败,特殊场景可能需要【重试】。
下面介绍一些常用的重试方案。
一、固定循环次数立即重试
可以查看dubbo中的com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoke方法相关代码:
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
...
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
...
// retry loop.
RpcException le = null; // last exception.
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
...
try {
Result result = invoker.invoke(invocation);
...
return result;
} catch (RpcException e) {
if (e.isBiz()) { // biz exception.
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException(le != null ? le.getCode() : 0);
}
tips
dubbo常用配置如下:
<dubbo:reference id="xxxx" interface="xx" retries="3" timeout="1000"/>
其中,retries="3" 重试三次,也就是最多尝试四次,如果失败就抛出异常;
timeout="1000" 单位为毫秒,表示服务超时时间,客户端在调用该dubbo服务时会启动超时检测,如果达到1秒就会报超时异常,超时异常后客户端会再次尝试调用,如果成功就返回,如果失败继续调用、直到达到最大重试次数。
如果是timeout超时异常,会重试;如果是其他异常导致dubbo服务调用抛异常,也会立即重试,而不会等到timeout的时间才重试。
所以,一定要注意,一定要注意,一定要注意:
- 最坏情况下:该接口最大响应时间是: (retries + 1) * timeout。
二、有固定delay的重试
在dubbo重试代码的基础上,新增一个固定delay,例如可以用Thread.sleep(delay)来模拟。
for (int i = 0; i < len; i++) {
...
try {
Result result = invoker.invoke(invocation);
...
return result;
} catch (RpcException e) {
Thread.sleep(delay);
}
}
这种方式会造成被依赖接口间歇性的毛刺
三、带有随机delay的重试
可以看gRPC的源码,参数含义如下:
- INITIAL_BACKOFF (how long to wait after the first failure before retrying)
- MULTIPLIER (factor with which to multiply backoff after a failed retry)
- JITTER (by how much to randomize backoffs).
- MAX_BACKOFF (upper bound on backoff)
- MIN_CONNECT_TIMEOUT (minimum time we're willing to give a connection to complete)
ConnectWithBackoff()
current_backoff = INITIAL_BACKOFF
current_deadline = now() + INITIAL_BACKOFF
while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))
!= SUCCESS)
SleepUntil(current_deadline)
current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
current_deadline = now() + current_backoff +
UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)
四、Spring retry库
@EnableRetry注解用于开启重试框架,必须修饰在类上
proxyTargetClass:Boolean类型,用于指明代理方式; 设置为true表示使用cglib代理,设置为false表示使用jdk动态代理,默认为false,即使用jdk动态代理
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(
proxyTargetClass = false
)
@Import({RetryConfiguration.class})
@Documented
public @interface EnableRetry {
boolean proxyTargetClass() default false;
}
@Retryable注解修饰在方法上
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 3000, multiplier = 2)) 设置重试3次,延迟3000毫秒再执行,每次延迟时间提高2倍。
重试策略
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
String interceptor() default "";
Class<? extends Throwable>[] value() default {};
Class<? extends Throwable>[] include() default {};
Class<? extends Throwable>[] exclude() default {};
String label() default "";
boolean stateful() default false;
int maxAttempts() default 3;
String maxAttemptsExpression() default "";
Backoff backoff() default @Backoff;
String exceptionExpression() default "";
}
value:只会重试抛出的指定的异常
include:作用和value类似。当exclude也为空时,默认所以异常
exclude:指定不需要处理的异常
maxAttempts:最大重试次数,默认3次
backoff:设置补偿机制
退避策略
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RetryConfiguration.class})
@Documented
public @interface Backoff {
long value() default 1000L;
long delay() default 0L;
long maxDelay() default 0L;
double multiplier() default 0.0D;
String delayExpression() default "";
String maxDelayExpression() default "";
String multiplierExpression() default "";
boolean random() default false;
}
backoff:重试等待策略
value: 设置延迟重试时间,默认为1000L,即1秒;
multiplier: 指定延迟倍数,默认为0,表示固定暂停1秒后进行重试
@Recover 当重试指定次数后,回调此方法
- 异常类型需要与Recover方法参数类型保持一致;
- 重试方法返回值要与recover方法返回值保持一致。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RetryConfiguration.class})
@Documented
public @interface Recover {
}
Spring Retry的具体源码分析可以参考:albenw.github.io/posts/69a96…