Skip to content

无状态高可用:超时检测

About 2248 wordsAbout 7 min

架构新书

2026-11-09

分布式系统,各个节点之间的调用,希望像同进程方法之间调用,结果要么返回,要么捕捉调用异常。性能上期望无延迟,快速返结果。但实际上,由于部署物理环境的问题或者目标节点的服务故障,调用会出现超时或者无响应。这需要调用者自己对服务调用进行超时控制而不能完全依赖目标服务或者操作系统,或者微服务框架。 在指定的时间内,微服务未返回结果,尽快结束调用。实现快速失败

通常来说,微服务框架的的默认超时设置都较为保守,或者依赖更为保守的底层网络系统,所以应用必须显示的设置超时配置。本节描述了微服务调用超时的可能各种原因,并在后面给出解决方法。

服务调用之间超时,情况非常复杂,依赖服务部署的环境,以及服务自身。服务节点部署的环境导致出现调用超时的情况,如下表总结

环境超时原因
网路设置电信运营商,或者云产品提供商对网络调整引起网络暂时不可用, 我公司产品曾经在南方某市访问延迟非常大,后来是运营商误设置黑名单导致的
网络抖动网络设备故障切换到备用网络,会导致传输超时。虚拟化网络也可能因为调整导致传输故障
网络黑洞数据传输包被网络设备丢弃,也不告知传送方。导致传送方一直等待应答。
ECS主机调整云服务器ECS所在物理主机的调整,或者ECS 迁移到其他主机,网络暂时不可用。
防火墙限制有时候防火墙会对流量进行控制,如果超过流量,会丢弃或者拒绝,但丢弃的时候并没有给客户端响应,客户端等待只到超时
负载均衡器像SLB和Nginx,Haproxy,也可能限流或者调整均衡策略,云产品常会按照流量收费,超过流量的包将被丢弃。
物理宕机如果客户端等待服务端响应时服务器宕机,则客户端会一直等待,直到超过 tcp_keepalive_time 指定的时间,默认是2小时,实际上,在2小时是开始探测网络状况的开始时间,在探测tcp_keepalve_probes次后才会确认超时
进程宕机进程被误杀,或者进程出现故障导致宕机,比如内存溢出。

当出现这些情况导致超时的时候,需要从系统的物理架构入手排查超时原因,很难诊快速断到具体故障原因,需要考虑主机、网络、运营商、以及网络中的防火墙和负载均衡器 。 像运营商导致的超时很难排查,需要协调电信相关部门

即使部署环境正常,也可能因为服务自身问题导致的超时

组件描述
WEB服务器Web服务器设置处理线程过少到导致请求排队,比如Tomcat设置了较少的线程。
服务器正在预热过程服务器所在JVM会预热,自身业务存在懒加载,连接池等组件的初始化导致此时请求超时。
锁竞争应用自身存在锁竞争,导致调用阻塞。
线程池线程池参数设置不合理导致调用发生阻塞,比如设置线程池的策略为CallerRunsPolicy
JVM Full GC当应用存在内存溢出情况前,JVM会频繁FULL GC ,这时候JVM会暂停请求服务。关于垃圾回收,参考内存管理章节
高耗CPU业务像SSL握手,HASH算法,数据压缩等耗费CPU较大,导致主机CPU升高。需要关注主机的CPU使用率,一般使用率超过80%,服务处理开始明显变慢。
服务调用其他服务调用其他服务阻塞
数据库连接池数据库连接池耗尽导致其他数据库访问必须等待链接释放。

服务中的组件,设置超时时间也较为保守,或者依赖更保守的底层系统。

组件默认超时设置
Dubbo默认设置为-1,无超时限制,可以分别指定客户端consumer超时和服务端provider 根据业务情况,必须指定一个超时时间,如3000 毫秒。
FeignFeign可以指定Connect Timeout 和Read Timeout俩个参数。默认分别是10000毫秒和60000毫秒。
Thrift默认不做超时处理,依赖底层操作系统处理超时
Java Socket默认不做超时处理,依赖底层操作系统处理超时
RedisRedis客户端工具依赖Socket设置,以Jdeis为例子,Connect Timeout和Read Timeout 默认设置为2000毫秒。通常这个设置是正常的。也可以设置更为保守,如Read Timeout 为6000毫秒
Kafka生产端发送消息默认等待30秒,可以通配置request.timeout.ms调整。另外一个是 max.block.ms,表示缓冲区慢导致阻塞的最长时间,默认是60秒

通常,无论是网络环境,还是分布式框架都有默认的调用超时时间或者依赖底层设置超时时间,越接近底层,设置的超时时间会越保守,以Linux系统的网络设置来举例子,连接超时、读超时和写超时都设置的非常保守

  • Connection Timeout: 建立一个链接需要3次TCP握手,当客户端发送SYN到服务器端没有得到响应的时候,会进行tcp_syn_retries 次尝试,默认是5次,重拾的间隔时间按照指数级增长, 分别是1S,2S,4S,8S,16S,。在服务器收到SYN后,给客户端发送ACK,如果未收到客户端的ACK,则会尝试tcp_synack_retries次,默认值是5。 可见在一个不稳定的网路环境,从建立链接到链接超时,需要的时间很长。
  • Write Timeout : Linux下通过SO_SNDTIMEO 控制写到TCP Buffer的超时时间。默认是永不超时 。
  • Read Timeout :SO_RCVTIMEO控制从TCP Buffer读取的超时是按,默认不超时。如果网络有问题, 操作系统会根据tcp_keepalive_time指定的时间开始探测网络是否链接正常,默认是2小时

运维可以根据业务帮助我们设置Linux系统的TCP超时时间,但我们需要按照业务设定设置一个期望的超时时间,而不是任由Linux系统,网络状况,或者服务端状态来决定超时时间。

超时时间需要设置在合理的范围,比如,Redis的连接池设置为10分钟,会发生什么情况?当redis主从切换,或者网络故障,Redis连接池发现超时了,需要等待10分钟才有可能报错。然而,Redis集群通常在数秒内就能自动恢复。如此长的超时设置导致即使基础设施已经恢复,服务10分钟还是不可用。 超时时间设置过短也可能有问题,比如Redis中的有Key业务,确实需要数十毫秒才能完成大Key操作,那么超时时间不应该设置过小

作为调用客户端,一方面要设置应用的底层框架和操作系统的超时时间,不要采用其默认的保守的设置,在出错的时候尽快抛出错误。 另外通过Future类封装任何分布式调用,在业务层指定超时时间,而不依赖从分布框架和底层linux网络的各个超时设置,如下设置2秒超时

//executor 是线程池
Future<String> future = executor.submit(() -> {
            // 模拟耗时5秒的业务
            TimeUnit.SECONDS.sleep(5);
            return "任务结果";
});

String result = future.get(2, TimeUnit.SECONDS);
System.out.println("输出:" + result);

像Dubbo等分布式框架,都可以配置的调用超时,如下是Dubbo配置超时时间,分别设置默认超时时间,以及服务级超时时间,和具体到某一个方法的超时时间。

<!-- provider全局默认超时 2s -->
<dubbo:provider timeout="2000" retries="0"/>

<!-- 暴露服务:接口级别超时1s -->
<dubbo:service interface="com.ibeetl.UserService"
               ref="userService"
               timeout="1000">
    <!-- 方法级超时,优先级最高 -->
    <dubbo:method name="getUserById" timeout="10000"/>
</dubbo:service>

分布系统编程,通常使用的分布式框架的API,都有一个可选参数为超时设置,切记需要显示的设置超时时间而不要使用其默认时间,因为这种默认超时时间太保守。

知行合一