☰
达梦数据库“网络通信异常“排查实战
2026/9/30 7:18:35 网站建设 项目流程

达梦数据库"网络通信异常"排查实战

适用场景:应用日志出现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 发给数据库了,但在超时时间内没等到回包。

它不等于"网络断了"。实际原因按概率排序:

  1. SQL 执行太慢(本次案例的根因:索引失效 → 全表扫描 2400 万行);
  2. 数据库太忙/卡顿(备份、批量任务、磁盘 IO 打满);
  3. 网络链路问题(中间代理掐断、丢包)。

先看报错发生在哪个环节,能立刻缩小范围:

报错位置大概率原因
获取连接/建立连接时网络不通、端口、连接池耗尽
执行 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 步:验证

修复后按四层验收:

  1. 状态:DBA_OBJECTS.STATUS = VALID;
  2. 计划:EXPLAIN 出现SSEK2+ 索引名,不再有CSCN2;
  3. 耗时:真实 SQL 从秒级降到毫秒级;
  4. 实战窗口:在原本报错的时间点(本次是 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 到任何数据库问题)

  1. 读完整报错,尤其Caused by链——它往往比表面错误名诚实。
  2. 先分类再动手:连接问题 / 慢 SQL / DB 卡死 / 网络问题,每类的证据特征不同(见第 0 节的表)。
  3. 每个猜想都要设计一个"能证伪它"的实验,做完看结果再决定下一步。本次依次证伪了:连接池配置未生效(查了依赖源码)→ 空闲连接被掐(test-on-borrow 逻辑推理)→ DB 进程卡死(checkpoint 节奏+参数核对)→ ORDER BY 写法问题(去掉 ORDER BY 仍全表扫)→ 统计信息失真(收完计划不变)→ 最终落到索引 INVALID。
  4. 对照实验是最强武器:找同结构的健康对象(上月表)跑同一条 SQL,一次对比胜过十次猜测。
  5. 配置改动必须验证生效:IDEA 的Cannot resolve property警告不是误报,未知配置会被静默忽略。
  6. 时间点特征是金线索:报错总在整点/固定时间 → 优先怀疑定时任务 + 慢查询的组合。

附 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 抖动)
卡在执行 SELECTCOMMIT 提交
形态每天定时窗口必现瞬时几次,自行消失
时间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)

"明明有索引却不走"的排查清单(按本次实战验证过的顺序)

  1. 索引是不是 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'索引名%';
  1. 统计信息是不是失真:DBA_TABLES里NUM_ROWS=0但表实际有几千万行 →DBMS_STATS.GATHER_TABLE_STATS('模式名','表名');
  2. 列上有没有套函数、有没有隐式类型转换(如字符串列传了数字);
  3. 查询命中行数占比太大(如要查出全表 30% 的行)——这种情况 CSCN 是正确选择,别硬逼它走索引;
  4. LIKE '%xxx%'这类写法天生用不上 B+ 树索引。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询