viernes, 14 de agosto de 2015

En defensa del ESB - SOA con microservicios (Parte II)


Siguiendo con esta entrada, les voy a compartir una experiencia de hace algún tiempo como consultor, donde se me presentó un caso que requería un gran desafío para la época en atributos de calidad. El requisito estaba relacionado con desempeño y exigía un manejo de 140 mil transacciones en un día (con un pico de unas pocas horas).

Dicha organización había experimentado con un famoso Appliance que prometía el procesamiento de un alto volumen transaccional. Sin embargo, con el transcurrir de un tiempo luego de la implementación de los respectivos flujos de mediación se llegó a la siguiente conclusión: es más útil como tranca para que la puerta del centro de cómputo no se abra; suena mal, pero fue así y regresaron a la estrategia de servidores de SOCKETS escalables en su infraestructura.

Ver parte I

Quizás alguien les pudo comentar que un Appliance es por naturaleza Stateless y que no se debía mantener un estado conversacional o que de pronto fue por una mala práctica de implementación. Pero, aquí la cuestión está relacionada con desempeño y con escalabilidad, porque como se podría soportar una variación del negocio que exigiera un incremento de dichas transacciones a un nivel de 500 mil o que la misma Organización ingresara a nuevos mercados ofreciendo nuevos servicios incrementando las transacciones a millones en unos cuantos meses; ¿como se podría atender ahora 10 millones de transacciones? ¿bastaría con adquirir o actualizar al último Appliance que ya soporta más carga? ¿este se podría escalar?, lo dudo.

De igual manera existen organizaciones que enfrentan desafíos de comercio electrónico o de aplicaciones móviles utilizando productos con una limitada capacidad de escalabilidad. ¿Será que los productos que conocemos como ESBSystem podrán soportar cargas de millones de transacciones no en un día sino en minutos? ¿Será que Amazon o Netflix funcionan así? ¿será que tienen que pactar acuerdos marco con los fabricantes de ESBSystem para no tener problemas de licenciamiento? no lo creo, y no soy el primero ni el último que lo comenta.

Ante lo planteado nacen ciertas preocupaciones a nivel de arquitectura para proporcionar escalabilidades elásticas y el manejo de números de transacciones por segundo TPS elevadas; para todo esto los esfuerzos podrían centrarse en autonomía y separación de responsabilidades únicas y no solo el VETRO Pattern que es el centro de los ESBSystem. Aquí es donde aparece bastante útil varias de las ventajas propuestas por los microservicios que hacen posible llevar a cabo los principios planteados en SOA; lo que quiere decir es que esto no es nuevo y que en realidad aporta un buen diseño que permite crear buenos componentes de software que implementen las interfaces definidas sin tener modelos monolíticos como se muestra a continuación:

Microservicios - Fowler

  • Autonomía de servicios: cada servicio con un un proceso autónomo y con funcionalidades que den respuesta a una responsabilidad única que promueva una alta cohesión permitiendo que los componentes desplegados se puedan mantener y evolucionar aislados de otros.  ¿Si se tiene un servicio clave para la operación del negocio tendría sentido que éste comparta el recurso de cómputo con otros componentes diferentes a los que lo implementan?
  • Escalar a nivel de microservicios: cada servicio o grupo de ellos exponen funcionalidades específicas que pueden ser escalados en modelos elásticos en donde los componentes se desplieguen en contenedores o imágenes que contengan todas las aplicaciones  involucradas colocando un balanceador que distribuya la carga. Esto modelo elimina la idea poco eficiente de replicar aplicaciones enteras cargadas de funcionalidad por unas pocas que representen la mayor carga y permitirá en la medida que se aumente la carga transaccional escalar en un número de imágenes de forma automática o si esta baja se podrán reducir (ej. AWS de Amazon - Contenedores). Ver el cubo de escalabilidad propuesto en "El arte de la escalabilidad". De esta manera se podrá escalar a millones de transacciones en un segundo.
  • Simplificar el mantenimiento: al separarse los componentes de cada microservicio siguiendo el principio de responsabilidad única y de empaquetamiento se podrá evolucionar y mantener de forma separada las funcionalidades. De igual forma se podrá eliminar componentes que no se estén utilizando sin afectar a otros servicios; los equipos de desarrollo se podrán separar para que cada quien mantenga y evolucione cada servicio, sin preocuparse de los fallos que llevan a tener miedo a los cambios facilitando la coordinación y comunicación entre éstos proporcionando más valor al negocio. 
  • Implementación en tecnología y entorno apropiado: cada servicio se puede implementar en tecnologías apropiadas que permitan desarrollar los componentes cumpliendo con las expectativas a nivel de atributos de calidad. Algunos componentes podría implementarse en Java, quizás otros en modelos reactivos en la tecnología que más se domine o incluso sobre Java, otros en C++, .NET, etc, etc.
  • Los fallos no perjudican a todo un sistema: si un componente falla no afecta a los demás componentes. Esto porque los servicios se pueden aislar y cada cual tendrá un tratamiento del error específico.
  • Entregas continuas: entregas en múltiples contenedores de cada uno de los servicios. Esto ocasiona un gran desafió que brinda junto a enfoques ágiles una reducción de costes en vez de seguir enfoques tradicionales en cascada o iterativos. Para ésto es clave la automatización, verificación de código, automatización de pruebas y el proceso de despliegue en diferentes entornos, control de versiones y la arquitectura orientada a modelos no monolíticos que permitan hacer pequeños cambios incrementales. 
  • Gestión separada de atributos de calidad: control de atributos de desempeño, escalabilidad, disponibilidad, entre otros, sin afectar a otros servicios. Esto permitirá que cada servicio responda al negocio en la calidad que éste requiere.

Pero ..... ¿Cómo implementar una arquitectura de servicios así?

Identificación y separación de servicios


Se requiere una orientación a servicios que ofrezca al negocio objetivo un alto nivel de calidad separándoles de acuerdo a la funcionalidad que proporcionan acotando bien el contexto. Esto implica que se debe establecer los límites de responsabilidad que proporciona cada servicio.

Para esto, se puede aplicar el principio de diseño de responsabilidades únicas propuesto por Uncle-Bob que define que "Se deben reunir aquellas cosas que cambian por la misma razón y separar aquellas cosas que cambian por diferentes razones".

De igual manera, hay que descomponer funcionalidades en servicios. Quizás la técnica más tradicional consiste en la descomposición basada en verbos separando los servicios que implementan una funcionalidad; ejemplo: consultar, registrar, activar, etc.. Otra manera de hacerlo es a través de sustantivos, separando responsables por las operaciones relacionadas con cada entidad en particular; ejemplo: Cliente, Cuenta, OrdenDeCompra, etc.

Autonomía por servicio

Otro aspecto a tener en cuenta es garantizar la autonomía de cada servicio separándoles por las responsabilidades antes planteadas. Sin embargo, separar cada servicio por un proceso en un recurso de cómputo independiente es complicado por la administración de cada contenedor o plataforma, y por tal motivo en algunos casos se introduce el concepto de dominio o de inventario de servicios.

Separación en dominios o inventarios

Lo anterior, implica que una Organización podría separar los servicios en dominios de negocio relacionados por los productos que proporcionan o en las unidades organizacionales que la componen. Esto podría parecer una herejía con el modelo de microservicios, pero en términos generales es práctico, aunque no quiere decir que ante un servicio crítico de negocio, éste no pueda estar separado en un contenedor independiente que garantice los aspectos de calidad que el negocio requiere.

En el ejemplo anterior se agruparon servicios por productos y algunas unidades de una organización típica de un Banco. Sin embargo, esto no implica que incluso se puedan dividir en más sub-dominios más concretos de acuerdo al comportamiento del negocio o a los atributos de calidad relacionados de éstos; ejemplo: tarjetas de crédito, tarjetas débito o incluso tarjetas franquicia uno o dos en una granularidad más fina.

Esto quiere decir que si se requieren servicios totalmente orientados a microservicios, éstos se podrán implementar de manera autónoma. Sin embargo, algunos se podrían agrupar en un mismo contenedor siguiendo el principio de responsabilidad única o incluso algunos podrían compartir recursos en sistemas monolíticos, dependiendo de los atributos de calidad que se manejen; no hay verdades absolutas.

En la imagen anterior se aprecia la replica de componentes de servicio en otros dominios. Esto se puede manejar si se requieren garantizar autonomía o un mejor desempeño, aunque sería recomendable una orientación a DevOps para controlar la complejidad.

El estado ideal es que cada microservicio se ejecute totalmente en un proceso autónomo, pero esto no es posible muchas veces, y depende de la Organización en la que se desarrolle. Sin embargo, la división en sub-dominios es una buena alternativa.

La división lógica de dominios para la organización de inventarios de servicios también se debe llevar a la práctica en modelos físicos. Aquí es clave la separación de cargas transaccionales evitando que los servicios compartan recursos de cómputo o que se coloquen todos en una sola caja monolítica así esta sea escalada en un modelo vertical o horizontal.
Cada dominio, o cada negocio tendrá sus propios requisitos no funcionales.

Implementación de componentes

Cada componente se podrá implementar en la plataforma o tecnología más conveniente de acuerdo a los requisitos de cada servicio. Esto promueve la diversificación de plataformas y el uso de tecnologías neutrales.

Esto quiere decir que el conjunto de herramientas y tecnologías son diversos y no se basan exclusivamente en una única plataforma. Sin embargo, si es recomendable implementar los componentes internos de cada microservicio con un modelo basado en eventos, que libere recursos de cómputo por cada petición "Stateless" evitando bloqueos con estilos de comunicación petición/respuesta; es recomendable plataformas que se  basen en eventos AMQP.

Algo que se debe tener en cuenta es la diferencia que hay entre orquestación de procesos y servicios. Ambas se fundamentan en el manejo de un componente centralizado que coordina los llamados entre servicios individuales manejando una memoria persistent (long-running) o de corta duración; esto no se debe implementar en una capa de integración (ESB) por su naturaleza stateless y mucho menos en una estrategia de microservicios teniendo en cuenta que lo que se buscan como alternativas de composición son las coreografías.

Con las coreografías se le informa a cada participante que debe ejecutar su trabajo pero no existe un coordinador centralizado que organice los llamados (punto único de control) evitando el uso de recurso de cómputo bloqueante. Cada participante de una coreografía hará su trabajo de forma independiente promoviendo un modelo desacoplado que permita más flexibilidad.

Gestión de datos descentralizada

Uno de los inconvenientes relevantes al implementar servicios autónomos esta relacionado con los medios de almacenamiento. Esto, porque los modelos monolíticos tradicionales de bases de datos RDBMS bloquean las peticiones al implementarse bajo un estilo de comunicación petición/respuesta, ocasionando que las hebras que se ejecuten queden bajo contención a pesar de que se maneje un modelo de eventos, ésto puede ocasionar fallos teniendo en cuenta que ante un bloqueo o una pérdida de conexión se afecten los servicios.

La solución más simple estaría orientada a tener soluciones de bases de datos monolíticas (cluster) con invocaciones petición/respuesta manejando timeouts de conexión, por tiempo de ejecución de consulta o de respuesta de socket. Esto, permitirá desbloquear las peticiones, aunque no se tendrá un modelo autónomo con la implicación de transformaciones entre dominios de sistemas origen y destino.

Por este motivo y teniendo en cuenta que se presume la utilización de coreografías como táctica de composición, se requiere una gestión de datos descentralizada utilizando persistencia políglota. Cada servicio tendrá su propio mecanismo de persistencia a conveniencia combinando incluso estrategias: Llave/Valor, RDBMS, Blobs, documentales, memoria, almacenamiento de grafos, etc.

El uso de modelos de persistencia políglota difieren bastante de los modelos monolíticos donde se maneja un un estilo de persistencia específico orientado casi siempre a RDBMS.

Lo anterior, añade complejidad, pero permite que cada servicio evolucione independiente y con la persistencia que más se adecue a su funcionalidad. DDD proporciona una gran ayuda para solucionar este tipo problemática a través de la separación de dominios, permitiendo que expertos de dominio y desarrolladores puedan tener un lenguaje de entendimiento sobre un dominio en particular haciendo énfasis en el contexto.

Esto ayuda bastante al hacer parte del contexto de un microservicio el medio de almacenamiento. Para dicha implementación puede ser conveniente la utilización de un CQRS para separar los comandos de escritura y las consultas, y un EventSourcing para la gestión general de estados.


Una manera útil para la sincronización entre bases de datos es el uso de un bus de eventos con una implementación de un modelo publicador/suscriptor o el clásico patrón observer con un modelo AMQP, con el fin de que cada suscriptor interesado en algún evento en particular pueda procesar a conveniencia y en el dominio particular los datos de su interés.
Esto facilita la extensión y el mantenimiento desacoplando a los interesados en los datos de un microservicio en particular.

Protocolos de exposición

Los microservicios corren en diferentes procesos, y  en muchos casos en distintas máquinas, por lo tanto los servicios deben usar un mecanismo de comunicación entre procesos. Los mecanismos más comunes son las llamados sincrónicos HTTP y los de mensajería asincrónica AMQP.

Los llamados HTTP siguen un modelo simple petición/respuesta siguiendo en muchos casos un estilo de arquitectura RESTful. Este le suma la ventaja se soportar modelos idempotentes a las comunicaciones entre componentes.

Para el caso de peticiones asíncronas se puede utilizar AMQP. Este enfoque permite desacoplar a productos de consumidores de mensajes, pero añade la complejidad de utilizar una plataforma de mensajería, como RabbitMQ, OpenAMQP, entre otros.

Recuperación de fallos

Es clave que cada microservicio tenga un diseño robusto que esté preparado para el tratamiento de fallos. A diferencia del modelo monolítico se requiere un esfuerzo adicional para esto debido a la intervención de más componentes entre sí que necesitan que se comuniquen rápidamente ante cualquier fallo. A continuación algunos de los patrones básicos definidos el el libro Release it  que nos pueden ayudar a manejar distintos escenarios típicos de fallos.

Es clave la determinación de timeouts entre peticiones de microservicios para ejecutar acciones de gestión de errores. Si algún microservicio no responde en el tiempo máximo pactado SLA se ejecutará una acción para el tratamiento de fallo.

Otro patrón a utilizarse para la protección de invocaciones de microservicios es el Circuit breaker para la protección de éstos ante escenarios de estrés. Éste permite esquivar llamadas cuando el sistema no esté operativo. Este difiere de los re-intentos, en donde los circuit breakers existen para prevenir operaciones en lugar de re-ejecutarlas.

Otra patrón que contiene a utilizase es el de Bulkheads con el fin de aislar fallos entre componentes. La idea es que si se producen un fallo, este no tiene porque afectar los demás servicios, para mantener una falla en una parte del sistema evitando destruir todo el conjunto.

Mensajería

Para nadie es un secreto que un mensaje XML es más grande que una trama separada por algún carácter. Recuerdo un proyecto donde se transmitían mensajes en una arquitectura EAI XML transformándose a partir de un mensaje en una trama; la relación de tamaño era de 300 KB (trama) a 3MB en un documento XML.

Creo que los mensajes con formatos tipo JSON son una buena alternativa porque son ligeros y son descriptivos. La relación entre XML y JSON es también considerable en cuanto a tamaño, y esto puede mejorar el desempeño de los servicios.

Las interfaces estandarizadas para la creación de APIs JSON pueden construirse a través de meta-data generada. Para esto se pueden considerar estándares como apiary.io, ioDocs, Swagger, WADL, o WSDL 2.0.

A través de JSON y algún modelo de construcción de APIs se puede generar un modelo estandarizado interoperable dentro de una organización o en la conectividad con socios de negocio.

Componentes comunes

En un modelo orientado a servicios guiado por microservicios pareciera que no se debiera usar algún tipo de componente común que apoye transversalmente a otros microservicios por ser cada uno independiente dentro de un proceso. Sin embargo, hay atributos no funcionales que se requieren en muchos casos implementar.

Estos, están relacionados con manejo de aspectos de seguridad, gestión de SLAs, descubrimiento de end-points, entre otros, que de no manejarse por un único componente con dicha responsabilidad implicaría replicarlo en cada microservicio. Por tal motivo aparece un patrón orientado a APIs denominado API Gateway que además de resolver aspectos no funcionales proporciona interfaces unificadas para resolver la complejidad de invocar servicios de granularidad fina.

El API Gateway no solo resuelve la complejidad del acceso a servicios de múltiples invocaciones individuales, sino que también permite ante una misma petición determinar de acuerdo al consumidor que respuesta o qué detalle de información se le envía. Esto, teniendo en cuenta que un cliente móvil probablemente no requiere el nivel de detalle que un cliente Web o un tercero o probablemente diferentes canales no necesitan exactamente los mismos datos.
Esto se ve de forma más clara en una estructura de Cliente, donde el cliente móvil requiere un subconjunto de datos de la estructura completa que probablemente requiere un canal Web por aspectos relacionados con experiencia de cliente.


Otro aspecto relacionado es el de optimización para la reducción de latencias y de la reducción del tamaño de los mensajes de acuerdo al canal. Un muy buen ejemplo de un API Gateway lo suministra NetFlix a través de un diseño de aplicación basado en un modelo de programación funcional reactivo donde son capaces de soportar 2 billones de transacciones en un día.

La localización e instancias de servicios (host+puerto) pueden cambiar en incluso de manera dinámica. Por tal motivo el Gateway también tendría dicha responsabilidad; sin embargo, es clave mencionar que en ningún momento estoy planteando el uso de algo similar a un UDDI.

El API Gateway además de optimizar la comunicación entre clientes y la aplicación, encapsula los detalles de los microservicios ocultando cambios en los microservicios. Esto permite evolucionar los microservicios sin impactar a los clientes. Si se cambian algunos microservicios separándose en dos o más microservicios, sólo habría que actualizar el API Gateway para reflejar los cambios, y los consumidorese no se verían afectados.

En cuanto al descubrimiento se recomienda la aplicación de dos patrones que están relacionado a la ubicación desde la cual se descubren los microservicios (Arquitectura de microservicios): descubrimiento desde el servidor o desde el cliente.

El registro más popular para estas iniciativas es Eureka en combinación con Ribbon como cliente para implementar las consultas. Otros registros son Etcd, Consul y Apache zookeeper.

Para implementar un API Gatewy podría considerarse tecnologías basadas en event-driven/reactive debido al desempeño y descoplamiento que se requiere para escalar a gran nivel. Sobre una JVM podría utilizarse librarías basadas en NIO como Netty, Spring Reactor, o SCALA.

El Descubrimiento server-side: al hacer una solicitud a un servicio, el cliente realiza la petición vía un enrutador (también conocido como, balanceador de carga) el cual corre en una ubicación bien definida. Este enrutador consulta a un Registro de Servicios, el cual puede estar construido dentro del mismo enrutador, y envía la solicitud a una instancia disponible del servicio.
Es viable a utilizarse en un modelo en la nube para implementar escalabilidad elástica.


Los escalamientos utilizando EC2 Autoscaling proporcionan direcciones IPs que cambian de forma dinámica. La reducción de esta complejidad se proporciona con el balanceador. Al unir el registro con el API Gateway, se soluciona el único acceso a través del balanceador ocultando detalles del escalamiento elástico proporcionado por la plataforma al ubicar el contenedor o la máquina virtual específica que atenderá la petición.

El descubrimiento del lado del cliente Client-side:  implica la consulta a un registro de servicios que conoce todas las instancias. Este enfoque tiene ventajas relacionadas con la disminución de saltos de red, pero trae como inconveniente el acoplamiento con el registro de servicios de parte del consumidor e implica la implementación de un proxy que conecte el cliente al registro. 
Netflix utiliza este patrón de descubrimiento de servicios.

Esta alternativa de descubrimiento se puede mejorar utilizando el balanceador LB para soportar el comportamiento elástico de escalabilidad al utilizarse contenedores o máquinas virtuales, pero eliminando la responsabilidad de registro. Este modelo se mejora con el LB porque los servicios se registran con una ubicación determinada pero se elimina la complejidad de conocer la instancia específica de servicio a utilizarse porque de esto se encarga el LB.

Gobierno

El gobierno propuesto no se enfoca en una estructura burocrática sino en el control de atributos de calidad de los microservicios y en la gestión del modelo operacional que pudiera implementarse con un enfoque DevOps. El objetivo es controlar las cargas transaccionales de cada microservicio.

No se pretende crear una estructura jerárquica organizacional ni una oficina SOA o un comité o un centro de excelencia. El enfoque está orientado al control de aspectos no funcionales entre los equipos de desarrollo y el equipo operacional.

Conclusiones


Esta entrada pretende mostrar algunos aspectos relevantes a tener en cuenta en un modelo orientado a servicio centrado en un buen diseño guiado por microservicios. Espero le sea de utilidad a algún lector.

Saludos,
@GabrielMorrisS

Ver parte I


4 comentarios:

  1. Gracias Gabriel, muy buena serie de posts

    ResponderEliminar
  2. Gracias Guido ... espero aporte un poco a los interesados...

    ResponderEliminar
  3. Que gran post; Te felicito por este gran aporte.
    Totalmente de acuerdo con lo del ESB que es un cuello de botella. Y que lo vean como un patrón mas no como un producto.
    . Preguntas:
    1- Al implementar un API gateway, no se convierte como un cuello de botella? osea como un ESB que todo tiene q pasar por ahí.
    2- Podrías explicarme mejor lo de coreografía?? Si yo tengo que realizar una operación que involucra varios microservicios, porque es posible reutilizar; se crearía otro microservicio que tuviera esa composición?? o como seria según lo expuesto por ti.

    Gracias!!!

    ResponderEliminar
  4. Gracias Juan ... respondo tus preguntas:
    - El Api gateway si se convierte en un cuello de botella. Sin embargo, aporta reducción de complejidad de elementos comunes como seguridad, logging, invocación de funcionalidades gruesas o de adaptación de acuerdo al canal invocador.
    Creo clave que los gateways sean escalables en un modelo de microservicios; implementados como un proceso autónomo y soportados en contenedores con boots completos que contengan todo lo necesario para su ejecución y que se puedan escalar arrancando nuevos contenedores de acuerdo a incrementos de carga (ver Api gateway de netflix); también es clave que se implementen en modelos reactivos o en entornos non-blocking input-output).

    - La coreografía no implica un componente coordinador. Eso es una orquestación.
    La coreografía es algo parecido al paso de mensajes entre servicios sin que exista uno de ellos que esté sincronizando las invocaciones, memoria, etc.
    Es un modelo desacoplado que busca que los servicios sean stateless..

    Saludos,

    ResponderEliminar