前言
超时能够说是除了空指针咱们最相熟的异样了,从零碎的接入层,到服务层,再到数据库层等等都能看到超时的身影;超时很多状况下同时随同着重试,因为某些状况下比方网络抖动问题等,重试是能够胜利的;当然重试往往也会指定重试次数下限,因为如果程序的确存在问题,重试多少次都杯水车薪,那其实也是对资源的节约。
为什么要设置超时
对于开发人员来说咱们平时最常见的就是设置超时工夫,比方数据库超时设置、缓存超时设置、中间件客户端超时设置、HttpClient超时设置、可能还有业务超时;为什么要设置超时工夫,因为如果不设置超时工夫,可能因为某个申请无奈即时响应导致整个链路处于长时间期待状态,这种申请如果过多,间接导致整个零碎瘫痪,抛出超时异样其实也是及时止损;纵观各种超时工夫设置,能够看到其实大多数都是围绕网络超时,而网络超时不得不提Socket超时设置。
Socket超时
Socket是作为网络通信最根底的类,要进行通信根本分为两步:
- 建设连贯:在进行读写音讯之前必须首先建设连贯;连贯阶段会有连贯超时设置ConnectTimeout;
- 读写操作:读写也就是单方正式替换数据,此阶段会有读写超时设置ReadTimeOut;
连贯超时
Socket提供的connect办法提供了连贯超时设置:
public void connect(SocketAddress endpoint) throws IOExceptionpublic 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 outjava.net.SocketTimeoutException: Read timed out
UnknownHostException:批示无奈确定主机的IP地址;
java.net.UnknownHostException: localhost1
NoRouteToHostException:连贯到近程地址和端口时出错。通常,因为防火墙的染指,或者两头路由器敞开,无法访问近程主机;
java.net.NoRouteToHostException: Host unreachablejava.net.NoRouteToHostException: Address not available
SocketException:创立或拜访套接字时出错;
java.net.SocketException: Socket closedjava.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源码、架构、算法和面试。