服务间调用细节:超时、重试与错误处理

上一章让调用"发得出去",但网络不会一直听话:连接可能建不上、对端可能迟迟不响应、服务可能返回 500。如果调用方没有超时与错误兜底,一个慢接口就能拖垮整条链路。本章围绕三个词展开:超时(及时止损)、重试(尽力而为)、错误处理(体面失败)。

先区分两种超时

连接超时(connectTimeout):建立 TCP 连接的最大等待时间,服务不存在、端口不通时很快触发。读取超时(readTimeout):连接建立后等待响应体的最长时间,对端慢、线程池耗尽时触发。两者含义不同,配置时都要显式给出,不要依赖默认值。生产环境还要让网关、调用方、被调方三层的超时"逐层递减":外层超时短于内层,否则外层先放弃、内层还在白干活,容易出现"慢请求堆积"。

OpenFeign 超时配置

在 OpenFeign 中按客户端配置超时(单位毫秒):

spring:
  cloud:
    openfeign:
      client:
        config:
          user-service:
            connect-timeout: 2000    # 连接超时 2 秒
            read-timeout: 5000       # 读取超时 5 秒

对没有特别要求的服务可以配在 default 下,个别慢服务再单独覆盖。超时不是越小越好:过小会在大促抖动时误杀正常请求,过大则起不到保护作用,建议结合对方历史 P99 延迟设定并持续调优。

重试:默认不重试,开启需谨慎

Spring Cloud OpenFeign 默认不进行重试(相当于"永不重试"),需要重试时自行提供 feign.Retryer 的 Bean;新版 feign 的 Retryer 推荐用其 Builder 或实现接口的方式构造,具体写法以所用 feign 版本的官方文档为准。开启重试前先回答一个问题:这次调用失败后重放一次安全吗?GET、查询类幂等操作可以重试;下单、扣款等写操作盲目重试可能造成重复扣款,必须配合幂等键或业务去重,否则宁可失败让上层补偿。

重试要与超时配合

每次重试都重新走一遍连接与读取超时,所以总耗时 ≈ 单次超时 × 最大重试次数。重试之间建议带间隔,并设置最大次数(通常 1~2 次足够)。还要想清楚重试的对象:对"连接失败"重试意义大(可能只是瞬时抖动),对"读取超时/服务端 5xx"重试要慎重——若对端已过载,重试只会雪上加霜,这种情况更适合走熔断降级(第 15 章)。最后要防"重试风暴":故障恢复的瞬间,大量积压请求同时重试可能把刚缓过来的服务再次打垮,因此重试应带递增间隔与随机抖动,并限制各层的并发重试量;调用链每一层都写操作就都不重试、读操作才有限重试,避免层叠放大。

错误处理三层打法

第一层,识别错误类型:连接失败、超时属于可重试的瞬时错误;4xx 属于调用方问题,重试无意义;5xx 属于服务端问题,视幂等性决定是否重试。第二层,用 ErrorDecoder 把 HTTP 错误码翻译成业务异常:自定义实现 feign 的 ErrorDecoder,在 decode 方法里按 status 抛不同类型的异常,调用方就能 catch 到语义明确的异常而非一律 RemoteServiceException。第三层,提供降级响应:启用熔断组件后,为 @FeignClient 配置 fallback 或 FallbackFactory(第 15 章会给出实例),返回默认值或走缓存,避免错误直接打到用户。

快速失败优于无限等待

当依赖方持续不响应时,每个请求都"等满超时时间"才失败,并发一高,线程池就被这种慢性等待耗尽,故障从依赖方传染到调用方。与其陪着慢,不如设置较短的超时并配合熔断让它快速失败,把损失控制在固定范围内,剩余能力留给健康请求;业务上可接受的结果由降级逻辑兜住(默认值、缓存、友好提示)。"快速失败"不是消极处理,而是把失败成本从"不确定的拖垮"变成"确定的、可控的拒绝"。

工程建议

给每个外部依赖维护一份超时/重试/降级配置清单:对外部依赖分类设定默认模板(例如查询类连接 1 秒、读取 3 秒;写类连接 2 秒、读取 5 秒),特殊服务单独例外并在清单中注明理由。调用链路上任何一环超时,日志都要能关联到 traceId(第 22 章)以便定位;对"慢"与"错"分别监控——错误率上升报警,P99 延迟上升也要报警,很多故障是从"慢"开始的。

小结

超时让系统及时止损,重试让瞬时故障自愈,错误处理让失败体面可控;三者共同构成服务调用的基础韧性。它们只能对付偶发抖动,对付不了持续的故障——持续的慢与错要靠熔断与降级,我们会在 Sentinel 章节系统讲解。下一章先补上调用链上的负载均衡。

笔记加载中…