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
会被服务器断开。
如果connectionTimeout配置为20000,这个配置导致建立一个socket连接后,如果一直没有收到客户端的FIN,也没有数据过来,那么此连接也必须等到20s后,才能被超时释放。
2.4.6 Dubbo Timeout
Dubbo协议超时实现使用了Future模式。ResponseFuture.get()在请求还未处理完或未到超时前一直是wait状态;响应达到后,设置请求状态,并进行notify唤醒。即使用了Object的 await-notify-notifyAll
机制。
Dubbo消费端
- 全局超时配置
- 指定接口以及特定方法超时配置
Dubbo服务端
- 全局超时配置
- 指定接口以及特定方法超时配置
2.5 DB Timeout
以下是应用(WAS/BLOC)、连接池(DBCP)、Timeout层级和DBMS直接的关系图:
解释说明:
- 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处理过程
解释说明:
- 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_timeout
和client_header_timeout
,用于防止客户攻击Dos攻击,如分别20s、10s - 建议设置
max_fails
和fail_timeout
,解决每次请求宕机服务端时,都需要等待超时问题,如分别为3次、30s - 建议设置
proxy_connect_timeout
、proxy_send_timeout
和proxy_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