Optimización de la conexión

Completado

Las conexiones de base de datos son recursos costosos. Cada conexión consume memoria del servidor, requiere sobrecarga de autenticación y cuenta con límites de servidor. En el caso de las aplicaciones de inteligencia artificial que realizan consultas vectoriales frecuentes, la administración eficaz de conexiones es esencial para lograr un alto rendimiento sin agotar los recursos.

Nota:

En los ejemplos de código de esta unidad se muestran los patrones de administración de conexiones para Python (psycopg) y .NET (Npgsql). Estas bibliotecas se actualizan con frecuencia. Visite la documentación de psycopg y la documentación de Npgsql para obtener los detalles actuales de la API.

Sobrecarga de conexión

La creación de una nueva conexión de PostgreSQL implica varios pasos, cada uno de los cuales agrega latencia:

  1. Protocolo de enlace TCP: establecimiento de la conexión de red (normalmente 1-3 recorridos de ida y vuelta)
  2. Negociación TLS: El cifrado de la conexión (necesario para Azure Database for PostgreSQL)
  3. Autenticación: Comprobación de credenciales (intercambio de contraseñas o tokens)
  4. Asignación de procesos de servidor: PostgreSQL genera un proceso de back-end para cada conexión
  5. Inicialización de sesión: Establecimiento de parámetros de sesión y configuraciones de carga

Esta secuencia tarda entre 50 y 200 milisegundos en función de la latencia de red y la carga del servidor. Para un motor de recomendaciones que controla miles de solicitudes por segundo, la creación de nuevas conexiones por solicitud consumiría más tiempo en la configuración de conexión que en la ejecución de consultas real.

Azure Database for PostgreSQL limita las conexiones simultáneas en función del nivel de proceso. B1ms ampliable permite 50 conexiones, 2 núcleos virtuales de uso general permiten 859 conexiones, 4 núcleos virtuales de uso general permiten 1.718 conexiones, 4 núcleos virtuales optimizados para memoria permiten 3.437 conexiones y 16 núcleos virtuales optimizados para memoria permiten 5000 conexiones. Si se superan estos límites, se producen errores de conexión. Las aplicaciones que crean conexiones por solicitud pueden alcanzar rápidamente estos límites durante los picos de tráfico.

Agrupación de conexiones con PgBouncer

PgBouncer es un agrupador de conexiones ligero que se encuentra entre la aplicación y PostgreSQL. Mantiene un grupo de conexiones de base de datos y conexiones de cliente multiplexas entre ellas, lo que reduce drásticamente el número de conexiones de base de datos reales necesarias.

Azure Database for PostgreSQL incluye compatibilidad integrada con PgBouncer en los niveles de proceso de propósito general y optimizado para memoria. El nivel Ampliable no admite esta característica. Habilite PgBouncer a través de Azure Portal o la CLI. Una vez habilitado, conéctese a través del puerto 6432 (el puerto PgBouncer) en lugar de 5432 (el puerto directo de PostgreSQL). La cadena de conexión PgBouncer usa postgresql://user:password@myserver.postgres.database.azure.com:6432/mydb.

az postgres flexible-server parameter set \
    --resource-group myResourceGroup \
    --server-name myserver \
    --name pgbouncer.enabled \
    --value true

PgBouncer admite tres modos de agrupación, cada uno con diferentes ventajas. El modo de sesión significa que un cliente contiene una conexión de servidor para toda la sesión (hasta que se desconecte). Este modo admite todas las características de PostgreSQL, pero proporciona una reducción mínima de la conexión. El modo de transacción significa que un cliente contiene una conexión de servidor solo durante una transacción. Entre las transacciones, la conexión vuelve al grupo. Este modo funciona bien para la mayoría de las aplicaciones y reduce significativamente los requisitos de conexión. El modo de instrucción significa que un cliente obtiene una conexión solo para instrucciones individuales. Este modo proporciona una reducción máxima de conexiones, pero no admite transacciones de varias instrucciones. En el caso de las cargas de trabajo de búsqueda vectorial, el modo de transacción suele ser la mejor opción.

PgBouncer expone varios parámetros que controlan el comportamiento del grupo, los límites de conexión y el control de tiempo de espera. En el caso de las cargas de trabajo de búsqueda vectorial con patrones de tráfico de ráfaga, el ajuste de estos parámetros ayuda a equilibrar la disponibilidad de la conexión con el consumo de recursos. Configure pgbouncer.default_pool_size (de 20 a 50 en función de las necesidades de simultaneidad), pgbouncer.max_client_conn (5000+ para aplicaciones de alto tráfico), pgbouncer.pool_mode (transacción) y pgbouncer.query_wait_timeout (30-120 segundos).

El modo de transacción devuelve las conexiones al grupo después de que cada transacción se confirme o se revierta. Esto afecta a varias características de PostgreSQL. Las variables de sesión se restablecen entre transacciones, por lo que la configuración aplicada con SET no se conserva en las transacciones. Use SET LOCAL dentro de las transacciones o configure los valores predeterminados del lado servidor. Es posible que las instrucciones preparadas no funcionen porque las instrucciones preparadas con nombre están vinculadas a las conexiones. En el modo de transacción, es posible que una instrucción preparada creada en una transacción no esté disponible en la siguiente transacción si se asigna una conexión diferente. LISTEN/NOTIFY no funciona porque estas características requieren conexiones persistentes y no son compatibles con la agrupación de transacciones. En el caso de las aplicaciones de búsqueda vectorial, estas limitaciones rara vez son problemáticas, ya que las consultas suelen ser simples selecciones sin estado específico de la sesión.

Agrupación de conexiones de nivel de aplicación

Además de (o en lugar de) PgBouncer, la aplicación puede administrar grupos de conexiones directamente. Esto proporciona un mayor control sobre el ciclo de vida de la conexión y se integra con marcos de trabajo de aplicaciones.

El paquete psycopg_pool proporciona agrupación de conexiones para psycopg. Los grupos a nivel de aplicación te proporcionan control sobre el ciclo de vida de la conexión, el comportamiento del tiempo de espera en estado inactivo y la verificación del estado de salud. También se integran de forma natural con el control y el registro de errores de la aplicación. Cuando se combina con PgBouncer, los grupos de aplicaciones controlan la administración de conexiones locales, mientras que PgBouncer controla la multiplexación del lado servidor. El administrador de contexto with pool.connection() devuelve automáticamente la conexión al grupo cuando se cierra el bloque, incluso si se produce una excepción.

from psycopg_pool import ConnectionPool

# Create a connection pool
pool = ConnectionPool(
    conninfo="postgresql://user:password@myserver.postgres.database.azure.com:6432/mydb",
    min_size=5,      # Minimum connections to maintain
    max_size=20,     # Maximum connections allowed
    max_idle=300,    # Close idle connections after 5 minutes
    max_lifetime=3600  # Recycle connections after 1 hour
)

# Use connections from the pool
def search_similar_products(query_embedding, limit=10):
    with pool.connection() as conn:
        with conn.cursor() as cur:
            cur.execute("""
                SELECT id, name, embedding <=> %s AS distance
                FROM products
                ORDER BY embedding <=> %s
                LIMIT %s
            """, (query_embedding, query_embedding, limit))
            return cur.fetchall()

Npgsql incluye la agrupación de conexiones integrada habilitada de forma predeterminada, por lo que no necesita un paquete independiente. El grupo administra automáticamente la creación, reutilización y eliminación de conexiones en función de los parámetros que especifique en la cadena de conexión. Cada cadena de conexión única mantiene su propio grupo, por lo que las cadenas de conexión coherentes en toda la aplicación garantizan un uso eficaz del grupo. Cuando se llama a conn.Close() o se elimina la conexión, esta vuelve a la agrupación en lugar de ser destruida. Configure la agrupación mediante parámetros de cadena de conexión como Minimum Pool Size=5;Maximum Pool Size=20;Connection Idle Lifetime=300;Connection Lifetime=3600.

El tamaño del grupo afecta tanto al rendimiento como al consumo de recursos. Establecer el tamaño del grupo demasiado pequeño hace que las solicitudes esperen por las conexiones que se liberen, lo que aumenta la latencia. Si se establece en demasiado grande, se desperdicia memoria y se puede sobrecargar el servidor de bases de datos. El tamaño correcto depende de los patrones de tráfico, la duración de las consultas y el número de instancias de aplicación que comparten la base de datos. Mantenga el tamaño mínimo suficiente como para controlar el tráfico de línea base sin esperar. Limitar el tamaño máximo en lo que la base de datos puede controlar dividida por el número de instancias de aplicación. Si tiene 10 instancias de aplicación y la base de datos admite 1.000 conexiones, establezca el límite máximo en 100 por instancia (dejando un margen de seguridad). Recicle las conexiones periódicamente (cada 30-60 minutos) para mantener la salud, ya que las conexiones de larga duración pueden acumular pérdidas de memoria o contener planes de caché obsoletos.

Administración de sesiones para cargas de trabajo de IA

Algunas consultas vectoriales se benefician de la configuración de nivel de sesión que asigna más recursos a la consulta que los valores predeterminados de todo el servidor permiten.

Las consultas de similitud de vectores que ordenan conjuntos de resultados grandes se benefician del aumento de work_mem. Establézcalo para sesiones o transacciones específicas mediante SET LOCAL work_mem = '256MB'. SET LOCAL solo se aplica dentro de la transacción actual. Cuando finaliza la transacción, la configuración se revierte al valor predeterminado, que es seguro para las conexiones agrupadas.

Ajuste hnsw.ef_search o ivfflat.probes para consultas con requisitos de precisión diferentes. Se usa SET LOCAL hnsw.ef_search = 200 para una recuperación mayor en las consultas en las que la precisión es crítica o SET LOCAL hnsw.ef_search = 20 para consultas más rápidas en las que los resultados aproximados son aceptables. Este patrón permite equilibrar la precisión y la velocidad en función del caso de uso específico sin afectar a otras consultas.

Patrones de uso eficientes del SDK

Más allá de la administración de conexiones, la forma de estructurar las interacciones de la base de datos afecta al rendimiento.

Los recorridos de ida y vuelta de red agregan latencia a cada operación de base de datos. Cuando necesite varios fragmentos de datos, capturarlos en una sola consulta elimina la sobrecarga por consulta de la transmisión de red, el análisis de consultas y la serialización de resultados. En el caso de las aplicaciones de inteligencia artificial que recuperan incrustaciones para varios elementos, el procesamiento por lotes puede reducir la latencia total de cientos de milisegundos a dígitos únicos. En lugar de realizar varios recorridos de ida y vuelta con consultas individuales, use una sola consulta con WHERE id = ANY(%s) y pase una lista de identificadores.

Para cargar un gran número de vectores, el comando PostgreSQL COPY es considerablemente más rápido que las instrucciones individuales INSERT . COPY envía datos directamente a la tabla en formato binario o de texto, evitando la sobrecarga de analizar instrucciones SQL individuales. Al cargar datos de incrustación desde canalizaciones de procesamiento por lotes o migraciones de datos iniciales, COPY puede reducir los tiempos de carga de horas a minutos. COPY puede cargar cientos de miles de filas por segundo, mientras que las inserciones individuales están limitadas a miles por segundo.

Cuando la aplicación puede paralelizar el trabajo, las operaciones de base de datos asincrónicas mejoran el rendimiento ejecutando varias consultas simultáneamente sin bloquear subprocesos. Este patrón es útil para las cargas de trabajo de INTELIGENCIA ARTIFICIAL que necesitan buscar varias colecciones de vectores simultáneamente o combinar la búsqueda de vectores con otra recuperación de datos. Los grupos asincrónicos administran las conexiones de forma eficaz entre operaciones simultáneas, a la vez que respetan los límites de tamaño del grupo. Usa AsyncConnectionPool de psycopg_pool y asyncio.gather para ejecutar múltiples búsquedas de manera concurrente.

Resistencia de la conexión

Los problemas de red, los reinicios del servidor y las conmutaciones por error pueden interrumpir las conexiones de base de datos. Las aplicaciones robustas manejan estas situaciones con elegancia.

Los errores transitorios, como los fallos de red, los reinicios de conexión y la breve indisponibilidad del servidor durante el mantenimiento, son inevitables en entornos en la nube. La implementación de la lógica de reintento con retroceso exponencial ayuda a la aplicación a recuperarse correctamente de estos problemas temporales sin sobrecargar el servidor con reintentos inmediatos. Agregue vibración aleatoria para evitar que varias instancias de aplicación vuelvan a intentarlo simultáneamente. Capture OperationalError excepciones, calcule el tiempo de espera como (2 ** attempt) + random.uniform(0, 1) y vuelva a intentarlo hasta un número máximo de intentos.

Los tiempos de espera impiden que la aplicación espere indefinidamente cuando la base de datos sea lenta o inaccesible. Los tiempos de espera de conexión limitan el tiempo de espera al establecer nuevas conexiones, mientras que los tiempos de espera de instrucciones limitan el tiempo de ejecución de las consultas. En el caso de las aplicaciones de búsqueda vectorial, elija tiempos de espera que admita las consultas legítimas más lentas, al tiempo que produce un error rápido en las consultas que superan la latencia aceptable. Configure los tiempos de espera en la cadena de conexión mediante parámetros como connect_timeout=10 y options=-c statement_timeout=30000. En el caso de las consultas vectoriales, establezca tiempos de espera de instrucciones que sean adecuados para las consultas aceptables más lentas. Un tiempo de espera de 30 segundos es razonable para búsquedas vectoriales complejas; las aplicaciones interactivas pueden usar valores inferiores.

Cuando todas las conexiones de grupo están en uso y llegan nuevas solicitudes, el grupo debe poner en cola las solicitudes (agregar latencia) o rechazarlas inmediatamente. Ninguna opción es ideal, por lo que la utilización del grupo de supervisión le ayuda a escalar antes de que el agotamiento sea frecuente. Cuando se produce el agotamiento, enviar un mensaje de error claro ayuda a los clientes a implementar su propia lógica de reintento en lugar de que se supere el tiempo de espera de manera imprevisible. Controle las excepciones PoolTimeout devolviendo un error estable, como {"error": "Service temporarily busy, please retry"}. Supervise el uso y la escala del grupo si el agotamiento se produce con frecuencia.

Recursos adicionales