遥测的 Postgres 池吃 asyncpg 默认的 min_size=10,共享实例上建池即失败并永久降级 #15
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
现象
2026-08-23 一次满量运行的中途,某一路模型调用的遥测静默停止写入,一直持续到进程重启。日志里唯一的线索是这一行:
后果:那一路此后跑了 19 次调用、一行遥测都没落库,按用途汇总的成本因此少记约 $5。业务侧全程正常,库里看不出任何异常——是拿日志里的完成里程碑条数和
llm_calls的行数对了一次账才发现的。机制
读的是环境里装着的 1.1.2(
polygateway-1.1.2.dist-info)。四步:_build_telemetry(client.py:377-384)在每次from_settings里各 new 一个PostgresRecorder,所以每个GatewayClient一个池。_open_pool(postgres.py:144)调asyncpg.create_pool(self._dsn, timeout=10),不传min_size/max_size,吃 asyncpg 的默认值 10 / 10。min_size条连接连满(asyncpg/pool.py的_initialize:先await first_ch.connect(),再并发补齐剩下的min_size - 1条)。self._failed = True(postgres.py:150),此后_ensure_ready返回None、每次写入直接return;而aclose()(postgres.py:236-241)只清_schema_ready、不清_failed,所以只有进程重启才恢复。为什么它比业务连接更容易倒
业务侧的连接是一条一条按需要的——实例余量剩几条时,业务代码照样跑得动,它只要一条。
而这个池要么一次拿到 10 条、要么建池失败。所以余量紧张时,先倒下的必然是它。它是整条链路上最脆的一环,承担的却是最不该悄悄失败的职责之一。
规模
我们这边一个 worker 进程建 4 个
GatewayClient(4 个用途),就是 40 条常驻连接专用于写遥测;机器上有两条这样的外呼队列,两套环境并排跑,理论天花板 160 条。而遥测的实际写入量是每次外呼一行,需要的并发是个位数。
那台 PostgreSQL 是多个项目共用的,
max_connections=100。可能的方向(不主张任何一种,判断哪种合适是维护者的事)
第三条要谨慎:
postgres.py:147-149的注释写明永久降级是刻意的,理由是遥测在业务路径上await、每次重试都要内联吞掉一次 connect 超时。如果这条设计不变,那前两条的价值更大——把池子调小之后,「一次要 10 条」这个脆点本身就消失了,即使总连接数一条没少。版本
/PolyGateway的 1.2.4 checkout 我也对照读了一遍,上面这四点逐字未变(postgres.py:100 / 106 / 246-251、client.py:405-420),min_size/max_size在两个版本的整个包里一次都没出现过。已随 1.3.0 修复并合并入 main(feat/issue-15-telemetry-pool-lifecycle)。四层缺陷逐条落地: ① min_size=0 + PGW_TELEMETRY_PG_POOL_MAX(缺省 4)+ 每次写入硬预算 PGW_TELEMETRY_PG_WRITE_TIMEOUT_S(缺省 5.0s);② 判死判据从'哪一步失败'改挂'失败是什么性质',永久失能收窄到只剩 DSN 不可解析一类,其余 60s 冷却后自动重试;③ 进入/恢复各一条日志 + 降级期间节流复述 + client.telemetry_status 只读快照(含 dropped_rows);④ 全库统一'谁建的谁关'纪律,共享 recorder 路径打通。真实 PG 实测: 建完 recorder 0 条连接 → 一次写入后 1 条 → 20 行并发后 4 条 → aclose() 后回到 0。详见 CHANGELOG 1.3.0。