云数据库 PostgreSQL 安全更新插件 pg_savior

云数据库 PostgreSQL 提供安全更新插件 pg_savior,这里说明关于安全更新插件 pg_savior 的说明及使用方法。

相关产品
云数据库 PostgreSQL 产品页,含版本规格、价格与购买入口。

一、概述

pg_savior 是一个用于云数据库 PostgreSQL / PostgreSQL 的安全更新保护插件。该插件通过内核 Hook 在 SQL 执行前拦截高风险 DML 和 DDL 操作,防止因误执行无条件 DELETE、无条件 UPDATE、大表 TRUNCATE、大表 DROP TABLE、非并发建索引等操作造成数据丢失、长时间锁表或业务不可用。

二、支持版本

v17.10_r1.19、v18.4_r1.10及以上版本。

三、使用限制

·CREATE EXTENSION pg_savior 仅用于在当前数据库注册扩展信息,不会单独激活保护能力。保护能力由 pg_savior 共享库中的 Hook 实现,必须通过 shared_preload_libraries、session_preload_libraries 或 LOAD 'pg_savior' 加载共享库后生效。

·如需对所有会话生效,建议将 pg_savior 配置到 shared_preload_libraries 并重启数据库。

·pg_savior.enabled 和 pg_savior.bypass 均为会话级参数,变更后只影响当前会话。

·pg_savior.max_rows_affected 使用优化器估算行数进行判断,不是实际影响行数。估算结果依赖统计信息,表数据变化较大时建议先执行 ANALYZE。

·pg_savior.large_table_threshold_rows 使用 pg_class.reltuples 判断表大小,判断结果同样依赖统计信息。

·分区表或继承表场景下,插件会检查父表及其所有子表,取最大 reltuples 作为判断依据。

·TRUNCATE … CASCADE 场景下,插件会检查被级联影响的外键关联表,避免通过小表级联清空大表。

·该插件用于防误操作,不替代权限管理、SQL 审核、备份恢复、灰度发布等安全治理能力。

四、功能概览

pg_savior 当前支持如下保护能力:

保护类型 默认行为 说明
DELETE 无 WHERE 阻断 防止整表删除。
UPDATE 无 WHERE 阻断 防止整表更新。
modifying CTE 中的 DELETE / UPDATE 无 WHERE 阻断 支持识别 WITH … DELETE/UPDATE … 场景。
DELETE / UPDATE 影响行数超过阈值 可配置阻断 通过 pg_savior.max_rows_affected 开启。
CREATE INDEX 未指定 CONCURRENTLY 阻断 临时表除外。
大表 ALTER TABLE ADD COLUMN … DEFAULT 阻断 保守判断,避免潜在全表改写。
大表 ALTER TABLE ALTER COLUMN TYPE 阻断 避免全表改写。
大表 TRUNCATE 阻断 支持多表、分区/继承表和 CASCADE 场景。
大表 DROP TABLE 阻断 仅拦截表,DROP VIEW 等不受影响。
DROP DATABASE 阻断 DROP DATABASE IF EXISTS 在数据库存在时仍阻断,不存在时允许无操作通过。
临时绕过 支持 通过 pg_savior.bypass = on 临时放行。
全局开关 支持 通过 pg_savior.enabled = off 关闭当前会话保护。

六、加载共享库

检查 shared_preload_libraries 是否已包含 pg_savior:

SHOW shared_preload_libraries;

七、创建扩展

共享库加载完成后,在需要使用保护能力的数据库中创建扩展:

CREATE EXTENSION pg_savior;

检查扩展是否创建成功:

SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_savior';

示例输出:

extname | extversion———–+———— pg_savior | 0.1.0(1 row)

注意:说明:

注意:pg_savior–0.1.0.sql 中不创建额外 SQL 对象,保护逻辑全部由共享库 Hook 完成。

八、参数说明

pg_savior 提供如下配置参数:

参数 默认值 类型 生效范围 说明
pg_savior.enabled on boolean 当前会话 总开关。设置为 off 后,当前会话不执行任何 pg_savior 检查。
pg_savior.bypass off boolean 当前会话 临时绕过开关。设置为 on 后,当前会话所有检查均放行。
pg_savior.max_rows_affected 0 integer 当前会话 DML 行数估算阈值。为0时关闭行数阈值检查;大于0时,阻断优化器估算影响行数超过该阈值的 DELETE / UPDATE。
pg_savior.large_table_threshold_rows 1000000 integer 当前会话 大表阈值。表或其分区/继承子表的 reltuples 超过该值时,视为大表并启用高风险 DDL 保护。

查看当前参数:

SHOW pg_savior.enabled;SHOW pg_savior.bypass;SHOW pg_savior.max_rows_affected;SHOW pg_savior.large_table_threshold_rows;

修改参数示例:

SET pg_savior.max_rows_affected = 10000;SET pg_savior.large_table_threshold_rows = 500000;

在事务内临时设置:

BEGIN;SET LOCAL pg_savior.bypass = on;– 执行确认无误的高风险操作COMMIT;

十、阻断 DELETE 无 WHERE

pg_savior 会在 parse analyze 阶段检查 DELETE 语句是否包含 WHERE 条件。无 WHERE 条件的 DELETE 会被直接阻断,事务不会执行到执行器阶段。

示例:

CREATE TABLE emp (id int);INSERT INTO emp VALUES (1), (2), (3);DELETE FROM emp;

返回结果:

ERROR: pg_savior: DELETE without WHERE clause is blockedHINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.

带 WHERE 条件的 DELETE 可以正常执行:

DELETE FROM emp WHERE id = 1;

注意:说明:

注意:包含子查询的 WHERE 条件同样允许执行,例如:

注意:DELETE FROM emp WHERE id IN (SELECT id FROM dept);

十一、阻断 UPDATE 无 WHERE

pg_savior 会在 parse analyze 阶段检查 UPDATE 语句是否包含 WHERE 条件。无 WHERE 条件的 UPDATE 会被阻断。

示例:

CREATE TABLE emp (id int);INSERT INTO emp VALUES (1), (2), (3);UPDATE emp SET id = 99;

返回结果:

ERROR: pg_savior: UPDATE without WHERE clause is blockedHINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.

带 WHERE 条件的 UPDATE 可以正常执行:

UPDATE emp SET id = 99 WHERE id = 1;

十二、阻断 CTE 中的无条件 DELETE / UPDATE

pg_savior 支持检查 modifying CTE 中的 DELETE / UPDATE。如果 CTE 内部存在无 WHERE 的 DELETE 或 UPDATE,即使外层语句本身不是无条件写操作,也会被阻断。

十三、CTE 中 DELETE 无 WHERE

WITH d AS (DELETE FROM emp RETURNING id)SELECT count(*) FROM d;

返回结果:

ERROR: pg_savior: DELETE without WHERE clause is blockedHINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.

十四、CTE 中 UPDATE 无 WHERE

WITH u AS (UPDATE emp SET id = 99 RETURNING id)SELECT count(*) FROM u;

返回结果:

ERROR: pg_savior: UPDATE without WHERE clause is blockedHINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.

十五、CTE 中带 WHERE

WITH d AS (DELETE FROM emp WHERE id = 1 RETURNING id)SELECT count(*) FROM d;

该语句可以正常执行。

十六、配置 DELETE / UPDATE 影响行数阈值

默认情况下,pg_savior.max_rows_affected = 0,表示不检查 DELETE / UPDATE 的估算影响行数。

开启阈值后,pg_savior 会在 ExecutorStart 阶段读取执行计划中 ModifyTable 子计划的 plan_rows。当优化器估算影响行数超过阈值时,语句会被阻断。

十七、设置阈值

SET pg_savior.max_rows_affected = 10;

十八、DELETE 超过阈值

CREATE TABLE emp (id int);INSERT INTO emp SELECT generate_series(1, 1000);ANALYZE emp;DELETE FROM emp WHERE id > 0;

返回结果:

ERROR: pg_savior: DELETE estimated to affect 1000 rows, exceeds pg_savior.max_rows_affected (10)HINT: Refine the WHERE clause, raise pg_savior.max_rows_affected, or set pg_savior.bypass = on. Run ANALYZE if the estimate looks wrong.

十九、UPDATE 超过阈值

UPDATE emp SET id = id + 10000 WHERE id > 0;

返回结果:

ERROR: pg_savior: UPDATE estimated to affect 1000 rows, exceeds pg_savior.max_rows_affected (10)HINT: Refine the WHERE clause, raise pg_savior.max_rows_affected, or set pg_savior.bypass = on. Run ANALYZE if the estimate looks wrong.

注意:说明:

20、阻断非并发 CREATE INDEX

pg_savior 会阻断未指定 CONCURRENTLY 的 CREATE INDEX,避免在业务表上长时间持有阻塞写入的锁。

示例:

CREATE TABLE emp (id int);CREATE INDEX emp_id_idx ON emp (id);

返回结果:

ERROR: pg_savior: CREATE INDEX without CONCURRENTLY is blockedHINT: Use CREATE INDEX CONCURRENTLY (it cannot run in a transaction block), or set pg_savior.bypass = on for this session.

推荐写法:

CREATE INDEX CONCURRENTLY emp_id_idx ON emp (id);

注意:说明:

注意:临时表上的普通 CREATE INDEX 只影响当前会话,pg_savior 会放行临时表上的非并发建索引。

21、阻断大表 ADD COLUMN DEFAULT

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断:

ALTER TABLE … ADD COLUMN … DEFAULT …

示例:

SET pg_savior.large_table_threshold_rows = 10;CREATE TABLE big_emp (id int);INSERT INTO big_emp SELECT generate_series(1, 100);ANALYZE big_emp;ALTER TABLE big_emp ADD COLUMN status text DEFAULT 'active';

返回结果:

ERROR: pg_savior: ALTER TABLE ADD COLUMN with DEFAULT on a large table (100 rows) is blockedHINT: This operation may rewrite the whole table (it is blocked conservatively). Plan a batched migration; raise pg_savior.large_table_threshold_rows; or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.

推荐迁移方式:

ALTER TABLE big_emp ADD COLUMN status text;– 分批回填UPDATE big_emp SET status = 'active' WHERE id BETWEEN 1 AND 10000;UPDATE big_emp SET status = 'active' WHERE id BETWEEN 10001 AND 20000;– 如需默认值,回填完成后再设置ALTER TABLE big_emp ALTER COLUMN status SET DEFAULT 'active';

注意:说明:

注意:当前实现是保守策略:只要大表 ADD COLUMN 带 DEFAULT 就会阻断,不区分默认值是否会真实触发表重写。

22、阻断大表 ALTER COLUMN TYPE

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断:

ALTER TABLE … ALTER COLUMN … TYPE …

示例:

SET pg_savior.large_table_threshold_rows = 10;CREATE TABLE big_emp (id int, name text);INSERT INTO big_emp SELECT generate_series(1, 100), 'x';ANALYZE big_emp;ALTER TABLE big_emp ALTER COLUMN id TYPE bigint;

返回结果:

ERROR: pg_savior: ALTER TABLE ALTER COLUMN TYPE on a large table (100 rows) is blockedHINT: This operation may rewrite the whole table (it is blocked conservatively). Plan a batched migration; raise pg_savior.large_table_threshold_rows; or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.

注意:说明:

注意:同一个 ALTER TABLE 中如果包含多个子命令,只要其中任意子命令属于危险操作,就会触发拦截。例如:

注意:ALTER TABLE big_emp ADD COLUMN extra int, ALTER COLUMN name TYPE varchar(50);

23、阻断大表 TRUNCATE

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断 TRUNCATE。

示例:

SET pg_savior.large_table_threshold_rows = 10;CREATE TABLE big_emp (id int);INSERT INTO big_emp SELECT generate_series(1, 100);ANALYZE big_emp;TRUNCATE big_emp;

返回结果:

ERROR: pg_savior: TRUNCATE on a large table "big_emp" (100 rows) is blockedHINT: Verify the target, raise pg_savior.large_table_threshold_rows, or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.

注意:说明:

24、阻断大表 DROP TABLE

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断 DROP TABLE。

示例:

SET pg_savior.large_table_threshold_rows = 10;CREATE TABLE big_emp (id int);INSERT INTO big_emp SELECT generate_series(1, 100);ANALYZE big_emp;DROP TABLE big_emp;

返回结果:

ERROR: pg_savior: DROP TABLE on a large table "big_emp" (100 rows) is blockedHINT: Verify the target, raise pg_savior.large_table_threshold_rows, or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.

注意:说明:

25、阻断 DROP DATABASE

pg_savior 默认阻断 DROP DATABASE,防止误删数据库。

示例:

DROP DATABASE production_db;

返回结果:

ERROR: pg_savior: DROP DATABASE "production_db" is blockedHINT: Set pg_savior.bypass = on for this session if you really mean it.

注意:说明:

26、临时绕过保护

当确认需要执行高风险操作时,可以在当前会话开启绕过:

SET pg_savior.bypass = on;

执行完成后建议立即关闭:

RESET pg_savior.bypass;

推荐在事务中使用 SET LOCAL,避免绕过状态泄露到后续操作:

BEGIN;SET LOCAL pg_savior.bypass = on;TRUNCATE big_emp;COMMIT;

注意:说明:

注意:pg_savior.bypass 为 USERSET 参数,普通用户也可以设置。生产环境如需强管控,应配合权限治理、审计和变更流程使用。

27、临时关闭保护

如果需要在当前会话关闭所有 pg_savior 检查,可执行:

SET pg_savior.enabled = off;

恢复保护:

SET pg_savior.enabled = on;

注意:说明:

注意:pg_savior.enabled = off 会关闭当前会话中所有 DML 和 DDL 保护能力。生产环境不建议长期关闭。

29、开启实例级保护

1. 在业务数据库中创建扩展:

CREATE EXTENSION pg_savior;

2. 按业务规模设置阈值:

ALTER DATABASE postgres SET pg_savior.max_rows_affected = 100000;ALTER DATABASE postgres SET pg_savior.large_table_threshold_rows = 1000000;

3. 新建连接后确认参数:

SHOW pg_savior.enabled;SHOW pg_savior.max_rows_affected;SHOW pg_savior.large_table_threshold_rows;

30、执行一次确认的高风险变更

BEGIN;SET LOCAL pg_savior.bypass = on;ALTER TABLE big_emp ADD COLUMN status text DEFAULT 'active';COMMIT;

建议在执行前确认:

SELECT relname, reltuples FROM pg_class WHERE relname = 'big_emp';

如统计信息不准确,先执行:

ANALYZE big_emp;

31、实现原理

pg_savior 通过三个 PostgreSQL Hook 实现保护能力。

32、post_parse_analyze_hook

在 SQL parse analyze 后执行,用于检查:

·DELETE 是否缺少 WHERE。

·UPDATE 是否缺少 WHERE。

·modifying CTE 中的 DELETE / UPDATE 是否缺少 WHERE。

该阶段早于执行计划生成,能够在语句执行前快速失败。

33、ExecutorStart_hook

在执行器启动前执行,用于检查:

·DELETE / UPDATE 的优化器估算影响行数是否超过 pg_savior.max_rows_affected。

如果超过阈值,会在触碰任何数据前阻断。

34、ProcessUtility_hook

用于检查 DDL / Utility 语句,包括:

·CREATE INDEX 未指定 CONCURRENTLY。

·大表 ALTER TABLE ADD COLUMN … DEFAULT。

·大表 ALTER TABLE ALTER COLUMN TYPE。

·大表 TRUNCATE。

·大表 DROP TABLE。

·DROP DATABASE。

大表判断使用 pg_class.reltuples。对于分区表、继承表、级联截断等场景,会检查实际受影响的关系集合。

36、为什么执行 CREATE EXTENSION 后没有生效?

CREATE EXTENSION pg_savior 只注册扩展,保护逻辑由共享库 Hook 实现。

37、为什么带 WHERE 的 DELETE 也被阻断?

如果设置了 pg_savior.max_rows_affected 且优化器估算影响行数超过阈值,带 WHERE 的 DELETE / UPDATE 也会被阻断。

可检查当前阈值:

SHOW pg_savior.max_rows_affected;

38、为什么小表被识别为大表?

插件依赖 pg_class.reltuples,该值来自统计信息。请执行:

ANALYZE table_name;

然后重试。

39、如何确认表是否超过大表阈值?

SELECT relname, reltuplesFROM pg_classWHERE relname = 'table_name';

对于分区表或继承表,需要同时检查子表:

SELECT c.relname, c.reltuplesFROM pg_class cJOIN pg_inherits i ON i.inhrelid = c.oidWHERE i.inhparent = 'parent_table'::regclass;

40、CREATE INDEX 为什么被阻断?

pg_savior 默认要求持久表使用并发建索引:

CREATE INDEX CONCURRENTLY idx_name ON table_name (column_name);

临时表上的普通 CREATE INDEX 会被放行。

41、如何执行确认无误的 DROP DATABASE?

在当前会话开启绕过后执行:

SET pg_savior.bypass = on;DROP DATABASE target_db;RESET pg_savior.bypass;

建议仅在变更窗口内执行,并确保已完成备份和确认。

赞 (0)
上一篇 4天前
下一篇 4天前