SQL 采集的总时限与客户端取消
状态: 已随 v1.6.0 发布。
决策/最后核验: 2026-11-09。
范围: main 上的 v1.6.0,源码6bee809,解决 #116,未随 v1.5.0 发布。
交付: v1.6.0 标签指向6bee809;GitHub Release、带版本号的压缩包、RPM/DEB 和校验和已发布。源码 CI、制品验收与 amd64/arm64 容器 TLS 验证通过。软件发布不等于生产部署或生产负载验证。
决策
此前查询从 context.Background() 派生 context,指标客户端断开后工作仍可能继续,后续 HTTP 请求也可能无限等待 exporter 锁。本次限制这些工作,同时保留失败隔离与缓存复用。
最多接纳两个指标请求,涵盖采集、等待及响应写入。一次只执行一轮采集,第二个抓取者可以在剩余请求预算内等待同一把采集锁,更多请求返回 HTTP 503。等待可以取消,不启动额外 goroutine,也不执行 SQL。这样支持双 Prometheus 抓取,且不引入无界队列或共享任务的取消归属问题。
请求预算沿用现有 30 秒 HTTP 写入时限,合法、有限且为正数的 X-Prometheus-Scrape-Timeout-Seconds 请求头可以缩短它;排队也消耗同一预算。预检继承请求 context;采集在每个数据库和采集器开始前检查 context,过期后不再启动新的采集器。已开始执行且设有 timeout 的采集器,其谓词与主查询共用同一时限,即使请求结束也会执行完毕,结果照常缓存供后续抓取使用。负数 timeout 取消单查询期限,因此只有这类查询会随请求一起取消。
沿用原有失败缓存策略:刷新失败(包括被取消的无上限查询)时,清空该采集器结果,并按正常 TTL 记录此次尝试,下个请求不会立即重跑。客户端离开不会把健康的有界刷新变成失败:fatal 采集器不会向其他抓取者报告 pg_up 0,TTL 为 60 或 300 秒的容量与膨胀采集器也不会在整个 TTL 内缺失。例外是 timeout 为负数的 fatal 采集器:取消它会在 TTL 内缓存 fatal 失败,因此 fatal 采集器应保留 timeout。因客户端离开或请求预算耗尽而中断的采集轮次的汇总日志为 INFO,不计入 pg_exporter_scrape_error_count 与 pg_exporter_server_scrape_error_count,这两个计数继续只统计连接错误与 fatal 采集器错误。其他采集器的缓存保持有效。不发布中断采集器的部分结果,也不把过期值悄悄当作新数据返回。TTL 为零或 --disable-cache 仍表示每次抓取都刷新。因此较长 TTL 也会延迟失败后的恢复,这是已有权衡,不增加重试调度器。
采集形成响应快照后,在编码和网络写入前释放采集锁。请求直到响应结束仍占用接纳名额,从而同时限制等待和慢响应。保留 Prometheus 编码、压缩、Go/进程指标及 HTTP 自监控;标准采集器接口继续可用,不在 exporter 上保存可变请求 context。
备选方案与边界
一律立即拒绝重叠请求会给双抓取者制造不必要的缺口;取消后清零重试时间会反复运行昂贵查询并重建连接,评审后放弃这两个方案。同样放弃随请求取消有界查询:一个客户端断开会变成所有抓取者共享的失败缓存,而且 lib/pq 每次取消都会关闭连接。让正在执行的采集器执行完毕,代价通常是预算过期后最多再占用采集锁该采集器自身的 timeout(默认配置最长 2 秒)。也放弃在同一采集器的谓词与主查询之间中止:那需要单独的“未执行”状态和回滚,省下的工作仍受同一时限约束。无限返回旧结果会掩盖数据陈旧,需要单独的新鲜度契约。本次不增加后台刷新、共享 gather、并行调度、新参数,也不迁移驱动。
预算过期会停止采集并取消无上限 SQL。正在执行的有界采集器可能让响应超出预算,通常最多为其自身 timeout。网络阻塞时驱动可能占用更久,v1.5.0 亦然,因此预算与查询 timeout 都不是硬性的墙钟上限,也不预留固定编码余量;HTTP 写入期限仍是传输层限制。尤其是 lib/pq 建连握手需要合适的 DSN connect_timeout,仅有查询 context 无法约束每个网络阶段。独立健康探测见目标状态记录。
错误计数边界
中断轮次不增加 pg_exporter_scrape_error_count 和 pg_exporter_server_scrape_error_count,轮次汇总日志为 INFO interrupted;查询错误计数与 fatal 查询的 ERROR 日志仍然保留。因此,恰好与请求取消重叠的真实失败可能不计入这两个轮次计数。请求没有取消时,连接/预检失败和 fatal 查询失败(包括自身超时)仍按错误计数;非 fatal 失败仍体现在查询级计数中。
验收
验证连续取消不会绕过 TTL,健康采集器及有效缓存仍可用,并在 TTL 过期后恢复尝试。覆盖双抓取重叠、取消等待者、接纳名额已满、取消无上限 SQL、有界 SQL 执行中客户端离开后结果仍须缓存、中断的采集轮次不得增加错误计数,以及慢响应不能占用采集锁。真实 PostgreSQL 须确认取消、SQL 尝试次数受限及后续抓取成功,另行验证 PgBouncer。本地验收不等于发布或生产负载保证。
本地验证
完整 race 测试与重点并发用例连续十轮通过。在 PostgreSQL 18.6 上,被取消的刷新在 TTL 内只执行一次;后续六次请求返回健康指标,没有重复失败 SQL,TTL 过期后可以成功重试。五轮双抓取均返回 HTTP 200,每轮只刷新一次 SQL。测试还覆盖取消等待者、接纳名额耗尽、部分行、谓词/预检取消,以及慢响应结束前释放采集锁。本次未增加后台刷新或旧值回退。
上述取消实测早于一项评审调整,该调整只影响 timeout 为正数的查询。调整后在 PostgreSQL 18.6 上,0.8 秒的有界查询执行期间客户端离开或 0.3 秒预算到期,查询都会执行完毕;预算到期的请求在 0.8 秒后返回 503,下一次抓取直接复用结果,不再执行。3 秒的无上限查询在客户端于 1.5 秒离开时被 PostgreSQL 取消(SQLSTATE 57014),没有残留活跃后端,并在 TTL 内保持缺失。调整前,同一有界查询会被取消,并在整个 TTL 内缺失。回归测试确认,fatal 采集器执行中客户端离开,下一个抓取者不再看到 pg_up 0。
第二轮评审发现,取消后正在执行的采集器还会继续执行其余谓词与主查询,并且中断的采集轮次会使两个错误计数增加、记录 error 日志。上文契约已改为按采集器描述这一行为,中断轮次也不再计数。在 PostgreSQL 18.6 上,0.8 秒的有界 fatal 查询执行中客户端离开,此前两个计数均为 1 并有 error 日志;修正后均保持 0,只记 info 日志,下一次抓取返回 pg_up 1 与缓存指标。回归测试改用显式同步而非 sleep,并确认真实连接失败仍会计数;完整测试在 race 检测下连续 20 轮通过。