把 PostgreSQL 复制巡检做成可审计闭环:确定性采集、阈值判定与受控模型归纳

问题背景

流复制的"连接仍在"并不等于灾备链路健康。主库可以看到备库连接,但备库可能长期未回放 WAL;复制延迟的字节数可能很大,却未必立刻影响既定恢复目标;同步复制场景中,一台备库的异常还可能扩大为业务提交延迟。

常见的失误,是把 pg_stat_replication 的一行输出直接交给模型,让模型判断"是否正常"。这会带来两个问题:第一,模型不应决定阈值、切换或写库等控制动作;第二,原始系统视图包含角色名、地址、应用名乃至业务库名,直接外发会扩大数据暴露面。

更稳妥的结构是三层分工:采集器只读取并规范化事实,规则引擎只执行预先批准的判断,模型只根据已经脱敏且结构化的结果生成面向人的摘要和排查建议。模型不可用时,告警与巡检结论仍应成立。

先明确复制语义

在流复制中,主库为每个直连备库维护 WAL 位点。常见字段包括:

  • sent_lsn:主库已发送给备库的 WAL 位置。
  • write_lsn:备库已写入但不一定已刷盘的位置。
  • flush_lsn:备库已刷盘的位置。
  • replay_lsn:备库已回放的位置。
  • state:如 streaming、catchup 等连接状态。

在支持 pg_wal_lsn_diff 的 PostgreSQL 版本中,可以用主库当前 WAL 位置减去 replay_lsn,得到近似的回放积压字节数。它是位点差,不是时间延迟:WAL 产生速率变化时,相同字节数对应的恢复时间并不相同。因此,告警策略通常需要同时考虑"是否正在流式复制""位点积压"和"最近收到回放进度的时间"。

主库的 pg_stat_replication 只能反映主库观察到的直连下游;备库自身还应检查 pg_stat_wal_receiver 和 pg_last_wal_replay_lsn()。级联复制、逻辑复制以及不同 PostgreSQL 大版本的字段与语义可能不同,投产前应以目标版本官方文档和拓扑为准。

架构与权限边界

建议让巡检任务运行在受限的运维环境中,并使用独立只读角色。角色不应拥有建表、写数据、复制槽管理或切换权限。

sql 复制代码
CREATE ROLE replication_observer LOGIN PASSWORD '由密钥系统注入';
GRANT pg_monitor TO replication_observer;

pg_monitor 可读取多项统计视图,是否符合组织的最小权限要求,需要由数据库管理员审查。若不能授予该预定义角色,可针对实际查询涉及的对象逐项授权,并在升级 PostgreSQL 后重新验证。

采集链路可按以下顺序实现:

  1. 在主库以只读账号查询复制视图。
  2. 仅保留状态、位点差、时间戳和匿名节点标识等必要字段。
  3. 在本地执行阈值规则,形成 ok、warning 或 critical。
  4. 把结构化结论发送给监控系统;只有需要值班摘要时,才调用模型接口。
  5. 将原始采集结果、规则版本、输入摘要和模型输出分别留档,便于复盘。

最小化采集 SQL

以下查询应在主库执行。application_name 可能包含环境或业务信息,示例中仅在本地做匿名化映射,不将其作为模型输入。对于尚未回复回放位点的连接,结果会保留为 NULL,规则必须显式处理,不能默认为零延迟。

sql 复制代码
SELECT
  application_name,
  state,
  sync_state,
  pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
  EXTRACT(EPOCH FROM now() - reply_time) AS reply_age_seconds
FROM pg_stat_replication
ORDER BY application_name;

阈值不宜直接照搬。先根据恢复目标、可用磁盘、链路带宽和可接受追赶时间确定内部标准。例如,streaming 以外的状态可被标记为需要关注;reply_age_seconds 为空或超过内部上限时应升级;积压字节数则应结合 WAL 产生速率解释。

可执行巡检脚本

下面的 Python 脚本把数据库连接信息全部从环境变量读取,查询结果只在本进程中使用。它输出可被日志系统采集的一行 JSON,并通过退出码区分严重程度。运行前安装 psycopg:pip install psycopg[binary]。

python 复制代码
import json
import os
import sys
from hashlib import sha256

import psycopg

MAX_LAG_BYTES = int(os.environ.get("MAX_REPLAY_LAG_BYTES", "1073741824"))
MAX_REPLY_AGE = int(os.environ.get("MAX_REPLY_AGE_SECONDS", "120"))

SQL = """
SELECT application_name, state, sync_state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
       EXTRACT(EPOCH FROM now() - reply_time) AS reply_age_seconds
FROM pg_stat_replication
ORDER BY application_name
"""

def node_id(name):
    return sha256((name or "unknown").encode()).hexdigest()[:12]

def assess(row):
    lag = row[3]
    age = row[4]
    if row[1] != "streaming" or lag is None or age is None:
        return "critical"
    if lag > MAX_LAG_BYTES or age > MAX_REPLY_AGE:
        return "warning"
    return "ok"

with psycopg.connect(os.environ["PG_DSN"]) as conn:
    with conn.cursor() as cur:
        cur.execute(SQL)
        rows = cur.fetchall()

replicas = []
for row in rows:
    replicas.append({
        "node": node_id(row[0]),
        "state": row[1],
        "sync_state": row[2],
        "replay_lag_bytes": row[3],
        "reply_age_seconds": row[4],
        "severity": assess(row),
    })

rank = {"ok": 0, "warning": 1, "critical": 2}
overall = max((item["severity"] for item in replicas), key=rank.get, default="critical")
print(json.dumps({"check": "postgres_replication", "severity": overall, "replicas": replicas}))
sys.exit({"ok": 0, "warning": 1, "critical": 2}[overall])

例如,以 systemd timer 或 Kubernetes CronJob 定时执行时,应由部署平台注入 PG_DSN、MAX_REPLAY_LAG_BYTES 和 MAX_REPLY_AGE_SECONDS。不要把 DSN、密码或 API 密钥写进镜像、仓库或日志。脚本返回非零退出码不一定适合所有调度器的重试策略:若平台会把任何非零退出反复重试,建议同时保留结构化日志,并由监控规则对 severity 告警。

模型归纳应是可选支路

模型适合把一组已经判定的结果压缩成值班摘要,例如指出"哪个匿名节点积压最高、规则命中了什么条件、下一步先查什么"。它不应执行 pg_promote()、修改 synchronous_standby_names、清理复制槽,或替代故障切换审批。

可以定义稳定的输入契约,只传递巡检脚本输出、拓扑类别和内部运行手册中允许外发的条目:

json 复制代码
{
  "role": "replication-incident-summarizer",
  "input": {
    "severity": "warning",
    "topology": "primary-standby",
    "replicas": [
      {"node": "a91d7f03e2c1", "state": "streaming", "replay_lag_bytes": 2147483648, "reply_age_seconds": 8, "severity": "warning"}
    ]
  },
  "required_output": ["observations", "possible_checks", "escalation_condition"],
  "constraints": ["不得建议自动故障切换", "不得臆测未提供的原因", "无法判断时明确说明"]
}

调用端应为模型设置超时、有限重试、响应长度上限和降级文案,并校验输出是否为预期 JSON。若供应商文档明确提供与所用 SDK 兼容的接口,可把其基地址和密钥作为部署变量接入;例如,HaerAPI(https://www.haerapi.com)可以作为需要评估的模型接入候选之一,但具体认证方式、模型名称、限额和数据处理范围必须以其当前文档为准。

模型调用前还应完成三项检查:确认输入是否含个人信息、主机地址、库名或业务标识;确认供应商、区域和保留策略满足组织要求;确认审计日志只记录必要的请求摘要,且不会反向泄露密钥或敏感上下文。

常见问题

只有一台备库时,pg_stat_replication 返回空行怎么办?

先确认查询是否在主库执行,再检查备库的连接与认证配置。空行不能被解释为"无延迟";对要求具备备库的环境,应将其作为拓扑不符合预期的信号。对于故意不配置物理备库的实例,则应在资产清单中明确排除该检查。

积压很大,为什么业务没有报错?

异步复制下,主库提交通常不等待备库回放,因此应用写入可以正常完成。风险主要体现为故障时可能丢失尚未复制或尚未回放的数据,以及备库无法按恢复目标接管。是否影响业务取决于复制模式和恢复承诺,不能只根据一项指标下结论。

能否用模型直接判断是否切换主备?

不建议。切换涉及法定拓扑、客户端连接、数据一致性、隔离操作和回滚预案。应由经过演练的自动化流程和人工审批共同控制;模型最多提供证据摘要与检查清单。

规则阈值怎样避免频繁告警?

使用持续时间窗口和恢复阈值。例如连续多个采样周期超过上限才触发,恢复到更低阈值后才关闭。窗口长度必须与采样周期、恢复目标及值班响应时间一起设定,并在故障演练或历史事件复盘后调整。

总结

可靠的复制巡检不是让模型"看懂数据库",而是先用数据库自身可验证的状态建立事实,再用明确规则完成分级,最后把模型限制在解释和归纳的位置。这样既保留了模型在值班沟通中的价值,也确保模型服务不可用、输出错误或合规审查收紧时,核心监控和处置链路仍然可运行。

相关推荐
微学AI2 小时前
不让每一步都调用最贵模型:用蓝耘智能路由改造自主式研究 Agent
数据库·人工智能·蓝耘
yjb.gz2 小时前
Oracle19 RAC查看集群状态及磁盘空间情况(巡检)
数据库
钝挫力PROGRAMER3 小时前
开发踩坑记:MyBatis selectKey、空 SQL
数据库·sql·mybatis
JosieBook3 小时前
【数据库】MySQL 实战精通系列 · 第6篇:InnoDB 存储引擎、日志与崩溃恢复
数据库·mysql
杨云龙UP3 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
梦帮科技4 小时前
领域认知知识库图谱注入:从双式记账图网络到高质量问答对自动化合成流水线
运维·网络·数据库·人工智能·矩阵·架构·自动化
Long long ago.4 小时前
vastbase数据库运行sql宕机重启解决
数据库·sql
SelectDB技术团队4 小时前
一条日志两套引擎的账:把 Elasticsearch 检索与分析合并到同一份数据的落地写法
大数据·数据库·elasticsearch·搜索引擎·全文检索·日志·apache doris
数据库百宝箱4 小时前
rum&gin索引对比
java·数据库·gin