Ускорить BFF параллельными вызовами

12. Ускорить BFF параллельными вызовами

Условие задачи:
BFF обращается к двум внешним сервисам:

  • cart-service отвечает примерно за 0.7 секунды;

  • recommendations-service отвечает примерно за 0.5 секунды.

При последовательном выполнении запрос занимает около 1.2 секунды. Необходимо выполнять вызовы параллельно и возвращать ответ менее чем за одну секунду.

Код:

@FeignClient(
        name = "cart-service",
        url = "http://shopping-cart"
)
public interface ShoppingCartServiceClient {

    @GetMapping("/api/v1/shopping-cart")
    CartDto getClientCart();
}

@FeignClient(
        name = "recommendations-service",
        url = "http://recommendations"
)
public interface RecommendationsServiceClient {

    @GetMapping("/api/v1/recommendations")
    List<RecommendationDto> getClientRecommendations();
}

public record ClientCartScreenDto(
        CartDto cart,
        List<RecommendationDto> recommendations
) {
}

@Service
public class ClientCartScreenService {

    public ClientCartScreenDto getScreenData() {
        // код тут
    }
}

Спойлеры к решению

Подсказки
💡 Запусти оба запроса до ожидания результата.
💡 Для параллельного выполнения можно использовать CompletableFuture.
💡 Для блокирующих Feign-вызовов используй отдельный пул потоков.
💡 Установи тайм-ауты, не превышающие общий SLA.
💡 При ошибке сервиса рекомендаций можно вернуть пустой список.

Решение

Создадим отдельный пул потоков для блокирующих запросов:

@Configuration
public class BffExecutorConfig {

    @Bean("bffExecutor")
    public Executor bffExecutor() {
        ThreadPoolTaskExecutor executor =
                new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("bff-");
        executor.initialize();

        return executor;
    }
}

Реализация сервиса:

@Service
public class ClientCartScreenService {

    private final ShoppingCartServiceClient cartClient;
    private final RecommendationsServiceClient recommendationsClient;
    private final Executor executor;

    public ClientCartScreenService(
            ShoppingCartServiceClient cartClient,
            RecommendationsServiceClient recommendationsClient,
            @Qualifier("bffExecutor") Executor executor
    ) {
        this.cartClient = cartClient;
        this.recommendationsClient = recommendationsClient;
        this.executor = executor;
    }

    public ClientCartScreenDto getScreenData() {
        CompletableFuture<CartDto> cartFuture =
                CompletableFuture.supplyAsync(
                                cartClient::getClientCart,
                                executor
                        )
                        .orTimeout(
                                900,
                                TimeUnit.MILLISECONDS
                        );

        CompletableFuture<List<RecommendationDto>>
                recommendationsFuture =
                CompletableFuture.supplyAsync(
                                recommendationsClient
                                        ::getClientRecommendations,
                                executor
                        )
                        .completeOnTimeout(
                                List.of(),
                                700,
                                TimeUnit.MILLISECONDS
                        )
                        .exceptionally(error -> List.of());

        CartDto cart = cartFuture.join();

        List<RecommendationDto> recommendations =
                recommendationsFuture.join();

        return new ClientCartScreenDto(
                cart,
                recommendations
        );
    }
}

Оба запроса запускаются до вызова join(), поэтому выполняются параллельно. Общее время ответа будет примерно равно времени самого медленного запроса, а не сумме времени двух запросов.

Корзина является обязательной частью экрана, поэтому ошибка её получения прерывает выполнение. Рекомендации являются дополнительными данными: при ошибке или превышении тайм-аута возвращается пустой список.

Для Feign-клиентов также необходимо настроить сетевые connect timeout и read timeout. Тайм-аут CompletableFuture ограничивает ожидание результата, но не гарантирует остановку самого HTTP-запроса.