Optimice la integración de software: un tutorial de Apache Camel
Publicado: 2022-03-11El software rara vez, si es que existe, existe en un vacío informativo. Al menos, esa es la suposición que los ingenieros de software podemos hacer para la mayoría de las aplicaciones que desarrollamos.
En cualquier escala, cada pieza de software, de una forma u otra, se comunica con algún otro software por varias razones: para obtener datos de referencia de algún lugar, para enviar señales de monitoreo, para estar en contacto con otros servicios mientras es parte de un sistema distribuido. sistema, y más.
En este tutorial, aprenderá cuáles son algunos de los mayores desafíos de integrar software de gran tamaño y cómo Apache Camel los resuelve con facilidad.
El problema: diseño de arquitectura para la integración de sistemas
Es posible que haya hecho lo siguiente al menos una vez en su vida como ingeniero de software:
- Identifique un fragmento de su lógica empresarial que debería iniciar el envío de datos.
- En la misma capa de aplicación, escriba las transformaciones de datos de acuerdo con lo que espera el destinatario.
- Envuelva los datos en una estructura que sea adecuada para transferir y enrutar a través de una red.
- Abra una conexión a una aplicación de destino utilizando un controlador apropiado o un SDK de cliente.
- Envía los datos y maneja la respuesta.
¿Por qué es esta una mala línea de acción?
Si bien solo tiene unas pocas conexiones de este tipo, sigue siendo manejable. Con un número creciente de relaciones entre sistemas, la lógica comercial de la aplicación se mezcla con la lógica de integración, que se trata de adaptar datos, compensar las diferencias tecnológicas entre dos sistemas y transferir datos al sistema externo con SOAP, REST o solicitudes más exóticas. .
Si estuviera integrando varias aplicaciones, sería increíblemente difícil rastrear la imagen completa de las dependencias en dicho código: ¿Dónde se producen los datos y qué servicios los consumen? Tendrá muchos lugares donde se duplica la lógica de integración, para empezar.
Con tal enfoque, aunque la tarea está técnicamente cumplida, terminamos con grandes problemas con la capacidad de mantenimiento y escalabilidad de la integración. La reorganización rápida de los flujos de datos en este sistema es casi imposible, sin mencionar problemas más profundos como la falta de monitoreo, ruptura de circuitos, recuperación laboriosa de datos, etc.
Todo esto es especialmente importante cuando se integra software en el ámbito de una empresa considerablemente grande. Lidiar con la integración empresarial significa trabajar con un conjunto de aplicaciones, que operan en una amplia gama de plataformas y existen en diferentes ubicaciones. El intercambio de datos en un entorno de software de este tipo es bastante exigente. Debe cumplir con los altos estándares de seguridad de la industria y proporcionar una forma confiable de transferir datos. En un entorno empresarial, la integración de sistemas requiere un diseño de arquitectura completamente elaborado e independiente.
Este artículo le presentará las dificultades únicas que enfrenta la integración de software y le brindará algunas soluciones basadas en la experiencia para las tareas de integración. Nos familiarizaremos con Apache Camel, un marco útil que puede aliviar las peores partes del dolor de cabeza de un desarrollador de integración. Seguiremos con un ejemplo de cómo Camel puede ayudar a establecer la comunicación en un grupo de microservicios con tecnología de Kubernetes.
Dificultades de integración
Un enfoque ampliamente utilizado para resolver el problema es desacoplar una capa de integración en su aplicación. Puede existir dentro de la misma aplicación o como una pieza de software dedicada que se ejecuta de forma independiente; en este último caso, se denomina middleware.
¿A qué problemas se enfrenta normalmente al desarrollar y dar soporte a middleware? En general, tiene los siguientes elementos clave:
- Todos los canales de datos son poco fiables hasta cierto punto. Es posible que los problemas derivados de esta falta de confiabilidad no ocurran mientras la intensidad de los datos sea de baja a moderada. Cada nivel de almacenamiento, desde la memoria de la aplicación hasta los cachés inferiores y el equipo que se encuentra debajo, está sujeto a fallas potenciales. Algunos errores raros surgen solo con grandes volúmenes de datos. Incluso los productos de proveedores maduros y listos para la producción tienen problemas de seguimiento de errores sin resolver relacionados con la pérdida de datos. Un sistema de middleware debería ser capaz de informarle sobre estas pérdidas de datos y proporcionar la reenvío de mensajes de manera oportuna.
- Las aplicaciones utilizan diferentes protocolos y formatos de datos. Esto significa que un sistema de integración es una cortina para transformaciones de datos y adaptadores para otros participantes y utiliza una variedad de tecnologías. Estos pueden incluir llamadas simples a la API REST, pero también pueden ser acceder a un corredor de colas, enviar pedidos CSV a través de FTP o extraer datos por lotes a una tabla de base de datos. Esta es una lista larga y nunca será más corta.
- Los cambios en los formatos de datos y las reglas de enrutamiento son inevitables. Cada paso en el proceso de desarrollo de una aplicación, que cambia la estructura de datos, generalmente conduce a cambios en los formatos y transformaciones de datos de integración. A veces, son necesarios cambios de infraestructura con flujos de datos empresariales reorganizados. Por ejemplo, estos cambios pueden ocurrir cuando se introduce un único punto de validación de datos de referencia que tiene que procesar todas las entradas de datos maestros en toda la empresa. Con
N
sistemas, podemos terminar teniendo un máximo de casiN^2
conexiones entre ellos, por lo que la cantidad de lugares donde se deben aplicar cambios crece bastante rápido. Será como una avalancha. Para mantener la capacidad de mantenimiento, una capa de middleware debe proporcionar una imagen clara de las dependencias con enrutamiento versátil y transformación de datos.
Estas ideas deben tenerse en cuenta al diseñar la integración y elegir la solución de middleware más adecuada. Una de las formas posibles de manejarlo es aprovechar un bus de servicio empresarial (ESB). Pero los ESB proporcionados por los principales proveedores generalmente son demasiado pesados y a menudo son más problemáticos de lo que valen: es casi imposible tener un comienzo rápido con un ESB, tiene una curva de aprendizaje bastante empinada y su flexibilidad se sacrifica a una larga lista. de funciones y herramientas integradas. En mi opinión, las soluciones de integración ligeras de código abierto son muy superiores: son más elásticas, fáciles de implementar en la nube y fáciles de escalar.
La integración de software no es fácil de hacer. Hoy, a medida que construimos arquitecturas de microservicios y tratamos con enjambres de pequeños servicios, también tenemos grandes expectativas sobre la eficiencia con la que deben comunicarse.
Patrones de integración empresarial
Como era de esperar, al igual que el desarrollo de software en general, el desarrollo del enrutamiento y la transformación de datos implica operaciones repetitivas. La experiencia en esta área ha sido resumida y sistematizada por profesionales que manejan problemas de integración desde hace bastante tiempo. Como resultado, hay un conjunto de plantillas extraídas llamadas patrones de integración empresarial que se usan para diseñar flujos de datos. Estos métodos de integración fueron descritos en el libro del mismo nombre por Gregor Hophe y Bobby Wolfe, que es muy parecido al importante libro de Gang of Four pero en el área del software de pegado.
Para dar un ejemplo, el patrón del normalizador introduce un componente que asigna mensajes semánticamente iguales que tienen diferentes formatos de datos a un solo modelo canónico, o el agregador es un EIP que combina una secuencia de mensajes en uno.
Dado que son abstracciones independientes de la tecnología establecidas que se utilizan para resolver problemas de arquitectura, los EIP ayudan a escribir un diseño de arquitectura, que no profundiza en el nivel de código pero describe los flujos de datos con suficiente detalle. Dicha notación para describir rutas de integración no solo hace que el diseño sea conciso, sino que también establece una nomenclatura común y un lenguaje común, que son muy importantes en el contexto de resolver una tarea de integración con miembros del equipo de varias áreas comerciales.
Introduciendo Apache Camel
Hace varios años, estaba construyendo una integración empresarial en una enorme red minorista de comestibles con tiendas en ubicaciones ampliamente distribuidas. Empecé con una solución ESB propietaria, que resultó ser demasiado engorrosa de mantener. Luego, nuestro equipo se encontró con Apache Camel y, después de realizar un trabajo de "prueba de concepto", rápidamente reescribimos todos nuestros flujos de datos en rutas de Camel.
Apache Camel se puede describir como un "enrutador de mediación", un marco de middleware orientado a mensajes que implementa la lista de EIP, con la que me familiaricé. Hace uso de estos patrones, es compatible con todos los protocolos de transporte comunes y tiene un amplio conjunto de adaptadores útiles incluidos. Camel permite el manejo de una serie de rutinas de integración sin necesidad de escribir su propio código.
Aparte de esto, destacaría las siguientes características de Apache Camel:
- Las rutas de integración se escriben como tuberías hechas de bloques. Crea una imagen totalmente transparente para ayudar a rastrear los flujos de datos.
- Camel tiene adaptadores para muchas API populares. Por ejemplo, obtener datos de Apache Kafka, monitorear instancias de AWS EC2, integrarse con Salesforce: todas estas tareas se pueden resolver utilizando componentes disponibles listos para usar.
Las rutas de Apache Camel se pueden escribir en Java o Scala DSL. (También está disponible una configuración XML, pero se vuelve demasiado detallada y tiene peores capacidades de depuración). No impone restricciones en la pila tecnológica de los servicios de comunicación, pero si escribe en Java o Scala, puede integrar Camel en una aplicación en su lugar. de ejecutarlo de forma independiente.
La notación de enrutamiento utilizada por Camel se puede describir con el siguiente pseudocódigo simple:
from(Source) .transform(Transformer) .to(Destination)
El Source
, el Transformer
y el Destination
son puntos finales que hacen referencia a los componentes de implementación por sus URI.
¿Qué permite a Camel resolver los problemas de integración que describí anteriormente? Echemos un vistazo. En primer lugar, la lógica de enrutamiento y transformación ahora vive solo en una configuración dedicada de Apache Camel. En segundo lugar, a través del DSL sucinto y natural junto con el uso de EIP, aparece una imagen de las dependencias entre los sistemas. Está hecho de abstracciones comprensibles y la lógica de enrutamiento es fácilmente ajustable. Y finalmente, no tenemos que escribir montones de código de transformación porque es probable que ya se hayan incluido los adaptadores apropiados.
Debo agregar, Apache Camel es un marco maduro y recibe actualizaciones periódicas. Tiene una gran comunidad y una considerable base de conocimientos acumulados.
Tiene sus propias desventajas. Camel no debe tomarse como una suite de integración compleja. Es una caja de herramientas sin funciones de alto nivel como herramientas de gestión de procesos comerciales o monitores de actividad, pero se puede usar para crear dicho software.
Los sistemas alternativos pueden ser, por ejemplo, Spring Integration o Mule ESB. Para Spring Integration, aunque se considera liviano, en mi experiencia, armarlo y escribir muchos archivos de configuración XML puede resultar inesperadamente complicado y no es una salida fácil. Mule ESB es un conjunto de herramientas robusto y muy funcional, pero como sugiere el nombre, es un bus de servicio empresarial, por lo que pertenece a una categoría de peso diferente. Mule se puede comparar con Fuse ESB, un producto similar basado en Apache Camel con un amplio conjunto de características. Para mí, usar Apache Camel para los servicios de pegado es una obviedad hoy en día. Es fácil de usar y produce una descripción clara de qué va y dónde; al mismo tiempo, es lo suficientemente funcional para crear integraciones complejas.
Escribir una ruta de muestra
Comencemos a escribir el código. Comenzaremos con un flujo de datos síncrono que enruta los mensajes desde una única fuente a una lista de destinatarios. Las reglas de enrutamiento se escribirán en Java DSL.
Usaremos Maven para construir el proyecto. En primer lugar, agregue la siguiente dependencia al pom.xml
:
<dependencies> ... <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-core</artifactId> <version>2.20.0</version> </dependency> </dependencies>
Alternativamente, la aplicación se puede construir sobre el camel-archetype-java
.
Las definiciones de rutas Camel se declaran en el método RouteBuilder.configure
.
public void configure() { errorHandler(defaultErrorHandler().maximumRedeliveries(0)); from("file:orders?noop=true").routeId("main") .log("Incoming File: ${file:onlyname}") .unmarshal().json(JsonLibrary.Jackson, Order.class) // unmarshal JSON to Order class containing List<OrderItem> .split().simple("body.items") // split list to process one by one .to("log:inputOrderItem") .choice() .when().simple("${body.type} == 'Drink'") .to("direct:bar") .when().simple("${body.type} == 'Dessert'") .to("direct:dessertStation") .when().simple("${body.type} == 'Hot Meal'") .to("direct:hotMealStation") .when().simple("${body.type} == 'Cold Meal'") .to("direct:coldMealStation") .otherwise() .to("direct:others"); from("direct:bar").routeId("bar").log("Handling Drink"); from("direct:dessertStation").routeId("dessertStation").log("Handling Dessert"); from("direct:hotMealStation").routeId("hotMealStation").log("Handling Hot Meal"); from("direct:coldMealStation").routeId("coldMealStation").log("Handling Cold Meal"); from("direct:others").routeId("others").log("Handling Something Other"); }
En esta definición, creamos una ruta que obtiene registros del archivo JSON, los divide en elementos y los enruta a un conjunto de controladores según el contenido del mensaje.
Vamos a ejecutarlo en datos de prueba preparados. Obtendremos la salida:
INFO | Total 6 routes, of which 6 are started INFO | Apache Camel 2.20.0 (CamelContext: camel-1) started in 10.716 seconds INFO | Incoming File: order1.json INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Drink', name='Americano', qty='1'}] INFO | Handling Drink INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Hot Meal', name='French Omelette', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Hot Meal', name='Lasagna', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Hot Meal', name='Rice Balls', qty='1'}] INFO | Handling Hot Meal INFO | Exchange[ExchangePattern: InOnly, BodyType: com.antongoncharov.camel.example.model.OrderItem, Body: OrderItem{, type='Dessert', name='Blueberry Pie', qty='1'}] INFO | Handling Dessert
Como era de esperar, Camel enrutó los mensajes a los destinos.
Opciones de transferencia de datos
En el ejemplo anterior, la interacción entre los componentes es síncrona y se realiza a través de la memoria de la aplicación. Sin embargo, hay muchas más formas de comunicarse cuando tratamos con aplicaciones separadas que no comparten memoria:
- Intercambio de archivos. Una aplicación produce archivos de datos compartidos para que la otra los consuma. Es donde vive el espíritu de la vieja escuela. Este método de comunicación tiene una plétora de consecuencias: falta de transacciones y consistencia, bajo rendimiento y coordinación aislada entre sistemas. Muchos desarrolladores terminaron escribiendo soluciones de integración caseras para que el proceso fuera más o menos manejable.
- Base de datos común. Haga que las aplicaciones almacenen los datos que desean compartir en un esquema común de una única base de datos. Diseñar un esquema unificado y manejar el acceso simultáneo a las tablas son los desafíos más destacados de este enfoque. Al igual que con el intercambio de archivos, es fácil que esto se convierta en un cuello de botella permanente.
- Llamada API remota. Proporcione una interfaz para permitir que una aplicación interactúe con otra aplicación en ejecución, como una llamada de método típica. Las aplicaciones comparten funcionalidad a través de invocaciones de API, pero las acopla estrechamente en el proceso.
- Mensajería. Haga que cada aplicación se conecte a un sistema de mensajería común e intercambie datos e invoque el comportamiento de forma asincrónica mediante mensajes. Ni el remitente ni el destinatario tienen que estar funcionando al mismo tiempo para recibir el mensaje.
Hay más formas de interactuar, pero debemos tener en cuenta que, a grandes rasgos, existen dos tipos de interacción: sincrónica y asincrónica. El primero es como llamar a una función en su código: el flujo de ejecución esperará hasta que se ejecute y devuelva un valor. Con un enfoque asincrónico, los mismos datos se envían a través de una cola de mensajes intermedia o un tema de suscripción. Se puede implementar una llamada de función remota asincrónica como EIP de solicitud-respuesta.
Sin embargo, la mensajería asíncrona no es una panacea; implica ciertas restricciones. Rara vez ve API de mensajería en la web; Los servicios REST sincrónicos son mucho más populares. Pero el middleware de mensajería se usa ampliamente en la intranet empresarial o en la infraestructura de back-end del sistema distribuido.
Uso de colas de mensajes
Hagamos nuestro ejemplo asíncrono. Un sistema de software que administra colas y temas de suscripción se denomina intermediario de mensajes. Es como un RDBMS para tablas y columnas. Las colas sirven como integración punto a punto, mientras que los temas son para la comunicación de publicación y suscripción con muchos destinatarios. Usaremos Apache ActiveMQ como intermediario de mensajes JMS porque es sólido e integrable.
Agregue la siguiente dependencia. A veces es excesivo agregar activemq-all
, que contiene todos los archivos jar de ActiveMQ, al proyecto, pero mantendremos las dependencias de nuestra aplicación sin complicaciones.
<dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-all</artifactId> <version>5.15.2</version> </dependency>
A continuación, inicie el intermediario mediante programación. En Spring Boot, obtenemos una configuración automática para esto conectando la dependencia spring-boot-starter-activemq
Maven.
Ejecute un nuevo intermediario de mensajes con los siguientes comandos, especificando solo el extremo del conector:
BrokerService broker = new BrokerService(); broker.addConnector("tcp://localhost:61616"); broker.start();
Y agregue el siguiente fragmento de código de configure
al cuerpo del método de configuración:
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); this.getContext().addComponent("activemq", ActiveMQComponent.jmsComponent(connectionFactory));
Ahora podemos actualizar el ejemplo anterior usando colas de mensajes. Las colas se crearán automáticamente en la entrega del mensaje.
public void configure() { errorHandler(defaultErrorHandler().maximumRedeliveries(0)); ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); this.getContext().addComponent("activemq", ActiveMQComponent.jmsComponent(connectionFactory)); from("file:orders?noop=true").routeId("main") .log("Incoming File: ${file:onlyname}") .unmarshal().json(JsonLibrary.Jackson, Order.class) // unmarshal JSON to Order class containing List<OrderItem> .split().simple("body.items") // split list to process one by one .to("log:inputOrderItem") .choice() .when().simple("${body.type} == 'Drink'") .to("activemq:queue:bar") .when().simple("${body.type} == 'Dessert'") .to("activemq:queue:dessertStation") .when().simple("${body.type} == 'Hot Meal'") .to("activemq:queue:hotMealStation") .when().simple("${body.type} == 'Cold Meal'") .to("activemq:queue:coldMealStation") .otherwise() .to("activemq:queue:others"); from("activemq:queue:bar").routeId("barAsync").log("Drinks"); from("activemq:queue:dessertStation").routeId("dessertAsync").log("Dessert"); from("activemq:queue:hotMealStation").routeId("hotMealAsync").log("Hot Meals"); from("activemq:queue:coldMealStation").routeId("coldMealAsync").log("Cold Meals"); from("activemq:queue:others").routeId("othersAsync").log("Others"); }
Muy bien, ahora la interacción se ha vuelto asíncrona. Los consumidores potenciales de estos datos pueden acceder a ellos cuando estén listos para hacerlo. Este es un ejemplo de acoplamiento flexible, que tratamos de lograr en una arquitectura reactiva. La falta de disponibilidad de uno de los servicios no bloqueará los demás. Además, un consumidor puede escalar y leer de la cola en paralelo. La cola en sí puede escalar y dividirse. Las colas persistentes pueden almacenar los datos en el disco, a la espera de ser procesados, incluso cuando todos los participantes se cayeron. En consecuencia, este sistema es más tolerante a fallos.

Un hecho asombroso es que el CERN usa Apache Camel y ActiveMQ para monitorear los sistemas del Gran Colisionador de Hadrones (LHC). También hay una tesis de maestría interesante que explica la elección de una solución de middleware adecuada para esta tarea. Entonces, como dicen en el discurso de apertura, "¡Sin JMS, no hay física de partículas!"
Supervisión
En el ejemplo anterior, creamos el canal de datos entre dos servicios. Es un punto potencial adicional de falla en una arquitectura, por lo que debemos cuidarlo. Echemos un vistazo a las características de monitoreo que proporciona Apache Camel. Básicamente, expone información estadística sobre sus rutas a través de los MBeans, accesibles por JMX. ActiveMQ expone las estadísticas de la cola de la misma manera.
Activemos el servidor JMX en la aplicación, para permitir que se ejecute con las opciones de la línea de comandos:
-Dorg.apache.camel.jmx.createRmiConnector=true -Dorg.apache.camel.jmx.mbeanObjectDomainName=org.apache.camel -Dorg.apache.camel.jmx.rmiConnector.registryPort=1099 -Dorg.apache.camel.jmx.serviceUrlPath=camel
Ahora ejecuta la aplicación para que la ruta haya hecho su trabajo. Abra la herramienta jconsole
estándar y conéctese al proceso de aplicación. Conéctese al service:jmx:rmi:///jndi/rmi://localhost:1099/camel
. Vaya al dominio org.apache.camel en el árbol de MBeans.
Podemos ver que todo lo relacionado con el enrutamiento está bajo control. Tenemos la cantidad de mensajes en tránsito, el conteo de errores y el conteo de mensajes en las colas. Esta información se puede canalizar a algún conjunto de herramientas de monitoreo con una funcionalidad rica como Graphana o Kibana. Puede hacer esto implementando la conocida pila ELK.
También hay una consola web conectable y extensible que proporciona una interfaz de usuario para administrar Camel, ActiveMQ y muchos más, llamada hawt.io.
Rutas de prueba
Apache Camel tiene una funcionalidad bastante amplia para escribir rutas de prueba con componentes simulados. Es una herramienta poderosa, pero escribir rutas separadas solo para probar es un proceso que requiere mucho tiempo. Sería más eficiente ejecutar pruebas en rutas de producción sin modificar su canalización. Camel tiene esta característica y se puede implementar usando el componente AdviceWith.
Habilitemos la lógica de prueba en nuestro ejemplo y ejecutemos una prueba de muestra.
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-test</artifactId> <version>2.20.0</version> <scope>test</scope> </dependency>
La clase de prueba es:
public class AsyncRouteTest extends CamelTestSupport { @Override protected RouteBuilder createRouteBuilder() throws Exception { return new AsyncRouteBuilder(); } @Before public void mockEndpoints() throws Exception { context.getRouteDefinition("main").adviceWith(context, new AdviceWithRouteBuilder() { @Override public void configure() throws Exception { // we substitute all actual queues with mock endpoints mockEndpointsAndSkip("activemq:queue:bar"); mockEndpointsAndSkip("activemq:queue:dessertStation"); mockEndpointsAndSkip("activemq:queue:hotMealStation"); mockEndpointsAndSkip("activemq:queue:coldMealStation"); mockEndpointsAndSkip("activemq:queue:others"); // and replace the route's source with test endpoint replaceFromWith("file://testInbox"); } }); } @Test public void testSyncInteraction() throws InterruptedException { String testJson = "{\"id\": 1, \"order\": [{\"id\": 1, \"name\": \"Americano\", \"type\": \"Drink\", \"qty\": \"1\"}, {\"id\": 2, \"name\": \"French Omelette\", \"type\": \"Hot Meal\", \"qty\": \"1\"}, {\"id\": 3, \"name\": \"Lasagna\", \"type\": \"Hot Meal\", \"qty\": \"1\"}, {\"id\": 4, \"name\": \"Rice Balls\", \"type\": \"Hot Meal\", \"qty\": \"1\"}, {\"id\": 5, \"name\": \"Blueberry Pie\", \"type\": \"Dessert\", \"qty\": \"1\"}]}"; // get mocked endpoint and set an expectation MockEndpoint mockEndpoint = getMockEndpoint("mock:activemq:queue:hotMealStation"); mockEndpoint.expectedMessageCount(3); // simulate putting file in the inbox folder template.sendBodyAndHeader("file://testInbox", testJson, Exchange.FILE_NAME, "test.json"); //checks that expectations were met assertMockEndpointsSatisfied(); } }
Ahora ejecute pruebas para la aplicación con mvn test
. Podemos ver que nuestra ruta se ha ejecutado con éxito con el consejo de prueba. No hay mensajes pasados a través de las colas reales y se han superado las pruebas.
INFO | Route: main started and consuming from: file://testInbox <...> INFO | Incoming File: test.json <...> INFO | Asserting: mock://activemq:queue:hotMealStation is satisfied
Uso de Apache Camel con Kubernetes Cluster
Uno de los problemas de integración hoy en día es que las aplicaciones ya no son estáticas. En una infraestructura en la nube, tratamos con servicios virtuales que se ejecutan en varios nodos al mismo tiempo. Habilita la arquitectura de microservicios con una red de servicios pequeños y livianos que interactúan entre sí. Estos servicios tienen una vida útil poco fiable y tenemos que descubrirlos dinámicamente.
Unir los servicios en la nube es una tarea que se puede resolver con Apache Camel. Es especialmente interesante por el estilo EIP y el hecho de que Camel tiene muchos adaptadores y admite una amplia gama de protocolos. La versión reciente 2.18 agrega el componente ServiceCall, que presenta una función para llamar a una API y resolver su dirección a través de mecanismos de descubrimiento de clústeres. Actualmente, es compatible con Consul, Kubernetes, Ribbon, etc. Se pueden encontrar fácilmente algunos ejemplos de código, donde ServiceCall está configurado con Consul. Usaremos Kubernetes aquí porque es mi solución de agrupación en clústeres favorita.
El esquema de integración será el siguiente:
El servicio de Order
y el servicio de Inventory
serán un par de aplicaciones Spring Boot triviales que devolverán datos estáticos. No estamos atados a una pila de tecnología en particular aquí. Estos servicios están produciendo los datos que queremos procesar.
Controlador de servicio de pedidos:
@RestController public class OrderController { private final OrderStorage orderStorage; @Autowired public OrderController(OrderStorage orderStorage) { this.orderStorage = orderStorage; } @RequestMapping("/info") public String info() { return "Order Service UU/orders") public List<Order> getAll() { return orderStorage.getAll(); } @RequestMapping("/orders/{id}") public Order getOne(@PathVariable Integer id) { return orderStorage.getOne(id); } }
Produce datos en el formato:
[{"id":1,"items":[2,3,4]},{"id":2,"items":[5,3]}]
El controlador del servicio de Inventory
es absolutamente similar al del servicio de Order
:
@RestController public class InventoryController { private final InventoryStorage inventoryStorage; @Autowired public InventoryController(InventoryStorage inventoryStorage) { this.inventoryStorage = inventoryStorage; } @RequestMapping("/info") public String info() { return "Inventory Service UU/items") public List<InventoryItem> getAll() { return inventoryStorage.getAll(); } @RequestMapping("/items/{id}") public InventoryItem getOne(@PathVariable Integer id) { return inventoryStorage.getOne(id); } }
InventoryStorage
es un repositorio genérico que contiene datos. En este ejemplo, devuelve objetos estáticos predefinidos, que se calculan según el siguiente formato.
[{"id":1,"name":"Laptop","description":"Up to 12-hours battery life","price":499.9},{"id":2,"name":"Monitor","description":"27-inch, response time: 7ms","price":200.0},{"id":3,"name":"Headphones","description":"Soft leather ear-cups","price":29.9},{"id":4,"name":"Mouse","description":"Designed for comfort and portability","price":19.0},{"id":5,"name":"Keyboard","description":"Layout: US","price":10.5}]
Escribamos una ruta de puerta de enlace que los conecte, pero sin ServiceCall en este paso:
rest("/orders") .get("/").description("Get all orders with details").outType(TestResponse.class) .route() .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .to("http4://localhost:8082/orders?bridgeEndpoint=true") .unmarshal(formatOrder) .enrich("direct:enrichFromInventory", new OrderAggregationStrategy()) .to("log:result") .endRest(); from("direct:enrichFromInventory") .transform().simple("${null}") .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .to("http4://localhost:8081/items?bridgeEndpoint=true") .unmarshal(formatInventory);
Ahora imagine que cada servicio ya no es una instancia específica sino una nube de instancias que funcionan como una sola. Usaremos Minikube para probar el clúster de Kubernetes localmente.
Configure las rutas de red para ver los nodos de Kubernetes localmente (el ejemplo dado es para un entorno Mac/Linux):
# remove existing routes sudo route -n delete 10/24 > /dev/null 2>&1 # add routes sudo route -n add 10.0.0.0/24 $(minikube ip) # 172.17.0.0/16 ip range is used by docker in minikube sudo route -n add 172.17.0.0/16 $(minikube ip) ifconfig 'bridge100' | grep member | awk '{print $2}' # use interface name from the output of the previous command # needed for xhyve driver, which I'm using for testing sudo ifconfig bridge100 -hostfilter en5
Envuelva los servicios en contenedores Docker con una configuración Dockerfile como esta:
FROM openjdk:8-jdk-alpine VOLUME /tmp ADD target/order-srv-1.0-SNAPSHOT.jar app.jar ADD target/lib lib ENV JAVA_OPTS="" ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar
Compile y envíe las imágenes de servicio al registro de Docker. Ahora ejecute los nodos en el clúster local de Kubernetes.
Configuración de implementación de Kubernetes.yaml:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: inventory spec: replicas: 3 selector: matchLabels: app: inventory template: metadata: labels: app: inventory spec: containers: - name: inventory image: inventory-srv:latest imagePullPolicy: Never ports: - containerPort: 8081
Exponga estas implementaciones como servicios en el clúster:
kubectl expose deployment order-srv --type=NodePort kubectl expose deployment inventory-srv --type=NodePort
Ahora podemos verificar si las solicitudes son atendidas por nodos elegidos al azar del clúster. Ejecute curl -X http://192.168.99.100:30517/info
secuencialmente varias veces para acceder a minikube NodePort para el servicio expuesto (usando su host y puerto). En el resultado, vemos que hemos logrado el equilibrio de solicitudes.
Inventory Service UUID = 22f8ca6b-f56b-4984-927b-cbf9fcf81da5 Inventory Service UUID = b7a4d326-1e76-4051-a0a6-1016394fafda Inventory Service UUID = b7a4d326-1e76-4051-a0a6-1016394fafda Inventory Service UUID = 22f8ca6b-f56b-4984-927b-cbf9fcf81da5 Inventory Service UUID = 50323ddb-3ace-4424-820a-6b4e85775af4
Agregue camel-kubernetes
y camel-netty4-http
al pom.xml
del proyecto. Luego configure el componente ServiceCall para usar el descubrimiento de nodo principal de Kubernetes compartido para todas las llamadas de servicio entre las definiciones de ruta:
KubernetesConfiguration kubernetesConfiguration = new KubernetesConfiguration(); kubernetesConfiguration.setMasterUrl("https://192.168.64.2:8443"); kubernetesConfiguration.setClientCertFile("/Users/antongoncharov/.minikube/client.crt"); kubernetesConfiguration.setClientKeyFile("/Users/antongoncharov/.minikube/client.key"); kubernetesConfiguration.setNamespace("default”); ServiceCallConfigurationDefinition config = new ServiceCallConfigurationDefinition(); config.setServiceDiscovery(new KubernetesClientServiceDiscovery(kubernetesConfiguration)); context.setServiceCallConfiguration(config);
ServiceCall EIP complementa bien Spring Boot. La mayoría de las opciones se pueden configurar directamente en el archivo application.properties
.
Potencie la ruta Camel con el componente ServiceCall:
rest("/orders") .get("/").description("Get all orders with details").outType(TestResponse.class) .route() .hystrix() .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .serviceCall("customer-srv","http4:customer-deployment?bridgeEndpoint=true") .unmarshal(formatOrder) .enrich("direct:enrichFromInventory", new OrderAggregationStrategy()) .to("log:result") .endRest(); from("direct:enrichFromInventory") .transform().simple("${null}") .setHeader("Content-Type", constant("application/json")) .setHeader("Accept", constant("application/json")) .setHeader(Exchange.HTTP_METHOD, constant("GET")) .removeHeaders("CamelHttp*") .serviceCall("order-srv","http4:order-srv?bridgeEndpoint=true") .unmarshal(formatInventory);
También activamos Circuit Breaker en la ruta. Es un gancho de integración que permite pausar las llamadas al sistema remoto en caso de errores de entrega o falta de disponibilidad del destinatario. Esto está diseñado para evitar la falla del sistema en cascada. El componente Hystrix ayuda a lograr esto mediante la implementación del patrón Circuit Breaker.
Ejecutémoslo y enviemos una solicitud de prueba; obtendremos la respuesta agregada de ambos servicios.
[{"id":1,"items":[{"id":2,"name":"Monitor","description":"27-inch, response time: 7ms","price":200.0},{"id":3,"name":"Headphones","description":"Soft leather ear-cups","price":29.9},{"id":4,"name":"Mouse","description":"Designed for comfort and portability","price":19.0}]},{"id":2,"items":[{"id":5,"name":"Keyboard","description":"Layout: US","price":10.5},{"id":3,"name":"Headphones","description":"Soft leather ear-cups","price":29.9}]}]
El resultado es el esperado.
Otros casos de uso
Mostré cómo Apache Camel puede integrar microservicios en un clúster. ¿Cuáles son otros usos de este marco? En general, es útil en cualquier lugar donde el enrutamiento basado en reglas pueda ser una solución. For instance, Apache Camel can be a middleware for the Internet of Things with the Eclipse Kura adapter. It can handle monitoring by ferrying log signals from various components and services, like in the CERN system. It can also be an integration framework for enterprise SOA or be a pipeline for batch data processing, although it doesn't compete well with Apache Spark in this area.
Conclusión
You can see that systems integration isn't an easy process. We're lucky because a lot of experience has been gathered. It's important to apply it correctly to build flexible and fault-tolerant solutions.
To ensure correct application, I recommend having a checklist of important integration aspects. Must-have items include:
- Is there a separate integration layer?
- Are there tests for integration?
- Do we know the expected peak data intensity?
- Do we know the expected data delivery time?
- Does message correlation matter? What if a sequence breaks?
- Should we do it in a synchronous or asynchronous way?
- Where do formats and routing rules change more frequently?
- Do we have ways to monitor the process?
In this article, we tried Apache Camel, a lightweight integration framework, which helps save time and effort when solving integration problems. As we showed, it can serve as a tool, supporting the relevant microservice architecture by taking full responsibility for data exchange between microservices.
If you're interested in learning more about Apache Camel, I highly recommend the book “Camel in Action” by the framework's creator, Claus Ibsen. Official documentation is available at camel.apache.org.