本系列围绕稳定性容错四大核心能力:超时、重试、限流、熔断 分上中下三篇完整落地讲解重试:
线上稳定性事故中,80% 的故障根源都和变更发布强相关,其中两大高频痛点:一是重试机制设计缺陷,节点下线后无法自动自愈;二是服务优雅上下线流程缺失,下线流量未排空就销毁实例。
本次故障正是「优雅下线不完善 + Feign 原生重试能力残缺」双重问题叠加导致。本文先完整复盘节点下线引发的调用雪崩问题,后续系列文章会专门推出《优雅上下线》专题,从注册中心、流量等待、容器销毁、网关多级视角完整拆解标准化落地方案。
上游服务通过 OpenFeign 发起 POST 批量查询接口调用,下游服务执行发布迭代,实例执行优雅下线流程,但下线逻辑存在漏洞,流量未完全排空就关闭 TCP 端口。
业务侧批量任务持续大面积报错,无任何自动自愈逻辑,只能等待新实例完全启动、旧实例全部摘除后手动重试任务恢复。
异常栈信息
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)
// 底层重复连接堆栈省略日志中出现两层嵌套异常,核心报错如下:
feign.RetryableExceptionjava.net.ConnectException: Connection refusedjava.io.IOException: unexpected end of stream表格
异常类型 | 含义 | 实例状态 |
|---|---|---|
unexpected end of stream | 长连接 TCP 通道正常建立,但服务端执行下线,主动关闭 Socket | 实例正在优雅下线,存量连接被强制断开 |
Connection refused | 进程已关闭监听端口,新建 TCP 连接无法握手 | 实例下线完成,端口销毁 |
很多开发看到 Suppressed 多层异常栈,会误以为程序执行了多次业务重试。事实完全相反:两段连接失败仅发生在单次 Feign 调用内部 OkHttp 连接池逻辑,没有发起第二次业务请求,整套链路全程未触发任何自愈重试。
本次故障的源头并非进程崩溃,而是服务发布时优雅下线流程不完善,存在两处关键缺陷:
补充说明:优雅上下线标准化流程、注册中心等待、容器生命周期适配、流量排空等待等完整解决方案,后续系列独立篇章详细展开。
OpenFeign 整套调用链路存在三层完全隔离的重试逻辑,每层生效范围、能力限制完全不同,也是本次故障无法自愈的核心诱因。
所属组件:RetryAndFollowUpInterceptor
生效范围:仅处理长连接复用层面异常,不会重新发起完整 HTTP 业务请求。
执行流程:
unexpected end of stream;Connection refused;核心短板:仅清理失效连接,不触发业务接口重试,无法切换其他健康实例,完全达不到故障自愈。
所属组件:feign.Retryer.Default
核心源码逻辑:
while (true) {
try {
return executeAndDecode(template, options);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
// 复用原有请求模板,目标IP固定不变
continue;
}
}
致命缺陷:即便开启该重试,也只会持续请求已经下线的旧实例,不会重新走负载均衡挑选新节点,下线场景下所有重试请求全部无效;且线上默认配置配置feign.retryEnable=false,默认完全关闭。
唯一具备切换实例能力的重试层,但存在致命默认限制。核心判断源码:
public boolean canRetry(LoadBalancedRetryContext context) {
HttpMethod method = context.getRequest().getMethod();
// 默认retryOnAllOperations=false,仅GET允许重试
return HttpMethod.GET.equals(method) || properties.getRetry().isRetryOnAllOperations();
}默认行为:
choose()选取健康节点。结合本次「优雅下线不完善 + 重试失效」双重故障,总结原生 OpenFeign 重试体系生产高危问题:
业务批量查询、提交类接口绝大多数为 POST,服务发布、实例下线时无法自动切换健康节点,必须人工介入恢复。
Feign 自带 Retryer 仅复用原有实例地址,实例下线后持续打无效请求,浪费线程池资源,放大报错量。
若强行打开retryOnAllOperations=true,支付、下单等非幂等 POST 接口超时后会重复提交,框架无任何拦截约束。
大批量任务同时调用下线实例,全部失败后瞬间并发重试,瞬时流量翻倍冲击剩余健康节点,引发连锁抖动。
仅支持全局、服务级开关,不能针对支付、订单等高危写接口单独关闭重试,线上风险不可精细化管控。
本次事故是双重缺陷叠加:
原生重试框架无法适配发布、节点下线等常规变更场景,亟需一套兼顾安全幂等校验、自动切换实例、阶梯流量防护、精细化配置的自研重试组件。
中篇将落地一套服务端幂等注解 + 调用方 Nacos 动态配置双条件安全重试框架,一次性解决本次复盘暴露的全部缺陷: