Las pruebas de rendimiento son fundamentales para garantizar que una aplicación pueda soportar escenarios de uso del mundo real, especialmente cuando se enfrenta a una alta carga de tráfico. Mediante la implementación y ejecución sistemática de pruebas de rendimiento, el análisis de los resultados y la optimización basada en estos hallazgos, es posible mejorar significativamente la capacidad de respuesta, estabilidad y escalabilidad de la aplicación. El uso de marcos de prueba como Locust facilita este proceso y permite evaluar el comportamiento de la aplicación frente a diversos escenarios de tráfico.

Para comenzar a realizar pruebas de rendimiento, es necesario contar con una aplicación funcional y un marco de pruebas adecuado. En este caso, usaremos Locust, que es un marco basado en Python, ideal para simular cargas de usuarios y medir el rendimiento de una aplicación web. A continuación, se describen los pasos para configurar y ejecutar una prueba de rendimiento básica utilizando Locust.

Preparación

Antes de comenzar, debes asegurarte de que tu aplicación esté funcionando correctamente. Para los fines de esta demostración, utilizaremos una aplicación prototipo basada en FastAPI, llamada protoapp. También necesitarás instalar Locust en tu entorno virtual. Si aún no lo has hecho, puedes instalarlo ejecutando el siguiente comando:

ruby
$ pip install locust

Una vez que tengas Locust instalado, puedes proceder a configurar el archivo de pruebas, locustfile.py, en la raíz de tu proyecto. Este archivo define cómo los usuarios simularán la interacción con tu aplicación bajo prueba. Un ejemplo mínimo de configuración para un archivo locustfile.py sería el siguiente:

python
from locust import HttpUser, task class ProtoappUser(HttpUser): host = "http://localhost:8000" @task def hello_world(self): self.client.get("/home")

Ejecución de la prueba

Para ejecutar la prueba, primero asegúrate de que el servidor de FastAPI esté en funcionamiento. Puedes iniciar el servidor con el siguiente comando:

ruby
$ uvicorn protoapp.main:app

Luego, abre otra ventana de terminal y ejecuta Locust:

ruby
$ locust

A continuación, abre tu navegador y navega a la URL http://localhost:8089, donde se encuentra la interfaz web de Locust. Desde allí podrás configurar los parámetros de la prueba:

  • Usuarios concurrentes: Define el número máximo de usuarios que accederán al servicio simultáneamente.

  • Tasa de incremento de usuarios: Especifica la cantidad de nuevos usuarios que se añadirán por segundo para simular un aumento en el tráfico.

Una vez que hayas configurado estos parámetros, haz clic en el botón "Start" para iniciar la simulación de tráfico a tu aplicación a través del endpoint /home definido en el archivo locustfile.py.

También puedes simular tráfico sin necesidad de la interfaz web, utilizando el siguiente comando desde la terminal:

scss
$ locust --headless --users 10 --spawn-rate 1

Este comando ejecutará Locust en modo sin cabeza (sin interfaz gráfica), simulando 10 usuarios concurrentes con una tasa de generación de un usuario por segundo.

Integración en un pipeline CI/CD

Puedes llevar aún más lejos tus pruebas de rendimiento integrándolas en un pipeline de Integración Continua/Entrega Continua (CI/CD). Esto te permitirá realizar pruebas de rendimiento automáticamente antes de desplegar tu aplicación o incluso incluirlas en una rutina de pruebas más amplia. La documentación oficial de Locust ofrece una guía detallada sobre cómo probar cada aspecto del tráfico de tu aplicación.

Ampliación del alcance de las pruebas

Las pruebas de rendimiento no deben limitarse solo a medir cuántos usuarios puede soportar tu aplicación. Es importante analizar cómo se comporta la aplicación bajo diferentes condiciones, como:

  1. Pruebas de escalabilidad: ¿Cómo responde la aplicación cuando aumentas el número de usuarios?

  2. Pruebas de estrés: ¿Hasta qué punto tu aplicación puede manejar tráfico excesivo antes de fallar?

  3. Pruebas de resistencia: ¿Cómo se comporta la aplicación durante periodos prolongados de carga constante?

  4. Pruebas de recuperación: ¿Es capaz tu aplicación de recuperarse de fallos o caídas?

Estas pruebas deben realizarse en condiciones que imiten escenarios de uso realistas. Cuanto más se acerquen estas condiciones a lo que experimentarán los usuarios reales, más precisos serán los resultados. Además, es clave que las pruebas se repitan periódicamente para detectar problemas que puedan surgir tras actualizaciones o cambios en la infraestructura.

Optimización posterior a las pruebas

Una vez obtenidos los resultados de las pruebas, el siguiente paso es la optimización. Con base en los datos recolectados durante las pruebas de rendimiento, puedes identificar los cuellos de botella de la aplicación y trabajar en su mejora. Esto puede incluir:

  • Optimización de consultas SQL: Revisar y ajustar las consultas a la base de datos para que sean más eficientes.

  • Mejora del uso de recursos: Revisar el uso de memoria y CPU para asegurarte de que los recursos no se sobrecarguen.

  • Caché: Implementar mecanismos de caché para reducir la carga en el servidor y mejorar los tiempos de respuesta.

Recuerda que las pruebas de rendimiento son una parte esencial del ciclo de vida del desarrollo de software, y es fundamental realizarlas antes de realizar despliegues en producción.

¿Cómo integrar MongoDB con FastAPI para operaciones CRUD?

Para desarrollar aplicaciones eficientes y escalables, las operaciones CRUD (Crear, Leer, Actualizar y Eliminar) son fundamentales. En el contexto de FastAPI, utilizar bases de datos NoSQL como MongoDB ofrece flexibilidad en el manejo de datos sin necesidad de esquemas estrictos. A continuación, se describe cómo integrar MongoDB con FastAPI, configurando una conexión y creando endpoints para realizar operaciones CRUD en una aplicación de ejemplo.

Para comenzar, se debe tener Python y el paquete FastAPI instalados en el entorno, así como una instancia de MongoDB corriendo y accesible. Si aún no se tiene MongoDB, es posible configurar una instancia local siguiendo la documentación oficial (https://www.mongodb.com/try/download/community). Para esta receta, se utilizará una instancia de MongoDB que esté corriendo en http://localhost:27017. Si se usa otro puerto o una máquina remota, solo se debe ajustar la URL de conexión.

En primer lugar, es necesario instalar el paquete motor, el cual es el controlador asincrónico desarrollado por MongoDB para Python. Para instalarlo, se debe ejecutar el siguiente comando en la línea de comandos:

bash
pip install motor

El siguiente paso es establecer la conexión con MongoDB. Esto se hace en un módulo llamado db_connection.py, donde se define un cliente de MongoDB. El código básico para la conexión es el siguiente:

python
from motor.motor_asyncio import AsyncIOMotorClient mongo_client = AsyncIOMotorClient("mongodb://localhost:27017")

Cada vez que se necesite interactuar con la instancia de MongoDB, se utilizará el objeto mongo_client. Para asegurar que la conexión esté activa, se puede crear una función que haga un "ping" al servidor de MongoDB y verificar si responde correctamente:

python
import logging logger = logging.getLogger("uvicorn.error") async def ping_mongo_db_server(): try: await mongo_client.admin.command("ping") logger.info("Conectado a MongoDB") except Exception as e: logger.error(f"Error al conectar con MongoDB: {e}") raise e

Esta función intentará hacer un ping al servidor. Si no se recibe respuesta, se lanzará un error y el código dejará de ejecutarse.

Una vez que la conexión esté configurada, es necesario gestionar el ciclo de vida del servidor FastAPI. Esto se hace mediante un contexto asincrónico llamado lifespan, el cual se encarga de verificar la conexión a MongoDB cuando se inicia el servidor:

python
from contextlib import asynccontextmanager from fastapi import FastAPI from app.db_connection import ping_mongo_db_server @asynccontextmanager async def lifespan(app: FastAPI): await ping_mongo_db_server() yield app = FastAPI(lifespan=lifespan)

Al iniciar el servidor con el siguiente comando:

bash
uvicorn app.main:app

Se verá un mensaje de log que confirmará la conexión exitosa con MongoDB.

Con la conexión lista, ahora se puede proceder a implementar las operaciones CRUD en FastAPI. Primero, se debe crear una base de datos en MongoDB. En este caso, se usará una base de datos llamada beat_streaming:

python
from app.db_connection import mongo_client database = mongo_client.beat_streaming

No es necesario realizar ninguna acción adicional en el lado del servidor de MongoDB, ya que la librería motor se encargará de verificar si la base de datos y las colecciones existen y las creará automáticamente si no lo están.

Luego, se debe definir una función que devuelva esta base de datos como dependencia en las rutas de FastAPI, lo cual ayudará a mantener el código organizado y reutilizable:

python
def mongo_database(): return database

Ahora se puede proceder a crear los endpoints para las operaciones CRUD. El primero es para agregar una canción a la colección songs en MongoDB. Este endpoint recibirá un objeto JSON con los detalles de la canción y lo almacenará en la base de datos:

python
from bson import ObjectId from fastapi import Body, Depends from app.database import mongo_database from fastapi.encoders import ENCODERS_BY_TYPE ENCODERS_BY_TYPE[ObjectId] = str @app.post("/song") async def add_song( song: dict = Body( example={ "title": "My Song", "artist": "My Artist", "genre": "My Genre", } ), mongo_db=Depends(mongo_database), ): await mongo_db.songs.insert_one(song) return { "message": "Canción agregada con éxito", "id": song["_id"], }

Este endpoint permitirá agregar una canción a la colección de MongoDB, devolviendo el ID del documento insertado.

Para leer una canción desde la base de datos, se crea un endpoint de tipo GET que reciba el song_id y devuelva los datos correspondientes:

python
@app.get("/song/{song_id}") async def get_song(song_id: str, db=Depends(mongo_database)): song = await db.songs.find_one( {"_id": ObjectId(song_id)} if ObjectId.is_valid(song_id) else None ) if not song: raise HTTPException(status_code=404, detail="Canción no encontrada") return song

Si la canción con el ID proporcionado no existe, se devolverá un error 404.

Para actualizar una canción, el endpoint utilizará el método PUT. Recibirá un song_id y los nuevos datos para la canción, y actualizará el documento correspondiente:

python
@app.put("/song/{song_id}") async def update_song(song_id: str, song: dict, db=Depends(mongo_database)): result = await db.songs.update_one( {"_id": ObjectId(song_id)}, {"$set": song}, ) if result.modified_count == 0: raise HTTPException(status_code=404, detail="Canción no encontrada o no modificada") return {"message": "Canción actualizada con éxito"}

Este endpoint actualizará la canción correspondiente, o devolverá un error si no se encuentra el documento o si no se realizó ninguna modificación.

Estas son las bases de las operaciones CRUD con MongoDB en FastAPI, pero se pueden añadir muchas más funcionalidades como filtros avanzados, validación de datos o paginación. Es importante tener en cuenta que, al trabajar con MongoDB, se tiene la ventaja de no estar atado a un esquema rígido como en las bases de datos SQL. Esto facilita el trabajo con datos no estructurados o de estructuras flexibles, lo cual es particularmente útil en aplicaciones modernas como plataformas de streaming, donde los tipos de datos pueden cambiar rápidamente.

¿Cómo implementar la internacionalización y optimizar el rendimiento de una aplicación FastAPI?

La implementación de la internacionalización (i18n) y la localización (l10n) en una aplicación web no solo permite que el contenido se adapte a distintos idiomas, sino que también facilita la entrega de contenido personalizado, como la moneda o la fecha, según la región del usuario. Estos conceptos se han vuelto imprescindibles en el desarrollo de plataformas globales, como en el caso de aplicaciones que desean ofrecer una experiencia fluida a usuarios de todo el mundo. En este sentido, el uso de FastAPI, un framework eficiente y flexible para la creación de APIs, es una excelente opción para lograr estos objetivos de manera óptima.

Al desarrollar la funcionalidad de internacionalización, uno de los primeros pasos consiste en crear una función que maneje el encabezado Accept-Language, utilizado por los navegadores para enviar al servidor la preferencia de idioma del usuario. Para lograrlo, podemos implementar una función como resolve_accept_language, que procesa este encabezado para devolver un valor adecuado de localización.

La implementación de la función comienza importando el módulo babel y creando una instancia de la clase Locale. La función toma el valor del encabezado Accept-Language y lo divide en una lista de preferencias de idioma, utilizando el parámetro q que indica la prioridad del idioma preferido. Luego, esta lista se ordena según la prioridad del valor q, asegurando que el idioma preferido sea el primero en la lista. Para determinar cuál es el idioma más adecuado para la aplicación, utilizamos la función negotiate_locale de Babel. Si no hay un idioma que coincida con los valores disponibles en la configuración, se retorna por defecto el idioma inglés (en_US).

Además, en una implementación real, los contenidos traducidos no se deben almacenar directamente en el código como un diccionario estático, sino que deben ser recuperados desde una base de datos, lo que permite una mayor escalabilidad y flexibilidad. Por ejemplo, para un endpoint /homepage, el contenido de la página de inicio se puede almacenar en diferentes idiomas (como "Welcome to Trip Platform" para inglés y "Bienvenue sur Trip Platform" para francés). A través del uso de la dependencia resolve_accept_language, FastAPI selecciona el contenido adecuado para el usuario.

En un escenario similar, es posible que se necesite adaptar la moneda en función del idioma o la región. Para ello, un endpoint como /show/currency podría devolver la moneda correcta para cada usuario, utilizando un patrón similar al de la página de inicio. En este caso, los valores de las monedas, como "USD" para Estados Unidos y "EUR" para Francia, se gestionan también con la ayuda del encabezado Accept-Language.

Es importante tener en cuenta que la localización y la internacionalización no solo se limitan a la traducción de cadenas de texto. Factores como el formato de fecha y hora, el tipo de moneda, y el uso de unidades de medida son esenciales para ofrecer una experiencia de usuario verdaderamente local. Por ejemplo, la visualización de la fecha en formato "dd/mm/yyyy" en lugar de "mm/dd/yyyy" o la adaptación de las unidades métricas según la región del usuario (kilómetros frente a millas) son detalles fundamentales para que la aplicación se sienta auténtica y relevante para cada región.

En paralelo con la internacionalización, la optimización del rendimiento de la aplicación es otro aspecto clave en el desarrollo de plataformas escalables y rápidas. FastAPI, aunque es conocido por su alta eficiencia, puede beneficiarse de técnicas adicionales para detectar cuellos de botella en el código y mejorar su desempeño general. Una estrategia comúnmente utilizada para este fin es la instrumentación de código, que permite medir el tiempo de ejecución de las funciones y detectar posibles áreas problemáticas.

El uso de herramientas como pyinstrument permite crear perfiles de rendimiento del código, lo que facilita la identificación de secciones del código que consumen más recursos. Para implementar un profiler en FastAPI, primero es necesario configurar el paquete pyinstrument, activando el modo asíncrono para asegurarse de que se registren los tiempos de ejecución de las funciones que usan await. Luego, se puede crear un middleware que se encargue de iniciar y detener el profiler antes y después de cada solicitud, lo que garantiza que solo se mida el tiempo de ejecución de los endpoints, evitando así el sobrecosto de la instrumentación en otras partes de la aplicación.

Este middleware se integra fácilmente en la aplicación FastAPI, y al final de cada solicitud, se genera un archivo profiler.html que muestra un desglose detallado del rendimiento de cada función llamada. Este enfoque permite a los desarrolladores detectar fácilmente las partes del código que pueden estar causando retrasos o sobrecarga, y optimizarlas según sea necesario.

En resumen, la implementación de la internacionalización y localización en una aplicación FastAPI es un proceso esencial para asegurar que la plataforma esté lista para ser utilizada por un público global. Además, es crucial implementar técnicas de optimización del rendimiento para garantizar que la aplicación pueda manejar grandes cantidades de tráfico sin perder eficiencia. Con las herramientas y métodos descritos, se puede crear una plataforma escalable, rápida y accesible para usuarios de diferentes partes del mundo.

¿Cómo ejecutar una aplicación FastAPI con múltiples trabajadores en Docker para mejorar el rendimiento?

En entornos con alto tráfico, ejecutar una aplicación FastAPI con un único trabajador puede no ser suficiente para manejar todas las solicitudes de manera eficiente. Para mejorar el rendimiento y optimizar el uso de los recursos, es posible ejecutar la aplicación FastAPI utilizando múltiples trabajadores. Esta configuración se puede lograr utilizando herramientas como Gunicorn. En este artículo, exploraremos cómo ejecutar una aplicación FastAPI con múltiples trabajadores en un contenedor Docker, y discutiremos la capacidad de Uvicorn para manejar múltiples trabajadores, así como sus limitaciones.

Preparativos previos

Es importante tener en cuenta que el paquete Gunicorn no es compatible con Windows. Para garantizar la operabilidad del sistema operativo, ejecutaremos nuestra aplicación en un contenedor Docker. Este enfoque es útil para mantener la coherencia en los entornos de desarrollo y producción, ya que Docker proporciona una plataforma independiente del sistema operativo. A continuación, abordamos los pasos para ejecutar FastAPI con Gunicorn y Docker.

Implementación de FastAPI con múltiples trabajadores

FastAPI, cuando se ejecuta con múltiples trabajadores, inicia copias de la aplicación en procesos de CPU diferentes. Esto permite distribuir la carga entre varios procesos, mejorando la escalabilidad y el rendimiento del servicio web. Para visualizar mejor esto, podemos modificar el punto de entrada de la aplicación para que devuelva el ID del proceso (PID) en cada solicitud.

En el archivo main.py, agregue las siguientes líneas de código:

python
import logging from os import getpid # resto del módulo logger = logging.getLogger("uvicorn") @app.get("/") def read_root(): logger.info(f"Procesado por el trabajador {getpid()}") return {"Hello": "World"}

A continuación, agregue Gunicorn a las dependencias del archivo requirements.txt:

nginx
fastapi gunicorn

Configuración de Gunicorn

En entornos Linux o macOS, basta con instalar Gunicorn en el entorno de desarrollo con el siguiente comando:

bash
$ pip install gunicorn

Luego, se puede ejecutar el servidor con cuatro trabajadores utilizando el siguiente comando:

bash
$ gunicorn app.main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker

Sin embargo, si está utilizando Windows, es necesario ejecutar el contenedor Docker debido a la incompatibilidad de Gunicorn con este sistema operativo. En el archivo Dockerfile.dev, agregue la instrucción CMD para especificar cómo ejecutar Gunicorn:

Dockerfile
CMD ["gunicorn", "app.main:app", "--bind", "0.0.0.0:80", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--log-level", "debug"]

Ahora, construya la imagen Docker utilizando el siguiente comando:

bash
$ docker build -t live-application-gunicorn -f Dockerfile.dev .

Una vez construida la imagen, ejecute el contenedor con el siguiente comando:

bash
$ docker run -p 8000:80 -i live-application-gunicorn

El parámetro -i permite ejecutar el contenedor en modo interactivo para poder visualizar los registros.

Verificación del funcionamiento

Con el servidor en funcionamiento, puede acceder a la documentación interactiva en http://localhost:8000/docs y realizar algunas pruebas. En la salida del terminal, podrá observar diferentes PIDs que varían en cada solicitud, lo que indica que Gunicorn distribuye la carga entre varios procesos, aprovechando múltiples núcleos de CPU.

De esta manera, hemos aprendido cómo ejecutar una aplicación FastAPI con Gunicorn y múltiples trabajadores, lo que mejora el rendimiento y la escalabilidad de su servicio web. Puede experimentar con diferentes configuraciones y opciones para encontrar la mejor solución adaptada a sus necesidades.

Consideraciones adicionales

Aunque Uvicorn también puede manejar múltiples trabajadores, su gestión de procesos no es tan avanzada como la de Gunicorn en este momento. Gunicorn es más robusto para gestionar aplicaciones de alto rendimiento, especialmente cuando se utiliza con Uvicorn como su clase de trabajador.

Es importante tener en cuenta que ejecutar múltiples trabajadores tiene sus propios desafíos. Cada proceso de trabajador tiene su propio espacio de memoria y no puede compartir datos con otros procesos. Por lo tanto, es necesario utilizar un servicio centralizado o distribuido para almacenar componentes con estado, como cachés o sesiones, utilizando tecnologías como Redis o Memcached.

Además, ejecutar múltiples trabajadores puede aumentar el consumo de recursos y la posibilidad de contención en el servidor, especialmente si la aplicación es intensiva en CPU o depende mucho de las entradas y salidas. Por lo tanto, es fundamental elegir el número adecuado de trabajadores según las características de la aplicación y los recursos disponibles. Una regla general es usar la fórmula trabajadores = (2 x núcleos) + 1, donde "núcleos" es el número de núcleos de CPU en el servidor. Sin embargo, esta fórmula puede no ser adecuada para todos los casos, y será necesario realizar pruebas y ajustes finos para obtener los mejores resultados.