凌晨两点四十分,我被手机连续弹出的监控告警震醒——达梦数据库连接失败、应用服务大量超时。登录跳板机后发现 dmserver 进程还在,但数据库实例已经完全“假死”:连接全部卡住,任意一条SQL都无响应,查个 V$SESSIONS 都能卡十几秒。这种状态最难受,进程没挂,业务却已经挂了。
事后复盘,一切的起点,是一条每天凌晨定时执行的统计信息更新任务。这本来是数据库中再普通不过的维护动作,为什么能把 DM8 整个干到不可用?这篇就把那次事故的完整过程写下来,包括现象、排查链路、根因、参数调整和恢复步骤。如果你也在负责达梦数据库的运维,或者正在做国产化数据库迁移,这篇值得你花十分钟看完。
1. 事故回溯:一次常规统计信息更新引发的连锁反应
1.1 最初的异常信号:不是连不上,而是“越来越慢”
当天第一次告警并不是“数据库宕机”,而是“会话数超过阈值”。值班系统显示连接数在凌晨两点十几分开始快速上涨,从正常的三四百跳到一千多。紧接着应用层开始报“获取连接超时”,随后才是数据库无法访问。
这里有个非常迷惑人的点:dmserver 进程明明活着,ps -ef | grep dmserver 能看到进程,端口也在监听,但新连接进来后全部卡在认证或等待阶段。用客户端工具连接时要么一直转圈,要么直接报“网络通信异常”。这也是我后来反复和同事强调的——数据库“假死”比真崩溃更危险,因为它不触发高可用切换,全靠人工发现。
当时我们的另一套监控只能发现“进程是否存活”和“端口是否监听”,完全没有覆盖到 SQL 耗时和活跃会话数,导致故障发生了将近十分钟才开始人工介入。你要是还没给自己的 DM8 加 SQL 响应耗时监控,看完这篇赶紧去补上。
1.2 触发因素:为什么这次更新特别重
这套系统是典型的 ERP 类业务,主表是一张订单流水表,平时数据量五千万行左右,每天增量不大。定时任务里配置了一条统计信息更新语句,执行 DBMS_STATS.GATHER_TABLE_STATS 对主表做采样,正常情况下两三分钟就跑完,跑完就结束,从来没出过问题。
但事故前两周,因为接入了一套新业务,这张表的日增量从每天几十万行涨到了几百万行,总行数在事故发生时已经接近三亿。数据量上了一个数量级,统计信息更新需要扫描和采样的数据量也随之暴增。
更麻烦的是,这张表设计得比较宽,有几十个字段,其中还有两个 VARCHAR(2000) 的长字段。当初写统计信息更新任务的人图省事,直接用了全列采样和 AUTO 直方图,没有限制采样率,也没有指定 DEGREE 参数,数据库就按默认并行度跑。三亿行 × 全列采样 × 过长字段排序,几项叠加,计算量直接爆炸。
1.3 事故时间线:从任务启动到强制重启
事后我们把监控数据和日志拼了一下,还原出完整的时间线:
| 时间点 | 现象 |
|---|---|
| 02:00:00 | 定时任务按计划启动,开始更新统计信息 |
| 02:01:30 | 实例内存占用开始快速攀升,临时表空间使用量上升 |
| 02:03:00 | 活跃会话数量持续增加,部分业务SQL开始变慢 |
| 02:08:00 | 会话数突破连接数上限,新连接开始排队 |
| 02:12:00 | 操作系统层出现内存交换,磁盘I/O长时间打满 |
| 02:15:00 | 几乎所有的SQL都处于等待状态,数据库基本不可用 |
| 02:40:00 | DBA介入,尝试登录查询被卡住 |
| 03:05:00 | 保留现场信息后强制重启实例,业务恢复 |
这个时间线很有参考价值。从统计信息任务启动到数据库彻底不可用,中间大概有十几分钟的窗口期。如果监控能在“活跃会话数突增”或“SQL平均耗时变长”这个阶段就告警,完全来得及提前干预。我们的监控恰好漏了这两项,只能眼睁睁看着它从亚健康滑向不可用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 统计信息更新为什么是“重操作”:DM8 的资源消耗机制
2.1 全表扫描与采样策略:采样率不是越高越好
很多人对统计信息更新有个误解,觉得它就是个“算算列的最大值最小值、算算行数”的轻量操作。实际上完全不是。更新统计信息的核心是扫描数据并计算分布,扫描范围由采样率决定,而计算复杂度由采样率和列特征共同决定。
达梦 DM8 的 DBMS_STATS.GATHER_TABLE_STATS 和 Oracle 的用法很像,常用的参数包括:
sql复制DBMS_STATS.GATHER_TABLE_STATS(
'APP_SCHEMA',
'ORDER_FLOW',
NULL,
20, --采样率,这里代表20%
'FOR ALL COLUMNS SIZE AUTO'
);
其中第二个百分数参数是采样率。如果你不指定,很多配置下会走默认策略,可能是全量扫描,也可能是系统估算的采样率。在数据量小的阶段,全量扫描也就几秒钟,没人在意。但当表到三亿行的时候,全量扫描意味着要把整张表从头到尾读一遍,对内存、临时表空间、I/O 是三重冲击。
直方图参数 SIZE AUTO 也有坑。AUTO 模式会让数据库自动判断哪些列需要生成直方图,列特别多的时候,计算量会成倍上涨。尤其是有长字符串列的宽表,排序和哈希计算的成本远高于普通数字列。那次事故里,两个 VARCHAR(2000) 长字段的直方图计算贡献了很大一部分临时表空间的消耗。
2.2 三大资源消耗点:内存排序、临时表空间、I/O 带宽
统计信息更新的资源消耗,主要集中在三个地方。
第一个是内存。生成直方图需要对采样数据进行排序或哈希聚合,这些操作会申请排序缓冲区。如果设置的是会话级排序缓冲区,多个并发会话同时做统计信息更新时,内存占用是成倍叠加的。DM8 中和排序、哈希相关的内存参数有好几个,比较关键的是 SORT_BUF_SIZE 和 HJ_BUF_SIZE。排序区不是越大越好,但太小会有另一个问题——频繁落盘。
第二个是临时表空间。当排序数据量超过排序缓冲区能容纳的上限,DM8 会把中间结果写到临时表空间。三亿行数据全列采样后的排序中间结果可能达到几十个GB,如果临时表空间初始文件不够大,或者没有自动扩展,那么直接会报“临时表空间不足”。就算临时表空间够大,几十GB的读写也会把磁盘I/O打到接近100%。
第三个是I/O带宽。全表扫描本身是对磁盘的持续读取,加上排序落盘,会形成读和写双重的I/O压力。如果底层存储是机械盘,或者虚拟化的共享存储,这种压力会立刻反映到所有业务SQL上,表现为全库变慢。我们的环境就是虚拟化集群,存储本身不算差,但当一个统计任务产生了几十GB的临时读写之后,其他虚拟机都跟着受影响。
2.3 被忽略的锁与元数据竞争:统计更新会阻塞 DML 吗
还有一层容易被忽略的竞争来自锁。DM8 中通过 DBMS_STATS 更新统计信息时,在收集阶段需要对目标表获取元数据锁。虽然不同版本的锁策略有差异,但在某些模式下,统计信息更新的过程中会短暂阻塞表的 DML 操作。
这会造成一个非常恶性的循环:统计信息更新任务占住了资源,导致业务SQL变慢;业务SQL变慢后,事务持锁时间变长;事务持锁时间变长,反过来让统计信息更新需要等待更多资源,甚至产生锁等待。等所有会话都堆积在一起时,数据库就会被拖入死锁式的假死状态。
所以,统计信息更新从来不是可以随意跑的任务,它的锁影响、内存影响、临时表空间影响都需要纳入评估。尤其在大表、宽表、高并发业务并存的场景下,它的破坏力远超大多数人的预期。
3. 宕机根因定位:从系统日志到内存参数的完整排查链路
3.1 第一步:看操作系统层面确认是不是资源耗尽
DBA介入之后,第一步不是进数据库,而是先看操作系统。我们当时的操作顺序是:free -g 看内存、iostat -x 1 看磁盘、vmstat 1 看CPU和交换分区。
系统层面的结果非常典型:内存剩余量从几十GB一路掉到个位数,swap 使用量持续上涨;磁盘I/O等待百分比长期保持在90%以上。这说明数据库实例不是被单个SQL卡住,而是整个实例的内存和I/O资源都已经被耗尽了。操作系统开始大量使用swap,而一旦数据库进程的内存页面被交换到磁盘,整个实例的响应速度就会指数级恶化,进一步加剧SQL堆积。
当时我们通过 top 看到了 dmserver 进程的驻留内存已经到了将近 40GB,而实例配置的内存目标参数 MEMORY_TARGET 只有 30GB 左右,说明进程实际内存已经严重超过了配置预期。这是数据库假死最典型的预兆之一。
3.2 第二步:查 DM8 日志看有没有显式报错
确认系统层异常后,立刻去看 DM8 的实例日志。达梦的日志文件一般在数据目录下的 log 路径里,常见的文件名是 dm_实例名_日期.log,记录了实例启动、停止、错误等关键信息。
我们当时在日志里看到了类似下面的报错(不同版本日志格式有差异,大意一致):
code复制[ERROR] sort run out of temp space
[ERROR] no more free memory pages
[ERROR] failed to allocate memory for sort operation
这三个错误信息串联起来就是完整的因果链:排序操作申请不到内存,转去用临时表空间,临时表空间也不够用,最终内存和临时空间双双耗尽。日志中还夹杂着大量 SQL 执行超时和会话被终止的记录。
3.3 第三步:动态性能视图定位具体消耗大头
日志能确认错误类型,但要定位到底是哪个会话、哪条SQL吃掉了资源,还是得靠动态性能视图。实例已经假死的时候查视图很卡,但只要还能登录,就要尽量查一次。我们当时在恢复后复盘时查了这些关键视图:
sql复制-- 查看当前会话及其状态
SELECT SESS_ID, USER_NAME, STATE, SQL_TEXT, LAST_SEND_TIME
FROM V$SESSIONS
ORDER BY LAST_SEND_TIME;
-- 查看排序内存使用
SELECT * FROM V$SORT_USAGE;
-- 查看临时表空间使用情况
SELECT * FROM V$TEMP_SPACE;
-- 查看连接总数
SELECT COUNT(*) AS CONN_COUNT FROM V$SESSIONS;
从事后采集到的信息看,统计信息更新任务所在会话的排序内存占用达到了数GB,排在所有会话的第一位;临时表空间使用率接近100%。而其他两三百个业务会话全部处于等待状态,都是在等锁或等I/O资源,最终造成了全库不可用的局面。
3.4 根因结论:不是单点问题,是多个因素叠加爆发的系统性风险
那次事故的最终结论,我们写在复盘报告里的原话是:“因大表统计信息更新任务未配置采样率和并行度限制,在数据量突发增长后产生超量内存排序与临时表空间消耗,叠加元数据锁竞争,最终导致实例资源耗尽假死。”
一句话总结就是:任务配置不当 × 数据量突增 × 缺少资源隔离 = 数据库宕机。它不是某一个配置改错就能解释的,而是多个本来“看起来没问题”的因素在同一天叠加到一起,才酿成了事故。这也提醒我们,统计信息更新的风险评估必须和数据量挂钩,不能一套脚本吃三年。
4. 安全更新统计信息的参数调优与操作规范
4.1 调优前先确认现状:别上来就改参数
出事后我见过不少同事的第一反应就是调大内存参数,这其实很危险。在没搞清楚当前参数和资源基线之前盲目调大,可能导致内存占用过高,反而加剧问题。正确的做法是先查现状。
sql复制-- 查看当前内存、排序、临时表空间相关参数
SELECT * FROM V$PARAMETER WHERE NAME IN ('MEMORY_TARGET','SORT_BUF_SIZE','HJ_BUF_SIZE','TMP_SIZE');
-- 查看当前临时表空间文件大小和自动扩展配置
SELECT * FROM V$TEMP_SPACE;
不同 DM8 版本的参数名可能有细微差异,但核心就那几个:内存目标、排序缓冲区、哈希缓冲区、临时表空间大小。先把这些确认清楚,再决定怎么调。
4.2 关键参数配置的实践经验
结合这次事故和我后续做过的压测,我给出几个关键参数的参考建议。以下数值针对的是单实例 16GB 到 64GB 内存的常见生产环境,不是通用标准,但方向可以参考:
| 参数 | 事故前配置 | 建议配置 | 说明 |
|---|---|---|---|
| SORT_BUF_SIZE | 过小,导致频繁落盘 | 8MB - 16MB | 排序缓冲区,过小会导致排序频繁落到临时表空间 |
| HJ_BUF_SIZE | 默认 | 16MB - 32MB | 哈希连接缓冲区,统计信息采样时影响明显 |
| 临时表空间 | 固定20GB,无自动扩展 | 扩到100GB以上并开启自动扩展 | 临时空间不足会直接报错和拖慢全局 |
| MEMORY_TARGET | 30GB | 32GB - 40GB(需评估操作系统内存) | 给实例设定合理的内存上限,避免无限增长 |
| 统计任务并行度 | 默认(可能过高) | 限制为2或4 | 并行度越高,资源消耗越大,不是越大越好 |
4.3 分批采样更新:控制统计信息任务的影响范围
参数调整是基础,更关键的是改掉原来“全量全列一把梭”的统计信息更新方式。我现在的做法是,对大表单独建一批更新任务,和普通表分开跑。
第一步,明确哪些是大表。我们按数据量把库里的表分成了超大表、大表、普通表三档,超大表和大表单独走定制脚本,普通表继续走默认任务。
第二步,为超大表定制采样率。没有特殊需求的表,采样率控制在 10% 到 20%,不要轻易用 100%。对于有长字符串列的宽表,可以进一步降低到 5% 到 10%,甚至只对业务高频使用且参与关联的列生成直方图,而不是所有列。
第三步,限制并行度。在调用 GATHER_TABLE_STATS 时显式指定 DEGREE 参数:
sql复制DBMS_STATS.GATHER_TABLE_STATS(
'APP_SCHEMA',
'ORDER_FLOW',
NULL,
10,
'FOR ALL COLUMNS SIZE AUTO',
DEGREE => 2
);
把并行度限制在 2 或 4,能显著降低瞬时资源峰值,代价是任务运行时间变长。对凌晨批处理来说,多跑几分钟完全值得,保证实例稳定才是第一位的。
4.4 避开业务高峰窗口与锁等待评估
统计信息更新尽量放到业务低谷没问题,但要关注“你的低谷是不是真的是低谷”。我们那次事故的任务是凌晨两点跑,平时确实是低谷,但接入新业务后,半夜会有其他系统批量导入数据。两拨大任务撞在一起,资源竞争加倍。
所以,我强烈建议在排任务时,除了避开业务高峰,还要确认没有其他批量任务和你同时段运行。如果不好调整,就在任务前加一个简单的检测逻辑:连接数超过阈值或活跃会话数过高时直接跳过本次统计信息更新,第二天再补。
对锁竞争,也可以做一次主动评估。在更新前查一下目标表上有没有长时间活动的事务:
sql复制SELECT T.SESS_ID, T.SQL_TEXT, S.STATE, S.LAST_SEND_TIME
FROM V$SESSIONS S, V$TRX T
WHERE S.SESS_ID = T.SESS_ID
AND S.STATE = 'ACTIVE';
如果有长时间未提交的事务,建议先处理完再更新统计信息,或者直接跳过。
4.5 监控和预警:在异常发生前收到消息
如果这次事故的监控能提前十分钟告警,我们完全可以在数据库假死前手动终止统计信息任务,避免全库瘫痪。所以我把监控补全放在了和参数调整同样重要的位置。
现在我在 DM8 环境里至少会监控这几个指标:
- 活跃会话数:超过正常基线的两倍就告警
- SQL平均响应耗时:连续三分钟超过阈值就告警
- 临时表空间使用率:超过80%就告警
- 内存使用率:实例内存达到配置上限的85%就告警
- 连接数:达到 MAX_SESSIONS 的70%就告警
不需要复杂的部署,写一个 shell 脚本定时查动态视图,把结果推给监控平台就行。我当时用了一个很简单的方案:每30秒执行一次 SELECT COUNT(*) FROM V$SESSIONS WHERE STATE='ACTIVE',超过阈值就调告警接口。这套方式成本很低,但作用非常大。
5. 应急恢复与后续防护:这套流程我踩过的坑
5.1 数据库假死的应急恢复步骤:先留现场,再重启
数据库假死状态下,最忌讳的是不采集任何信息就直接 kill 进程。虽然重启能恢复服务,但下次再出问题你还是两眼一抹黑。
我们这次的应急流程是:
- 先尝试用命令行登录数据库,执行一个最简单的
SELECT 1,如果能秒回就不算彻底假死,可以尝试定点清理会话。 - 登录不了或者 SQL 完全卡住,就通过操作系统层面记录
top、free、iostat的输出保存现场。 - 将 DM8 的日志目录复制一份,保留报错原始文件。
- 尝试用
dmserver的停库命令正常关闭实例,如果等太久就直接kill -9(进程假死时优雅关闭往往无效)。 - 重启实例,等待数据库完成恢复流程,再启动应用。
这里有个教训:如果库里有未提交事务,强制重启后需要走崩溃恢复流程,重启时间可能比平时长很多。当时我们等实例恢复花了十几分钟,业务方面因为提前发了通知,所以压力还能接受。
5.2 恢复后如何验证统计信息和执行计划
实例恢复后,第一件事不是马上让所有业务连接进来,而是先确认统计信息任务当时到底把表更新到了什么程度。如果统计信息是半成品状态,查询优化器可能基于错误的统计信息生成极差的执行计划,导致恢复后的SQL比恢复前更慢。
我们当时的做法是:
sql复制-- 查看指定表的统计信息收集时间
SELECT TABLE_NAME, STALE_STATS, LAST_ANALYZED
FROM USER_TAB_STATISTICS
WHERE TABLE_NAME = 'ORDER_FLOW';
确认统计信息最后更新时间。如果停在事故开始前的旧数据,那就保留旧统计信息,先把业务恢复。如果已经收集到一半或者部分列是新的,就要对比一下执行计划有没有明显变化。
对比执行计划可以用 EXPLAIN 命令,选择几条核心业务SQL,在故障前和故障后的计划之间做对比。发现计划异常,就通过 DBMS_STATS.RESTORE_TABLE_STATS 之类的功能回滚到事故前的统计信息。DM8 和 Oracle 类似,提供了统计信息备份恢复的能力,这个功能关键时刻能救命。
5.3 长期防护:把统计信息更新当成高影响变更来管理
这次事故促成了我们团队一个很重要的转变:统计信息更新从“无人关心的定时任务”升级为“高影响变更”。
现在的操作规范是,所有大表的统计信息更新必须提前一天提变更申请,变更单里要写清楚目标表、预计扫描行数、采样率、预计耗时、影响评估和回退方案。执行时再到现场观察,确认资源消耗在预期范围内。
对超大表,我们还引入了一个折中方案:不在统计信息更新任务里做全表级的直方图刷新,而是平时维护基础统计信息,在业务低谷挑特殊表做单独的深度分析。这样既保证了优化器有数据可用,又避免了一个任务拖垮全库。
另外,初始建的定时任务脚本要有“熔断”机制——如果执行超过预设时间还没有完成,就主动终止。我们当时加了一个简单的超时控制,统计信息任务超过30分钟直接杀会话并告警,防止意外情况再次拖垮实例。
5.4 几个容易踩的坑:连接数、备份和参数持久化
最后分享三个容易踩的坑。
第一个是连接数打满之后不要盲目调大 MAX_SESSIONS。连接数上限是配合内存资源的,你调大连接数上限,每个会话还要占内存,反而可能加速内存耗尽。正确思路是先杀掉非核心会话,腾出资源,而不是把上限调大。
第二个是恢复后的备份验证。强制重启后一定要做一次数据库逻辑备份,过程中能发现很多隐藏的页面损坏。我们那次重启后做了一次 dmp 导出,虽然表数据没事,但确实发现了一个索引页异常,及时做了修复。平时备份没问题,不代表强制重启后没问题,这点很多运维容易忽略。
第三个是参数持久化。通过 SP_SET_PARA_VALUE 或会话级调整的参数,有的只在当前实例运行时生效,重启后可能被配置文件里的旧值覆盖。调整完参数后,确认一下配置文件里的实际值,确保重启后配置依然生效。不要等到下次重启才发现参数又变回去了。
达梦 DM8 在国产数据库里使用范围越来越广,但很多人还是拿 Oracle 的运维习惯直接套。统计信息更新这件事,Oracle 环境和 DM8 环境踩的坑不完全一样,尤其是内存参数和临时表空间策略,DM8 有它自己的脾气。这篇是那次故障的完整复盘,也包含了后来我们在生产环境验证过的调优方案。不一定适合所有环境,但排查思路和预防思路是通用的。希望你能借着我的经验,避免在同样的地方跌倒。
