Introdução aos microsserviços Python com Nameko
Publicados: 2022-03-11Introdução
O padrão de arquitetura de microsserviços é um estilo de arquitetura que está crescendo em popularidade, devido à sua flexibilidade e resiliência. Juntamente com tecnologias como Kubernetes, está ficando mais fácil inicializar um aplicativo usando uma arquitetura de microsserviços como nunca antes.
De acordo com um artigo clássico do blog de Martin Fowler, o estilo de arquitetura de Microservices pode ser resumido como:
Em suma, o estilo de arquitetura de microsserviço é uma abordagem para desenvolver um único aplicativo como um conjunto de pequenos serviços, cada um executando seu próprio processo e se comunicando com mecanismos leves, geralmente uma API de recursos HTTP. Esses serviços são construídos em torno dos recursos de negócios e implantados de forma independente por máquinas de implantação totalmente automatizadas.
Em outras palavras, uma aplicação seguindo uma arquitetura de microsserviços é composta por diversos serviços independentes e dinâmicos que se comunicam entre si por meio de um protocolo de comunicação. É comum usar HTTP (e REST), mas como veremos, podemos usar outros tipos de protocolos de comunicação como RPC (Remote Procedure Call) sobre AMQP (Advanced Message Queuing Protocol).
O padrão de microsserviços pode ser pensado como um caso específico de SOA (arquitetura orientada a serviços). Em SOA é comum, entretanto, utilizar um ESB (enterprise service bus) para gerenciar a comunicação entre os serviços. Os ESBs geralmente são altamente sofisticados e incluem funcionalidades para roteamento de mensagens complexas e aplicação de regras de negócios. Em microsserviços, é mais comum empregar uma abordagem alternativa: “smart endpoints and dumb pipes”, o que significa que os próprios serviços devem conter toda a lógica de negócios e complexidade (alta coesão), mas a conexão entre os serviços deve ser tão simples quanto possível (alta dissociação), o que significa que um serviço não precisa necessariamente saber quais outros serviços se comunicarão com ele. Esta é uma separação de preocupações aplicada no nível arquitetural.
Outro aspecto dos microsserviços é que não há fiscalização sobre quais tecnologias devem ser usadas em cada serviço. Você deve ser capaz de escrever um serviço com qualquer pilha de software que possa se comunicar com os outros serviços. Cada serviço também tem seu próprio gerenciamento de ciclo de vida. Tudo isso significa que em uma empresa é possível ter equipes trabalhando em serviços separados, com tecnologias diferentes e até metodologias de gestão. Cada equipe estará preocupada com as capacidades de negócios, ajudando a construir uma organização mais ágil.
Microsserviços Python
Tendo esses conceitos em mente, neste artigo vamos nos concentrar na construção de um aplicativo de microsserviços de prova de conceito usando Python. Para isso, usaremos Nameko, um framework de microsserviços Python. Possui RPC sobre AMQP integrado, permitindo que você se comunique facilmente entre seus serviços. Ele também possui uma interface simples para consultas HTTP, que usaremos neste tutorial. No entanto, para escrever microsserviços que expõem um endpoint HTTP, é recomendável usar outra estrutura, como Flask. Para chamar métodos Nameko sobre RPC usando Flask, você pode usar flask_nameko, um wrapper construído apenas para interoperar Flask com Nameko.
Configurando o ambiente básico
Vamos começar executando o exemplo mais simples possível, extraído do site Nameko, e expandi-lo para nossos propósitos. Primeiro, você precisará do Docker instalado. Usaremos o Python 3 em nossos exemplos, portanto, certifique-se de instalá-lo também. Em seguida, crie um virtualenv python e execute $ pip install nameko
.
Para executar o Nameko, precisamos do corretor de mensagens RabbitMQ. Será responsável pela comunicação entre nossos serviços Nameko. Mas não se preocupe, pois você não precisa instalar mais uma dependência em sua máquina. Com o Docker, podemos simplesmente baixar uma imagem pré-configurada, executá-la e, quando terminarmos, simplesmente interromper o contêiner. Sem daemons, apt-get
ou dnf install
.
Inicie um contêiner RabbitMQ executando $ docker run -p 5672:5672 --hostname nameko-rabbitmq rabbitmq:3
(você pode precisar de sudo para fazer isso). Isso iniciará um contêiner do Docker usando a versão 3 mais recente do RabbitMQ e o exporá na porta padrão 5672.
Olá Mundo com Microsserviços
Vá em frente e crie um arquivo chamado hello.py
com o seguinte conteúdo:
from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): return "Hello, {}!".format(name)
Os serviços Nameko são classes. Essas classes expõem pontos de entrada, que são implementados como extensões. As extensões integradas incluem a capacidade de criar pontos de entrada que representam métodos RPC, ouvintes de eventos, terminais HTTP ou temporizadores. Existem também extensões da comunidade que podem ser usadas para interagir com o banco de dados PostgreSQL, Redis, etc… É possível escrever suas próprias extensões.
Vamos seguir em frente e executar nosso exemplo. Se você tem o RabbitMQ rodando na porta padrão, simplesmente execute $ nameko run hello
. Ele encontrará o RabbitMQ e se conectará a ele automaticamente. Então, para testar nosso serviço, execute $ nameko shell
em outro terminal. Isso criará um shell interativo que se conectará a essa mesma instância do RabbitMQ. O melhor é que, usando RPC sobre AMQP, a Nameko implementa a descoberta automática de serviços. Ao chamar um método RPC, o nameko tentará encontrar o serviço em execução correspondente.
Ao executar o shell Nameko, você obterá um objeto especial chamado n
adicionado ao namespace. Este objeto permite despachar eventos e fazer chamadas RPC. Para fazer uma chamada RPC para nosso serviço, execute:
> >> n.rpc.greetingservice.hello(name='world') 'Hello, world!'
Chamadas simultâneas
Essas classes de serviço são instanciadas no momento em que uma chamada é feita e destruídas após a conclusão da chamada. Portanto, eles devem ser inerentemente sem estado, o que significa que você não deve tentar manter nenhum estado no objeto ou classe entre as chamadas. Isso implica que os próprios serviços devem ser apátridas. Com a suposição de que todos os serviços são sem estado, Nameko é capaz de alavancar a simultaneidade usando greenthreads eventlet. Os serviços instanciados são chamados de “trabalhadores” e pode haver um número máximo configurado de trabalhadores em execução ao mesmo tempo.
Para verificar a simultaneidade do Nameko na prática, modifique o código-fonte adicionando um sono à chamada de procedimento antes de retornar a resposta:
from time import sleep from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): sleep(5) return "Hello, {}!".format(name)
Estamos usando a sleep
do módulo de time
, que não é habilitado para assíncrona. No entanto, ao executar nossos serviços usando nameko run
, ele corrigirá automaticamente os rendimentos do acionador de bloqueio de chamadas como sleep(5)
.
Espera-se agora que o tempo de resposta de uma chamada de procedimento demore cerca de 5 segundos. No entanto, qual será o comportamento do trecho a seguir, quando o executarmos a partir do shell nameko?
res = [] for i in range(5): hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) res.append(hello_res) for hello_res in res: print(hello_res.result())
Nameko fornece um método call_async
sem bloqueio para cada ponto de entrada RPC, retornando um objeto de resposta de proxy que pode ser consultado para obter seu resultado. O método de result
, quando chamado no proxy de resposta, será bloqueado até que a resposta seja retornada.
Como esperado, este exemplo é executado em apenas cerca de cinco segundos. Cada trabalhador será bloqueado aguardando a conclusão da chamada de sleep
, mas isso não impede que outro trabalhador seja iniciado. Substitua essa chamada de sleep
por uma chamada de banco de dados de E/S de bloqueio útil, por exemplo, e você terá um serviço simultâneo extremamente rápido.
Conforme explicado anteriormente, Nameko cria workers quando um método é chamado. O número máximo de trabalhadores é configurável. Por padrão, esse número é definido como 10. Você pode testar alterando o range(5)
no trecho acima para, por exemplo, intervalo(20). Isso chamará o método hello
20 vezes, que agora deve levar dez segundos para ser executado:
> >> res = [] > >> for i in range(20): ... hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) ... res.append(hello_res) > >> for hellores in res: ... print(hello_res.result()) Hello, 0! Hello, 1! Hello, 2! Hello, 3! Hello, 4! Hello, 5! Hello, 6! Hello, 7! Hello, 8! Hello, 9! Hello, 10! Hello, 11! Hello, 12! Hello, 13! Hello, 14! Hello, 15! Hello, 16! Hello, 17! Hello, 18! Hello, 19!
Agora, suponha que você estava recebendo muitos (mais de 10) usuários simultâneos chamando esse método hello
. Alguns usuários irão travar esperando mais do que os cinco segundos esperados pela resposta. Uma solução foi aumentar o número de trabalhos substituindo as configurações padrão usando, por exemplo, um arquivo de configuração. No entanto, se o seu servidor já estiver no limite com esses dez trabalhadores porque o método chamado depende de algumas consultas de banco de dados pesadas, aumentar o número de trabalhadores pode fazer com que o tempo de resposta aumente ainda mais.
Escalando nosso serviço
Uma solução melhor é usar os recursos do Nameko Microservices. Até agora, usamos apenas um servidor (seu computador), executando uma instância do RabbitMQ e uma instância do serviço. Em um ambiente de produção, você desejará aumentar arbitrariamente o número de nós executando o serviço que está recebendo muitas chamadas. Você também pode construir um cluster RabbitMQ se quiser que seu agente de mensagens seja mais confiável.
Para simular o dimensionamento de um serviço, podemos simplesmente abrir outro terminal e executar o serviço como antes, usando $ nameko run hello
. Isso iniciará outra instância de serviço com potencial para executar mais dez trabalhadores. Agora, tente executar esse trecho novamente com range(20)
. Agora deve levar cinco segundos novamente para ser executado. Quando houver mais de uma instância de serviço em execução, a Nameko fará o rodízio das solicitações RPC entre as instâncias disponíveis.
Nameko é construído para lidar de forma robusta com essas chamadas de métodos em um cluster. Para testar isso, tente executar o snipped e antes de terminar, vá para um dos terminais que executam o serviço Nameko e pressione Ctrl+C
duas vezes. Isso desligaria o host sem esperar que os trabalhadores terminassem. Nameko irá realocar as chamadas para outra instância de serviço disponível.

Na prática, você usaria o Docker para conteinerizar seus serviços, como faremos posteriormente, e uma ferramenta de orquestração, como o Kubernetes, para gerenciar seus nós executando o serviço e outras dependências, como o agente de mensagens. Se feito corretamente, com o Kubernetes, você transformaria efetivamente seu aplicativo em um sistema distribuído robusto, imune a picos inesperados. Além disso, o Kubernetes permite implantações sem tempo de inatividade. Portanto, implantar uma nova versão de um serviço não afetará a disponibilidade do seu sistema.
É importante criar serviços com alguma compatibilidade com versões anteriores em mente, pois em um ambiente de produção pode acontecer que várias versões diferentes do mesmo serviço sejam executadas ao mesmo tempo, especialmente durante a implantação. Se você usar o Kubernetes, durante a implantação, ele apenas matará todos os contêineres da versão antiga quando houver novos contêineres em execução suficientes.
Para Nameko, ter várias versões diferentes do mesmo serviço rodando ao mesmo tempo não é um problema. Como ele distribui as chamadas de forma round-robin, as chamadas podem passar por versões antigas ou novas. Para testar isso, mantenha um terminal com nosso serviço executando a versão antiga e edite o módulo de serviço para ficar assim:
from time import sleep from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): sleep(5) return "Hello, {}! (version 2)".format(name)
Se você executar esse serviço de outro terminal, as duas versões serão executadas ao mesmo tempo. Agora, execute nosso snippet de teste novamente e você verá as duas versões sendo mostradas:
> >> res = [] > >> for i in range(5): ... hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) ... res.append(hello_res) > >> for hellores in res: ... print(hello_res.result()) Hello, 0! Hello, 1! (version 2) Hello, 2! Hello, 3! (version 2) Hello, 4!
Trabalhando com várias instâncias
Agora sabemos como trabalhar efetivamente com Nameko e como funciona o dimensionamento. Vamos agora dar um passo adiante e usar mais ferramentas do ecossistema Docker: docker-compose. Isso funcionará se você estiver implantando em um único servidor, o que definitivamente não é ideal, pois você não aproveitará muitas das vantagens de uma arquitetura de microsserviços. Novamente, se você deseja ter uma infraestrutura mais adequada, pode usar uma ferramenta de orquestração como o Kubernetes para gerenciar um sistema distribuído de contêineres. Então, vá em frente e instale o docker-compose.
Novamente, tudo o que precisamos fazer é implantar uma instância do RabbitMQ e a Nameko cuidará do resto, já que todos os serviços podem acessar essa instância do RabbitMQ. O código-fonte completo para este exemplo está disponível neste repositório do GitHub.
Vamos construir um aplicativo de viagem simples para testar os recursos do Nameko. Esse aplicativo permite cadastrar aeroportos e viagens. Cada aeroporto é simplesmente armazenado como o nome do aeroporto, e a viagem armazena os ids dos aeroportos de origem e destino. A arquitetura do nosso sistema se parece com o seguinte:
Idealmente, cada microsserviço teria sua própria instância de banco de dados. No entanto, para simplificar, criei um único banco de dados Redis para os microsserviços Trips e Airports compartilharem. O microsserviço Gateway receberá solicitações HTTP por meio de uma API simples do tipo REST e usará RPC para se comunicar com Aeroportos e Viagens.
Vamos começar com o microsserviço Gateway. Sua estrutura é simples e deve ser muito familiar para quem vem de um framework como o Flask. Basicamente definimos dois endpoints, cada um permitindo os métodos GET e POST:
import json from nameko.rpc import RpcProxy from nameko.web.handlers import http class GatewayService: name = 'gateway' airports_rpc = RpcProxy('airports_service') trips_rpc = RpcProxy('trips_service') @http('GET', '/airport/<string:airport_id>') def get_airport(self, request, airport_id): airport = self.airports_rpc.get(airport_id) return json.dumps({'airport': airport}) @http('POST', '/airport') def post_airport(self, request): data = json.loads(request.get_data(as_text=True)) airport_id = self.airports_rpc.create(data['airport']) return airport_id @http('GET', '/trip/<string:trip_id>') def get_trip(self, request, trip_id): trip = self.trips_rpc.get(trip_id) return json.dumps({'trip': trip}) @http('POST', '/trip') def post_trip(self, request): data = json.loads(request.get_data(as_text=True)) trip_id = self.trips_rpc.create(data['airport_from'], data['airport_to']) return trip_id
Vamos dar uma olhada no serviço de Aeroportos agora. Como esperado, ele expõe dois métodos RPC. O método get
simplesmente consultará o banco de dados Redis e retornará o aeroporto para o id fornecido. O método create
gerará um id aleatório, armazenará as informações do aeroporto e retornará o id:
import uuid from nameko.rpc import rpc from nameko_redis import Redis class AirportsService: name = "airports_service" redis = Redis('development') @rpc def get(self, airport_id): airport = self.redis.get(airport_id) return airport @rpc def create(self, airport): airport_id = uuid.uuid4().hex self.redis.set(airport_id, airport) return airport_id
Observe como estamos usando a extensão nameko_redis
. Dê uma olhada na lista de extensões da comunidade. As extensões são implementadas de uma maneira que emprega injeção de dependência. Nameko se encarrega de iniciar o objeto de extensão real que cada trabalhador usará.
Não há muita diferença entre os microsserviços Airports e Trips. Veja como ficaria o microsserviço Trips:
import uuid from nameko.rpc import rpc from nameko_redis import Redis class AirportsService: name = "trips_service" redis = Redis('development') @rpc def get(self, trip_id): trip = self.redis.get(trip_id) return trip @rpc def create(self, airport_from_id, airport_to_id): trip_id = uuid.uuid4().hex self.redis.set(trip_id, { "from": airport_from_id, "to": airport_to_id }) return trip_id
O Dockerfile
para cada microsserviço também é muito simples. A única dependência é nameko
, e no caso dos serviços Airports and Trips, há a necessidade de instalar nameko-redis
também. Essas dependências são fornecidas no requirements.txt
de cada serviço. O Dockerfile para o serviço Aeroportos se parece com:
FROM python:3 RUN apt-get update && apt-get -y install netcat && apt-get clean WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY config.yml ./ COPY run.sh ./ COPY airports.py ./ RUN chmod +x ./run.sh CMD ["./run.sh"]
A única diferença entre isso e o Dockerfile para os outros serviços é o arquivo de origem (neste caso, airports.py
), que deve ser alterado de acordo.
O script run.sh
se encarrega de esperar até o RabbitMQ e, no caso dos serviços de Aeroportos e Viagens, o banco de dados Redis está pronto. O snippet a seguir mostra o conteúdo de run.sh
para aeroportos. Novamente, para os outros serviços, basta mudar de aiports
para gateway
ou trips
de acordo:
#!/bin/bash until nc -z ${RABBIT_HOST} ${RABBIT_PORT}; do echo "$(date) - waiting for rabbitmq..." sleep 1 done until nc -z ${REDIS_HOST} ${REDIS_PORT}; do echo "$(date) - waiting for redis..." sleep 1 done nameko run --config config.yml airports
Nossos serviços já estão prontos para serem executados:
$ docker-compose up
Vamos testar nosso sistema. Execute o comando:
$ curl -i -d "{\"airport\": \"first_airport\"}" localhost:8000/airport HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:05:53 GMT f2bddf0e506145f6ba0c28c247c54629
Essa última linha é o id gerado para o nosso aeroporto. Para testar se está funcionando, execute:
$curl localhost:8000/airport/f2bddf0e506145f6ba0c28c247c54629 {"airport": "first_airport"} Great, now let's add another airport: $ curl -i -d "{\"airport\": \"second_airport\"}" localhost:8000/airport HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:06:00 GMT 565000adcc774cfda8ca3a806baec6b5
Agora temos dois aeroportos, o suficiente para formar uma viagem. Vamos criar uma viagem agora:
$ curl -i -d "{\"airport_from\": \"f2bddf0e506145f6ba0c28c247c54629\", \"airport_to\": \"565000adcc774cfda8ca3a806baec6b5\"}" localhost:8000/trip HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:09:10 GMT 34ca60df07bc42e88501178c0b6b95e4
Como antes, essa última linha representa o ID da viagem. Vamos verificar se foi inserido corretamente:
$ curl localhost:8000/trip/34ca60df07bc42e88501178c0b6b95e4 {"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000adcc774cfda8ca3a806baec6b5'}"}
Resumo
Vimos como o Nameko funciona criando uma instância de execução local do RabbitMQ, conectando-se a ela e realizando vários testes. Em seguida, aplicamos o conhecimento adquirido para criar um sistema simples usando uma arquitetura de Microsserviços.
Apesar de ser extremamente simples, nosso sistema está muito próximo do que seria uma implantação pronta para produção. De preferência, você usaria outra estrutura para lidar com solicitações HTTP, como Falcon ou Flask. Ambos são ótimas opções e podem ser facilmente usados para criar outros microsserviços baseados em HTTP, caso você queira quebrar seu serviço de Gateway, por exemplo. O Flask tem a vantagem de já ter um plugin para interagir com o Nameko, mas você pode usar o nameko-proxy diretamente de qualquer framework.
Nameko também é muito fácil de testar. Nós não cobrimos testes aqui por simplicidade, mas confira a documentação de testes de Nameko.
Com todas as partes móveis dentro de uma arquitetura de microsserviços, você deseja garantir um sistema de registro robusto. Para construir um, veja Python Logging: An In-Depth Tutorial by Toptaler and Python Developer: Son Nguyen Kim.