Webflux + Netty NIO性能下降约30倍,与传统IO相比。

4
我们在使用Spring Boot 2.0、Webflux 5.0.7和Netty 4.1.25时,在网络传输方面遇到了问题。我们想要将100000个以JSON格式序列化的项目(大约10MB的网络流量)传输给1个客户端。
NIO与传统IO之间的网络传输性能差异非常显著。测试结果如下:
Start reading 100000 from server in 5 iterations
Avg HTTP 283 ms
Avg stream 8130 ms

目前每秒请求的数量不是问题,但网络传输速度是。我们已经了解到,在网络速度方面,NIO可能会慢约30%,但1 / 30x太过于严重。

在客户端和服务器端采样时,我们观察到原因主要在服务器端实现上。从下面的截图可以看出,服务器大部分时间都花在select()doWrite()方法中。

Sampling

端点代码本身:

@RestController
@RequestMapping(produces = {APPLICATION_JSON_VALUE, APPLICATION_STREAM_JSON_VALUE})
@Validated
public class StreamingController {

    @GetMapping("/instruments/{eodDate}")
    public Flux<TestItem> getInstruments(
            @PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate eodDate,
            @RequestParam(required = false) Instant asOfTimestamp) {
        //Generate test data in memory
        List<TestItem> collect = IntStream.range(0, 100000)
             .mapToObj(i -> new TestItem.Builder().build())
             .collect(Collectors.toList());
        return Flux.fromIterable(collect);
    }
}

我们正在使用Spring Boot配置Netty,但我们怀疑Netty默认配置不正确。我们需要您的帮助。如果需要,我可以提供其他详细信息。
更新: 目标是批量读取整个响应,以避免将所有响应放入内存中,因为预期的数据量非常大(几个GB)。在客户端上消耗一批数据而不是一个元素是可以接受的。

我不明白。你在将那段代码与什么进行比较呢?是内存中的 Stream吗?还是文件 I/O?或者是原始套接字 I/O? - Stephen C
@StephenC 在客户端,我首先使用常规的REST HTTP命中此端点,然后使用Webflux客户端,并比较下载内容所需的时间。Spring处理头文件“application/json”与“application/stream+json”的差异,并将返回的Flux转换为Collection进行HTTP调用。因此,我基本上正在比较网络速度。 - schaffe
请详细说明 - 您能否在此处粘贴更多有关您正在比较的内容,以及您正在进行的HTTP请求/响应头? - Brian Clozel
@BrianClozel 我正在比较Swagger中2种方法的响应时间。为了测试REST HTTP,我发送“application/json”头文件;为了测试Webflux,我发送“application/stream+json”。我检查浏览器中的“网络”选项卡,以避免Json反序列化所需的时间。 - schaffe
1个回答

12

您并未实际测试 NIO 与 IO。Spring WebFlux 应用程序始终在服务器级别使用非阻塞 IO(使用 Netty、Undertow 或任何 Servlet 3.1+ 异步 IO 兼容服务器)。

在这种情况下,您正在比较:

  1. 使用 Spring WebFlux 在一次请求中提供 "application/json" 负载
  2. 使用 Spring WebFlux 提供流式 "application/stream+json" 响应

在第一个案例中,Spring WebFlux 以反应式方式生成响应正文,但将缓冲和刷新决策留给服务器本身。写入网络有一个成本,缓冲有点多并且更大的块写入效率更高。

在第二个案例中,您要求 Spring WebFlux 对 Flux 的每个元素进行写入和刷新。当客户端监听可能无限的事件流时,这很有用,两个不同事件之间可能会有一些时间。这种方法会消耗更多资源,并解释了性能差异。

因此,此基准测试未显示 IO vs. NIO,而是流式 vs. 非流式。

如果您想对响应写入/刷新进行精细控制,可以降到 ServerHttpResponse 的级别,并使用 writeAndFlushWith(Flux<Flux<DataBuffer>>),但这是相当低级的,因为您正在直接处理 DataBuffer 实例。

另一种方法是创建中间的 JSON 对象,其中包含 TestItem 列表,例如:

public Flux<TestItemBatch> batch() {
    Flux<TestItem> items= //...;
    Flux<List<TestItem>> itemsLists = items.buffer(100);
    return itemsLists.map(list -> new TestItemBatch(list));
}

谢谢!我同意这可能是问题所在,您能建议一下如何配置Webflux或Netty以批量刷新项目吗? - schaffe
您的客户端应该请求纯 "application/json",然后它将正常写入/刷新。 - Brian Clozel
1
也许我没有表达清楚。我想使用 application/stream+json,但希望能够分批刷新数据以提高速度。当前情况是生产者(内存中)可以非常快地提供项目,消费者的消费速度也很快,因此当前情况的瓶颈在于每个项目上的 flush。我注意到您对 ServerHttpResponse 的编辑,您能否详细说明或提供示例的参考? - schaffe
2
在WebFlux中,"application/stream+json"流契约在每个元素后刷新,并且据我所知,没有精确控制刷新行为的契约。如果您愿意考虑解决方法,则可以将元素分批放入JSON对象中,并刷新其中的每一个。 - Brian Clozel
2
感谢您的回答,我们团队也想到了类似的解决方案。目标是分批读取整个响应,以避免将所有响应放入内存中,因为预期的数据量非常大(几个GB)。在客户端消耗一批数据而不是一个元素是可以接受的。 - schaffe
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接