龙空技术网

超时与重试浅析

ksfzhaohui 293

前言:

现在我们对“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中的超时重试机制,可用作为一个很好的参考。

标签: #nginx超时重试 #nginx自动重试