¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
Yavé Guadaño Ibáñez 15/04/2024 Cargando comentarios…
Hace unas semanas comenzamos una serie de post de patrones de arquitectura de microservicios primero hablando de qué son, de su organización y estructura y de la comunicación y coordinación de microservicios.
Ahora, continuamos con esta serie de patrones de arquitectura de microservicios con la cuarta entrada de la serie, donde vamos a continuar con aquellos que solucionan problemas de comunicación y coordinación entre microservicios.
Todavía nos quedan bastantes que ver de este tipo, pero empezaremos con el patrón SAGA, ya que está muy relacionado con los patrones vistos recientemente: orquestación de servicios y coreografía de servicios.
Después seguiremos con Event Sourcing y EDA, dejando para el final CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend) y Outbox.
Recordamos una de las ventajas principales que nos proporcionan las arquitecturas de microservicios: la principal virtud de la arquitectura de microservicios es que es modular.
Esto significa que cada uno de los servicios del software se puede construir de forma independiente. Además, una vez que ya están funcionando, los servicios utilizan su propio proceso.
El patrón SAGA se utiliza en sistemas distribuidos para gestionar transacciones largas y complejas de manera eficiente y consistente. Este patrón se inspira en la estructura narrativa de las sagas literarias, donde una serie de eventos están interconectados para formar una historia coherente.
En el ámbito de la informática, el patrón SAGA permite coordinar y gestionar la ejecución de múltiples operaciones que involucran varios servicios y garantizar que la transacción mantenga su consistencia global, incluso en entornos distribuidos y con fallos.
En resumen, el patrón SAGA ofrece una solución efectiva para gestionar transacciones distribuidas en entornos complejos, como el comercio electrónico. Aunque ofrece numerosas ventajas, su implementación puede ser desafiante y requiere un diseño cuidadoso para garantizar la consistencia y la fiabilidad del sistema.
Lo vamos a ver mucho más claro con un ejemplo. Imaginemos un sistema de comercio electrónico donde los usuarios pueden realizar compras de productos.
El proceso de compra implica varios pasos, como la reserva de productos en el inventario, el procesamiento del pago, la generación de la factura y la notificación de envío.
Utilizaremos el patrón SAGA para gestionar este proceso de manera consistente, incluso en un entorno distribuido y ante posibles fallos.
Cuando un usuario agrega productos a su carrito de compras, se inicia una transacción para reservar dichos productos en el inventario. Si el inventario tiene suficientes existencias, la reserva se confirma. Si no, se produce un fallo y se ejecuta el paso de compensación para liberar cualquier reserva realizada.
Una vez que los productos están reservados, se procede al procesamiento del pago. Se realiza una transacción para cobrar al cliente el monto total de la compra. Si la transacción de pago se completa con éxito, se avanza al siguiente paso. En caso de fallo, se ejecuta la compensación para liberar las reservas de productos en el inventario.
Después de que el pago se ha procesado con éxito, se genera una factura para el cliente. Este paso implica la creación de un registro de la transacción en la base de datos y la generación de un documento de factura. Si la generación de la factura se completa correctamente, se procede al paso final. De lo contrario, se ejecuta la compensación para revertir la transacción de pago y liberar las reservas de productos en el inventario.
Finalmente, se notifica al cliente que su pedido ha sido enviado. Esta acción implica actualizar el estado del pedido y enviar una notificación por correo electrónico o mensaje de texto al cliente. Si la notificación se envía correctamente, se completa el proceso de compra. En caso de fallo, se ejecuta la compensación para revertir la generación de la factura, el procesamiento del pago y la liberación de las reservas de productos en el inventario.
En cada paso del proceso de compra, se define un mecanismo de compensación que puede revertir las operaciones realizadas en caso de fallo. Por ejemplo, si el procesamiento del pago falla, se ejecuta la compensación para liberar las reservas de productos en el inventario y garantizar la integridad del sistema.
Si este ejemplo lo plasmamos en código elaborado con Java y Spring Boot, a modo esquemático y sin completar la lógica sería:
import org.springframework.stereotype.Service;
@Service
public class OrderServoce {
public boolean processOrder(Order order) {
// Aquí va la lógica para reservar productos en el inventario
// Si hay suficiente stock, realiza la reserva
// Si no, lanza una excepción
// En caso de error, se realizará automáticamente el rollback
return true;
}
}
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public boolean processPayment(Order order) {
// Lógica para procesar el pago
// Si el pago se procesa correctamente, retorna true
// Si hay un fallo, lanza una excepción
// En caso de error, se realizará automáticamente el rollback
return true;
}
}
import org.springframework.stereotype.Service;
@Service
public class BillingService {
public boolean processBilling(Order order) {
// Lógica para generar la factura
// Si la factura se genera correctamente, retorna true
// Si hay un fallo, lanza una excepción
// En caso de error, se realizará automáticamente el rollback
return true;
}
}
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
public boolean sendNotification(Order order) {
// Lógica para notificar al cliente sobre el envío
// Si la notificación se envía correctamente, retorna true
// Si hay un fallo, lanza una excepción
// En caso de error, se realizará automáticamente el rollback
return true;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PurchaseController {
private final OrderService orderService;
private final PaymentService paymentService;
private final BillingService billingService;
private final NotificationService notificationService;
@Autowired
public PurchaseController(OrderService orderService, PaymentService paymentService,
BillingService billingService, NotificationService notificationService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.billingService = billingService;
this.notificationService = notificationService;
}
@PostMapping("/purchase")
public ResponseEntity<String> processPurchase(@RequestBody Purchase purchase) {
try {
// Paso 1: Reservar productos en el inventario
orderService.reserveProducts(purchase);
// Paso 2: Procesar el pago
paymentService.processPayment(purchase);
// Paso 3: Generar factura
billingService.generateInvoice(purchase);
// Paso 4: Notificar el envío
notificationService.notifyShipping(purchase);
return ResponseEntity.ok("Purchase processed successfully.");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error processing purchase: " + e.getMessage());
}
}
}
En este caso, dejamos que sea el framework de Spring quien realiza el rollback de manera automática, ya que tiene transaccionalidad.
El patrón de API Gateway actúa como un punto de entrada único para gestionar las solicitudes de clientes en un sistema de microservicios. Proporciona funcionalidades como enrutamiento, autenticación, autorización y agregación de datos, simplificando así la complejidad para los clientes y mejorando la eficiencia.
Supongamos que tenemos un servicio de productos y un servicio de carrito en nuestro sistema de ecommerce. El API Gateway podría exponer un endpoint /ecommerce que dirige las solicitudes a los servicios correspondientes.
@RestController
@RequestMapping("/ecommerce")
public class ApiGatewayController {
@Autowired
private ProductService productService;
@Autowired
private CartService cartService;
@GetMapping("/products/{productId}")
public Product getProduct(@PathVariable String productId) {
// Enrutamiento al servicio de productos
return productService.getProductById(productId);
}
@GetMapping("/cart/{userId}")
public Cart getCart(@PathVariable String userId) {
// Enrutamiento al servicio de carrito
return cartService.getUserCart(userId);
}
@PostMapping("/cart/{userId}/add")
public ResponseEntity<String> addToCart(@PathVariable String userId, @RequestBody CartItem item) {
// Enrutamiento y llamada al servicio de carrito para agregar un elemento
cartService.addToCart(userId, item);
return ResponseEntity.ok("Item added to the cart.");
}
}
El API Gateway maneja las rutas de los clientes y las redirige a los microservicios correspondientes.
Puede integrar lógica de autenticación y autorización para proteger los microservicios subyacentes.
@GetMapping("/user/{userId}")
public ResponseEntity<User> getUser(@PathVariable String userId, @RequestHeader("Authorization") String token) {
// Lógica de verificación de token y llamada al servicio de usuarios
if (isValidToken(token)) {
return ResponseEntity.ok(userService.getUserById(userId));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
En solicitudes complejas, puede llamar a múltiples microservicios y agregar los resultados antes de enviar una respuesta al cliente.
@GetMapping("/user/{userId}/details")
public ResponseEntity<UserDetails> getUserDetails(@PathVariable String userId) {
// Lógica para llamar al servicio de usuario y al servicio de pedidos y combinar los datos
User user = userService.getUserById(userId);
List<Order> orders = orderService.getOrdersByUserId(userId);
UserDetails userDetails = new UserDetails(user, orders);
return ResponseEntity.ok(userDetails);
}
Los clientes interactúan con un único punto de entrada, lo que simplifica la experiencia de desarrollo y mejora la mantenibilidad. El API Gateway puede simplificar la experiencia del cliente al ofrecer servicios combinados, evitando que los clientes llamen a múltiples endpoints para obtener información relacionada.
Permite escalar los servicios de manera independiente según las necesidades, ya que los clientes solo interactúan con el API Gateway.
@GetMapping(value = "/products/{productId}", headers = "API-Version=1")
public ResponseEntity<ProductV1> getProductV1(@PathVariable String productId) {
// Lógica para llamar al servicio de productos con lógica de versión 1
return ResponseEntity.ok(productService.getProductV1(productId));
}
@GetMapping(value = "/products/{productId}", headers = "API-Version=2")
public ResponseEntity<ProductV2> getProductV2(@PathVariable String productId) {
// Lógica para llamar al servicio de productos con lógica de versión 2
return ResponseEntity.ok(productService.getProductV2(productId));
}
// Utilizando herramientas como Springfox para generar documentación Swagger
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.ecommerce"))
.paths(PathSelectors.any())
.build();
}
}
Esto es un ejemplo simple de como hacerlo en Java, pero lo más común es que se realice en un sistema especializado, con herramientas llamadas API Manager, que facilitan todos los aspectos de seguridad, enrutamiento, escalabilidad, etc. Estos son algunos:
El patrón de descubrimiento de servicios es fundamental en la arquitectura de microservicios y permite a los servicios encontrar y comunicarse entre sí de manera dinámica sin depender de configuraciones estáticas.
La autoconfiguración permite a los servicios adaptarse automáticamente a los cambios en la infraestructura, como la adición o eliminación de instancias de servicios.
Facilita la escalabilidad dinámica, ya que los servicios pueden escalar horizontalmente sin requerir cambios manuales en la configuración de otros servicios.
Si una instancia de servicio falla, el registro de servicios puede actualizar la información, y los consumidores pueden redirigir automáticamente sus solicitudes a instancias disponibles.
Simplifica el despliegue continuo, ya que los nuevos servicios pueden registrarse automáticamente y los consumidores pueden descubrirlos sin intervención manual.
Puede haber una pequeña latencia al buscar servicios, especialmente en grandes sistemas distribuidos.
Mantener la consistencia en el registro puede ser un desafío en entornos altamente distribuidos.
Se deben implementar medidas de seguridad para proteger la información del registro y garantizar la integridad de las comunicaciones entre servicios.
Agrega una capa adicional de complejidad a la arquitectura general, y su introducción debe equilibrarse con los beneficios que aporta.
@SpringBootApplication
@EnableDiscoveryClient
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Autowired
private RestTemplate restTemplate;
@GetMapping("/placeOrder/{productId}")
public String placeOrder(@PathVariable String productId) {
// Utiliza el nombre del servicio registrado ("product-service") en lugar de la URL
String productInfo = restTemplate.getForObject("http://product-service/product/" + productId, String.class);
return "Order placed for Product ID " + productId + ". Product Info: " + productInfo;
}
}
El patrón de descubrimiento de servicios es crucial para arquitecturas de microservicios, ya que permite una interacción flexible y dinámica entre servicios.
Sin embargo, debe implementarse y administrarse cuidadosamente para abordar desafíos potenciales y garantizar la fiabilidad y la seguridad en el entorno de producción.
Por otro lado, en los casos de PaaS gestionados suele ser una funcionalidad que ya nos viene dada.
En resumen, hemos visto como SAGA nos sirve para la gestión de transacciones distribuidas, API Gateway para la entrada única de solicitudes y Service Discovery para la localización dinámica de servicios.
Se evidencia la importancia de adoptar estrategias eficientes en entornos distribuidos.
Estas herramientas ofrecen soluciones sólidas para abordar desafíos como la escalabilidad, la disponibilidad y la confiabilidad, al tiempo que facilitan la adaptabilidad y la mantenibilidad de los sistemas. Si quieres seguir profundizando, en este post vemos un caso de escalabilidad y gestión de recursos con escalado automático. ¡Espero que sea útil!
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.