连接优化

已完成

数据库连接成本高昂。 每个连接都消耗服务器内存,需要身份验证开销,并计入服务器限制。 对于进行频繁矢量查询的 AI 应用程序,高效连接管理对于在不耗尽资源的情况下实现高吞吐量至关重要。

注释

本单元中的代码示例演示 Python(psycopg)和 .NET(Npgsql)的连接管理模式。 这些库经常更新。 有关当前的 API 详细信息,请访问 psycopg 文档 和 Npgsql 文档 。

连接开销

创建新的 PostgreSQL 连接涉及多个步骤,每个步骤都会添加延迟:

  1. TCP 握手: 建立网络连接(通常为 1-3 次往返)
  2. TLS 协商:加密连接(Azure Database for PostgreSQL 必需)
  3. 认证: 验证凭据(密码或令牌交换)
  4. 服务器进程分配: PostgreSQL 生成每个连接的后端进程
  5. 会话初始化: 设置会话参数和加载配置

此序列需要 50-200 毫秒,具体取决于网络延迟和服务器负载。 对于每秒处理数千个请求的建议引擎,每个请求创建新连接所需的时间比实际查询执行时间要多。

Azure Database for PostgreSQL 基于计算层限制并发连接。 弹性 B1ms 允许 50 个连接,通用型 2 vCore 允许 859 个连接,通用型 4 vCore 允许 1,718 个连接,内存优化型 4 vCore 允许 3,437 个连接,内存优化型 16 vCore 允许 5,000 个连接。 超过这些限制会导致连接失败。 每请求创建连接的应用在流量高峰时可能迅速触及这些限制。

使用 PgBouncer 建立连接池

PgBouncer 是一种轻型连接池器,位于应用程序和 PostgreSQL 之间。 它维护一个数据库连接池,并在其间复用客户端连接,大幅减少实际所需的数据库连接数。

Azure Database for PostgreSQL 在通用和内存优化计算层中内置 PgBouncer 支持。 弹性层不支持此功能。 通过 Azure 门户或 CLI 启用 PgBouncer。 启用后,通过端口 6432(PgBouncer 端口)而不是 5432(直接 PostgreSQL 端口)进行连接。 PgBouncer 连接字符串使用 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 支持三种连接池模式,每种模式有不同权衡。 会话模式表示客户端保留整个会话的服务器连接(直到断开连接)。 此模式支持所有 PostgreSQL 功能,但仅减少少量连接。 事务模式表示客户端仅在事务期间保留服务器连接。 事务之间,连接返回连接池。 此模式适用于大多数应用程序,并显著减少连接要求。 语句模式表示客户端仅针对单个语句获取连接。 此模式最大限度减少连接数,但不支持多语句事务。 对于矢量搜索工作负载,事务模式通常是最佳选择。

PgBouncer 公开多个参数,用于控制池行为、连接限制和超时处理。 对于具有突发流量模式的矢量搜索工作负荷,优化这些参数有助于平衡连接可用性与资源消耗。 根据并发需求配置 pgbouncer.default_pool_size (20-50),(对于高流量应用程序为 5000+)、 pgbouncer.max_client_connpgbouncer.pool_mode (事务)和 pgbouncer.query_wait_timeout (30-120 秒)。

事务模式在每个事务提交或回滚后将连接返回池中。 这会影响多个 PostgreSQL 功能。 会话变量会在事务之间重置,所以通过 SET 所应用的设置无法在不同的事务中保持不变。 在事务中使用 SET LOCAL 或在服务器端配置默认值。 命名预处理语句可能无法使用,因为它们与连接绑定。 在事务模式下,在一个事务中创建的预处理语句,如果下一次分配了不同连接,可能无法使用。 LISTEN/NOTIFY 不起作用,因为这些功能需要持久连接,并且与事务池不兼容。 对于矢量搜索应用程序,这些限制很少存在问题,因为查询通常是简单的选择,没有特定于会话的状态。

应用程序级连接池

除了 (或而不是) PgBouncer 之外,应用程序还可以直接管理连接池。 这可以更好地控制连接生命周期并与应用程序框架集成。

包 psycopg_pool 为 psycopg 提供连接池。 应用级连接池让你控制连接生命周期、空闲超时行为和健康检查。 它们还会自然地与应用程序的错误处理和日志记录集成。 与 PgBouncer 结合使用时,应用程序池处理本地连接管理,而 PgBouncer 处理服务器端多路复用。 with pool.connection() 上下文管理器在块退出时自动将连接返回池中,即使发生异常。

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 默认启用内置连接池,因此不需要单独的包。 连接池会根据你在连接字符串中指定的参数自动管理连接的创建、重用和释放。 每个唯一的连接字符串都维护自己的池,因此在整个应用程序中保持一致的连接字符串可确保有效的池利用率。 调用 conn.Close() 时或当连接被释放时,它会返回到池中,而不是被销毁。 通过连接字符串参数配置池,例如 Minimum Pool Size=5;Maximum Pool Size=20;Connection Idle Lifetime=300;Connection Lifetime=3600。

池大小会影响性能和资源消耗。 设置池太小会导致请求等待可用连接,从而增加延迟。 设置过大浪费内存并可能压垮数据库服务器。 正确的大小取决于流量模式、查询持续时间和共享数据库的应用程序实例数。 保持最小规模足够大,以便无需等待即可处理基线流量。 数据库可以处理的最大大小上限,除以应用程序实例数。 如果有 10 个应用程序实例,并且数据库支持 1,000 个连接,请将每个实例的最大连接数设置为 100 个,以留有余地。 定期(每 30-60 分钟)回收连接以保持运行状况,因为长期连接可能会累积内存泄漏或保留过时的缓存计划。

人工智能工作负载的会话管理

某些向量查询受益于会话级别设置,这些设置向查询分配的资源比服务器范围的默认值允许的更多。

对大型结果集进行排序的矢量相似性查询通过增加work_mem而受益。 使用 SET LOCAL work_mem = '256MB' 为特定会话或事务设置它。 SET LOCAL 仅适用于当前事务。 事务结束时,该设置将还原为默认设置,这对于共用连接是安全的。

调整 hnsw.ef_search 或 ivfflat.probes 以适应不同准确度要求的查询。 请在需要更高召回率且查询准确性至关重要时使用 SET LOCAL hnsw.ef_search = 200,或者在可接受近似结果且需要更快查询时使用 SET LOCAL hnsw.ef_search = 20。 此模式使你可以根据特定的用例平衡准确性和速度,而不会影响其他查询。

高效的 SDK 使用模式

除了连接管理之外,构建数据库交互的方式会影响性能。

网络往返会增加每个数据库操作的延迟。 当需要多个数据段时,在单个查询中获取数据能够消除网络传输、查询解析和结果序列化的每个查询的开销。 对于检索多个项嵌入的 AI 应用程序,批处理可以减少从数百毫秒到单个数字的总延迟。 不要用多个单独查询进行多次往返,而应使用 WHERE id = ANY(%s) 单条查询并传递 ID 列表。

对于加载大量向量,PostgreSQL COPY 命令比单个 INSERT 语句快得多。 COPY 以二进制或文本格式将数据直接流式传输到表中,从而绕过分析单个 SQL 语句的开销。 从批处理管道或初始数据迁移加载嵌入数据时, COPY 可将加载时间从小时减少到几分钟。 COPY 可以每秒加载数十万行,而单个插入限制为每秒数千行。

当应用程序可以并行化工作时,异步数据库作可以通过并发执行多个查询来提高吞吐量,而无需阻塞线程。 此模式对于需要同时搜索多个矢量集合或将矢量搜索与其他数据检索相结合的 AI 工作负载非常有用。 异步连接池在遵循池大小限制的同时,有效地管理并发操作中的连接。 使用 psycopg_pool 的 AsyncConnectionPool 和 asyncio.gather 并行执行多次搜索。

连接复原能力

网络问题、服务器重启和故障转移可能会中断数据库连接。 可靠的应用程序可正常处理这些作。

在云环境中,在维护期间,暂时性故障(例如网络故障、连接重置和短暂的服务器不可用)是不可避免的。 使用指数退避实现重试逻辑有助于应用程序从这些临时问题中正常恢复,而无需让服务器立即重试。 添加随机抖动以防止多个应用程序实例同时重试。 捕获 OperationalError 异常,将等待时间计算为 (2 ** attempt) + random.uniform(0, 1),并重试至最大尝试次数。

超时可防止应用程序在数据库速度缓慢或无法访问时无限期等待。 连接超时限制建立新连接时等待的时间,而语句超时限制查询执行时间。 对于向量搜索应用,应选择能够容纳最慢合法查询的超时,同时对超出可接受延迟的查询快速失败。 使用类似 connect_timeout=10 和 options=-c statement_timeout=30000. 的参数在连接字符串中配置超时。 对于矢量查询,请设置语句超时以满足您对查询速度的最低要求。 对于复杂的矢量搜索,30 秒超时是合理的;交互式应用程序可能使用较低的值。

当所有连接池连接被占用且有新请求到达时,连接池必须排队请求(增加延迟)或立即拒绝。 两种选择都不理想,因此监控连接池使用情况有助于在频繁耗尽之前扩展。 发生耗尽时,返回明确的错误信息有助于客户端实现自己的重试逻辑,而不是不可预测地超时。 通过返回优雅错误(如 {"error": "Service temporarily busy, please retry"})来处理 PoolTimeout 异常。 监控连接池使用情况,并在频繁耗尽时进行扩展。

其他资源