使用MongoDB的Spring WebFlux-限制SSE客户端

时间:2019-01-03 08:10:35

标签: spring-webflux project-reactor spring-mongodb reactor-netty

我正在使用Spring Boot 2.1.1和WebFlux,Reactor 3.2.3,Mongo 3.8.2和Netty 4.1.31运行的简单聊天服务。

每个聊天室都有2个集合-消息存档和带有当前事件(例如,新消息事件,用户键入指示符等)的加盖集合。上限为100个元素,我正在使用ReactiveMongoTemplate的tail()方法来检索最新事件。

该服务公开了两种用于检索最近事件的终结点:SSE和轮询。我已经对2000个并发用户进行了压力测试,这些用户除了听聊天以外,还发送大量事件。

观察结果是:

  • 每2秒轮询一次会给服务带来一点压力(测试期间约40%的CPU使用率),而对MongoDB几乎没有压力(约4%)
  • 通过SSE进行监听会使MongoDB达到最大使用率(约90%),也给服务带来了压力(该服务试图使用剩余的可用资源),但是Mongo尤其苦苦挣扎,总体而言,该服务几乎没有响应。

观察似乎很明显,因为当我在测试过程中通过SSE连接时,它在新事件到来时几乎立即更新了我的信息-基本上,SSE的响应速度比每2秒轮询一次要高数百倍。

问题是:

鉴于客户端最终是订户(或者至少我认为是有限的知识),我可以通过ReactiveMongoTemplate限制发布消息的速度吗?还是以某种方式减少了对新事件的需求,而不必这样做在客户端?

我一直在尝试通过Flux缓冲和缓存来实现运气,但这带来了更大的压力...

代码:

// ChatRepository.java

private static final Query chatEventsQuery = new Query();

public Flux<ChatEvent> getChatEventsStream(String chatId) {
    return reactiveMongoTemplate.tail(
            chatEventsQuery,
            ChatEvent.class,
            chatId
    );
}

// ChatHandler.java

public Mono<ServerResponse> getChatStream(ServerRequest request) {

    String chatId = request.pathVariable(CHAT_ID_PATH_VARIABLE);
    String username = getUsername(request);

    Flux<ServerSentEvent> chatEventsStream = chatRepository
            .getChatEventsStream(chatId)
            .map(addUserSpecificPropsToChatEvent(username))
            .map(event -> ServerSentEvent.<ChatEvent>builder()
                    .event(event.getType().getEventName())
                    .data(event)
                    .build());

    log.debug("\nExposing chat stream\nchat: {}\nuser: {}", chatId, username);

    return ServerResponse.ok().body(
            chatEventsStream,
            ServerSentEvent.class
    );
}

// ChatRouter.java

RouterFunction<ServerResponse> routes(ChatHandler handler) {
    return route(GET("/api/chat/{chatId}/stream"), handler::getChatStream);
}

1 个答案:

答案 0 :(得分:0)

答案是: 您可以使用Flux.buffer方法来完成。然后,流量将以定义的速率将事件批​​量发送给订户。

我发布的代码有2个主要问题

  1. 鉴于多个用户通常在听一个聊天,我重构了ChatRepository以利用“热”可重播的流量(现在我每个聊天有1个流,而不是每个用户1个流)在Caffeine缓存中。 此外,我会以较短的时间间隔对其进行缓冲,以避免在繁忙的聊天中将事件推送给客户端时浪费大量资源。

  2. 我在ChatRepository中使用的new Query()是多余的。一世 已经查看了ReactiveMongoTemplate的代码,以及是否为非null 提供查询,逻辑有点复杂。最好通过null 改为ReactiveMongoTemplate的tail()方法。

代码后重构

// ChatRepository.java

public Flux<List<ChatEvent>> getChatEventsStream(String chatId) {
    return Optional.ofNullable(chatStreamsCache.getIfPresent(chatId))
            .orElseGet(newCachedChatEventsStream(chatId))
            .autoConnect();
}

private Supplier<ConnectableFlux<List<ChatEvent>>> newCachedChatEventsStream(String chatId) {
    return () -> {
        ConnectableFlux<List<ChatEvent>> chatEventsStream = reactiveMongoTemplate.tail(
                null,
                ChatEvent.class,
                chatId
        ).buffer(Duration.ofMillis(chatEventsBufferInterval))
                .replay(chatEventsReplayCount);

        chatStreamsCache.put(chatId, chatEventsStream);

        return chatEventsStream;
    };
}

// ChatHandler.java

public Mono<ServerResponse> getChatStream(ServerRequest request) {

    String chatId = request.pathVariable(CHAT_ID_PATH_VARIABLE);
    String username = getUsername(request);

    Flux<ServerSentEvent> chatEventsStream = chatRepository
            .getChatEventsStream(chatId)
            .map(addUserSpecificPropsToChatEvents(username))
            .map(event -> ServerSentEvent.<List<ChatEvent>>builder()
                    .event(CHAT_SSE_NAME)
                    .data(event)
                    .build());

    log.debug("\nExposing chat stream\nchat: {}\nuser: {}", chatId, username);

    return ServerResponse.ok().body(
            chatEventsStream,
            ServerSentEvent.class
    );
}

应用这些更改后,该服务即使在3000个活动用户中也能正常运行(JVM使用约50%的CPU,Mongo约7%,这主要是由于大量的插入-流现在还不那么明显)