API de datos sin servidor: Arquitectura DuckDB + FastAPI
En el actual ecosistema de datos, la velocidad y la eficiencia en costos son fundamentales. Los directores técnicos y líderes de ingeniería buscan continuamente eliminar las cargas administrativas sin sacrificar el rendimiento. Este artículo detalla un patrón de alta concurrencia y bajo costo para servir datos analíticos, combinando la velocidad "in-process" de DuckDB con el poder asíncrono de FastAPI, desplegados en un entorno completamente libre de servidores.
Nos adentraremos en ejemplos complejos y de uso real para construir una API analítica de nivel profesional, que solo lee datos. El principal desafío arquitectónico que abordamos es utilizar una base de datos (DuckDB) que requiere estado dentro del proceso, junto con un entorno de cálculo sin servidor y sin estado.
La solución implica tratar el archivo de la base de datos DuckDB como un artefacto inmutable y de solo lectura, creado por un proceso anterior y consumido a gran escala por nuestra API.
Equipo de ingeniería de software compartido bajo demanda, mediante suscripción.
Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gerente de proyecto gratuito le ayudan a crear MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.
Situación en un mundo sin estado
Una función sin servidor (como AWS Lambda o Google Cloud Run) es temporal. Su sistema de archivos local es transitorio y no se comparte entre instancias concurrentes.
- Utilizando
:memory::duckdb.connect(':memory:')crea una base de datos en memoria. Esto es inútil para una API sin servidor, ya que la base de datos está vacía y se destruirá en el momento en que finalice la función. - Utilizando
local.db:duckdb.connect('local.db')escribe en el disco episódico de la función. Esto también es inútil, ya que los datos no son persistentes y, crucialmente, no se comparten. Cada ejecución concurrente de su función tendría una base de datos diferente y vacía. - La falacia del "Escritura Concurrente": Se podría intentar montar un sistema de archivos compartido (como EFS) y que todas las funciones se conecten a
shared/data.duckdb. Esto fallará. DuckDB es una base de datos en el proceso; utiliza un registro de escritura anticipada (WAL) y bloqueos a nivel de archivo para gestionar las escrituras. Múltiples procesos que escriben al mismo archivo simultáneamente conducirán a la corrupción de la base de datos.
Por lo tanto, el patrón de servidor sin servidor correcto esdesacoplar el proceso de escritura de datos de la API de lectura de datos.
- Preparación de datos (El "camino de escritura"): Un proceso separado y programado (por ejemplo, una ejecución de dbt, una acción de GitHub o una tubería CI/CD) es responsable de la ingestión de datos. Este proceso ejecuta, recopila datos de las fuentes y crea un nuevo archivo de base de datos DuckDB (
analytics.db. - Almacenamiento de artefactos: Luego, este archivo se sube al almacenamiento de objetos en la nube (por ejemplo, AWS S3 o Google Cloud Storage).
- API sin servidor (El "camino de lectura"): Nuestra aplicación FastAPI, que se ejecuta en Lambda o Cloud Run, está configurada para conectarse a este archivo S3/GCS en modo de solo lectura..
Esta arquitectura permite una alta concurrencia de lecturas. Se pueden manejar un millón de usuarios concurrentes mediante un millón de instancias separadas sin servidor, cada una con su propio solo lecturaacceso solo para lectura
Paso 1: El código de la aplicación FastAPI
Nuestra aplicación FastAPI será sencilla. La clave está en cómo y dónde se conecta a DuckDB. Utilizaremos la extensión httpfs de DuckDB, que permite leer directamente desde S3 y GCS.
Primero, definamos la estructura del proyecto:
/fastapi-duckdb
|-- app/
| |-- main.py
|-- Dockerfile
|-- requirements.txt
requisitos.txt
Necesitamos FastAPI, un servidor ASGI (Uvicorn), y DuckDB. Para AWS Lambda, también necesitamos <s1>mangummangum para adaptar la aplicación ASGI.
fastapi
uvicorn
duckdb
mangum # Required only for AWS Lambda
app/main.py
Este código inicializa la conexión de DuckDB al iniciar la aplicación. El evento inicio es clave, ya que permite establecer la conexión una vez cuando se inicia la instancia sin servidor (un "inicio en frío") y reutilizarla para todas las solicitudes posteriores "calientes".
import os
import duckdb
from fastapi import FastAPI, Depends
from contextlib import asynccontextmanager
# Retrieve the database path from environment variables
# Example: 's3://my-data-bucket/analytics.db'
# Example: 'gs://my-data-bucket/analytics.db'
DB_PATH = os.environ.get("DUCKDB_PATH")
READ_ONLY_MODE = True
class DuckDBConnection:
"""
A class to manage the DuckDB connection lifecycle.
It initializes the connection on startup and closes it on shutdown.
It installs and loads necessary extensions (httpfs) and sets
credentials for S3/GCS if provided via environment variables.
"""
def __init__(self):
self.connection = None
def connect(self):
print(f"Connecting to DuckDB at: {DB_PATH}")
self.connection = duckdb.connect(database=DB_PATH, read_only=READ_ONLY_MODE)
# Install and load the httpfs extension to read from S3/GCS
self.connection.install_extension("httpfs")
self.connection.load_extension("httpfs")
# Configure credentials for GCS
if DB_PATH.startswith("gs://"):
gcs_key_id = os.environ.get("GCS_KEY_ID")
gcs_secret = os.environ.get("GCS_SECRET")
if gcs_key_id and gcs_secret:
print("Setting GCS credentials...")
self.connection.execute("CREATE SECRET gcs_secret (TYPE GCS, KEY_ID ?, SECRET ?)", [gcs_key_id, gcs_secret])
self.connection.execute("SET s3_region='auto'") # Use 'auto' for GCS
self.connection.execute("SET s3_url_style='path'")
# Configure credentials for S3
elif DB_PATH.startswith("s3://"):
aws_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
aws_secret = os.environ.get("AWS_SECRET_ACCESS_KEY")
aws_region = os.environ.get("AWS_REGION", "us-east-1")
if aws_key_id and aws_secret:
print(f"Setting S3 credentials for region: {aws_region}")
self.connection.execute(f"SET s3_region='{aws_region}'")
self.connection.execute(f"SET s3_access_key_id='{aws_key_id}'")
self.connection.execute(f"SET s3_secret_access_key='{aws_secret}'")
else:
print("Using IAM role for S3 access.")
print("DuckDB connection established.")
def close(self):
if self.connection:
self.connection.close()
print("DuckDB connection closed.")
def get_connection(self):
return self.connection
# Initialize the connection manager
db_manager = DuckDBConnection()
@asynccontextmanager
async def lifespan(app: FastAPI):
# On startup
db_manager.connect()
yield
# On shutdown
db_manager.close()
app = FastAPI(lifespan=lifespan)
# Dependency to get the DB connection
def get_db():
return db_manager.get_connection()
@app.get("/")
async def root():
return {"message": "Serverless DuckDB API is running"}
@app.get("/query")
async def query_db(sql: str, db=Depends(get_db)):
"""
Execute a SQL query.
WARNING: In production, NEVER pass raw SQL from a user.
This is for demonstration. Build specific endpoints.
"""
try:
# Execute the query and fetch results as a dictionary
result = db.sql(sql).to_df().to_dict('records')
return {"data": result}
except Exception as e:
return {"error": str(e)}
@app.get("/tables")
async def show_tables(db=Depends(get_db)):
"""A safe endpoint to list all tables."""
try:
tables = db.sql("SHOW TABLES").to_df().to_dict('records')
return {"tables": tables}
except Exception as e:
return {"error": str(e)}
# --- AWS Lambda Specific ---
# If deploying to Lambda, wrap the app with Mangum
# This part is ignored by Uvicorn/Cloud Run
try:
from mangum import Mangum
handler = Mangum(app)
except ImportError:
pass
Paso 2: Despliegue en Google Cloud Run
Cloud Run es el entorno más sencillo para esta configuración. Crea un contenedor y lo ejecuta, gestionando la escalabilidad desde cero hasta N.
Dockerfile
# Use an official Python runtime as a parent image
FROM python:3.11-slim
# Set the working directory in the container
WORKDIR /code
# Copy the requirements file and install dependencies
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# Copy the application code
COPY ./app /code/app
# Command to run the application using uvicorn
# Uvicorn will look for the 'app' variable in /code/app/main.py
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
Pasos de implementación:
- Otorgar Permisos: Asegúrese de que su cuenta de servicio para Cloud Run tenga los permisos "Visualizador de Objetos del Almacenamiento" en el bucket de GCS que contiene su archivo DuckDB.
- Definir Variables de Entorno: Cuando se despliega la cuenta de servicio de Cloud Run, configure lo siguiente:
DUCKDB_PATH:gs://your-bucket-name/analytics.db- (Opcional) Si no está utilizando los permisos predeterminados de la cuenta de servicio, configure
GCS_KEY_IDyGCS_SECRET.
Equipo de Ingeniería de Software Compartido Bajo Demanda, Por Suscripción.
Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gerente de proyecto gratuito le ayudan a construir MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.
Construir e implementar:
gcloud run deploy fastapi-duckdb-api \
--source . \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars="DUCKDB_PATH=gs://your-bucket-name/analytics.db"
Cloud Run construirá automáticamente el contenedor, lo subirá al registro de Artifactos y lo desplegará.
Paso 3: Despliegue en AWS Lambda
Implementar en Lambda es más complejo debido a su entorno de ejecución. DuckDB es un archivo binario compilado, y el que instalas con instalar con pippip install
Tiene dos soluciones principales:
- Despliegue de Imagen de Contenedor (Recomendado): Este es el método moderno y preferido. Utiliza el Dockerfile exacto
igual que el ejemplo de Cloud Run. - Despliegue con ZIP y Capas Lambda (Tradicional): Si no puedes utilizar contenedores, debes usar una capa Lambda precompilada de DuckDB.
Solución A: Implementación de contenedores Lambda
- Cree una función Lambda:
- En la Consola de AWS, cree una nueva función Lambda.
- Seleccione "Imagen de contenedor" como origen.
- Explore ECR y seleccione su
fastapi-duckdb-api:latestimagen. - Permisos: Asigne un rol de IAM a la función que tenga
s3:GetObjectpermisos en suanalytics.dbarchivo. - Variables de entorno: Establezca
DUCKDB_PATHas3://your-bucket-name/analytics.db.
- Cree un API Gateway:
- Cree una nueva API HTTP en API Gateway.
- Cree una ruta (por ejemplo,
GET /y una ruta de proxy{proxy+}). - Configure una "Integración" para que apunte a su nueva función Lambda.
Construye y despliega en ECR:
# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com
# Create the ECR repository
aws ecr create-repository --repository-name fastapi-duckdb-api
# Build, tag, and push
docker build -t fastapi-duckdb-api .
docker tag fastapi-duckdb-api:latest <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/fastapi-duckdb-api:latest
docker push <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/fastapi-duckdb-api:latest
Modificar Dockerfile: Añadir el handler "mangum".
# Use an AWS-provided base image for Python Lambda
FROM public.ecr.aws/lambda/python:3.11
# Set the working directory
WORKDIR ${LAMBDA_TASK_ROOT}
# Copy requirements and install
COPY ./requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY ./app ./app
# Set the command to run the Mangum handler
# This tells Lambda to execute 'handler' in 'app.main'
CMD ["app.main.handler"]
Solución B: Implementación de Lambda ZIP (con capas)
Si debe utilizar un despliegue con formato ZIP, no puede simplemente usar pip install duckdb. Debe utilizar una capa precompilada.
- Encuentra una Capa: Busca una capa pública de DuckDB Lambda compatible con tu entorno de ejecución de Python (por ejemplo, awslambda-layer-duckdb).
- Añade el ARN de la Capa: En la configuración de tu función Lambda, añade el ARN de la capa.
- Empaqueta el Código: Comprime el directorio
app/sin duckdbenrequirements.txt(como lo proporciona la capa).(tal como lo proporciona la capa). - Carga: Carga el archivo ZIP como la fuente de código de tu Lambda.
- Configura: Establece el manejador a
app.main.handlery configura API Gateway y variables de entorno como en la solución de contenedor.
Equipo de Ingeniería de Software Compartido Bajo Demanda, Por Suscripción.
Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda mediante una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gerente de proyecto gratuito le ayudan a crear MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.
Implicaciones en rendimiento y coste
Esta arquitectura presenta ventajas y desventajas claras a nivel de directivos:
- Costo: Extremadamente bajo. Solo paga por el almacenamiento de objetos (cientos de céntimos por GB) y el tiempo de ejecución sin servidor (milisegundos). No hay costo por un servidor de base de datos inactivo.
- Escalabilidad: Casi ilimitada. El rendimiento de lectura escala con tu límite de concurrencia sin servidor, que suele ser de miles de solicitudes concurrentes.
- Rendimiento:
- Inicio en frío: La primera solicitud a una nueva instancia tendrá latencia. La función debe inicializarse y DuckDB debe establecer su conexión remota con S3/GCS. Esto puede tardar de 1 a 3 segundos.
- Lecturas en caliente: Una vez que está "caliente", la API es excepcionalmente rápida. Las consultas se ejecutan mediante el motor DuckDB integrado, a menudo en milisegundos (dígitos individuales).
- Frescura de los datos: Esta es la principal compensación. Los datos son solo para lectura y solo tan frescos como tu última versión de preparación de datos. Este patrón no es adecuado para datos transaccionales en tiempo real, pero es perfecto para paneles de control, inteligencia empresarial (BI) y puntos finales analíticos donde la frescura de los datos de unos pocos minutos o horas es aceptable.
Un nuevo estándar para las API analíticas
Al combinar FastAPI, DuckDB y la computación sin servidor, hemos diseñado una solución altamente escalable, rentable y de fácil operación para servir datos analíticos. La clave es adoptar el patrón de "artefacto inmutable y de solo lectura". Esta arquitectura desacopla la ingestión de datos del servicio de datos, permitiendo que cada uno se escale independientemente y eficazmente, eliminando la necesidad de un costoso servidor de base de datos analítica siempre activo para muchos casos de uso comunes.elemento inalterable y de solo lecturaEsta arquitectura desacopla la ingestión de datos del servicio de datos, lo que permite que cada uno se escale de forma independiente y eficiente, eliminando la necesidad de un costoso servidor de base de datos analítica siempre activo para muchos casos de uso comunes.
Preguntas frecuentes
¿Cómo facilita la combinación de DuckDB y FastAPI una arquitectura sin servidor?
Al combinar DuckDB con FastAPI, los desarrolladores pueden crear una API analítica de alta concurrencia sin necesidad de administrar un servidor de base de datos dedicado. DuckDB funciona como un motor SQL en proceso que permite a la aplicación FastAPI leer datos directamente del almacenamiento de objetos en la nube (como AWS S3 o Google Cloud Storage) utilizando la extensión httpfs. Esta configuración trata el archivo de base de datos como un artefacto de solo lectura, lo que permite que la API se escale infinitamente en instancias sin servidor (como AWS Lambda o Cloud Run) manteniendo bajos costos y un alto rendimiento de lectura.
¿Cuál es la solución para gestionar datos con estado en un entorno de servidor sin estado?
El principal desafío en los entornos sin servidor es que las instancias de cálculo son temporales y no comparten almacenamiento local. Para abordar esto, se utiliza el patrón del "artefacto de solo lectura". En lugar de escribir en una base de datos local o en memoria, el archivo de la base de datos DuckDB se genera mediante un proceso upstream (por ejemplo, una tubería CI/CD) y se carga en el almacenamiento de objetos inmutable. La API sin servidor luego se conecta a este archivo remoto en modo de solo lectura, desacoplando el almacenamiento de datos del nivel de cálculo y garantizando la consistencia en las ejecuciones concurrentes.
¿Cuáles son las ventajas y desventajas en términos de rendimiento y coste de utilizar este patrón de API de datos sin servidor?
Esta arquitectura ofrece importantes ahorros de costes, ya que solo se paga por el almacenamiento y el tiempo de ejecución real, eliminando los gastos de servidores de base de datos inactivos. Si bien las "solicitudes cálidas" proporcionan respuestas excepcionalmente rápidas (a menudo en milisegundos), las "arrancadas frías" pueden introducir unos pocos segundos de latencia mientras se establece la conexión con el almacenamiento de objetos. Además, dado que la base de datos es solo de lectura y se actualiza mediante procesos por lotes, esta solución es ideal para paneles analíticos (OLAP) en lugar de sistemas transaccionales en tiempo real (OLTP) que requieren una actualización inmediata de los datos.