如何让Spring应用遇Direct buffer内存OOM时崩溃重启?
我们有一个部署在Cloud Foundry上的Spring应用,主要负责调用各类端点和API。近期遇到一个棘手问题:应用抛出Direct buffer内存类型的OutOfMemoryError后,无法继续执行任务,但仍处于RUNNING状态,超过2小时无响应,只能手动重启。
我们希望实现:当该错误发生时,应用自动崩溃,让Cloud Foundry销毁并重建容器以恢复服务。已经尝试过-XX:+ExitOnOutOfMemoryError和-XX:+CrashOnOutOfMemoryError参数,但发现它们仅对堆内存OOM生效。
考虑过捕获该错误并终止程序,但不确定这是否是最佳实践,且该错误被多层异常包裹。现寻求以下两个方向的解决方案:
- 如何让应用在Direct buffer OOM时自动崩溃重启
- 不重启的情况下,如何让程序恢复响应
堆栈跟踪
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.web.reactive.function.client.WebClientRequestException: java.lang.OutOfMemoryError: Direct buffer memory; nested exception is reactor.netty.ReactorNetty$InternalNettyException: java.lang.OutOfMemoryError: Direct buffer memory Caused by: org.springframework.web.reactive.function.client.WebClientRequestException: java.lang.OutOfMemoryError: Direct buffer memory; nested exception is reactor.netty.ReactorNetty$InternalNettyException: java.lang.OutOfMemoryError: Direct buffer memory at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:141) Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: Error has been observed at the following site(s): *__checkpoint ⇢ Request to POST <random-endpoint> [DefaultWebClient] Original Stack Trace: at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:141) at reactor.core.publisher.MonoErrorSupplied.subscribe(MonoErrorSupplied.java:55) at reactor.core.publisher.Mono.subscribe(Mono.java:4455) at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:103) at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222) at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222) at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222) at reactor.core.publisher.MonoNext$NextSubscriber.onError(MonoNext.java:93) at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onError(MonoFlatMapMany.java:204) at reactor.core.publisher.SerializedSubscriber.onError(SerializedSubscriber.java:124) at reactor.core.publisher.FluxRetryWhen$RetryWhenMainSubscriber.whenError(FluxRetryWhen.java:225) at reactor.core.publisher.FluxRetryWhen$RetryWhenOtherSubscriber.onError(FluxRetryWhen.java:274) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerError(FluxConcatMap.java:309) at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onError(FluxConcatMap.java:875) at reactor.core.publisher.Operators.error(Operators.java:198) at reactor.core.publisher.MonoError.subscribe(MonoError.java:53) at reactor.core.publisher.Mono.subscribe(Mono.java:4455) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:451) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:251) at reactor.core.publisher.EmitterProcessor.drain(EmitterProcessor.java:537) at reactor.core.publisher.EmitterProcessor.tryEmitNext(EmitterProcessor.java:343) at reactor.core.publisher.SinkManySerialized.tryEmitNext(SinkManySerialized.java:100) at reactor.core.publisher.InternalManySink.emitNext(InternalManySink.java:27) at reactor.core.publisher.FluxRetryWhen$RetryWhenMainSubscriber.onError(FluxRetryWhen.java:190) at reactor.core.publisher.MonoCreate$DefaultMonoSink.error(MonoCreate.java:201) at reactor.netty.http.client.HttpClientConnect$HttpObserver.onUncaughtException(HttpClientConnect.java:399) at reactor.netty.ReactorNetty$CompositeConnectionObserver.onUncaughtException(ReactorNetty.java:670) at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onUncaughtException(DefaultPooledConnectionProvider.java:205) at reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnection.onUncaughtException(DefaultPooledConnectionProvider.java:454) at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:232) at reactor.netty.channel.FluxReceive.onInboundError(FluxReceive.java:453) at reactor.netty.channel.ChannelOperations.onInboundError(ChannelOperations.java:488) at reactor.netty.channel.ChannelOperationsHandler.exceptionCaught(ChannelOperationsHandler.java:144) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:346) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:325) at io.netty.channel.AbstractChannelHandlerContext.fireExceptionCaught(AbstractChannelHandlerContext.java:317) at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireExceptionCaught(CombinedChannelDuplexHandler.java:424) at io.netty.channel.ChannelHandlerAdapter.exceptionCaught(ChannelHandlerAdapter.java:92) at io.netty.channel.CombinedChannelDuplexHandler$1.fireExceptionCaught(CombinedChannelDuplexHandler.java:145) at io.netty.channel.ChannelInboundHandlerAdapter.exceptionCaught(ChannelInboundHandlerAdapter.java:143) at io.netty.channel.CombinedChannelDuplexHandler.exceptionCaught(CombinedChannelDuplexHandler.java:231) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:346) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:325) at io.netty.channel.AbstractChannelHandlerContext.fireExceptionCaught(AbstractChannelHandlerContext.java:317) at io.netty.handler.ssl.SslHandler.exceptionCaught(SslHandler.java:1105) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:346) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:325) at io.netty.channel.AbstractChannelHandlerContext.fireExceptionCaught(AbstractChannelHandlerContext.java:317) at io.netty.channel.DefaultChannelPipeline$HeadContext.exceptionCaught(DefaultChannelPipeline.java:1377) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:346) at io.netty.channel.AbstractChannelHandlerContext.invokeExceptionCaught(AbstractChannelHandlerContext.java:325) at io.netty.channel.DefaultChannelPipeline.fireExceptionCaught(DefaultChannelPipeline.java:907) at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.handleReadException(AbstractEpollStreamChannel.java:728) at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:826) at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe$1.run(AbstractEpollChannel.java:425) at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174) at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167) at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470) at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:403) at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Unknown Source) Caused by: reactor.netty.ReactorNetty$InternalNettyException: java.lang.OutOfMemoryError: Direct buffer memory Caused by: java.lang.OutOfMemoryError: Direct buffer memory at java.base/java.nio.Bits.reserveMemory(Unknown Source) at java.base/java.nio.DirectByteBuffer.<init>(Unknown Source) at java.base/java.nio.ByteBuffer.allocateDirect(Unknown Source) at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:701) at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:676) at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:215) at io.netty.buffer.PoolArena.tcacheAllocateNormal(PoolArena.java:197) at io.netty.buffer.PoolArena.allocate(PoolArena.java:139) at io.netty.buffer.PoolArena.allocate(PoolArena.java:129) at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:396) at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188) at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:179) at io.netty.channel.unix.PreferredDirectByteBufAllocator.ioBuffer(PreferredDirectByteBufAllocator.java:53) at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:120) at io.netty.channel.epoll.EpollRecvByteAllocatorHandle.allocate(EpollRecvByteAllocatorHandle.java:75) at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:785) at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe$1.run(AbstractEpollChannel.java:425) at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174) at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167) at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470) at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:403) at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Unknown Source)
一、让应用在Direct buffer OOM时自动崩溃重启
1. 增强JVM OOM处理参数
默认的ExitOnOutOfMemoryError仅针对堆内存,添加以下参数覆盖所有OOM类型:
-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:OnOutOfMemoryError="kill -9 %p"
-XX:+ExitOnOutOfMemoryError:触发任意OOM时退出JVM-XX:+HeapDumpOnOutOfMemoryError:生成堆转储文件用于事后根因分析-XX:OnOutOfMemoryError="kill -9 %p":如果前一个参数未生效,强制杀死进程(%p代表当前进程ID)
2. 全局捕获Reactor异常并终止进程
由于错误被Reactor和Spring的多层异常包裹,需在全局钩子中穿透捕获根因错误:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Hooks; @Configuration public class OomCrashConfig { @Bean public void registerOomHook() { // 处理被丢弃的错误 Hooks.onErrorDropped(error -> handleOom(error)); // 处理操作链中的错误 Hooks.onOperatorError((throwable, context) -> { handleOom(throwable); return throwable; }); } private void handleOom(Throwable throwable) { Throwable rootCause = getRootCause(throwable); if (rootCause instanceof OutOfMemoryError && "Direct buffer memory".equals(rootCause.getMessage())) { // 直接终止JVM,避免进程挂起 Runtime.getRuntime().halt(1); } } private Throwable getRootCause(Throwable throwable) { while (throwable.getCause() != null) { throwable = throwable.getCause(); } return throwable; } }
使用Runtime.getRuntime().halt(1)而非System.exit(1),因为halt会直接终止JVM,不执行shutdown钩子,确保进程快速退出。
3. 配置Cloud Foundry健康检查
让Cloud Foundry主动检测无响应的应用并自动重启:
在manifest.yml中添加健康检查配置:
applications: - name: your-app-name health-check-type: http health-check-http-endpoint: /actuator/health health-check-invocation-timeout: 10s timeout: 120s
当应用无响应时,健康检查失败后Cloud Foundry会自动重建容器。
二、不重启恢复响应的临时方案(建议长期解决根源问题)
1. 限制Direct buffer最大内存
通过JVM参数设置Direct buffer的上限:
-XX:MaxDirectMemorySize=256m
根据应用实际流量调整大小,避免无限制占用系统内存。
2. 优化Reactor Netty资源管理
- 配置合理的WebClient连接池,避免连接泄漏:
import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; import java.time.Duration; @Configuration public class WebClientConfig { @Bean public WebClient webClient() { ConnectionProvider connectionProvider = ConnectionProvider.builder("webclient-pool") .maxConnections(100) .pendingAcquireTimeout(Duration.ofSeconds(10)) .maxIdleTime(Duration.ofSeconds(30)) .build(); HttpClient httpClient = HttpClient.create(connectionProvider) .responseTimeout(Duration.ofSeconds(15)); return WebClient.builder() .clientConnector(new reactor.netty.http.client.ReactorClientHttpConnector(httpClient)) .build(); } }
- 确保响应体被完全消费:使用
bodyToMono或bodyToFlux后,必须订阅并完成处理,避免Direct buffer因未释放而泄漏。
3. 监控Direct buffer使用情况
添加Micrometer监控指标,提前预警内存占用:
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MetricsConfig { @Bean public JvmMemoryMetrics jvmMemoryMetrics(MeterRegistry meterRegistry) { JvmMemoryMetrics metrics = new JvmMemoryMetrics(); metrics.bindTo(meterRegistry); return metrics; } }
通过监控平台观察jvm_buffer_memory_used_bytes指标,当Direct buffer占用接近上限时触发告警,提前干预。
内容的提问来源于stack exchange,提问作者Denis

