Simplifique a integração de software: um tutorial do Apache Camel

Publicados: 2022-03-11

O software raramente, se é que existe, existe em um vácuo informacional. Pelo menos, essa é a suposição que nós, engenheiros de software, podemos fazer para a maioria dos aplicativos que desenvolvemos.

Em qualquer escala, cada pedaço de software - de uma forma ou de outra - se comunica com algum outro software por vários motivos: obter dados de referência de algum lugar, enviar sinais de monitoramento, estar em contato com outros serviços enquanto faz parte de uma rede distribuída. sistema e muito mais.

Neste tutorial, você aprenderá quais são alguns dos maiores desafios da integração de grandes softwares e como o Apache Camel os resolve com facilidade.

O Problema: Projeto de Arquitetura para Integração de Sistemas

Você pode ter feito o seguinte pelo menos uma vez em sua vida de engenharia de software:

  • Identifique um fragmento de sua lógica de negócios que deve iniciar o envio de dados.
  • Na mesma camada de aplicação, escreva as transformações de dados de acordo com o que o destinatário está esperando.
  • Envolva os dados em uma estrutura adequada para transferência e roteamento em uma rede.
  • Abra uma conexão com um aplicativo de destino usando um driver apropriado ou um SDK de cliente.
  • Envie os dados e manipule a resposta.

Por que essa é uma linha de ação ruim?

Embora você tenha apenas algumas conexões desse tipo, ele permanece gerenciável. Com um número crescente de relações entre os sistemas, a lógica de negócios do aplicativo se mistura com a lógica de integração, que trata de adaptar dados, compensar diferenças tecnológicas entre dois sistemas e transferir dados para o sistema externo com SOAP, REST ou solicitações mais exóticas .

Se você estivesse integrando vários aplicativos, seria incrivelmente difícil refazer todo o quadro de dependências nesse código: onde os dados são produzidos e quais serviços os consomem? Você terá muitos lugares onde a lógica de integração é duplicada, para inicializar.

Com essa abordagem, embora a tarefa seja tecnicamente cumprida, acabamos com grandes problemas com a capacidade de manutenção e escalabilidade da integração. A rápida reorganização dos fluxos de dados neste sistema é quase impossível, sem mencionar questões mais profundas como a falta de monitoramento, interrupção de circuito, recuperação de dados trabalhosa, etc.

Tudo isso é especialmente importante ao integrar software no escopo de uma empresa consideravelmente grande. Lidar com a integração empresarial significa trabalhar com um conjunto de aplicativos, que operam em uma ampla gama de plataformas e existem em diferentes locais. A troca de dados em tal cenário de software é bastante exigente. Ele deve atender aos padrões de alta segurança do setor e fornecer uma maneira confiável de transferir dados. Em um ambiente corporativo, a integração de sistemas requer um projeto de arquitetura separado e cuidadosamente elaborado.

Este artigo apresentará as dificuldades exclusivas enfrentadas na integração de software, além de fornecer algumas soluções orientadas à experiência para tarefas de integração. Vamos nos familiarizar com o Apache Camel, uma estrutura útil que pode aliviar as piores dores de cabeça de um desenvolvedor de integração. Seguiremos com um exemplo de como o Camel pode ajudar a estabelecer a comunicação em um cluster de microsserviços com tecnologia Kubernetes.

Dificuldades de Integração

Uma abordagem amplamente utilizada para resolver o problema é desacoplar uma camada de integração em seu aplicativo. Ele pode existir dentro do mesmo aplicativo ou como um software dedicado de execução independente - no último caso, chamado de middleware.

Quais problemas você normalmente enfrenta ao desenvolver e dar suporte ao middleware? Em geral, você tem os seguintes itens principais:

  • Todos os canais de dados não são confiáveis ​​até certo ponto. Os problemas decorrentes dessa falta de confiabilidade podem não ocorrer enquanto a intensidade dos dados for baixa a moderada. Cada nível de armazenamento, desde a memória do aplicativo até os caches e equipamentos inferiores, está sujeito a possíveis falhas. Alguns erros raros surgem apenas com grandes volumes de dados. Mesmo produtos de fornecedores maduros prontos para produção têm problemas não resolvidos de rastreadores de bugs relacionados à perda de dados. Um sistema de middleware deve ser capaz de informá-lo sobre essas perdas de dados e fornecer reenvio de mensagens em tempo hábil.
  • Os aplicativos usam diferentes protocolos e formatos de dados. Isso significa que um sistema de integração é uma cortina para transformações de dados e adaptadores para outros participantes e utiliza uma variedade de tecnologias. Isso pode incluir chamadas de API REST simples, mas também podem acessar um agente de filas, enviar pedidos CSV por FTP ou extrair dados em lote para uma tabela de banco de dados. Esta é uma lista longa e nunca será menor.
  • Mudanças nos formatos de dados e regras de roteamento são inevitáveis. Cada etapa no processo de desenvolvimento de um aplicativo, que altera a estrutura de dados, geralmente leva a mudanças nos formatos e transformações dos dados de integração. Às vezes, são necessárias mudanças na infraestrutura com fluxos de dados corporativos reorganizados. Por exemplo, essas alterações podem ocorrer ao introduzir um único ponto de validação de dados de referência que deve processar todas as entradas de dados mestre em toda a empresa. Com sistemas N , podemos acabar tendo no máximo quase N^2 conexões entre eles, então o número de lugares onde as mudanças devem ser aplicadas cresce bastante rápido. Será como uma avalanche. Para sustentar a capacidade de manutenção, uma camada de middleware deve fornecer uma imagem clara das dependências com roteamento versátil e transformação de dados.

Essas ideias devem ser mantidas em mente ao projetar a integração e escolher a solução de middleware mais adequada. Uma das maneiras possíveis de lidar com isso é alavancar um barramento de serviço corporativo (ESB). Mas os ESBs fornecidos pelos principais fornecedores geralmente são muito pesados ​​e geralmente causam mais problemas do que valem a pena: é quase impossível ter um início rápido com um ESB, ele tem uma curva de aprendizado bastante íngreme e sua flexibilidade é sacrificada a uma longa lista de recursos e ferramentas integradas. Na minha opinião, as soluções leves de integração de código aberto são muito superiores – elas são mais elásticas, fáceis de implantar na nuvem e fáceis de dimensionar.

A integração de software não é fácil de fazer. Hoje, à medida que construímos arquiteturas de microsserviços e lidamos com enxames de pequenos serviços, também temos grandes expectativas sobre a eficiência com que eles devem se comunicar.

Padrões de Integração Empresarial

Como seria de esperar, como o desenvolvimento de software em geral, o desenvolvimento de roteamento e transformação de dados envolve operações repetitivas. A experiência nesta área tem sido resumida e sistematizada por profissionais que lidam com problemas de integração há algum tempo. No resultado, há um conjunto de modelos extraídos chamados padrões de integração empresarial usados ​​para projetar fluxos de dados. Esses métodos de integração foram descritos no livro de mesmo nome de Gregor Hophe e Bobby Wolfe, que é muito parecido com o livro significativo da Gangue dos Quatro, mas na área de software de colagem.

Para dar um exemplo, o padrão normalizador introduz um componente que mapeia mensagens semanticamente iguais que possuem diferentes formatos de dados para um único modelo canônico, ou o agregador é um EIP que combina uma sequência de mensagens em uma.

Uma vez que são abstrações agnósticas de tecnologia estabelecidas usadas para resolver problemas de arquitetura, os EIPs ajudam a escrever um projeto de arquitetura, que não se aprofunda no nível do código, mas descreve os fluxos de dados com detalhes suficientes. Essa notação para descrever as rotas de integração não apenas torna o projeto conciso, mas também define uma nomenclatura comum e uma linguagem comum, que são muito importantes no contexto da resolução de uma tarefa de integração com membros da equipe de várias áreas de negócios.

Apresentando o Apache Camel

Vários anos atrás, eu estava construindo uma integração corporativa em uma enorme rede de varejo de supermercado com lojas em locais amplamente distribuídos. Comecei com uma solução ESB proprietária, que acabou sendo excessivamente complicada de manter. Então, nossa equipe encontrou o Apache Camel e, depois de fazer um trabalho de “prova de conceito”, reescrevemos rapidamente todos os nossos fluxos de dados nas rotas do Camel.

O Apache Camel pode ser descrito como um “roteador de mediação”, uma estrutura de middleware orientada a mensagens que implementa a lista de EIPs, com a qual me familiarizei. Ele faz uso desses padrões, suporta todos os protocolos de transporte comuns e possui um vasto conjunto de adaptadores úteis incluídos. O Camel permite o manuseio de várias rotinas de integração sem a necessidade de escrever seu próprio código.

Além disso, destacaria os seguintes recursos do Apache Camel:

  • As rotas de integração são escritas como pipelines feitos de blocos. Ele cria uma imagem totalmente transparente para ajudar a rastrear os fluxos de dados.
  • O Camel possui adaptadores para muitas APIs populares. Por exemplo, obter dados do Apache Kafka, monitorar instâncias do AWS EC2, integrar-se ao Salesforce — todas essas tarefas podem ser resolvidas usando componentes disponíveis prontos para uso.

As rotas do Apache Camel podem ser escritas em Java ou Scala DSL. (Uma configuração XML também está disponível, mas se torna muito detalhada e tem recursos de depuração piores.) Ela não impõe restrições à pilha de tecnologia dos serviços de comunicação, mas se você escrever em Java ou Scala, poderá incorporar o Camel em um aplicativo. de executá-lo sozinho.

A notação de roteamento usada pelo Camel pode ser descrita com o seguinte pseudocódigo simples:

 from(Source) .transform(Transformer) .to(Destination)

Source , Transformer e Destination são endpoints que se referem a componentes de implementação por seus URIs.

O que permite que o Camel resolva os problemas de integração que descrevi anteriormente? Vamos dar uma olhada. Em primeiro lugar, a lógica de roteamento e transformação agora vive apenas em uma configuração dedicada do Apache Camel. Em segundo lugar, através do sucinto e natural DSL em conjunto com o uso de EIPs, surge um quadro de dependências entre sistemas. É feito de abstrações compreensíveis e a lógica de roteamento é facilmente ajustável. E, finalmente, não precisamos escrever pilhas de código de transformação porque os adaptadores apropriados provavelmente já estão incluídos.

Integrações

Devo acrescentar que o Apache Camel é uma estrutura madura e recebe atualizações regulares. Tem uma grande comunidade e uma base de conhecimento cumulativa considerável.

Ele tem suas próprias desvantagens. O Camel não deve ser considerado um conjunto de integração complexo. É uma caixa de ferramentas sem recursos de alto nível, como ferramentas de gerenciamento de processos de negócios ou monitores de atividades, mas pode ser usada para criar esse software.

Sistemas alternativos podem ser, por exemplo, Spring Integration ou Mule ESB. Para Spring Integration, embora seja considerado leve, na minha experiência, juntá-lo e escrever muitos arquivos de configuração XML pode se tornar inesperadamente complicado e dificilmente é uma saída fácil. O Mule ESB é um conjunto de ferramentas robusto e muito funcional, mas como o nome sugere, é um barramento de serviço corporativo, portanto, pertence a uma categoria de peso diferente. O Mule pode ser comparado ao Fuse ESB, um produto similar baseado no Apache Camel com um rico conjunto de recursos. Para mim, usar o Apache Camel para serviços de colagem é um acéfalo hoje. É fácil de usar e produz uma descrição clara do que vai para onde — ao mesmo tempo, é funcional o suficiente para construir integrações complexas.

Escrevendo uma rota de amostra

Vamos começar a escrever o código. Começaremos com um fluxo de dados síncrono que roteia mensagens de uma única fonte para uma lista de destinatários. As regras de roteamento serão escritas em Java DSL.

Usaremos o Maven para construir o projeto. Primeiramente adicione a seguinte dependência ao pom.xml :

 <dependencies> ... <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-core</artifactId> <version>2.20.0</version> </dependency> </dependencies>

Alternativamente, o aplicativo pode ser construído sobre o camel-archetype-java .

As definições de rota de camelo são declaradas no 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"); }

Nesta definição, criamos uma rota que busca registros do arquivo JSON, os divide em itens e roteia para um conjunto de manipuladores com base no conteúdo da mensagem.

Vamos executá-lo em dados de teste preparados. Teremos a saída:

 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 esperado, Camel encaminhou mensagens para os destinos.

Opções de transferência de dados

No exemplo acima, a interação entre os componentes é síncrona e realizada através da memória da aplicação. No entanto, existem muitas outras maneiras de se comunicar quando lidamos com aplicativos separados que não compartilham memória:

  • Troca de arquivos. Um aplicativo produz arquivos de dados compartilhados para o outro consumir. É onde vive o espírito da velha guarda. Esse método de comunicação tem uma infinidade de consequências: falta de transações e consistência, baixo desempenho e coordenação isolada entre os sistemas. Muitos desenvolvedores acabaram escrevendo soluções de integração caseiras para tornar o processo mais ou menos gerenciável.
  • Banco de dados comum. Faça com que os aplicativos armazenem os dados que desejam compartilhar em um esquema comum de um único banco de dados. Projetar o esquema unificado e lidar com o acesso simultâneo às tabelas são os desafios mais importantes dessa abordagem. Assim como na troca de arquivos, é fácil que isso se torne um gargalo permanente.
  • Chamada de API remota. Forneça uma interface para permitir que um aplicativo interaja com outro aplicativo em execução, como uma chamada de método típica. Os aplicativos compartilham a funcionalidade por meio de invocações de API, mas os une firmemente no processo.
  • Mensagens. Faça com que cada aplicativo se conecte a um sistema de mensagens comum e troque dados e invoque o comportamento de forma assíncrona usando mensagens. Nem o remetente nem o destinatário precisam estar funcionando ao mesmo tempo para que a mensagem seja entregue.

Existem mais formas de interação, mas devemos ter em mente que, em linhas gerais, existem dois tipos de interação: síncrona e assíncrona. A primeira é como chamar uma função em seu código - o fluxo de execução estará esperando até que seja executado e retorne um valor. Com uma abordagem assíncrona, os mesmos dados são enviados por meio de uma fila de mensagens intermediária ou tópico de assinatura. Uma chamada de função remota assíncrona pode ser implementada como o EIP de solicitação-resposta.

No entanto, as mensagens assíncronas não são uma panacéia; envolve certas restrições. Você raramente vê APIs de mensagens na web; os serviços REST síncronos são muito mais populares. Mas o middleware de mensagens é amplamente usado na intranet corporativa ou na infraestrutura de back-end do sistema distribuído.

Usando filas de mensagens

Vamos tornar nosso exemplo assíncrono. Um sistema de software que gerencia filas e tópicos de assinatura é chamado de agente de mensagens. É como um RDBMS para tabelas e colunas. As filas servem como integração ponto a ponto, enquanto os tópicos são para comunicação de publicação-assinatura com muitos destinatários. Usaremos o Apache ActiveMQ como um agente de mensagens JMS porque é sólido e incorporável.

Adicione a seguinte dependência. Às vezes é excessivo adicionar activemq-all , que contém todos os jars ActiveMQ, ao projeto, mas manteremos as dependências do nosso aplicativo descomplicadas.

 <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-all</artifactId> <version>5.15.2</version> </dependency>

Em seguida, inicie o broker programaticamente. No Spring Boot, obtemos uma configuração automática para isso conectando a dependência do Maven spring-boot-starter-activemq .

Execute um novo agente de mensagens com os seguintes comandos, especificando apenas o endpoint do conector:

 BrokerService broker = new BrokerService(); broker.addConnector("tcp://localhost:61616"); broker.start();

E adicione o seguinte trecho de configure :

 ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616"); this.getContext().addComponent("activemq", ActiveMQComponent.jmsComponent(connectionFactory));

Agora podemos atualizar o exemplo anterior usando filas de mensagens. As filas serão criadas automaticamente na entrega da mensagem.

 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"); }

Tudo bem, agora a interação se tornou assíncrona. Os potenciais consumidores desses dados podem acessá-los quando estiverem prontos. Este é um exemplo de acoplamento fraco, que tentamos alcançar em uma arquitetura reativa. A indisponibilidade de um dos serviços não bloqueará os outros. Além disso, um consumidor pode dimensionar e ler a fila em paralelo. A própria fila pode ser dimensionada e particionada. Filas persistentes podem armazenar os dados no disco, esperando para serem processados, mesmo quando todos os participantes estiverem inativos. Consequentemente, este sistema é mais tolerante a falhas.

Um fato surpreendente é que o CERN usa Apache Camel e ActiveMQ para monitorar os sistemas do Large Hadron Collider (LHC). Há também uma tese de mestrado interessante explicando a escolha de uma solução de middleware apropriada para esta tarefa. Então, como eles dizem na palestra, “Sem JMS – sem física de partículas!”

Monitoramento

No exemplo anterior, criamos o canal de dados entre dois serviços. É um ponto potencial adicional de falha em uma arquitetura, então temos que cuidar dele. Vamos dar uma olhada nos recursos de monitoramento que o Apache Camel oferece. Basicamente, ele expõe informações estatísticas sobre suas rotas através dos MBeans, acessíveis pelo JMX. O ActiveMQ expõe as estatísticas da fila da mesma maneira.

Vamos ativar o servidor JMX no aplicativo, para habilitá-lo para ser executado com as opções de linha de comando:

 -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

Agora execute o aplicativo para que a rota tenha feito seu trabalho. Abra a ferramenta jconsole padrão e conecte-se ao processo de aplicação. Conecte-se ao service:jmx:rmi:///jndi/rmi://localhost:1099/camel . Acesse o domínio org.apache.camel na árvore MBeans.

Captura de tela 1

Podemos ver que tudo sobre roteamento está sob controle. Temos o número de mensagens em andamento, a contagem de erros e a contagem de mensagens nas filas. Essas informações podem ser canalizadas para algum conjunto de ferramentas de monitoramento com funcionalidades avançadas, como Graphana ou Kibana. Você pode fazer isso implementando a conhecida pilha ELK.

Há também um console da Web conectável e extensível que fornece uma interface do usuário para gerenciar Camel, ActiveMQ e muito mais, chamado hawt.io.

Captura de tela 2

Rotas de teste

O Apache Camel tem uma funcionalidade bastante ampla para escrever rotas de teste com componentes simulados. É uma ferramenta poderosa, mas escrever rotas separadas apenas para teste é um processo demorado. Seria mais eficiente executar testes em rotas de produção sem modificar seu pipeline. O Camel possui esse recurso e pode ser implementado usando o componente AdviceWith.

Vamos habilitar a lógica de teste em nosso exemplo e executar um teste de amostra.

 <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>

A classe de teste é:

 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(); } }

Agora execute testes para o aplicativo com mvn test . Podemos ver que nossa rota foi executada com sucesso com o conselho de teste. Não há mensagens passadas pelas filas reais e os testes foram aprovados.

 INFO | Route: main started and consuming from: file://testInbox <...> INFO | Incoming File: test.json <...> INFO | Asserting: mock://activemq:queue:hotMealStation is satisfied

Usando Apache Camel com cluster Kubernetes

Um dos problemas de integração hoje é que os aplicativos não são mais estáticos. Em uma infraestrutura em nuvem, lidamos com serviços virtuais executados em vários nós ao mesmo tempo. Ele habilita a arquitetura de microsserviços com uma rede de serviços pequenos e leves interagindo entre si. Esses serviços têm uma vida útil não confiável e precisamos descobri-los dinamicamente.

A colagem de serviços em nuvem é uma tarefa que pode ser resolvida com o Apache Camel. É especialmente interessante devido ao sabor EIP e ao fato de o Camel ter muitos adaptadores e suportar uma ampla variedade de protocolos. A versão recente 2.18 adiciona o componente ServiceCall, que apresenta um recurso de chamar uma API e resolver seu endereço por meio de mecanismos de descoberta de cluster. Atualmente, ele suporta Consul, Kubernetes, Ribbon, etc. Alguns exemplos de código, onde ServiceCall é configurado com Consul, podem ser encontrados facilmente. Usaremos o Kubernetes aqui porque é minha solução de cluster favorita.

O esquema de integração será o seguinte:

Esquema

O serviço de Order e o serviço de Inventory serão alguns aplicativos triviais do Spring Boot que retornam dados estáticos. Não estamos vinculados a uma pilha de tecnologia específica aqui. Esses serviços estão produzindo os dados que queremos processar.

Controlador de serviço de pedido:

 @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); } }

Produz dados no formato:

 [{"id":1,"items":[2,3,4]},{"id":2,"items":[5,3]}]

O controlador de serviço de Inventory é absolutamente semelhante ao serviço 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 é um repositório genérico que contém dados. Neste exemplo, ele retorna objetos estáticos predefinidos, que são empacotados para o formato a seguir.

 [{"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}]

Vamos escrever uma rota de gateway conectando-os, mas sem ServiceCall nesta etapa:

 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);

Agora imagine que cada serviço não é mais uma instância específica, mas uma nuvem de instâncias operando como uma. Usaremos o Minikube para testar o cluster Kubernetes localmente.

Configure as rotas de rede para ver os nós do Kubernetes localmente (o exemplo fornecido é para um ambiente 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

Envolva os serviços em contêineres do Docker com uma configuração do 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

Crie e envie as imagens de serviço para o registro do Docker. Agora execute os nós no cluster local do Kubernetes.

Configuração de implantação do 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

Exponha essas implantações como serviços no cluster:

 kubectl expose deployment order-srv --type=NodePort kubectl expose deployment inventory-srv --type=NodePort

Agora podemos verificar se as solicitações são atendidas por nós escolhidos aleatoriamente do cluster. Execute curl -X http://192.168.99.100:30517/info sequencialmente várias vezes para acessar o minikube NodePort para o serviço exposto (usando seu host e porta). Na saída, vemos que alcançamos o balanceamento de solicitações.

 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

Adicione camel-kubernetes e camel-netty4-http ao pom.xml do projeto. Em seguida, configure o componente ServiceCall para usar a descoberta de nó mestre do Kubernetes compartilhada para todas as chamadas de serviço entre as definições de rota:

 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);

O ServiceCall EIP complementa bem o Spring Boot. A maioria das opções pode ser configurada diretamente no arquivo application.properties .

Capacite a rota Camel com o 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);

Também ativamos o Circuit Breaker na rota. É um gancho de integração que permite pausar chamadas de sistema remoto em caso de erros de entrega ou indisponibilidade do destinatário. Isso foi projetado para evitar falhas do sistema em cascata. O componente Hystrix ajuda a conseguir isso implementando o padrão Circuit Breaker.

Vamos executá-lo e enviar uma solicitação de teste; obteremos a resposta agregada de ambos os serviços.

 [{"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}]}]

O resultado é o esperado.

Outros casos de uso

Mostrei como o Apache Camel pode integrar microsserviços em um cluster. Quais são os outros usos desse framework? Em geral, é útil em qualquer lugar onde o roteamento baseado em regras possa ser uma solução. 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.

Conclusão

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.