说一说 SpringCloud Gateway 堆外内存溢出排查

简介: 我是小假 期待与你的下一次相遇 ~

问题描述

报错详情

网关模块偶现 OutOfDirectMemoryError 错误,两次问题出现相隔大概 3 个月。两次发生的时机都是正在大批量接收数据 (大约 500w),TPS 60 左右,网关服务波动不大,完全能扛住,按理不应该出现此错误。

详细报错信息如下:

  1. 2021-05-06 13:44:18|WARN |[reactor-http-epoll-5]|[AbstractChannelHandlerContext.java : 311]|An exception 'io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16384 byte(s) of direct memory (used: 8568993562, max: 8589934592)' [enable DEBUG level for full stacktrace] was thrown by a user handler's exceptionCaught() method while handling the following exception:
  2. io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16384 byte(s) of direct memory (used: 8568993562, max: 8589934592)
  3.        at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:754)
  4.        at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:709)
  5.        at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.allocateDirect(UnpooledUnsafeNoCleanerDirectByteBuf.java:30)
  6.        at io.netty.buffer.UnpooledDirectByteBuf.<init>(UnpooledDirectByteBuf.java:64)
  7.        at io.netty.buffer.UnpooledUnsafeDirectByteBuf.<init>(UnpooledUnsafeDirectByteBuf.java:41)
  8.        at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.<init>(UnpooledUnsafeNoCleanerDirectByteBuf.java:25)
  9.        at io.netty.buffer.UnsafeByteBufUtil.newUnsafeDirectByteBuf(UnsafeByteBufUtil.java:625)
  10.        at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:359)
  11.        at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
  12.        at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
  13.        at io.netty.channel.unix.PreferredDirectByteBufAllocator.ioBuffer(PreferredDirectByteBufAllocator.java:53)
  14.        at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)
  15.        at io.netty.channel.epoll.EpollRecvByteAllocatorHandle.allocate(EpollRecvByteAllocatorHandle.java:75)
  16.        at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:777)
  17.        at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:475)
  18.        at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)
  19.        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
  20.        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
  21.        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
  22.        at java.lang.Thread.run(Thread.java:748)

JVM 配置

  1. -server -Xmx8g -Xms8g -Xmn1024m
  2. -XX:PermSize=512m -Xss256k
  3. -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled
  4. -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m
  5. -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly
  6. -XX:CMSInitiatingOccupancyFraction=70 -Djava.awt.headless=true
  7. -Djava.net.preferIPv4Stack=true

版本信息

Spring cloud : Hoxton.SR5

Spring cloud starter gateway : 2.2.3.RELEASE

Spring boot starter : 2.3.0.RELEASE

Netty : 4.1.54.Final

Reactor-netty: 0.9.7.RELEASE

山重水复疑无路

JVM 参数详解:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

报错的信息是 OutOfDirectMemoryError,即堆外内存不足。

  1. 堆外内存是在 NIO 中使用的;
  2. 堆外内存通过 -XX:MaxDirectMemorySize 参数控制大小,注意和 -XX:+DisableExplicitGC 参数的搭配使用;
  3. JDK8 中堆外内存默认和堆内存一样大(-Xmx);
  4. JDK8 如果配置 -XX:MaxDirectMemorySize 参数,则堆外内存大小以设置的参数为准;

SpringCloudGateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。

网上查阅相关资料,有些场景是因为堆外内存没有手动 release 导致,于是简单查看了网关模块的相关代码发现并无此问题,关键的地方也都调用了相关方法释放内存。堆外内存通过操作堆的命令无法看到,只能监控实例总内存走势判断。

  1. // 释放内存方法
  2. DataBufferUtils.release(dataBuffer);

Dump 堆内存下来也没有发现有什么问题:

柳暗花明又一村

抱着试一试的想法到 SpringCloudGateway 官方仓库 issue 搜索有没有人遇到相同的问题,果不其然,有人提了类似的 issue。https://github.com/spring-cloud/spring-cloud-gateway/issues/1704

在 issue 中开发人员也给出了回应,确实是 SpringCloudGateway 的 BUG!此问题已在 2.2.6.RELEASE 版本中修复。而项目中使用版本为 2.2.3.RELEASE,所以就会出现这个问题。

原因是:包装原生的 pool 后没有释放内存。

https://github.com/spring-cloud/spring-cloud-gateway/milestone/42?closed=1

出乎意料

问题原因已经找到,想着在测试环境复现后升级版本再验证即可。可结果却出乎意料。

  1. 测试环境将堆内存调小尝试进行复现生产问题,在压测将近 1 个小时后出现了同样的问题,复现成功。
  2. 升级 SpringCloudGateway 的版本至 2.2.6.RELEASE。
  3. 重新压测,问题再次出现。

没看错,问题再次出现,且报错信息一模一样。很快又陷入了沉思。

深究原因

排除了组件的问题,剩下的就是代码的问题了,最有可能的就是程序中没有显式调用释放内存导致。

网关模块共定义了三个过滤器,一个全局过滤器 RequestGatewayFilter implements GlobalFilter。两个自定义过滤器 RequestDecryptGatewayFilterFactory extends AbstractGatewayFilterFactoryResponseEncryptGatewayFilterFactory extends AbstractGatewayFilterFactory

依次仔细排查相关逻辑,在全局过滤器 RequestGatewayFilter 中有一块代码引起了注意:

  1. // 伪代码
  2. @Override
  3. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  4.    HttpHeaders headers = request.getHeaders();
  5.    return DataBufferUtils.join(exchange.getRequest().getBody())
  6.        .flatMap(dataBuffer -> {
  7.            DataBufferUtils.retain(dataBuffer);
  8.            Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
  9.            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
  10.                @Override
  11.                public Flux<DataBuffer> getBody() {
  12.                    return cachedFlux;
  13.                }
  14.                @Override
  15.                public HttpHeaders getHeaders() {
  16.                    return headers;
  17.                }
  18.            };
  19.            return chain.filter(exchange.mutate().request(mutatedRequest).build());
  20.        });
  21. }

Request 的 Body 是只能读取一次的,如果直接通过在 Filter 中读取,而不封装回去回导致后面的服务无法读取数据。

此全局过滤器的目的就是把原有的 request 请求中的 body 内容读出来,并且使用ServerHttpRequestDecorator 这个请求装饰器对 request 进行包装,重写 getBody 方法,并把包装后的请求放到过滤器链中传递下去。这样后面的过滤器中再使用 exchange.getRequest().getBody() 来获取 body 时,实际上就是调用的重载后的 getBody() 方法,获取的最先已经缓存了的 body 数据。这样就能够实现 body 的多次读取了。

但是将 DataBuffer 读取出来后并没有手动释内存,会导致堆外内存持续增长。于是添加了一行代码手动释放堆外内存:

  1. DataBufferUtils.release(dataBuffer);
  2. // 伪代码
  3. @Override
  4. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  5.    HttpHeaders headers = request.getHeaders();
  6.    return DataBufferUtils.join(exchange.getRequest().getBody())
  7.        .flatMap(dataBuffer -> {
  8.            byte[] bytes = new byte[dataBuffer.readableByteCount()];
  9.            dataBuffer.read(bytes);
  10.            // 释放堆外内存
  11.            DataBufferUtils.release(dataBuffer);
  12.            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
  13.                @Override
  14.                public Flux<DataBuffer> getBody() {
  15.                    return Flux.defer(() -> {
  16.                        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
  17.                        DataBufferUtils.retain(buffer);
  18.                        return Mono.just(buffer);
  19.                    });
  20.                }
  21.                @Override
  22.                public HttpHeaders getHeaders() {
  23.                    return headers;
  24.                }
  25.            };
  26.            return chain.filter(exchange.mutate().request(mutatedRequest).build());
  27.        });
  28. }

再次压测未出现堆外内存溢出问题。在网络上查询到了类似的案例:

https://github.com/reactor/reactor-netty/issues/788


相关文章
|
3月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
2月前
|
缓存 JSON NoSQL
别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码
小富分享Spring Cloud Gateway内置30+过滤器,涵盖请求、响应、路径、安全等场景,无需重复造轮子。通过配置实现Header处理、限流、重试、熔断等功能,提升网关开发效率,避免代码冗余。
319 1
|
5月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
340 0
|
6月前
|
Java API Nacos
|
12月前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
997 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
10月前
|
前端开发 Java Nacos
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
1743 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
|
12月前
|
JavaScript Java Kotlin
深入 Spring Cloud Gateway 过滤器
Spring Cloud Gateway 是新一代微服务网关框架,支持多种过滤器实现。本文详解了 `GlobalFilter`、`GatewayFilter` 和 `AbstractGatewayFilterFactory` 三种过滤器的实现方式及其应用场景,帮助开发者高效利用这些工具进行网关开发。
1628 1
|
负载均衡 Java Nacos
SpringCloud基础2——Nacos配置、Feign、Gateway
nacos配置管理、Feign远程调用、Gateway服务网关
SpringCloud基础2——Nacos配置、Feign、Gateway
|
负载均衡 Java API
项目中用的网关Gateway及SpringCloud
Spring Cloud Gateway 是一个功能强大、灵活易用的API网关解决方案。通过配置路由、过滤器、熔断器和限流等功能,可以有效地管理和保护微服务。本文详细介绍了Spring Cloud Gateway的基本概念、配置方法和实际应用,希望能帮助开发者更好地理解和使用这一工具。通过合理使用Spring Cloud Gateway,可以显著提升微服务架构的健壮性和可维护性。
661 0

热门文章

最新文章