这与 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 秒不等。
我针对 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
,我必须使用它。