Spring Cloud升级之路 - Hoxton - 10. 网关重试带Body的请求Body丢失的问题
带 Body 的重试 Body 丢失
之前我们的配置里面,只对 Get 请求针对 500 响应码重试,但是针对像 Post 这样的请求,只对那种根本还没到发送 Body 的阶段的异常(例如连接异常)这种重试,那么如果我们要对带 Body 的 Post 进行重试呢?或者就是用 Postman 构建一个带 Body 的 Get 请求,重试是否正常呢?
我们启动之前第6节的 EurekaServer,修改/test-exception-thrown
接口,增加 RequestBody 参数:
@RequestMapping(value = "/test-exception-thrown", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})
public String testExceptionThrown(HttpServletRequest httpServletRequest, @RequestBody Map<String, String> body) {log.info("testExceptionThrow called {}, {}", httpServletRequest.getMethod(), body);if (shouldThrowException) {throw new IllegalStateException();}return zone;
}
启动zone1-service-provider-instance1
,zone1-service-provider-instance2
,其中,zone1-service-provider-instance1
是接口访问会抛出异常的那个实例。启动网关,使用 Postman 调用接口,发现出现重试,请求先发送到了zone1-service-provider-instance1
,之后重试到了zone1-service-provider-instance2
,但是zone1-service-provider-instance2
返回 400 错误,也就是没有收到 RequestBody,这是怎么回事呢?
Api网关调用日志
2020-07-28 01:55:29.781 INFO [service-api-gateway,fc71e34f22e1bd17,fc71e34f22e1bd17][7860] [boundedElastic-1][com.github.hashjang.hoxton.api.gateway.filter.InstanceCircuitBreakerFilter:54]: try to send request to: http://192.168.0.142:8001/test-exception-thrown: stats: {"numberOfNotPermittedCalls":0,"numberOfSlowCalls":0,"numberOfBufferedCalls":0,"slowCallRate":-1.0,"failureRate":-1.0,"numberOfSuccessfulCalls":0,"numberOfFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSlowFailedCalls":0}
2020-07-28 01:55:30.115 INFO [service-api-gateway,fc71e34f22e1bd17,fc71e34f22e1bd17][7860] [boundedElastic-1][com.github.hashjang.hoxton.api.gateway.filter.InstanceCircuitBreakerFilter:54]: try to send request to: http://192.168.0.142:8001/test-exception-thrown: stats: {"numberOfNotPermittedCalls":0,"numberOfSlowCalls":0,"numberOfBufferedCalls":1,"slowCallRate":-1.0,"failureRate":-1.0,"numberOfSuccessfulCalls":1,"numberOfFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSlowFailedCalls":0}
zone1-service-provider-instance1
日志:
2020-07-28 01:55:29.789 ERROR[service-provider,,] [24956][XNIO-2 task-4][io.undertow.servlet.api.LoggingExceptionHandler:80]:UT005023: Exception handling request to /test-exception-thrown
zone1-service-provider-instance2
日志:
2020-07-28 01:55:30.133 WARN[service-provider,fc71e34f22e1bd17,da6d3f91fcfc053f] [24956][XNIO-2 task-5][org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver:199]:Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.lang.String com.github.hashjang.hoxton.service.provider.controller.TestServiceController.testExceptionThrown(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)]
为了定位问题,我们添加一个放在最开头的 LogFilter,开启 Body 的追踪:
@Component
public class LogFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return chain.filter(exchange.mutate().request(new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic Flux<DataBuffer> getBody() {//body开启日志,记录操作body的filterreturn exchange.getRequest().getBody().log();}}).build());}@Overridepublic int getOrder() {//放在最开头return Ordered.HIGHEST_PRECEDENCE;}
}
重启网关,发送请求:
2020-07-28 02:22:16.026 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [boundedElastic-1][com.github.hashjang.hoxton.api.gateway.filter.InstanceCircuitBreakerFilter:54]: try to send request to: http://192.168.0.142:8001/test-exception-thrown: stats: {"numberOfNotPermittedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"numberOfSlowFailedCalls":0,"numberOfFailedCalls":0,"failureRate":-1.0,"slowCallRate":-1.0,"numberOfBufferedCalls":0,"numberOfSlowCalls":0}
2020-07-28 02:22:16.034 INFO [service-api-gateway,,][4408] [reactor-http-nio-4][reactor.util.Loggers$Slf4JLogger:274]: onContextUpdate(Context3{class brave.propagation.TraceContext=1ae80e0b643da3c7/1ae80e0b643da3c7, class org.springframework.cloud.sleuth.instrument.web.client.HttpClientBeanPostProcessor$CurrentClientSpan=NoopSpan(1ae80e0b643da3c7/38ecc8fd2b789c2e), reactor.onDiscard.local=reactor.core.publisher.Operators$$Lambda$1179/0x00000008019d1840@469db4af})
2020-07-28 02:22:16.034 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onSubscribe([Fuseable] ScopePassingSpanSubscriber)
2020-07-28 02:22:16.034 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: request(unbounded)
2020-07-28 02:22:16.035 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onNext(PooledSlicedByteBuf(ridx: 0, widx: 10, cap: 10/10, unwrapped: PooledUnsafeDirectByteBuf(ridx: 326, widx: 326, cap: 1024)))
2020-07-28 02:22:16.035 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onComplete()
2020-07-28 02:22:16.165 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [boundedElastic-1][com.github.hashjang.hoxton.api.gateway.filter.InstanceCircuitBreakerFilter:54]: try to send request to: http://192.168.0.142:8002/test-exception-thrown: stats: {"numberOfNotPermittedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"numberOfSlowFailedCalls":0,"numberOfFailedCalls":0,"failureRate":-1.0,"slowCallRate":-1.0,"numberOfBufferedCalls":0,"numberOfSlowCalls":0}
2020-07-28 02:22:16.169 INFO [service-api-gateway,,][4408] [reactor-http-nio-3][reactor.util.Loggers$Slf4JLogger:274]: onContextUpdate(Context3{class brave.propagation.TraceContext=1ae80e0b643da3c7/1ae80e0b643da3c7, class org.springframework.cloud.sleuth.instrument.web.client.HttpClientBeanPostProcessor$CurrentClientSpan=NoopSpan(1ae80e0b643da3c7/d05978ee5d1cb64c), reactor.onDiscard.local=reactor.core.publisher.Operators$$Lambda$1179/0x00000008019d1840@3a55aa8})
2020-07-28 02:22:16.170 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onSubscribe([Fuseable] ScopePassingSpanSubscriber)
2020-07-28 02:22:16.170 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: request(unbounded)
2020-07-28 02:22:16.170 INFO [service-api-gateway,1ae80e0b643da3c7,1ae80e0b643da3c7][4408] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onComplete()
我们发现,这个 Body 的 Flux 在重试的时候,使用的还是原来同样的 Flux,但是这个 Flux 已经被第一次调用消费过了,所以重试的时候,再去消费,直接返回消费完成,不会有:onNext(PooledSlicedByteBuf(ridx: 0, widx: 10, cap: 10/10, unwrapped: PooledUnsafeDirectByteBuf(ridx: 326, widx: 326, cap: 1024)))
那么如何解决呢?有两种方式,一种是自己实现 Body 缓存,参考我提的 Issue + PR(https://github.com/spring-cloud/spring-cloud-gateway/pull/1863),但是这实际上是我的乌龙,我没注意到 Spring Cloud Gateway实际上已经实现了:
Publisher<Void> publisher = chain.filter(exchange.mutate().request(new ServerHttpRequestDecorator(request) {@Overridepublic Flux<DataBuffer> getBody() {int currentIteration = exchange.getAttributeOrDefault(RETRY_ITERATION_KEY, -1);//根据currentIteration判断是否是重试,如果不是,就返回原始Request 的 body Flux//如果是,则返回缓存的String重新生成的Flux,保证重试也有正确的bodyreturn currentIteration > -1 ? Flux.from(Mono.just(dataBufferFactory.wrap(((String) exchange.getAttributes().get(BODY)).getBytes()))) :request.getBody().map(dataBuffer -> {if (LEGAL_LOG_MEDIA_TYPES.contains(contentType)) {try {String body = (String) exchange.getAttributes().get(BODY);if (body == null) {byte[] content = new byte[dataBuffer.readableByteCount()];try {dataBuffer.read(content);} finally {DataBufferUtils.release(dataBuffer);}String s = new String(content, Charset.defaultCharset());exchange.getAttributes().put(BODY, s);dataBuffer = dataBufferFactory.wrap(s.getBytes());} else {dataBuffer = dataBufferFactory.wrap(body.getBytes());}} catch (Exception e) {log.error("error read body in retry", e);}}return dataBuffer;});}}
).build())
另一种是使用 Spring Cloud Gateway 已有的缓存机制AdaptCachedBodyGlobalFilter
:
AdaptCachedBodyGlobalFilter
源码:
public class AdaptCachedBodyGlobalFilterimplements GlobalFilter, Ordered, ApplicationListener<EnableBodyCachingEvent> {/*** 缓存RequestBody的Route*/private ConcurrentMap<String, Boolean> routesToCache = new ConcurrentHashMap<>();/*** 缓存RequestBody的Attribute Key*/@Deprecatedpublic static final String CACHED_REQUEST_BODY_KEY = CACHED_REQUEST_BODY_ATTR;/*** 收到EnableBodyCachingEvent,则将EnableBodyCachingEvent中的RouteId加入到要缓存的Route的Map*/@Overridepublic void onApplicationEvent(EnableBodyCachingEvent event) {this.routesToCache.putIfAbsent(event.getRouteId(), true);}//。。。。略
}
由于我们是全局的重试,所以可以对每一个Route
都加上缓存 Body 的机制,所以可以这么实现:
ApiGatewayConfig
@Configuration
@EnableConfigurationProperties(ApiGatewayRetryConfig.class)
@LoadBalancerClients(defaultConfiguration = CommonLoadBalancerConfig.class)
public class ApiGatewayConfig {@Autowiredprivate AdaptCachedBodyGlobalFilter adaptCachedBodyGlobalFilter;@Autowiredprivate GatewayProperties gatewayProperties;@PostConstructpublic void init() {//让每一个路径都做body Cache,这样重试有Body的请求的时候,重试的请求不会没有body,因为原始body是一次性的基于netty的FluxReceivegatewayProperties.getRoutes().forEach(routeDefinition -> {EnableBodyCachingEvent enableBodyCachingEvent = new EnableBodyCachingEvent(new Object(), routeDefinition.getId());adaptCachedBodyGlobalFilter.onApplicationEvent(enableBodyCachingEvent);});}
}
这样修改后,重启网关,我们再调用触发重试:
2020-07-28 02:48:18.972 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onContextUpdate(Context2{class brave.propagation.TraceContext=72eba79a3afc324f/72eba79a3afc324f, reactor.onDiscard.local=reactor.core.publisher.Operators$$Lambda$1041/0x0000000801979440@119927bc})
2020-07-28 02:48:18.972 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onSubscribe([Fuseable] ScopePassingSpanSubscriber)
2020-07-28 02:48:18.973 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: request(unbounded)
2020-07-28 02:48:18.973 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onNext(PooledSlicedByteBuf(ridx: 0, widx: 10, cap: 10/10, unwrapped: PooledUnsafeDirectByteBuf(ridx: 326, widx: 326, cap: 512)))
2020-07-28 02:48:18.974 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger:274]: onComplete()
2020-07-28 02:48:18.986 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [boundedElastic-1][com.github.hashjang.hoxton.api.gateway.filter.InstanceCircuitBreakerFilter:54]: try to send request to: http://192.168.0.142:8001/test-exception-thrown: stats: {"numberOfNotPermittedCalls":0,"failureRate":-1.0,"slowCallRate":-1.0,"numberOfSlowFailedCalls":0,"numberOfFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"numberOfBufferedCalls":0,"numberOfSlowCalls":0}
2020-07-28 02:48:19.138 INFO [service-api-gateway,72eba79a3afc324f,72eba79a3afc324f][6784] [boundedElastic-1][com.github.hashjang.hoxton.api.gateway.filter.InstanceCircuitBreakerFilter:54]: try to send request to: http://192.168.0.142:8002/test-exception-thrown: stats: {"numberOfNotPermittedCalls":0,"failureRate":-1.0,"slowCallRate":-1.0,"numberOfSlowFailedCalls":0,"numberOfFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":1,"numberOfBufferedCalls":1,"numberOfSlowCalls":0}
发现重试调用,Body没有丢,重试成功了
Spring Cloud升级之路 - Hoxton - 10. 网关重试带Body的请求Body丢失的问题相关推荐
- Spring Cloud升级之路 - Hoxton - 8. 修改实例级别的熔断为实例+方法级别
实例级别的熔断带来的困扰 如之前系列(Spring Cloud升级之路 - Hoxton - 4. 使用Resilience4j实现实例级别的隔离与熔断)所述,我们实现了实例级别的熔断.但是在生产中发 ...
- Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖
1. 背景知识.需求描述与公共依赖 1.1. 背景知识 & 需求描述 Spring Cloud 官方文档说了,它是一个完整的微服务体系,用户可以通过使用 Spring Cloud 快速搭建一个 ...
- Spring Cloud 系列之 Netflix Zuul 服务网关(三)
本篇文章为系列文章,未读前几集的同学请猛戳这里: Spring Cloud 系列之 Netflix Zuul 服务网关(一) Spring Cloud 系列之 Netflix Zuul 服务网关(二) ...
- Spring Cloud 升级最新 Finchley 版本,踩了所有的坑
转载自 Spring Cloud 升级最新 Finchley 版本,踩了所有的坑 Spring Boot 2.x 已经发布了很久,现在 Spring Cloud 也发布了 基于 Spring Bo ...
- 【Spring Cloud 基础设施搭建系列】Spring Cloud Demo项目 Spring Cloud Config Client 失败快速响应与超时重试
文章目录 Spring Cloud Config Client 失败快速响应与重试 测试 参考 源代码 Spring Cloud Config Client 失败快速响应与重试 Spring Clou ...
- docker 安装nacos_「Java Spring Cloud 实战之路」 使用nacos配置网关
0. 前言 在上一节中,我们创建了一个项目架构,后续的项目都会在那个架构上做补充. 1. Nacos 1.1 简介 Nacos可以用来发现.配置和管理微服务.提供了一组简单易用的特性集,可以快速实现动 ...
- Spring Cloud构建微服务架构-服务网关
通过之前几篇Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了.比如下图所示: 愿意了解源码的朋友直接求求交流分享技术 一零三八七七四六二六 我们使用Sp ...
- spring cloud 学习(6) - zuul 微服务网关
微服务架构体系中,通常一个业务系统会有很多的微服务,比如:OrderService.ProductService.UserService...,为了让调用更简单,一般会在这些服务前端再封装一层,类似下 ...
- Spring Cloud Gateway(十):网关过滤器工厂 GatewayFilterFactory
本文基于 spring cloud gateway 2.0.1 1.GatewayFilterFactory 简介 路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应. 路径过滤器的范 ...
最新文章
- 性能测试,负载测试,压力测试以及容量测试的联系与区别--网搜及总结
- LOJ#6044. 「雅礼集训 2017 Day8」共(Prufer序列)
- “7th-place-solution-microsoft-malware-prediction”——kaggle微软恶意代码检测比赛第七名代码
- Selenium3+MySQL数据库进行数据驱动测试
- 光遇安卓服务器维护时间,光遇国服安卓和IOS什么时候互通?
- 采用MiniProfiler监控EF与.NET MVC项目
- Windows 如何完整备份驱动
- XTU OJ String game
- 收到谷歌实习邀请 “比被清华录取还激动”
- python关于模块说法错误的是_python常用模块错题
- 框架学习(1)——service层,dao层和service实现类进行数据库操作
- python读取EXCEL的方式
- 在国内用Windows给BT做种,真是一山绕过一山缠(附解决方案)
- Unity3D-设置天空盒
- 11G新特性 -- archival(long-term)backups
- Xposed去除抖音Toast教程
- IBM实习工作(一)
- python通达信接口_mootdx: 通达信数据读取 pytdx 的一个简便使用封装
- 我的计算机桌面被分成三格,用四宫格管理你的电脑桌面,工作效率蹭蹭蹭的提高...
- 深圳市云海麒麟计算机系统,北京云海麒麟容错服务器解决方案