¿Cómo construir microservicios en Python? (1/2)

Microservicios por aquí, microservicios por allá. Nos han contado una y mil veces cómo funciona esta arquitectura, lo mucho que mola y lo impresionante que es utilizar el Stack de de Netflix.

Pero a la hora de ponernos manos a la obra, si queremos construir microservicios que no sean con Netflix y Java, ¿por dónde empezamos? ¿Tenemos que reinventar la rueda para construir microservicios en Python? ¿De qué herramientas y librerías disponemos?

No vamos a entrar en cómo diseñar un microservicIo, ya que sobre este tema ya podemos encontrar mucha mucha literatura. En este post vamos a centrarnos en los frameworks, librerías y cómo estructurar un proyecto en Python para crear nuestros microservicios. En futuros post, nos centraremos en cómo desplegarlos.

El código

Nuestro esqueleto: Flask vs Django

Estos dos frameworks son los reyes en el mundo Python para crear proyectos web. Para nuestro objetivo de crear un microservicio, hemos optado por Flask, ya que encaja perfectamente con el principio de responsabilidad única en la arquitectura de microservicios.

Además, al ser un framework menos “opinionated”, permite ser más todo terreno. Por ejemplo, si queremos crear un API Management, con bases de datos no relacionales u otros sistemas como GraphQL.

Hay que tener en cuenta que cuando hablamos de microservicios suele ir acompañado de un mayor consumo de memoria y recursos. Frente a esto, hicimos comparativas de rendimiento entre un framework y otro, levantando las aplicaciones en un contenedor de Docker y Gunicorn con 8 procesos.

Lanzamos unas pruebas de rendimiento de 1.000 clientes realizando 10.000 peticiones y otras de 1.000 clientes realizando 100.000 peticiones a un endpoint que realizaban una consulta a base de datos recuperando varios registros. Como resultado, frente a las mismas operaciones, Django resultó consumir un 8,3% más memoria y tener un 5,9% de tiempos de respuesta más lentos que Flask.

Estas cifras según el caso puede variar, pero sí denotan las ventajas de Flask por su código ligero. Las dos microservicios de ejemplo podréis descargarlos en los enlaces que hay al final de este post.

Como ejemplo de su maravillosa ligereza, una aplicación en Flask puede ser ejecutada con estas 5 líneas de código:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def index():
    return 'Hello World'

Ejecutando desde la consola:

$ FLASK_APP=app.py flask run

Si estos datos no te convencen, sigue leyendo y te contaré por qué pienso que Flask es el mejor para nuestro propósito (creando hype).

Nuestro ORM: SQLAlchemy

Con las últimas actualizaciones de Django, en especial a partir de la versión 1.11, SQLAlchemy está perdiendo el podium de ORM más flexible.

Es cierto que la curva de aprendizaje es bastante más alta que el del Framework de la “D” muda, pero a la hora de hacer queries muy complejas, SQLAlchemy desde mi punto de vista se llevaba el primer puesto de calle.

Para simplificar el uso de SQLAlchemy, hemos usado la librería Flask-SQLAlchemy que, a grandes rasgos, implementa de base scoped_session, que nos permite trabajar con este ORM de una forma más “Django friendly”, por ejemplo, para realizar una query simple sería:

Con SQLAlchemy:

Colors.query.all()

Con Flask-SQLAlchemy:

session.query(Color).all()

Nuestras Trazas: Opentracing y Jaeger

Hasta ahora, cada lenguaje tenía su método preferido para la trazabilidad de peticiones entre microservicios. Por ejemplo, si nuestros microservicios de Java tenemos Sleuth que, ciertamente, si incluimos la librería en nuestros micros, nos hace magia negra y tenemos trazas en todos lados.

Pero, ¿qué pasa en stacks de microservicios que utilizan muchas tecnologías? Para ello ha surgido esta iniciativa, para estandarizar el uso de trazas entre los lenguajes y frameworks más populares.

Además, permite integrarse con muchísimos clientes como Jaeger (desarrollado por Uber y compatible con la propagación B3 y Zipkin), LightStep, Apache SkyWalking…

La única pega que se les puede sacar es que actualmente tienen unas cuantas issues con Python 3.

De apoyo: Flask Injector

Las aplicaciones en Flask suelen estar enfocadas a proyectos ligeros de no más de un fichero. Pero en el momento que empezamos a meter configuración para varios entornos, modelos, más de 5 ó 10 endpoints tenemos que pensar en partir nuestra aplicación. Para ello, nos podemos apoyar en los blueprints de Flask.

Con esta solución para organizar nuestro código, si a su vez empezamos a meter muchas librerías para base de datos, para seguimiento de trazas, un sistema de autenticación con JWT… caeremos con facilidad en importaciones cíclicas donde necesitaremos nuestra librería en el fichero donde arrancamos nuestra app y en los ficheros que heredan de los blueprints. Para evitar este problema, esta librería puede ser nuestra amiga.

*
# Route with injection
@app.route("/foo")
def foo(db: sqlite3.Connection):
    users = db.execute('SELECT * FROM users').all()
    return render("foo.html")

def configure(binder):
    binder.bind(
        sqlite3.Connection,
        to=sqlite3.Connection(':memory:'),
        scope=request,
    )

# Initialize Flask-Injector. This needs to be run *after* you attached all
# views, handlers, context processors and template globals.

FlaskInjector(app=app, modules=[configure])

# All that remains is to run the application

app.run()
*

Testing: Tox

Cuando realizas un software ad hoc no sueles tener problemas entre versiones, pero cuando quieres crear una librería o un módulo, empiezan los problemas de versiones: “En mi Django 1.8 no funciona”, “En Python 3.3 falla pero en Python 3.6 ok”

Cuando trabajamos con microservicios antes o después es muy fácil tener duplicidad de código entre un micro y otro. Para ello, lo normal es recurrir a desacoplar estas funcionalidades comunes en librerías pero, ¿cómo probar todas estas permutaciones de versiones y frameworks para validar que nuestra librería es compatible en todos los casos?

En este momento entra en escena Tox. Esta librería nos permite definir unos pasos para ejecutar nuestros tests para cada una de las versiones que especifiquemos.

Por ejemplo, para Python 2.7 con Django 1.8; Python 2.7 con Django 1.11; para Python 3.6 con Django 1.8; Python 3.6 con Django.

Además, con cada ejecución se instala un entorno virtual desde cero, para que ese “pip install” que se hizo deprisa y corriendo y que no añadimos en los requirements nos avise testeando en local. “En mi local funciona” ¿no? ;)

Documentación: Swagger

Como pudimos ver en otros post de nuestro blog, una de las mejores y más comunes herramientas de documentación de APIs es Swagger, además que nos proporciona editores online para crear esta.

Para crear microservicios de calidad: Connexion

Como mencionaba al principio en la comparativa de Flask y Django, esta librería hace que Flask se establezca como el señor de los microservicios, el rey en el norte, el dueño de las gemas del infinito.

La mayoría de las veces, se programa un endpoint y posteriormente se crea/actualiza la documentación. Pero tarde o temprano llegan las prisas, cambiamos un valor, añadimos nuevos códigos de respuesta, y ¿quién se acuerda de actualizarlo?

Cuando trabajamos en un ecosistema de microservicios, cuando existen decenas o centenares de partes, es crucial tener la documentación al día. Si no, empezarán a llegar los problemas de integración y el caos.

Gracias a Connexion este problema es cosa del pasado. Su “magia” reside en que leyendo nuestra documentación en Swagger, crea los endpoint, las rutas y validaciones de las definiciones de nuestros objetos. Es decir, nos “obliga” a seguir un diseño de API First. ¿Cómo? ¿Eso es posible? ¿Cómo se hace?

Podemos crear el esquema desde nuestra aplicación desde el editor online de Swagger, exportar ese fichero en formato yaml y añadirlo en nuestro proyecto con Flask.

“Mágicamente” ya tenemos creado el esqueleto de nuestra aplicación. Incluso, podemos ir más lejos, ya que Swagger Editor nos genera el proyecto en código Python para que únicamente tengamos que añadir el origen de los datos de nuestra aplicación.

Viendo un ejemplo, si tenemos nuestro fichero YAML con nuestro Swagger:

paths:
  /colors/:
    get:
      tags:
      - "colors"
      summary: "Example endpoint return a list of colors by palette"
      description: ""
      operationId: "list_view"
      consumes:
      - "application/json"
      produces:
      - "application/json"
      responses:
        200:
          description: "A list of colors"
          schema:
            $ref: '#/definitions/Color'
        204:
          description: "Color not found"
        405:
          description: "Validation exception"
      x-swagger-router-controller: "project.views.views"

Con Connexion, al ejecutar nuestra aplicación y entrar, por ejemplo en http:localhost:5000/colors/, buscará el path en nuestra definición del endpoint, en este caso /colors/ y buscará la función que tenemos definida en:

x-swagger-router-controller: "project.views.views"

Junto con:

operationId: "list_view"

Es decir, ejecutará la función list_view que tenemos definida en /project/views/views.py.

Si en nuestra definición además, incluyésemos parámetros como:

      parameters:
      - in: "body"
        name: "name"
        description: "Color object that needs to be added"
        required: true
        schema:
          $ref: "#/definitions/Color"

Connexion nos validará que mandamos estos campos si son obligatorios y nos validará el tipado, ahorrándonos mucho trabajo innecesario.

La imagen

Ya tendremos nuestro código en local, pero todavía no estará listo para desplegarlo. En este caso, prepararemos una imagen de Docker que será la que promocionemos por los diferentes entornos de desarrollo, preproducción y producción.

Si no estás familiarizado con Docker y contenedores y quieres trabajar con microservicios, es el momento de ponerse al día.

A diferencia de los despliegues en un servidor “tradicional”, no utilizaremos ni Nginx o Apache, pero para ejecutar la aplicación dentro de la imagen será a través de Gunicorn que es el programa encargado de ejecutar nuestra aplicación con workers, pudiendo levantar muchos procesos para soportar mucha más carga.

Además, para mejorar todavía más el rendimiento, junto a Gunicorn, usaremos Gevent para que nuestros worker funcionen de manera asíncrona y soporten mucha más concurrencia.

Con eso, nuestro comando en Dockerfile para ejecutar nuestra App quedaría tal que:

CMD ["gunicorn", "--worker-class", "gevent", "--workers", "4", "--log-level", "INFO", "--bind", "0.0.0.0:5000", "manage:app"]

El microservicio

Para tener nuestro microservicio en un entorno de integración o de producción, hemos apostado por utilizar Kubernetes.

¿Por qué no usar el stack Neftlix? Como decía al principio, aunque este stack está pensado para microservicios, la principal implementación ha sido realizada por Spring, por lo que para otros lenguajes obligaría a reinventar o implementar las librerías.

Kubernetes proporciona algunas de las piezas necesarias para el manejo y control de una arquitectura basada en microservicios (registro de los servicios, resiliencia, balanceo de carga…), así que nos basaremos en él para la orquestación de los microservicios, añadiendo los componentes que no cubre con otras herramientas.

Como hemos podido ver, construir microservicios con Python es posible a pesar de la poca literatura que existe actualmente a diferencia de otros lenguajes. Todo esto tiene que evolucionar mucho todavía y me gustaría pensar que muchas de las cosas aquí contadas quedarán obsoletas en un par de años.

Próximamente, en la siguiente parte, veremos cómo unir nuestro código con Kubernetes y tener una visión global de un ecosistema de microservicios en Python, pero a su vez compatible con cualquier otro lenguaje de programación.

Esto lo veremos en futuros post, pero para ir abriendo boca, recomiendo encarecidamente practicar con Minikube. Este software nos permite instalar y trabajar con kubernetes de manera local para ir familiarizándonos y trastear sin riesgo de corromper un entorno.

Además, Google ha creado un montón tutoriales interactivos para ir introduciéndonos al uso de estas herramientas de una manera muy simple

El arquetipo

Para ver todas estas librerías y partes juntas, podéis verlo en el siguiente repositorio el arquetipo u otros ejemplos en el grupo Python Microservicios.

Incluso el mismo arquetipo montado con Django si a pesar de todo lo explicado no os he arrastrado al lado oscuro de Flask. Todavía está en un estado Beta pero os animo a descargaros el código, trastear y a aportar todas las mejoras que se consideren oportunas.

Developer, SyAdmin, QA, DevOps, Fullstack, Hacker... ¡qué importa la especialización! Mientras el puzzle suponga un reto, todo lo demás es superficial. Apasionado de la informática desde que tengo uso de conciencia y desde que en 2010 unos locos decidieron empezar a pagarme por hacer lo que me gusta las 24 horas del día

Ver toda la actividad de Alberto Vara

Escribe un comentario