Spring WebFlux: publishOn vs subscribeOn for Improving Microservices Performance
This article explains the benefits of using PublishOn and SubscribeOn Reactor operators for improving microservices performance.
Join the DZone community and get the full member experience.
Join For FreeWith the rise of microservices architecture, there has been a rapid acceleration in the modernization of legacy platforms, leveraging cloud infrastructure to deliver highly scalable, low-latency, and more responsive services.
Why Use Spring WebFlux?
Traditional blocking architectures often struggle to keep up performance, especially under high load. Being Spring Boot developers, we know that Spring WebFlux, introduced as part of Spring 5, offers a reactive, non-blocking programming model designed to address these challenges.
WebFlux leverages event-driven, non-blocking, asynchronous processing to maximize resource efficiency, making it particularly well-suited for I/O-intensive tasks such as database access, API calls, and streaming data.
About This Article
Adopting Spring WebFlux can significantly enhance the performance of the Spring Boot applications. Under normal load, Spring Boot WebFlux applications perform excellently, but for the scenarios where the source of the data is blocking, such as I/O-bound, the default (main) IO thread pool can cause contention if downstream responses are very slow and degrade the performance. In this article, I will try to cover how publishOn
and subscribeOn
reactor operators can come to the rescue.
Understanding publishOn and subscribeOn
(Note: I have used the Schedulers.boundedElastic()
scheduler as a more scalable reactive thread pool group. Spring Boot WebFlux provides other schedulers that can be used based on need.)
publishOn(Schedulers.boundedElastic())
and subscribeOn(Schedulers.boundedElastic())
are used by Reactor to control where certain parts of the reactive pipeline are executed, specifically on a different thread or thread pool. However, the two operators serve different purposes:
1. publishOn(Schedulers.boundedElastic())
Purpose
This switches the downstream execution to the specified scheduler, meaning that any operators that come after the publishOn
will be executed on the provided scheduler (in this case, boundedElastic
).
Use Case
If you want to switch the execution thread for all the operators after a certain point in your reactive chain (for example, to handle blocking I/O or expensive computations), publishOn
is the operator to use.
Example
Mono.fromSupplier(() -> expensiveBlockingOperation())
.publishOn(Schedulers.boundedElastic()) // Switch downstream to boundedElastic threads
.map(result -> process(result))
.subscribe();
When To Use publishOn
- Use when you need to run only the downstream operations on a specific scheduler, but want to keep the upstream on the default (or another) scheduler.
- It is useful for separating the concerns of upstream and downstream processing, especially if upstream operators are non-blocking and you want to handle blocking operations later in the pipeline.
2. subscribeOn(Schedulers.boundedElastic())
Purpose
This changes the thread where the subscription (upstream) occurs. It moves the entire chain of operators (from subscription to completion) to the provided scheduler, meaning all the work (including upstream and downstream operators) will run on the specified scheduler.
Use Case
Use this if you need to run the entire chain (both upstream and downstream) on a specific thread pool or scheduler, such as for blocking I/O tasks or when you want to handle the subscription (data fetching, database calls, etc.) on a different thread.
Example
Mono.fromSupplier(() -> expensiveBlockingOperation())
.subscribeOn(Schedulers.boundedElastic()) // Runs the entire chain on boundedElastic threads
.map(result -> process(result))
.subscribe();
When To Use subscribeOn
- Use when you want to run the entire pipeline (upstream + downstream) on the
boundedElastic
scheduler. - It's particularly useful for situations where the source of the data is blocking, such as I/O-bound operations (reading from disk, network calls, database queries, etc.), and you want to move everything off the default event loop thread (if using
Netty
orReactor Netty
).
Differences Between publishOn and subscribeOn
publishOn
affects only the downstream operations from the point where it is called. If placed in the middle of a chain, everything after thepublishOn
will be scheduled on the provided scheduler, but everything before it will stay on the previous scheduler.subscribeOn
affects the entire reactive chain, both upstream and downstream. It's often used to move blocking upstream operations (like I/O) to a non-blocking thread pool.
Choosing Between the Two
- Use
publishOn(Schedulers.boundedElastic())
if:- You need fine-grained control over where specific parts of the reactive chain run.
- You want to switch only the downstream operations (after a certain point) to a specific scheduler.
- Example: You're performing non-blocking reactive operations first, and then want to handle blocking operations downstream in a different thread pool.
- Use
subscribeOn(Schedulers.boundedElastic())
if:- You want to run the entire reactive chain (from the point of subscription onward) on a different scheduler.
- The source operation (like a network call or database query) is blocking, and you want to move the blocking subscription and all subsequent operations to a specific scheduler like
boundedElastic
.
- In short:
- If you have blocking code upstream (like a blocking I/O source), use
subscribeOn
. - If you want to isolate downstream blocking work (e.g., after some non-blocking calls), use
publishOn
.
- If you have blocking code upstream (like a blocking I/O source), use
Common Use Case: Blocking I/O in Reactive Programming
If you're working with I/O-bound operations (like file reads, database queries, etc.), and you want to offload blocking operations to a bounded thread pool, here's how you might use these:
Database Call or a Blocking I/O Call
Mono.fromCallable(() -> performBlockingDatabaseCall())
.subscribeOn(Schedulers.boundedElastic()) // Offload blocking database call to boundedElastic
.map(result -> process(result)) // Further processing
.subscribe();
Here, the subscribeOn
ensures that the entire pipeline, including the blocking I/O, runs on the boundedElastic
scheduler.
Mixed Non-Blocking and Blocking Operations
Mono.just("Initial value")
.map(value -> transformNonBlocking(value)) // Non-blocking operation
.publishOn(Schedulers.boundedElastic()) // Switch thread for blocking operation
.flatMap(value -> Mono.fromCallable(() -> performBlockingOperation(value))) // Blocking operation
.subscribe();
In this case, the publishOn
ensures that only the downstream blocking work (i.e., the flatMap
) is moved to a different scheduler, while the earlier non-blocking operations stay on the default one.
Summary
subscribeOn
affects the entire reactive chain and is typically used when the source operation (like database access) is blocking.publishOn
switches the scheduler for all operations downstream from where it is called and is better when you want to run only certain parts of the chain on a different thread.
Opinions expressed by DZone contributors are their own.
Comments