连接池让许多应用请求在不同时间借用较少的 PostgreSQL 会话。复用能够摊薄建立连接的成本,也能限制同时借出的数量,但不会让已经过载的数据库自动变快。池本身又增加了一条队列,它的容量、等待时间和取消行为,都属于请求预算的一部分。应把这条队列作为服务行为来设计,而不是把它隐藏在一个“复用更快”的标签后面。
坏掉的样子 数据库后端处于 ClientRead,表示正在等待客户端发数据,并不表示应用请求正在等待借用连接。真正的池内等待者可能还没有数据库连接,因此服务器根本没有对应的后端可供观察。获取连接的等待应在池里测量,查询执行和锁等待应在服务器测量;把它们混成一个所谓数据库池等待事件,会掩盖实际瓶颈的位置。
归还连接也不保证抹掉所有会话状态。Psycopg 的连接上下文在正常退出时提交,在异常离开上下文时回滚;额外重置取决于配置。已经提交的会话级设置,可以继续留给下一个借用者,即使没有泄漏未结束的事务。临时对象和其他会话资源同样需要明确策略,不能因为使用了连接池,就假定所有内容都会自动恢复到初始状态。
快速验证 准备 Python 3.10 或更新版本、PostgreSQL 18 可丢弃数据库,以及允许创建实验模式的角色。原演练使用以下依赖;本轮审阅没有重新执行并发数据库实验。安装方式为:python3 -m pip install "psycopg[binary]==3.3.5" "psycopg-pool==3.3.1"。使用已有认证配置,将 PG_DSN 设置为该测试数据库的连接串。把程序保存为 pool-demo.py,在安装依赖的同一个 Python 环境中运行:
import os import time from concurrent.futures import ThreadPoolExecutor import psycopg from psycopg_pool import ConnectionPool, PoolTimeout, TooManyRequests dsn = os.environ['PG_DSN'] with psycopg.connect(dsn, autocommit=True) as admin: admin.execute('CREATE SCHEMA pool_demo') try: admin.execute('CREATE TABLE pool_demo.counter (n integer)') admin.execute('INSERT INTO pool_demo.counter VALUES (0)') with ConnectionPool( dsn, min_size=1, max_size=1, max_waiting=1, timeout=3, open=True, kwargs={'application_name': 'pool_example'}, ) as pool: pool.wait(timeout=3) def queued_request(): try: with pool.connection(timeout=1): raise AssertionError('holder should still own the only connection') except PoolTimeout: return 'PoolTimeout' with ThreadPoolExecutor(max_workers=1) as workers: with pool.connection() as held: original_pid = held.info.backend_pid waiting = workers.submit(queued_request) deadline = time.monotonic() + 0.5 while pool.get_stats()['requests_waiting'] != 1: if time.monotonic() >= deadline: raise TimeoutError('request did not enter the pool queue') time.sleep(0.005) state = admin.execute( 'SELECT state,wait_event_type,wait_event ' 'FROM pg_stat_activity WHERE pid=%s', (original_pid,) ).fetchone() assert state == ('idle', 'Client', 'ClientRead'), state print('waiting_in_pool', 1, 'server', state) try: with pool.connection(timeout=0.1): raise AssertionError('queue limit was not applied') except TooManyRequests: print('queue_limit', 'TooManyRequests') assert waiting.result(timeout=2) == 'PoolTimeout' print('queued_request', 'PoolTimeout') with pool.connection() as conn: assert conn.info.backend_pid == original_pid conn.execute("SET application_name='previous_borrower'") try: with pool.connection() as conn: conn.execute('UPDATE pool_demo.counter SET n=99') raise ValueError('simulated request failure') except ValueError as error: assert str(error) == 'simulated request failure' with pool.connection() as conn: value = conn.execute('SELECT n FROM pool_demo.counter').fetchone()[0] setting = conn.execute('SHOW application_name').fetchone()[0] assert value == 0 and setting == 'previous_borrower' assert conn.info.backend_pid == original_pid print('same_backend', True, 'rollback_value', value, 'retained_setting', setting) conn.execute('RESET application_name') finally: admin.execute('DROP SCHEMA pool_demo CASCADE') 池里只有一条连接,最多允许一个请求排队。主线程借走连接但不执行查询,工作线程尝试借用它。池报告一个等待者以后,独立观察连接看到被借走的数据库后端正空闲等待客户端读取事件。两种状态可以同时存在:应用已经排队,数据库会话却没有正在执行的查询。这正好说明为什么不能用服务器的空闲状态代替池的等待指标。
...