共计 10035 个字符,预计需要花费 26 分钟才能阅读完成。
前言
超时能够说是除了空指针咱们最相熟的异样了,从零碎的接入层,到服务层,再到数据库层等等都能看到超时的身影;超时很多状况下同时随同着重试,因为某些状况下比方网络抖动问题等,重试是能够胜利的;当然重试往往也会指定重试次数下限,因为如果程序的确存在问题,重试多少次都杯水车薪,那其实也是对资源的节约。
为什么要设置超时
对于开发人员来说咱们平时最常见的就是设置超时工夫,比方数据库超时设置、缓存超时设置、中间件客户端超时设置、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 源码、架构、算法和面试。