Spring Boot中WebClient导致OutOfMemoryError的解决方法

Spring Boot是一个非常流行的 Java 企业应用程序框架。与内部或外部应用程序集成的一种常见方法是通过 HTTP REST 连接。我们从RestTemplate升级到基于 Java NIO 的WebClient,它可以通过在调用 REST 服务端点时允许并发来显着提高应用程序性能。WebClient 的好处如下:

  • 并发性: WebClient 能够同时处理多个连接,而不会阻塞线程,从而实现更好的并发性。
  • 异步:异步编程允许应用程序在等待 I/O 操作完成的同时执行其他任务,从而提高整体效率。
  • 性能:非阻塞 I/O 可以用更少的线程管理更多的连接,从而减少处理并发请求所需的资源。

尽管性能有所提高,但在并发连接数相同的情况下,WebClient 会崩溃OutOfMemoryError。我们将分析 WebClient 崩溃问题以及如何排除故障和修复它们。

为了利用 NIO 的优势(如并发和异步处理),我们将其余客户端调用从 Spring RestTemplate 升级到了 WebClient,如下所示。

Spring RestTemplate

public void restClientCall(Integer id, String url,String imagePath) {

        // Create RestTemplate instance
        RestTemplate restTemplate = new RestTemplate();

        
// Prepare the image file
        File imageFile = new File(imagePath);

        
// Prepare headers
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        
// Prepare the request body
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add(
"file", new org.springframework.core.io.FileSystemResource(imageFile));

        
// 创建带有标题和多部分主体的 HTTP 实体
        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

        System.out.println(
"Starting to post an image for Id"+id);

        
// Perform the POST request
        ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestEntity, 
String.class);

        
// Print the response status code and body
        System.out.println(
"Response Id "+id +":"+ responseEntity.getBody());
        System.out.println(
" Time: " + LocalTime.now());
  }

下面是 Spring WebClient:

public void webHeavyClientCall(Integer id,String url, String imagePath) {

    // Create a WebClient instance
    WebClient webClient = WebClient.create();

    
// Prepare the image file
    File imageFile = new File(imagePath);

    
//执行 POST 请求,将图片作为请求正文的一部分
    MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
    body.add(
"file", new FileSystemResource(imageFile));
    System.out.println(
"Image upload started "+id);
        
webClient.post().uri(url).contentType(MediaType.MULTIPART_FORM_DATA).body(BodyInserters.fromMultipartData
(body)).retrieve().bodyToMono(String.class).subscribe(response -> {
           System.out.println(
"Response Id"+id+ ":" + response);
    });

WebClient 导致内存不足错误
当我们在 OpenJDK 11 中运行这两个程序时。使用基于 NIO 的 Spring WebClient 的程序在迭代几次后出现了 "java.lang.OutOfMemoryError:Direct buffer memory",而基于 Spring RestTemplate 的程序则成功完成。

下面是基于 NIO 的 Spring WebClient 程序的输出结果。您可以看到报告了 "java.lang.OutOfMemoryError"(java.lang.OutOfMemoryError)。

Starting to post an image for Id0

Starting to post an image for Id1

Starting to post an image for Id2

Starting to post an image for Id3

Starting to post an image for Id4

Starting to post an image for Id5

Starting to post an image for Id6

Starting to post an image for Id7

Starting to post an image for Id8

Starting to post an image for Id9

Starting to post an image for Id10

Starting to post an image for Id11

Starting to post an image for Id12

Starting to post an image for Id13

Starting to post an image for Id14

2023-12-06 17:21:46.730  WARN 13804 --- [tor-http-nio-12] io.netty.util.concurrent.DefaultPromise  : An 
exception was thrown by reactor.ipc.netty.FutureMono$FutureSubscription.operationComplete()

reactor.core.Exceptions$ErrorCallbackNotImplemented: 
io.netty.channel.socket.ChannelOutputShutdownException: Channel output shutdown

Caused by: java.lang.OutOfMemoryError: Direct buffer memory

    at java.base/java.nio.Bits.reserveMemory(Bits.java:175) ~[na:na]

    at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:118) ~[na:na]

    at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:318) ~[na:na]

    at java.base/sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:242) ~[na:na]

    at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:164) ~[na:na]

    at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:130) ~[na:na]

    at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:496) ~[na:na]

    at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:418) ~[netty-
transport-4.1.23.Final.jar!/:4.1.23.Final]

    at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:934) ~[netty-
transport-4.1.23.Final.jar!/:4.1.23.Final]

    ... 18 common frames omitted


Spring WebClient 是基于 Java NIO 技术开发的。在 Java NIO 中,对象存储在 JVM 本机内存的 "直接缓冲存储器 "区域,而 RestTemplate 对象存储在 JVM 本机内存的 "其他 "区域。JVM 中有不同的内存区域。要了解它们,请观看本视频短片。

执行上述两个程序时,我们将直接缓冲区内存大小设置为 200k(即 -XX:MaxDirectMemorySize=200k)。这个大小对 Spring RestTemplate 来说足够了,因为对象从未存储在这个区域中,但对 Spring WebClient 来说却不够。因此,Spring WebClient 出现了 java.lang.OutOfMemoryError:直接缓冲内存。

增加 -XX:MaxDirectMemorySize
发现这个问题后,我们使用 JVM 参数 -XX:MaxDirectMemorySize=1000k 将直接内存大小增加到更高值。做出这一更改后,Spring WebClient 程序运行得非常好,没有出现任何问题。

结论
在本篇博文中,我们讨论了从 Spring RestTemplate 升级到基于 Java NIO 的 WebClient 时遇到的 OutOfMemoryError 问题。