深入 pgwatch 源码Reaper 采集架构与 400 个数据源故障不停机的韧性设计【免费下载链接】pgwatchpgwatch: PostgreSQL metrics monitor/dashboard项目地址: https://gitcode.com/gh_mirrors/pg/pgwatchpgwatch 是一款开源的 PostgreSQL 监控与仪表盘工具PostgreSQL metrics monitor/dashboard它的核心采集引擎名为Reaper负责从成百上千个数据源拉取指标并写入存储端。本文带你深入 pgwatch 源码看看 Reaper 如何用三层韧性设计让 400 个数据源在网络故障时不停机、不互相拖累、并自动恢复。一个真实的故障场景400 个库同时失联pgwatch 曾在生产环境中遇到这样的案例一台 pgwatch 主机托管着约400 个PostgreSQL 数据源每月会出现 1~2 次采集全面停摆——所有源的指标同时中断只有重启服务才能恢复。调查发现两个层面触发因素环境问题pgwatch 主机的 DNS 解析器或网络出现短暂抖动brownout导致所有源同时出现 DNS 超时、拨号超时、TLS 超时放大因素pgwatch 自身无超时的数据库请求让半开 TCP 连接占住连接池长达 15~30 分钟主循环顺序扫描各数据源一个源挂住就拖垮后面所有源发现型数据源一旦解析失败就被整体拆除重建。于是项目制定了 spec/design-source-failure-resilience.md 设计文档拆成三个可独立上线的工作流WS1~WS3把全局停摆改造成有界、可自愈、按源隔离的降级。先看全局Reaper 采集架构长什么样Reaper 位于 internal/reaper/是一个主循环 每源一个 worker的经典架构主循环reaper.Reap()每隔刷新周期默认 120 秒醒来重新加载数据源与指标定义然后对每个源执行连接 → 拉取运行时信息 → 启动/维持采集 worker每源 workerDbConnReaper每个数据源只有一个 goroutine按指标间隔的**最大公约数GCD**对齐节拍用pgx.Batch把多条 SQL 合并成一次网络往返批量执行统一写入管道worker 把测量值丢进 256 缓冲的 channel由WriteMeasurements()单线程串行写入 sinks天然避免写并发。韧性设计一给每一次数据库往返都装上闹钟WS1网络故障中最阴险的是半开连接TCP 已建立但对端或链路静默丢包客户端的写操作会一直重传直到内核放弃Linux 默认约 15~30 分钟。如果采集请求没有客户端超时一个 worker 就会在某个源上卡死几十分钟。WS1 的解法朴素但彻底所有数据库操作都必须运行在带 deadline 的派生 context 下。这些默认值集中在 internal/db/deadlines.go 一个文件里非常好读场景默认超时说明指标批量采集max(指标间隔, 30s)短间隔指标也有可用窗口单指标降级重试同上按各自指标间隔计算变更检测查询60sDetect*Changes家族运行时信息版本/平台/大小30s每个子查询独立计时主循环 Ping 门禁连接超时 5s默认 10s覆盖池排队场景Postgres 连续发现解析15s含建池 发现查询关键细节超时触发后pgx 会丢弃该连接、下次重新拨号——这正是把数小时停摆压缩成错过一个采集周期的自愈原语每个超时都带可 grep 的 cause 字符串如batch deadline、ping deadline方便在日志里定位是哪一类操作超时。效果坏连接被客户端主动杀掉worker 记录错误后继续下一个节拍而不是无限等待。韧性设计二有界并行扫描一个源挂住不拖累 399 个WS2如果 Ping 最多卡 10 秒400 个源顺序扫描在一次全网抖动中仍需 400 × 10s ≈ 1 小时才能扫完。WS2 把主循环的逐源处理改成有界并行var g errgroup.Group g.SetLimit(32) // 固定上限不随数据源数量增长 for _, monitoredSource : range r.monitoredSources { g.Go(func() error { /* 连接、Ping、启动 worker */ }) } _ g.Wait() // 屏障清理阶段在全部源处理完后顺序执行这段逻辑就在 reaper.Reap()几个设计取舍值得注意并发上限固定为 32常量 maxConcurrentSourceConnects刻意不随集群规模放大避免 400 个源同时拨号引发 DNS/重连风暴——那正是本次要对抗的故障模式单个源失败只记录日志并跳过不取消兄弟 goroutine错误绝不向上传播清理保持顺序执行CleanupRemovedWorkers在所有源处理完之后才运行保证 worker 生命周期操作无竞态cancelFuncs等共享状态用互斥锁保护。最坏情况从400 × 单源超时降为ceil(400/32) × 单源超时——故障影响面被算术式地压小了。韧性设计三最后已知良好缓存发现失败不拆监控WS3pgwatch 支持postgres-continuous-discovery这类发现型数据源运行时查询pg_database动态枚举要监控的库。旧行为是——发现查询一次失败哪怕只是 DNS 抖动或一条权限报错整个源的 worker 就被拆除、连接池关闭、sinks 收到删除操作等下次刷新再重建。400 个源的场景下这就是灾难放大器。WS3 在 internal/sources/resolver.go 引入最后已知良好缓存lastFoundDatabases解析成功 → 用新列表替换缓存解析失败但缓存非空 →直接返回缓存列表并返回 nil 错误只打一条 Warning 日志主循环因此不会拆 worker、不会误写instance_up0缓存键包含名称 连接串 包含/排除模式防止配置变更后被旧目标的数据库污染首次启动就失败 → 保持旧行为写instance_up0等下轮刷新。Patroni 数据源早已用同样的模式lastFoundClusterMembers扛住 DCS 抖动WS3 只是把这套经验推广到 Postgres 发现并用一把互斥锁同时修掉了并发解析引入的潜在数据竞争。还有哪些隐藏的防抖细节主循环和 worker 里还散落着几处小巧思共同构成韧性底盘instance_up 探针源连不上时只写一条instance_up 0见 WriteInstanceDown健康源永远不被兄弟的故障连累——这是按源隔离降级的最小保证批量级联重试与指标降级pgx.Batch中一条 SQL 失败会中止同批后续查询协议特性。executeBatch 会把失败的条目逐条重试区分真实故障与级联故障连续失败的指标被标记为 degraded改用单条查询路径直到自动恢复实例级缓存同集群多库共享的实例级指标如pg_stat_archiver由 InstanceMetricCache 去重带 TTL 过期降低采集压力紧急暂停触发文件检测到紧急暂停文件时Reaper 会立即停止监控所有库LoadSources运维可一键刹车worker 生命周期幂等StartWorker对已存在的源是 no-opShutdownWorker统一取消 context、关池、通知 sinks 删除避免 goroutine 泄漏。恢复后长这样全自动无需重启韧性设计的验收标准写得很直白见设计文档第 10 节注入全网断流后指标在超时时间内失败并记录连通性恢复后自动恢复采集无需重启 pgwatchinstance_up0只出现在真正不可达的源上发现型源在发现失败的窗口期继续监控其已知数据库故障清除后 goroutine 数回落基线。延伸阅读关键源码与文档内容路径韧性设计总规格WS1/WS2/WS3spec/design-source-failure-resilience.mdReaper 主循环与并行扫描internal/reaper/reaper.go每源 workerGCD 节拍与批量重试internal/reaper/database.go超时默认值集中定义internal/db/deadlines.go发现型源的最后已知良好缓存internal/sources/resolver.goPing 门禁超时internal/sources/conn.go批量合并与故障注入实践docs/developer/reaper-batch-consolidation.md指标定义示例contrib/sample.metrics.yaml数据源配置示例contrib/sample.sources.yaml一句话总结pgwatch 的韧性不是靠更复杂的容错算法而是靠三件朴素的事——一切操作有界超时、故障按源并行隔离、已知良好状态永不轻易丢弃。这也是所有高可用采集系统值得借鉴的通用配方。【免费下载链接】pgwatchpgwatch: PostgreSQL metrics monitor/dashboard项目地址: https://gitcode.com/gh_mirrors/pg/pgwatch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考