乐趣区

关于java:超时与重试浅析

前言

超时能够说是除了空指针咱们最相熟的异样了,从零碎的接入层,到服务层,再到数据库层等等都能看到超时的身影;超时很多状况下同时随同着重试,因为某些状况下比方网络抖动问题等,重试是能够胜利的;当然重试往往也会指定重试次数下限,因为如果程序的确存在问题,重试多少次都杯水车薪,那其实也是对资源的节约。

为什么要设置超时

对于开发人员来说咱们平时最常见的就是设置超时工夫,比方数据库超时设置、缓存超时设置、中间件客户端超时设置、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 中的超时重试机制,可用作为一个很好的参考。

感激关注

能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。

退出移动版