首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >说我PG Freezing Boom 讲的一般的那个同学,专帖给你,看看这次可满意

说我PG Freezing Boom 讲的一般的那个同学,专帖给你,看看这次可满意

作者头像
AustinDatabases
发布2025-08-02 13:00:55
发布2025-08-02 13:00:55
15500
代码可运行
举报
文章被收录于专栏:AustinDatabasesAustinDatabases
运行总次数:0
代码可运行

最近有同学说,老师你课上讲的PostgreSQL Freezing Boom的部分可以讲的更清楚一点,咱们主打的就是一个听劝。

其实我也挺难的,上课的同学不少都没接触过PostgreSQL.别的数据库也没有“炸弹”这个词,再加上课时紧张。不过还是那句话,咱们主打一个听劝。今天就通过文章把这个部分优化一下。

Freezing Boom 到底是什么,中文冻结炸弹,怎么一个数据库就弄出了个炸弹,今天好好的详细的把来龙去脉给同学说清楚。

说这个问题,还是要从PostgreSQL设计说起,我个人看PG的设计者一直走的复杂简单化的路径,虽然PostgreSQL管理起来比MySQL复杂的多,但是PG的设计本身却是大繁化简的设计模式,中国话够用就好,从不进行太多余的担心和过分的设计。

曾经有不少人,包括我也认为把XID,也就是PG的事务ID设计成64为不好吗,这样PG的课不就不会讲 Freezing BOOM了。另外也有人会问,为什么当初设计的人没有设计64XID,非要用32XID。

那我也问你一句话,你觉得在你出生的80,90年代,为什么有万元户这个名称,家里有几万块在那时就非常了得了,现在你有几万块的存款那叫穷光蛋,这叫未来的不可预见性,以及快乐的人生法则,我把现在的事情处理好就OK ,我有必要想那么远,所以我理解了PG的设计大师们都是快乐主义和够用就好的理念的实践者。

那么现在数据库已经设计成32XID了,每个操作都有一个XID被分配的情况下,就会产生一个事情XID被分配光了的问题,终究一个上世纪设计的数据库,你让他服务今天的那堆复杂的业务需求,别太强人所难好吧!

这里我们把可能产生Freezing Boom的思维导图来梳理一下。

PostgreSQL Freezing Boom

1. 产生原因

1 冻结超时(XID 接近上限,触发强制冻结)

2 大表未及时 vacuum,历史版本行积压

3 autovacuum 被阻塞

1 大事务长时间未提交

2 表被 DML/DDL 操作锁定

4 autovacuum 配置不合理

4.1 参数如 freeze_min_age / freeze_max_age 设置不合适

4.2 autovacuum worker 数量太少或太慢

5 存在逻辑坏块或索引损坏

5.1 vacuum freeze 报错中断

6 IO 或 CPU 资源瓶颈

6.1 系统负载高,vacuum 执行缓慢

2. 诊断方法

7 查询数据库 XID 使用情况

7.1SELECT datname, age(datfrozenxid) FROM pg_database;

8 检查表的 relfrozenxid

8.1 pg_class 中 age(relfrozenxid)

9 查看 autovacuum 运行情况

9.1 pg_stat_activity 中是否存在 autovacuum 被阻塞

9.2 pg_locks 中是否存在阻塞链

10 检查 PostgreSQL 日志

10.1 是否存在 vacuum 错误、XID wraparound 警告

11 系统资源监控

11.1 使用 top / iostat / vmstat 或 Prometheus

3. 解决方案

12 手动执行冻结

12.1 VACUUM (FREEZE) 表名;

13 增加 autovacuum 并发能力

13.1 autovacuum_max_workers

13.2 autovacuum_cost_limit

14 优化 autovacuum 参数

14.1 freeze_min_age、freeze_table_age、freeze_max_age

15 终止长事务

15.1 pg_stat_activity + pg_terminate_backend()

16 修复逻辑坏块或索引损坏

16.1 REINDEX、pg_amcheck、pg_dump 恢复等

4. 预防机制

17 设置 XID 冻结预警

17.1 XID 使用率超 80% 告警

18 定期检查 relfrozenxid 最大值

18.1 筛查 age 大于阈值的表

19 控制事务生命周期

19.1 设置 idle_in_transaction_session_timeout

20 优化表结构

20.1 使用分区表降低单表冻结压力

21 自动化维护任务

21.1 定期执行 VACUUM (FREEZE) 或调度批处理任务

这里基于参数的调节等部分之前写过很多文章

PostgreSQL 玩PG我们是认真的,vacuum 稳定性平台我们有了

Postgresql autovacuum 6 为什么大表不进行autovacuum 的原因 (非事务,复制槽原因)

PostgreSQL autovacuum 5 怎么监控(autovacuum 扫描表工作的百分比)

postgresql autovacuum 4 怎么调整参数,让autovacuum 多干活,与成本的计算

Postgresql autovacuum 3 怎么调整参数,拯救你惨淡的性能

PostgreSQL autovacuum 优化与调试 (2 autovacuum 进行了什么样的工作)

PostgreSQL autovacuum 优化与调试 (1 触发 autovacuum 的条件)


我们手动画一个图,下面是一个解释的图,咱们主打一个认真,手工画,虽然比较难看。

其实冻结炸弹,非常形象,在分配XID,他们是分配一半,留了一半。这样就能保证事务的可见性,当重复利用XID进行二次分配的时候,可以知道这是第二次分配了,事务的可见性是根据XID的比大小,所以必须封存一部分XID保证程序判断的时候,一些已经回收的XID所标注的数据不能被显示出来,一句话避免原本属于已提交事务的 ID,可能变为未来事务或回滚事务的 ID,进而使可见行突然不可见,造成数据损坏。所以为了保证数据的正确性,如果你让系统无法回收XID,那么就给你数据库冻结上,冻上的数据库不在工作,只能单用户工作,且进去你只能进行XID的回收的工作,直到你让卡主XID被回收,数据库才能再次可以工作。你数据库不能工作的这段时间,就叫你触发了数据库的冻结炸弹。

解释怎么产生冻结炸弹
解释怎么产生冻结炸弹

解释怎么产生冻结炸弹

那么下面我们关注,怎么发现问题,解决问题即可。

问题 1: Autovacuum 对表不进行工作

Autovacuum对表不进行工作,分三种,一种是表的autovacuum参数设置问题,导致autovacuum无法被有效触发。第二种是表产生了问题,导致autovacuum触发了但无法再这个表上进行操作,这里我们称为autovacuum 被阻塞。

针对这个部分我们需要有相关的语句来进行查询(比如有些表根本就把autovacuum给关闭了,这个算第三种情况)

1 查询那个表被禁止autovacuum

代码语言:javascript
代码运行次数:0
运行
复制
SELECT
    relname AS table_name,
    reloptions AS table_options,
    pg_settings.setting AS autovacuum_global_setting,
    CASE
        WHEN reloptions::text LIKE '%autovacuum_enabled=off%' THEN 'Disabled by table option'
        WHEN pg_settings.setting = 'off' THEN 'Disabled globally'
        ELSE 'Enabled'
    END AS autovacuum_status
FROM
    pg_class
JOIN
    pg_namespace ON pg_namespace.oid = pg_class.relnamespace
LEFT JOIN
    pg_settings ON pg_settings.name = 'autovacuum'
WHERE
    relkind = 'r' -- Only tables
    AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema') -- Exclude system schemas
    AND (
        reloptions::text LIKE '%autovacuum_enabled=off%' OR pg_settings.setting = 'off'
    )
ORDER BY
    table_name;

2 我们是否可以写一个监控程序来去分析这个问题比如你发现有的表就是非预期的没有autovacuum的活动你可以检查 Autovacuum 进程的锁阻塞情况

代码语言:javascript
代码运行次数:0
运行
复制
SELECT
    blocked_activity.pid AS blocked_pid,
    blocked_activity.application_name AS blocked_application,
    blocked_activity.query AS blocked_query,
    blocking_activity.pid AS blocking_pid,
    blocking_activity.application_name AS blocking_application,
    blocking_activity.query AS blocking_query,
    blocking_activity.wait_event_type AS blocking_wait_event_type,
    blocking_activity.wait_event AS blocking_wait_event,
    pg_locks.mode AS blocked_lock_mode,
    pg_locks.relation::regclass AS blocked_table
FROM
    pg_catalog.pg_locks AS blocked_locks
JOIN
    pg_catalog.pg_stat_activity AS blocked_activity
    ON blocked_activity.pid = blocked_locks.pid
JOIN
    pg_catalog.pg_locks AS blocking_locks
    ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
    AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
    AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
    AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
    AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
    AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
    AND blocked_locks.pid != blocking_locks.pid
JOIN
    pg_catalog.pg_stat_activity AS blocking_activity
    ON blocking_activity.pid = blocking_locks.pid
WHERE
    NOT blocked_locks.granted
    AND (blocked_activity.backend_type = 'autovacuum worker' OR blocked_activity.application_name = 'autovacuum launcher')
ORDER BY
    blocked_table, blocked_pid;

在比如,有一些从ORACLE迁移过来的,把PG设置成不自动提交,然后就忘记这件事,以为PG是自动提交,然后就出事了。

代码语言:javascript
代码运行次数:0
运行
复制
SELECT
    pid,
    datname,
    usename,
    application_name,
    client_addr,
    backend_start,
    xact_start,
    now() - xact_start AS transaction_duration,
    state,
    state_change,
    wait_event_type,
    wait_event,
    query_start,
    query
FROM
    pg_stat_activity
WHERE
    state IN ('idle in transaction', 'idle in transaction (error)')
    AND (now() - xact_start) > interval '10 minutes' -- Adjust time as needed
ORDER BY
    transaction_duration DESC;

那么到底这里那个表是没有回收XID最老的那个表,也就是最可能阻碍你回收XID的表是那个,需要找出来。

代码语言:javascript
代码运行次数:0
运行
复制
SELECT
    n.nspname AS schema_name,
    c.relname AS table_name,
    age(c.relfrozenxid) AS xid_age_since_freeze,
    pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
    pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
    pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze
FROM
    pg_class c
JOIN
    pg_namespace n ON n.oid = c.relnamespace
WHERE
    c.relkind = 'r'
    AND n.nspname NOT IN ('pg_catalog', 'information_schema') -- 排除系统模式
ORDER BY
    xid_age_since_freeze DESC
LIMIT 1;

另外还有一个点,就是我们要分析我们的日志,对POSTGRESQL的日志,我们可以通过node.js的脚本来分析你的日志来获得一些关于vacuum的工作信息,你可以通过分析出来的信息来去对你的vacuum的工作进行一些调整,比如调整参数,或者对于一些影响autovacuum工作的情况有所了解。

下面是分析PG日志的 NODE.JS的脚本,后面加上日志的参数可以进行分析。

代码语言:javascript
代码运行次数:0
运行
复制
const fs = require('fs');
const readline = require('readline');
const path = require('path');

/**
 * PostgreSQL 日志分析器,专注于 VACUUM 信息。
 *
 * @param {string} logFilePath - PostgreSQL 日志文件的完整路径。
 * @returns {Promise<Array<Object>>} - 解析出的 VACUUM 事件数组。
 */
async function analyzePgVacuumLogs(logFilePath) {
    // 确保日志文件存在
    if (!fs.existsSync(logFilePath)) {
        console.error(`错误:日志文件不存在:${logFilePath}`);
        return [];
    }

    const vacuumEvents = [];
    let lineNumber = 0;

    // 创建 readline 接口,用于逐行读取文件
    const fileStream = fs.createReadStream(logFilePath, { encoding: 'utf8' });
    const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity // 处理 Windows 和 Unix 风格的换行符
    });

    console.log(`开始分析日志文件: ${logFilePath}`);

    // 正则表达式来匹配 PostgreSQL 的日志行和 VACUUM 相关信息
    // 这是一个简化版本,具体正则表达式可能需要根据你的 PostgreSQL 日志格式调整
    const logLineRegex = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [A-Z]{3}) \[(.+?)\] (.+?):(\s+autovacuum:)?\s*(VACUUM|ANALYZE)\s*(?:\((freeze)\))?\s*of table\s*"(.+?)"(?::\s*(.+?))?$/;
    const durationRegex = /duration:\s*(\d+\.\d+)\s*ms/; // 用于捕获 duration 信息

    for await (const line of rl) {
        lineNumber++;

        // 检查是否是 VACUUM 或 autovacuum 相关的日志行
        if (line.includes('VACUUM') || line.includes('autovacuum')) {
            const match = line.match(logLineRegex);
            const durationMatch = line.match(durationRegex);

            if (match) {
                const timestamp = match[1];
                const logType = match[3].trim(); // LOG, DETAIL, etc.
                const isAutovacuum = !!match[4]; // 是否是 autovacuum
                const operationType = match[5]; // VACUUM 或 ANALYZE
                const isFreeze = !!match[6]; // 是否包含 freeze
                const tableNameWithSchema = match[7]; // 表名,可能包含模式
                const details = match[8] ? match[8].trim() : ''; // 后续的详细信息

                let durationMs = null;
                if (durationMatch) {
                    durationMs = parseFloat(durationMatch[1]);
                }

                // 从 "schema.table" 中分离模式和表名
                const parts = tableNameWithSchema.split('.');
                const schemaName = parts.length > 1 ? parts[0] : 'public';
                const tableName = parts.length > 1 ? parts.slice(1).join('.') : parts[0];

                const event = {
                    lineNumber,
                    timestamp,
                    logType,
                    isAutovacuum,
                    operationType,
                    isFreeze,
                    schemaName: schemaName.replace(/"/g, ''), // 移除双引号
                    tableName: tableName.replace(/"/g, ''),
                    durationMs,
                    details: details,
                    rawLog: line
                };
                vacuumEvents.push(event);
            } else {
                // 如果没有匹配到预期的 VACUUM 格式,但包含关键字,仍然记录原始日志
                vacuumEvents.push({
                    lineNumber,
                    rawLog: line,
                    parsed: false
                });
            }
        }
    }

    console.log(`日志分析完成。发现 ${vacuumEvents.length} 条 VACUUM 相关事件。`);
    return vacuumEvents;
}

// --- 使用命令行参数 ---
(async () => {
    // 获取命令行参数: node pg-log-analyzer.js <日志路径> <日志文件名>
    const args = process.argv.slice(2); // 移除 'node' 和 'pg-log-analyzer.js'

    if (args.length < 2) {
        console.error('用法: node pg-log-analyzer.js <日志文件目录> <日志文件名>');
        console.error('例如: node pg-log-analyzer.js /var/lib/postgresql/data/pg_log postgresql-2025-07-25_000000.log');
        console.error('或者在 Windows 上: node pg-log-analyzer.js "C:\\Program Files\\PostgreSQL\\16\\data\\log" postgresql-2025-07-25_000000.log');
        process.exit(1); // 退出并返回错误码
    }

    const logDirectory = args[0];
    const logFileName = args[1];
    const fullLogPath = path.join(logDirectory, logFileName); // 拼接完整路径

    const events = await analyzePgVacuumLogs(fullLogPath);

    if (events.length > 0) {
        console.log('\n--- 识别到的 VACUUM 事件摘要 ---');
        // 打印前 10 条事件,或所有事件如果数量不多
        const eventsToDisplay = events.slice(0, Math.min(events.length, 10));

        eventsToDisplay.forEach((event, index) => {
            console.log(`\n事件 ${index + 1}:`);
            console.log(`  行号: ${event.lineNumber}`);
            console.log(`  时间: ${event.timestamp}`);
            console.log(`  操作: ${event.isAutovacuum ? '自动' : '手动'}${event.operationType}${event.isFreeze ? ' (冻结)' : ''}`);
            console.log(`  表名: ${event.schemaName}.${event.tableName}`);
            if (event.durationMs !== null) {
                console.log(`  耗时: ${event.durationMs.toFixed(3)} ms`);
            }
            if (event.details) {
                console.log(`  详情: ${event.details}`);
            }
            if (!event.parsed) {
                console.log(`  (未完全解析,原始日志): ${event.rawLog}`);
            }
        });

        // 你可以将所有结果保存到 JSON 文件
        const outputFileName = `vacuum_analysis_${path.basename(logFileName, path.extname(logFileName))}.json`;
        fs.writeFileSync(outputFileName, JSON.stringify(events, null, 2), 'utf8');
        console(`\n完整分析结果已保存到:${outputFileName}`);

        // 进一步分析:例如,找出最耗时的 VACUUM 操作
        const longestVacuum = events.filter(e => e.operationType === 'VACUUM' && e.durationMs !== null)
                                    .sort((a, b) => b.durationMs - a.durationMs)[0];
        if (longestVacuum) {
            console(`\n最耗时的 VACUUM 操作:`);
            console(`  表名: ${longestVacuum.schemaName}.${longestVacuum.tableName}`);
            console(`  耗时: ${longestVacuum.durationMs.toFixed(3)} ms`);
            console(`  时间: ${longestVacuum.timestamp}`);
            console(`  行号: ${longestVacuum.lineNumber}`);
        }

    } else {
        console('\n未找到 VACUUM 相关事件。请检查日志路径、文件名以及 `log_autovacuum_min_duration` 配置。');
    }
})();

比如你的这个脚本名字叫 pg-log-analyzer.js 那么可以通过这个脚本+你的日志来进行分析。

代码语言:javascript
代码运行次数:0
运行
复制
node pg-log-analyzer.js /var/lib/postgresql/data/pg_log postgresql-2025-07-25_000000.log

最后,如果发现问题可以通过手动的方式来对要进行XID回收的表进行相关的操作。VACUUM (FREEZE, VERBOSE) your_table_name,那么还有一些建议,相信爱学习的同学也发现了,比如为什么pgbench 在进行数据灌入后,会有选择默认直接对插入数据的表进行 vacuum的操作,举一反三在你初始化一个大表不断插入数据的时候,就建议你在操作完毕后,对表进行vacuum freeze的工作。

喜迎上合在天津召开,本地人宣传人人有责,感谢周欣的视频,如有侵权立即删除

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AustinDatabases 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PostgreSQL Freezing Boom
    • 1. 产生原因
    • 2. 诊断方法
    • 3. 解决方案
    • 4. 预防机制
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档