失败重试次数一般设置几次(超时与重试浅析)

超时可以说是除了空指针我们最熟悉的异常了,从系统的接入层,到服务层,再到数据库层等等都能看到超时的身影;超时很多情况下同时伴随着重试,因为某些情况下比如网络抖动问题等,重试是可以成功的;当然重试往往也会指定重试次数上限,因为如果程序确实存在问题,重试多少次都无济于事,那其实也是对资源的浪费,下面我们就来说一说关于失败重试次数一般设置几次?我们一起去了解并探讨一下这个问题吧!

失败重试次数一般设置几次(超时与重试浅析)

失败重试次数一般设置几次

前言

超时可以说是除了空指针我们最熟悉的异常了,从系统的接入层,到服务层,再到数据库层等等都能看到超时的身影;超时很多情况下同时伴随着重试,因为某些情况下比如网络抖动问题等,重试是可以成功的;当然重试往往也会指定重试次数上限,因为如果程序确实存在问题,重试多少次都无济于事,那其实也是对资源的浪费。

为什么要设置超时

对于开发人员来说我们平时最常见的就是设置超时时间,比如数据库超时设置、缓存超时设置、中间件客户端超时设置、HttpClient超时设置、可能还有业务超时;为什么要设置超时时间,因为如果不设置超时时间,可能因为某个请求无法即时响应导致整个链路处于长时间等待状态,这种请求如果过多,直接导致整个系统瘫痪,抛出超时异常其实也是及时止损;纵观各种超时时间设置,可以看到其实大多数都是围绕网络超时,而网络超时不得不提Socket超时设置。

Socket超时

Socket是作为网络通信最基础的类,要进行通信基本分为两步:

  • 建立连接:在进行读写消息之前必须首先建立连接;连接阶段会有连接超时设置ConnectTimeout;
  • 读写操作:读写也就是双方正式交换数据,此阶段会有读写超时设置ReadTimeOut;
连接超时

Socket提供的connect方法提供了连接超时设置:

public void connect(SocketAddress endpoint) throws IOException public void connect(SocketAddress endpoint, int timeout) throws IOException

不设置timeout默认是0,理论上应该是没有时间限制,经测试默认还是有一个时间限制大概21秒左右;

在建立连接的时候可能会抛出多种异常,常见的比如:

  • ProtocolException:基础协议中存在错误,例如TCP错误;java.net.ProtocolException: Protocol error
  • ConnectException:远程拒绝连接(例如,没有进程正在侦听远程地址/端口);java.net.ConnectException: Connection refused
  • SocketTimeoutException:套接字读取(read)或接受(accept)发生超时;java.net.SocketTimeoutException: connect timed out java.net.SocketTimeoutException: Read timed out
  • UnknownHostException:指示无法确定主机的IP地址;java.net.UnknownHostException: localhost1
  • NoRouteToHostException:连接到远程地址和端口时出错。通常,由于防火墙的介入,或者中间路由器关闭,无法访问远程主机;java.net.NoRouteToHostException: Host unreachable java.net.NoRouteToHostException: Address not available
  • SocketException:创建或访问套接字时出错;java.net.SocketException: Socket closed java.net.SocketException: connect failed

这里我们重点要讨论的是SocketTimeoutException,同时Connection refused也经常出现,这里做一个简单的对比

Connect timed out

本地可以直接使用一个不存在的ip尝试连接:

SocketAddress endpoint = new InetSocketAddress("111.1.1.1", 8080); socket.connect(endpoint, 2000);

尝试连接报如下错误:

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)

Connection refused

本地测试可以使用127.x.x.x来进行模拟,尝试连接报如下错误:

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)

对比
  • Connection refused:表示从本地客户端到目标IP地址的路由是正常的,但是该目标端口没有进程在监听,然后服务端拒绝掉了连接;127开头用作本地环回测试(loopback test)本主机的进程之间的通信,所以数据报不会发送给网络,路由都是正常的;
  • Connect timed out:超时的可能性比较多常见的如服务器无法ping通、防火墙丢弃了请求报文、网络间隙性问题等;
读写超时

Socket可以设置SoTimeout表示读写的超时时间,如果不设置默认为0,表示没有时间限制;可以简单做一个模拟,模拟服务器端业务处理延迟10秒,而客户端设置的读写超时时间为2秒:

Socket socket = new Socket(); SocketAddress endpoint = new InetSocketAddress("127.0.0.1", 8189); socket.connect(endpoint, 2000);//设置连接超时为2秒 socket.setSoTimeout(1000);//设置读写超时为1秒 InputStream inStream = socket.getInputStream(); inStream.read();//读取操作

因为服务器端做了延迟处理,所以超过客户端设置的读写超时时间,直接报如下错误:

java.net.SocketTimeoutException: Read timed out at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141) at java.net.SocketInputStream.read(SocketInputStream.java:224)

NIO超时

以上是基于传统Socket的超时配置,NIO提供的SocketChannel也同样存在超时的情况;NIO模式提供了阻塞模式和非阻塞模式,阻塞模式和传统的Socket是一样的,而且存在对应关系;而非阻塞模式并没有提供超时时间的设置;

阻塞模式

SocketChannel client = SocketChannel.open(); //阻塞模式 client.configureBlocking(true); InetSocketAddress endpoint = new InetSocketAddress("128.5.50.12", 8888); client.socket().connect(endpoint, 1000);

以上阻塞模式下可以通过client.socket()可以获取到SocketChannel对应的Socket,设置连接超时时间,报如下错误:

java.net.SocketTimeoutException at sun.nio.ch.SocketAdaptor.connect(SocketAdaptor.java:118)

非阻塞模式

SocketChannel client = SocketChannel.open(); // 非阻塞模式 client.configureBlocking(false); // select注册 Selector selector = Selector.open(); client.register(selector, SelectionKey.OP_CONNECT); InetSocketAddress endpoint = new InetSocketAddress("127.0.0.1", 8888); client.connect(endpoint);

同样模拟以上两种情况,报如下错误:

//连接超时异常 java.net.ConnectException: Connection timed out: no further infORMation at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717) //连接拒绝异常 java.net.ConnectException: Connection refused: no further information at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)

常见超时

了解了Socket超时,那么了解其他因为网络而引发的超时就简单多了,常见的网络读写超时设置包括:数据库客户端超时、缓存客户端超时、RPC客户端超时、HttpClient超时,网关层超时;以上几种情况其实都是以客户端的角度来进行的超时时间设置,像Web容器在服务器端也做了超时处理;当然除了网络相关的超时可能也有一些业务超时的情况,下面分别介绍;

网络超时

这里重点看一下客户端相关的超时设置,服务端重点看一下Web容器;

数据库客户端超时

以Mysql为例,最简单的超时时间设置只需要在url后面添加即可:

jdbc:mysql://localhost:3306/ds0?connectTimeout=2000&socketTimeout=200

connectTimeout:连接超时时间;

socketTimeout:读写超时时间;

除了数据库驱动本身提供的超时时间配置,我们一般都直接使用ORM框架,比如Mybatis等,这些框架本身也会提供相应的超时时间:

<setting name="defaultStatementTimeout" value="25"/>

defaultStatementTimeout:设置超时时间,它决定数据库驱动等待数据库响应的秒数。

缓存客户端超时

以Redis为例,使用Jedis为例,在创建连接的时候同样可以配置超时时间:

public Jedis(final String host, final int port, final int timeout)

这里只配置了一个超时时间,但其实连接和读写超时共用一个值,可以查看Connection源码:

public void connect() { if (!isConnected()) { try { socket = new Socket(); socket.setReuseAddress(true); socket.setKeepAlive(true); socket.setTcpNoDelay(true); socket.setSoLinger(true, 0); //timeout连接超时设置 socket.connect(new InetSocketAddress(host, port), timeout); //timeout读写超时设置 socket.setSoTimeout(timeout); outputStream = new RedisOutputStream(socket.getOutputStream()); inputStream = new RedisInputStream(socket.getInputStream()); } catch (IOException ex) { throw new JedisConnectionException(ex); } } }

RPC客户端超时

以Dubbo为例,可以直接在xml中配置超时时间:

<dubbo:consumer timeout="" >

默认时间为1000ms,Dubbo作为RPC框架,底层使用的是Netty等通信框架,但是Dubbo通过Future实现了自己的超时机制,可以直接查看DefaultFuture,部分代码如下所示:

// 内部锁 private final Lock lock = new ReentrantLock(); private final Condition done = lock.newCondition(); // 在指定时间内不能获取直接返回TimeoutException public Object get(int timeout) throws RemotingException { if (timeout <= 0) { timeout = Constants.DEFAULT_TIMEOUT; } if (!isDone()) { long start = System.currentTimeMillis(); lock.lock(); try { while (!isDone()) { done.await(timeout, TimeUnit.MILLISECONDS); if (isDone() || System.currentTimeMillis() - start > timeout) { break; } } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } if (!isDone()) { throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); } } return returnFromResponse(); }

HttpClient超时

HttpClient可以说是我们最常使用的Http客户端了,可以通过RequestConfig来设置超时时间:

RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(1000) .setConnectionRequestTimeout(3000).build();

其中可以配置三个超时时间分别是:

  • socketTimeout:连接建立成功,读写超时时间;
  • connectTimeout:连接超时时间;
  • connectionRequestTimeout:从连接管理器请求连接时使用的超时;
网关层超时

以常见的Nginx为例,作为代理转发,从下游Web服务器的角度来看,Nginx作为转发器其实就是客户端,同样需要配置连接、读写等超时时间:

server { listen 80; server_name localhost; location / { // 超时配置 proxy_connect_timeout 2s; proxy_read_timeout 2s; proxy_send_timeout 2s; //重试机制 proxy_next_upstream error timeout; proxy_next_upstream_tries 5; proxy_next_upstream_timeout 5; } }

相关超时时间配置:

  • proxy_connect_timeout:与后端服务器建立连接超时时间,默认60s;
  • proxy_read_timeout:从后端服务器读取响应的超时时间,默认60s;
  • proxy_send_timeout:往后端服务器发送请求的超时时间,默认60s;

Nginx作为代理服务器,同样提供了重试机制,对于上游服务器往往会配置多台来实现负载均衡,相关配置如下:

  • proxy_next_upstream:什么情况下需要请求下一台后端服务器进行重试,默认error timeout;
  • proxy_next_upstream_tries:重试次数,默认为0表示不限次数;
  • proxy_next_upstream_timeout:重试最大超时时间,默认为0表示不限次数;
服务端超时

以上几种情况我们都是站在客户端的角度,也是作为开发人员最常使用的超时配置,其实在服务器端也同样可以配置相应的超时时间,比如最常见的Web容器Tomcat、上面介绍的Nginx等,下面看一下Tomcat的相关超时配置:

<Connector connectionTimeout="20000" socket.soTimeout="20000" asyncTimeout="20000" disableUploadTimeout="20000" connectionUploadTimeout="20000" keepAliveTimeout="20000" />

  • connectionTimeout:连接器在接受连接后,指定时间内没有接收到请求URI行,则表示连接超时;
  • socket.soTimeout:从客户端读取请求数据的超时时间,默认同connectionTimeout;
  • asyncTimeout:异步请求的超时时间;
  • disableUploadTimeout和connectionUploadTimeout:文件上传使用的超时时间;
  • keepAliveTimeout:设置Http长连接超时时间;

更多配置:Tomcat8.5

业务超时

基本上我们用到的中间件都提供了超时设置,当然业务中某些情况也需要我们自己做超时处理,比如某个功能需要调用多个服务,每个服务都有自己的超时时间,但是此功能有个总的超时时间,这时候我们可以参考Dubbo使用Future来解决超时问题。

重试

重试往往伴随着超时一起出现,因为超时可能是因为某些特殊原因导致暂时性的请求失败,也就是说重试是有可能出现请求再次成功的;其实现在很多提供负载均衡的系统,不仅是在超时的时候重试,出现任何异常都会重试,比如类似Nginx的网关,RPC,MQ等;下面具体看看各种系统都是如何实现重试的;

RPC重试

RPC系统一般都会提供注册中心,服务提供方会提供多个节点,所以如果某个服务端节点异常,消费端会重新选择其他的节点;以Dubbo为例,提供了容错机制类FailoverClusterInvoker,默认会失败重试两次,具体重试是通过for循环来实现的:

for (int i = 0; i < len; i ) { try{ //负载均衡选择一个服务端 Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked); //执行 Result result = invoker.invoke(invocation); } catch (Throwable e) { //出现异常并不会退出 le = new RpcException(e.getMessage(), e); } }

以上通过for循环捕获异常来实现重试是一种比较好的方式,比在catch子句中再实现重试更方便;

MQ重试

很多消息系统都提供了重试机制比如ActiveMQ、RocketMQ、Kafka等;

ActiveMQ中的ActiveMQMessageConsumer类的rollback提供了重试机制,最大的重发次数DEFAULT_MAXIMUM_REDELIVERIES=6;

RocketMQ在消息量大,网络有波动的情况下,重试也是一个大概率事件;Producer中的setRetryTimesWhenSendFailed设置在同步方式下自动重试的次数,默认值为2;

网关重试

网关作为一个负载均衡器,其中一个核心功能就是重试机制,除了此机制外还有健康检测机制,事先把有问题的业务逻辑节点排除掉,这样也减少了重试的几率,重试本身也是很浪费时间的;Nginx相关重试的配置上节中已经介绍,这里不在重复;

HttpClient重试

HttpClient内部其实提供了重试机制,实现类RetryExec,默认重试次数为3次,代码部分如下:

for (int execCount = 1;; execCount ) { try { return this.requestExecutor.execute(route, request, context, execAware); } catch (final IOException ex) { // 重试异常检查 } }

  • 只有发生IOExecetion时才会发生重试;
  • InterruptedIOException、UnknownHostException、ConnectException、SSLException,发生这4种异常不重试;

可以发现SocketTimeoutException继承于InterruptedIOException,所以并不会重试;

定时器重试

之前有遇到过需要通知外部系统的情况,因为实时性没那么高,而且很多外部系统都不是那么稳定,不一定什么时候就进入维护中;采用数据库 定时器的方式来进行重试,每条通知记录会保存下一次重试的时间(重试时间采用递增的方式),定时器定期查找哪些下一次重试时间在当前时间内的,如果成功更新状态为成功,如果失败更新下一次重试时间,重试次数 1,当然也会设置最大重试值;

注意点

当然重试也需要注意是查询类的还是更新类的,如果是查询类的多次重试并不影响结果,如果是更新类的,需要做好幂等性。

总结

合理的设置超时与重试机制,是保证系统高可用的前提之一;太多故障因为不合理的设置超时时间导致的,所以我们在开发过程中一定要注意;另外一点就是可用多看看一些中间件的源码,很多解决方案都可用在这些中间件中找到答案,比如Dubbo中的超时重试机制,可用作为一个很好的参考。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页