Ribbon对于SocketTimeOutException重试的坑以及重试代码解析
背景
本文基于Spring-Cloud, Daltson SR4
微服务一般多实例部署,在发布的时候,我们要做到无感知发布;微服务调用总会通过Ribbon,同时里面会实现一些重试的机制,相关配置是:
#最多重试多少台服务器
ribbon.MaxAutoRetriesNextServer=2
#每台服务器最多重试次数,但是首次调用不包括在内
ribbon.MaxAutoRetries=1
- 1
- 2
- 3
- 4
在发布时,为了适应Eureka注册中心的注册信息变换(参考Eureka上线下线解析),我们挨个重启实例,并且在每个实例启动后等待一段时间((Eureka客户端注册信息刷新时间+Eureka客户端Ribbon刷新事件)*3)再重启另外一个实例,来避免注册信息变化带来的影响,这样这个被重启的实例的微服务的调用方总能负载均衡重试调用到可用的实例。
但是,实际生产中,我们发现,某个实例重启其他实例正常工作时,会有一小段时间,调用方调用到被重启的实例,直接失败,没有触发重试。
代码分析
无论上层是Feign调用还是Zuul调用,到了Ribbon这一层都是创建一个LoadBalancerCommand,调用其中的submit方法执行http请求,这里利用了RxJava机制:
public Observable<T> submit(final ServerOperation<T> operation) {
final ExecutionInfoContext context = new ExecutionInfoContext();
if (listenerInvoker != null) {
try {
listenerInvoker.onExecutionStart();
} catch (AbortExecutionException e) {
return Observable.error(e);
}
}
//这里就是读取上面说的配置最多重试多少台服务器以及每台服务器最多重试次数
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
// 利用RxJava生成一个Observable用于后面的回调
Observable<T> o =
//选择一个server进行调用
(server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1<Server, Observable<T>>() {
@Override
// Called for each server being selected
public Observable<T> call(Server server) {
context.setServer(server);
//获取这个server调用监控记录,用于各种统计和LoadBalanceRule的筛选server处理
final ServerStats stats = loadBalancerContext.getServerStats(server);
//获取本次server调用的回调入口,用于重试同一实例的重试回调
Observable<T> o = Observable
.just(server)
.concatMap(new Func1<Server, Observable<T>>() {
@Override
public Observable<T> call(final Server server) {
context.incAttemptCount();
loadBalancerContext.noteOpenConnection(stats);
if (listenerInvoker != null) {
try {
listenerInvoker.onStartWithServer(context.toExecutionInfo());
} catch (AbortExecutionException e) {
return Observable.error(e);
}
}
final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start();
return operation.call(server).doOnEach(new Observer<T>() {
private T entity;
@Override
public void onCompleted() {
recordStats(tracer, stats, entity, null);
// TODO: What to do if onNext or onError are never called?
}
@Override
public void onError(Throwable e) {
recordStats(tracer, stats, null, e);
logger.debug("Got error {} when executed on server {}", e, server);
if (listenerInvoker != null) {
listenerInvoker.onExceptionWithServer(e, context.toExecutionInfo());
}
}
@Override
public void onNext(T entity) {
this.entity = entity;
if (listenerInvoker != null) {
listenerInvoker.onExecutionSuccess(entity, context.toExecutionInfo());
}
}
private void recordStats(Stopwatch tracer, ServerStats stats, Object entity, Throwable exception) {
tracer.stop();
loadBalancerContext.noteRequestCompletion(stats, entity, exception, tracer.getDuration(TimeUnit.MILLISECONDS), retryHandler);
}
});
}
});
//设置针对同一实例的重试回调
if (maxRetrysSame > 0)
o = o.retry(retryPolicy(maxRetrysSame, true));
return o;
}
});
//设置重试下一个实例的回调
if (maxRetrysNext > 0 && server == null)
o = o.retry(retryPolicy(maxRetrysNext, false));
//设置重试超过次数则终止调用并设置对应异常的回调
return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {
@Override
public Observable<T> call(Throwable e) {
if (context.getAttemptCount() > 0) {
if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) {
e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED,
"Number of retries on next server exceeded max " + maxRetrysNext
+ " retries, while making a call for: " + context.getServer(), e);
}
else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) {
e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_EXEEDED,
"Number of retries exceeded max " + maxRetrysSame
+ " retries, while making a call for: " + context.getServer(), e);
}
}
if (listenerInvoker != null) {
listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo());
}
return Observable.error(e);
}
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
我们重点看一下设置重试的回调的详细回调代码:
private Func2<Integer, Throwable, Boolean> retryPolicy(final int maxRetrys, final boolean same) {
return new Func2<Integer, Throwable, Boolean>() {
//只有返回为true的时候才会retry
@Override
public Boolean call(Integer tryCount, Throwable e) {
//抛出的异常是AbortExecutionException则不重试
if (e instanceof AbortExecutionException) {
return false;
}
//超过最大重试次数则不重试
if (tryCount > maxRetrys) {
return false;
}
if (e.getCause() != null && e instanceof RuntimeException) {
e = e.getCause();
}
//判断是否是可以重试的exception
return retryHandler.isRetriableException(e, same);
}
};
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
这个判断是否是可以重试的exception里面的逻辑是:
public boolean isRetriableException(Throwable e, boolean sameServer)
{
//如果已经配置了ribbon.okToRetryOnAllErrors为true,则不论什么异常都会重试,我们没有这么配置,一般也不会这么配置
if (okToRetryOnAllErrors)
{
return true;
}
else if (e instanceof ClientException)
{
ClientException ce = (ClientException) e;
if (ce.getErrorType() == ClientException.ErrorType.SERVER_THROTTLED)
{
return !sameServer;
}
else
{
return false;
}
}
else
{
if (e instanceof RetryableHttpCodeAndMethodException)
{
//如果是有response返回的异常就会到这里
if (((RetryableHttpCodeAndMethodException) e).getMethod().equals("GET") || okToRetryOnAllOperations)
return true;
return false;
}
//其他情况,就是连接失败的判断。首先需要配置ribbon.okToRetryOnConnectErrors为true,这个默认就是true;然后通过isConnectionException判断
return okToRetryOnConnectErrors && isConnectionException(e);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
最后,我们来看看如何判断一个Exception为ConnectionException:
protected List<Class<? extends Throwable>> connectionRelated = Lists
.<Class<? extends Throwable>> newArrayList(SocketException.class);
public boolean isConnectionException(Throwable e)
{
return Utils.isPresentAsCause(e, connectionRelated);
}
- 1
- 2
- 3
- 4
- 5
- 6
这个方法其实就看这个异常的异常以及Cause中是否有SocketException,如果有则返回true。
问题定位
在Windows环境下调试,我们发现一个有意思的现象,当我们设置ribbon连接超时
ribbon.ConnectTimeout=500时(这个和我们线上配置一样),重试失败,捕获到“java.net.SocketTimeoutException:
connect timed
out”这个Exception;当设置连接超时为1000ms以上时(不包括1000),抛出的异常就是“java.net.ConnectException:
Connection refused: connect”
我们写一段测试代码看一下:
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
try {
socket.connect(new InetSocketAddress("127.0.0.1", 8080), 500);
} catch (Exception e) {
e.printStackTrace();
}
socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 8080), 1100);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
这个端口没有启用,输出为:
java.net.SocketTimeoutException: connect timed out
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at com.hash.test.TestRxJava.main(TestRxJava.java:14)
Exception in thread "main" java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at com.hash.test.TestRxJava.main(TestRxJava.java:19)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
就是不一样的Exception
而SocketTimeoutException不是一种SocketException,所以,原有的重试逻辑不能重试。
对于这个问题,我在Feign的github源代码库提了个issue
所以,我们要改造isConnectionException这个方法;对于SocketTimeoutException,不是全都重试,只重试msg为connect timed out的Exception。同时,SocketTimeoutException可能会被封装,我们为了简单,只通过msg进行判断:
public boolean isConnectionException(Throwable e)
{
return Utils.isPresentAsCause(e, connectionRelated)
|| e.getMessage().contains("connect timed out");
}
- 1
- 2
- 3
- 4
- 5
这段代码,也提了Pull Request
修改替换源代码后,线上问题解决
https://blog.csdn.net/zhxdick/article/details/78906462
Ribbon对于SocketTimeOutException重试的坑以及重试代码解析的更多相关文章
- 客户端负载均衡Ribbon之三:AvailabilityFilteringRule的坑(Spring Cloud Finchley.SR2)
我们项目配置了AvailabilityFilteringRule作为所有Ribbon调用的负载均衡规则,它有那些坑呢(理解歧义和注意点)? 首先来看com.netflix.loadbalancer.A ...
- Feign Ribbon Hystrix 三者关系 | 史上最全, 深度解析
史上最全: Feign Ribbon Hystrix 三者关系 | 深度解析 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -25[ 博客园 总入口 ] 前言 疯狂创客圈(笔者尼恩创建的 ...
- FastClick 填坑及源码解析
最近产品妹子提出了一个体验issue —— 用 iOS 在手Q阅读书友交流区发表书评时,光标点击总是不好定位到正确的位置: 如上图,具体表现是较快点击时,光标总会跳到 textarea 内容的尾部.只 ...
- 开坑Java编写Json解析器,简明教程
https://zhuanlan.zhihu.com/p/22460835?refer=json-tutorial 课程不是我原创,我打算照他的这个C版本来重写一遍Java的,打算用面向对象的方式来编 ...
- 我的踩坑之旅-代码不规范引发的“bug”
今早公司上班,老大跟我说有一个服务老是上线,下线,问我啥情况.我回想了下我的项目部署,觉得不可能会出现这个问题呀.然后各种鼓捣,倒腾了一个早上,终于找出了罪魁祸首. 场景:我们的服务部署在亚马逊上.我 ...
- 史上最坑 idea 更改代码不生效
原来, 如果本地多次调整过系统时间,那么gradle 的缓存 会缓存 你的 上次编译时间再未来,那么你再怎么编译,都很难生效,即使删除了生成的字节码目录. 然后invalidate caches/re ...
- [爬坑记录] Qt 代码卡住 不发信号 不触发槽
先让我激动一会儿 [捂脸] 最近在用Qt做个程序 用来参加比赛 期间总共遇到两次如标题的问题 也即是 莫名其妙的不触发槽函数了 而且原因也不一样 {先说明 我学习Qt依旧只是入门级 也许入不了大佬法眼 ...
- Zuul + Ribbon 脱离Eureka完成负载均衡+重试机制
Zuul + Ribbon 脱离Eureka完成负载均衡+重试机制 因为没有注册中心,所以需要网关对下游服务做负载均衡,然后果断集成Ribbon.中间遇到很多坑,最后终于解决了. 其实Ribbon里面 ...
- 为Spring Cloud Ribbon配置请求重试(Camden.SR2+)
当我们使用Spring Cloud Ribbon实现客户端负载均衡的时候,通常都会利用@LoadBalanced来让RestTemplate具备客户端负载功能,从而实现面向服务名的接口访问. 下面的例 ...
随机推荐
- IntelliJ IDEA 2017.2.2 的破解 有效期 2116年
破解三部曲 下载破解文件 JetbrainsCrack-2.6.6-release-enc.jar http://idea.lanyus.com/jar/JetbrainsCrack-2.6.6- ...
- c#实现统计代码运行时间
方法一: //实例化一个计时器 Stopwatch watch = new Stopwatch(); //開始计时 watch.Start(); //此处为要计算的执行代码 for (int i = ...
- 读源码 | metisMenu侧边栏插件
————————————————————————————————————————————————————————— 使用方法 实现效果 引入文件 <link rel="styleshe ...
- DIV布局之position详解
相对定位和绝对定位 定位标签:position 包含属性:relative(相对) absolute(绝对) 1.position:relative; 如果对一个元素进行相对定位,首先它将出现在它所在 ...
- SDUT 2623 The number of steps (概率)
The number of steps Time Limit: 1000ms Memory limit: 65536K 有疑问?点这里^_^ 题目描述 Mary stands in a stra ...
- Android学习系列(19)--App离线下载
宜未雨而绸缪,毋临渴而掘井.----朱用纯<治家格言> 离线下载,在有网络的情况下下载服务器数据,以便无网络时也能阅读,就是离线阅读. 离线下载的功能点如下: ...
- Cookies揭秘 [Asp.Net, Javascript]
一,前言 Cookies想必所有人都了解, 但是未必所有人都精通.本文讲解了Cookies的各方面知识, 并且提出来了最佳实践.这是笔者在日常工作中的积累和沉淀. 二,基础知识 1.什么是Cookie ...
- ios 中的tintColor
在iOS 7后,UIView新增加了一个tintColor属性,这个属性定义了一个非默认的着色颜色值,其值的设置会影响到以视图为根视图的整个视图层次结构.它主要是应用到诸如app图标.导航栏.按钮等一 ...
- python练习笔记——map | sum | pow 的应用
1 函数简要 map 函数 | sum 函数 | pow函数 | lambda函数 2 简要计算 2.1 1^2 + 2^2 + 3^2 .....9^2 方法1 print([pow(x,2 ...
- 转:使用Mosquitto-Auth-Plugin对mqtt客户端进行验证
https://www.lixiaodong.com/?p=1631.安装需要的包sudo apt-get install libc-ares-dev libcurl4-openssl-dev lib ...