【金仓数据库征文】数据库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 地平线无法推进,形成恶性循环。
核心解法:
- 紧急 kill 失控的 autovacuum worker,恢复业务
- 调整 freeze 相关参数,将大表 freeze 任务拆分到业务低峰期执行
- 治理长事务,推进 xmin 地平线
- 优化 WAL 检查点策略与 shared_buffers 配置
- 建立 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等待 → 响应超时 ↓ 应用连接池耗尽 → 业务大面积不可用
根因总结:
- 直接原因:autovacuum freeze 全表扫描大表,产生大量 I/O 和锁竞争
- 深层原因:应用侧存在大量长事务(idle in transaction),拖住 xmin 地平线,导致 autovacuum 无法及时清理和推进 freeze
- 放大因素: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%,安全了。
三、参考资料
-
KingbaseES V9R3C18产品手册 - JDBC驱动使用指南
https://www.kingbase.com.cn/download.html#drive -
KingbaseES MySQL兼容版开发指南
https://www.kingbase.com.cn/download.html#database
-
Druid官方文档 - 常见问题
https://github.com/alibaba/druid/wiki/常见问题 -
HikariCP官方文档 - 配置说明
https://github.com/brettwooldridge/HikariCP -
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 实战细节回顾
这次故障给我印象最深的不是问题本身有多复杂,而是**「温水煮青蛙」式的积累过程**:
- 业务上线初期,autovacuum 正常运行,freeze 年龄缓慢增长
- 应用代码有个批次处理接口,事务里嵌了外部 HTTP 调用,经常卡住,产生长事务
- 长事务拖住 xmin 地平线,autovacuum 清理效率越来越低,死元组越积越多
- freeze 年龄一天天逼近警戒线,但因为没有监控预警,完全没人察觉
- 终于在一个业务高峰期,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 就是这样一个"沉默的杀手"——平时看不见摸不着,一旦爆发就是大面积故障。




