首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >后端稳定性建设|容错四大金刚:重试(上)线上 Feign 重试故障深度复盘

后端稳定性建设|容错四大金刚:重试(上)线上 Feign 重试故障深度复盘

作者头像
用户12591013
发布2026-07-22 18:57:28
发布2026-07-22 18:57:28
1040
举报

系列导读

本系列围绕稳定性容错四大核心能力:超时、重试、限流、熔断 分上中下三篇完整落地讲解重试:

  • 上篇:线上真实 Feign 故障复盘,拆解原生三层重试底层缺陷,同步埋下优雅上下线优化伏笔;
  • 中篇:自研 OpenFeign 双条件安全重试完整架构与代码落地;
  • 下篇:对齐设计思想,实现 gRPC 统一重试方案 + 配套监控治理。

引子:两类发布故障的连锁影响

线上稳定性事故中,80% 的故障根源都和变更发布强相关,其中两大高频痛点:一是重试机制设计缺陷,节点下线后无法自动自愈;二是服务优雅上下线流程缺失,下线流量未排空就销毁实例。

本次故障正是「优雅下线不完善 + Feign 原生重试能力残缺」双重问题叠加导致。本文先完整复盘节点下线引发的调用雪崩问题,后续系列文章会专门推出《优雅上下线》专题,从注册中心、流量等待、容器销毁、网关多级视角完整拆解标准化落地方案。

一、故障现场真实现象

1.1 业务场景

上游服务通过 OpenFeign 发起 POST 批量查询接口调用,下游服务执行发布迭代,实例执行优雅下线流程,但下线逻辑存在漏洞,流量未完全排空就关闭 TCP 端口。

业务侧批量任务持续大面积报错,无任何自动自愈逻辑,只能等待新实例完全启动、旧实例全部摘除后手动重试任务恢复。

异常栈信息

代码语言:javascript
复制
feign.RetryableException: Failed to connect to /172.19.115.10:8080 executing POST http/data-rpc-service/data/batchQuery
	at feign.FeignException.errorExecuting(FeignException.java:268)
	at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:129)
	at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89)
	at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100)
	at org.springframework.cloud.openfeign.FeignCachingInvocationHandlerFactory$1.proceed(FeignCachingInvocationHandlerFactory.java:66)
	at org.springframework.cache.interceptor.CacheInterceptor.lambda$invoke$0(CacheInterceptor.java:54)
	at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:351)
	at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:64)
	at org.springframework.cloud.openfeign.FeignCachingInvocationHandlerFactory.lambda$create$1(FeignCachingInvocationHandlerFactory.java:53)
	at jdk.proxy3.$Proxy240.batchQuery(Unknown Source)
        // 上层业务链路
	at com.xxx.search.core.infra.service.DataGatewayImpl.batchQuery
	at com.xxx.search.core.application.biz.impl.DataServiceImpl$RebuildTask.run
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
	at java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at com.alibaba.ttl.TtlRunnable.run(TtlRunnable.java:60)
	at org.dromara.dynamictp.core.support.task.runnable.MdcRunnable.run
	at org.dromara.dynamictp.core.support.task.runnable.DtpRunnable.run
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.lang.Thread.run(Thread.java:959)
Caused by: java.net.ConnectException: Failed to connect to /172.19.115.10:8080
	at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.kt:297)
	at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:207)
	at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:226)
	at okhttp3.internal.connection.ExchangeFinder.findHealthConnection(ExchangeFinder.kt:106)
	at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:74)
	at okhttp3.internal.connection.RealCall.initExchange$okhttp(RealCall.kt:255)
	at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:32)
	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
	at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95)
	at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83)
	at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76)
	at com.framework.rpc.feign.interceptor.MetricInterceptor.intercept
	at com.framework.rpc.feign.interceptor.ProtocolInterceptor.intercept
	at io.opentelemetry.javaagent.shaded.instrumentation.okhttp.v3_0.internal.ConnectionErrorSpanInterceptor.intercept
	at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
	at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154)
	at feign.okhttp.OkHttpClient.execute(OkHttpClient.java:172)
	at org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient.lambda$execute$2
	at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329)
	at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:225)
	at org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient.execute
	at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:119)
	... 22 more
Suppressed: java.io.IOException: unexpected end of stream on http://172.19.115.10:8080/...
	at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:202)
	at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:106)
	at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:79)
Caused by: java.io.EOFException: \n not found: limit=0 content=…
	at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:332)
	at okhttp3.internal.http1.HeadersReader.readLine(HeadersReader.kt:29)
	at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:178)
Caused by: java.net.ConnectException: Connection refused
	at sun.nio.ch.Net.pollConnect(Native Method)
	at sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:549)
	at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.kt:295)
// 底层重复连接堆栈省略
1.2 异常堆栈核心特征

日志中出现两层嵌套异常,核心报错如下:

  1. 外层: feign.RetryableException
  2. 根异常:java.net.ConnectException: Connection refused
  3. 被抑制异常(Suppressed):java.io.IOException: unexpected end of stream
两种异常对应实例下线不同阶段

表格

异常类型

含义

实例状态

unexpected end of stream

长连接 TCP 通道正常建立,但服务端执行下线,主动关闭 Socket

实例正在优雅下线,存量连接被强制断开

Connection refused

进程已关闭监听端口,新建 TCP 连接无法握手

实例下线完成,端口销毁

关键误区澄清

很多开发看到 Suppressed 多层异常栈,会误以为程序执行了多次业务重试。事实完全相反:两段连接失败仅发生在单次 Feign 调用内部 OkHttp 连接池逻辑,没有发起第二次业务请求,整套链路全程未触发任何自愈重试。

二、故障根因前置:下游优雅下线漏洞

本次故障的源头并非进程崩溃,而是服务发布时优雅下线流程不完善,存在两处关键缺陷:

  1. 实例向注册中心发送下线注销信号后,未预留足够流量等待窗口期,直接关闭服务监听端口;
  2. 本地 OkHttp 连接池缓存大量未过期长连接,注册中心同步延迟,上游仍持有旧实例地址,继续发起调用;
  3. 旧长连接被服务端主动关闭,新建连接又因端口销毁被拒绝,双重连接异常集中爆发。

补充说明:优雅上下线标准化流程、注册中心等待、容器生命周期适配、流量排空等待等完整解决方案,后续系列独立篇章详细展开。

三、OpenFeign 三层独立重试机制完整拆解

OpenFeign 整套调用链路存在三层完全隔离的重试逻辑,每层生效范围、能力限制完全不同,也是本次故障无法自愈的核心诱因。

3.1 第一层:OkHttp 连接层重试

所属组件RetryAndFollowUpInterceptor

生效范围:仅处理长连接复用层面异常,不会重新发起完整 HTTP 业务请求

执行流程

  1. Feign 调用时从本地连接池取出缓存的旧长连接;
  2. 下游实例正在优雅下线,Socket 已被主动关闭,读取报文抛出unexpected end of stream
  3. OkHttp 识别连接失效,自动清理该陈旧连接,尝试新建 TCP 连接;
  4. 此时实例端口已关闭,新建连接触发Connection refused
  5. Java异常 Suppressed 机制将前一段 EOF 异常附加到最终报错堆栈输出。

核心短板:仅清理失效连接,不触发业务接口重试,无法切换其他健康实例,完全达不到故障自愈。

3.2 第二层:Feign 原生 Retryer 客户端重试

所属组件:feign.Retryer.Default

核心源码逻辑:

代码语言:javascript
复制
while (true) {
    try {
        return executeAndDecode(template, options);
    } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        // 复用原有请求模板,目标IP固定不变
        continue;
    }
}

致命缺陷:即便开启该重试,也只会持续请求已经下线的旧实例,不会重新走负载均衡挑选新节点,下线场景下所有重试请求全部无效;且线上默认配置配置feign.retryEnable=false,默认完全关闭。

3.3 第三层:Spring Cloud LoadBalancer 负载均衡重试

唯一具备切换实例能力的重试层,但存在致命默认限制。核心判断源码:

代码语言:javascript
复制
public boolean canRetry(LoadBalancedRetryContext context) {
    HttpMethod method = context.getRequest().getMethod();
    // 默认retryOnAllOperations=false,仅GET允许重试
    return HttpMethod.GET.equals(method) || properties.getRetry().isRetryOnAllOperations();
}

默认行为

  1. GET 查询接口:网络异常、实例下线场景可触发换实例重试;
  2. POST/PUT/DELETE 等业务常用接口:直接关闭重试逻辑;
  3. 满足重试条件时,会清空当前故障实例,重新执行choose()选取健康节点。

四、本次故障完整链路还原

  1. 下游服务发布,旧实例启动优雅下线,但未预留充足流量排空等待时间;
  2. 实例向注册中心注销,同时快速关闭监听端口;
  3. 注册中心存在同步延迟,上游本地缓存服务列表仍保留已下线实例;
  4. 上游发起 POST 批量 Feign 调用,OkHttp 取出缓存的陈旧长连接;
  5. 下游已主动关闭 Socket,读取报文抛出 EOF 异常;
  6. OkHttp 清理失效连接,尝试新建 TCP 连接,端口关闭触发 Connection refused;
  7. 两层连接异常合并输出 Suppressed 堆栈,但仅为连接池内部行为,无业务重试;
  8. 请求为 POST 类型,Spring LoadBalancer 原生判断不允许重试,不会切换健康实例;
  9. Feign 原生重试默认关闭,整条链路无任何自愈逻辑,任务直接报错。

五、原生三层重试五大致命线上缺陷

结合本次「优雅下线不完善 + 重试失效」双重故障,总结原生 OpenFeign 重试体系生产高危问题:

缺陷 1 POST 接口默认关闭自愈,发布下线必大面积报错

业务批量查询、提交类接口绝大多数为 POST,服务发布、实例下线时无法自动切换健康节点,必须人工介入恢复。

缺陷 2 原生重试不会更换实例,下线节点反复无效调用

Feign 自带 Retryer 仅复用原有实例地址,实例下线后持续打无效请求,浪费线程池资源,放大报错量。

缺陷 3 无幂等安全校验,手动开启重试易造成脏数据 / 资损

若强行打开retryOnAllOperations=true,支付、下单等非幂等 POST 接口超时后会重复提交,框架无任何拦截约束。

缺陷 4 无指数退避机制,下线瞬间流量风暴

大批量任务同时调用下线实例,全部失败后瞬间并发重试,瞬时流量翻倍冲击剩余健康节点,引发连锁抖动。

缺陷 5 配置粒度一刀切,无法单独管控高危接口

仅支持全局、服务级开关,不能针对支付、订单等高危写接口单独关闭重试,线上风险不可精细化管控。

六、风险综合总结

本次事故是双重缺陷叠加:

  1. 变更侧:优雅下线流程存在漏洞,流量未排空直接销毁端口,大量请求集中报错;优雅上下线标准化方案后续独立篇章详细落地;
  2. 调用侧:原生 Feign 重试能力残缺,POST 场景无自愈、无安全兜底、无流量防护。

原生重试框架无法适配发布、节点下线等常规变更场景,亟需一套兼顾安全幂等校验、自动切换实例、阶梯流量防护、精细化配置的自研重试组件。

下篇预告

中篇将落地一套服务端幂等注解 + 调用方 Nacos 动态配置双条件安全重试框架,一次性解决本次复盘暴露的全部缺陷:

  1. 服务端注解标记接口幂等性,从根源禁止非幂等接口重试; 2 POST/GET 统一支持故障切换实例,实例优雅下线场景自动自愈;
  2. 内置指数退避,规避下线瞬间重试流量风暴;
  3. Nacos 分层动态配置,单接口独立管控重试参数; 同时该组件可与后续《优雅上下线》配套方案联动,双重降低发布故障发生率。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 鹏哥调优笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 系列导读
  • 引子:两类发布故障的连锁影响
  • 一、故障现场真实现象
    • 1.1 业务场景
    • 1.2 异常堆栈核心特征
      • 两种异常对应实例下线不同阶段
      • 关键误区澄清
  • 二、故障根因前置:下游优雅下线漏洞
  • 三、OpenFeign 三层独立重试机制完整拆解
    • 3.1 第一层:OkHttp 连接层重试
    • 3.2 第二层:Feign 原生 Retryer 客户端重试
    • 3.3 第三层:Spring Cloud LoadBalancer 负载均衡重试
  • 四、本次故障完整链路还原
  • 五、原生三层重试五大致命线上缺陷
    • 缺陷 1 POST 接口默认关闭自愈,发布下线必大面积报错
    • 缺陷 2 原生重试不会更换实例,下线节点反复无效调用
    • 缺陷 3 无幂等安全校验,手动开启重试易造成脏数据 / 资损
    • 缺陷 4 无指数退避机制,下线瞬间流量风暴
    • 缺陷 5 配置粒度一刀切,无法单独管控高危接口
  • 六、风险综合总结
  • 下篇预告
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档