超时与重试机制

网上发现这篇好文章,这里记录学习。

介绍

      在实际开发过程中,笔者见过太多故障是因为超时没有设置或者设置的不对而造成的。而这些故障都是因为没有意识到超时设置的重要性而造成的。如果应用不设置超时,则可能会导致请求响应慢,慢请求累积导致连锁反应,甚至应用雪崩。而有些中间件或者框架在超时后会进行重试(如设置超时重试两次),读服务天然适合重试,但写服务大多不能重试(如写订单,如果写服务是幂等,则重试是允许的),重试次数太多会导致多倍请求流量,即模拟了DDoS攻击,后果可能是灾难,因此,务必设置合理的重试机制,并且应该和熔断、快速失败机制配合。在进行代码Review时,一定记得Review超时与重试机制

      本文主要从Web应用/服务化应用的角度出发介绍如何设置超时与重试(系统层面的超时设置本文没有涉及),而Web应用需要在如下链条中设置超时与重试机制。

 

      从上图来看,在整个链条中的每一个点都要考虑设置超时与重试机制。而其中最重要的超时设置是网络连接/读/写的超时时间设置

本文将按照如下分类进行超时与重试机制的讲解。

  • 代理层超时与重试:如Haproxy、Nginx、Twemproxy,这些组件实现代理功能,如Haproxy和Nginx可以实现请求的负载均衡。而Twemproxy可以实现Redis的分片代理。需要设置代理与后端真实服务器之间的网络连接/读/写超时时间。
  • Web容器超时:如Tomcat、Jetty等,提供HTTP服务运行环境的。需要设置客户端与容器之间的网络连接/读/写超时时间,和在此容器中默认Socket网络连接/读/写超时时间。
  • 中间件客户端超时与重试:如JSF(京东SOA框架)、Dubbo、JMQ(京东消息中间件)、CXF、Httpclient等,需要设置客户的网络连接/读/写超时时间与失败重试机制。
  • 数据库客户端超时:如Mysql、Oracle,需要分别设置JDBC Connection、Statement的网络连接/读/写超时时间。事务超时时间,获取连接池连接等待时间。
  • NoSQL客户端超时:如Mongo、Redis,需要设置其网络连接/读/写超时时间,获取连接池连接等待时间。
  • 业务超时:如订单取消任务、超时活动关闭。还有如通过Future#get(timeout,unit)限制某个接口的超时时间。
  • 前端Ajax超时:浏览器通过Ajax访问时的网络连接/读/写超时时间。

从如上分类可以看出,其中最重要的超时设置是网络相关的超时设置。

一、代理层超时与重试

对于代理层我们以Nginx和Twemproxy案例来讲解。首先,看下Nginx的相关超时设置。

1. Nginx

      Nginx主要有四类超时设置:客户端超时设置、DNS解析超时设置、代理超时设置,如果使用ngx_lua,则还有lua相关的超时设置。

(1) 客户端超时设置

      对于客户端超时主要设置有读取请求头超时时间、读取请求体超时时间、发送响应超时时间、长连接超时时间。通过客户端超时设置避免客户端恶意或者网络状况不佳造成连接长期占用,影响服务端的可处理的能力。

  • client_header_timeout time:设置读取客户端请求头超时时间,默认为60s,如果在此超时时间内客户端没有发送完请求头,则响应408(RequestTime-out)状态码给客户端。
  • client_body_timeout time:设置读取客户端内容体超时时间,默认为60s,此超时时间指的是两次成功读操作间隔时间,而不是发送整个请求体的超时时间,如果在此超时时间内客户端没有发送任何请求体,则响应408(RequestTime-out)状态码给客户端。
  • send_timeout time:设置发送响应到客户端的超时时间,默认为60s,此超时时间指的也是两次成功写操作间隔时间,而不是发送整个响应的超时时间。如果在此超时时间内客户端没有接收任何响应,则Nginx关闭此连接。
  • keepalive_timeout timeout [header_timeout]:设置HTTP长连接超时时间,其中,第一个参数timeout是告诉Nginx长连接超时时间是多少,默认为75s。第二个参数header_timeout是用于设置响应头“Keep-Alive: timeout=time”,即告知客户端长连接超时时间。两个参数可以不一样,“Keep-Alive:timeout=time”响应头可以在Mozilla和Konqueror系列浏览器起作用,而MSIE长连接默认大约为60s,而不会使用“Keep-Alive: timeout=time”。如Httpclient框架会使用“Keep-Alive: timeout=time”响应头的超时(如果不设置默认,则认为是永久)。如果timeout设置为0,则表示禁用长连接。

    此参数要配合keepalive_disable 和keepalive_requests一起使用

    keepalive_disable 表示禁用哪些浏览器的长连接,默认值为msie6,即禁用一些老版本的MSIE的长连接支持。

    keepalive_requests参数作用是一个客户端可以通过此长连接的请求次数,默认为100。

首先,浏览器在请求时会通过如下请求头告知服务器是否支持长连接。

http/1.0默认是关闭长连接的,需要添加HTTP请求头“Connection:Keep-Alive”才能启用。

而http/1.1默认启用长连接,需要添加HTTP请求头“Connection: close”才关闭。

 

接着,如果Nginx设置keepalive_timeout 5s,则浏览器会收到如下响应头。

下图是wireshark抓包,可以看到后两次请求没有三次握手。

如果Nginx设置keepalive_timeout 10s 10s,则浏览器会收到如下响应头。

服务器端会在10s后发送FIN主动关闭连接。

     如果Nginx设置keepalive_timeout为75s 30s。

     如下是Chrome浏览器的Wireshark抓包,在45秒时,Chrome发送了TCPKeep-Alive来保活TCP连接,在第57秒时,浏览器又发出了一次请求。而132秒时,Nginx发出了FIN来关闭连接(75秒连接没活跃了)。

如下是IE浏览器抓包数据,在请求后第65秒左右时,浏览器重置了连接。

可以看出不同浏览器超时处理方式不一样,而HTTP响应头“Keep-Alive: timeout=30”对Chrome和IE都没有起作用。

接着,如果keepalive_timeout 0,则浏览器会收到如下响应头。

      对于客户端超时设置,要根据实际场景来决定,如果是短连接服务,则可以考虑设置的短一些,如果是文件上传,则需要考虑设置的时间长一些。另外,笔者见过很多人长连接并没有配置正确,建议配置完成后通过抓包查看长连接是否起作用了。keepalive_timeout和keepalive_requests是控制长连接的两个维度,只要其中一个到达设置的阈值连接就会被关闭。

(2) DNS解析超时设置

      resolver_timeout 30s:设置DNS解析超时时间,默认为30s。其配合resolver address ... [valid=time]进行DNS域名解析。当在Nginx中使用域名时,就需要考虑设置这两个参数。在社区版Nginx中采用如下配置。

  1. upstream backend { 
  2.     server c0.3.cn; 
  3.     server c1.3.cn; 

      如上两个域名会在Nginx解析配置文件的阶段被解析成IP地址并记录到upstream上,当这两个域名对应的IP地址发生变化时,该upstream不会更新。Nginx商业版是支持动态更新的。

一种简单办法是使用如下方式,每次都会动态解析域名,这种情况在多域名情况下比较麻烦,实现就不优雅了。

  1. location /test { 
  2.    proxy_pass http://c0.3.cn; 

如果使用Openresty,则可以通过Lua库lua-resty-dns进行DNS解析。

  1. localresolver = require "resty.dns.resolver" 
  2.   local r, err = resolver:new{ 
  3.       nameservers = {"8.8.8.8",{"8.8.4.4", 53} }, 
  4.       retrans = 5,  -- 5 retransmissions on receive timeout 
  5.       timeout = 2000,  -- 2 sec 
  6.   } 

当使用Nginx 1.5.8、1.7.4及遇到

  1. could not be resolved(110:Operation timed out); 

或者

  1. wrong ident 37278 response for ***.jd.local, expected 33517 
  2. unexpected response for ***.jd.local 

可能是遇到了如下BUG(http://nginx.org/en/CHANGES-1.6、http://nginx.org/ en/CHANGES-1.8)。

  1. Bugfix: requests might hang if resolver was usedand a timeout 
  2.       occurred during a DNS request. 

请考虑升级到Nginx 1.6.2、1.7.5或者在Nginx本机部署dnsmasq提升DNS解析性能。

(3) 代理超时设置

Nginx配置如下所示。

  1. upstream backend_server { 
  2.     server 192.168.61.1:9080 max_fails=2 fail_timeout=10s weight=1; 
  3.     server 192.168.61.1:9090 max_fails=2 fail_timeout=10s weight=1; 
  4.  
  5. server { 
  6.     …… 
  7.     location /test { 
  8.        proxy_connect_timeout 5s; 
  9.        proxy_read_timeout 5s; 
  10.        proxy_send_timeout 5s; 
  11.   
  12.        proxy_next_upstream error timeout; 
  13.        proxy_next_upstream_timeout 0; 
  14.        proxy_next_upstream_tries 0; 
  15.   
  16.        proxy_pass http://backend_server; 
  17.        add_header upstream_addr $upstream_addr; 
  18.     } 

backend_server定义了两个上游服务器192.168.61.1:9080(返回hello)和192.168.61.1:9090(返回hello2)。

如上指令主要有三组配置:网络连接/读/写超时设置、失败重试机制设置、upstream存活超时设置。

网络连接/读/写超时设置。

  • proxy_connect_timeout time:与后端/上游服务器建立连接的超时时间,默认为60s,此时间不超过75s。
  • proxy_read_timeout time:设置从后端/上游服务器读取响应的超时时间,默认为60s,此超时时间指的是两次成功读操作间隔时间,而不是读取整个响应体的超时时间,如果在此超时时间内上游服务器没有发送任何响应,则Nginx关闭此连接。
  • proxy_send_timeout time:设置往后端/上游服务器发送请求的超时时间,默认为60s,此超时时间指的是两次成功写操作间隔时间,而不是发送整个请求的超时时间,如果在此超时时间内上游服务器没有接收任何响应,则Nginx关闭此连接。

      对于内网高并发服务,请根据需要调整这几个参数,比如内网服务TP999为1s,可以将连接超时设置为100~500毫秒,而读超时可以为1.5~3秒左右。

失败重试机制设置。

  1. proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 |http_403 | http_404 | non_idempotent | off ...: 

      配置什么情况下需要请求下一台上游服务器进行重试。默认为“error timeout”。

      error表示与上游服务器建立连接、写请求或者读响应头出错。timeout表示与上游服务器建立连接、写请求或者读响应头超时。invalid_header表示上游服务器返回空的或错误的响应头。http_XXX表示上游服务器返回特定的状态码。non_idempotent表示RFC-2616定义的非幂等HTTP方法(POST、LOCK、PATCH),也可以在失败后重试下一台上游服务器(即默认幂等方法GET、HEAD、PUT、DELETE、OPTIONS、TRACE才可以重试)。off表示禁用重试。

重试不能无限制进行,因此,需要如下两个指令控制重试次数和重试超时时间。

  • proxy_next_upstream_tries number:设置重试次数,默认0表示不限制,注意此重试次数指的是所有请求次数(包括第一次和之后的重试次数之和)。
  • proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。

      即在proxy_next_upstream_timeout时间内允许proxy_next_upstream_tries次重试。如果超过了其中一个设置,则Nginx也会结束重试并返回客户端响应(可能是错误码)。

      如下配置表示当error/timeout时重试upstream中的下一台上游服务器,如果重试的总时间超出了6s或者重试了1次,则表示重试失败(因为之前已经请求一次了,所以还能重试一次),Nginx结束重试并返回客户端响应。

  1. proxy_next_upstream error timeout; 
  2. proxy_next_upstream_timeout 6s; 
  3. proxy_next_upstream_tries 2; 

(4) upstream存活超时设置

      max_fails和fail_timeout:配置什么时候Nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用/不存活。并在接下来的fail_timeout时间内从upstream摘掉该节点(即请求不会转发到该上游服务器)

      什么情况下被认定为失败呢?其由 proxy_next_upstream定义,不过,不管 proxy_next_upstream如何配置,error, timeout and invalid_header 都将被认为是失败。

      如server 192.168.61.1:9090 max_fails=2 fail_timeout=10s;表示在10s内如果失败了2次,则在接下来的10s内认定该节点不可用/不存活。这种存活检测机制是只有当访问该上游服务器时,采取惰性检查,可以使用ngx_http_upstream_check_module配置主动检查。

     max_fails设置为0表示不检查服务器是否可用(即认为一直可用),如果upstream中仅剩一台上游服务器时,则该服务器是不会被摘除的,将从不被认为不可用。

(5) ngx_lua超时设置

当我们使用ngx_lua时,也请考虑设置如下网络连接/读/写超时。

  1. lua_socket_connect_timeout  100ms; 
  2. lua_socket_send_timeout    200ms; 
  3. lua_socket_read_timeout    500ms; 

在使用lua时,我们会按照如下策略进行重试。

  1. if (status == 502 or status == 503 or status ==504) and request_time < 200 then 
  2.     resp =capture(proxy_uri) 
  3.     status =resp.status 
  4.     body =resp.body 
  5.    request_timerequest_time = request_time + tonumber(var.request_time) * 1000 
  6. end 

即如果状态码是500/502/503/504时,并且该次请求耗时在200毫秒以内,则我们进行一次重试。

2. Twemproxy

Twemproxy是Twitter开源的Redis和Memcache代理中间件,其目的是减少与后端缓存服务器的连接数。

  • timeout:表示与后端服务器建立连接、接收响应的超时时间,默认永不超时。
  • server_retry_timeout和server_failure_limit:当开启auto_eject_hosts,即当后端服务器不可用时自动摘除这些节点并在一定时间后进行重试。server_failure_limit设置连续失败多少次后将节点临时摘除,server_retry_timeout设置摘除节点后等待多久进行重试,从而保证不永久性的将节点摘除。

二、Web容器超时

笔者生产环境用的Java Web容器是Tomcat,本部分将以Tomcat8.5作为例子进行讲解。

  • connectionTimeout:配置与客户端建立连接超时时间,从接收到连接后在配置的时间内还没有接收到客户端请求行时,将被认定为连接超时,默认为60000(60s)。
  • socket.soTimeout:从客户端读取请求数据的超时时间,默认同connectionTimeout,NIO and NIO2 支持该配置。
  • asyncTimeout:Servlet 3异步请求的超时时间,默认为30000(30s)。
  • disableUploadTimeout 和connectionUploadTimeout:当配置disableUploadTimeout为false时(默认为true,和connectionTimeout一样),文件上传将使用connectionUploadTimeout作为超时时间。
  • keepAliveTimeout和maxKeepAliveRequests:和Nginx配置类似。keepAliveTimeout默认为connectionTimeout,配置-1表示永不超时。maxKeepAliveRequests默认为100。

三、中间件客户端超时与重试

JSF是京东自研的SOA框架,主要有三个组件:注册中心、服务提供端、服务消费端。

  • 首先是服务提供端/消费端与注册中心之间的进行服务注册/发现时可以配置timeout(调用注册中心超时时间,默认为5s)和connectTimeout(连接注册中心的超时时间,默认为20s)。
  • 服务提供端可以配置timeout(服务端调用超时,默认为5s)。
  • 服务消费端可以配置timeout(调用端调用超时时间,默认为5s),connectTimeout(建立连接超时时间,默认为5s),disconnectTimeout(断开连接/等待结果超时时间,默认为10s),reconnect(调用端重连死亡服务端的间隔,配置小于0表示不重连,默认为10s),heartbeat(调用端往服务端发心跳包间隔,配置小于0代表不发送,默认为30s),retries(失败后重试次数,默认0不重试)。

Dubbo也有类似的配置,在此就不阐述了。

JMQ是京东消息中间件,主要有四个组件:注册中心、Broker(JMQ的服务端实例,生产和消费消息都跟它交互)、生产者、消费者。

  • 首先是生产者/消费者与Broker进行发送/接收消息时可以配置connectionTimeout(连接超时)、sendTimeout(发送超时)、soTimeout(读超时)。
  • 生产者可以配置retryTimes(发送失败后的重试次数,默认为2次)。
  • 消费者可以配置pullTimeout(长轮询超时时间,即拉取消息超时时间)、maxRetrys(最大重试次数,对于消费者要允许无限制重试,即一直拉取消息)、retryDelay(重试延迟,通过exponential配置延迟增加倍数一直增加到maxRetryDelay)、maxRetryDelay(最大重试延迟)。消费者还需要配置应答超时时间(服务端需要等待客户端返回应答才能移除消息,如果没有应答返回,则会等待应答超时,在这段时间内锁定的消息不能被消费,必须等待超时后才能被消费)。

对于消息中间件我们实际应用中关注超时配置会少一些,因为生产者默认配置了重试次数,可能会存在重复消息,消费者需要进行去重处理。

CXF可以通过如下方式配置CXF客户端连接超时、等待响应超时和长连接。

  1. HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy(); 
  2. httpClientPolicy.setConnectionTimeout(30000);//默认为30s 
  3. httpClientPolicy.setReceiveTimeout(60000); //默认为60s 
  4. httpClientPolicy.setConnection(ConnectionType.KEEP_ALIVE);//默认为Keep- Alive 
  5. ((HTTPConduit)client.getConduit()).setClient(httpClientPolicy); 

Httpclient 4.2.x可以通过如下代码配置网络连接、等待数据超时时间。

  1. HttpParams params = new BasicHttpParams(); 
  2. //设置连接超时时间 
  3. Integer CONNECTION_TIMEOUT = 2 * 1000;    //设置请求超时2秒钟 
  4. Integer SO_TIMEOUT = 2 * 1000;        //设置等待数据超时时间2秒钟 
  5. Long CONN_MANAGER_TIMEOUT = 1L * 1000;        //定义了当从ClientConnectionManager中检索ManagedClientConnection实例时使用的毫秒级的超时时间 
  6. params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,CONNECTION_TIMEOUT); 
  7. params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT,SO_TIMEOUT); 
  8. //在提交请求之前,测试连接是否可用 
  9. params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK,true); 
  10. //这个参数期望得到一个java.lang.Long类型的值。如果这个参数没有被设置,则连接请求就不会超时(无限大的超时时间) 
  11. params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT,CONN_MANAGER_TIMEOUT); 
  12. PoolingClientConnectionManager conMgr = new PoolingClientConnectionManager(); 
  13. conMgr.setMaxTotal(200);//设置最大连接数 
  14. //是路由的默认最大连接(该值默认为2),限制数量实际使用DefaultMaxPerRoute并非MaxTotal 
  15. //设置过小,无法支持大并发(ConnectionPoolTimeoutException: Timeout waiting for connection frompool),路由是对maxTotal的细分 
  16. conMgr.setDefaultMaxPerRoute(conMgr.getMaxTotal());//(目前只有一个路由,因此让他等于最大值) 
  17.   
  18. //设置访问协议 
  19. conMgr.getSchemeRegistry().register(new Scheme("http",80, PlainSocketFactory. getSocketFactory())); 
  20. conMgr.getSchemeRegistry().register(new Scheme("https",443, SSLSocketFactory. getSocketFactory())); 
  21. httpClient = newDefaultHttpClient(conMgr, params); 
  22. httpClient.setHttpRequestRetryHandler(newDefaultHttpRequestRetryHandler(0, false)); 

      因为我们使用http connection连接池,所以需要配置CONN_MANAGER_TIMEOUT,表示从连接池获取http connection的超时时间。

      此处还通过httpClient.setHttpRequestRetryHandler(newDefaultHttpRequestRetry Handler(0, false))配置了请求重试策略(默认重试3次)。当执行请求时遇到异常时会调用retryRequest来判断是否进行重试,而retryRequest在以下情况不会进行重试:达到重试次数、服务器不可达、连接被拒绝、连接终止、请求已发送。而幂等HTTP方法的请求、requestSentRetryEnabled=true且请求还未成功发送时可以重试。

      如果响应是503错误状态码时,如上重试机制是不可用的,则可以考虑使用AutoRetryHttpClient客户端,其可以配置ServiceUnavailableRetryStrategy,默认实现为DefaultServiceUnavailableRetryStrategy,可以配置重试次数maxRetries和重试间隔retryInterval。每次重试之前都会等待retryInterval毫秒时间。

假设我们服务有多个机房提供,其中一个机房服务出现问题时应该自动切到另一个机房,可以考虑使用如下方法。

  1. public static String get(List<String> apis, Object[] args, String encoding,Header[] headers, Integer timeout) throws Exception { 
  2.     Stringresponse = null; 
  3.     for(String api : apis) { 
  4.         String uri =UriComponentsBuilder.fromHttpUrl(api).buildAndExpand(args). toUriString(); 
  5.        response = HttpClientUtils.getDataFromUri(uri, encoding, headers,timeout); 
  6.         //如果失败了,重试一次 
  7.         if(Objects.equal(response, HTTP_ERROR)){ 
  8.             continue; 
  9.         } 
  10.         //如果域名解析失败重试 
  11.         if(Objects.equal(response,HTTP_UNKNOWN_HOST_ERROR)) { 
  12.            response = HTTP_ERROR; //掉用方根据这个判断是否有问题 
  13.             continue; 
  14.         } 
  15.         if(Objects.equal(response,HTTP_SOCKET_TIMEOUT_ERROR)) { 
  16.            response = HTTP_ERROR; //调用方根据这个判断是否有问题 
  17.             continue; 
  18.         } 
  19.         return response; 
  20.     } 
  21.     return response; 

参数传入不同机房的API即可,当其中一个不可用自动重试另一个机房的API。

 

四、数据库客户端超时

在使用数据库客户端时,我们会使用数据库连接池,数据库连接池可以进行如下超时设置。

  1. <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> 
  2.     <!--Statement默认超时时间 --> 
  3.     <property name="defaultQueryTimeout" value="3"/> 
  4.   
  5.     <!-- 另外可以通过如下配置来配置socket连接/读超时:--> 
  6.     <property name="connectionProperties" 
  7.              value="connectTimeout=2000; socketTimeout=2000 "/> 
  8.     <!--这个是等待获取连接池连接时间,也不要太大,比如设置在500毫秒--> 
  9.     <property name="maxWaitMillis" value="500"/> 
  10. </bean> 

● 网络连接/读超时:使用connectionProperties配置Mysql超时时间,如果是Oracle则可以通过如下配置。

  1. <property name="connectionProperties" 
  2. value="oracle.net.CONNECT_TIMEOUT=2000;oracle.jdbc.ReadTimeout=2000"/> 

● 默认Statement超时时间,通过defaultQueryTimeout配置,单位是秒。

● 从连接池获取连接的等待时间,通过maxWaitMillis配置。

● Statement超时,如果使用ibatis,则可以通过如下方式配置Statement超时。

因此我们只需要如下配置。

  1. <settings cacheModelsEnabled="false"enhancementEnabled="true" 
  2. lazyLoadingEnabled="false"errorTracingEnabled="true" maxRequests="32" 
  3. defaultStatementTimeout="2"/> 

defaultStatementTimeout单位是秒,根据业务配置。如果数据库连接池配置了,则此处可以不用配置。

如果想只设置某个Statement的超时时间,则可以考虑:

  1. <insert……timeout="2"> 

如上配置其实最终会调用Statement.setQueryTimeout方法设置Statement超时时间。

● 事务超时是总Statement超时设置,比如我们使用Spring管理事务的话,可以使用如下方式配置全局默认的事务级别的超时时间。

  1. <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 
  2.   <propertynamepropertyname="dataSource" ref="dataSource" /> 
  3.   <propertynamepropertyname="defaultTimeout" value="3"/> 
  4. </bean> 

      这里我们分析下为什么说事务超时是Statement超时的总和,此处我们分析spring的DataSourceTransactionManager,首先开启事务时会调用其doBegin方法。

  1. //先获取@Transactional定义的timeout,如果没有,则使用defaultTimeout 
  2. int timeout =determineTimeout(definition); 
  3. if (timeout !=TransactionDefinition.TIMEOUT_DEFAULT) {  
  4.    txObject.getConnectionHolder().setTimeoutInSeconds(timeout);  
  5. }  

      其中determineTimeout用来获取我们设置的事务超时时间,然后设置到ConnectionHolder对象上(其是ResourceHolder子类),接着看ResourceHolderSupport的setTimeoutInSeconds实现。

  1. public voidsetTimeoutInSeconds(int seconds) { 
  2.     setTimeoutInMillis(seconds* 1000); 
  3.   
  4. public voidsetTimeoutInMillis(long millis) { 
  5.     this.deadline = newDate(System.currentTimeMillis() + millis);   

大家可以看到,此处会设置一个deadline时间,用来判断事务超时时间,那什么时候调用呢?首先检查该类中的代码,会发现。

  1. public int getTimeToLiveInSeconds() { 
  2.     double diff = ((double) getTimeToLiveInMillis()) /1000; 
  3.     int secs = (int) Math.ceil(diff); 
  4.     checkTransactionTimeout(secs <= 0); 
  5.     return secs; 
  6.   
  7. public long getTimeToLiveInMillis() throwsTransactionTimedOutException{ 
  8.     if (this.deadline == null) { 
  9.         throw new IllegalStateException("No timeoutspecified for this resource holder"); 
  10.     } 
  11.     long timeToLive = this.deadline.getTime() -System.currentTimeMillis(); 
  12.     checkTransactionTimeout(timeToLive <= 0); 
  13.     return timeToLive; 
  14.   
  15. private void checkTransactionTimeout(booleandeadlineReached) throws TransactionTimedOutException { 
  16.     if (deadlineReached) { 
  17.         setRollbackOnly(); 
  18.        throw newTransactionTimedOutException("Transaction timed out: deadline was " +this.deadline); 
  19.     } 

      我们发现调用getTimeToLiveInSeconds和getTimeToLiveInMillis会检查是否超时,如果超时了,则标记事务需回滚,并抛出TransactionTimedOutException异常进行回滚。

      DataSourceUtils.applyTransactionTimeout会调用DataSourceUtils. applyTimeout, DataSourceUtils.applyTimeout代码如下。

  1. public static void applyTimeout(Statement stmt,DataSource dataSource, int timeout) throws SQLException { 
  2.     ConnectionHolder holder =         (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource); 
  3.     if (holder != null && holder.hasTimeout()){ 
  4.         // 计算剩余的事务超时时间覆盖Statement超时 
  5.        stmt.setQueryTimeout(holder.getTimeToLiveInSeconds()); 
  6.     } else if (timeout > 0) { 
  7.         //如果没有配置事务超时,则使用Statement超时 
  8.        stmt.setQueryTimeout(timeout); 
  9.     } 

      在stmt.setQueryTimeout(holder.getTimeToLiveInSeconds())时会调用getTimeToLiveIn Seconds(),这会检查事务是否超时。在JdbcTemplate中,执行SQL之前,会调用其applyStatementSettings方法,其将调用DataSourceUtils.applyTimeout(stmt,getDataSource(), getQueryTimeout())设置超时时间。

      此处有一个问题,如果设置了事务超时,Statement级别的就不起作用了,整体会使用事务超时覆盖Statement超时。

五、NoSQL客户端超时

对于MongoDB,我们使用的是spring-data-mongodb客户端,可以通过如下配置设置相关的超时时间。

  1. <mongo:mongo id="tryMongo"replica-set="${try.mongo.hostAndPorts}"> 
  2.    <mongo:options 
  3.            connections-per-host="${mongo.connectionsPerHost}" 
  4.            threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}" 
  5.            max-wait-time="${mongo.maxWaitTime}" 
  6.            connect-timeout="${mongo.connectTimeout}" 
  7.            socket-timeout="${mongo.socketTimeout}" 
  8.            socket-keep-alive="${mongo.socketKeepAlive}" 
  9.            auto-connect-retry="${mongo.autoConnectRetry}" /> 
  10. </mongo:mongo> 

我们曾经就遇到过因为不设置mongodb客户端timeout而导致服务响应慢的情况。

对于Redis,我们使用的是Jedis客户端,可以通过如下配置分配等待获取连接池连接的超时时间和网络连接/读超时时间。

  1. PoolJedisConnectionFactory connectionFactory = new PoolJedisConnectionFactory(); 
  2. connectionFactory.setMaxWaitMillis(maxWaitMillis); 
  3. connectionFactory.setTimeout(timeoutInMillis); 

Jedis在建立Socket时通过如下代码设置超时。

  1. this.socket.connect(new InetSocketAddress(this.host, this.port),this. timeout); 
  2. this.socket.setSoTimeout(this.timeout); 

      可以在JVM启动时通过添加-Dsun.net.client.defaultConnectTimeout=60000-Dsun.net.client.defaultReadTimeout=60000来配置默认全局的Socket连接/读超时。即如Httpclient、JDBC等,如果没有配置socket超时,则默认会使用该超时。

六、业务超时

      任务型:比如,订单超时未支付取消,超时活动自动关闭等,这属于任务型超时,可以通过Worker定期扫描数据库修改状态即可。还有如有时候需要调用的远程服务超时了(比如,用户注册成功后,需要给用户发放优惠券),可以考虑使用队列或者暂时记录到本地稍后重试。

      服务调用型:比如,某个服务的全局超时时间为500ms,但我们有多处服务调用,每处的服务调用超时时间可能不一样,此时,可以简单地使用Future来解决问题,通过如Future.get(3000,TimeUnit.MILLISECONDS)来设置超时。

七、前端Ajax超时

我们使用jQuery来进行Ajax请求,可以在请求时带上timeout参数设置超时时间。

  1. $.ajax({ 
  2.     url:"http://ins.jd.com:9090/test", 
  3.     dataType:"jsonp", 
  4.     jsonp:"test", 
  5.     jsonpCallback:"test", 
  6.     timeout:2000, 
  7.     success:function(result,status,xhr) { 
  8.        //success 
  9.     }, 
  10.     error: function(result,status,xhr){ 
  11.         if(status== 'timeout') { 
  12.             //timeout 
  13.         } 
  14.     } 
  15. }); 

      当进行跨域JSONP请求时,使用jQuery 1.4.x版本时,IE9、Chrome 52、Firefox 49测试 JSONP时,请求在超时后不能被取消,即使客户端超时了,该脚本也将一直运行;使用jQuery1.5.2时超时是起作用了,但是,发出去的请求是没有取消的(请求还处于执行状态)。

      如还有一种办法来进行超时重试,通过setTimeout进行超时重试,比如,京东首页的某个异步接口,其中一个域名(A机房)超时了,想超时后通过另一个域名(B机房)重新获取数据,代码如下所示。

  1. var id = setTimeout(retryCallback, 5000); 
  2. $.ajax({ 
  3.    dataType: 'jsonp', 
  4.     success:function() { 
  5.        clearTimeout(id); 
  6.         ... 
  7.     } 
  8. }); 

除了客户端设置超时外,服务端也一定要配置合理的超时时间。

总结

      本文主要介绍了如何在Web应用访问的整个链路上进行超时时间设置。通过配置合理的超时时间,防止出现某服务的依赖服务超时时间太长而响应慢,以致自己响应慢甚至崩溃。

      客户端和服务端都应该设置超时时间,而且客户端根据场景可以设置比服务端更长的超时时间。如果存在多级依赖关系,如A调用B,B调用C,则超时设置应该是A>B>C,否则可能会一直重试,引起DDoS攻击效果。不过最终如何选择还是要看场景,有时候客户端设置的就是要比服务端的超时时间短,通过在服务端实施限流/降级等手段防止DDoS攻击。

      超时之后应该有相应的策略来处理,常见的策略有重试(等一会儿再试、尝试其他分组服务、尝试其他机房服务,重试算法可考虑使用如指数退避算法)、摘掉不存活节点(负载均衡/分布式缓存场景下)、托底(返回历史数据/静态数据/缓存数据)、等待页或者错误页。

      对于非幂等写服务应避免重试,或者可以考虑提前生成唯一流水号来保证写服务操作通过判断流水号来实现幂等操作。

      在进行数据库/缓存服务器操作时,记得经常检查慢查询,慢查询通常是引起服务出问题的罪魁祸首。也要考虑在超时严重时,直接将该服务降级,待该服务修复后再取消降级。

      对于有负载均衡的中间件请考虑配置心跳/存活检查,而不是惰性检查。

      超时重试必然导致请求响应时间增加,最坏情况下的响应时间=重试次数×单次超时时间,这很可能严重影响到用户体验,导致用户会不断刷新页面来重复请求,最后导致服务接收的请求太多而挂掉,因此除了控制单次超时时间,也要控制好用户能忍受的最坏超时时间。

      超时时间太短会导致服务调用成功率降低,超时时间太长又导致本应成功的调用却失败了,这也要根据实际场景来选择最适合当前业务的,甚至是程序动态自动计算超时时间。比如商品详情页的库存状态服务,可以设置较短的超时时间,当超时时降级返回有货,而结算页服务就需要设置稍微长一些的超时时间保证确实有货。

      在实际开发中,不要轻视超时时间,很多重大事故都是因为超时时间不合理导致的,设置超时时间一定是只有好处没有坏处的,请立即Review你的代码吧。

 

转自:

http://zhuanlan.51cto.com/art/201706/542734.htm

http://zhuanlan.51cto.com/art/201707/543853.htm

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值