Embracing Reactive Programming: Optimizing Cloud, Database, and Web Operations

Embracing Reactive Programming: Optimizing Cloud, Database, and Web Operations

In the modern Java ecosystem, asynchronous processing is becoming crucial for achieving high performance and scalability.

Developers often choose between traditional and reactive approaches, especially in large-scale systems. Here, we explore how reactive programming optimizes database interactions, minimizes latency, and maximizes resource utilization, enhancing application responsiveness and throughput through non-blocking I/O and reactive streams.

We will see use cases of AWS S3 With Reactive Support, Reactive Database interactions using ReactiveMongoTemplate, and network calls using the Spring WebClient, comparing their performance to the traditional counterparts, namely AWS S3, MongoTemplate, and RestTemplate for network calls.

We’ll deep-dive into the technical aspects of these implementations, non-blocking, event-driven architectures, how they’re leveraged in a few Perfios products at scale, and how they enhance scalability and performance in these applications.

We will gain insights into building highly responsive and resilient systems for today’s data-intensive environments.

What is the need for Reactive Programming?

In modern applications, multiple asynchronous events, such as network requests, database operations, etc., must be handled. These operations may take time, and we don’t want to block the program’s execution while waiting for them.

You send a request to an external system. You wait for the response but don’t want to halt everything else in the system.

In traditional imperative programming, you’d typically block and wait for each task to finish, which could be more efficient when handling multiple tasks simultaneously and serving many requests.

Reactive programming solves this by allowing non-blocking execution and handling multiple tasks concurrently without waiting for each to finish.

When we explored Reactive Programming, JDK 21, which introduced the concept of Virtual Threads, was not released. As a result, we decided to use JDK 17 with Reactive Programming for our exploration.

Let’s start with a few prerequisites before delving deeper!

What is Context Switching, and why is it Expensive?

Imagine you’re working on a complex project and need to use multiple IDEs. First, you’re coding in Cursor (It’s the new popular IDE in town). Then, halfway through writing a function, you’re told to stop and switch to IntelliJ IDEA. After you’ve set that up and started getting into the flow, someone says, now jump to PyCharm for some Python debugging. And right as you open the debugger, they shout back to the Cursor IDE for more code generation.

Every time you switch IDEs, you must adjust your mindset to the specific IDE tooling. That mental shift alone is draining.

Q: What is the primary IDE you use for your daily work?

A thread is the fundamental unit that the CPU uses for execution. It consists of a Thread ID, an execution context like a stack, a set of registers, and a program counter.

Concurrency is inherently limited by the number of cores available on the host machine. Scaling beyond this is constrained by factors like the fact that Java and other popular runtimes like Python create threads mapped to operating system threads, which imposes overhead and limits the effective use of additional threads.

By default, each thread has 512 KB/1MB of stack space, depending on the JVM runtime whether it is active or idle. Even if this configuration can be fine-tuned, we still face limits on the number of threads we can create, primarily due to Memory constraints.

Even if memory weren’t a limiting factor, we would still be limited by how quickly the JVM instructs the Operating System to switch between threads and bound by this constraint.

Presenting the Cornerstone of Reactive Programming, the Event Loop

Imagine you’re WFH and decide to order food from a food ordering service. These are the steps involved:

1. Choose a restaurant and place your order

2. Track the status of your food

3. Place a second order with Swiggy

4. Rate the meal afterward

Task 1: You browse the app, select a restaurant and choice of food, and place your order. Once the order is confirmed, this task is marked as completed.

Task 2: You wait for the restaurant to prepare the order and mark this task as pending.

Task 3: You place a separate food order through the app and mark it as pending.

While the orders are getting ready, you carry on with your work.

You receive a notification that your first order is ready. You mark Task 2 as completed and get ready for the delivery. Shortly after, the second food order arrives. You receive it and mark Task 3 as completed.

Once your food arrives, you have your meal and rate your experience in the app. Task 4 is now complete.

Now that the above example is clear, it highlights a key limitation of the existing Thread per Request model. Let’s explore this further and see how Reactor Netty and the concept of Event Loops address this issue.

Thread Per Request Model:

In the Servlet world, incoming requests are packaged and placed into a holding pattern, awaiting their turn to be picked up and processed by available worker threads.

This means that you keep tracking your food order until it arrives.

This introduces inefficiencies and limitations regarding scalability and resource utilization.

What is Netty?

Reactor Netty embraces an event-driven architecture centered on EventLoops to handle incoming requests.

This fundamental architectural difference enables Netty to achieve impressive performance in scenarios involving high concurrency or long-lived connections.

EventLoop:

Just as Swiggy handles multiple requests (receiving orders from customers, placing orders with the restaurant, sending notifications to customers, and coordinating with the delivery partners), Netty’s Event Loop is essentially a non-blocking I/O thread, similar to what the Java NIO provides.

A channel is a nexus to a network socket or component capable of I/O operations such as read, write, connect, and bind.

These channels handle incoming requests on the server side and send outgoing requests from the client.

Once registered, an EventLoop will handle all the I/O operations for a Channel. Typically, several EventLoops run continuously and are managed by an EventLoopGroup. Each EventLoop manages multiple Channels.

EventLoopGroup allows for the registration of channels that get processed for later selection during the event loop.

Before getting started on how some technical challenges faced at Perfios were solved using Reactive Programming, let’s examine Mono and Flux.

Read More@ https://medium.com/perfiostechblog/embracing-reactive-programming-optimizing-cloud-database-and-web-operations-36e16913ded9

要查看或添加评论,请登录

Perfios的更多文章

社区洞察

其他会员也浏览了