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

Webflux + Netty NIO performance decrease ~30 times compared to traditional IO

我们在使用 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

目前每秒 请求数不是问题,但网络传输速度是。我们已经读到,就网络速度而言,NIO 可能慢了大约 30%,但 1/30x 是一个大材小用。

在客户端和服务器端进行采样时,我们发现原因主要在于服务器端的实现。从下面的屏幕截图可以看出,服务器大部分时间花费在方法 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);
    }
}

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

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

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

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

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

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

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

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

如果你想对响应 writing/flushing 进行细粒度控制,你可以下降到 ServerHttpResponse 级别并使用 writeAndFlushWith(Flux<Flux<DataBuffer>>),但这是相当低的级别,因为你'直接处理 DataBuffer 个实例。

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

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