Introducción a los microservicios de Python con Nameko
Publicado: 2022-03-11Introducción
El patrón arquitectónico de microservicios es un estilo arquitectónico que está ganando popularidad debido a su flexibilidad y resistencia. Junto con tecnologías como Kubernetes, cada vez es más fácil arrancar una aplicación utilizando una arquitectura de microservicios como nunca antes.
Según un artículo clásico del blog de Martin Fowler, el estilo arquitectónico de Microservicios se puede resumir en:
En resumen, el estilo arquitectónico de microservicio es un enfoque para desarrollar una sola aplicación como un conjunto de pequeños servicios, cada uno ejecutándose en su propio proceso y comunicándose con mecanismos livianos, a menudo una API de recursos HTTP. Estos servicios se basan en capacidades comerciales y se implementan de forma independiente mediante maquinaria de implementación totalmente automatizada.
En otras palabras, una aplicación que sigue una arquitectura de microservicios se compone de varios servicios independientes y dinámicos que se comunican entre sí mediante un protocolo de comunicación. Es común usar HTTP (y REST), pero como veremos, podemos usar otro tipo de protocolos de comunicación como RPC (Remote Procedure Call) sobre AMQP (Advanced Message Queuing Protocol).
El patrón de microservicios puede pensarse como un caso específico de SOA (arquitectura orientada a servicios). Sin embargo, en SOA es común utilizar un ESB (bus de servicios empresariales) para gestionar la comunicación entre servicios. Los ESB suelen ser muy sofisticados e incluyen funcionalidades para el enrutamiento de mensajes complejos y la aplicación de reglas comerciales. En los microservicios, es más común emplear un enfoque alternativo: "puntos finales inteligentes y canalizaciones tontas", lo que significa que los servicios en sí mismos deben contener toda la lógica comercial y la complejidad (alta cohesión), pero la conexión entre los servicios debe ser tan simple como posible (desacoplamiento alto), lo que significa que un servicio no necesariamente necesita saber qué otros servicios se comunicarán con él. Esta es una separación de preocupaciones aplicada a nivel arquitectónico.
Otro aspecto de los microservicios es que no se impone qué tecnologías se deben usar dentro de cada servicio. Debería poder escribir un servicio con cualquier pila de software que pueda comunicarse con los otros servicios. Cada servicio también tiene su propia gestión del ciclo de vida. Todo eso hace que en una empresa sea posible tener equipos trabajando en servicios separados, con diferentes tecnologías e incluso metodologías de gestión. Cada equipo se preocupará por las capacidades comerciales, ayudando a construir una organización más ágil.
Microservicios de Python
Teniendo estos conceptos en mente, en este artículo nos enfocaremos en construir una aplicación de Microservicios de prueba de concepto usando Python. Para eso, usaremos Nameko, un marco de microservicios de Python. Tiene RPC sobre AMQP incorporado, lo que le permite comunicarse fácilmente entre sus servicios. También tiene una interfaz simple para consultas HTTP, que usaremos en este tutorial. Sin embargo, para escribir microservicios que expongan un extremo HTTP, se recomienda usar otro marco, como Flask. Para llamar a los métodos de Nameko a través de RPC usando Flask, puede usar Flask_nameko, un envoltorio creado solo para la interoperabilidad de Flask con Nameko.
Configuración del entorno básico
Comencemos ejecutando el ejemplo más simple posible, extraído del sitio web de Nameko, y ampliándolo para nuestros propósitos. Primero, necesitará Docker instalado. Usaremos Python 3 en nuestros ejemplos, así que asegúrese de tenerlo instalado también. Luego, cree un virtualenv de python y ejecute $ pip install nameko
.
Para ejecutar Nameko, necesitamos el intermediario de mensajes RabbitMQ. Será responsable de la comunicación entre nuestros servicios de Nameko. Sin embargo, no se preocupe, ya que no necesita instalar una dependencia más en su máquina. Con Docker, podemos simplemente descargar una imagen preconfigurada, ejecutarla y, cuando hayamos terminado, simplemente detener el contenedor. Sin demonios, apt-get
o dnf install
.
Inicie un contenedor RabbitMQ ejecutando $ docker run -p 5672:5672 --hostname nameko-rabbitmq rabbitmq:3
(es posible que necesite sudo para hacerlo). Esto iniciará un contenedor Docker utilizando la versión 3 RabbitMQ más reciente y lo expondrá sobre el puerto predeterminado 5672.
Hola mundo con microservicios
Continúe y cree un archivo llamado hello.py
con el siguiente contenido:
from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): return "Hello, {}!".format(name)
Los servicios de Nameko son clases. Estas clases exponen puntos de entrada, que se implementan como extensiones. Las extensiones integradas incluyen la capacidad de crear puntos de entrada que representan métodos RPC, escuchas de eventos, puntos finales HTTP o temporizadores. También hay extensiones comunitarias que se pueden usar para interactuar con la base de datos PostgreSQL, Redis, etc. Es posible escribir sus propias extensiones.
Avancemos y ejecutemos nuestro ejemplo. Si tiene RabbitMQ ejecutándose en el puerto predeterminado, simplemente ejecute $ nameko run hello
. Encontrará RabbitMQ y se conectará automáticamente. Luego, para probar nuestro servicio, ejecuta $ nameko shell
en otra terminal. Esto creará un shell interactivo que se conectará a esa misma instancia de RabbitMQ. Lo bueno es que, al usar RPC sobre AMQP, Nameko implementa el descubrimiento automático de servicios. Al llamar a un método RPC, nameko intentará encontrar el servicio en ejecución correspondiente.
Al ejecutar el shell de Nameko, obtendrá un objeto especial llamado n
agregado al espacio de nombres. Este objeto permite despachar eventos y hacer llamadas RPC. Para hacer una llamada RPC a nuestro servicio, ejecute:
> >> n.rpc.greetingservice.hello(name='world') 'Hello, world!'
Llamadas concurrentes
Estas clases de servicio se instancian en el momento en que se realiza una llamada y se destruyen una vez que se completa la llamada. Por lo tanto, deben ser intrínsecamente sin estado, lo que significa que no debe intentar mantener ningún estado en el objeto o la clase entre llamadas. Esto implica que los propios servicios deben ser apátridas. Con el supuesto de que todos los servicios no tienen estado, Nameko puede aprovechar la simultaneidad mediante el uso de eventlet greenthreads. Los servicios instanciados se denominan "trabajadores" y puede haber un número máximo configurado de trabajadores ejecutándose al mismo tiempo.
Para verificar la concurrencia de Nameko en la práctica, modifique el código fuente agregando una suspensión a la llamada al procedimiento antes de devolver la respuesta:
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 utilizando sleep
del módulo de time
, que no está habilitado para sincronización. Sin embargo, al ejecutar nuestros servicios usando nameko run
, parcheará automáticamente los rendimientos de activación de llamadas bloqueadas como sleep(5)
.
Ahora se espera que el tiempo de respuesta de una llamada de procedimiento sea de alrededor de 5 segundos. Sin embargo, ¿cuál será el comportamiento del siguiente fragmento cuando lo ejecutemos desde el 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 proporciona un método call_async
sin bloqueo para cada punto de entrada de RPC, que devuelve un objeto de respuesta de proxy que luego se puede consultar para obtener su resultado. El método de result
, cuando se invoca en el proxy de respuesta, se bloqueará hasta que se devuelva la respuesta.
Como era de esperar, este ejemplo se ejecuta en unos cinco segundos. Cada trabajador se bloqueará esperando que termine la llamada de sleep
, pero esto no impide que otro trabajador comience. Reemplace esta llamada de sleep
con una útil llamada de base de datos de E/S de bloqueo, por ejemplo, y obtendrá un servicio concurrente extremadamente rápido.
Como se explicó anteriormente, Nameko crea trabajadores cuando se llama a un método. El número máximo de trabajadores es configurable. De manera predeterminada, ese número se establece en 10. Puede probar cambiar el range(5)
en el fragmento anterior a, por ejemplo, rango (20). Esto llamará al método hello
20 veces, que ahora debería tardar diez segundos en ejecutarse:
> >> 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!
Ahora, suponga que recibe demasiados (más de 10) usuarios simultáneos llamando a ese método de hello
. Algunos usuarios se colgarán esperando más de los cinco segundos esperados para la respuesta. Una solución fue aumentar la cantidad de trabajos anulando la configuración predeterminada usando, por ejemplo, un archivo de configuración. Sin embargo, si su servidor ya está en su límite con esos diez trabajadores porque el método llamado depende de algunas consultas de base de datos pesadas, aumentar la cantidad de trabajadores podría hacer que el tiempo de respuesta aumente aún más.
Escalando Nuestro Servicio
Una mejor solución es usar las capacidades de Nameko Microservices. Hasta ahora, solo hemos usado un servidor (su computadora), ejecutando una instancia de RabbitMQ y una instancia del servicio. En un entorno de producción, querrá aumentar arbitrariamente la cantidad de nodos que ejecutan el servicio que recibe demasiadas llamadas. También puede crear un clúster RabbitMQ si desea que su intermediario de mensajes sea más confiable.
Para simular el escalado de un servicio, simplemente podemos abrir otra terminal y ejecutar el servicio como antes, usando $ nameko run hello
. Esto iniciará otra instancia de servicio con el potencial de ejecutar diez trabajadores más. Ahora, intente ejecutar ese fragmento nuevamente con range(20)
. Ahora debería tomar cinco segundos nuevamente para ejecutarse. Cuando hay más de una instancia de servicio en ejecución, Nameko hará un round-robin de las solicitudes RPC entre las instancias disponibles.
Nameko está diseñado para manejar de manera robusta esas llamadas a métodos en un clúster. Para probar eso, intente ejecutar el recorte y, antes de que finalice, vaya a uno de los terminales que ejecutan el servicio Nameko y presione Ctrl+C
dos veces. Esto apagaría el host sin esperar a que los trabajadores terminen. Nameko reasignará las llamadas a otra instancia de servicio disponible.

En la práctica, usaría Docker para contener sus servicios, como lo haremos más adelante, y una herramienta de orquestación como Kubernetes para administrar sus nodos que ejecutan el servicio y otras dependencias, como el intermediario de mensajes. Si se hace correctamente, con Kubernetes, transformará efectivamente su aplicación en un sistema distribuido robusto, inmune a picos inesperados. Además, Kubernetes permite implementaciones sin tiempo de inactividad. Por lo tanto, implementar una nueva versión de un servicio no afectará la disponibilidad de su sistema.
Es importante crear servicios teniendo en cuenta cierta compatibilidad con versiones anteriores, ya que en un entorno de producción puede suceder que varias versiones diferentes del mismo servicio se ejecuten al mismo tiempo, especialmente durante la implementación. Si usa Kubernetes, durante la implementación solo eliminará todos los contenedores de la versión anterior cuando haya suficientes contenedores nuevos en ejecución.
Para Nameko, tener varias versiones diferentes del mismo servicio ejecutándose al mismo tiempo no es un problema. Dado que distribuye las llamadas de forma rotatoria, las llamadas pueden pasar por versiones antiguas o nuevas. Para probar eso, mantenga una terminal con nuestro servicio ejecutando la versión anterior y edite el módulo de servicio para que se vea así:
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)
Si ejecuta ese servicio desde otro terminal, obtendrá las dos versiones ejecutándose al mismo tiempo. Ahora, ejecute nuestro fragmento de prueba nuevamente y verá que se muestran ambas versiones:
> >> 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!
Trabajar con varias instancias
Ahora sabemos cómo trabajar eficazmente con Nameko y cómo funciona el escalado. Ahora demos un paso más y usemos más herramientas del ecosistema de Docker: docker-compose. Esto funcionará si está implementando en un solo servidor, lo que definitivamente no es ideal ya que no aprovechará muchas de las ventajas de una arquitectura de microservicios. Nuevamente, si desea tener una infraestructura más adecuada, puede usar una herramienta de orquestación como Kubernetes para administrar un sistema distribuido de contenedores. Entonces, adelante e instale docker-compose.
Nuevamente, todo lo que tenemos que hacer es implementar una instancia de RabbitMQ y Nameko se encargará del resto, dado que todos los servicios pueden acceder a esa instancia de RabbitMQ. El código fuente completo de este ejemplo está disponible en este repositorio de GitHub.
Construyamos una aplicación de viaje simple para probar las capacidades de Nameko. Esa aplicación permite registrar aeropuertos y viajes. Cada aeropuerto se almacena simplemente como el nombre del aeropuerto, y el viaje almacena los identificadores de los aeropuertos de origen y destino. La arquitectura de nuestro sistema es similar a la siguiente:
Idealmente, cada microservicio tendría su propia instancia de base de datos. Sin embargo, para simplificar, he creado una única base de datos de Redis para que la compartan los microservicios de viajes y aeropuertos. El microservicio Gateway recibirá solicitudes HTTP a través de una API similar a REST simple y utilizará RPC para comunicarse con Airports and Trips.
Comencemos con el microservicio Gateway. Su estructura es sencilla y debería ser muy familiar para cualquiera que venga de un marco como Flask. Básicamente, definimos dos puntos finales, cada uno de los cuales permite los métodos GET y 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
Echemos un vistazo al servicio de aeropuertos ahora. Como era de esperar, expone dos métodos RPC. El método get
simplemente consultará la base de datos de Redis y devolverá el aeropuerto para la identificación dada. El método de create
generará una identificación aleatoria, almacenará la información del aeropuerto y devolverá la identificación:
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 cómo estamos usando la extensión nameko_redis
. Eche un vistazo a la lista de extensiones de la comunidad. Las extensiones se implementan de una manera que emplea la inyección de dependencia. Nameko se encarga de iniciar el objeto de extensión real que utilizará cada trabajador.
No hay mucha diferencia entre los microservicios Airports y Trips. Así es como se vería el microservicio de Viajes:
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
El Dockerfile
para cada microservicio también es muy sencillo. La única dependencia es nameko
y, en el caso de los servicios Aeropuertos y Viajes, también es necesario instalar nameko-redis
. Esas dependencias se dan en los requirements.txt
en cada servicio. El Dockerfile para el servicio de aeropuertos tiene el siguiente aspecto:
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"]
La única diferencia entre eso y el Dockerfile para los otros servicios es el archivo de origen (en este caso, airports.py
), que debe cambiarse en consecuencia.
El script run.sh
se encarga de esperar hasta RabbitMQ y, en el caso de los servicios de Aeropuertos y Viajes, la base de datos de Redis está lista. El siguiente fragmento muestra el contenido de run.sh
para aeropuertos. Nuevamente, para los otros servicios, simplemente cambie de aiports
a gateway
de enlace o trips
en consecuencia:
#!/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
Nuestros servicios ya están listos para funcionar:
$ docker-compose up
Probemos nuestro sistema. Ejecute el 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
Esa última línea es la identificación generada para nuestro aeropuerto. Para probar si está funcionando, ejecute:
$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
Ahora tenemos dos aeropuertos, eso es suficiente para formar un viaje. Vamos a crear un viaje ahora:
$ 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, esa última línea representa la identificación del viaje. Vamos a comprobar si se ha insertado correctamente:
$ curl localhost:8000/trip/34ca60df07bc42e88501178c0b6b95e4 {"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000adcc774cfda8ca3a806baec6b5'}"}
Resumen
Hemos visto cómo funciona Nameko al crear una instancia de ejecución local de RabbitMQ, conectarse a ella y realizar varias pruebas. Luego, aplicamos los conocimientos adquiridos para crear un sistema simple usando una arquitectura de Microservicios.
A pesar de ser extremadamente simple, nuestro sistema se parece mucho a una implementación lista para producción. Preferiría usar otro marco para manejar solicitudes HTTP como Falcon o Flask. Ambas son excelentes opciones y se pueden usar fácilmente para crear otros microservicios basados en HTTP, en caso de que desee romper su servicio Gateway, por ejemplo. Flask tiene la ventaja de que ya tiene un complemento para interactuar con Nameko, pero puede usar nameko-proxy directamente desde cualquier marco.
Nameko también es muy fácil de probar. No hemos cubierto las pruebas aquí por simplicidad, pero consulte la documentación de pruebas de Nameko.
Con todas las partes móviles dentro de una arquitectura de microservicios, desea asegurarse de tener un sistema de registro sólido. Para crear uno, consulte Registro de Python: un tutorial detallado del compañero Toptaler y desarrollador de Python: Son Nguyen Kim.