-
Notifications
You must be signed in to change notification settings - Fork 0
00 Java Microservices & Spring Cloud & Reactive programming
lukes8 edited this page May 28, 2024
·
1 revision
Drawbacks of Reactive Programming non-blocking async WebClient vs synchronized approach with Feign client
Complexity:
Reactive programming introduces a learning curve, especially for developers unfamiliar with functional programming concepts or reactive frameworks like Project Reactor. Writing and debugging reactive code may be more challenging compared to imperative or blocking code.
Debugging and Profiling:
Debugging reactive code can be more complex due to the asynchronous and non-linear nature of reactive streams. Tools for debugging and profiling reactive applications may not be as mature or readily available compared to traditional blocking applications.
Overhead:
Reactive programming introduces additional overhead, both in terms of performance and memory usage, due to the need for maintaining reactive streams, event loops, and managing asynchronous operations. In some cases, this overhead may outweigh the benefits, especially for simpler applications.
Resource Consumption:
While reactive programming can improve resource utilization by reducing thread usage, it may still consume significant CPU and memory resources, especially in scenarios with high-concurrency or long-running operations.
Integration Complexity:
Integrating reactive components with existing synchronous or blocking systems can be complex and may require additional effort, especially when dealing with legacy codebases or third-party libraries that are not reactive.
When to Use Feign Client Instead of Reactive Programming:
Synchronous Communication:
If your application primarily relies on synchronous communication with external services or APIs and does not have strict requirements for non-blocking or asynchronous processing, Feign client may be a more straightforward and suitable choice.
Blocking Operations:
In scenarios where your application performs mostly CPU-bound operations or short-lived tasks that do not benefit significantly from non-blocking processing, using Feign client with traditional blocking I/O may be sufficient and more straightforward to implement.
Familiarity and Expertise:
If your development team is more familiar with traditional blocking paradigms and lacks experience with reactive programming, using Feign client may lead to faster development, easier maintenance, and fewer learning curve challenges.
Resource Constraints:
In resource-constrained environments where memory or CPU usage needs to be tightly controlled, using Feign client with blocking I/O may be preferable to avoid the overhead associated with reactive frameworks and event-driven architectures.
Conclusion:
While reactive programming offers significant advantages in terms of non-blocking performance, scalability, and responsiveness, it's essential to carefully consider its potential drawbacks and suitability for your specific use case. Feign client remains a viable option for scenarios where simplicity, familiarity, and synchronous communication with external services are more important than the benefits offered by reactive programming. Ultimately, the choice between reactive programming and Feign client should be based on factors such as application requirements, development team expertise, and resource constraints.
Example Story:
1. Define Custom Metric:
Bob wants to monitor the number of orders processed by his e-commerce application. To achieve this, he creates a custom metric named orders.processed.
java
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Counter ordersProcessedCounter;
@Autowired
public OrderService(MeterRegistry meterRegistry) {
this.ordersProcessedCounter = meterRegistry.counter("orders.processed");
}
public void processOrder(Order order) {
// Process the order logic
// Increment the orders processed counter
ordersProcessedCounter.increment();
}
}
In this example, Bob injects MeterRegistry into his OrderService to create a Counter metric named orders.processed. Whenever an order is processed, the processOrder method is called, and the counter is incremented.
2. Configure Actuator Endpoint:
Bob wants to expose the orders.processed metric through the Spring Boot Actuator endpoints.
properties
# application.properties
management.endpoints.web.exposure.include=metrics
3. Access Metric Endpoint:
With the Actuator endpoint configured, Bob can now access the orders.processed metric data via the /actuator/metrics/orders.processed endpoint.
http
GET /actuator/metrics/orders.processed HTTP/1.1
Host: localhost:8080
4. Example Endpoint Response:
When Bob accesses the /actuator/metrics/orders.processed endpoint, he receives a JSON response containing the orders.processed metric data.
json
{
"name": "orders.processed",
"description": null,
"baseUnit": "none",
"measurements": [
{
"statistic": "COUNT",
"value": 42.0
}
],
"availableTags": []
}
show me a few examples in bullets when Bob can use micrometer for spring microservice app?
Request Metrics:
Bob can use Micrometer to measure the number of incoming HTTP requests, response times, and error rates for each endpoint in his microservice.
He can track metrics like the number of successful requests, client errors (4xx), server errors (5xx), etc.
Database Performance:
Bob can monitor database interactions within his microservice, measuring the number of database queries executed, their execution times, and any potential bottlenecks.
He can track metrics such as database connection pool usage, query throughput, and cache hit rates.
Service Health:
Micrometer can help Bob monitor the health of his microservice by measuring metrics like CPU and memory usage, garbage collection times, and thread pool utilization.
He can set up alerts based on predefined thresholds to detect abnormal behavior and potential issues.
External Service Integration:
If Bob's microservice interacts with external services (e.g., third-party APIs), he can measure the latency and success rates of these interactions using Micrometer.
Tracking metrics for external service dependencies helps Bob identify performance bottlenecks and potential failures.
Custom Business Metrics:
Bob can define custom metrics specific to his application's business logic and key performance indicators (KPIs) using Micrometer.
For example, he can measure the number of orders processed per minute, the average order value, or customer retention rates.
Cache Performance:
If Bob's microservice utilizes caching mechanisms (e.g., Redis, Memcached), he can monitor cache hit rates, eviction rates, and the overall effectiveness of caching using Micrometer.
Tracking cache performance metrics helps Bob optimize cache configurations and improve application performance.
Concurrency and Threading:
Micrometer enables Bob to measure concurrency-related metrics, such as the number of active threads, thread pool queue sizes, and thread contention.
Monitoring these metrics helps Bob understand his microservice's concurrency behavior and identify potential scalability issues.
Scheduled Task Metrics:
If Bob's microservice includes scheduled background tasks (e.g., cron jobs, batch processing), he can measure the execution times and frequencies of these tasks using Micrometer.
Monitoring scheduled task metrics helps Bob ensure timely and reliable execution of critical background processes.
By incorporating Micrometer into his Spring microservice application, Bob gains valuable insights into its performance, health, and behavior. Micrometer's flexible and extensible nature allows Bob to monitor various aspects of his microserv
Cross cutting concerns - when we need to duplicate more non related business code - logging, security, transaction - then it leads to cross cutting concerns. To reduce them we need to simplify Bob live with AOP or next annotations
2. Service Class with @Transactional
Next, the service class method is annotated with @Transactional.
Service Class
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentService paymentService;
@Transactional
public void placeOrder(Order order, Payment payment) {
orderRepository.save(order);
paymentService.processPayment(payment);
}
}
3. Proxy Creation
When the Spring context is initialized, Spring creates a proxy for any bean that has methods annotated with @Transactional. This proxy wraps the original bean.
JDK Dynamic Proxy: If the bean implements one or more interfaces, Spring uses JDK dynamic proxy.
CGLIB Proxy: If the bean does not implement any interfaces, Spring uses CGLIB to create a subclass proxy.
4. Intercepting Method Calls
When a method annotated with @Transactional is called, the call is intercepted by the proxy. Here’s a simplified flow of what happens under the hood:
Method Call: The client calls orderService.placeOrder(order, payment).
Proxy Interception: The proxy intercepts the call to placeOrder.
Transaction Interceptor: The proxy delegates the call to a TransactionInterceptor.
Transaction Interceptor
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// Get transaction attribute (e.g., @Transactional metadata)
TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
// Get the transaction manager
PlatformTransactionManager tm = determineTransactionManager(txAttr);
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// Proceed with the actual method call
retVal = invocation.proceed();
} catch (Throwable ex) {
// Handle rollback if necessary
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// Commit or rollback the transaction
cleanupTransactionInfo(txInfo);
}
return retVal;
}
}
5. Transaction Management
Begin Transaction: The TransactionInterceptor begins a new transaction before the method execution if required.
Method Execution: The actual method (placeOrder) is called.
Commit/Rollback: After the method execution, the transaction is committed. If an exception occurs, the transaction is rolled back.
Example in Action
Here’s how the process looks in code form:
Client Code
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = context.getBean(OrderService.class);
orderService.placeOrder(new Order(), new Payment());
}
}
Proxy intercepts the call to placeOrder
TransactionInterceptor starts a transaction
OrderRepository.save(order) is called
PaymentService.processPayment(payment) is called
TransactionInterceptor commits the transaction
By understanding this flow, Bob can see how Spring's @Transactional annotation leverages proxy objects to manage transactions seamlessly, allowing developers to focus on business logic while Spring handles the complexities of transaction management.
- app.diagrams.net
gatekeeper that keep cloud architecture components in secure way
What we can maintain with this?
- routing which means allowing us to use only one IP address to access to different resources (eureka server, micro services, etc.)
- with routes we are able to allow what kind of services or REST API can be reached behind the API Gateway (lets imagine it like kind of proxy)
- auth mechanism on gatekeeper securing all flows coming to our cloud architecture
- load balancing LB means when having more instances of some service and we want having ability how to transparently access it without interruption.
it will use free one instance that has less traffic or is with no troubles
- as far as the services have high load we can balance or spread this load using LB pattern
discovery server DS allows us maintain and manage more micro services to communicate between themself without more effort
in real example, we can have one DS and more client as consumers that are registered to one instance of DS.
They can fetch information from DS registry to check with what micro services they can communicate. So this bring us good option how to keep this registry info with list of services for smooth communication.
we can even define Load balancing from client if we expect other service have multiple instances. If we dont provide it then WebClient cannot reach other service as it expects one service.
test