Spring boot Reactor Netty、S3AsyncClient 和 AsyncResponseTransformer 用于大文件下载

问题描述 投票:0回答:1

这与 S3AsyncClient 和 AsyncResponseTransformer 在下载期间保持背压相对应。

这是一个非常基本的 Spring Boot 反应式应用程序,可从 S3 流式传输文件。

这是一个基本的代码片段:

@GetMapping(path="/{filekey}")
Mono<ResponseEntity<Flux<ByteBuffer>>> downloadFile(@PathVariable("filekey") String filekey) {    
    GetObjectRequest request = GetObjectRequest.builder()
      .bucket(s3config.getBucket())
      .key(filekey)
      .build();
    
    return Mono.fromFuture(s3client.getObject(request, AsyncResponseTransformer.toPublisher()))
      .map(response -> {
        checkResult(response.response());
        String filename = getMetadataItem(response.response(),"filename",filekey);            
        return ResponseEntity.ok()
          .header(HttpHeaders.CONTENT_TYPE, response.response().contentType())
          .header(HttpHeaders.CONTENT_LENGTH, Long.toString(response.response().contentLength()))
          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
          .body(Flux.from(response));
      });
}

从背压的角度来看,切换到 Netty 而不是 CRT 效果更好。 Spring Boot 端有一个 netty,我将其称为reactor-netty,S3 客户端端有一个 netty,我将其称为 s3-netty。

我看到,对于每个请求,reactor-netty 消耗的直接内存会增加 8BM,这是一个缓存块大小(maxOrder=10),并增长到某个数字,但随后停止增长。我猜这是基于reactor-netty线程和池区域的数量。

我在本地使用Windows,因此reactor-netty使用reactor-http-nio。在服务器上我们有linux,因此它使用reactor-http-epoll。

当我创建 90 个客户端在本地下载 500MB 文件时,所有客户端都在慢慢获取块(客户端越多,速度越慢),但没有一个客户端空闲,Jmeter 显示延迟很好。

当我在服务器上执行相同操作时,由于延迟超过 20 秒,多个请求失败,即某些请求正在获取数据,而某些请求则长时间等待第一个字节。

我不确定瓶颈是否是S3异步客户端忙于将块推送到reactor-netty,或者reactor-netty忙于将块推送到客户端。但总的来说我很困惑:reactor-netty 和 s3-netty 都有自己的事件循环。 S3 也有响应编写器线程,但如果我在 Flux 中记录某些内容,我会看到日志语句是由 aws-java-sdk-NettyEventLoop 打印的,因此来自 s3 的块被 s3-netty 事件推送到reactor-netty 中循环线程。

我尝试使用

.publishOn()
专用并行调度程序,但这并没有改变任何东西。

总的来说,如果我的服务器唯一做的就是从 S3 流式传输大文件,那么使用反应式/异步内容是否有意义,或者我应该简单地切换回旧的阻塞 IO 并手动进行分块?

根据用户数量,文件下载时间从 10 到 100 秒不等。

spring-boot amazon-s3 streaming netty reactor-netty
1个回答
0
投票

我针对 aws sdk 提出了一个问题:https://github.com/aws/aws-sdk-java-v2/issues/5158

同时我发现了原因:s3异步客户端(无论底层http客户端如何)确实尊重reactor-netty在

request(n)
上完成的
Flux<ByteBuffer>
。问题是不同客户端之间的块大小不同。

s3-CRT 默认使用

8MB
块。

s3-Netty 默认使用

8KB
块,即小 1024 倍的大小。

Reactor-netty 首先请求

128
项,然后通过
64
重新填充。 (@参见
MonoSendMany
MonoSend
MAX_SIZE/REFILL_SIZE
)。

现在,如果您的消费者速度足够慢并且下载了一个大文件,reactor-netty 会从 s3-crt 请求

128 * 8 = 1024MB
,最终 Reactor-netty 缓冲区会被该数据填满,即使通道 WRITABILITY_CHANGED 为 false。

如果您下载多个文件,很容易达到最大直接内存限制。

由于

MAX_SIZE/REFILL_SIZE
是reactor-netty中的硬编码静态字段,唯一的解决方案是通过使用以下方法来减少S3部分/块的大小:

S3AsyncClient.crtBuilder()
    .minimumPartSizeInBytes(1L * 1024 * 1024) // 1 MB

这将使 S3-crt 在每个下载请求时将

128 * 1 = 128MB
max 推送到reactor-netty 缓冲区。虽然它可能会降低 s3 异步客户端和下载的整体吞吐量/性能,但它有助于支持更多并行下载,而不会因 OutOfDirectMemoryError 而失败。

这更像是一种解决方法而不是解决方案,但在有一种方法可以配置reactor-netty背压之前

MAX_SIZE/REFILL_SIZE
,我必须使用它。

© www.soinside.com 2019 - 2024. All rights reserved.