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

sch*_*ffe 4 java spring netty spring-boot spring-webflux

使用Spring Boot 2.0,Webflux 5.0.7和Netty 4.1.25时,我们在网络传输中遇到问题。我们想将序列化为JSON的100000个项目(大约10Mb的网络流量)传输到1个客户端。

NIO和传统IO之间的网络传输性能差异很大。测试结果如下:

Start reading 100000 from server in 5 iterations
Avg HTTP 283 ms
Avg stream 8130 ms
Run Code Online (Sandbox Code Playgroud)

目前,每秒请求数不是问题,但网络传输速度却是问题。我们已经读到,就网络速度而言,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Upd: 目标是分批读取整个响应,以避免将所有响应都放入内存,因为预期的数据量非常大(Gb对)。可以在客户端使用一批数据而不是一个元素。

Bri*_*zel 5

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

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

  1. 服务的"application/json"一次性有效载荷与Spring WebFlux
  2. "application/stream+json"通过Spring WebFlux 服务流响应

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

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

因此,该基准测试并未显示IO与NIO,而是流与非流。

如果您希望对响应的编写/刷新进行细粒度的控制,则可以降至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));
}
Run Code Online (Sandbox Code Playgroud)

  • WebFlux 中的“application/stream+json”流契约在每个元素之后刷新,并且没有精确控制刷新行为的契约 AFAIK。如果您愿意考虑一种解决方法,我已经改进了我的答案:批处理 JSON 对象中的元素并刷新每个元素。 (2认同)
  • 感谢您的回答,在我们的团队中,我们也提出了类似的解决方案。目标是批量读取整个响应以避免将所有响应放入内存中,因为预期的数据量很大(几 Gb)。在客户端消费一批数据而不是一个元素是可以接受的。 (2认同)