前言:
现在我们对“nginx超时重试”大体比较注重,看官们都想要分析一些“nginx超时重试”的相关文章。那么小编同时在网络上汇集了一些有关“nginx超时重试””的相关资讯,希望咱们能喜欢,我们一起来了解一下吧!前言
超时可以说是除了空指针我们最熟悉的异常了,从系统的接入层,到服务层,再到数据库层等等都能看到超时的身影;超时很多情况下同时伴随着重试,因为某些情况下比如网络抖动问题等,重试是可以成功的;当然重试往往也会指定重试次数上限,因为如果程序确实存在问题,重试多少次都无济于事,那其实也是对资源的浪费。
为什么要设置超时
对于开发人员来说我们平时最常见的就是设置超时时间,比如数据库超时设置、缓存超时设置、中间件客户端超时设置、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 errorConnectException:远程拒绝连接(例如,没有进程正在侦听远程地址/端口);java.net.ConnectException: Connection refusedSocketTimeoutException:套接字读取(read)或接受(accept)发生超时;java.net.SocketTimeoutException: connect timed out java.net.SocketTimeoutException: Read timed outUnknownHostException:指示无法确定主机的IP地址;java.net.UnknownHostException: localhost1NoRouteToHostException:连接到远程地址和端口时出错。通常,由于防火墙的介入,或者中间路由器关闭,无法访问远程主机;java.net.NoRouteToHostException: Host unreachable java.net.NoRouteToHostException: Address not availableSocketException:创建或访问套接字时出错;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中的超时重试机制,可用作为一个很好的参考。