Micro Service Architecture — Timeout

原创
2018/08/10 14:09
阅读数 336

1 Overview

常见的微服务架构: 微服务架构

做好超时时间的限定,用于判定超时后资源能够及时被释放,用于处理其它的请求,从而提升的性能。

  • 前端:Ajax、Node
  • 代理层:DNS、LB(SLB、F5、Keepalived+LVS、Haproxy、A10等)、Ngixn、Gateway
  • 服务容器:Tomcat、Jetty
  • 中间件:Feign、Dubbo、HTTPClient、ES、MongoDB、Redis
  • 数据库:MySQL、Oracle

2 Solution

2.1 Front-end Timeout

2.1.1 ajax Timeout

​ ajax底层使用的是XMLHttpRequest,其超时参数可以设置:连接超时、读超时和写超时。但对于包装后的ajax,我们通常只需要设置请求超时时间(timeout)即可,具体案例如下:

var ajaxTimeoutTest = $.ajax({
  url:'/demo',
    // 设置请求超时时间(毫秒),此设置将覆盖全局设置
  timeout : 1000,
  type : 'get',
  data :{},
  dataType:'json',
  success:function(data){
    alert("成功");
  },
  complete : function(XMLHttpRequest,status){
	  // 超时处理: status还有success,error等值的情况
      if(status=='timeout'){
       ajaxTimeoutTest.abort();
       alert("超时");
    }
  }
});

2.1.2 Node.js Timeout

  • Server Timeout
const http = require("http");
const server = http.createServer( function(req, res){
    // ......
});

// 设置服务端请求处理的超时时间
server.setTimeout(30 * 1000);
server.listen(3000, "localhost", function(){
    console.log("开始监听"+server.address().port+"......");
});
  • Client Timeout
const http = require('http');

const options = {host: 'localhost', method: 'GET', port: 8080, path: '/test'}
var req = http.request(options);

// 设置客户端每个外调的超时时间
req.setTimeout(20 * 1000);
req.on('response', (res) => {
  res.setEncoding('utf8');
  res.on('data', function(chunk){
	console.log('收到数据:%s', chunk);
  });
  res.on('end', function(){
	console.log(res.trailers);
  });
});
req.end();

2.2 Ngixn Timeout

2.2.1 keepalive_timeout

​ HTTP是一种无状态协议,其客户端底层向服务器发送一个TCP请求,服务端响应完毕后就会断开连接。如果客户端向服务器发送多个请求,每个请求都要建立各自独立的连接以传输数据。

​ HTTP的KeepAlive就用于告诉服务器在处理完请求后保持一段这个TCP连接的打开状态。若接收到来自客户端的其它请求,服务端会利用这个未被关闭的连接,而不需要再建立一个连接。KeepAlive在一段时间内保持打开状态,它们会在这段时间内占用资源,但占用过多就会影响性能。

​ 因此,Nginx使用 keepalive_timeout 来指定KeepAlive的超时时间,用于指定每个TCP 连接最多可以保持多长时间。Nginx的默认值是75 秒,然而有些浏览器最多只保持 60秒,所以可以设定为 60 秒 更安全。若将它设置为 0,就禁止了 keepalive 连接。

# 配置段: http、server、location, 默认值是75秒
keepalive_timeout 60s;

2.2.2 client_body_timeout

​ 用于指定客户端与服务端建立连接后发送 request body 的超时时间,如果客户端在指定时间内没有发送一个完整的 request body,Nginx就会返回 HTTP 408(Request Timed Out)

# 配置段: http、server、location
client_body_timeout 20s;

2.2.3 client_header_timeout

​ 客户端向服务端发送一个完整的 request header 的超时时间,如果客户端在指定时间内没有发送一个完整的 request header,Nginx 返回 HTTP 408(Request Timed Out)

# 配置段: http、server、location
client_header_timeout 10s;

2.2.4 proxy_upstream_fail_timeout

​ fail_timeout通常是配合max_fails一起来使用的,实现熔断隔离的功能。其作用主要是指在 30 秒内请求某一应用失败 3 次,则认为该应用宕机,之后会等待 30 秒,这期间内不会再把新请求发送到宕机应用,而是直接发到正常的那一台。时间到后再有请求进来,则继续尝试连接宕机应用且仅尝试 1 次,如果还是失败,则继续等待 30 秒…...以此循环,直到恢复。

# 配置段: upstream, fail_timeout默认为10s, max_fails默认为1
upstream  web_tomcat {
	server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
	server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
}

2.2.5 proxy_connect_timeout

​ 用于设置Nginx向后端服务器的连接超时时间,即为发起TCP握手等候响应的超时时间。

# 配置段: http、server、location, 默认为60s
location / {
  	proxy_connect_timeout 500s;
    proxy_pass http://web_tomcat;
 }

2.2.6 proxy_read_timeout

​ 连接成功后,等候后端服务器响应时间,其实已经进入后端的排队之中等候处理,也可以说是后端服务器处理请求的 时间。

# 配置段: http、server、location, 默认为60s
location / {
  	proxy_read_timeout 500s;
    proxy_pass http://web_tomcat;
 }

2.2.7 proxy_send_timeout

​ 用于设置后端服务器数据回传时间,就是在规定时间之内后端服务器必须传完所有的数据。

# 配置段: http、server、location, 默认为60s
location / {
	proxy_send_timeout 500s;
    proxy_pass http://web_tomcat;
 }

2.2.8 Others

  • resolver_timeout:域名解析超时,默认30s。配置段:http、server、location
  • lingering_timeout:设置TCP连接关闭时的SO_LINGER延时,默认为5s。配置段:http、server、location
  • tcp_nodelay:默认情况下,当数据发送时,内核并不会马上发送,可能会等待更多的字节组成一个数据包,这样可以提高 I/O 性能,但是在每次只发送很少字节的业务场景中,等待时间会比较长

注意事项

  • 客户端连接Nginx超时,建议5s内
  • proxy_connect_timeout的值不能超过75s
  • 通常client_body_timeout应该比keepalive_timeout小

扩展:tcp_nodelay与tcp_nopush

  • tcp_nodelay:开启或关闭Nginx使用TCP_NODELAY选项的功能
  • tcp_nopush:开启或者关闭Nginx在FreeBSD上使用TCP_NOPUSH套接字选项的功能
# tcp_nodelay配置段: http、server、location, 默认值为 tcp_nodelay on;
# tcp_nopush配置段: http、server、location, 默认值为 tcp_nopush off;
http {
    tcp_nodelay on;
}

2.3 Gateway Timeout

2.3.1 Zuul Timeout

  • 使用Ribbon路由 Zuul的超时与Ribbon、Hystrix相关(RibbonRoutingFilter整合了Hystrix和Ribbon),此时Zuul的超时可以配置如下:

    # Hystrix,设置调用者等待命令执行的超时限制,超过此时间,HystrixCommand被标记为TIMEOUT,并执行回退逻辑
    hystrix.command.xxx.execution.isolation.thread.timeoutInMilliseconds: 1000
    
    # Ribbon
    ribbon:
     read-timeout: 1000
     connect-timeout: 1000
    
  • 未使用Ribbo路由(SimpleHostRoutingFilter整合了Apache HttpClient)

    zuul.routes.xxx.path: /user/**
    zuul.routes.xxx.url: http://localhost:8000/
    # TCP连接超时时间
    zuul.host.connect-timeout-millis: 2000
    # Socket超时,即数据传输的超时时间
    zuul.host.socket-timeout-millis: 10000
    

2.4 Middleware Timeout

2.4.1 Ribbon Timeout

全局配置:

ribbon:
	read-timeout: 60000
	connect-timeout: 60000

局部配置:

service-id:
	ribbon:
		read-timeout: 1000
		connect-timeout: 1000

2.4.2 Feign Timeout

​ 从Spring Cloud Edgware开始,Feign支持使用属性配置超时(对于老版本,可以写个feign.Request.Options 即可):

feign.client.config:
	feign-name:
	connect-timeout: 5000
	read-timeout: 5000

2.4.3 RestTemplate Timeout

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
	SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
	factory.setConnectTimeout(1000);
	factory.setReadTimeout(1000);
	return new RestTemplate(factory);
}

2.4.4 Hystrix Timeout

# 默认开启超时机制
hystrix.command.default|xxx.execution.timeout.enabled: true
# 是否打开超时线程中断, Thread模式有效
hystrix.command.default|xxx.execution.isolation.thread.interruptOnTimeout: true
# 超时时间, 默认为1秒:
# 1.在THREAD模式下,达到超时时间,可以中断
# 2.在SEMAPHORE模式下,会等待执行完成后,再去判断是否超时
hystrix.command.default|xxx.execution.isolation.thread.timeoutInMilliseconds: 1000

2.4.5 Tomcat Timeout

​ tomcat对每个请求的超时时间是通过connectionTimeout参数设置的。默认的server.xml里的设置是20秒,如果不设置这个参数代码里会使用60秒。这个参数也会对POST请求有影响,但并不是指上传完的时间限制,而是指两次数据发送中间的间隔超过connectionTimeout会被服务器断开。

<Connector port="7001" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

​ 如果connectionTimeout配置为20000,这个配置导致建立一个socket连接后,如果一直没有收到客户端的FIN,也没有数据过来,那么此连接也必须等到20s后,才能被超时释放。

2.4.6 Dubbo Timeout

​ Dubbo协议超时实现使用了Future模式。ResponseFuture.get()在请求还未处理完或未到超时前一直是wait状态;响应达到后,设置请求状态,并进行notify唤醒。即使用了Object的 await-notify-notifyAll 机制。 Dubbo Architecture

Dubbo消费端

  • 全局超时配置
    <dubbo:consumer timeout="5000" />
    
  • 指定接口以及特定方法超时配置
    <dubbo:reference interface="com.foo.BarService" timeout="2000">
        <dubbo:method name="sayHello" timeout="3000" />
    </dubbo:reference>
    

Dubbo服务端

  • 全局超时配置
    <dubbo:provider timeout="5000" />
    
  • 指定接口以及特定方法超时配置
    <dubbo:provider interface="com.foo.BarService" timeout="2000">
        <dubbo:method name="sayHello" timeout="3000" />
    </dubbo:provider>
    

2.5 DB Timeout

以下是应用(WAS/BLOC)、连接池(DBCP)、Timeout层级和DBMS直接的关系图: 应用&数据库间 — Timeout架构

解释说明

  • statement timeout无法处理网络连接失败时的超时,它能做的仅仅是限制statement的操作时间
  • 网络连接失败时的timeout必须交由JDBC来处理
  • JDBC的socket timeout会受到操作系统socket timeout设置的影响
  • timeout层级与DBCP是相互独立,DBCP负责的是数据库连接的创建和管理,并不干涉timeout的处理
  • 在应用中调用DBCP的getConnection()时,你可以设置获取数据库连接的超时时间,但是这和JDBC的timeout无关

案例:JDBC连接会在网络出错后阻塞30分钟,然后又奇迹般恢复,即使并没有对JDBC的socket timeout进行设置

2.5.1 Transaction Timeout

​ 一般存在于框架或应用级,用于设置是一个事务的执行总时间,其中可能包含多个statement。在Spring中可以使用XML或在源码中使用@Transactional注解来进行设置。

  • 1个statement ~ 0.1s,10w个statement ~ 1w秒(约7个小时)
  • 1个statement × 1个statement执行200ms,则transaction timeout至少应该设置为:1100ms(200×5+100)

2.5.2 Statement Timeout

​ 用于设置单个statement的执行超时时间,即Driver等待statement执行完成,接收到数据的超时时间。timeout的值通过调用JDBC的java.sql.Statement.setQueryTimeout(int timeout) API进行设置,但更多的是是通过框架来进行设置。

注意

  • statement timeout的具体值需要依据应用本身的特性而定,并没有可供推荐的配置
  • statement的timeout不是整个查询的timeout,只是statement执行完成并拉取数据返回的超时时间

MySQL JDBC Statement的QueryTimeout处理过程 MySQL JDBC Statement的QueryTimeout处理过程

解释说明

  • statement创建一个新的timeout-execution线程用于超时处理,5.1版本后改为每个connection分配一个timeout-execution线程
  • 达到超时时间,TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法,timeout-execution线程创建一个和statement配置相同的connection,向超时query发送:cancel query(KILL QUERY “connectionId”)

2.5.3 JDBC socket timeout

​ 用于设置jdbc I/O socket read and write operations的超时时间,防止因网络问题或数据库问题,导致Driver会一直阻塞等待。(建议比statement timeout的时间长)

  • mysql(单位为毫秒)

    jdbc:mysql://localhost:3306/ag_admin?useUnicode=true&characterEncoding=UTF8&connectTimeout=60000&socketTimeout=60000
    
  • pg(单位为秒)

    jdbc:postgresql://localhost/test?user=fred&password=secret&&connectTimeout=60&socketTimeout=60
    
  • oracle ​ oracle需要通过oracle.jdbc.ReadTimeout参数来设置,连接超时参数是oracle.net.CONNECT_TIMEOUT。可以通过以下两种方式进行设置:

    通过properties设置

    Class.forName("oracle.jdbc.driver.OracleDriver");
    Properties props = new Properties() ;
    props.put( "user" , "test_schema") ;
    props.put( "password" , "pwd") ;
    props.put( "oracle.net.CONNECT_TIMEOUT" , "10000000") ;
    props.put( "oracle.jdbc.ReadTimeout" , "2000" ) ;
    Connection conn = DriverManager.getConnection( "jdbc:oracle:thin:@127.0.0.1:1521:orcl" , props ) ;
    

    通过环境变量设置 —— 注意需要在connection连接之前设置环境变量

    String readTimeout = "10000"; // ms
    System.setProperty("oracle.jdbc.ReadTimeout", readTimeout);
    Class.forName("oracle.jdbc.OracleDriver");
    Connection conn = DriverManager.getConnection(jdbcUrl, user, pwd);
    

2.5.4 OS socket timeout

​ 这是操作系统级别的socket设置,用来检测坏死socket连接,Linux一般默认2小时。如果jdbc socket timeout没有设置,而OS级别的socket timeout有设置,则使用系统的socket timeout值。

# 查看OS的keepalive配置信息
sudo sysctl -a|grep keepalive

# 修改OS的keepalive配置信息,并修改以下配置信息
vim /etc/sysctl.conf

# 表示TCP连接在多少秒之后没有数据报文传输时启动探测报文(发送空的报文),单位为秒(s)
net.ipv4.tcp_keepalive_time = 7200
# 表示前一个探测报文和后一个探测报文之间的时间间隔,单位为秒(s)
net.ipv4.tcp_keepalive_intvl = 75
# 表示探测的次数
net.ipv4.tcp_keepalive_probes = 9

# 让修改的参数即时生效
sysctl -p

总结 ​ jdbc的socketTimeout值的设置要非常小心,不同数据库的jdbc driver设置不一样,特别是使用不同连接池的话,设置也可能不尽相同。对于严重依赖数据库操作的服务来说,非常有必要设置这个值,否则万一网络或数据库异常,会导致服务线程一直阻塞在java.net.SocketInputStream.socketRead0。

  • 如果查询数据多,则会导致该线程持有的data list不能释放,相当于内存泄露,最后导致OOM
  • 如果请求数据库操作很多且阻塞住了,会导致服务器可用的woker线程变少,严重则会导致服务不可用

3 Practice

3.1 Focus

各层组件的超时时间,主要是设置以下两个参数:

  • connectTimeout
  • socketTimeout

当然针对特殊的场景,则可以设置更详细的超时参数,如:

  • readTimeout
  • writeTimeout

3.2 Suggest

ajax —— 5s ~ 60s

  • 建议全局设置一个统一的超时时间,如60s
    • 从使用的互联网产品来看,一般网络较差时,加载网页可能需要等待30秒或1分钟左右后才出现网络异常等的情况
  • 特殊场景自定义设置超时时间,从而覆盖全局超时时间
    • 如上传较大文件时,则可以设置时间更长(当然太大的文件,则建议单独考虑,如分块处理等)
    • 如实时性要求较高的场景,则可以设置更短,如5s等

Ngixn

  • 建议设置 keepalived_time 来提高Ngixn支持的并发能力与复用HTTP建立的TCP连接,如设置为5s
  • 建议设置 client_body_timeoutclient_header_timeout,用于防止客户攻击Dos攻击,如分别20s、10s
  • 建议设置 max_failsfail_timeout ,解决每次请求宕机服务端时,都需要等待超时问题,如分别为3次、30s
  • 建议设置 proxy_connect_timeoutproxy_send_timeoutproxy_read_timeout 参数,用于控制Ngixn转发到后台的超时控制

Node 使用Node作为网关代理转发请求时:

  • Server —— 60s
    • 如果代理层有一定的功能逻辑,则建议加上Server的处理超时时间
    • 如果代理层几乎没有逻辑,则Server层的超时可以不配置
  • Client Client用于代理转发,而后端业务场景不同,要求也有所不同,所以建议设置较长的默认值,并支持请求自定义

Middleware Ribbon、Zuul(Apache HTTPClient)、Feign、RestTemplate和Netty等,都建议必须设置以下两个参数:

  • connectTimeout
  • socketTimeout

Hystrix 使用Hystrix时,建议设置提交线程后的等待超时时间:thread.timeoutInMilliseconds ,默认为1000ms

DB

  • Transaction Timeout
  • connectTimeout
  • socketTimeout
  • OS socket timeout

3.3 Timeout Architecture

微服务超时架构

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部