一、为什么要设置超时与重试
平时开发过程中,可能很多人都不会意识到设置超时的重要性。如果应用不设置超时,调用的服务接口可能会因为各种原因(异常、连接池/线程池耗尽、SQL慢查询等)导致响应慢,一旦慢请求累积地越来越多,导致的后果就是连锁反应,即我们常说的雪崩效应。超时后,我们可以选择重试几次。读服务重试没有问题,但写服务重试可能会造成重复,如下订单,除非业务方做了幂等处理。重试的频率和次数如果过多,也会造成请求流量过大,对系统造成冲击。因此必须根据实际业务场景设置合理的超时重试机制,必要时应该熔断快速失败,而不是不断重试。
二、超时的链路层次
通常我们的应用会像下图的结构,请求开始到返回响应会经过一条完整的链路:浏览器/app客户端 -> 代理层(负载均衡) -> Web容器层(Tomcat等) -> 业务逻辑层(Future等) -> 中间件层(Redis等) -> 数据库层(Mysql等)。每一层都可以设置超时与重试机制。下面展开细说。
1.代理层超时
这里以nginx为例。nginx主要有客户端超时、DNS解析超时、代理超时。
①客户端超时
client_header_timeout time: 设置读取客户端请求头超时时间,默认60s。
client_body_timeout time: 设置读取客户端内容体超时时间,默认60s。
send_timeout time:设置发送响应到客户端的超时时间,默认60s。
keepalive_timeout timeout [header_timeout]:设置HTTP长连接超时时间。第一个参数timeout是告诉nginx长连接超时时间是多少,默认75s,第二个参数header_timeout用于设置响应头“Keep-Alive: timeout=time”,即告知客户端长连接超时时间。
②DNS解析超时
resolver_timeout time:设置DNS解析超时时间,默认30s。
③代理超时
proxy_connect_timeout time:与后端/上游服务器建立连接的超时时间,默认60s。
proxy_read_timeout time:设置从后端/上游服务器读取响应的超时时间,默认60s。
proxy_send_timeout time:设置往后端/上游服务器发送请求的超时时间,默认60s。
nginx除了超时参数设置,也有重试参数设置。
proxy_next_upstream error|timeout|invalid_header|http_500|http_502|http_503
配置什么情况下需要请求下一台上游服务器进行重试。
proxy_next_upstream_tries number:设置重试次数,默认0表示不限制。
proxy_next_upstream_timeout time:设置重试最大超时时间,默认0表示不限制。
max_fails和fail_timeout:配置什么时候nginx将上游服务器认定为不可用/不存活。当上游服务器在fail_timeout时间内失败了max_fails次,则认为该上游服务器不可用,并在接下来的fail_timeout时间内从upstream摘除该节点。
2.Web容器超时
以Tomcat8.5版本为例。
connectionTimeout:配置与客户端建立连接的超时时间,默认60s
socket.soTimeout:从客户端读取请求数据的超时时间,默认60s
asyncTimeout:servlet3异步请求的超时时间,默认30s
keepAliveTimeout和maxKeepAliveRequests:和nginx长连接类似,keepAliveTimeout默认为-1表示永不超时,maxKeepAliveRequests默认为100
3.业务超时
在多线程编程中,我们经常会使用Future接口,来异步接收线程的执行结果,我们可以设置超时时间,而不是一直阻塞下去,即使用Future.get(timeout, TimeUnit)。
4.中间件超时
以Dubbo为例。
<dubbo:consumer timeout="1000"></dubbo:consumer>
<dubbo:provider timeout="1000"></dubbo:provider>
其实像zookeeper,redis,kafka等中间件都有相应的超时设置,感兴趣地可以去翻下官方文档。
5.数据库超时
以dpcp连接池为例
<bean id="dataSource" class="org.apache.commons.dbcp2.BaseDataSource" destroy-method="close">
<!-- Statement默认超时时间-->
<property name="defaultQueryTimeout" value="3" />
<!-- socket连接/读超时-->
<property name="connectionProperties" value="connectTimeout=2000; socketTimeout=2000" />
<!-- 等待获取连接池连接最大超时时间-->
<property name="maxWaitMillis" value="500" />
</bean>
三、总结
本文介绍了超时的重要性。超时后的策略可以是重试(等一会再试,重试其他分组服务,重试其他机房服务等)、摘掉不可用节点,返回托底数据和页面等等。
对于非幂等的写服务应该避免重试,或者可以考虑提前生成唯一流水号来保证写服务操作的幂等性。
超时重试虽然提高了可用性,但也必然会导致RT即响应时间增加,重试次数过多可能会影响用户体验,因此在实际开发中要根据业务场景考虑用户的容忍最长等待时间。
参考资料
《亿级流量网站架构核心技术》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。