Redis 报错“OutOfDirectMemoryError(堆外内存溢出) ”问题

Redis 报错“OutOfDirectMemoryError(堆外内存溢出) ”问题如下:

一、复现条件

  1. 准备环境 :SpringBoot 整合 Redis
  2. 测试服务,设置 VM 参数 -Xmx100m 启动。
  3. JMeter 压测某个业务接口,并发 200

二、异常情况——Lettuce客户端 Netty框架内部原因导致。

1、压测

使用 Redis 的业务接口 ,产生 OutOfDirectMemoryError(堆外内存溢出),如图:

压测示意图

压测报错信息

2、详细错误信息:

​ “lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296

{"timestamp":"2022-06-04 00:07:56","status":500,"error":"Internal Server Error","message":"Redis exception; nested exception is io.lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)","trace":"org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)\r\n\tat org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74)\r\n\tat org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)\r\n\tat org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)\r\n\tat org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)\r\n\tat org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:268)\r\n\tat org.springframework.data.redis.connection.lettuce.LettuceStringCommands.convertLettuceAccessException(LettuceStringCommands.java:799)\r\n\tat org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68)\r\n\tat org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:253)\r\n\tat org.springframework.data.redis.connection.DefaultStringRedisConnection.get(DefaultStringRedisConnection.java:377)\r\n\tat org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperations.java:57)\r\n\tat org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60)\r\n\tat org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:225)\r\n\tat org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:185)\r\n\tat org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96)\r\n\tat org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53)\r\n\tat com.atguigu.gulimall.product.service.impl.CategoryServiceImpl.getCatelogJson(CategoryServiceImpl.java:113)\r\n\tat com.atguigu.gulimall.product.service.impl.CategoryServiceImpl$$FastClassBySpringCGLIB$$32451e6b.invoke(<generated>)\r\n\tat org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)\r\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:687)\r\n\tat com.atguigu.gulimall.product.service.impl.CategoryServiceImpl$$EnhancerBySpringCGLIB$$b2641cc9.getCatelogJson(<generated>)\r\n\tat com.atguigu.gulimall.product.web.IndexController.getCatalogJson(IndexController.java:44)\r\n\tat sun.reflect.GeneratedMethodAccessor190.invoke(Unknown Source)\r\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat java.lang.reflect.Method.invoke(Method.java:497)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:798)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:634)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\r\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:94)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\r\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\r\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.lang.Thread.run(Thread.java:745)\r\nCaused by: io.lettuce.core.RedisException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)\r\n\tat io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:129)\r\n\tat io.lettuce.core.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:69)\r\n\tat io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80)\r\n\tat com.sun.proxy.$Proxy169.get(Unknown Source)\r\n\tat org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:66)\r\n\t... 67 more\r\nCaused by: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)\r\n\tat io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:754)\r\n\tat io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:709)\r\n\tat io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:755)\r\n\tat io.netty.buffer.PoolArena$DirectArena.newUnpooledChunk(PoolArena.java:745)\r\n\tat io.netty.buffer.PoolArena.allocateHuge(PoolArena.java:262)\r\n\tat io.netty.buffer.PoolArena.allocate(PoolArena.java:232)\r\n\tat io.netty.buffer.PoolArena.reallocate(PoolArena.java:394)\r\n\tat io.netty.buffer.PooledByteBuf.capacity(PooledByteBuf.java:118)\r\n\tat io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:306)\r\n\tat io.netty.buffer.AbstractByteBuf.ensureWritable(AbstractByteBuf.java:282)\r\n\tat io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1104)\r\n\tat io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1097)\r\n\tat io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1088)\r\n\tat io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:554)\r\n\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\r\n\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\r\n\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\r\n\tat io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)\r\n\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\r\n\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\r\n\tat io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)\r\n\tat io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)\r\n\tat io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)\r\n\tat io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)\r\n\tat io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)\r\n\tat io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)\r\n\tat io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)\r\n\tat io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)\r\n\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\r\n\t... 1 more\r\n","path":"/index/catalog.json"}

3、源码分析:

 public final class PlatformDependent {
     // 直接内存大小,可通过 “-Dio.netty.maxDirectMemory”参数设置。
     private static final long DIRECT_MEMORY_LIMIT;
     
     // 默认的最大直接内存大小 ,方法略。大概意思还是:先获取“Eclipse OpenJ9”的“sun.misc.VM”参数,如果没有则获取JVM的“-XX:MaxDirectMemorySize”作为默认值。
     private static final long MAX_DIRECT_MEMORY = maxDirectMemory0();
     
     static {
        // 其他赋值操作,略
         
      // 给 直接内存大小赋值,如果有设置 "-Dio.netty.maxDirectMemory" 参数,则使用用户设置的,如果没有则使用默认的
      logger.debug("-Dio.netty.maxDirectMemory: {} bytes", maxDirectMemory);
      DIRECT_MEMORY_LIMIT = maxDirectMemory >= 1 ? maxDirectMemory : MAX_DIRECT_MEMORY;
                
     }

    private static void incrementMemoryCounter(int capacity) {
        if (DIRECT_MEMORY_COUNTER != null) {
            long newUsedMemory = DIRECT_MEMORY_COUNTER.addAndGet(capacity);
            
            //关键判断:如果 netty内部使用的内存大小 大于 “直接内存大小”的话,就抛出 "OutOfDirectMemoryError"异常。
            if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
                DIRECT_MEMORY_COUNTER.addAndGet(-capacity);
                throw new OutOfDirectMemoryError("failed to allocate " + capacity
                                                 + " byte(s) of direct memory (used: " + (newUsedMemory - capacity)
                                                 + ", max: " + DIRECT_MEMORY_LIMIT + ')');
            }
        }
    }
 }    

4、总结原因:

1)、Springboot 2.x 以后默认使用 Lettuce作为操作 redis 的客户端。它是使用 netty 进行网络通信的。

2)、从spring-boot-starter-data-redis(2.1.14.RELEASE) 依赖可以看出内置使用的确实是 Lettuce 客户端,分析源码得知,lettuce 使用的 netty 框架,引用的netty包netty-common-4.1.49.Final.jar里面有一个PlatformDependent.java类 ,底层有个-Dio.netty.maxDirectMemory 参数,会自己校验堆外内存是否大于当前服务可使用的内存,如果大于则抛出 OutOfDirectMemoryError(堆外内存溢出)。显然,这是属于 Netty(netty-common-4.1.49.Final.jar)的bug导致堆外内存溢出的。

5、解决方案:

​ 不能使用-Dio.netty.maxDirectMemory 只调大堆外内存,这只能延迟bug出现的时机,不能完全解决该问题。想解决有如下两个方案:

  1. 升级 Lettuce客户端,期待新版本会解决该问题。

  2. 排除 Lettuce客户端,切换使用没有该问题的 Jedis 客户端。

    PS: Netty 框架性能更好,吞吐量更大,但是Springboot默认使用的Lettuce 客户端对Netty的支持不够好;

    Jedis客户端虽然没有Netty更快,但胜在稳定,没有上述bug。

spring-boot-starter-data-redis 依赖 切换使用 Jedis客户端
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.14.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<!-- 引入 redis 依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 排除 lettuce 客戶端,引入 Jedis客戶端-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

二、正常情况——其他原因综合导致

1、压测

​ Redis 使用 Jedis 客户端后,继续 Jmeter压测,200并发,提示“java.lang.OutOfMemoryError: GC overhead limit exceeded”。

2、详细错误信息:

Exception in thread "File Watcher" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.String.toLowerCase(String.java:2578)
	at java.io.WinNTFileSystem.hashCode(WinNTFileSystem.java:640)
	at java.io.File.hashCode(File.java:2132)
	at org.springframework.boot.devtools.filewatch.FileSnapshot.hashCode(FileSnapshot.java:72)
	at java.util.HashMap.hash(HashMap.java:338)
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:70)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67)
	at org.springframework.boot.devtools.filewatch.FolderSnapshot.<init>(FolderSnapshot.java:58)
	at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.getCurrentSnapshots(FileSystemWatcher.java:280)
	at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.scan(FileSystemWatcher.java:254)
	at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.run(FileSystemWatcher.java:239)
	at java.lang.Thread.run(Thread.java:745)
2022-06-04 01:34:33.183 ERROR 11988 --- [o-10000-exec-27] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause

java.lang.OutOfMemoryError: Java heap space

3、Jvisualvm - Visual GC 界面分析:

测试服务前,设置启动参数 VM options: -Xmx100m ,意思是本次测试的服务 JVM 最大堆内存只有 100M。

Jvisualvm - Visual GC 界面

这次虽然还是 OutOfMemoryError,但是原因已经变了,变成了“GC overhead limit exceeded”,已经没有前面由于框架问题导致的异常信息了。这次异常是属于“正常情况”。

4、真正造成java.lang.OutOfMemoryError: GC overhead limit exceeded的原因:

由于本次测试设置了服务JVM堆内存最大只有 100M。测试的并发数 200,瞬间吞吐量压力过大,导致Old Gen 区直接占满。

JVM的GC过程会因为STW,只不过停顿短到不容易感知。当引起停顿时间的98%都是在进行 GC,但是结果只能得到小于2%的堆内存恢复时,就会抛出 java.lang.OutOfMemoryError: GC overhead limit exceeded这个错误。

这个错误其实就是空闲内存与GC之间平衡的一个限制,当经过几次GC之后,只有少于2%的内存被释放,也就是很少的空闲内存,可能会再次被快速填充,这样就会触发再一次的GC。这就是一个恶性循环了,CPU大部分的时间在做GC操作,没有时间做具体的业务操作,可能几毫秒的任务需要几分钟都无法完成,整个应用程序就形同虚设了。

5、解决方案

我这里主要是限制了JVM `-Xmx100m`模拟高并发场景导致的,重启一下服务或者调大`-Xmx`,减少并发数,都是能解决的。

​ 而实际生产环境会有更多复杂的因素影响综合导致,因此要考虑有哪些原因会造成“性能瓶颈”,具体情况要具体分析;

​ 造成性能瓶颈往往在“中间件的选取 ”和 业务代码的编写(业务代码主要表现在数据库的IO操作过多或者数据量多大),可以优先从这两个方面入手优化,最后才是硬件上的扩容升级。

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值