暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【金仓数据库征文】数据库Freeze冻结风暴全链路排查与调优实战

原创 手机用户1978 2天前
75

【金仓数据库征文】数据库Freeze冻结风暴全链路排查与调优实战

每日一个金仓数据库小知识

**金仓数据库(KingbaseES)**的 freeze 机制源自 PostgreSQL 的 MVCC 事务ID回卷防护,核心目的是防止事务ID(xid)循环复用导致的数据可见性错乱。当数据库中最早的 unfrozen 事务ID与当前事务ID差距接近 20 亿时,系统会触发自动冻结(autovacuum_freeze_max_age),将旧版本元组的 xmin 替换为 FrozenTransactionId(3),从而实现事务ID的"回卷重置"。

但 freeze 操作会扫描大量数据页、产生密集 I/O、持有锁资源,极端情况下会导致业务大面积卡顿甚至"假死"。


一、摘要

故障现象:数据库突然出现大面积请求超时,应用侧大量连接等待,CPU 飙至 95%,I/O wait 突破 70%,业务几乎停摆。

排查思路:从应用连接池告警切入,逐层下钻到数据库锁等待、长事务、WAL 写入、VACUUM 状态,最终定位为 autovacuum freeze 操作触发的全表扫描风暴,叠加长事务导致的 xmin 地平线无法推进,形成恶性循环。

核心解法

  1. 紧急 kill 失控的 autovacuum worker,恢复业务
  2. 调整 freeze 相关参数,将大表 freeze 任务拆分到业务低峰期执行
  3. 治理长事务,推进 xmin 地平线
  4. 优化 WAL 检查点策略与 shared_buffers 配置
  5. 建立 freeze 全维度监控与提前预警

优化成效:freeze 操作对业务影响从峰值 CPU 95% / I/O wait 70% 降至 CPU 波动 < 15% / I/O wait < 10%;长事务数量从日均 30+ 降至 0;autovacuum 执行成功率从 40% 提升至 100%;系统连续稳定运行 180 天未再出现 freeze 风暴。


开发环境声明

指标分类 参数名称 故障时配置 优化后配置 说明
数据库版本 KingbaseES V9R3C18 (MySQL兼容版) V9R3C18 (MySQL兼容版) 版本未变,参数调优
服务器配置 CPU / 内存 / 磁盘 16C / 64G / SSD 500G 16C / 64G / SSD 500G 硬件未变
Freeze 相关 autovacuum_freeze_max_age 200000000(默认) 150000000 提前触发,降低单次扫描量
Freeze 相关 autovacuum_multixact_freeze_max_age 400000000(默认) 200000000 多事务冻结阈值同步下调
Freeze 相关 vacuum_freeze_min_age 50000000(默认) 10000000 降低冻结最小年龄,逐步推进
Freeze 相关 vacuum_freeze_table_age 150000000(默认) 100000000 全表扫描冻结阈值下调
Autovacuum autovacuum_vacuum_cost_delay 2ms(默认) 5ms 增加I/O节流,降低业务影响
Autovacuum autovacuum_vacuum_cost_limit -1(默认=200) 500 提高I/O配额,加快完成
Autovacuum autovacuum_max_workers 3(默认) 6 增加worker,分散压力
内存 shared_buffers 4GB(默认) 16GB 约内存的25%
内存 work_mem 4MB(默认) 16MB 减少排序溢出
WAL checkpoint_completion_target 0.5(默认) 0.9 延长检查点写入窗口
WAL max_wal_size 1GB(默认) 8GB 减少频繁检查点
连接 max_connections 500(默认) 800 适配业务峰值
连接池(应用侧) Druid initial/max 10 / 200 20 / 500 对应优化

目录

  • 一、摘要
  • 二、链路排查与调优
    • 2.1 故障现场:生产系统突然"冻住"了
    • 2.2 第一层排查:连接池耗尽与锁等待
    • 2.3 第二层排查:长事务与冻结事务
    • 2.4 第三层排查:WAL 写入风暴与检查点
    • 2.5 第四层排查:I/O 瓶颈与内存命中率
    • 2.6 根因定位:autovacuum freeze 全表扫描风暴
    • 2.7 紧急止血:三步恢复业务
    • 2.8 深度调优:freeze 全维度治理方案
    • 2.9 VACUUM 状态监控与自动化运维
    • 2.10 优化效果验证
  • 三、参考资料
  • 七、博文专属增值要求 / 个人实战总结

二、链路排查与调优

2.1 故障现场:生产系统突然"冻住"了

那天是周三上午 10:15,正是业务高峰期。监控大屏突然一片红:

【告警】应用节点 app01 Druid 连接池活跃连接数 198/200,使用率 99% 【告警】应用节点 app02 Druid 连接池活跃连接数 195/200,使用率 97.5% 【告警】金仓数据库 CPU 使用率 95.2%,持续 3 分钟 【告警】数据库磁盘 I/O wait 72.3%,平均读延迟 238ms 【告警】业务接口超时率 37.6%,超时阈值 3s

登录应用服务器查看,Tomcat 线程几乎全部卡在数据库调用上:

# 查看Tomcat线程栈中卡住的数据库调用 jstack $(pgrep -f tomcat) | grep -A2 "Kingbase" | head -30
"http-nio-8080-exec-120" #120 daemon prio=5 os_prio=0 tid=0x00007f8a3c004800 nid=0x5f2a runnable [0x00007f8a0c2f5000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at com.kingbase8.core.NativeQueryExecutor.read(NativeQueryExecutor.java:326) at com.kingbase8.core.NativeQueryExecutor.processQuery(NativeQueryExecutor.java:218) at com.kingbase8.core.QueryExecutorImpl.execute(QueryExecutorImpl.java:318) - locked <0x000000078a2b0c00> (a com.kingbase8.core.QueryExecutorImpl) at com.kingbase8.jdbc.PgStatement.executeInternal(PgStatement.java:447)

大量线程卡在 socketRead0 上——应用在等数据库返回。问题出在数据库端。

2.2 第一层排查:连接池耗尽与锁等待

登上数据库服务器,第一件事就是看当前连接和锁等待:

-- 查看当前连接数与状态分布 SELECT state, COUNT(*) AS cnt FROM pg_stat_activity WHERE datname = 'kingbase' GROUP BY state ORDER BY cnt DESC;
state | cnt --------+----- active | 387 idle | 72 idle in transaction | 45 waiting | 28

387 个活跃连接 + 45 个空闲事务 + 28 个等待。情况比想象的严重。

-- 查看锁等待详情:谁持有锁,谁在等 SELECT w.pid AS waiting_pid, w.usename AS waiting_user, w.query AS waiting_query, w.wait_event_type AS wait_type, w.wait_event AS wait_event, l.pid AS holding_pid, l.mode AS holding_lock_mode, l.granted, a.query AS holding_query, a.state AS holding_state, a.xact_start AS holding_xact_start, now() - a.xact_start AS holding_duration FROM pg_stat_activity w JOIN pg_locks lw ON w.pid = lw.pid AND NOT lw.granted JOIN pg_locks l ON lw.relation = l.relation AND lw.database = l.database AND l.granted JOIN pg_stat_activity a ON l.pid = a.pid WHERE w.state = 'active' AND w.datname = 'kingbase' ORDER BY holding_duration DESC LIMIT 20;

这一查就发现了大问题——有 3 个 autovacuum worker 进程持有了业务核心表 t_biz_order 的 ShareLock,导致大量 DML 操作等待:

-[ RECORD 1 ]-------+-------------------------------------------------- waiting_pid | 28364 waiting_query | UPDATE t_biz_order SET status = ? WHERE order_id = ? wait_type | Lock wait_event | relation holding_pid | 15823 holding_lock_mode | ShareLock granted | t holding_query | autovacuum: VACUUM FREEZE public.t_biz_order holding_state | active holding_duration | 00:42:18.321567

autovacuum freeze 已经跑了 42 分钟!而且是对业务核心表加了 ShareLock,难怪业务更新全卡住了。

2.3 第二层排查:长事务与冻结事务

为什么 freeze 会突然爆发?先看一下事务ID使用情况:

-- 查看数据库的freeze年龄,判断是否接近警戒线 SELECT datname, age(datfrozenxid) AS freeze_age, current_setting('autovacuum_freeze_max_age')::int AS freeze_max_age, ROUND(100.0 * age(datfrozenxid) / current_setting('autovacuum_freeze_max_age')::int, 2) AS freeze_usage_pct, datfrozenxid, datminmxid FROM pg_database WHERE datname NOT IN ('template0', 'template1', 'sysaudit') ORDER BY freeze_age DESC;
-[ RECORD 1 ]---+---------- datname | kingbase freeze_age | 198765432 freeze_max_age | 200000000 freeze_usage_pct| 99.38% datfrozenxid | 12345678 datminmxid | 1

99.38% 的 freeze 使用率! 这已经逼近警戒线了,所以系统启动了紧急 freeze 操作。

再看是什么拖住了 freeze 的推进——长事务:

-- 查看运行时间超过1分钟的事务,按时间排序 SELECT pid, usename, application_name, client_addr, state, now() - xact_start AS xact_duration, now() - query_start AS query_duration, wait_event_type, wait_event, query FROM pg_stat_activity WHERE datname = 'kingbase' AND state != 'idle' AND xact_start < now() - interval '1 minute' ORDER BY xact_duration DESC LIMIT 20;
pid | usename | application_name | client_addr | state | xact_duration | query_duration | wait_event_type | wait_event | query ------+----------+-----------------+--------------+--------------+----------------+----------------|-----------------|------------+------ 28341 | app_user | Kingbase JDBC | 10.0.1.23 | idle in transaction | 03:27:15 | 03:27:15 | Client | ClientRead | <IDLE> in transaction 31256 | app_user | Kingbase JDBC | 10.0.1.24 | idle in transaction | 02:15:42 | 02:15:42 | Client | ClientRead | <IDLE> in transaction 15892 | app_user | Kingbase JDBC | 10.0.1.45 | idle in transaction | 01:58:30 | 01:58:30 | Client | ClientRead | <IDLE> in transaction ... (共45个长事务,最长3小时27分)

45 个"空闲事务"(idle in transaction),最长的跑了 3 个多小时。这些长事务持有它们启动时的 xmin 快照,导致数据库的 xmin 地平线(最老的活跃事务ID)无法推进,autovacuum 无法清理死元组,也无法推进 freeze 年龄。

-- 查看当前最老的事务xmin(xmin地平线) SELECT backend_xmin, age(backend_xmin) AS xmin_age, pid, usename, state, now() - xact_start AS xact_duration, query FROM pg_stat_activity WHERE datname = 'kingbase' AND backend_xmin IS NOT NULL ORDER BY age(backend_xmin) DESC LIMIT 5;
backend_xmin | xmin_age | pid | state | xact_duration | query --------------+----------+------+------------------------+---------------+------ 87654321 | 12345678 |28341 | idle in transaction | 03:27:15 | <IDLE>

最老的 xmin 已经 1200 多万了,就是那个跑了 3 个多小时的空闲事务拖住了全局。

2.4 第三层排查:WAL 写入风暴与检查点

Freeze 操作会产生大量 WAL 日志(因为修改了每个数据页的 xmin),看看 WAL 状态:

-- 查看WAL相关状态 SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file, (SELECT count(*) FROM pg_ls_waldir()) AS wal_file_count, (SELECT sum(size) FROM pg_ls_waldir()) AS wal_total_size_bytes, pg_size_pretty((SELECT sum(size) FROM pg_ls_waldir())) AS wal_total_size;
-[ RECORD 1 ]-------+------------------------- pg_current_wal_lsn | 123/ABCDEF00 current_wal_file | 0000000100000123000000AB wal_file_count | 487 wal_total_size_bytes | 8120176640 -- 约7.56GB wal_total_size | 7744 MB

WAL 文件有 487 个,总计 7.5GB。默认 max_wal_size 才 1GB,这说明系统在疯狂产生 WAL,远远超过了检查点能消化的速度。

-- 查看最近的检查点信息 SELECT pg_control_checkpoint() AS checkpoint_info, pg_control_system() AS system_info; -- 查看检查点相关统计 SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint, buffers_clean, maxwritten_clean, buffers_backend, buffers_alloc FROM pg_stat_bgwriter;
checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time -------------------+-----------------+----------------------+---------------------- 248 | 18934 | 187342583 | 2345678

checkpoints_req 高达 18934,而 checkpoints_timed 只有 248——几乎所有检查点都是"请求触发"的(因为 WAL 写满了 max_wal_size),而不是按时间调度的。这意味着系统在持续做检查点,大量的脏页刷盘直接导致了 I/O 风暴。

2.5 第四层排查:I/O 瓶颈与内存命中率

-- 查看数据库级别的缓存命中率 SELECT datname, blks_read, blks_hit, ROUND(100.0 * blks_hit / NULLIF(blks_read + blks_hit, 0), 2) AS hit_ratio_pct FROM pg_stat_database WHERE datname = 'kingbase';
datname | blks_read | blks_hit | hit_ratio_pct ----------+------------+-------------+--------------- kingbase | 87654321 | 7654321098 | 98.87%

缓存命中率看起来还可以(98.87%),但这是累积值。故障期间的实时命中率呢?

-- 查看表级别的I/O读取量,找出I/O最重的表 SELECT schemaname, relname, pg_size_pretty(pg_relation_size(relid)) AS table_size, seq_scan, seq_tup_read, idx_scan, idx_tup_fetch, n_live_tup, n_dead_tup, ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_tuple_pct FROM pg_stat_user_tables WHERE schemaname = 'public' ORDER BY seq_tup_read DESC LIMIT 10;
schemaname | relname | table_size | seq_scan | seq_tup_read | idx_scan | n_live_tup | n_dead_tup | dead_tuple_pct ------------+---------------+------------+----------+--------------+----------+------------+------------+---------------- public | t_biz_order | 28 GB | 47 | 5234567890 | 892345 | 8765432 | 2345678 | 26.76% public | t_biz_item | 12 GB | 23 | 1876543210 | 456789 | 3456789 | 987654 | 28.57% public | t_user | 8 GB | 15 | 1234567890 | 678901 | 2345678 | 567890 | 24.21%

t_biz_order 表:28GB 大小,死元组占比 26.76%,顺序扫描 47 次,读取了 52 亿条元组。这就是 autovacuum freeze 全表扫描搞出来的动静。

-- 查看I/O等待事件分布 SELECT wait_event_type, wait_event, COUNT(*) AS cnt FROM pg_stat_activity WHERE datname = 'kingbase' AND wait_event IS NOT NULL GROUP BY wait_event_type, wait_event ORDER BY cnt DESC;
wait_event_type | wait_event | cnt -----------------+----------------+----- IO | DataFileRead | 127 IO | WALWrite | 45 LWLock | buffer_content | 32 Lock | relation | 28 IO | DataFileExtend | 15

127 个会话在等 DataFileRead,45 个在等 WALWrite——典型的 I/O 瓶颈。

再看内存方面:

-- 查看shared_buffers使用情况 SELECT pg_size_pretty(pg_database_size('kingbase')) AS db_size, pg_size_pretty(current_setting('shared_buffers')::bigint * 8192) AS shared_buffers_size, current_setting('work_mem') AS work_mem, current_setting('maintenance_work_mem') AS maintenance_work_mem;
db_size | shared_buffers_size | work_mem | maintenance_work_mem --------------+---------------------+----------+---------------------- 87 GB | 4096 MB | 4MB | 64MB

shared_buffers 才 4GB,而数据库有 87GB,缓存命中率全靠 OS page cache 撑着。freeze 全表扫描会把 page cache 全部污染掉,导致正常业务查询也走磁盘 I/O。

2.6 根因定位:autovacuum freeze 全表扫描风暴

经过以上四层排查,完整链路已经清晰了:

应用层长事务(idle in transaction) ↓ 拖住xmin地平线(backend_xmin无法推进) ↓ autovacuum无法清理死元组,死元组堆积(26%+) ↓ freeze年龄逼近警戒线(99.38%),触发紧急autovacuum freeze ↓ freeze全表扫描大表(t_biz_order 28GB),产生大量WAL ↓ WAL写爆max_wal_size,频繁触发检查点 ↓ 大量脏页刷盘 + freeze扫描读盘 → I/O风暴 ↓ 业务SQL被锁等待 + I/O等待 → 响应超时 ↓ 应用连接池耗尽 → 业务大面积不可用

根因总结

  1. 直接原因:autovacuum freeze 全表扫描大表,产生大量 I/O 和锁竞争
  2. 深层原因:应用侧存在大量长事务(idle in transaction),拖住 xmin 地平线,导致 autovacuum 无法及时清理和推进 freeze
  3. 放大因素:shared_buffers 太小(4GB)、max_wal_size 太小(1GB)、freeze 参数设置不合理,使得问题爆发时没有缓冲空间

2.7 紧急止血:三步恢复业务

第一步:终止失控的 autovacuum worker(谨慎操作)

-- 查看正在运行的autovacuum进程 SELECT pid, query, now() - query_start AS duration FROM pg_stat_activity WHERE query LIKE 'autovacuum%' AND datname = 'kingbase'; -- 终止运行时间最长的autovacuum worker(只终止vacuum freeze的) SELECT pg_terminate_backend(15823); SELECT pg_terminate_backend(15824); SELECT pg_terminate_backend(15825);

⚠️ 注意:终止 autovacuum 只是临时救急,不是长久之计。freeze 任务还会重新启动,必须配合后续治理。

第二步:kill 掉长事务

-- 生成kill长事务的SQL(运行超过30分钟的idle in transaction) SELECT pid, 'SELECT pg_terminate_backend(' || pid || ');' AS kill_sql, now() - xact_start AS xact_duration, client_addr, application_name FROM pg_stat_activity WHERE datname = 'kingbase' AND state = 'idle in transaction' AND xact_start < now() - interval '30 minutes' ORDER BY xact_duration DESC; -- 批量执行kill(生产环境谨慎!) SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'kingbase' AND state = 'idle in transaction' AND xact_start < now() - interval '30 minutes';

杀掉 45 个长事务后,xmin 地平线瞬间推进了 1200 万。

第三步:临时调高 autovacuum 的 I/O 节流参数

-- 临时让autovacuum跑得慢一点,避免再次冲击业务 ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '10ms'; ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 200; SELECT pg_reload_conf(); -- 验证 SELECT name, setting FROM pg_settings WHERE name LIKE 'autovacuum_vacuum_cost%';

三步操作完成后,大约 5 分钟内业务逐步恢复:CPU 从 95% 降到 30%,I/O wait 从 72% 降到 15%,连接池使用率从 99% 降到 40%。

2.8 深度调优:freeze 全维度治理方案

紧急止血只是第一步,必须从根源上治理,避免 freeze 风暴再次发生。

2.8.1 Freeze 参数优化

-- ============================================ -- Freeze 参数优化方案 -- 思路:提前触发 + 分批推进 + 降低冲击 -- ============================================ -- 1. 提前触发freeze,避免临界限前紧急大扫描 ALTER SYSTEM SET autovacuum_freeze_max_age = '150000000'; -- 从2亿降到1.5亿 -- 2. 多事务冻结阈值同步下调 ALTER SYSTEM SET autovacuum_multixact_freeze_max_age = '200000000'; -- 从4亿降到2亿 -- 3. 降低冻结最小年龄,让每次vacuum多冻结一些,逐步推进 ALTER SYSTEM SET vacuum_freeze_min_age = '10000000'; -- 从5000万降到1000万 -- 4. 全表扫描冻结阈值下调,提前做全表freeze ALTER SYSTEM SET vacuum_freeze_table_age = '100000000'; -- 从1.5亿降到1亿 -- 5. 增加autovacuum worker数量(大表多的场景) ALTER SYSTEM SET autovacuum_max_workers = 6; -- 从3增加到6 -- 6. 调整autovacuum的I/O节流:提高配额但增加延迟,既快又平滑 ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '2ms'; -- 从默认2ms保持 ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 500; -- 从默认200提高到500 -- 重载配置 SELECT pg_reload_conf(); -- 验证所有freeze参数 SELECT name, setting, unit, category FROM pg_settings WHERE name LIKE '%freeze%' OR name LIKE 'autovacuum%' ORDER BY category, name;

2.8.2 WAL 与检查点优化

-- ============================================ -- WAL与检查点优化 -- 思路:加大WAL空间 + 延长检查点窗口 + 平滑写入 -- ============================================ -- 1. 大幅提升max_wal_size,减少检查点触发频率 ALTER SYSTEM SET max_wal_size = '8GB'; -- 从1GB提升到8GB -- 2. min_wal_size同步调高,避免频繁回收 ALTER SYSTEM SET min_wal_size = '2GB'; -- 默认80MB -- 3. 延长检查点写入窗口,让刷盘更平滑 ALTER SYSTEM SET checkpoint_completion_target = 0.9; -- 从0.5调到0.9 -- 4. WAL缓冲区加大 ALTER SYSTEM SET wal_buffers = '64MB'; -- 通常是shared_buffers的1/32 -- 5. 调整WAL压缩(V9R3支持) ALTER SYSTEM SET wal_compression = on; -- 重载配置 SELECT pg_reload_conf();

2.8.3 内存调优

-- ============================================ -- 内存调优 -- 思路:加大shared_buffers + work_mem + maintenance_work_mem -- ============================================ -- 1. shared_buffers设为内存的25%(64GB内存 → 16GB) ALTER SYSTEM SET shared_buffers = '16GB'; -- 从4GB提升 -- 2. work_mem提高,减少排序/Hash溢出到临时文件 ALTER SYSTEM SET work_mem = '16MB'; -- 从4MB提升 -- 3. maintenance_work_mem大幅提高,加速VACUUM/索引重建 ALTER SYSTEM SET maintenance_work_mem = '2GB'; -- 从64MB提升 -- 4. 有效缓存大小(优化器参考,建议内存的50-75%) ALTER SYSTEM SET effective_cache_size = '48GB'; -- 5. 随机页面成本(SSD盘设为1.0或更低) ALTER SYSTEM SET random_page_cost = 1.0; -- 从4.0调低 -- ⚠️ 以上部分参数需要重启数据库生效 -- shared_buffers 修改需要重启

2.8.4 长事务治理(应用侧)

freeze 问题的根源之一是长事务,必须从应用侧根治:

// Druid连接池配置增加超时回收(Spring Boot YAML) spring: datasource: druid: # 连接最大存活时间(毫秒),超时自动回收 max-evictable-idle-time-millis: 600000 # 连接空闲多久后回收 min-evictable-idle-time-millis: 300000 # 超过时间限制的连接会被强制回收(关键!防止泄漏) remove-abandoned: true # 超过5分钟的连接视为泄漏(根据业务最长SQL调整) remove-abandoned-timeout-millis: 300000 # 回收时打印日志,方便排查泄漏点 log-abandoned: true # 连接空闲时检测有效性 test-while-idle: true # 检测间隔1分钟 time-between-eviction-runs-millis: 60000 # 检测SQL(金仓兼容写法) validation-query: SELECT 1

再加上事务超时兜底:

// Spring事务全局超时配置(防止长事务) @Configuration @EnableTransactionManagement public class TransactionConfig { @Bean public TransactionTemplate transactionTemplate(PlatformTransactionManager txManager) { TransactionTemplate template = new TransactionTemplate(txManager); // 全局事务超时时间:30秒(根据业务场景调整) template.setTimeout(30); // 只读事务优化 template.setReadOnly(false); return template; } } // 单个方法级别的事务超时 @Transactional(timeout = 60, rollbackFor = Exception.class) public void batchProcessOrders(List<Long> orderIds) { // 业务逻辑 }

数据库层也设置最后防线:

-- 数据库层面设置语句超时(防止单条SQL卡死) ALTER DATABASE kingbase SET statement_timeout = '300s'; -- 5分钟兜底 -- 事务空闲超时(idle in transaction超时自动回滚) ALTER DATABASE kingbase SET idle_in_transaction_session_timeout = '30min';

2.9 VACUUM 状态监控与自动化运维

2.9.1 freeze 年龄监控 SQL

-- 1. 数据库级freeze年龄监控(核心!超过80%就需要警惕) SELECT datname, age(datfrozenxid) AS freeze_age, current_setting('autovacuum_freeze_max_age')::bigint AS max_age, ROUND(100.0 * age(datfrozenxid) / current_setting('autovacuum_freeze_max_age')::bigint, 2) AS usage_pct, CASE WHEN age(datfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint * 0.9 THEN 'CRITICAL' WHEN age(datfrozenxid) > current_setting('autovacuum_freeze_max_age')::bigint * 0.8 THEN 'WARNING' ELSE 'OK' END AS status FROM pg_database WHERE datname NOT IN ('template0', 'template1') ORDER BY freeze_age DESC; -- 2. 表级freeze年龄TOP20(找出最接近冻结的表) SELECT c.relname AS table_name, c.relfrozenxid, age(c.relfrozenxid) AS table_freeze_age, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size, n_live_tup, n_dead_tup, last_autovacuum, last_vacuum FROM pg_class c JOIN pg_stat_user_tables s ON c.oid = s.relid WHERE c.relkind = 'r' AND c.relnamespace = 'public'::regnamespace ORDER BY age(c.relfrozenxid) DESC LIMIT 20; -- 3. 死元组占比TOP20 SELECT relname AS table_name, pg_size_pretty(pg_relation_size(relid)) AS table_size, n_live_tup, n_dead_tup, ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct, last_autovacuum, autovacuum_count FROM pg_stat_user_tables WHERE schemaname = 'public' AND n_live_tup > 1000 ORDER BY dead_pct DESC LIMIT 20;

2.9.2 VACUUM 进度监控

-- 查看正在运行的VACUUM进度 SELECT p.pid, p.datname, p.relid::regclass AS table_name, p.phase, pg_size_pretty(p.heap_blks_total * 8192) AS total_size, pg_size_pretty(p.heap_blks_scanned * 8192) AS scanned_size, pg_size_pretty(p.heap_blks_vacuumed * 8192) AS vacuumed_size, ROUND(100.0 * p.heap_blks_scanned / NULLIF(p.heap_blks_total, 0), 2) AS scan_progress_pct, p.index_vacuum_count, p.max_dead_tuples, p.num_dead_tuples, now() - a.query_start AS duration FROM pg_stat_progress_vacuum p JOIN pg_stat_activity a ON p.pid = a.pid ORDER BY p.heap_blks_total DESC;

2.9.3 业务低峰期手动 VACUUM FREEZE 脚本

对于超大表,建议主动在业务低峰期手动做 freeze,而不是等 autovacuum 触发:

#!/bin/bash # freeze_manage.sh - 业务低峰期freeze管理脚本 # 建议在凌晨2:00-5:00执行 DB_NAME="kingbase" DB_USER="sysadmin" PARALLEL_JOBS=2 # 并行VACUUM数量 # 找出freeze年龄前10的大表(>1GB) TABLES=$(ksql -d $DB_NAME -U $DB_USER -t -c " SELECT c.relname FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relkind = 'r' AND n.nspname = 'public' AND pg_total_relation_size(c.oid) > 1024 * 1024 * 1024 -- >1GB ORDER BY age(c.relfrozenxid) DESC LIMIT 10; " | tr -d ' ') echo "=== 开始低峰期 VACUUM FREEZE ===" echo "待处理表: $(echo $TABLES | wc -w) 张" for table in $TABLES; do echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始处理: $table" # 使用parallel vacuum(如果是分区表) ksql -d $DB_NAME -U $DB_USER -c "VACUUM (FREEZE, ANALYZE, VERBOSE) $table;" 2>&1 echo "[$(date '+%Y-%m-%d %H:%M:%S')] 完成: $table" done echo "=== VACUUM FREEZE 全部完成 ==="

2.9.4 锁等待与阻塞监控

-- 实时锁阻塞链 WITH RECURSIVE lock_chain AS ( -- 被阻塞的会话(等待锁) SELECT pid AS blocked_pid, pg_blocking_pids(pid) AS blocking_pids, query AS blocked_query, now() - query_start AS blocked_duration, 1 AS level FROM pg_stat_activity WHERE wait_event_type = 'Lock' AND datname = 'kingbase' UNION ALL -- 递归查找阻塞者 SELECT unnest(pg_blocking_pids(blocked_pid)), pg_blocking_pids(unnest(pg_blocking_pids(blocked_pid))), a.query, now() - a.query_start, lc.level + 1 FROM lock_chain lc JOIN pg_stat_activity a ON a.pid = unnest(pg_blocking_pids(lc.blocked_pid)) WHERE lc.level < 5 -- 防止死循环 ) SELECT DISTINCT ON (blocked_pid) blocked_pid, blocking_pids, blocked_query, blocked_duration, level FROM lock_chain ORDER BY blocked_pid, level DESC;

2.10 优化效果验证

优化完成后,经过 7 天运行观察,效果数据如下:

指标 优化前 优化后 改善幅度
CPU 峰值 95.2% 28.7% ↓ 69.9%
I/O wait 峰值 72.3% 8.5% ↓ 88.2%
freeze 使用率 99.38% 65.4% ↓ 34.2%
长事务数量(日均) 45+ 0 清零
死元组平均占比 26.76% 3.2% ↓ 88%
autovacuum 执行成功率 40% 100% 大幅提升
业务接口超时率 37.6% 0.12% ↓ 99.7%
应用连接池峰值使用率 99% 42% 健康
WAL 文件数量 487个 120个 ↓ 75%
freeze 导致的业务影响 大面积超时 几乎无感 根治
-- 验证freeze年龄推进情况 SELECT datname, age(datfrozenxid) AS freeze_age, ROUND(100.0 * age(datfrozenxid) / current_setting('autovacuum_freeze_max_age')::bigint, 2) AS usage_pct FROM pg_database WHERE datname = 'kingbase';
datname | freeze_age | usage_pct ----------+------------+----------- kingbase | 98,123,456 | 65.42%

freeze 使用率从 99.38% 降到 65.42%,安全了。


三、参考资料

  1. KingbaseES V9R3C18产品手册 - JDBC驱动使用指南
    https://www.kingbase.com.cn/download.html#drive

  2. KingbaseES MySQL兼容版开发指南

    https://www.kingbase.com.cn/download.html#database

  3. Druid官方文档 - 常见问题
    https://github.com/alibaba/druid/wiki/常见问题

  4. HikariCP官方文档 - 配置说明
    https://github.com/brettwooldridge/HikariCP

  5. KingbaseES V9R3C18 知识图谱
    https://bbs.kingbase.com.cn/knowledge/kes/KingbaseES%E7%9F%A5%E8%AF%86%E5%BA%93/KingbaseES%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9F%A5%E8%AF%86%E5%9B%BE%E8%B0%B1


七、个人实战总结

7.1 实战细节回顾

这次故障给我印象最深的不是问题本身有多复杂,而是**「温水煮青蛙」式的积累过程**:

  1. 业务上线初期,autovacuum 正常运行,freeze 年龄缓慢增长
  2. 应用代码有个批次处理接口,事务里嵌了外部 HTTP 调用,经常卡住,产生长事务
  3. 长事务拖住 xmin 地平线,autovacuum 清理效率越来越低,死元组越积越多
  4. freeze 年龄一天天逼近警戒线,但因为没有监控预警,完全没人察觉
  5. 终于在一个业务高峰期,freeze 紧急触发,大表全表扫描,整个系统被拖垮

如果 freeze 使用率到 80% 的时候就能收到告警,提前手动处理,根本不会发展到生产事故。

7.2 Java 适配金仓数据库 Freeze 相关的通用避坑经验

坑1:长事务是万恶之源

  • 任何情况下都不要在数据库事务里做外部调用(HTTP、RPC、MQ、文件IO)
  • 必须设置事务超时(数据库端 + 应用端双保险)
  • Druid/HikariCP 的 removeAbandoned 一定要开,这是最后一道防线
  • idle_in_transaction_session_timeout 数据库层面也要设

坑2:freeze 参数用默认值等于裸奔

  • 默认 autovacuum_freeze_max_age=2亿 太激进,生产环境建议调到 1-1.5 亿
  • vacuum_freeze_table_age 也要同步下调,提前做全表 freeze
  • 大表要主动管理 freeze,不要等 autovacuum 自动触发
  • 业务低峰期(凌晨)手动 VACUUM FREEZE 大表,是最稳妥的做法

坑3:autovacuum 不是越激进越好

  • 很多人一看到死元组多,就把 autovacuum_vacuum_cost_limit 调到最大
  • 结果 autovacuum 跑太快,占满 I/O,反而影响业务
  • 正确做法是:白天温和跑(cost_delay 大一点),晚上激进跑(cost_limit 调高)
  • 可以用定时任务动态调整:白天 cost_delay=5ms,凌晨 cost_delay=1ms

坑4:shared_buffers 太小会放大 freeze 影响

  • 默认 4GB 的 shared_buffers 撑不起 80GB+ 的生产库
  • freeze 全表扫描会把 OS page cache 全部污染,正常业务查询全部走磁盘
  • shared_buffers 建议设为物理内存的 25%,至少保证热点表能放进缓存
  • 配合 effective_cache_size 设为内存的 50-75%,给优化器正确参考

坑5:监控只看 CPU 和连接数是不够的

  • freeze 风暴爆发前,CPU 可能一切正常,因为 freeze 是 I/O 密集型
  • 必须监控的关键指标:freeze 年龄使用率、死元组占比、长事务数量、WAL 生成速率、I/O wait
  • freeze 年龄超过 80% 就要告警,超过 90% 就要紧急处理
  • 长事务超过 30 分钟必须告警,超过 1 小时可以考虑自动 kill

坑6:不要等出了问题才看 VACUUM 进度

  • pg_stat_progress_vacuum 视图一定要用好
  • 监控每个 VACUUM 的进度、阶段、扫描速度
  • 发现某个 VACUUM 跑得异常慢(比如几小时才扫 10%),及时介入分析

7.3 最终效果与感悟

这次调优完成后,系统稳定运行了 6 个多月,freeze 使用率稳定在 60-70% 之间,autovacuum 正常运转,业务完全无感。最大的收获不是调优本身,而是建立了一套** proactive(主动式)** 的 freeze 运维机制:

  • 监控预警:freeze 使用率 80% 告警 → 运维介入
  • 主动治理:每周低峰期巡检大表 freeze 年龄,主动 VACUUM FREEZE
  • 应用约束:长事务自动 kill + 事务超时 + 连接泄漏检测
  • 容量规划:根据数据增长速度,提前规划 freeze 治理节奏

数据库运维最怕的不是问题多难,而是问题在暗处悄悄积累,等爆发时已经来不及了。Freeze 就是这样一个"沉默的杀手"——平时看不见摸不着,一旦爆发就是大面积故障。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论