terça-feira, 29 de outubro de 2019

Orquestração versus coreografia: Um conto de dois microsserviços


#engenhariaDeSoftware #microsserviços #orquestração #coreografia #java #python #Zookeeper #RabbitMQ

Então, você tem ai um punhado de microsserviços e não sabe exatamente como vai colocá-los em “produção”, ok? É exatamente sobre isso que eu quero falar com você. Falar não! Mostrar!




Neste artigo, vou usar dois programas bem simples, um em Python e outro em Java, para simular dois microsserviços que devem se comunicar para executar uma tarefa. O que mostrarei aqui vale para dois ou mais microsserviços, independentemente da linguagem de programação na qual foram codificados.

Vou mostrar várias tecnologias úteis para implementação de microsserviços, como:
    • gRPC para comunicação;
    • Dropwizard para autosserviço de apps JAX-RS;
    • PJNIUS para invocar classes Java a partir de programas Python;
    • Apache Zookeeper e Apache Curator, para orquestração de microsserviços;
    • Coreografia de microsserviços com RabbitMQ.

O repositório deste exemplo é: https://github.com/cleuton/servicechoreography

A opção mais simples


Temos dois microsserviços e a forma mais simples de os entregarmos é subirmos dois processos (ou dois containers) que se comunicam diretamente. 

Um programa em Python que invoca um programa em Java, de modo a verificar a assinatura digital de um arquivo de texto. 


Na figura anterior, vemos um teste destes dois serviços: O código Python invoca através de HTTP o serviço Java (feito com Dropwizard).

O código completo desse exemplo simples está em:
  • Java: ./javaApp/signature;
  • Python: ./pythonApp/pythonclient.py;

Para as dependências Python, consulte o arquivo: ./pythonApp/requirements.txt

Para executar o teste basta compilar o código Java:

cd ./javaApp/signature
mvn clean package

E para subir o servidor Dropwizard:

java -jar signature-0.0.1-SNAPSHOT.jar server ../src/main/resources/signature.yml

Desta forma, podemos simular a chamada do microsserviço Python para o Java com este comando:

python pythonclient.py

Se tudo der certo, o resultado JSON será este:

{'status': 'success', 'data': {'mensagem': 'assinatura ok!'}}

Estude o código fonte dos dois serviços e veja que é bastante simples. Se quiser saber como criar um serviço Dropwizard em Java, leia o meu artigo: http://www.obomprogramador.com/2015/05/micro-servicos-imutaveis-receita-dos.html

Ok, os serviços funcionam. Podemos simplesmente empacotá-los como Contêineres Docker e enviá-los para o ambiente de produção.

Há alguma coisa errada com isso?
Não. Como eu gosto de dizer, em TI não existe “certo” ou “errado”. Existe “funcionando” ou “não funcionando”. Os serviços estão funcionando? Ótimo, então nada existe de errado.

Alguns podem alegar que não há “contingência”. Bom, dá para subirmos várias instâncias e balancearmos com DNS, ou com um Proxy reverso NGINX, não? Isso enterra a questão.

Veja bem, estamos usando HTTP, REST e JSON, coisas simples e práticas. Outras coisas poderiam ser pensadas, mas nada seria mais simples do que isto.

O relacionamento destes dois exemplos é um exemplo bem simples de orquestração. É mesmo. É um exemplo tosco, é claro, mas é orquestração. Temos um fluxo iniciado e guiado por um serviço principal, neste caso, o serviço Python, que inicia o fluxo, invoca cada serviço necessário, e entrega o resultado.

É claro que também existe orquestração externa, quando o condutor do fluxo está fora dele. Mas há um fluxo de qualquer modo. Orquestração ocorre quando:
  • Há um fluxo pré-determinado e ordenado de invocações de serviços;
  • Há um condutor, responsável por executar o fluxo;
  • O condutor conhece cada serviço a ser invocado e conhece a arquitetura do sistema e da rede.

Apesar de ser simples, este modelo de entrega necessita trabalho manual para mantê-lo. Por exemplo, caso um dos serviços fique fora do ar, um balanceamento simples pode fazer o cliente cair em um serviço que esteja indisponível, gerando erros.

Orquestração com Zookeeper


Precisamos endereçar algumas preocupações para aprimorar a arquitetura do nosso software, aumentando a sua robustez. Podemos implementar uma orquestração mais elaborada, que contemple:
  • Independência de endereçamento de chamadas;
  • Balanceamento de instâncias de microsserviços;
  • Monitoração de serviços.

É claro que podemos utilizar um ESB – Enterprise Service Bus para isto, como o TIBCO (https://www.tibco.com/) ou o Mule (https://www.mulesoft.com/platform/mule), o que aumentaria muito a complexidade (e o custo) do nosso software.

Mas podemos atender aos requisitos de maneira mais simples, utilizando um software como o Apache Zookeeper (https://zookeeper.apache.org/). Ele não é um ESB, mas um File System distribuído, que permite criarmos algumas “receitas” de serviços, entre elas, a de orquestração. E foi o que eu fiz.

Nesta versão, os serviços ainda invocam uns aos outros, só que o balanceamento é feito pelo Zookeeper e pelo Apache Curator, que é uma API para gerenciamento de serviços no Zookeeper. Entre as funcionalidades que o Curator oferece está a de Service Discovery que, além de balancear, permite monitorar e desativar instâncias problemáticas, substituindo-as.

Basicamente, o fluxo é este:

Temos um servidor Python (Flask) que invoca um servidor Java (gRPC Server). Aqui, eu usei o protocolo gRPC (https://grpc.io/) no Python e no Java, pois ele oferece as vantagens:

  • É um pouco mais rápido que o HTTP/REST;
  • Usa Protocol Buffers para serialização de mensagens binárias, o que permite um “payload” menor.

Para comunicação entre serviços, o gRPC, pelas suas vantagens, é mais apropriado.

Antes de invocar o serviço, o MS1 obtém do Zookeeper (via Apache Curator Service Discovery) o endereço de um dos servidores. Depois, estabelece conexão direta com ele via gRPC.

A pasta com esta versão está em:
  • Java: ../javaApp/grpcserverjava;
  • Python: ../pythonApp/grpcClientImpl.

Veja as dependências Python em um arquivo para criar um environment Anaconda em: ./pythonApp/grpcClientImpl/conda-env.yml.

Para executar esta aplicação:

  1. Compile a versão Java: “mvn clean package”. As classes gRPC serão geradas pelo plugin maven e colocadas no classpath;
  2. Compile as classes do protocolo para Python: “python -m grpc_tools.protoc -I../../protos --python_out=. --grpc_python_out=. ../../protos/signature.proto”. Isto só é necessário se você quiser modificar o arquivo de protocolo;
  3. Suba um contêiner Zookeeper: “docker run --name some-zookeeper -p 2181:2181 --restart always -d zookeeper”;
  4. Vá para a pasta “target” do projeto Java e suba 3 instâncias do Servidor gRPC:

java -jar grpcserverjava-0.0.1-SNAPSHOT-shaded.jar localhost 8090
java -jar grpcserverjava-0.0.1-SNAPSHOT-shaded.jar localhost 8100
java -jar grpcserverjava-0.0.1-SNAPSHOT-shaded.jar localhost 8110

  1. Execute o servidor Python: “python signature_grpc.py ”;

Agora, podemos simular o cliente com “curl”:

curl -X POST -H "Content-Type: application/json" -d '{"textFile": "/home/user/java/servicechoreography/servicechoreography/pythonApp/arquivo.txt","hexSignature":"8ed7b4235f21db78c92e69082df3874c03d4135515cb04ff1592e66d70999d56c504dd8f6dd275f870873639ea8803ddae40272465101935a19a1877c0f07715f0cb65beb839dbf33d691acc30bd3a1af6bcc42a1b86215c6cc230e7f2ff2bcff0452df651c89659a2a6f4c8364f86ab2fccac5d7ca4d15654839aa9723e9c70f15f0699037e0745947f5253545f66b7cd3b549f9e94066c319c4e5945dddf6bafebf165c984cf60c2b4fb4ae8aade21f0a88a637161c9cb6314cf4fd42ad4c4a50337b911126f188e77dc83aeaed97338a5ee53ddc0c3575041413ab11655129f15418838a2a531516276cda5df1f814f3c3ae8986c6663533a3f31aba73e19"}' http://localhost:5000/api/signature

Você precisa passar o path do arquivo “arquivo.txt” e o valor da assinatura digital. Tudo em uma linha só, ok? Aqui, não dá para mostrar assim.

Como podemos ver, cada request cai em um servidor gRPC diferente (Round Robin):

Service instance: localhost:8100
127.0.0.1 - - [29/Oct/2019 13:24:08] "POST /api/signature HTTP/1.1" 200 -
<Request 'http://localhost:5000/api/signature' [POST]>
<com.obomprogramador.grpc.ApacheCuratorDiscovery at 0x7f744bb11a10 jclass=com/obomprogramador/grpc/ApacheCuratorDiscovery jself=<LocalRef obj=0x557ff3632510 at 0x7f744bb014b0>>
Service instance: localhost:8110
127.0.0.1 - - [29/Oct/2019 13:24:37] "POST /api/signature HTTP/1.1" 200 -
<Request 'http://localhost:5000/api/signature' [POST]>
<com.obomprogramador.grpc.ApacheCuratorDiscovery at 0x7f744bb11a10 jclass=com/obomprogramador/grpc/ApacheCuratorDiscovery jself=<LocalRef obj=0x557ff3632510 at 0x7f744bb014b0>>
Service instance: localhost:8090

O Apache Curator Service Discovery permite criarmos um serviço de monitoração, que testaria as instâncias, desabilitando as que não responderem (Heart beat). Eu não implementei isto neste exemplo, mas é bem simples de fazer. Veja só o Apache Curator Service Discovery Server (https://curator.apache.org/curator-x-discovery-server/index.html), que já está pronto e faz isso.

Continua sendo um exemplo de orquestração, afinal, há um serviço que inicia o fluxo. Apenas fornecemos balanceamento e monitoração, ganhando um pouco mais de performance ao usarmos gRPC.

É uma solução mais robusta que a anterior. É exatamente o que o meu componente Open Source: “ServKeeper” (https://github.com/cleuton/servkeeper) faz.


Utilizando Java dentro do Python


Um detalhe interessante é que o não há suporte para o Apache Curator Service Discovery em Python. Então, criei uma pequena classe Java que usa o Service Discovery, e usei o PJINIUS para invocá-la de dentro do código Python:

import jnius_config
import os
jnius_config.set_classpath(os.getenv('PJNIUS_CLASSPATH'))
print(os.getenv('PJNIUS_CLASSPATH'))
from jnius import autoclass

Essa variável de ambiente PJNIUS_CLASSPATH aponta para o path do Jar desta classe Java, que usa o Service discovery.

Coreografia com RabbitMQ


Coreografia é uma maneira muito interessante de integrar microsserviços, diminuindo totalmente o acoplamento entre eles. Na verdade, um serviço não tem ideia de quais outros serviços estão colaborando para atender a um fluxo. Na verdade, o fluxo nem existe mais. É tudo feito indireta e assincronamente.

Para isto, precisamos de um software gerenciador de fila de eventos, como o RabbitMQ, ou o Apache ActiveMQ.

Eu escolhi o RabbitMQ por ser muito mais simples e prático. O fluxo agora é assim:


Agora, o Serviço Python (MS1) desconhece totalmente o fluxo que está iniciando. Ele apenas posta uma mensagem em uma fila e consulta a resposta em outra. Há várias instâncias de serviço Java escutando a primeira fila. Uma delas recebe a mensagem, processa e posta uma resposta em outra fila.

O processamento é assíncrono e o Servidor Python tem que prever isso, fornecendo um recurso com método GET para consulta à fila.

O caminho para esta versão está em:
  • Java: ./javaApp/choreography;
  • Python: ./pythonApp/choreography;

Para executar esta solução:

  1. Compile o projeto Java: “mvn clean package”;
  2. Crie um ambiente virtual Python (Anaconda) e use as dependências de: ./pythonApp/requirements.txt;
  3. Suba uma instância de RabbitMQ com o comando: “docker run -d --hostname my-rabbit --name some-rabbit -p 5672:5672 rabbitmq:latest”;
  4. Suba o servidor Python (na pasta ./pythonApp/choreography) com o comando: “python queueVerifier.py”;
  5. Execute o listener Java: “java -jar choreography-0.0.1-SNAPSHOT-shaded.jar”
  6. Envie comandos para o Servidor com “curl”:

curl -X POST -H "Content-Type: application/json" \
-d '{"textFile": "../arquivo.txt", "hexSignature": "8ed7b4235f21db78c92e69082df3874c03d4135515cb04ff1592e66d70999d56c504dd8f6dd275f870873639ea8803ddae40272465101935a19a1877c0f07715f0cb65beb839dbf33d691acc30bd3a1af6bcc42a1b86215c6cc230e7f2ff2bcff0452df651c89659a2a6f4c8364f86ab2fccac5d7ca4d15654839aa9723e9c70f15f0699037e0745947f5253545f66b7cd3b549f9e94066c319c4e5945dddf6bafebf165c984cf60c2b4fb4ae8aade21f0a88a637161c9cb6314cf4fd42ad4c4a50337b911126f188e77dc83aeaed97338a5ee53ddc0c3575041413ab11655129f15418838a2a531516276cda5df1f814f3c3ae8986c6663533a3f31aba73e19"}' \

Lembrando que o parâmetro “textFile” é o path do arquivo texto (“./pythonApp/arquivo.txt”) e o “hexSignature” é a assinatura digital.

O serviço vai responder assim:

{"msg": "Verification request sent"}

Para saber o resultado, fazemos outro request usando GET:

curl -X GET -H "Content-Type: application/json" \

E o resultado deve ser assim:

{"verified": true, "response": true}

Qual é a vantagem? Para começar, podemos ter as mesmas vantagens do Zookeeper, como: balanceamento e monitoração, porém, com menor acoplamento entre os serviços, já que o Serviço Python apenas posta mensagem em uma fila e lê de outra, sem saber com quem está “conversando”. O componente Java deixou de ser um “serviço” para ser apenas um listener, que recebe mensagens e posta mensagens.

Podemos “plugar” serviços diversos na fila, sem afetar os que já existem, aumentando a flexibilidade do sistema.


Cleuton Sampaio, M.Sc.



Nenhum comentário:

Postar um comentário