Cuando una aplicación, sea web o API, empieza a crecer en su uso, siempre hay que tener en cuenta la cantidad de llamadas y conexiones, especialmente hacia los datos. Hay muchas formas de minimizar el impacto en el servidor, pero un factor crítico son las transacciones con las bases de datos, especialmente en sistemas donde se realizan múltiples llamadas por método.
Para este ejemplo usaremos PostgreSQL. Aunque el estándar web suele asociarse a MariaDB, prefiero PostgreSQL por ser más robusto y escalable, a pesar de que en ciertos escenarios de transacciones web simples no siempre iguale la velocidad de opciones más ligeras.
1. Forma individual
La forma común de conectarse a la base de datos en Flask es abrir y cerrar la conexión física en cada petición. Esto es: se hace la llamada, se abre una conexión, se realiza la transacción y se cierra.
import psycopg2
from config import config
# Conexión normal
def conn():
try:
params = config()
conexion = psycopg2.connect(
host=params['hostname'],
database=params['database'],
user=params['username'],
password=params['password']
)
return conexion
except Exception as e:
print(f"No se pudo conectar: {e}")
# Llamada común a la base de datos
def get_empresa():
conexion = conn()
query = "SELECT id, nombre FROM public.empresa;"
try:
cursor = conexion.cursor()
cursor.execute(query)
data = cursor.fetchall()
return data
except psycopg2.DatabaseError as e:
print(e)
finally:
if conexion:
conexion.close()
En esta modalidad, se abre una conexión física por cada consulta. Para aplicaciones pequeñas es normal; sin embargo, siempre ocupa un hilo por usuario. Si tenemos 100 usuarios simultáneos, son 100 conexiones físicas negociándose, lo que aumenta significativamente el uso de CPU y memoria.
El problema de las múltiples llamadas
Un controlador típico en Flask puede realizar múltiples llamados a la base de datos para renderizar una sola vista:
@user.route('/usuarios')
@login_required
def usuarios():
# Solo aquí ya hay 4 posibles aperturas de conexión física
menu = DBUtils.get_menu(3)
perfil = usuarioModel.get_perfil()
empresas = usuarioModel.get_empresa()
lista = usuarioModel.get_lista()
if menu is None:
return redirect(url_for('auth.login'))
DBUtils.set_history(session['usuario'][0], 'registro_usuario', 'Ingreso al módulo de usuarios')
return render_template('usuario/usuario.html', **menu, perfiles=perfil, empresas=empresas, lista=lista)
Con 100 usuarios, esto se traduce en 400 llamadas de apertura y cierre. Los recursos de hardware podrían empezar a colapsar y el servidor empezaría a cerrar conexiones para "protegerse".
2. Forma de Pools (Agrupamiento)
Un pool de conexiones es un conjunto de conexiones abiertas que están "vivas" y en espera.
Hagamos una analogía con un restaurante. Si en cada petición tuviéramos que fabricar la mesa, tomar el pedido y luego desarmar la mesa, sería una pérdida de tiempo. Es mejor dejar varias mesas armadas para que el cliente simplemente se siente y sea atendido.
El pool organiza los hilos y los cede a cada usuario. Si defino 20 hilos para 20 usuarios y llega el número 21, este esperará un tiempo mínimo a que uno de los hilos sea liberado. Con esto garantizas que todos consuman la data y que el servidor no explote.
3. Creación de la conexión de Pools
Utilizaremos ThreadedConnectionPool de la librería psycopg2:
import psycopg2
from psycopg2 import pool
from config import DBconfig
class DBConn:
__pool = None
@classmethod
def initialize(cls):
if cls.__pool is None:
try:
params = DBconfig()
# Definimos un mínimo de 1 y un máximo de 20 conexiones
cls.__pool = psycopg2.pool.ThreadedConnectionPool(
1, 20,
host=params['HOSTNAME'],
database=params['DATABASE'],
user=params['USERNAME'],
password=params['PASSWORD']
)
print("Pool de conexiones creado exitosamente")
except Exception as e:
print(f"Error al crear el pool de conexiones: {str(e)}")
@classmethod
def conexion(cls):
if cls.__pool is None:
cls.initialize()
return cls.__pool.getconn()
@classmethod
def liberar_conexion(cls, conexion):
if cls.__pool is not None:
cls.__pool.putconn(conexion)
@classmethod
def cerrar_pool(cls):
if cls.__pool is not None:
cls.__pool.closeall()
print("Pool de conexiones cerrado")
Implementación en el modelo
Lo único que cambia es el método de obtención y cierre de la conexión:
def get_empresa():
conexion = DBConn.conexion()
query = "SELECT id, nombre FROM public.empresa;"
try:
cursor = conexion.cursor()
cursor.execute(query)
data = cursor.fetchall()
return data
except psycopg2.DatabaseError as e:
print(f"Error: {e}")
finally:
# Liberamos la conexión al pool sin cerrarla físicamente
DBConn.liberar_conexion(conexion)
Conclusión
Mantenemos los hilos en espera (sleep) hasta que aparezca otra petición. Esto permite que el hardware mantenga una estabilidad de CPU y RAM sin súbitos consumo de recursos.
Aunque existen herramientas ORM como SQLAlchemy que ya manejan esto automáticamente, entender cómo funciona "bajo el capó" es una excelente práctica para optimizar el rendimiento de cualquier backend profesional.
Top comments (0)