达梦数据库"网络通信异常"排查实战
适用场景:应用日志出现
dm.jdbc.driver.DMException: 网络通信异常/SocketTimeoutException: Read timed out。
按实际排查顺序组织,每步给出命令和判断标准。
0. 先看懂这个报错
典型堆栈长这样(关键看最下面的Caused by):
org.springframework.dao.DataAccessResourceFailureException: ### Error querying database. Cause: dm.jdbc.driver.DMException: 网络通信异常 ### SQL: SELECT ... WHERE (ALARM_ID = ?) ... Caused by: java.net.SocketTimeoutException: Read timed out这句话的含义:客户端把 SQL 发给数据库了,但在超时时间内没等到回包。
它不等于"网络断了"。实际原因按概率排序:
- SQL 执行太慢(本次案例的根因:索引失效 → 全表扫描 2400 万行);
- 数据库太忙/卡顿(备份、批量任务、磁盘 IO 打满);
- 网络链路问题(中间代理掐断、丢包)。
先看报错发生在哪个环节,能立刻缩小范围:
| 报错位置 | 大概率原因 |
|---|---|
| 获取连接/建立连接时 | 网络不通、端口、连接池耗尽 |
执行 SQL 时Read timed out | 慢 SQL 或 DB 忙(本次案例) |
空闲很久后第一次用就报Connection reset/ EOF | 空闲连接被中间件(nginx/防火墙)掐断 |
第 1 步:收集基本信息
从日志里摘出来:
- 报错的完整 SQL(堆栈里
### SQL:后面就是原句); - 报错时间点:是随机的,还是集中在整点/固定时间?(集中在 0 点、6 点 → 强烈暗示和定时批量任务有关);
- 是单个请求偶发,还是多个线程同一秒集中报错?(集中报错 = 大家同时变慢,指向 DB 侧或共性资源,不是个别连接问题)。
本次案例:报错集中在00:00:10和06:01:45~06:02:41,多个 gRPC 线程同一秒内集体超时——这个特征最后证明是"批量并发 + 慢 SQL"的典型表现。
第 2 步:排除"空闲连接被掐断"嫌疑
看连接池配置(本项目application.yml,生产配置在服务器/xx/xxxx/application.yml):
druid:test-on-borrow:true# 借连接前先发 SELECT 1 FROM DUAL 验证test-while-idle:true# 空闲连接定期验证validation-query:SELECT 1 FROM DUALtime-between-eviction-runs-millis:10000min-evictable-idle-time-millis:30000# 空闲 30s 驱逐max-evictable-idle-time-millis:60000# 空闲 60s 强制关闭判断逻辑:
- 配了
test-on-borrow: true的,每次拿连接都会先验证一遍。如果连接已被中间件掐死,验证那一步就会失败并换新连接,轮不到执行真实 SQL 时才报错。堆栈显示失败点在execute(执行真实 SQL)→ 说明连接是活的 → 排除空闲连接问题。 - 配了
max-evictable-idle-time-millis: 60000的,空闲连接 60 秒就被物理关闭重建,中间件根本没机会掐断池内连接。
配置陷阱(本次踩过):yml 里写了keep-alive-between-time-millis: 30000,但 IDEA 提示Cannot resolve property——这个警告是真的:dynamic-datasource-spring-boot-starter3.5.x 的DruidConfig类里没有这个字段,Spring Boot 默认静默忽略未知配置,不写进日志也不报错。
第 3 步:数据库侧日志——DB 有没有卡死
在 DB 服务器上:
cd/xxxxxx/dmdba/dmdbms/logls-lht# 实例日志一般是 dm_DMSERVER_YYYYMM.log# 提取报错时间窗口的完整日志(不要只 grep 关键字,先看全貌)awk'/^2026-09-23 05:5[5-9]/,/^2026-09-23 06:15/'dm_DMSERVER_202609.log# 再找异常行awk'...'dm_DMSERVER_202609.log|grep-inE"error|fatal|fail|hang|timeout|kill"checkpoint 节奏判断(本次踩过的坑):
SELECTPARA_NAME,PARA_VALUEFROMV$DM_INIWHEREPARA_NAMELIKE'CKPT%';CKPT_INTERVAL=180表示每 180 秒一次 checkpoint 是正常节奏。看到日志里 3 分钟空档不要急着判"DB 卡死",先对一下这个参数。- 如果 checkpoint 在报错窗口内正常执行(时间戳连续、有
checkpoint end),说明DB 进程当时活着且在正常干活,"DB 整体卡死"基本排除。
查库内定时作业(备份、归档清理等,排除它们的干扰时段):
SELECT*FROMSYSJOB.SYSJOBS;-- 有哪些作业SELECT*FROMSYSJOB.SYSJOBHISTORIES2ORDERBYSTART_TIMEDESCLIMIT50;-- 最近执行记录本次案例:备份在 22:00/23:00,报错在 00:00/06:00,时间不重合 → 库内作业排除。报错窗口内 checkpoint 正常 → DB 卡死排除。
第 4 步:给报错的 SQL 做体检
4.1 表有多大
SELECTCOUNT(*)FROM模式名.表名;-- 本次:2377 万行4.2 看执行计划
/xxxxxx/dmdba/dmdbms/bin/disql 用户名@localhost:5236计划怎么看(记两个关键字就够):
| 关键字 | 含义 | 好坏 |
|---|---|---|
CSCN2 | 全表扫描(把整表读一遍再过滤) | 大表上出现 = 危险 |
SSEK2 | 索引查找(精准定位) | 正常 |
完整的计划阅读教程,见文末【附 4】。
本次案例:
WHERE ALARM_ID=?的 SQL 计划是CSCN2 ... 24062124(扫 2400 万行),实测单条耗时7~9 秒——病根浮出水面。
4.3 手动跑一遍计时
disql 里直接执行原 SQL,看used time。多跑几次取稳定值。
4.4 索引三层检查
-- ① 有没有索引SELECTINDEX_NAME,UNIQUENESSFROMDBA_INDEXESWHERETABLE_OWNER='模式名'ANDTABLE_NAME='表名';-- ② 索引建在哪些列上SELECTTABLE_NAME,INDEX_NAME,COLUMN_NAME,COLUMN_POSITIONFROMDBA_IND_COLUMNSWHERETABLE_OWNER='模式名'ANDTABLE_NAME='表名';-- ③ ★索引状态是不是 VALID(本次的根因就藏在这里)SELECTOBJECT_NAME,STATUSFROMDBA_OBJECTSWHEREOWNER='模式名'ANDOBJECT_TYPE='INDEX'ANDSTATUS<>'VALID';-- ④ 索引段大小(几千万行的索引应该是几百 MB~GB 级;1MB = 空壳)SELECTSEGMENT_NAME,BYTES/1024/1024ASMBFROMDBA_SEGMENTSWHEREOWNER='模式名'ANDSEGMENT_NAMELIKE'INDEX_UWMATI%202609';本次案例:索引存在,列也对,但
STATUS = INVALID、字段只有 1MB——是个空壳,优化器根本用不了它。
4.5 统计信息检查
SELECTTABLE_NAME,NUM_ROWS,LAST_ANALYZEDFROMDBA_TABLESWHEREOWNER='模式名'ANDTABLE_NAME='表名';NUM_ROWS=0但实际有几千万行 = 统计信息失真,会导致优化器选错计划。修复:
DBMS_STATS.GATHER_TABLE_STATS('模式名','表名');-- 或 DM 原生语法:STAT 100 ON 模式名.表名;4.6 对照实验
找一张同结构的老表(上月分表),跑同一条 SQL 的 EXPLAIN:
EXPLAINSELECTIDFROM模式名.表名_202608WHEREALARM_ID='xxxx';本次案例:老表
SSEK2(走索引,代价 1),新表CSCN2(全表扫,代价 3297)——同样的表结构和 SQL,命运不同 → 问题一定出在新表自身的状态上,和 SQL 写法无关。这一步直接终结了"是不是 ORDER BY 导致不走索引"的争论。
第 5 步:修复
-- INVALID 索引重建(会全量扫表重建索引段,2400 万行约 1~2 分钟)ALTERINDEX模式名.INDEX_UWMATI_ALARM_ID_202609 REBUILD;注意:
- 重建期间新旧索引段短暂并存,先确认表空间剩余空间(参照同表老月份的索引段大小);
- 批量生成所有坏索引的重建命令:
SELECT'ALTER INDEX '||OWNER||'.'||OBJECT_NAME||' REBUILD;'ASCMDFROMDBA_OBJECTSWHEREOWNER='模式名'ANDOBJECT_TYPE='INDEX'ANDSTATUS<>'VALID';查出来几条就修几条,结果为 0 行才算清完(别只修报错 SQL 用到的那一个)。
第 6 步:验证
修复后按四层验收:
- 状态:
DBA_OBJECTS.STATUS = VALID; - 计划:EXPLAIN 出现
SSEK2+ 索引名,不再有CSCN2; - 耗时:真实 SQL 从秒级降到毫秒级;
- 实战窗口:在原本报错的时间点(本次是 0 点、6 点)观察应用日志,
网络通信异常不再出现才算闭环。
附 1:本次故障完整因果链
建表脚本(迁移工具生成)里 CREATE INDEX 带 UNUSABLE 关键字 → 9 月新表的索引建出来就是 INVALID 空壳(1MB),没人做 REBUILD → 优化器用不了索引,`WHERE ALARM_ID=?` 只能全表扫描 → 9 月表从 0 涨到 2400 万行,单条查询从毫秒涨到 7~9 秒 → 平时请求零散,7 秒没超客户端超时,"看着正常" → 每天 0 点/6 点上游批量任务并发打进来,几十条全表扫描互相抢 IO → 每条拖到几十秒,集体越过读超时阈值 → 应用日志爆发"网络通信异常: Read timed out"为什么数据量越大报错越频繁:全表扫描耗时和行数成正比。
为什么修连接池没用:连接一直是好的,病在 SQL 执行速度。连接池校验(test-on-borrow)检查的是"电话线通不通",本次的问题是"电话打通了但对方半天不说话"。
附 2:排查方法论小结( transferable 到任何数据库问题)
- 读完整报错,尤其
Caused by链——它往往比表面错误名诚实。 - 先分类再动手:连接问题 / 慢 SQL / DB 卡死 / 网络问题,每类的证据特征不同(见第 0 节的表)。
- 每个猜想都要设计一个"能证伪它"的实验,做完看结果再决定下一步。本次依次证伪了:连接池配置未生效(查了依赖源码)→ 空闲连接被掐(test-on-borrow 逻辑推理)→ DB 进程卡死(checkpoint 节奏+参数核对)→ ORDER BY 写法问题(去掉 ORDER BY 仍全表扫)→ 统计信息失真(收完计划不变)→ 最终落到索引 INVALID。
- 对照实验是最强武器:找同结构的健康对象(上月表)跑同一条 SQL,一次对比胜过十次猜测。
- 配置改动必须验证生效:IDEA 的
Cannot resolve property警告不是误报,未知配置会被静默忽略。 - 时间点特征是金线索:报错总在整点/固定时间 → 优先怀疑定时任务 + 慢查询的组合。
附 3:如何看懂执行计划
是什么、怎么跑
EXPLAIN <SQL>让数据库把"打算怎么执行这条 SQL"的计划打印出来——只编译、不真正执行,随便跑不伤数据。
注意:有些 web 控制台会把 EXPLAIN 当"非查询语句"拒绝(报Error 9005: 非查询SQL语句),这时去 DB 服务器上用 disql:
/xxxxxx/dmdba/dmdbms/bin/disql 用户名@localhost:5236 SQL>EXPLAIN SELECT...;附 4:案例二:COMMIT 超时(存储 IO 抖动)
2026-09-26 发生的第二次同类报错,根因与案例一完全不同,形态和查法也不同,一并固化。
症状特征
org.springframework.transaction.TransactionSystemException: Could not commit JDBC transaction Caused by: dm.jdbc.driver.DMException: 网络通信异常 at dm.jdbc.a.a.commit(DBAccess.java:249) ← 关键:卡在 COMMIT,不是卡在执行 SQL Caused by: java.net.SocketTimeoutException: Read timed out与案例一的区分点:堆栈里是commit(DBAccess.java:xxx)(提交阶段),而不是executeInner(执行阶段)。说明 SQL 已执行完,死在等事务提交的回执。
为什么 COMMIT 会卡
数据库 WAL(预写日志)机制:COMMIT 必须等 redo 日志落盘成功,才能回执客户端。redo 盘一旦卡顿,所有提交排队 → 客户端读超时 → 报"网络通信异常"。
决定性证据:DB 实例日志里的刷盘告警
awk'/^2026-09-26 20:3[0-9]/,/^2026-09-26 20:40/'dm_DMSERVER_202609.log看到这种行就是事件真相:
[WARNING] rlog4_write_to_file rlog_pkg[...] uses 1877ms, pkg_len:4096含义:4KB 的 redo 写入花了 1.8 秒(正常为毫秒级)。本次窗口 10 分钟内出现 22 次(1~4.3 秒/次),同时 checkpoint 耗时从 0.1 秒涨到 5~11 秒、节奏迟到——存储抖动造成。
形态特征(与案例一区分)
| 案例一(索引 INVALID) | 案例二(IO 抖动) | |
|---|---|---|
| 卡在 | 执行 SELECT | COMMIT 提交 |
| 形态 | 每天定时窗口必现 | 瞬时几次,自行消失 |
| 时间 | 0 点/6 点批量时刻 | 随机 |
| DB 日志 | 干净 | rlog4_write_to_file uses XXXms告警 |
排查命令
# 告警是否常态、从何时开始(按 日期+小时 统计)grep"rlog4_write_to_file"dm_DMSERVER_YYYYMM.log|awk'{print $1" "substr($2,1,2)}'|sort|uniq-c# 磁盘当时状态sar-d-p-f/var/log/sa/sa<日>|lessdmesg-T|grep-iE"i/o error|blocked|hung"|taildf-h;lsblk# 确认 redo 盘与备份盘是否同一块盘(IO 互相抢)数据一致性检查
COMMIT 超时 = 客户端不知道服务端到底提交成没成(可能已提交,只是回执丢失)。上游若重试可能产生重复数据——报错时间窗口的业务数据要抽查一遍。
处置方向
- 确认抖动源(云盘类型/积分、同盘 IO 争抢、宿主争抢)后对症处理:换高性能盘、备份与数据分盘等;
- 把
rlog4_write_to_file告警纳入监控(出现即告警),它比应用报错更早暴露存储问题; - 应用侧不用为此改连接池配置——这既不是连接问题也不是 SQL 问题。
本次问题的真实计划对比
修复前——注意第 5 行:
1 #NSET2: [4376, 1, 329] 2 #PRJT2: [4376, 1, 329]; exp_num(12), is_atom(FALSE) 3 #SORT3: [4376, 1, 329]; key_num(1), top_flag(1) 4 #SLCT2: [4375, 1, 329]; 表名.ALARM_ID = '5000_...' 5 #CSCN2: [4375, 24062124, 329]; INDEX33557234(表名); btr_scan(1)- 执行顺序:⑤ 扫表(2400 万行全读)→ ④ 过滤 ALARM_ID → ③ 排序 → ② 选列 → ① 输出;
CSCN2后面跟着的INDEX33557234是主键聚集索引——“沿着主键把整表读一遍”,别被"INDEX"这个词迷惑,本质还是全表扫;- 代价
4375,实测 7~9 秒。
修复后(正常):
6 #SSEK2: [2, 131, 48]; scan_type(ASC), INDEX_UWMATI_ALARM_ID_202609(...), scan_range[UWMAI.ALARM_ID, UWMAI.ALARM_ID]- 直接写着用了哪个索引;
scan_range['5000_...','5000_...']= 只扫索引里等于该值的一小段; - 代价
2,实测毫秒级。代价差 2000 倍,与实测耗时完全对应。
常见节点速查表
CSCN 不是原罪,大表上的 CSCN 才是。判断顺序永远是:先看表多大(COUNT(*)),再看计划。
| 节点 | 含义 |
|---|---|
CSCN2 | 全表扫描 |
SSEK2 | 二级索引范围查找 |
BLKUP2 | 回表(按行号回表取索引里没有的列) |
SLCT2 | 过滤(把扫描出来的行按条件筛) |
SORT3 | 排序(top_flag(1)= LIMIT 的 top-N 优化) |
PRJT2 | 投影(挑出 SELECT 要的列) |
NSET2 | 结果集封装输出 |
HASH JOIN/INDEX JOIN SEMI JOIN | 两表关联方式(SEMI JOIN 来自 EXISTS/NOT EXISTS) |
"明明有索引却不走"的排查清单(按本次实战验证过的顺序)
- 索引是不是 INVALID/空壳(★本次根因):
SELECTOBJECT_NAME,STATUSFROMDBA_OBJECTSWHEREOWNER='模式名'ANDOBJECT_TYPE='INDEX'ANDSTATUS<>'VALID';-- 结果为 0 行才正常;有记录就用 ALTER INDEX <owner>.<索引名> REBUILD; 修复辅助确认空壳(大表的索引段应是几百 MB~GB 级,1MB = 空壳):
SELECTSEGMENT_NAME,BYTES/1024/1024ASMBFROMDBA_SEGMENTSWHEREOWNER='模式名'ANDSEGMENT_NAMELIKE'索引名%';- 统计信息是不是失真:
DBA_TABLES里NUM_ROWS=0但表实际有几千万行 →DBMS_STATS.GATHER_TABLE_STATS('模式名','表名'); - 列上有没有套函数、有没有隐式类型转换(如字符串列传了数字);
- 查询命中行数占比太大(如要查出全表 30% 的行)——这种情况 CSCN 是正确选择,别硬逼它走索引;
LIKE '%xxx%'这类写法天生用不上 B+ 树索引。