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

Posted

技术标签:

【中文标题】Webflux + Netty NIO 性能相比传统 IO 下降约 30 倍【英文标题】:Webflux + Netty NIO performance decrease ~30 times compared to traditional IO 【发布时间】:2019-03-21 01:55:17 【问题描述】:

我们在使用 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/30 倍是多余的。

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

端点代码本身:

@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);
    

我们正在为 Netty 使用 Spring Boot 配置,我们怀疑默认情况下 Netty 配置错误。 我们正在寻求您的帮助。我会根据您的要求添加任何其他详细信息。

更新: 目标是批量读取整个响应以避免将所有响应放入内存,因为预期的数据量很大(几 Gb)。在客户端消费一批数据而不是一个元素是可以接受的。

【问题讨论】:

我不明白。您将该代码与什么进行比较?内存中的Stream?文件输入/输出?原始套接字 I/O? @StephenC 在客户端,我首先使用常规 REST HTTP 访问此端点,然后使用 Webflux 客户端并比较下载内容所需的时间。 Spring 处理头文件“application/json”与“application/stream+json”的差异,对于 HTTP 调用,将返回的 Flux 转换为 Collection。所以我基本上是在比较网速。 请详细说明 - 您能否在此处粘贴更多关于您正在比较的内容、您正在制作的 HTTP 请求/响应标头的详细信息? @BrianClozel 我正在比较 Swagger 中两种方法的响应时间。为了测试 REST HTTP,我发送“application/json”标头,为了测试 Webflux,我发送“application/stream+json”。我在浏览器中检查“网络”选项卡以避免 Json 反序列化的时间。 【参考方案1】:

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

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

    使用 Spring WebFlux 一次性提供 "application/json" 有效负载 使用 Spring WebFlux 提供流 "application/stream+json" 响应

在第一种情况下,Spring WebFlux 以反应方式生成响应主体,但将缓冲和刷新决策留给服务器本身。写入网络是有代价的,缓冲 a 但是写入更大的块是有效的。

在第二种情况下,您要求 Spring WebFlux 为Flux 的每个元素写入和刷新。当客户端正在侦听(可能是无限的)事件流并且两个不同事件之间可能有一段时间时,这很有用。这种方法消耗更多资源并解释了性能差异。

所以这个基准测试不是显示 IO 与 NIO,而是显示流式与非流式。

如果您想要对响应写入/刷新进行细粒度控制,您可以下降到ServerHttpResponse 的级别并使用writeAndFlushWith(Flux&lt;Flux&lt;DataBuffer&gt;&gt;),但这是相当低的级别,因为您正在直接处理DataBuffer 实例.

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

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

【讨论】:

谢谢!我同意这可能是问题所在,您能否建议我如何配置 Webflux 或 Netty 以批量刷新项目? 你的客户端应该请求普通的"application/json",它会正常写入/刷新 也许我不是很清楚。我想使用application/stream+json,但可以批量刷新数据以提高速度。目前的情况是生产者(在内存中)可以非常快地提供项目,而消费者的消费速度很快,所以当前情况的瓶颈是每个项目上的flush。我注意到您对ServerHttpResponse 的修改,您能否详细说明或提供该示例的参考? WebFlux 中的 "application/stream+json" 流式合同在每个元素之后刷新,并且没有精确控制刷新行为的合同 AFAIK。如果您愿意考虑一种解决方法,我已经改进了我的答案:在 JSON 对象中批处理元素并刷新其中的每一个。 感谢您的回答,在我们的团队中,我们也提出了类似的解决方案。目标是批量读取整个响应以避免将所有响应放入内存,因为预期的数据量很大(几 Gb)。在客户端消费一批数据而不是一个元素是可以接受的。

以上是关于Webflux + Netty NIO 性能相比传统 IO 下降约 30 倍的主要内容,如果未能解决你的问题,请参考以下文章

Java异步NIO框架Netty实现高性能高并发

Netty系列之Netty高性能之道

Netty系列之Netty高性能之道

Netty 系列之 Netty 高性能之道

实战Netty系列之Netty高性能之道

Netty你会了吗?