跳转到主要内容

34 过载保护与资源故障判型——李代桃僵

数据库“慢、满、连不上”时,最危险的动作往往不是没有动作,而是把正确手段用在了 错误根因上:

连接风暴  -> 扩大 max_connections  -> 内存与调度更快耗尽
WAL 撑盘 -> 取消慢查询              -> restart_lsn 一字节也不前进
XID 保留 -> 清理普通表空间          -> 冻结边界仍被旧 xmin 钉住
I/O 排队 -> 同时重启所有组件        -> 证据消失,恢复负载叠加

本章把资源事故分成两条首先必须分开的路径:

flow pressure
  新工作到达得比系统完成得快
  -> 排队、拒绝、超时、重试放大
  -> 目标是减少进入量、并发量或单项成本

retention pressure
  某个仍被声明为“需要”的历史边界不能前进
  -> WAL、旧版本或事务状态不能回收
  -> 目标是识别 owner、保护证据、修复消费者或恢复链

二者可以同时发生,也可能都不是。如果证据不足,正确路线不是猜一个,而是 STOP_AND_INVESTIGATE:停止破坏性清理、冻结新增变量、保留 SQL 与主机证据,并明确 尚未回答的问题。

学习完成标准

完成本章后,读者应能:

  1. 把“CPU 高、磁盘满、延迟高、连接失败”视为症状,而不是根因;
  2. 区分流量型竞争与 WAL/XID/slot/归档/长事务造成的保留型压力;
  3. 解释到达率、服务率、并发、队列和超时为什么会形成正反馈;
  4. 为应用池、PgBouncer、HAProxy 与 PostgreSQL 分配一致的连接预算;
  5. 识别健康检查、短连接和无抖动重试造成的隐藏放大;
  6. pg_stat_activity、wait event、阻塞树和执行计划识别失控工作;
  7. 区分 pg_cancel_backendpg_terminate_backend 的影响和权限边界;
  8. 在结束长事务或大事务前评估锁释放、中止清理、既有 WAL、死版本与后续 vacuum 成本;
  9. 用并发最坏值估算 work_mem、并行 worker 与连接数的内存风险;
  10. 将 PostgreSQL 的 I/O 证据与主机设备延迟、队列和文件系统余量互证;
  11. 为限流、熔断、摘流、取消和只读降级写出收益、代价、停止线与回退;
  12. backend_xmin、复制槽 xmin/catalog_xmin 和 prepared transaction 判断 XID 保留者;
  13. restart_lsnwal_status、归档与备份状态判断 WAL 保留者;
  14. 解释为什么绝不能在运行中的实例里手工删除 pg_wal 文件;
  15. 用 Pigsty 的服务端点、连接池与监控缩小影响范围,但回到 PostgreSQL/OS 证据判型;
  16. 在不知道盲测答案时选择正确路线,并拒绝 34 类越界或误判证据。

一张判型表

问题 流量型 保留型
核心状态 到达工作超过可服务能力 最老的必需历史边界不能前进
典型信号 连接拒绝、队列、锁等待、CPU/I/O 饱和 inactive slot、旧 xmin、归档失败、WAL 累积
第一目标 减少 admission、并发或单项成本 找到 owner 与恢复来源,保护 lineage
可以立即做 限流、暂停批处理、精确 cancel、降级 留证、隔离增长、恢复消费者、评估精确释放
不应盲做 临时放大连接/内存,广域 terminate cancel 普通查询、删 pg_wal、随意 drop slot
成功证据 队列下降、拒绝停止、业务探针恢复 保留边界推进、归档/消费者恢复、恢复链完整

资源余量也不是一个百分比。至少要同时表达:

Hr=LrUr H_r = L_r - U_r

其中 $L_r$ 是资源 $r$ 的安全上限,$U_r$ 是当前与已承诺使用量。对连接、内存、WAL 空间、XID age、I/O 服务能力分别计算的 $H_r$ 不能相互替代。磁盘还有 30% 并不能 证明连接有余量;CPU 只有 20% 也不能证明 WAL 保留安全。

对流量型队列,若一段持续窗口内到达率 $\lambda$ 大于完成率 $\mu$:

dQdtλμ>0 \frac{dQ}{dt} \approx \lambda-\mu > 0

队列 $Q$ 就会增长。平均值暂时正常也救不了尾延迟;重试还会反过来抬高 $\lambda$。 对保留型压力,真正需要测的是“最老仍被需要的位置”及其推进速度,而不是只看目录 当前大小。

事故时的四步闭环

1. bound
   影响哪个服务、角色、数据库、主机和时间窗?

2. classify
   flow、retention、both 还是 unknown?

3. relieve or route
   流量型做精确减压;保留型保护证据并进入专门恢复路径

4. verify
   用户探针、队列、保留边界、拓扑与临时动作是否全部复位?

每一步都要记录 UTC 时间、证据引用、操作者、预期收益、停止条件和实际结果。仅仅看到 面板曲线下降,不能说明动作正确:流量也可能因为所有客户端都超时而“下降”。

正式实验

本章在已确认的 Pigsty 开发沙箱做两层实验:

managed pg-test
  Patroni / SQL read-only capture before and after
  no connection storm, slot, cancel, service or route mutation

pg-test-3 disposable PostgreSQL 18.6
  /tmp/pg36-ch34-overload-<run-id>
  listen_addresses=''
  private Unix socket
  max_connections=24
  exact cleanup after server stop

runner 用系统随机源安排两个 blind case,classifier 只能读取共同告警 postgresql-resource-headroom-at-risk 及观测字段,不能读取 hidden truth。正式顺序为:

RETENTION -> FLOW

正式观测:

情形 关键证据 判定与动作
connection storm 30 次尝试,21 个会话,9 次拒绝,20 个锁等待 RELIEVE_FLOW_PRESSURE;精确 cancel fixture sessions
WAL retention 1 个 inactive physical slot,保留 42,611,296 bytes PRESERVE_RETENTION_EVIDENCE;先留证,再 drop exact disposable slot

两个 case 完成后:

fixture sessions                   0
disposable physical slots          0
manual pg_wal file deletion    false
OOM / filesystem fill          false
managed topology changed       false
managed system id changed      false
managed timeline changed       false
exact temporary root remains   false

验证器同时构造并拒绝 34 个真实 mutant,包括生产边界被打开、blind packet 泄露答案、 阈值被削弱、广域 cancel、slot 仍残留以及谎报清理成功。公开证据见 overload-run.json

这份实验能证明在该隔离 PG18 合同内两类证据可区分,且精确动作能复位 fixture。 它不证明生产连接上限、真实 OOM victim、文件系统填满行为、归档仓库故障或未知 replication slot 可以安全删除;最终门禁固定为 production_ch34_gate=pending

阅读前后关系

本章目录

34.1 第一动作:流量型还是保留型

34.2 连接风暴与排队失控

34.3 失控查询、锁与事务

34.4 CPU、内存、I/O 与 OOM

34.5 流量型止血动作

34.6 保留型故障的安全路由

34.7 平台级流量控制与证据

34.8 实战:同一症状、两种成因

权威参考

PostgreSQL:

Pigsty:


上一章:故障切换与集群重建——力挽狂澜 · 返回下卷导读 · 下一章:数据抢救与工程取证——起死回生 · 查看全书目录 · 查看索引中心

34.1 第一动作:流量型还是保留型

资源告警出现后,先不要问“删什么”或“重启谁”,而要问:

当前资源是被正在到达和执行的工作消耗,还是被一个不能前进的历史边界 保留?

这不是给故障贴标签,而是选择安全动作。流量型压力需要减少进入量、并发或单项成本; 保留型压力需要找到保留者及其 owner。两类证据都成立时,先处理即将触发的硬失败, 同时保留另一条根因链;两类都不成立时,保持 unknown。

34.1.1 流量增长、慢查询、锁与连接导致的竞争

流量型的共同结构

下面四种表象最后都会形成“到达大于完成”:

放大源 到达侧变化 完成侧变化
业务流量增长 请求数增加 单次成本可能不变
慢查询或坏计划 请求数不变 每次占用 CPU/I/O/连接更久
锁竞争 等待工作继续占连接 有效并行度下降
连接风暴 建连、认证、backend 创建增加 正常查询拿不到 admission

若每秒进入 $\lambda$ 个工作、完成 $\mu$ 个工作,持续满足 $\lambda>\mu$,队列就增长。 这里的工作可以是 HTTP 请求、池等待者、数据库 session、正在运行的 statement 或磁盘 I/O。只看 PostgreSQL 的 active session 会漏掉在应用池和代理前排队的请求。

先把同一 UTC 窗口的证据放在一起:

SELECT application_name,
       state,
       wait_event_type,
       wait_event,
       count(*) AS sessions,
       max(clock_timestamp() - coalesce(xact_start, query_start))
         AS oldest_age
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2, 3, 4
ORDER BY sessions DESC;

再对照:

application arrival / timeout / retry
pool waiting clients / server connections
proxy accept / queue / backend health
PostgreSQL active / idle-in-transaction / Lock waits
host run queue / memory pressure / device latency

state='active' 不表示正在使用 CPU;PostgreSQL 文档明确指出 statewait_event 相互独立。active 且 wait_event_type='Lock' 的 backend 正在执行语句,但实际被阻塞。 这也是为什么“active 数很多”必须继续拆成 running、lock wait、I/O wait 与 client wait。

判定成立的最低条件

把问题判为 flow,至少应看到一条能够闭合的因果链:

arrival/retry increases
  -> admission or execution concurrency increases
  -> queue/wait/rejection grows
  -> completion rate or user success falls

仅凭 CPU 90% 不够。CPU 高也可能是 checkpoint 后的恢复工作、压缩、备份或一个与用户 延迟无关的后台任务;连接数高也可能都是长期 idle、但尚未达到瓶颈。

34.1.2 WAL、XID、复制槽、归档和长事务导致的保留

保留型不是“有人正在大量使用”

PostgreSQL 为恢复、复制和 MVCC 正确性保留历史。只要某个消费者仍声明“我可能需要 这里以前的内容”,系统就不能越过它回收:

被保留对象 常见保留者 关键边界
旧 tuple 版本 活跃快照、长事务、prepared transaction backend_xmin、prepared XID
catalog tuple logical replication slot catalog_xmin
WAL segment physical/logical slot、备库、备份 restart_lsn
待归档 WAL archive command/repository 失败 pg_stat_archiver 与归档队列
事务 ID 安全空间 未冻结表与被钉住的 xmin relation/database age

这里重要的是最老边界及其速度

SELECT pid,
       usename,
       application_name,
       state,
       xact_start,
       backend_xid,
       backend_xmin
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL
   OR backend_xmin IS NOT NULL
ORDER BY xact_start NULLS LAST;

SELECT slot_name,
       slot_type,
       active,
       xmin,
       catalog_xmin,
       restart_lsn,
       wal_status,
       safe_wal_size,
       invalidation_reason
FROM pg_replication_slots
ORDER BY slot_name;

SELECT transaction, gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;

inactive slot 不等于废弃 slot。它可能对应暂时离线的副本、迁移、CDC 消费者或恢复流程; active slot 也不等于健康,消费者可能连着却不推进。必须把 slot 映射到服务 owner、 consumer、恢复承诺和最后成功时间。

先测增长,再谈释放

两个快照比一个快照更有意义:

t0: free bytes, current LSN, restart_lsn, archive failure count
t1: same fields after a known interval

retained bytes     = current LSN - restart_lsn
growth rate        = (retained_t1 - retained_t0) / elapsed
time to hard limit = usable headroom / positive growth rate

max_slot_wal_keep_size=-1,replication slot 可以不受该参数上限地保留 WAL;即使配置 了有限值,相关状态也在 checkpoint 时才重新评估,不能把参数值误当作实时保险丝。

34.1.3 同样表现为“磁盘满”或“延迟高”,动作可以相反

症状相同,控制变量不同

症状 可能的 flow 根因 可能的 retention 根因 需要区分的证据
pg_wal 写流量突增、checkpoint 压力 slot/归档/备份钉住 WAL WAL 生成率与最老保留 LSN
表膨胀 更新/删除量增长 长快照或 slot catalog_xmin DML 率、vacuum 进度与 xmin
磁盘延迟高 并发查询、temp、checkpoint 被保留数据持续占满并触发写放大 device queue、文件分类、增长率
连接失败 到达/重试超过连接预算 磁盘满后新事务无法写 WAL pool queue、PG error、filesystem
CPU 高 执行/解析/自旋竞争 recovery/cleanup 追赶保留积压 backend type、wait、工作量变化

因此动作可以完全相反:

flow:
  stop admission -> cancel exact work -> queue falls

retention:
  preserve owner/lineage evidence -> repair consumer/archive
  -> only then release an exact, authorized boundary

把 retention 当 flow,取消再多普通查询也不会推进 restart_lsn。把 flow 当 retention, 忙着调查 slot 而不限制重试,服务可能先被连接和内存击穿。

同时发生怎么办

WAL slot 滞留期间又发生写入洪峰并不矛盾。事故记录应允许:

classification:
  flow: confirmed
  retention: confirmed
immediate_hard_failure: filesystem-full-in-18m
parallel_controls:
  - reduce noncritical write admission
  - preserve slot/archive evidence and contact owner
forbidden:
  - delete pg_wal files
  - drop unknown slot

“只能选一个根因”是复盘分类,不是在线处理原则。

34.1.4 判型不清时先停止破坏性清理

unknown 是一种有效状态

下面任一项不明,就不能执行不可逆释放:

which path is failing?
which object is growing?
who owns the oldest xmin/restart_lsn?
is there a valid backup or replica copy?
what client work will be canceled?
what data or recovery capability can be lost?

先收集最小证据包:

UTC + monotonic timestamp
user-facing probe and exact error class
pg_stat_activity grouped by app/state/wait
blocker tree and long/prepared transactions
pg_replication_slots and pg_stat_archiver
current/replay LSN and write rate
filesystem by mount/path plus inode usage
CPU/memory/pressure/device queue
recent config/deploy/failover/backup changes

然后将路线写成机器与人都能审阅的三态判定:

FLOW
  sufficient connection/queue/wait evidence
  and no contradictory retention evidence for this action

RETENTION
  exact owner boundary and retained quantity observed
  and flow relief cannot release that boundary

STOP_AND_INVESTIGATE
  两类证据同时成立,或任何一类都不足以支持动作

停止线包括:有人建议删 pg_wal、drop 未知 slot、终止未知大事务、在唯一副本上试验 危险参数、或以“磁盘快满”为由跳过 owner/backup 识别。此时应保护现场并回到第 31 章 的事故指挥框架。

本节交付物

进入具体止血前,至少写出:

symptom: exact user and resource observation
scope: service / database / node / time window
classification: FLOW | RETENTION | BOTH | UNKNOWN
supporting_evidence: [...]
contradicting_evidence: [...]
first_action: bounded and reversible
stop_condition: measurable
owner: named role

没有这些字段,“先重启看看”不是 runbook。


返回本章目录 · 下一节:连接风暴与排队失控 · 查看全书目录 · 查看索引中心

34.2 连接风暴与排队失控

PostgreSQL 采用一个 client connection 对应一个 backend process 的模型。连接不仅占一个 数字,还需要进程、内存、认证、catalog 初始化、socket、锁表与调度成本。连接池的 价值不是让数据库接受无限请求,而是把大量 client concurrency 变成有上限的 database concurrency。

34.2.1 数据库连接、代理池与应用池三层

三层都在排队

request
  -> application worker / local pool waiters
      -> PgBouncer client connections / waiters
          -> PgBouncer server connections
              -> PostgreSQL client backends

每层至少有四个量:

admitted
running
waiting
rejected/timed out

只看 PostgreSQL numbackends 会漏掉池前的队列;只看应用 pool size 又会漏掉多个 pod/进程/租户汇总后对数据库的总承诺。

一个粗略预算应满足:

iAiPi+M+BCpool \sum_i A_i P_i + M + B \le C_{\text{pool}}

其中 $A_i$ 是第 $i$ 类应用实例数,$P_i$ 是每实例可能占用的 server connection, $M$ 是迁移、运维和监控预算,$B$ 是故障切换/伸缩缓冲。对 PostgreSQL:

Cpool+Cdirect<max_connectionsCreserved C_{\text{pool}} + C_{\text{direct}} < \texttt{max\_connections} - C_{\text{reserved}}

这里不是要求把所有层的上限设成同一个数。应用 client queue 可以大于 PgBouncer server pool,但必须有长度、deadline 和拒绝策略;PgBouncer client connection 也不等于 PostgreSQL backend。

按事务语义分池

不能只按主机分池,还要按工作类型隔离:

特征 建议控制
OLTP 短事务、低尾延迟 最稳定的预算,快速失败
batch/ETL 长查询、吞吐优先 独立小池,可暂停
admin/migration 低频但高权限 保留直连/管理余量
monitoring 周期查询 有界并发,不能形成自激
read-only 可接受副本语义时 独立只读端点与 staleness 契约

若批处理与 OLTP 共用一池,批处理占满 server connections 后,所谓“主库健康”也无法 给在线请求提供 admission。Pigsty 的主写、只读、离线等服务端点可以提供路由分界,但 是否适合某事务仍由应用一致性语义决定。

事故时不要立刻放大 max_connections

扩大上限会让更多工作同时进入执行层,可能把一个有界连接拒绝变成内存、CPU、锁与 I/O 全面争用。只有在以下事实都成立时,调整才是经过评估的容量变更:

current connections are useful, not retry duplicates
per-backend and per-query memory worst case is safe
CPU/I/O still have service headroom
new reserved/admin budget remains available
pool and application limits will not simply refill the new space
rollback and restart/reload semantics are known

在线事故的默认路线是收紧 admission,而不是把硬边界向后推。

34.2.2 重试放大、健康检查和短连接

重试会把失败变成新流量

若原始到达率为 $\lambda_0$,每次失败平均触发 $r$ 次下一轮尝试,成功率没有及时恢复, 有效流量近似:

λeffective=λ0(1+r+r2+) \lambda_{\text{effective}} = \lambda_0(1+r+r^2+\dots)

当 $r\ge1$ 且没有 retry budget、deadline 或熔断时,系统进入正反馈:

latency rises
  -> client timeout
  -> synchronized retry
  -> more connections and work
  -> latency rises again

日志里的“请求量上升”可能不是用户流量,而是同一批请求的重复尝试。必须用稳定的 request/idempotency key 区分 original、retry 与 hedge。

健康检查也会成为负载

设 $N$ 个应用实例,每个实例维护 $P$ 个 worker,每 $h$ 秒建立一次检查连接,则单健康 检查一项就可能产生约 $NP/h$ 次每秒建连。以下设计尤其危险:

  • 每个业务请求先新建连接执行 SELECT 1
  • 每个 pod 同时启动并预热完整池;
  • 多级代理各自以高频新连接探测;
  • 故障时 autoscaling 新增实例,同时所有实例立刻重试;
  • liveness 把短暂数据库慢判为应用死亡,形成重启风暴。

健康检查要区分:

liveness: process itself是否需要重启
readiness: 是否接收新业务流量
dependency health: 数据库路径是否满足该业务语义

数据库慢通常应先让应用 not-ready 或熔断新请求,而不是把所有应用进程重启。

长连接也不是免疫

已有连接在代理切换、数据库重启、证书轮换或网络抖动后会同时重连。池应具备:

randomized connection lifetime
startup/prewarm rate limit
connect timeout shorter than request deadline
bounded reconnect concurrency
exponential backoff with full jitter
global retry budget

不要给每一层各自配置十次重试。应用、驱动、service mesh、代理和任务框架叠加后, 最坏尝试次数是乘法。

34.2.3 限流、队列、连接预算与指数退避

把过载变成显式 admission

好的过载控制不是“永不拒绝”,而是在系统仍能完成高价值工作时,尽早、明确地拒绝 超出预算的工作:

admit if:
  class budget available
  AND request deadline still useful
  AND downstream breaker allows probe
else:
  reject/queue with bounded cost

队列必须同时有:

  • 最大长度,防止内存成为下一瓶颈;
  • 最大等待时间,过期工作不再进入数据库;
  • 公平性或优先级,避免批处理饿死 OLTP;
  • 可观测的 admitted/waited/rejected/expired 计数;
  • drain 与 deploy 行为,避免发布时丢失或翻倍。

无限队列只是把快速失败变成更晚失败。Little’s Law 给出稳定系统中的关系:

L=λW L=\lambda W

当平均等待 $W$ 上升时,在途数量 $L$ 也上升;如果请求 deadline 已经小于排队时间, 即使最终执行成功,对用户也没有价值。

指数退避要带随机抖动

一个常见策略:

cap = min(max_backoff, base * 2^attempt)
sleep = random(0, cap)          # full jitter
stop when:
  request deadline exhausted
  retry budget exhausted
  operation is not idempotent/reconcilable

重试条件也要按错误分类:

错误 默认处理
认证/权限/语法 不重试,修配置或代码
连接拒绝/切换窗口 有预算、带 jitter 重试
statement timeout 先判是否仍在数据库执行及是否幂等
deadlock/serialization failure 整个事务按有限策略重试
unknown COMMIT outcome 先用业务 token 对账,不裸重放

连接事故的止血顺序

1. freeze autoscaling/restart/retry amplification
2. preserve admin/reserved path
3. cap low-priority application admission
4. pause batch and migration pools
5. inspect active/waiting/root blockers
6. cancel exact low-value work if necessary
7. verify queue, success rate and tail latency
8. only after stability, repair capacity/config root cause

验收不是“连接数下降”,而是:

new connection rejection stops or is intentional
pool waiting/expired work trends down
useful completion rate recovers
admin path remains available
no new memory/I/O bottleneck appears
temporary limits have an owner and expiry

上一节:第一动作:流量型还是保留型 · 返回本章目录 · 下一节:失控查询、锁与事务 · 查看全书目录 · 查看索引中心

34.3 失控查询、锁与事务

“杀慢查询”不是故障判型。一个耗时最长的 session 可能是阻塞根节点、也可能是等待者; 可能正在做有价值的恢复,也可能已经超过用户 deadline;可能可以安全 cancel,也可能 已经写入海量 WAL 和死版本,terminate 也不会把这些成本自动抹掉。动作必须绑定 query、transaction、application、owner 与业务语义。

34.3.1 识别高消耗查询和阻塞根节点

当前现场与历史重查询分开

pg_stat_activity 说明此刻有哪些 backend、状态与等待;pg_stat_statements 聚合的是 一段时间内同类语句的执行统计。前者适合回答“谁现在占着资源”,后者适合回答“哪类 语句长期贡献最多”。不能用累计榜单代替当前事故现场。

一个不导出完整 SQL 文本的当前投影:

SELECT pid,
       usename,
       application_name,
       state,
       wait_event_type,
       wait_event,
       clock_timestamp() - query_start AS query_age,
       clock_timestamp() - xact_start AS xact_age,
       backend_xid,
       backend_xmin,
       query_id
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND pid <> pg_backend_pid()
ORDER BY xact_start NULLS LAST, query_start NULLS LAST;

对历史工作量,可按目标排序,而不是永远按 total time:

SELECT queryid,
       calls,
       total_exec_time,
       mean_exec_time,
       rows,
       shared_blks_read,
       shared_blks_written,
       temp_blks_read,
       temp_blks_written,
       wal_bytes
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

版本、扩展列和统计起点应随报告一起记录。统计 reset 或重启后的短窗口不能与一周基线 直接比较。

找根阻塞者,不要只杀等待者

WITH RECURSIVE lock_tree AS (
  SELECT a.pid,
         a.application_name,
         a.xact_start,
         pg_blocking_pids(a.pid) AS blockers,
         ARRAY[a.pid] AS path
  FROM pg_stat_activity AS a
  WHERE cardinality(pg_blocking_pids(a.pid)) > 0

  UNION ALL

  SELECT b.pid,
         b.application_name,
         b.xact_start,
         pg_blocking_pids(b.pid),
         t.path || b.pid
  FROM lock_tree AS t
  CROSS JOIN LATERAL unnest(t.blockers) AS p(pid)
  JOIN pg_stat_activity AS b ON b.pid = p.pid
  WHERE NOT b.pid = ANY(t.path)
)
SELECT * FROM lock_tree;

根 blocker 可能显示 idle in transaction,因为它已经执行完持锁语句,正在等客户端下 一条命令。仅筛选 state='active' 会漏掉它。也要排除 autovacuum、logical worker、 备份和维护工作等不同 backend_type,不要把每个 PID 都当作应用会话。

高消耗不是自动有罪

取消前回答:

is this the root blocker or a victim?
is its client deadline already expired?
is it OLTP, migration, maintenance, backup, recovery, or batch?
what locks and objects does it own?
what rows/WAL/temp/I/O has it already produced?
does it carry a business idempotency key?
who owns the decision?

对 query_id 做执行计划分析时,转到第 10、11 章的方法;在线事故中不要在主库上无界 执行 EXPLAIN ANALYZE 复现一条未知重查询。

34.3.2 cancel、terminate 与中止后成本

两个函数的边界

SELECT pg_cancel_backend(:pid);
SELECT pg_terminate_backend(:pid);

pg_cancel_backend 向目标 backend 发送取消当前 query 的请求。session 通常仍存在; 当前事务会进入错误状态,客户端需要 ROLLBACKpg_terminate_backend 终止整个 session,连接断开,未提交事务由服务器回滚。两者都需要相应权限;不要通过给应用 超级用户来获得事故处置能力。

优先级一般是:

application cooperative cancel/deadline
  -> pg_cancel_backend exact PID
      -> wait and verify
          -> pg_terminate_backend exact PID when justified

“exact”至少绑定:

pid + backend_start
database + user + application_name
query_id / transaction age
incident run id or ticket

PID 会复用。先查 PID、过几分钟再裸 terminate,可能命中完全不同的新连接。执行动作的 SQL 应在同一事务/语句里重验识别字段。

cancel 不等于立即释放全部资源

  • query 可能在到达可中断点前继续运行;
  • client 可能自动重试同一工作;
  • 事务未 rollback 前仍可能持有锁;
  • parallel workers 与 leader 的收敛需要时间;
  • remote/extension 调用的中断语义取决于组件;
  • query 已经产生的 WAL、temp 或脏页不会凭空消失。

terminate 也不是免费的“更强 cancel”,但要准确理解 PostgreSQL 的代价:普通事务 中止不会通过物理 undo 逐行撤销已经写过的 tuple。backend 仍需响应信号、执行中止清理 并释放锁和本地资源;已经产生的 WAL、脏页、复制延迟和死版本不会消失,死版本通常要 由后续 vacuum 回收。因此不能套用“按修改量做数小时物理回滚”的模型,也不能因为 session 已消失就宣称资源影响全部结束。

动作后必须复核

SELECT pid, backend_start, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE pid = :pid;

同时观察:

root blocker disappeared?
dependent waiters made progress?
pool stopped recreating the work?
abort cleanup、vacuum 或 replica catch-up 是否仍在消耗 I/O?
user success and tail latency recovered?

如果应用立刻重建同一 session,数据库端 cancel 只是短暂擦除症状,真正控制点在 admission 和 retry。

34.3.3 长事务和大事务结束前先评估后果

“长”与“大”是两个维度

long but small
  idle transaction holds snapshot/locks for hours

short but large
  bulk UPDATE changes millions of rows in minutes

long and large
  migration/ETL both retains old state and creates rollback work

长事务主要风险是 lock、backend_xmin、vacuum 回收与连接占用;大事务还带来 WAL、 dirty buffers、replication lag、终止响应与后续清理成本。prepared transaction 即使 没有活动 session,也可长期保留锁和 XID 状态:

SELECT gid,
       prepared,
       owner,
       database,
       clock_timestamp() - prepared AS age
FROM pg_prepared_xacts
ORDER BY prepared;

不要看到 prepared transaction 就 ROLLBACK PREPARED。它属于两阶段提交协议,必须先 与 transaction manager/业务 ledger 对账,判断应 commit 还是 rollback。

结束前的后果清单

identity:
  pid_backend_start: ...
  application_owner: ...
  transaction_or_job_id: ...
business:
  partial_external_effects: ...
  idempotency_or_reconciliation: ...
database:
  locks: ...
  xmin_retention: ...
  rows_wal_temp_estimate: ...
  replicas_and_archive_effect: ...
abort_and_cleanup:
  expected_resource_cost: ...
  observation_query: ...
  escalation_timeout: ...

若事务包含数据库外部副作用,PostgreSQL rollback 只能撤销数据库内未提交状态,不能 撤回已经发出的邮件、支付或消息。此时需要业务补偿,不是更强的 terminate。

更好的预防

  • 为交互式事务设置合理的 idle_in_transaction_session_timeout
  • 为不同工作负载设置 statement/lock timeout,而不是一个全局极小值;
  • 大批处理分块提交,并让每块有可恢复 checkpoint;
  • schema change 使用受控 lock timeout 和发布门;
  • 统一 application_name、query tag 与业务 job id;
  • 为重要操作保留可对账 token。

timeout 是保护栏,不是容量。设置后还要验证应用如何处理取消、事务错误和重试。


上一节:连接风暴与排队失控 · 返回本章目录 · 下一节:CPU、内存、I/O 与 OOM · 查看全书目录 · 查看索引中心

34.4 CPU、内存、I/O 与 OOM

数据库资源彼此耦合。内存紧张会增加 reclaim 与 swap I/O;I/O 变慢会延长 query 和 transaction,占住更多连接与内存;连接排队又会触发超时重试,进一步增加 CPU。 事故中不能把每张主机图分开解释,而要寻找同一时间线上的因果方向。

34.4.1 饱和、排队、抖动与抢占

利用率不是完整答案

CPU 100% 可能仍有高吞吐且尾延迟可接受;CPU 40% 也可能因为单核热点、锁、自旋、 steal time 或 I/O 等待导致业务停滞。主机证据至少包括:

per-CPU user/system/iowait/steal
run queue and runnable tasks
context switches
memory pressure / reclaim / swap
per-device latency, queue depth, throughput and errors
cgroup/container limits and throttling
filesystem free bytes and inodes

再按 PostgreSQL backend type 与 wait event 对齐:

SELECT backend_type,
       wait_event_type,
       wait_event,
       count(*) AS processes
FROM pg_stat_activity
GROUP BY 1, 2, 3
ORDER BY processes DESC;

PostgreSQL 官方建议把统计视图与操作系统工具结合,因为数据库 I/O 统计不能区分数据 来自物理设备还是内核 page cache。数据库看到 read,也不等于磁盘实际发生同量读取。

饱和、排队和抖动

saturation
  resource has little service headroom

queueing
  work waits before resource service

jitter
  completion latency varies sharply over time

contention/preemption
  work loses CPU/lock/device service to other work

平均设备延迟 2 ms 可能掩盖 checkpoint 时 500 ms 尖峰;五分钟 CPU 平均值也会抹掉 每 30 秒同步到来的任务。保留原始采样粒度、时钟和分位数,不要只截一张平滑后的图。

先区分主机级还是数据库级

证据组合 更可能的方向
host run queue 高,PG 多数无 wait CPU runnable 竞争
PG 大量 Lock,CPU 不高 数据库锁序列化
device await/queue 高,PG 大量 IO 存储服务能力不足
cgroup throttled,宿主机空闲 容器/服务配额
swap/reclaim 高,连接与 query 同增 内存承诺或并发过大
PG 平稳,其他进程占资源 noisy neighbor/备份/扫描

不要在没确认 cgroup/虚拟化边界时用宿主机总容量推导数据库余量。

34.4.2 临时文件、并行、checkpoint 与后台维护

前台与后台会争同一设备

下面工作可能同时写盘:

sort/hash spill -> temp files
WAL writer / WAL sync
backend data writes
checkpointer flushing dirty buffers
autovacuum/vacuum
CREATE INDEX / REINDEX
base backup and archive
operating-system or storage maintenance

pg_stat_io 按 backend type、object 与 context 提供集群级 I/O 统计;pg_stat_database 的 temp 计数、日志中的 temporary file、pg_stat_checkpointerpg_stat_archiver 和 进度视图分别补充来源。统计是累计量,需要记录起点并取差值:

SELECT backend_type,
       object,
       context,
       reads,
       read_time,
       writes,
       write_time,
       extends,
       fsyncs,
       fsync_time
FROM pg_stat_io
ORDER BY backend_type, object, context;

列集合随 PostgreSQL 版本演进,生产脚本应绑定 major version 并做兼容检查。

临时文件是结果,不是单一根因

spill 可能来自:

  • work_mem 对该 sort/hash 太小;
  • 行数估计错误导致计划不合适;
  • 并发相同操作太多;
  • 查询本来就必须处理大量数据;
  • hash 操作按 hash_mem_multiplier 获得更高上限;
  • parallel workers 各自执行内存/临时工作。

直接把 work_mem 全局放大,可能把磁盘事故变成 OOM。优先修 query/统计、限制该工作 并发,必要时只对可控 role/session 做有界调整并验证。

checkpoint 峰值与追赶效应

checkpoint 需要把脏页推进到 durable storage。写流量突增、WAL 配置、恢复/重启后的 缓存重新填充和存储变慢都可能让 checkpoint 与前台 I/O 相互干扰。事故中记录:

checkpoint requested/timed
buffers written and write/sync duration
WAL generation rate
device write latency/queue
replica and archive progress

暂停 autovacuum 或 checkpoint 通常不是通用止血。autovacuum 还承担 XID freeze; 暂停后可能把短期 I/O 压力转成更危险的保留问题。只能对已识别对象、在明确时间窗与 回补计划下调整维护。

34.4.3 内存最坏并发、OOM killer 与进程重启

work_mem 不是每连接只分配一次

PostgreSQL 文档强调,work_mem 是一个 query operation(如 sort/hash)的基础上限; 一个复杂 query 可同时有多个 operation,多个 session 又可并发,parallel worker 也会 扩大总使用。粗略上界应按工作节点估算:

Mworkloadsactive sessionsoconcurrent operations(s)wparticipants(s,o)Ms,o,w M_{\text{workload}} \approx \sum_{s \in \text{active sessions}} \sum_{o \in \text{concurrent operations}(s)} \sum_{w \in \text{participants}(s,o)} M_{s,o,w}

其中 participants 包括执行该 operation 的 leader 与 parallel workers。这个表达式的 关键不是算出一个恒定值,而是把“同时活跃的 session × 同时活跃的内存节点 × 参与 进程”三层并发都纳入预算。

再加上:

shared_buffers and shared memory
backend base memory
maintenance_work_mem / autovacuum_work_mem
logical decoding and extension memory
kernel page cache
proxy, exporter, Patroni and other host processes
failure/recovery reserve

因此:

max_connections * work_mem

既不是准确实测,也不是足够保守的最坏值。它漏掉每 query 多个节点和并行,也忽略许多 非 work_mem 内存;反过来假设所有连接同时打满每个上限又可能极度悲观。容量测试要用 真实 workload envelope 和并发组合。

Linux OOM 不是数据库的流控机制

Linux overcommit 允许进程承诺超过物理内存的虚拟地址空间;真正耗尽时,OOM killer 可能选择某个进程。若 PostgreSQL child 被杀,postmaster 会把它当作异常退出,为保护 共享内存一致性,可能终止其他 server processes 并执行 crash recovery;若 postmaster 本身被杀,服务管理器行为又是另一条路径。

不要故意在共享/生产主机上“测一次 OOM”。安全实验应在有明确 cgroup/VM 边界的专用 环境里完成,并验证:

which cgroup/host reported OOM
which PID and backend_type was selected
whether postmaster remained
whether crash recovery occurred
client unknown outcomes
replica/archive/backup state after restart

本章正式实验明确不注入 OOM。

事故动作

内存压力下优先:

  1. 阻止新低价值工作和重试;
  2. 识别 exact 高内存 query/role/pool;
  3. 暂停可恢复 batch 与并行任务;
  4. 用 query cancel 逐步释放,而不是一次 terminate 全部;
  5. 保留管理连接与 OS 控制面;
  6. 观察 reclaim、swap、RSS、队列和业务成功率;
  7. 稳定后修正连接预算、query、并行与内存配置。

不要在事故中 drop OS cache:它既不能修复内存承诺,还会把后续读取推向存储,破坏 现场并制造新的 I/O 峰值。也不要把 swap 使用本身等同于故障;关键是持续 swap in/out、 memory pressure 与用户影响。

资源证据矩阵

cpu:
  utilization: ...
  run_queue: ...
  steal_or_throttle: ...
memory:
  available: ...
  pressure: ...
  swap_rate: ...
  oom_event: ...
io:
  device_latency_queue: ...
  pg_waits: ...
  checkpoint_temp_maintenance: ...
workload:
  admitted_running_waiting_rejected: ...
  root_queries_or_jobs: ...
decision:
  exact_control: ...
  stop_and_rollback: ...

单张 top 截图不够支撑数据库重启。


上一节:失控查询、锁与事务 · 返回本章目录 · 下一节:流量型止血动作 · 查看全书目录 · 查看索引中心

34.5 流量型止血动作

流量型止血的目标不是让所有请求都继续进入,而是让系统重新获得完成有价值工作的 能力。在过载区间里,少接收一些工作通常比全部接收、全部超时更可用。

34.5.1 限流、熔断、摘除非关键负载

越靠近来源,拒绝成本越低

client / edge
  -> application admission
      -> local pool
          -> proxy / PgBouncer
              -> PostgreSQL

在应用 admission 拒绝一个尚未创建事务的请求,成本通常远低于让它拿到 database connection、执行一半、生成 WAL 后再取消。因此控制顺序优先:

  1. 阻止新的低优先级到达;
  2. 冻结无抖动重试、autoscaling 和批量 worker 扩张;
  3. 缩小 batch/report/migration pool;
  4. 对依赖数据库的非关键功能打开熔断或静态降级;
  5. 最后才在数据库内取消已经进入的 exact work。

限流、熔断、摘流回答不同问题

控制 回答 典型状态
rate limit 单位时间允许多少新工作 token/leaky bucket
concurrency limit 同时允许多少在途工作 semaphore/pool
queue bound 等待多少、等多久 length + deadline
circuit breaker 下游失败时是否继续尝试 closed/open/half-open
load shedding 哪类工作先被拒绝 priority/admission class
route removal 哪个后端不再接新流量 health/maintenance state

熔断打开不等于数据库恢复;它只是停止继续伤害下游。half-open probe 必须有很小并发, 否则所有实例同时探测会形成下一轮风暴。

按业务价值而不是技术便利舍弃

一个可执行的 shedding 顺序需要产品 owner 参与:

preserve:
  - payment commit
  - authentication write
  - incident/admin probe
degrade:
  - recommendation to cached response
  - dashboard to stale snapshot
pause:
  - ETL
  - report export
  - reindex/migration
reject:
  - best-effort refresh

“所有 SELECT 都是低价值”或“写都更重要”并不成立。某些 read 是支付授权前置条件, 某些 write 只是可重算的埋点。

34.5.2 取消查询、暂停批处理与只读降级

取消只命中已证明的工作集

安全筛选示例:

WITH target AS (
  SELECT pid, backend_start
  FROM pg_stat_activity
  WHERE application_name LIKE 'report-worker:%'
    AND state = 'active'
    AND query_start < clock_timestamp() - interval '30 seconds'
    AND pid <> pg_backend_pid()
)
SELECT pid,
       backend_start,
       pg_cancel_backend(pid) AS signaled
FROM target;

生产中还应绑定数据库、role、query/job tag、变更单与 owner。LIKE 'report-worker:%' 只有在 application_name 受治理、不能被任意业务伪造时才够用。

取消后等待并验收,不要立即升级为 terminate:

target sessions disappear or become idle/aborted
root lock releases
waiters make progress
application does not recreate work
useful completion rate rises
rollback/recovery cost remains bounded

暂停 producer 比逐条 cancel 更有效

batch 系统通常有 scheduler、queue consumer 或 worker deployment。先暂停 producer, 再处理 in-flight work;否则数据库每取消一条,调度器就补一条。暂停要记录:

queue name and partition
last acknowledged item/checkpoint
in-flight ownership
resume condition
duplicate/replay semantics
maximum backlog after pause

如果任务没有 checkpoint 与幂等性,暂停本身可能产生业务不一致,需要应用 owner 决策。

只读降级有一致性前提

把读流量移到 replica 可以减少 primary 的部分 CPU/I/O,但必须回答:

can this operation tolerate replica lag?
does it require read-your-write or monotonic reads?
will long reads delay replay or create conflicts?
does replica share the same storage/CPU failure domain?
is offline/analytics capacity isolated?
what happens when no eligible replica exists?

不要把写请求改成“返回成功但不落库”,除非业务明确设计了 durable queue/ledger 和补偿。 也不要把所有查询涌向一台 replica:这可能让 replay 落后,进一步破坏读语义和 HA 候选质量。

Pigsty 的 replica/offline 服务可以表达路由意图,不能替代这些语义判断。路由后用 pg_is_in_recovery()transaction_read_only、replay lag 与业务 token 复核。

34.5.3 每个动作写清收益、代价、停止条件和回退

动作卡,而不是命令清单

action_id: FLOW-07
hypothesis:
  report batch consumes the OLTP server pool
target:
  application_name_prefix: "report-worker:"
  pool: report
expected_benefit:
  free_at_least_connections: 12
  reduce_lock_waiters_below: 3
cost:
  report_jobs_paused: true
  duplicate_risk: reconciled-by-job-id
guard:
  exclude_admin_and_oltp: true
stop_condition:
  useful_tps_not_improved_after: 120s
  post_cancel_io_exceeds_baseline_by: 2x
rollback:
  restore_pool_limit: after 15m stable window
evidence:
  before: ...
  after: ...
owner: ...

四个字段不可省:

  1. 收益:哪个指标应在多长时间内变化;
  2. 代价:谁被拒绝、延迟或需要补偿;
  3. 停止条件:什么证据说明假设错误或副作用更大;
  4. 回退:如何撤销临时配置、恢复任务并验证没有重放。

一次只改变可辨识的控制变量

同时扩容、重启、cancel、改 pool 和改路由,曲线即使恢复也无法知道哪个动作有效; 某个有害动作可能被另一个动作掩盖。事故很急时可以并行动作,但必须按独立目标分组, 记录精确时间和 owner:

T0 admission limit
T1 batch pause
T2 exact cancel
T3 service probe recovery
T4 queue below stop threshold

涉及同一变量的相反动作不能并发,例如一个人缩 pool、另一个人扩大 max_connections

止血成功的定义

至少同时满足:

user success and tail latency recover
queue/rejection trend is understood
database has resource headroom, not merely lower traffic
replicas/archive/backup remain healthy
no unknown large rollback or retention boundary remains
temporary controls have owner, expiry and rollback evidence

服务恢复后不要马上取消全部限流。先保持观察窗口,逐级放量;每一级都验证完成率、 尾延迟、资源余量与重试量。一次把 backlog 全部释放,会制造第二次尖峰。


上一节:CPU、内存、I/O 与 OOM · 返回本章目录 · 下一节:保留型故障的安全路由 · 查看全书目录 · 查看索引中心

34.6 保留型故障的安全路由

保留型事故的第一原则是:

先证明“谁还需要哪段历史”,再决定是恢复消费者、迁移恢复来源,还是释放这项需要。

一条复制槽、一个 xmin 或一批 WAL 文件本身不是垃圾。它们是恢复、复制、快照或事务 协议的状态。未经 owner 与恢复链确认的“清理”,可能把空间问题变成不可恢复的数据 问题。

34.6.1 XID:检查 backend_xmin、复制槽 xminpg_prepared_xacts

先列出所有可能的 horizon owner

活动 backend:

SELECT pid,
       datname,
       usename,
       application_name,
       state,
       xact_start,
       backend_xid,
       backend_xmin,
       wait_event_type,
       wait_event
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL
   OR backend_xmin IS NOT NULL
ORDER BY xact_start NULLS LAST;

复制槽:

SELECT slot_name,
       slot_type,
       database,
       active,
       xmin,
       catalog_xmin,
       restart_lsn,
       inactive_since,
       invalidation_reason
FROM pg_replication_slots
ORDER BY slot_name;

两阶段事务:

SELECT transaction,
       gid,
       prepared,
       owner,
       database
FROM pg_prepared_xacts
ORDER BY prepared;

再看 database/relation freeze age:

SELECT datname,
       age(datfrozenxid) AS xid_age,
       mxid_age(datminmxid) AS multixact_age
FROM pg_database
ORDER BY xid_age DESC;

这些视图回答的是不同问题:

  • backend_xmin:该 backend 当前快照仍可能看到多老的版本;
  • slot xmin:消费者需要的数据行版本边界;
  • catalog_xmin:逻辑解码需要的 catalog 版本边界;
  • prepared XID:已经 PREPARE TRANSACTION、等待外部决议的事务;
  • datfrozenxid:数据库中尚未冻结事务的保守下界。

不要把最老 PID 自动当成罪魁

一个长 session 未必持有 xmin;一个短暂但 prepared 的事务可能没有 session 却长期 持锁。logical slot 的 catalog_xmin 也可能成为 catalog vacuum 的约束。先把每个边界 映射到:

owner
business or replication purpose
last successful progress
expected outage/retention window
recoverability if released
approved decision maker

处置路线:

保留者 安全路线
应用长事务 联系 owner,停止新工作,评估 cancel/terminate 与补偿
prepared transaction 与 transaction manager/ledger 对账后 commit 或 rollback
logical slot 恢复 consumer,或从新起点重建并明确数据缺口
freeze 落后 修复 blocker/资源后执行受控 vacuum/freeze
无法识别 保持证据,升级,不释放

完整的 XID、freeze 与膨胀处理见第 28 章

34.6.2 WAL 撑盘:检查归档失败、复制槽和未完成备份

pg_wal 大小不是根因

WAL 目录可以因为正常高写入暂时变大,也可以因为保留者不推进持续增长。先记录:

SELECT pg_current_wal_lsn() AS current_lsn,
       pg_wal_lsn_diff(
         pg_current_wal_lsn(),
         '0/0'::pg_lsn
       ) AS absolute_lsn_bytes;

SELECT slot_name,
       slot_type,
       active,
       restart_lsn,
       pg_wal_lsn_diff(
         pg_current_wal_lsn(),
         restart_lsn
       ) AS retained_wal_bytes,
       wal_status,
       safe_wal_size,
       invalidation_reason
FROM pg_replication_slots
ORDER BY retained_wal_bytes DESC NULLS LAST;

SELECT archived_count,
       failed_count,
       last_archived_wal,
       last_archived_time,
       last_failed_wal,
       last_failed_time
FROM pg_stat_archiver;

同时检查:

physical replica receive/replay progress
logical consumer confirmed progress
archive command/repository health
base backup / restore / WAL summarization activity
checkpoint timing
WAL generation rate by workload
filesystem free bytes and inode headroom

pg_stat_archiver.failed_count 是累计量;单次历史失败不证明当前仍坏。需要看最近成功、 最近失败和时间窗增量。slot active=true 也只说明当前有人连接,不说明 restart_lsn 正在推进。

未完成备份也是恢复链的一部分

备份过程可能需要一段 WAL 才能形成一致恢复点。事故中贸然停止备份、删除 staging 或释放 WAL,可能让本来可用的恢复副本失效。记录:

backup run id / start LSN / stop LSN
repository and archive confirmation
base backup progress
restore validation state
current RPO source
alternative healthy backup/replica

若空间即将耗尽,可以同时减少非关键写入,降低 WAL 增长率;这只是争取时间,不是 修复 retention owner。

slot 的有限上限也不是无损方案

max_slot_wal_keep_size 可以限制 checkpoint 时允许 replication slot 保留的 WAL; 超过可用范围时 slot 可能不再可继续使用,wal_status/invalidation_reason 会反映 状态。它是防止磁盘无限增长的风险取舍,不保证 consumer 无损恢复。配置前必须明确:

consumer maximum outage
WAL generation envelope
alert lead time
reseed procedure
accepted data-loss semantics

34.6.3 绝不手工删除 pg_wal;保护现场后转 ch21/ch28/ch35

为什么文件看起来“旧”也不能删

PostgreSQL 自己依据 checkpoint、recovery、归档和复制需要管理 WAL segment。文件名 顺序不能告诉操作者某段是否仍被 crash recovery、standby、backup 或 timeline history 需要。运行中用 rm 删除 pg_wal

  • 不会更新 control/catalog/slot 状态;
  • 可能让当前实例在 crash recovery 时缺日志;
  • 可能让 replica、PITR 或 backup 无法继续;
  • 会破坏最重要的事故证据;
  • 当前进程暂时继续运行,也不能证明下次 restart 可恢复。

正确做法是:

reduce noncritical WAL generation
preserve SQL + filesystem + archive evidence
identify exact retention owner
repair archive/consumer or establish a new recovery source
release only an exact, authorized object through PostgreSQL
verify recovery chain and filesystem headroom

路由到正确章节

  • XID、vacuum、freeze、膨胀:转第 28 章
  • 归档、备份、恢复链:转第 21 章
  • 已发生文件缺失、checksum/页/索引异常:保护现场,转 第 35 章
  • 错删/误写但物理集群健康、需要时间点恢复:结合 第 32 章

每次转交都带:

facts:
  system_identifier_timeline: ...
  current_and_retained_lsn: ...
  owner_consumer: ...
  archive_backup_state: ...
  growth_rate_time_to_full: ...
actions_already_taken: [...]
unknowns: [...]
forbidden_actions:
  - manual delete pg_wal
  - drop unknown slot

34.6.4 本章的一般止血动作不构成保留型修复

常见伪修复

动作 可能短期效果 为什么没修复保留边界
cancel 普通慢查询 降低 CPU/I/O 不一定命中持 xmin 的事务
限制新连接 减少 flow slot/归档边界仍不推进
增加磁盘 延后满盘 owner 与恢复链仍旧失效
重启数据库 清部分 session prepared xact/slot/归档问题仍在,且增加恢复风险
drop 所有 inactive slot 快速释放 WAL 破坏未知 consumer 的恢复能力
删除 WAL 文件 目录变小 数据库状态未修复,恢复链被破坏

增加磁盘在硬故障逼近时可以是合法的时间购买动作,但报告必须写:

temporary headroom gained
new estimated time to full
retention owner unchanged
permanent repair owner/deadline
rollback or capacity reconciliation

保留型成功标准

不是“磁盘百分比下降”,而是:

exact retention owner identified
oldest required horizon is advancing or deliberately re-established
consumer/archive/backup has a valid recovery path
released capability and accepted loss are recorded
WAL/XID growth rate returns inside envelope
replicas and PITR validation pass
temporary storage/traffic controls are reconciled

如果唯一能说的是“删完之后 PostgreSQL 还在运行”,事故尚未被安全恢复。


上一节:流量型止血动作 · 返回本章目录 · 下一节:平台级流量控制与证据 · 查看全书目录 · 查看索引中心

34.7 平台级流量控制与证据

Pigsty 把 PostgreSQL、Patroni、HAProxy、PgBouncer、监控和配置管理组合成平台。它提供 多个可以减压或隔离的控制点,但不会替操作者判断一致性语义,也不会把一条面板曲线 自动变成根因。

34.7.1 从服务端点隔离批处理和只读流量

端口背后是服务契约

Pigsty 的默认服务意图通常是:

服务 常见端口 默认目标 适用工作
primary 5433 当前主库的 PgBouncer 短 OLTP 读写
replica 5434 可读节点的 PgBouncer 可容忍副本语义的读
default 5436 当前主库 PostgreSQL 直连 管理、迁移、session-sensitive
offline 5438 offline/replica PostgreSQL 直连 受控 OLAP/ETL

这些是参考配置,不是 PostgreSQL 固有端口;必须以当前 inventory 与生成配置为准。 HAProxy 通常用 Patroni role endpoint 判后端资格,PgBouncer 再把大量 client connection 映射为受控 server connection。

正确隔离:

OLTP write -> primary pooled service
read-with-staleness-contract -> replica pooled service
long report/ETL -> offline service + separate budget
DDL/admin/session semantics -> controlled direct service

不正确隔离:

all SELECT -> replica, regardless of read-your-write
all long queries -> offline, regardless of shared storage/CPU
database slow -> send everything to every replica
primary service full -> bypass PgBouncer through direct service

direct service 是为需要 session/管理语义的受控客户端准备的路径,不是池满时的逃生 后门。若应用能无治理地从 5433 切到 5436,连接预算就失去意义。

路由变化也要验收数据语义

切到 replica/offline 后检查:

SELECT pg_is_in_recovery(),
       current_setting('transaction_read_only'),
       pg_last_wal_receive_lsn(),
       pg_last_wal_replay_lsn(),
       pg_last_xact_replay_timestamp();

还要用业务 token 验证 staleness/可见性。transaction_read_only=on 只证明 session 写保护,不证明读到了业务所需的新鲜数据。

34.7.2 用连接池、代理与应用控制点逐级减压

每层职责

application
  request priority, deadline, idempotency, retry budget

PgBouncer
  client waiting, server-pool concurrency, pool mode, reserve

HAProxy
  role-aware backend eligibility, connection limits, queue, drain

PostgreSQL
  hard backend limit, statement/lock/session guard, workload execution

越靠上越理解业务价值,越靠下越能保护数据库硬边界。完整防线要组合,而不是希望 PgBouncer 一层解决所有过载。

先采配置事实

在变更前保存:

inventory revision and rendered config hash
service frontend/backend mapping
HAProxy health predicate and backend state
PgBouncer pool mode and per-database/user pool settings
client/server/waiting counts
PostgreSQL max/reserved/current connections
application instance and local-pool counts

Pigsty 可以通过 database/user 定义映射连接池参数;修改应回到声明式 inventory 和 受控部署流程。事故中对运行时做临时操作时,必须记录 drift,并在稳定后选择:

promote the change into config-as-code
or explicitly revert runtime state

否则下次部署会“神秘地”覆盖救火配置。

减压顺序

1. application: reject/queue low priority and cap retry
2. scheduler: pause batch producers
3. pool: preserve OLTP/admin budget, constrain noisy class
4. proxy: drain ineligible or overloaded path when evidence supports it
5. database: exact cancel, timeout, role/session controls

不要同时把 proxy backend 摘除和把应用全部转 direct;前者减少一条路径,后者可能在 另一条路径绕过所有 pool 限制。

故障切换期间的额外风险

切换会让旧 server connections 断开,新主同时接受大量重连。连接池要在新主 admission 前限速,HAProxy health 收敛与 Patroni role 收敛也要分别观测。恢复后逐级 prewarm, 不要让所有 pod 同时填满 pool。第 19、20、33 章分别给出连接路径、HA 和故障切换的 完整证据模型。

34.7.3 从面板判断范围,再用 SQL 与主机证据判型

面板适合回答“哪里、何时、范围多大”

Pigsty 的 PostgreSQL 监控可以把 cluster、instance、database、query、connection、 WAL、replication、checkpoint 和 host 指标放到同一时间线。事故开场先用面板定位:

single query / database / instance / whole cluster?
primary only or replicas too?
started at deploy, backup, checkpoint, traffic event, or failover?
connections, CPU, I/O, WAL and user latency which changed first?
is the signal rising, flat, oscillating, or recovering?

面板是索引,不是最终证据。采样、聚合、label 和 exporter 失败都可能造成误解;告警 静默也可能是监控链坏了。

回到原生 SQL

流量型最小 SQL:

pg_stat_activity by app/state/wait
pg_blocking_pids root tree
pg_stat_statements workload deltas
pg_stat_database temp/deadlock/session deltas
pg_stat_io by backend/context

保留型最小 SQL:

backend_xid/backend_xmin
pg_prepared_xacts
pg_replication_slots xmin/catalog_xmin/restart_lsn/status
pg_stat_archiver
replication receive/replay LSN
database/relation freeze age

SQL 再与主机证据互证:

filesystem mount and inode
device latency/queue/errors
memory pressure/swap/OOM
CPU run queue/steal/throttle
kernel and service events

时间与身份要可关联

每份证据包含:

captured_at_utc: ...
observer: ...
cluster_instance_database: ...
query_or_command_version: ...
source_config_revision: ...
redaction: ...

SQL 快照不要导出不必要的完整 query、口令、连接 URI 或业务数据。监控截图要保存 panel 时间窗、timezone、变量和 dashboard revision;否则复盘时无法重现。

平台动作的验收

Pigsty/HAProxy/PgBouncer state
  says intended route/pool changed

PostgreSQL
  proves actual role, session population, wait and retention state

host
  proves resource pressure changed

synthetic/business probe
  proves user-facing contract recovered

四层有冲突时保留冲突,不要挑最漂亮的图作为结论。


上一节:保留型故障的安全路由 · 返回本章目录 · 下一节:实战:同一症状、两种成因 · 查看全书目录 · 查看索引中心

34.8 实战:同一症状、两种成因

本实验不给操作者“连接风暴实验”和“WAL 实验”两个有答案的按钮。runner 随机安排 两种根因、生成无语义 case ID,classifier 只能看证据:

common alert: postgresql-resource-headroom-at-risk

case opaque-A:
  connection / retention / engine / filesystem observations

case opaque-B:
  connection / retention / engine / filesystem observations

hidden answer 在分类完成后才用于验收。这样练习的是判型,不是背剧本顺序。

34.8.1 随机注入连接风暴或 WAL 保留

先读实验合同

先做不连接远端的静态检查:

static/labs/ch34/task.sh lint

只读现场采集:

export PG36_EVIDENCE_DIR="$(
  mktemp -d "${TMPDIR:-/tmp}/pg36-ch34-capture.XXXXXX"
)"
static/labs/ch34/task.sh capture

它读取 managed pg-test 的 Patroni 成员、primary system identifier/timeline、连接与 replication slot 投影,并确认 pg-test-3 没有残留实验 root。它不执行 SQL 写入、 cancel、slot、service、route 或 DCS 变更。

完整演练 guard

export PG36_EVIDENCE_DIR="$(
  mktemp -d "${TMPDIR:-/tmp}/pg36-ch34.XXXXXX"
)"
export PG36_CH34_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH34_NONPRODUCTION=true
export PG36_CH34_PRODUCTION_DATA=false
export PG36_CH34_PRODUCTION_TRAFFIC=false
export PG36_CH34_CONFIRM=BLIND_FLOW_VS_RETENTION_CH34

static/labs/ch34/task.sh drill:overload

任一 guard 不符,runner 在创建远端目录前退出。evidence directory 已包含文件也会被 拒绝,避免混合两次 run。

隔离引擎

在线 fault 不发生在 managed PostgreSQL,而是在 pg-test-3 创建:

/tmp/pg36-ch34-overload-<uuid>/
  .pg36-ch34-owned
  data/
  socket/
  postgres.log

关键限制:

PostgreSQL 18.6
listen_addresses=''
Unix socket mode 0700
port 55444 only names the private socket
max_connections=24
superuser_reserved_connections=3
max_replication_slots=4
checksums=on
not a Patroni/DCS/HAProxy/PgBouncer member

实验不注入 OOM、不填满文件系统、不 drop cache,也不执行错误动作。无论正常或失败, 只在 marker 与 exact UUID root 匹配后停止临时 postmaster 并删除整个一次性目录。

两个真实 fault

FLOW:

30 non-superuser psql clients
same advisory lock
one holder sleeps for 20 seconds
other admitted clients wait on Lock
excess clients hit the connection boundary

RETENTION:

one exact inactive physical replication slot
immediately reserved restart_lsn
bounded WAL generation
stop after retained >= 32 MiB
hard cap 128 MiB

max_slot_wal_keep_size=-1 只在这个一次性实例中用于稳定展示保留机制,绝不是生产推荐 值。

34.8.2 在不知道答案时先判型,再选择动作

classifier 唯一允许读取的字段

{
  "case_id": "opaque",
  "observed_at": "UTC",
  "connection": {
    "observed_sessions": 0,
    "connection_rejections": 0,
    "lock_waiters": 0
  },
  "retention": {
    "inactive_physical_slots": 0,
    "retained_wal_bytes": 0
  },
  "engine": {},
  "filesystem": {}
}

判定合同:

FLOW =
  observed_sessions >= 18
  AND connection_rejections >= 1
  AND lock_waiters >= 1
  AND inactive_physical_slots = 0

RETENTION =
  inactive_physical_slots = 1
  AND retained_wal_bytes >= 33554432
  AND connection_rejections = 0

both or neither =
  STOP_AND_INVESTIGATE

classifier 不读取 hidden-answers.json。validator 会比较 case identity set、字段完整性、 classifier provenance 与 hidden answer;blind packet 若带 truthexpected_route 字段反而判失败。

正式 run

公开证据: overload-run.json

run id         1ae188cd-e7f2-4724-98d8-fe166cf4e33f
random order   RETENTION -> FLOW

WAL case 先出现,但 classifier 没把“第一个”解释成 flow:

inactive physical slots     1
retained WAL       42,611,296 bytes
connection rejects          0
route        PRESERVE_RETENTION_EVIDENCE

connection case:

attempted clients           30
observed sessions           21
connection rejects           9
lock waiters                20
inactive physical slots      0
route           RELIEVE_FLOW_PRESSURE

24 个 max_connections 减去 3 个 superuser reserved slots,正好留下 21 个普通 admission; 其余 9 个被拒绝。20 个 lock waiter 加 1 个持锁/睡眠 session 又构成 21 个已进入会话。 这是该临时配置下的可解释闭环,不应外推为任何生产集群连接预算。

34.8.3 对流量型恢复服务,对保留型完成安全路由

FLOW 动作

runner 用 run-specific application prefix:

pg36-ch34-flow-<run-prefix>-<client-number>

只对匹配 prefix 的 backend 执行 pg_cancel_backend。正式结果:

cancel signals sent                    21
broad cancel used                   false
max_connections changed             false
fallback client terminate/kill        0/0
post fixture sessions                   0
post SQL probe                          1

若 backend 没在 deadline 内退出,唯一 fallback 是 runner 直接持有的 exact child process; 不会扫描或终止主机上的其他 psql,更不会触碰 managed pg-test

生产映射不是“按 prefix 杀 21 个会话”,而是:

identify admission class and owner
stop producer/retry
preserve admin headroom
cancel exact expired/low-value work
verify queue and useful completion

RETENTION 动作

runner 先保存:

slot_name/type/active
restart_lsn
retained_wal_bytes
wal_status/safe_wal_size/invalidation_reason
filesystem projection

然后只 drop 带本 run identity 的 disposable slot:

scope                     exact-owned-disposable-slot
evidence preserved        before action
manual pg_wal deletion    false
post physical slots       0
post SQL probe            1

生产上的 inactive slot 没有这份授权。对应动作是联系 owner、确认 consumer/backup/RPO, 恢复消费或建立新的恢复起点,再由明确负责人批准 exact release。

managed 边界复核

before/after 都要求:

pg-test-1 primary running
pg-test-2/3 replica streaming
system identifier unchanged
timeline 19 unchanged
chapter fixture sessions 0
matching disposable roots []

公开报告因此写的是 managed mutations=0,而不是声称在 Pigsty managed cluster 上 验证了饱和行为。

34.8.4 输出动作时间线、误判代价与容量改进项

evidence bundle

before.json
exercise/
  run-manifest.json
  source-manifest.json
  exercise-evidence.json
  blind-packets.json
  hidden-answers.json
  cleanup.json
classification.json
after.json
validation-report.json
negative-report.json
public-summary.json
review.txt

已有完整证据包可重复做只读校验:

export PG36_EVIDENCE_DIR=/absolute/evidence/ch34/run
static/labs/ch34/task.sh all

validator 将 12 个实验 source file 与 SHA-256 绑定,并实际构造 34 个 mutant。反例覆盖:

production/managed mutation guard opened
scenario or classifier contract weakened
blind packet leaks truth or misses fields
classification duplicated/unsupported/wrong
flow evidence below threshold or broad cancel
retention slot/bytes/active state falsified
manual WAL deletion or leftover slot claimed
cleanup root left behind
production gate falsely approved

34 个都必须被拒绝;声明有 34 条 JSON 并不等于做了对抗验证。

误判代价

误判 结果
flow 当 retention 不限制 admission,连接失败与队列继续
retention 当 flow cancel session 不推进 restart_lsn,WAL 继续增长
unknown 强判 flow 可能 terminate 关键/大事务,并留下既有 WAL、死版本与后续清理压力
unknown 强判 retention 可能 drop 有效 slot、破坏 consumer/RPO
手工删 pg_wal 破坏 crash recovery、复制、备份与现场证据

因此 unknown route 不是实验的“第三种错误答案”,而是证据不足时唯一正确的动作类别。

从实验回写容量控制

这次结果至少导出四个可验证改进:

  1. 为普通连接显式保留 admin/reserved budget,并让 pool 上限小于硬边界;
  2. 对 application pool 总和、retry 与启动 prewarm 做全局预算;
  3. 对每个 slot 记录 owner、active、restart/catalog xmin、retained bytes 与推进率;
  4. 告警同时包含“当前余量”和“增长率/耗尽时间”,并带 FLOW/RETENTION 判型链接。

不能从这次实验导出:

production max_connections should be 24
every inactive slot may be dropped after 32 MiB
42 MB WAL implies a disk incident
exact cancel always has zero fallback
managed Pigsty can tolerate the same storm

最终门禁保持:

production_ch34_gate=pending

上一节:平台级流量控制与证据 · 返回本章目录 · 下一章:数据抢救与工程取证——起死回生 · 查看全书目录 · 查看索引中心