带 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-instance1zone1-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丢失的问题相关推荐

  1. Spring Cloud升级之路 - Hoxton - 8. 修改实例级别的熔断为实例+方法级别

    实例级别的熔断带来的困扰 如之前系列(Spring Cloud升级之路 - Hoxton - 4. 使用Resilience4j实现实例级别的隔离与熔断)所述,我们实现了实例级别的熔断.但是在生产中发 ...

  2. Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖

    1. 背景知识.需求描述与公共依赖 1.1. 背景知识 & 需求描述 Spring Cloud 官方文档说了,它是一个完整的微服务体系,用户可以通过使用 Spring Cloud 快速搭建一个 ...

  3. Spring Cloud 系列之 Netflix Zuul 服务网关(三)

    本篇文章为系列文章,未读前几集的同学请猛戳这里: Spring Cloud 系列之 Netflix Zuul 服务网关(一) Spring Cloud 系列之 Netflix Zuul 服务网关(二) ...

  4. Spring Cloud 升级最新 Finchley 版本,踩了所有的坑

    转载自   Spring Cloud 升级最新 Finchley 版本,踩了所有的坑 Spring Boot 2.x 已经发布了很久,现在 Spring Cloud 也发布了 基于 Spring Bo ...

  5. 【Spring Cloud 基础设施搭建系列】Spring Cloud Demo项目 Spring Cloud Config Client 失败快速响应与超时重试

    文章目录 Spring Cloud Config Client 失败快速响应与重试 测试 参考 源代码 Spring Cloud Config Client 失败快速响应与重试 Spring Clou ...

  6. docker 安装nacos_「Java Spring Cloud 实战之路」 使用nacos配置网关

    0. 前言 在上一节中,我们创建了一个项目架构,后续的项目都会在那个架构上做补充. 1. Nacos 1.1 简介 Nacos可以用来发现.配置和管理微服务.提供了一组简单易用的特性集,可以快速实现动 ...

  7. Spring Cloud构建微服务架构-服务网关

    通过之前几篇Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了.比如下图所示: 愿意了解源码的朋友直接求求交流分享技术 一零三八七七四六二六 我们使用Sp ...

  8. spring cloud 学习(6) - zuul 微服务网关

    微服务架构体系中,通常一个业务系统会有很多的微服务,比如:OrderService.ProductService.UserService...,为了让调用更简单,一般会在这些服务前端再封装一层,类似下 ...

  9. Spring Cloud Gateway(十):网关过滤器工厂 GatewayFilterFactory

    本文基于 spring cloud gateway 2.0.1 1.GatewayFilterFactory 简介 路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应. 路径过滤器的范 ...

最新文章

  1. 性能测试,负载测试,压力测试以及容量测试的联系与区别--网搜及总结
  2. LOJ#6044. 「雅礼集训 2017 Day8」共(Prufer序列)
  3. “7th-place-solution-microsoft-malware-prediction”——kaggle微软恶意代码检测比赛第七名代码
  4. Selenium3+MySQL数据库进行数据驱动测试
  5. 光遇安卓服务器维护时间,光遇国服安卓和IOS什么时候互通?
  6. 采用MiniProfiler监控EF与.NET MVC项目
  7. Windows 如何完整备份驱动
  8. XTU OJ String game
  9. 收到谷歌实习邀请 “比被清华录取还激动”
  10. python关于模块说法错误的是_python常用模块错题
  11. 框架学习(1)——service层,dao层和service实现类进行数据库操作
  12. python读取EXCEL的方式
  13. 在国内用Windows给BT做种,真是一山绕过一山缠(附解决方案)
  14. Unity3D-设置天空盒
  15. 11G新特性 -- archival(long-term)backups
  16. Xposed去除抖音Toast教程
  17. IBM实习工作(一)
  18. python通达信接口_mootdx: 通达信数据读取 pytdx 的一个简便使用封装
  19. 我的计算机桌面被分成三格,用四宫格管理你的电脑桌面,工作效率蹭蹭蹭的提高...
  20. 深圳市云海麒麟计算机系统,北京云海麒麟容错服务器解决方案

热门文章

  1. 直接访问mysql的BDB存储引擎
  2. 家谱(特殊的层级人物关系)数据结构与自动排版算法的一种实现
  3. mysql 优化总结
  4. Cadence Orcad Capture新建原理图Symbol及新建和添加元件库到工程的方法图文教程及视频演示
  5. 内存的分配与回收实验
  6. mac苹果系统安装虚拟机方法教程 虚拟机操作之一
  7. 仿真及设计工具下载安装方法详细说明
  8. 数字签名(代码签名)流程和数字签名的验证
  9. 交换机路由器网关配置的基本命令代码 Cisco思科
  10. 图解HTTP十一:Web 的攻击技术