背景

本文基于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重试的坑以及重试代码解析的更多相关文章

  1. 客户端负载均衡Ribbon之三:AvailabilityFilteringRule的坑(Spring Cloud Finchley.SR2)

    我们项目配置了AvailabilityFilteringRule作为所有Ribbon调用的负载均衡规则,它有那些坑呢(理解歧义和注意点)? 首先来看com.netflix.loadbalancer.A ...

  2. Feign Ribbon Hystrix 三者关系 | 史上最全, 深度解析

    史上最全: Feign Ribbon Hystrix 三者关系 | 深度解析 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -25[ 博客园 总入口 ] 前言 疯狂创客圈(笔者尼恩创建的 ...

  3. FastClick 填坑及源码解析

    最近产品妹子提出了一个体验issue —— 用 iOS 在手Q阅读书友交流区发表书评时,光标点击总是不好定位到正确的位置: 如上图,具体表现是较快点击时,光标总会跳到 textarea 内容的尾部.只 ...

  4. 开坑Java编写Json解析器,简明教程

    https://zhuanlan.zhihu.com/p/22460835?refer=json-tutorial 课程不是我原创,我打算照他的这个C版本来重写一遍Java的,打算用面向对象的方式来编 ...

  5. 我的踩坑之旅-代码不规范引发的“bug”

    今早公司上班,老大跟我说有一个服务老是上线,下线,问我啥情况.我回想了下我的项目部署,觉得不可能会出现这个问题呀.然后各种鼓捣,倒腾了一个早上,终于找出了罪魁祸首. 场景:我们的服务部署在亚马逊上.我 ...

  6. 史上最坑 idea 更改代码不生效

    原来, 如果本地多次调整过系统时间,那么gradle 的缓存 会缓存 你的 上次编译时间再未来,那么你再怎么编译,都很难生效,即使删除了生成的字节码目录. 然后invalidate caches/re ...

  7. [爬坑记录] Qt 代码卡住 不发信号 不触发槽

    先让我激动一会儿 [捂脸] 最近在用Qt做个程序 用来参加比赛 期间总共遇到两次如标题的问题 也即是 莫名其妙的不触发槽函数了 而且原因也不一样 {先说明 我学习Qt依旧只是入门级 也许入不了大佬法眼 ...

  8. Zuul + Ribbon 脱离Eureka完成负载均衡+重试机制

    Zuul + Ribbon 脱离Eureka完成负载均衡+重试机制 因为没有注册中心,所以需要网关对下游服务做负载均衡,然后果断集成Ribbon.中间遇到很多坑,最后终于解决了. 其实Ribbon里面 ...

  9. 为Spring Cloud Ribbon配置请求重试(Camden.SR2+)

    当我们使用Spring Cloud Ribbon实现客户端负载均衡的时候,通常都会利用@LoadBalanced来让RestTemplate具备客户端负载功能,从而实现面向服务名的接口访问. 下面的例 ...

随机推荐

  1. linux下sar tool command note

    linux下的sar工具简介 我习惯使用的命令是 : sar  -r  -f   /var/log/sa/sa24 sar 既能报告当前数据,也能报告历史数据 不带选项执行会以10分钟为间隔报告自午夜 ...

  2. .NET 垃圾回收机制要点整理

    1. .NET资源分托管资源和非托管资源,对于托管资源,.NET GC可以很好的回收无用的垃圾,而对于非托管(例如文件访问,网络访问等)需要手动清理垃圾(显式释放). 2. 非托管资源的释放,.NET ...

  3. Adaptive Thresholding & Otsu’s Binarization

    Adaptive Thresholding Adaptive Method - It decides how thresholding value is calculated. cv2.ADAPTIV ...

  4. 使用JDK自带的Stax操作XML

    操作的books.xml <?xml version="1.0" encoding="UTF-8"?> <bookstore> < ...

  5. 保护HTTP的安全

    #如果没有严格的限制访问的权限,公司放在服务器上的重要文档就存在隐患,web需要有一些安全的http形式: #安全方法: #基本认证.摘要认证.报文完整性检查都是一些轻量级的方法,但还不够强大,下面介 ...

  6. STL容器 erase的使用陷井

    http://www.cppblog.com/beautykingdom/archive/2008/07/09/55760.aspx?opt=admin 在STL(标准模板库)中经常会碰到要删除容器中 ...

  7. PHP-学习大规模高并发Web系统架构及开发推荐书籍

    以下书籍内容涵盖大型网站开发中几个关键点:高可用.高性能.分布式.易扩展.如果想对大规模高并发Web系统架构及开发有很系统的学习,可以阅读以下书籍,欢迎补充! 一.<Linux企业集群—用商用硬 ...

  8. springmvc+spring框架

    jar包 com.springsource.javax.validation-1.0.0.GA.jar com.springsource.org.aopalliance-1.0.0.jar com.s ...

  9. 最新最全的iOS手机支付总结

    关于手机支付,我想简单总结一下,我想主要分成三大类: 第一类,就是我们最常见的应用内支付(IAP),例如APPStore里面我们可以付费下载一些APP或者游戏. 第二类,就是我们经常使用第三方支付,例 ...

  10. ASP.NET自定义Web服务器控件-DropDownList/Select下拉列表控件

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; usin ...