过载保护与资源故障判型——李代桃僵
34 过载保护与资源故障判型——李代桃僵
数据库“慢、满、连不上”时,最危险的动作往往不是没有动作,而是把正确手段用在了 错误根因上:
本章把资源事故分成两条首先必须分开的路径:
二者可以同时发生,也可能都不是。如果证据不足,正确路线不是猜一个,而是
STOP_AND_INVESTIGATE:停止破坏性清理、冻结新增变量、保留 SQL 与主机证据,并明确
尚未回答的问题。
学习完成标准
完成本章后,读者应能:
- 把“CPU 高、磁盘满、延迟高、连接失败”视为症状,而不是根因;
- 区分流量型竞争与 WAL/XID/slot/归档/长事务造成的保留型压力;
- 解释到达率、服务率、并发、队列和超时为什么会形成正反馈;
- 为应用池、PgBouncer、HAProxy 与 PostgreSQL 分配一致的连接预算;
- 识别健康检查、短连接和无抖动重试造成的隐藏放大;
- 用
pg_stat_activity、wait event、阻塞树和执行计划识别失控工作; - 区分
pg_cancel_backend与pg_terminate_backend的影响和权限边界; - 在结束长事务或大事务前评估锁释放、中止清理、既有 WAL、死版本与后续 vacuum 成本;
- 用并发最坏值估算
work_mem、并行 worker 与连接数的内存风险; - 将 PostgreSQL 的 I/O 证据与主机设备延迟、队列和文件系统余量互证;
- 为限流、熔断、摘流、取消和只读降级写出收益、代价、停止线与回退;
- 从
backend_xmin、复制槽xmin/catalog_xmin和 prepared transaction 判断 XID 保留者; - 从
restart_lsn、wal_status、归档与备份状态判断 WAL 保留者; - 解释为什么绝不能在运行中的实例里手工删除
pg_wal文件; - 用 Pigsty 的服务端点、连接池与监控缩小影响范围,但回到 PostgreSQL/OS 证据判型;
- 在不知道盲测答案时选择正确路线,并拒绝 34 类越界或误判证据。
一张判型表
| 问题 | 流量型 | 保留型 |
|---|---|---|
| 核心状态 | 到达工作超过可服务能力 | 最老的必需历史边界不能前进 |
| 典型信号 | 连接拒绝、队列、锁等待、CPU/I/O 饱和 | inactive slot、旧 xmin、归档失败、WAL 累积 |
| 第一目标 | 减少 admission、并发或单项成本 | 找到 owner 与恢复来源,保护 lineage |
| 可以立即做 | 限流、暂停批处理、精确 cancel、降级 | 留证、隔离增长、恢复消费者、评估精确释放 |
| 不应盲做 | 临时放大连接/内存,广域 terminate | cancel 普通查询、删 pg_wal、随意 drop slot |
| 成功证据 | 队列下降、拒绝停止、业务探针恢复 | 保留边界推进、归档/消费者恢复、恢复链完整 |
资源余量也不是一个百分比。至少要同时表达:
其中 $L_r$ 是资源 $r$ 的安全上限,$U_r$ 是当前与已承诺使用量。对连接、内存、WAL 空间、XID age、I/O 服务能力分别计算的 $H_r$ 不能相互替代。磁盘还有 30% 并不能 证明连接有余量;CPU 只有 20% 也不能证明 WAL 保留安全。
对流量型队列,若一段持续窗口内到达率 $\lambda$ 大于完成率 $\mu$:
队列 $Q$ 就会增长。平均值暂时正常也救不了尾延迟;重试还会反过来抬高 $\lambda$。 对保留型压力,真正需要测的是“最老仍被需要的位置”及其推进速度,而不是只看目录 当前大小。
事故时的四步闭环
每一步都要记录 UTC 时间、证据引用、操作者、预期收益、停止条件和实际结果。仅仅看到 面板曲线下降,不能说明动作正确:流量也可能因为所有客户端都超时而“下降”。
正式实验
本章在已确认的 Pigsty 开发沙箱做两层实验:
runner 用系统随机源安排两个 blind case,classifier 只能读取共同告警
postgresql-resource-headroom-at-risk 及观测字段,不能读取 hidden truth。正式顺序为:
正式观测:
| 情形 | 关键证据 | 判定与动作 |
|---|---|---|
| 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 完成后:
验证器同时构造并拒绝 34 个真实 mutant,包括生产边界被打开、blind packet 泄露答案、
阈值被削弱、广域 cancel、slot 仍残留以及谎报清理成功。公开证据见
overload-run.json。
这份实验能证明在该隔离 PG18 合同内两类证据可区分,且精确动作能复位 fixture。
它不证明生产连接上限、真实 OOM victim、文件系统填满行为、归档仓库故障或未知
replication slot 可以安全删除;最终门禁固定为 production_ch34_gate=pending。
阅读前后关系
本章目录
34.1 第一动作:流量型还是保留型
- 34.1.1 流量增长、慢查询、锁与连接导致的竞争
- 34.1.2 WAL、XID、复制槽、归档和长事务导致的保留
- 34.1.3 同样表现为“磁盘满”或“延迟高”,动作可以相反
- 34.1.4 判型不清时先停止破坏性清理
34.2 连接风暴与排队失控
34.3 失控查询、锁与事务
34.4 CPU、内存、I/O 与 OOM
34.5 流量型止血动作
34.6 保留型故障的安全路由
- 34.6.1 XID:检查
backend_xmin、复制槽xmin与pg_prepared_xacts - 34.6.2 WAL 撑盘:检查归档失败、复制槽和未完成备份
- 34.6.3 绝不手工删除
pg_wal;保护现场后转 ch21/ch28/ch35 - 34.6.4 本章的一般止血动作不构成保留型修复
34.7 平台级流量控制与证据
34.8 实战:同一症状、两种成因
- 34.8.1 随机注入连接风暴或 WAL 保留
- 34.8.2 在不知道答案时先判型,再选择动作
- 34.8.3 对流量型恢复服务,对保留型完成安全路由
- 34.8.4 输出动作时间线、误判代价与容量改进项
权威参考
PostgreSQL:
- Monitoring Database Activity
pg_stat_activityand cumulative statistics- System Administration Functions
pg_replication_slots- Resource Consumption
- Kernel Resources and Linux OOM
- Replication Configuration
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 窗口的证据放在一起:
再对照:
state='active' 不表示正在使用 CPU;PostgreSQL 文档明确指出 state 与 wait_event
相互独立。active 且 wait_event_type='Lock' 的 backend 正在执行语句,但实际被阻塞。
这也是为什么“active 数很多”必须继续拆成 running、lock wait、I/O wait 与 client wait。
判定成立的最低条件
把问题判为 flow,至少应看到一条能够闭合的因果链:
仅凭 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 |
这里重要的是最老边界及其速度:
inactive slot 不等于废弃 slot。它可能对应暂时离线的副本、迁移、CDC 消费者或恢复流程; active slot 也不等于健康,消费者可能连着却不推进。必须把 slot 映射到服务 owner、 consumer、恢复承诺和最后成功时间。
先测增长,再谈释放
两个快照比一个快照更有意义:
若 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、工作量变化 |
因此动作可以完全相反:
把 retention 当 flow,取消再多普通查询也不会推进 restart_lsn。把 flow 当 retention,
忙着调查 slot 而不限制重试,服务可能先被连接和内存击穿。
同时发生怎么办
WAL slot 滞留期间又发生写入洪峰并不矛盾。事故记录应允许:
“只能选一个根因”是复盘分类,不是在线处理原则。
34.1.4 判型不清时先停止破坏性清理
unknown 是一种有效状态
下面任一项不明,就不能执行不可逆释放:
先收集最小证据包:
然后将路线写成机器与人都能审阅的三态判定:
停止线包括:有人建议删 pg_wal、drop 未知 slot、终止未知大事务、在唯一副本上试验
危险参数、或以“磁盘快满”为由跳过 owner/backup 识别。此时应保护现场并回到第 31 章
的事故指挥框架。
本节交付物
进入具体止血前,至少写出:
没有这些字段,“先重启看看”不是 runbook。
返回本章目录 · 下一节:连接风暴与排队失控 · 查看全书目录 · 查看索引中心
34.2 连接风暴与排队失控
PostgreSQL 采用一个 client connection 对应一个 backend process 的模型。连接不仅占一个 数字,还需要进程、内存、认证、catalog 初始化、socket、锁表与调度成本。连接池的 价值不是让数据库接受无限请求,而是把大量 client concurrency 变成有上限的 database concurrency。
34.2.1 数据库连接、代理池与应用池三层
三层都在排队
每层至少有四个量:
只看 PostgreSQL numbackends 会漏掉池前的队列;只看应用 pool size 又会漏掉多个
pod/进程/租户汇总后对数据库的总承诺。
一个粗略预算应满足:
其中 $A_i$ 是第 $i$ 类应用实例数,$P_i$ 是每实例可能占用的 server connection, $M$ 是迁移、运维和监控预算,$B$ 是故障切换/伸缩缓冲。对 PostgreSQL:
这里不是要求把所有层的上限设成同一个数。应用 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 全面争用。只有在以下事实都成立时,调整才是经过评估的容量变更:
在线事故的默认路线是收紧 admission,而不是把硬边界向后推。
34.2.2 重试放大、健康检查和短连接
重试会把失败变成新流量
若原始到达率为 $\lambda_0$,每次失败平均触发 $r$ 次下一轮尝试,成功率没有及时恢复, 有效流量近似:
当 $r\ge1$ 且没有 retry budget、deadline 或熔断时,系统进入正反馈:
日志里的“请求量上升”可能不是用户流量,而是同一批请求的重复尝试。必须用稳定的 request/idempotency key 区分 original、retry 与 hedge。
健康检查也会成为负载
设 $N$ 个应用实例,每个实例维护 $P$ 个 worker,每 $h$ 秒建立一次检查连接,则单健康 检查一项就可能产生约 $NP/h$ 次每秒建连。以下设计尤其危险:
- 每个业务请求先新建连接执行
SELECT 1; - 每个 pod 同时启动并预热完整池;
- 多级代理各自以高频新连接探测;
- 故障时 autoscaling 新增实例,同时所有实例立刻重试;
- liveness 把短暂数据库慢判为应用死亡,形成重启风暴。
健康检查要区分:
数据库慢通常应先让应用 not-ready 或熔断新请求,而不是把所有应用进程重启。
长连接也不是免疫
已有连接在代理切换、数据库重启、证书轮换或网络抖动后会同时重连。池应具备:
不要给每一层各自配置十次重试。应用、驱动、service mesh、代理和任务框架叠加后, 最坏尝试次数是乘法。
34.2.3 限流、队列、连接预算与指数退避
把过载变成显式 admission
好的过载控制不是“永不拒绝”,而是在系统仍能完成高价值工作时,尽早、明确地拒绝 超出预算的工作:
队列必须同时有:
- 最大长度,防止内存成为下一瓶颈;
- 最大等待时间,过期工作不再进入数据库;
- 公平性或优先级,避免批处理饿死 OLTP;
- 可观测的 admitted/waited/rejected/expired 计数;
- drain 与 deploy 行为,避免发布时丢失或翻倍。
无限队列只是把快速失败变成更晚失败。Little’s Law 给出稳定系统中的关系:
当平均等待 $W$ 上升时,在途数量 $L$ 也上升;如果请求 deadline 已经小于排队时间, 即使最终执行成功,对用户也没有价值。
指数退避要带随机抖动
一个常见策略:
重试条件也要按错误分类:
| 错误 | 默认处理 |
|---|---|
| 认证/权限/语法 | 不重试,修配置或代码 |
| 连接拒绝/切换窗口 | 有预算、带 jitter 重试 |
| statement timeout | 先判是否仍在数据库执行及是否幂等 |
| deadlock/serialization failure | 整个事务按有限策略重试 |
| unknown COMMIT outcome | 先用业务 token 对账,不裸重放 |
连接事故的止血顺序
验收不是“连接数下降”,而是:
上一节:第一动作:流量型还是保留型 · 返回本章目录 · 下一节:失控查询、锁与事务 · 查看全书目录 · 查看索引中心
34.3 失控查询、锁与事务
“杀慢查询”不是故障判型。一个耗时最长的 session 可能是阻塞根节点、也可能是等待者; 可能正在做有价值的恢复,也可能已经超过用户 deadline;可能可以安全 cancel,也可能 已经写入海量 WAL 和死版本,terminate 也不会把这些成本自动抹掉。动作必须绑定 query、transaction、application、owner 与业务语义。
34.3.1 识别高消耗查询和阻塞根节点
当前现场与历史重查询分开
pg_stat_activity 说明此刻有哪些 backend、状态与等待;pg_stat_statements 聚合的是
一段时间内同类语句的执行统计。前者适合回答“谁现在占着资源”,后者适合回答“哪类
语句长期贡献最多”。不能用累计榜单代替当前事故现场。
一个不导出完整 SQL 文本的当前投影:
对历史工作量,可按目标排序,而不是永远按 total time:
版本、扩展列和统计起点应随报告一起记录。统计 reset 或重启后的短窗口不能与一周基线 直接比较。
找根阻塞者,不要只杀等待者
根 blocker 可能显示 idle in transaction,因为它已经执行完持锁语句,正在等客户端下
一条命令。仅筛选 state='active' 会漏掉它。也要排除 autovacuum、logical worker、
备份和维护工作等不同 backend_type,不要把每个 PID 都当作应用会话。
高消耗不是自动有罪
取消前回答:
对 query_id 做执行计划分析时,转到第 10、11 章的方法;在线事故中不要在主库上无界
执行 EXPLAIN ANALYZE 复现一条未知重查询。
34.3.2 cancel、terminate 与中止后成本
两个函数的边界
pg_cancel_backend 向目标 backend 发送取消当前 query 的请求。session 通常仍存在;
当前事务会进入错误状态,客户端需要 ROLLBACK。pg_terminate_backend 终止整个
session,连接断开,未提交事务由服务器回滚。两者都需要相应权限;不要通过给应用
超级用户来获得事故处置能力。
优先级一般是:
“exact”至少绑定:
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 已消失就宣称资源影响全部结束。
动作后必须复核
同时观察:
如果应用立刻重建同一 session,数据库端 cancel 只是短暂擦除症状,真正控制点在 admission 和 retry。
34.3.3 长事务和大事务结束前先评估后果
“长”与“大”是两个维度
长事务主要风险是 lock、backend_xmin、vacuum 回收与连接占用;大事务还带来 WAL、
dirty buffers、replication lag、终止响应与后续清理成本。prepared transaction 即使
没有活动 session,也可长期保留锁和 XID 状态:
不要看到 prepared transaction 就 ROLLBACK PREPARED。它属于两阶段提交协议,必须先
与 transaction manager/业务 ledger 对账,判断应 commit 还是 rollback。
结束前的后果清单
若事务包含数据库外部副作用,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 等待导致业务停滞。主机证据至少包括:
再按 PostgreSQL backend type 与 wait event 对齐:
PostgreSQL 官方建议把统计视图与操作系统工具结合,因为数据库 I/O 统计不能区分数据 来自物理设备还是内核 page cache。数据库看到 read,也不等于磁盘实际发生同量读取。
饱和、排队和抖动
平均设备延迟 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 与后台维护
前台与后台会争同一设备
下面工作可能同时写盘:
pg_stat_io 按 backend type、object 与 context 提供集群级 I/O 统计;pg_stat_database
的 temp 计数、日志中的 temporary file、pg_stat_checkpointer、pg_stat_archiver 和
进度视图分别补充来源。统计是累计量,需要记录起点并取差值:
列集合随 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 相互干扰。事故中记录:
暂停 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 也会
扩大总使用。粗略上界应按工作节点估算:
其中 participants 包括执行该 operation 的 leader 与 parallel workers。这个表达式的
关键不是算出一个恒定值,而是把“同时活跃的 session × 同时活跃的内存节点 × 参与
进程”三层并发都纳入预算。
再加上:
因此:
既不是准确实测,也不是足够保守的最坏值。它漏掉每 query 多个节点和并行,也忽略许多 非 work_mem 内存;反过来假设所有连接同时打满每个上限又可能极度悲观。容量测试要用 真实 workload envelope 和并发组合。
Linux OOM 不是数据库的流控机制
Linux overcommit 允许进程承诺超过物理内存的虚拟地址空间;真正耗尽时,OOM killer 可能选择某个进程。若 PostgreSQL child 被杀,postmaster 会把它当作异常退出,为保护 共享内存一致性,可能终止其他 server processes 并执行 crash recovery;若 postmaster 本身被杀,服务管理器行为又是另一条路径。
不要故意在共享/生产主机上“测一次 OOM”。安全实验应在有明确 cgroup/VM 边界的专用 环境里完成,并验证:
本章正式实验明确不注入 OOM。
事故动作
内存压力下优先:
- 阻止新低价值工作和重试;
- 识别 exact 高内存 query/role/pool;
- 暂停可恢复 batch 与并行任务;
- 用 query cancel 逐步释放,而不是一次 terminate 全部;
- 保留管理连接与 OS 控制面;
- 观察 reclaim、swap、RSS、队列和业务成功率;
- 稳定后修正连接预算、query、并行与内存配置。
不要在事故中 drop OS cache:它既不能修复内存承诺,还会把后续读取推向存储,破坏 现场并制造新的 I/O 峰值。也不要把 swap 使用本身等同于故障;关键是持续 swap in/out、 memory pressure 与用户影响。
资源证据矩阵
单张 top 截图不够支撑数据库重启。
上一节:失控查询、锁与事务 · 返回本章目录 · 下一节:流量型止血动作 · 查看全书目录 · 查看索引中心
34.5 流量型止血动作
流量型止血的目标不是让所有请求都继续进入,而是让系统重新获得完成有价值工作的 能力。在过载区间里,少接收一些工作通常比全部接收、全部超时更可用。
34.5.1 限流、熔断、摘除非关键负载
越靠近来源,拒绝成本越低
在应用 admission 拒绝一个尚未创建事务的请求,成本通常远低于让它拿到 database connection、执行一半、生成 WAL 后再取消。因此控制顺序优先:
- 阻止新的低优先级到达;
- 冻结无抖动重试、autoscaling 和批量 worker 扩张;
- 缩小 batch/report/migration pool;
- 对依赖数据库的非关键功能打开熔断或静态降级;
- 最后才在数据库内取消已经进入的 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 参与:
“所有 SELECT 都是低价值”或“写都更重要”并不成立。某些 read 是支付授权前置条件, 某些 write 只是可重算的埋点。
34.5.2 取消查询、暂停批处理与只读降级
取消只命中已证明的工作集
安全筛选示例:
生产中还应绑定数据库、role、query/job tag、变更单与 owner。LIKE 'report-worker:%'
只有在 application_name 受治理、不能被任意业务伪造时才够用。
取消后等待并验收,不要立即升级为 terminate:
暂停 producer 比逐条 cancel 更有效
batch 系统通常有 scheduler、queue consumer 或 worker deployment。先暂停 producer, 再处理 in-flight work;否则数据库每取消一条,调度器就补一条。暂停要记录:
如果任务没有 checkpoint 与幂等性,暂停本身可能产生业务不一致,需要应用 owner 决策。
只读降级有一致性前提
把读流量移到 replica 可以减少 primary 的部分 CPU/I/O,但必须回答:
不要把写请求改成“返回成功但不落库”,除非业务明确设计了 durable queue/ledger 和补偿。 也不要把所有查询涌向一台 replica:这可能让 replay 落后,进一步破坏读语义和 HA 候选质量。
Pigsty 的 replica/offline 服务可以表达路由意图,不能替代这些语义判断。路由后用
pg_is_in_recovery()、transaction_read_only、replay lag 与业务 token 复核。
34.5.3 每个动作写清收益、代价、停止条件和回退
动作卡,而不是命令清单
四个字段不可省:
- 收益:哪个指标应在多长时间内变化;
- 代价:谁被拒绝、延迟或需要补偿;
- 停止条件:什么证据说明假设错误或副作用更大;
- 回退:如何撤销临时配置、恢复任务并验证没有重放。
一次只改变可辨识的控制变量
同时扩容、重启、cancel、改 pool 和改路由,曲线即使恢复也无法知道哪个动作有效; 某个有害动作可能被另一个动作掩盖。事故很急时可以并行动作,但必须按独立目标分组, 记录精确时间和 owner:
涉及同一变量的相反动作不能并发,例如一个人缩 pool、另一个人扩大 max_connections。
止血成功的定义
至少同时满足:
服务恢复后不要马上取消全部限流。先保持观察窗口,逐级放量;每一级都验证完成率、 尾延迟、资源余量与重试量。一次把 backlog 全部释放,会制造第二次尖峰。
上一节:CPU、内存、I/O 与 OOM · 返回本章目录 · 下一节:保留型故障的安全路由 · 查看全书目录 · 查看索引中心
34.6 保留型故障的安全路由
保留型事故的第一原则是:
先证明“谁还需要哪段历史”,再决定是恢复消费者、迁移恢复来源,还是释放这项需要。
一条复制槽、一个 xmin 或一批 WAL 文件本身不是垃圾。它们是恢复、复制、快照或事务 协议的状态。未经 owner 与恢复链确认的“清理”,可能把空间问题变成不可恢复的数据 问题。
34.6.1 XID:检查 backend_xmin、复制槽 xmin 与 pg_prepared_xacts
先列出所有可能的 horizon owner
活动 backend:
复制槽:
两阶段事务:
再看 database/relation freeze age:
这些视图回答的是不同问题:
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,停止新工作,评估 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 目录可以因为正常高写入暂时变大,也可以因为保留者不推进持续增长。先记录:
同时检查:
pg_stat_archiver.failed_count 是累计量;单次历史失败不证明当前仍坏。需要看最近成功、
最近失败和时间窗增量。slot active=true 也只说明当前有人连接,不说明 restart_lsn
正在推进。
未完成备份也是恢复链的一部分
备份过程可能需要一段 WAL 才能形成一致恢复点。事故中贸然停止备份、删除 staging 或释放 WAL,可能让本来可用的恢复副本失效。记录:
若空间即将耗尽,可以同时减少非关键写入,降低 WAL 增长率;这只是争取时间,不是 修复 retention owner。
slot 的有限上限也不是无损方案
max_slot_wal_keep_size 可以限制 checkpoint 时允许 replication slot 保留的 WAL;
超过可用范围时 slot 可能不再可继续使用,wal_status/invalidation_reason 会反映
状态。它是防止磁盘无限增长的风险取舍,不保证 consumer 无损恢复。配置前必须明确:
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 可恢复。
正确做法是:
路由到正确章节
- XID、vacuum、freeze、膨胀:转第 28 章;
- 归档、备份、恢复链:转第 21 章;
- 已发生文件缺失、checksum/页/索引异常:保护现场,转 第 35 章;
- 错删/误写但物理集群健康、需要时间点恢复:结合 第 32 章。
每次转交都带:
34.6.4 本章的一般止血动作不构成保留型修复
常见伪修复
| 动作 | 可能短期效果 | 为什么没修复保留边界 |
|---|---|---|
| cancel 普通慢查询 | 降低 CPU/I/O | 不一定命中持 xmin 的事务 |
| 限制新连接 | 减少 flow | slot/归档边界仍不推进 |
| 增加磁盘 | 延后满盘 | owner 与恢复链仍旧失效 |
| 重启数据库 | 清部分 session | prepared xact/slot/归档问题仍在,且增加恢复风险 |
| drop 所有 inactive slot | 快速释放 WAL | 破坏未知 consumer 的恢复能力 |
| 删除 WAL 文件 | 目录变小 | 数据库状态未修复,恢复链被破坏 |
增加磁盘在硬故障逼近时可以是合法的时间购买动作,但报告必须写:
保留型成功标准
不是“磁盘百分比下降”,而是:
如果唯一能说的是“删完之后 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。
正确隔离:
不正确隔离:
direct service 是为需要 session/管理语义的受控客户端准备的路径,不是池满时的逃生 后门。若应用能无治理地从 5433 切到 5436,连接预算就失去意义。
路由变化也要验收数据语义
切到 replica/offline 后检查:
还要用业务 token 验证 staleness/可见性。transaction_read_only=on 只证明 session
写保护,不证明读到了业务所需的新鲜数据。
34.7.2 用连接池、代理与应用控制点逐级减压
每层职责
越靠上越理解业务价值,越靠下越能保护数据库硬边界。完整防线要组合,而不是希望 PgBouncer 一层解决所有过载。
先采配置事实
在变更前保存:
Pigsty 可以通过 database/user 定义映射连接池参数;修改应回到声明式 inventory 和 受控部署流程。事故中对运行时做临时操作时,必须记录 drift,并在稳定后选择:
否则下次部署会“神秘地”覆盖救火配置。
减压顺序
不要同时把 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 指标放到同一时间线。事故开场先用面板定位:
面板是索引,不是最终证据。采样、聚合、label 和 exporter 失败都可能造成误解;告警 静默也可能是监控链坏了。
回到原生 SQL
流量型最小 SQL:
保留型最小 SQL:
SQL 再与主机证据互证:
时间与身份要可关联
每份证据包含:
SQL 快照不要导出不必要的完整 query、口令、连接 URI 或业务数据。监控截图要保存 panel 时间窗、timezone、变量和 dashboard revision;否则复盘时无法重现。
平台动作的验收
四层有冲突时保留冲突,不要挑最漂亮的图作为结论。
上一节:保留型故障的安全路由 · 返回本章目录 · 下一节:实战:同一症状、两种成因 · 查看全书目录 · 查看索引中心
34.8 实战:同一症状、两种成因
本实验不给操作者“连接风暴实验”和“WAL 实验”两个有答案的按钮。runner 随机安排 两种根因、生成无语义 case ID,classifier 只能看证据:
hidden answer 在分类完成后才用于验收。这样练习的是判型,不是背剧本顺序。
34.8.1 随机注入连接风暴或 WAL 保留
先读实验合同
- 环境、阈值与风险:
requirements.json - blind classifier 路由:
classification-contract.json - 34 个反例:
negative-cases.json - 人类可读边界:
lab-contract.md - managed/disposable 拓扑:
topology.mmd
先做不连接远端的静态检查:
只读现场采集:
它读取 managed pg-test 的 Patroni 成员、primary system identifier/timeline、连接与
replication slot 投影,并确认 pg-test-3 没有残留实验 root。它不执行 SQL 写入、
cancel、slot、service、route 或 DCS 变更。
完整演练 guard
任一 guard 不符,runner 在创建远端目录前退出。evidence directory 已包含文件也会被 拒绝,避免混合两次 run。
隔离引擎
在线 fault 不发生在 managed PostgreSQL,而是在 pg-test-3 创建:
关键限制:
实验不注入 OOM、不填满文件系统、不 drop cache,也不执行错误动作。无论正常或失败, 只在 marker 与 exact UUID root 匹配后停止临时 postmaster 并删除整个一次性目录。
两个真实 fault
FLOW:
RETENTION:
max_slot_wal_keep_size=-1 只在这个一次性实例中用于稳定展示保留机制,绝不是生产推荐
值。
34.8.2 在不知道答案时先判型,再选择动作
classifier 唯一允许读取的字段
判定合同:
classifier 不读取 hidden-answers.json。validator 会比较 case identity set、字段完整性、
classifier provenance 与 hidden answer;blind packet 若带 truth 或 expected_route
字段反而判失败。
正式 run
公开证据:
overload-run.json
WAL case 先出现,但 classifier 没把“第一个”解释成 flow:
connection case:
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:
只对匹配 prefix 的 backend 执行 pg_cancel_backend。正式结果:
若 backend 没在 deadline 内退出,唯一 fallback 是 runner 直接持有的 exact child process;
不会扫描或终止主机上的其他 psql,更不会触碰 managed pg-test。
生产映射不是“按 prefix 杀 21 个会话”,而是:
RETENTION 动作
runner 先保存:
然后只 drop 带本 run identity 的 disposable slot:
生产上的 inactive slot 没有这份授权。对应动作是联系 owner、确认 consumer/backup/RPO, 恢复消费或建立新的恢复起点,再由明确负责人批准 exact release。
managed 边界复核
before/after 都要求:
公开报告因此写的是 managed mutations=0,而不是声称在 Pigsty managed cluster 上
验证了饱和行为。
34.8.4 输出动作时间线、误判代价与容量改进项
evidence bundle
已有完整证据包可重复做只读校验:
validator 将 12 个实验 source file 与 SHA-256 绑定,并实际构造 34 个 mutant。反例覆盖:
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 不是实验的“第三种错误答案”,而是证据不足时唯一正确的动作类别。
从实验回写容量控制
这次结果至少导出四个可验证改进:
- 为普通连接显式保留 admin/reserved budget,并让 pool 上限小于硬边界;
- 对 application pool 总和、retry 与启动 prewarm 做全局预算;
- 对每个 slot 记录 owner、active、restart/catalog xmin、retained bytes 与推进率;
- 告警同时包含“当前余量”和“增长率/耗尽时间”,并带 FLOW/RETENTION 判型链接。
不能从这次实验导出:
最终门禁保持:
上一节:平台级流量控制与证据 · 返回本章目录 · 下一章:数据抢救与工程取证——起死回生 · 查看全书目录 · 查看索引中心