DMLens是我开发的一个小程序,用来可以自动获取达梦 AWR等相关信息。
另外还要特别感谢白鳝老师在我开发测试DMLens中提供的一些技术指导。
还要特别感谢白鳝老师公司所提供的BIC-QA产品
==另外受限于达梦官网编辑器对文章长度限制,在本系列最后(下)会有相应pdf 文章文章,可供下载。
==
个人介绍:尚雷 Oracle & PG ACE、Oracle OCM、公众号(尚雷的驿站)
📚 本文为系列连载(上篇) 原稿《 DMLens_让 DM9 的 AWR 说真话:八处口径陷阱、BIC-QA 在线印证与一条命令跑完的锁争用闭环》因超出官网单篇 40000 字词上限,拆分为上 / 中 / 下三篇发布。
- 上篇(本篇):写在前面 + 第 1 章 理论基础 + 第 2 章 实验环境与准备 + 第 3 章 实操步骤 1–4(环境体检 → 人为制造锁争用 → 采集 AWR → 规则诊断)
- 中篇(本篇):第 3 章 步骤 5–9(把 AWR 报告交给 BIC-QA 在线印证 → 定位根因 → 受控解决 → 复测验证 → 审计收尾)+ 全流程时序 + 第 4 章 一键复测
- 下篇(本篇):第 5 章 八处口径陷阱 + 第 6 章 实验数据总表 + 第 7 章 结论与建议 + 第 8 章 复测取证 + 附录 A–D
三篇可独立成篇,也可顺序阅读;所有截图(figures/ 下 8 张架构图 + 16 张复测证据图)与命令均来自同一台 DM9 实例的真实复测。
本文不是"读懂了就好"的原理综述,而是一次可完整复测的实验记录。笔者在一台 DM9 实例上,人为制造了一个真实的数据库性能问题(热点行锁争用),然后按下面的链路走完全程:
制造问题 → 采集 AWR → 规则诊断 + BIC-QA 知识印证 → 定位根因 → 受控解决 → 复测证明闭环
这条链路上的每一步都有真实的命令、真实的回显、真实的耗时与数据,并且最后被封装成一条命令,你可以在自己的环境里原样重跑。
实验一览(本文实测的一次完整运行):
| 项 | 实测值 |
|---|---|
| 制造的问题 | 热点行锁争用(1 个持锁事务阻塞 8 个会话) |
| AWR 区间 | SNAP_ID 21 → 22,区间跨时约 7.57 秒 |
| 规则诊断 | 健康 88.98 / 100,发现 7 条(严重 1 / 高 2 / 中 2 / 低 1 / 提示 1) |
| 知识印证 | 规则诊断运行时通道 offline(Key 已失效)→ 内置知识图谱兜底;随后以有效 Key 补跑官方在线分析,通道 online(见步骤 5) |
| AWR → BIC-QA | 原生 AWR 报告 1825 行(dm_awr_21_22.html);在线分析成功:online、2,701,275 tokens / 174.3 s(见步骤 5.3) |
| 受控处置 | SP_CLOSE_SESSION(140359434824088) → 结果 ok |
| 复测结果 | 阻塞链 1 → 0,等待会话 8 → 0,判定 healed,归因本工具 是 |
| 独立旁证 | 4/4 等待事务解除阻塞并成功获锁 |
| 完整性与收尾 | 审计哈希链 ok = true(已校验 56 条);残余阻塞链 0 条 |
| 全程耗时 | 约 14 秒 |
| 独立复测 | 本文数据来自四次独立完整运行:上表为第一次(一键 e2e,区间 21 → 22);第二次按第 8 章逐命令复测(区间 49 → 50,含四场景锁实验、死锁环、在线分析、收尾);第三 / 四次为 2026-09-15 受控执行补拍取证(在线 / 离线两种知识通道,见 6.2)。全部结论结构一致(多区间对照见第 6 章,21 张真实证据图已全部落位) |
三个问题,先给答案:
| 你可能关心的问题 | 本文的答案 | 直达章节 |
|---|---|---|
| 怎样人为造出一个有说服力的性能问题? | 用受控事务构造四类锁争用(单行锁 / 热点行 / 三级级联 / 死锁环路),并说明为什么等待方必须重试 | 第 3 章 步骤 2 |
| AWR 采到了,怎么保证算得对? | 立住"区间差分"口径,规避 八处口径陷阱 + 1 类单位陷阱 | 第 1 章、第 5 章 |
| 诊断建议凭什么可信?自动修复会不会冒领功劳? | BIC-QA 三通道印证 + 伪成功识别;闭环设六道防线与归因红线 | 第 3 章 步骤 5/7/8、第 1.5 节 |
在展开原理之前,先给出全局视图。本方案分五层,每层职责单一、可独立复算(图 1)。
图 1 总体架构:达梦 AWR 采集 × BIC-QA 知识印证 × 受控闭环
| 层 | 职责 | 关键设计 |
|---|---|---|
| 数据源层 | 读取 DM9 的 AWR 快照与动态视图 | 只读;WRH$_* 走快照对,实时状态走 V$* |
| 口径层 | 把"原始计数"变成"可信指标" | 区间差分、单位换算、计数器语义登记表 |
| 规则诊断层 | 规则引擎 + 八维健康评分 | 每条发现自带证据 SQL 与口径声明 |
| 知识印证层 | BIC-QA 官方 Open API + 内置图谱兜底 | 三通道;识别错误载荷 → 降级 + 熔断 |
| 受控闭环层 | 双闸门 → 白名单 → 身份复核 → 执行 → 复测 → 回流 | 全程哈希链审计 |
这五层合起来,正是 DM9“DB+AI 融合”在运维侧的一次具体落地:
数据库负责把数据采准、存住(AWR 快照体系与动态视图),AI 负责把现象解释成结论(BIC-QA 在线分析 + 内置图谱),
而中间那层口径与最后那层受控闭环,是让 AI 结论可以被采信、可以被执行的关键——
少了口径层,AI 会基于失真数字给出看似专业的错误结论;少了闭环层,AI 的建议永远停留在“建议”。
本文的全部工作,就是把这两层补齐,并用实测证明它们有效。
达梦的 AWR(Automatic Workload Repository)把系统级统计量按快照周期落库到 SYS.WRH$_* 系列表中。围绕它有三个必须建立的认知:
(1) 快照是"时间轴上的采样点",不是"区间表"。
WRH$_SYSSTAT 中 SNAP_ID = 22 的行,含义是"截至快照 22 的累计值",而不是"快照 22 这一段的统计"。所有"本次区间表现如何"的问题,都必须做差:
区间指标 = val(SNAP_ID = END) − val(SNAP_ID = BEGIN)
达梦原生 AWR 报告内部也是这么算的。本文所有指标都显式标注口径与单位。
(2) 快照周期可调,但决定了最小可分析粒度。
本环境实测控制项:SNAP_INTERVAL_SEC = 1800(30 分钟自动快照)、RETENTION_DAYS = 8、TOPNSQL = 30。对故障现场而言,30 分钟的自动粒度太粗,因此需要手工打快照把区间"夹"出来——这正是本文步骤 3 的做法。
(3) 单快照的系统级指标规模。
实测 SYS.WRH$_SYSSTAT 单个快照有 324 个不同名称的指标。规则不可能穷举,这也是需要知识库参与印证的原因之一(见 1.4 节)。
一个典型的阻塞链是这样长出来的:
事务 A:UPDATE T SET ... WHERE ID = 1; -- 未提交,持有 ID=1 的行锁
事务 B:UPDATE T SET ... WHERE ID = 1; -- 申请同一行 → 等待 A
事务 C:UPDATE T SET ... WHERE ID = 1; -- 申请同一行 → 排在 B 后面
...
uncommitted transaction 是最常见的根因);达梦的两条实测语义(很重要,直接影响脚本正确性):
| 现象 | 实测返回 | 对程序的含义 |
|---|---|---|
| 长锁等待超时 | [CODE:-608] 请求执行超时 |
客户端必须重试;不重试则等待方自行退出,阻塞链会在观测窗口内**“凭空消失”**,导致取证失败 |
| 死锁被检出 | [CODE:-6403] 死锁 |
受害事务被回滚,DEADLOCK_CYCLE 会被记录,可据此复盘环路 |
另需注意:达梦本构建未提供语句级取消,只有会话级终止(
SP_CLOSE_SESSION)。这一点在闭环设计里必须如实说明,不能做虚假承诺。
| 口径 | 含义 | 典型指标 | 禁止操作 |
|---|---|---|---|
interval |
区间增量 | 等待时间、解析耗时与次数 | 不可直接取值 |
cumulative |
实例级累计 | 缓冲池命中率 RAT_HIT |
不可做差(累计比率无意义) |
high_water_mark |
高水位(只增不减且结转) | transaction deadlock count |
不可当作"本区间发生了" |
一句话记忆:引用任何 AWR 指标前,先问三句——这是区间还是累计?单位是页还是字节?会不会结转?
规则引擎能回答"发生了什么"(锁等待 21.712 秒、存在 1 条阻塞链),但回答不了"为什么会这样、业界怎么处置"。规则若把结论与建议写死,遇到新场景就只能猜。
因此本方案把规则诊断与 BIC-QA 知识印证结合:规则给出本次的实测证据,知识库给出成因与处置经验,二者互相锚定。
DMLens 通过 BIC-QA 官方 Open API 接入达梦知识库(dataset_code = 2103)。先把官方接口面说清楚(依据《Open API 第三方开发对接文档》v1.1,接口路径原样保留):
| 能力 | 方法与路径 | 用途 |
|---|---|---|
| 建会话 | POST /open-api/v1/session/create |
鉴权自检;后续问答的容器 |
| 上传附件 | POST /open-api/v1/chat/attachments |
multipart/form-data,字段 batchId + file;返回 attachmentId、status |
| 流式问答 | POST /open-api/v1/chat |
SSE;可携带 batchId(带上附件)、model |
| 会话详情 | GET /open-api/v1/session/{conversationId} |
回看历史消息与文件 |
| 配额明细 | POST /open-api/v1/token-usage/details |
查消耗与剩余额度 |
关键认知(本文实测确认):官方 Open API 没有独立的"AWR 分析"端点。浏览器扩展里的「AWR分析」按钮,本质是两个通用接口的组合——
① 先把 AWR 报告作为附件上传拿到batchId,② 再用batchId提问。 这正是本文步骤 5 的实现方式。
三通道与"报告标注"(见图 3):工具必须把"结论从哪来"如实写在报告里,绝不"假在线":
| 通道 | 触发条件(实测判据) | 报告标注 |
|---|---|---|
| 在线问答 | API 可达且返回非错误载荷 | online |
| 降级标注 + 熔断 | API 可达,但 chat 返回错误载荷 |
degraded |
| 内置知识图谱兜底 | 鉴权失败 / 网络不可达 / 已熔断 | offline(走内置图谱) |
图 2 知识印证三通道与"伪成功"识别流程
四种返回都实测到了(这是本文最有价值的一手材料之一)。同一天、同一实例、同一区间,随 Key 状态不同,工具观察到截然不同的返回——而关键点是:HTTP 状态码都不足以判断成功。
| # | Key 状态 | session/create |
chat |
工具判定 | 若不校验载荷会怎样 |
|---|---|---|---|---|---|
| [0] | 有效且余额充足 | HTTP 200 √ | HTTP 200 + 14 900 个 delta 帧(正文 4,897 字,tokens=2,701,275) |
online |
——(这是正常路径,见步骤 5.3) |
| ① | 有效但配额耗尽 | HTTP 200 √ | HTTP 200 + 正常 SSE 帧序,正文=“Token余额不足,请先充值”(OPEN_API_TOKEN_INSUFFICIENT) |
degraded |
把"余额不足"当成知识印证写进报告 |
| ② | Key 失效 / 过期 | HTTP 401(OPEN_API_KEY_INVALID) |
未调用 | offline |
若只 catch 网络异常,会当成"网络抖动"反复重试 |
| ③ | 上传成功 ≠ 可分析 | HTTP 200 √ | —— | —— | 附件 status=READY 会误导人以为"已分析" |
配额是账户级、不是 Key 级:新建/更换 Key 不会带来新额度。判别规则只有一条——只要
session/create返回 200,Key 本身就没问题,此时chat被拒必然是账号余额问题,只能充值。
关键工程教训——"伪成功"识别:配额耗尽时,接口仍然返回 HTTP 200、SSE 流也正常帧序,但正文其实是错误文案。正确做法是:校验载荷内容 → 识别错误标记 → 标记
degraded/offline并写reason_code→ 熔断(首次失败后不再重试)→ 如实退回内置图谱。本工程把该判断集中在bicqa.looks_like_error()一个函数里,可单测、可审计。
自动化处置必须"经得起审问"。本方案设六道防线(见图 4):
| # | 防线 | 机制 |
|---|---|---|
| 1 | 双闸门 | 需配置 auto_execute = true 且命令行 --execute,缺一不执行 |
| 2 | 动作白名单 | 仅允许 SP_CLOSE_SESSION;并如实说明本构建无语句级取消 |
| 3 | 保护名单 + 单轮上限 | deny_users 名单与单轮动作上限 |
| 4 | 身份复核 | 执行前后各核对一次目标会话的 SESS_SEQ / 用户名,变更即跳过(防会话 ID 复用误杀) |
| 5 | 自身排除 | 永不处置本工具自身的会话 |
| 6 | 全程审计 | 每个动作(含被拦截、被跳过)落哈希链审计,可校验完整性 |
一条归因红线(全文最硬的一段):
未执行任何动作时,阻塞链的任何变化都不可归因于本工具。
因为阻塞链可能自己消失(等待方超时退出、持锁方提交)。此时工具只能输出 no_action_*,禁止声称 healed。三种模式的对照见第 3 章步骤 8 与图 5。
| 项目 | 实测值 |
|---|---|
| 实例名 | DMSERVER |
| 版本 | DM Database Server x64 V9 |
| 构建号 | 03151060506-20260417-322930-20218 |
| 状态 / 模式 | OPEN / NORMAL |
| 页大小 | 16384(16 KB) |
| AWR 快照间隔 | 1800 秒 |
| AWR 保留天数 | 8 天 |
TOPNSQL |
30 |
① 部署 DM9 实例,搭建试用环境。本文环境是笔者按 DM9 单机部署流程在 CentOS 7.9 上从零搭建的试用实例
(含 LVM 分层存储规划、系统参数与用户资源限制配置、实例初始化、归档与备份策略配置,全过程记录见笔者的 DM9 部署实操文档,此处不展开)。
就本文主题而言,搭建过程中与 AWR 直接相关的三点试用体感值得先说——它们决定了后面所有实验能否成立:
| 试用观察 | 体感与结论 |
|---|---|
| AWR 需显式初始化 | AWR 能力以系统包形式提供,需先执行初始化(自动建立 SYSAUX 表空间与 SYSAWR.DBF),并非装完即可用——这是上手时最容易卡住的一步 |
| 自动快照默认 30 分钟 | 对故障现场而言粒度过粗,真实排查必须手工打快照把区间“夹”出来(本文步骤 3 即如此);这与“开了自动采集就够了”的直觉相反 |
快照清单视图是 WRM$_* |
历史统计在 WRH$_*,而快照元数据在 SYS.WRM$_SNAPSHOT——前缀相近但不同,照习惯写 WRH$_SNAPSHOT 会直接报对象不存在 |
② 检查 AWR 是否就绪——若未初始化需先创建 AWR 系统包:
# 创建达梦 AWR 系统包(会自动建立 SYSAUX 表空间与 SYSAWR.DBF)
.\py.cmd run.py init
# 开启自动快照(间隔 30 分钟;本次实验主要依赖手工快照,此处可选)
.\py.cmd run.py enable --interval 30
③ 确认驱动与连接可用:本工程依赖 dmPython 驱动。若使用独立虚拟环境,请注意受管解释器可能不含该驱动,需使用已安装驱动的解释器。
📷 截图位 1|环境就绪(推荐作为开篇证据图)
复测命令:.\py.cmd tools\repro_preflight.py(只读自检,见 8.2)
截什么:四组检查(运行环境 / 配置与凭据 / 实例与 AWR / BIC-QA 三通道)与末尾"结论:具备完整复测条件"一行
存成:<>figures/shots/shot-01-check.png
插入位置:本段下方(即第 2 章 2.2 节末)
复测证据图 1 复测前自检:运行环境、配置凭据、实例与 AWR、BIC-QA 在线通道四项全 √(图为第二次复测实拍)
判读要点:
dmPython 可执行、AWR 可用、当前工作通道 = online三行决定后续步骤能否走满——步骤 5 的在线分析要求通道为online。
第二次复测实测读数(对照用):Python 3.13.14(受管解释器,dmsql登录路径已注入);SYSDBA@192.168.3.22:5236/DMDB;
BUILD_VERSION = 03151060506-…/STATUS$ = OPEN/PAGE_SIZE = 16384;SNAP_INTERVAL_SEC = 1800/RETENTION_DAYS = 8/TOPNSQL = 30;
快照历史 46 个;[√] 当前工作通道 online —— 官方 Open API 在线通道打通:session/create 鉴权通过 + chat SSE 返回有效回答;
末行结论:具备完整复测条件(含 BIC-QA 在线分析),可以开始按手册逐步执行。
配套的run.py check(usable=true)与自检输出的"实例画像"数字一致——两份只读检查互为旁证。
以下每一步都给出命令 → 真实回显 → 说明,并在关键处预留截图位。整套流程可被第 4 章的一条命令替代。
.\py.cmd run.py check
真实回显(节选):
{ "INSTANCE_NAME": "DMSERVER", "SVR_VERSION": "DM Database Server x64 V9",
"BUILD_VERSION": "03151060506-20260417-322930-20218", "STATUS$": "OPEN", "MODE$": "NORMAL" }
{ "page_size": 16384, "awr_enabled": true,
"control": { "SNAP_INTERVAL_SEC": 1800, "RETENTION_DAYS": 8, "TOPNSQL": 30, "STATUS_FLAG": 1 },
"snapshot_count": 20, "problems": [], "usable": true }
结论:AWR 可用,页大小 16384,基线快照 20 个。
核心代码(节选)|run.py:命令行入口与"环境体检"
脚本作用:
run.py是整套工具的唯一入口,负责"参数解析 → 调用引擎层 → 打印结构化结论"。
它本身不承载任何口径算法——页大小怎么读、AWR 可不可用,全部由引擎层dmlens/决定;
这样拆分的目的是让"命令怎么用"与"数字怎么算"互不污染,前者可改、后者可复算。
# run.py —— 环境体检命令(节选,保留原注释)
# 作用:把散落的检查项收敛为一条命令,输出结构化结论供报告与复测直接消费。
def cmd_check(args):
cfg = _cfg(args) # 加载 dmlens.ini;口令走 ${secret:...} 引用,不落明文
_banner(cfg)
with _conn(cfg) as c: # 上下文管理:无论成功失败都释放连接
vi = c.version_info() # 实例画像:版本 / 构建号 / STATUS$ / MODE$
print(json.dumps({"instance": {k: vi.get(k) for k in
("INSTANCE_NAME", "SVR_VERSION", "BUILD_VERSION",
"STATUS$", "MODE$")}},
ensure_ascii=False, indent=2))
info = awrmod.AwrRepository(c).check() # AWR 体检:页大小 / 开关 / 控制项 / 快照数
print(json.dumps(info, ensure_ascii=False, indent=2, default=str))
print("\n【结论】", "AWR 可用" if info["usable"] else "AWR 不可用",
";问题:" + (";".join(info["problems"]) if info["problems"] else "无"))
return 0
# run.py —— 子命令注册(节选)
# 作用:每个子命令只做「参数解析 + 调用引擎层」,一个子命令对应一个可复测动作。
sub = p.add_subparsers(dest="cmd")
s = sub.add_parser("check", help="AWR 环境体检"); s.set_defaults(func=cmd_check)
s = sub.add_parser("snapshot", help="手工创建快照"); s.set_defaults(func=cmd_snapshot)
s = sub.add_parser("diagnose", help="完整诊断")
s.add_argument("--begin", type=int); s.add_argument("--end", type=int)
s.set_defaults(func=cmd_diagnose)
# dmlens/dmdb.py —— 页大小与 AWR 开关(节选)
# 作用:把"达梦怎么问"这一层事实固定在引擎里,避免各处重复踩坑。
def page_size(self) -> Optional[int]:
# ★ 用系统函数取页大小;不要用 V$PARAMETER.NAME='PAGE_SIZE' 过滤(该列不是这样查的)
return int(self.scalar("SELECT SF_GET_PAGE_SIZE() FROM DUAL"))
def awr_enabled(self) -> bool:
# 0/1 表示 AWR 系统包是否已建立(CALL SP_INIT_AWR_SYS(1))
return str(self.scalar("SELECT SF_CHECK_AWR_SYS FROM DUAL")) == "1"
踩坑提示(口径类):页大小不能用
SELECT VALUE FROM V$PARAMETER WHERE NAME='PAGE_SIZE'查——那样会报[-2106] 无效的表或视图名。正确写法是取V$PARAMETER.PAGE列:SELECT DISTINCT PAGE FROM V$PARAMETER;
为什么要人为制造:故障不可控、不可复现,无法作为教学与验证素材。用受控事务构造争用,既能复现,又能随时收尾清零。构造原理应 1.2 节):
,灌入若干基线行(本实验实测为 8 行); *持锁会话**:UPDATE … WHERE ID = 1` 后不提交,从而持有该行锁;关键实现:等待方必须处理 -608 并重试
核心代码(节选)|scripts/lock_lab.py:锁模拟实验室
脚本作用:用"受控事务"构造可复现、可清零的锁争用(单行锁 / 热点行 / 三级级联 / 死锁环路四场景)。
它是本实验的问题注入器——真实故障不可控、不可复现,无法作为教学与验证素材;
而受控事务既能随时复现,又能在收尾阶段强制rollback + close把现场清零。
① 建表与基线行(节选)
SCHEMA, TABLE, ROWS = "DMLENS", "LOCK_T", 8 # 实验锁表与基线行数
def _setup(cfg) -> Dict[str, Any]:
"""建立实验锁表:主键 + 计数列 + 持锁者标记列,灌入基线行。"""
with dmdb.connect(cfg, label="locklab-setup") as c:
c.execute_ddl(f"CREATE TABLE {SCHEMA}.{TABLE} "
f"(ID INT PRIMARY KEY, N INT, HOLDER VARCHAR(32))")
cur = c.conn.cursor()
cur.executemany(f"INSERT INTO {SCHEMA}.{TABLE} (ID, N, HOLDER) VALUES (?, ?, ?)",
[(i, 0, "seed") for i in range(1, ROWS + 1)])
c.conn.commit()
② 等待方必须处理 -608 并重试(关键实现)
RETRY_MARKERS = ("-608", "请求执行超时", "-6403", "死锁", "timeout")
def _wait_update(sess, row: int, deadline: float) -> Dict[str, Any]:
"""发起一次会阻塞的 UPDATE,并在**请求超时 / 死锁**时重试直到 deadline。
★ 为什么要重试:达梦对长时间锁等待会返回 [CODE:-608] 请求执行超时。
若客户端不重试,等待方就会自行退出,观测窗口内"阻塞链凭空消失"——
既不利于实验复现,也会让"受控修复"无从触发。重试是真实客户端的正常行为,
此处如实记录 attempts 作为证据。
"""
attempts, acquired, last_error = 0, False, None
while time.time() < deadline:
attempts += 1
if sess.dml(f"UPDATE {SCHEMA}.{TABLE} SET N = N + 1, HOLDER = ? WHERE ID = ?",
[sess.label, row]):
acquired = True
break
last_error = sess.error
if sess.error and any(m in sess.error for m in RETRY_MARKERS):
sess.rollback(); sess.error = None; time.sleep(0.3)
continue # 超时 / 死锁 → 回滚后重试,维持等待状态
break # 其他错误:不掩盖,如实返回
return {"attempts": attempts, "acquired": acquired, "last_error": last_error}
③ 受控事务会话(节选)
class _Session:
"""受控事务会话:autoCommit=False,可精确控制"持锁多久"与"何时释放"。"""
def open(self):
# 关键:auto_commit=False。UPDATE 之后不提交,锁才会真实持有
self.conn = dmdb.connect(self.cfg, label=self.label, auto_commit=False)
self.sid = self.conn.self_session_id() # 记录自身 SESS_ID,便于对照观测
return self
def close(self):
self.rollback() # 收尾一定回滚,绝不把锁留在库里
self.conn.close()
执行命令(四类场景可任选,all 为全跑):
# 单行锁 / 热点行 / 三级级联 / 死锁环路
.\py.cmd run.py locklab --scenario hotspot --hold 60
# 或一次跑完四个场景
.\py.cmd run.py locklab --scenario all --hold 15
四类场景的实测观测(图 6):
图 6 锁模拟实验室四场景
| 场景 | 构造 | 实测观测 | 收尾 |
|---|---|---|---|
| 单行锁 fan-in | 1 持锁 + 1 等待 | 等待事务 1 / 阻塞链 1 / 链深 1 | 清零 |
| 热点行 | 1 根持锁 + 4 等待 | 根块 4 个 / 等待事务 7 / 阻塞锁 4 | 清零 |
| 三级级联阻塞链 | s0→s1→s2→s3 |
等待事务 3 / 阻塞链 1 / 链深 3;根 trx=28268 |
清零 |
| 死锁环路 | A、B 互等 | B→R1 返回 [CODE:-6403] 死锁;DEADLOCK_CYCLE 被记录 |
清零 |
第二次复测实测(
locklab --scenario all --hold 15,两轮):四场景全部ok=True 收尾清零=True;
热点行轮次实测"等待事务 7 / 阻塞链 1 / 被阻塞锁 4,根 trx 阻塞 8 个会话";死锁轮次实测
[CODE:-6403] 死锁(B→R1 侧)与[CODE:-608] 请求执行超时(A 侧)同时出现,
WRH$_DEADLOCK_HISTORY记录 3 条(第二轮累计 4 条)——即"检出即回滚 + 历史留痕"两条语义都被观测到。
每轮收尾后"等待事务 0 / 阻塞链 0 / 被阻塞锁 0",实验不留下任何残余锁。
📷 截图位 2|争用现场:阻塞链已经形成
复测命令:.\py.cmd run.py locklab --scenario hotspot --hold 60(或在"一键复测"中截步骤 2 阶段的回显)
截什么:观测[现场]段落——需能看清"根 trx / 根会话 SESS_ID / 阻塞 N 个会话 / 根 SQL"
存成:figures/shots/shot-02-contention.png
插入位置:本段下方(即步骤 2 四场景表之后)
复测证据图 2 争用现场:locklab 四场景连续运行(单行锁 / 热点行 / 三级级联),可见根事务、链上每个会话的 SESS_ID 与根 SQL(图为第二次复测拍)/p>
📷 截图位 3|动态视图旁证(可选,增强说服力)
复测命令:在争用持续期间另开一个disql会话,执行下方 SQL
截什么:V$TRXWAIT的等待关系明细 +WRH$_LOCK.BLOCKED <> 0行数
存成:figures/shots/shot-03-trxwait.png
插入位置:紧接上图之后
复测证据图 3 动态视图旁证:V$TRXWAIT 的等待关系(独立于本工具的第二条证据源)(图为第二次复测实拍:7 行等待关系,最长一条链 65658 → 65657 → 65656 → 65655 → 65649 前后跨越 5 个事务——级联阻塞链在动态视图里的原貌;WRH$_LOCK 中 BLOCKED <> 0 共 91 行)
判读要点(两个数字,两种口径):
①V$TRXWAIT的 7 行是实时等待关系——既有多对一的热点行等待(65657 / 65658等待65655 / 65656),也有跨事务的级联链(65658 → 65657 → 65656 → 65655),一个视图同时看到"热点 + 级联"两种形态;
②WRH$_LOCK的 91 行是快照侧留存(含历史快照的阻塞记录),远大于实时值——它只能证明"快照窗口内发生过阻塞",
不能当实时阻塞数用。这正是 5.4 节"计数噪声 vs 真实等待"的又一实例:实时判读用V$TRXWAIT,区间判读用等待事件差分。
-- 等待关系:谁在等谁
SELECT ID, WAIT_FOR_ID, WAIT_TIME, LOCK, WAIT_FOR_LOCK FROM V$TRXWAIT;
-- 被阻塞的锁
SELECT COUNT(*) AS BLOCKED_LOCKS FROM SYS.WRH$_LOCK WHERE BLOCKED <> 0;
方法:在争用开始前打"开始快照",在争用持续期间打"结束快照",两者组成负载区间。
.\py.cmd run.py snapshot # 开始快照 → 实测 SNAP_ID=21
# ……(此处争用正在持续)……
.\py.cmd run.py snapshot # 结束快照 → 实测 SNAP_ID=22
# 导出达梦原生 AWR 报告(HTML),便于与本文口径交叉核对
.\py.cmd run.py report --begin 21 --end 22
真实回显(节选):
[3] 16:44:18 采集 AWR —— 结束快照 SNAP_ID=22(区间 21->22)
[3] 16:44:18 固化证据 —— 阻塞链=1 条 锁汇总={"lock_rows": 110, "blocked_lock_rows": 4,
"trx_rows": 6, "waiting_trx": 4, "max_lock_cnt": 4, "lock_types": ["OBJECT", "TID"]}
达梦原生 AWR 报告(1825 行)→ output/e2e_20260914_164409/dm_awr_21_22.html
[report] 达梦原生 AWR 报告(区间 21→22,1825 行)→ output/dm_awr_21_22.html
区间信息(来自诊断报告):21 -> 22(2026-09-14 16:44:09.195986 → 2026-09-14 16:44:17.655459),区间跨时约 7.57 秒。
第二次复测实测:开始快照
SNAP_ID=49、结束快照SNAP_ID=50(两次独立的手工快照回显见下方证据图 4a);
report --begin 49 --end 50导出原生报告 1853 行(dm_awr_49_50.html)。区间49 → 50的
Snap 49 = 22:24:51.562900 → Snap 50 = 22:28:42.586140,跨时 231.13 秒(3.85 分钟)——
与第一次的 7.57 秒相比,这次是"长区间":两种跨时量级下,5.8 节的取端与精度规则同样成立(BIC-QA 在线分析按
END − END口径算出的 Elapsed 与快照表一致)。区间时长不影响结论结构,这正是"手工快照夹区间"的通用性。
核心代码(节选)|dmlens/awr.py:快照、报告与区间跨时
脚本作用:AWR 数据面。把"打快照 → 取报告 → 算区间"三件事收敛成可复算的原子操作,
并把本文第 5 章的口径陷阱(取端错误、CAST降精度)固化在实现里,而不是靠使用者记忆。
# dmlens/awr.py —— 快照与报告(节选,保留原注释)
def create_snapshot(self) -> int:
"""手工快照:把故障现场"夹"成一个区间的唯一手段。"""
self.conn.call("CALL DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT();")
return int(self.conn.scalar("SELECT MAX(SNAP_ID) FROM SYS.WRM$_SNAPSHOT"))
def report_lines(self, begin: int, end: int, fmt: str = "TEXT") -> List[str]:
"""取回 AWR 报告全文(跨主机安全)。
原理:AWR_REPORT_HTML/TEXT 是**服务端落盘**过程,客户端拿不到文件;
改用表函数 SELECT * FROM TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_HTML(b,e)),
让报告内容以结果集形式流回客户端(实测 1→2 区间 1834 行,本文区间 1825 行)。
"""
func = "AWR_REPORT_HTML" if fmt.upper().startswith("H") else "AWR_REPORT_TEXT"
cur = self.conn.conn.cursor()
try:
cur.execute(f"SELECT * FROM TABLE(DBMS_WORKLOAD_REPOSITORY.{func}(?, ?))", (begin, end))
return ["" if row[0] is None else str(row[0]) for row in cur.fetchall()]
finally:
cur.close()
# dmlens/awr.py —— 区间跨时(节选):第 5 章 5.8 节的实现级修法
def interval_span_seconds(self, begin: int, end: int) -> Optional[float]:
"""区间跨时(与达梦原生 AWR 报告一致:end 快照 END → begin 快照 END)。
★ 两处实测坑,缺一即错:
① 取端:原生报告 Snap Time 取的是 END_INTERVAL_TIME,Elapsed 亦按
END(end) − END(begin)。写成 END(end) − BEGIN(begin) 会把**起始快照的采集窗口**
一并算进来(实测虚增约 0.9s:8.46s vs 正确 7.57s)。
② 精度:CAST(x AS DATE) 在达梦会把 DATETIME 降精度到秒并四舍五入,
实测把 8.46s 变成 9.0s。必须用原生 DATETIME 相减。
"""
v = self.conn.scalar(
"SELECT ROUND(((SELECT END_INTERVAL_TIME FROM SYS.WRM$_SNAPSHOT WHERE SNAP_ID=?) "
"- (SELECT END_INTERVAL_TIME FROM SYS.WRM$_SNAPSHOT WHERE SNAP_ID=?)) * 86400, 2) "
"FROM DUAL", [end, begin])
return float(v) if v is not None else None
一句话记住:跨时用
END₂₂ − END₂₁(原生DATETIME相减 × 86400),不要先CAST成DATE,也不要用BEGIN端。
📷 截图位 4|AWR 快照列表与原生报告
复测命令:.\py.cmd run.py snapshots(快照列表)+.\py.cmd run.py report --begin <B> --end <E>(导出原生报告);<B>/<E>用当次回显里的两个SNAP_ID
截什么:① 快照列表(能看清区间终点的SNAP_ID与END_INTERVAL_TIME);② 浏览器打开dm_awr_<B>_<E>.html的首屏(实例信息 + Top 等待事件)
存成:figures/shots/shot-04-snapshots.png、figures/shots/shot-04-awr-report.png
插入位置:本段下方(两张图上下相邻)
复测证据图 4a 两次手工快照的回显(上:开始快照 SNAP_ID=49;下:结束快照 SNAP_ID=50——区间由这两个点"夹"出来)(图为第二次复测实拍,两次快照之间执行了四场景锁实验)
复测证据图 4b 达梦原生 AWR 报告首屏(2026-09-15 实拍,区间 70 → 71):可见 DB Name=DMDB / Instance=DMSERVER / Host=kb-db02、Elapsed = 0.12(mins)、DB Time = 0.03(mins)——Elapsed 即 5.8 节的跨时基准;Buffer Pool 段按子池逐行展开(NORMAL 池十余行、各自 Rat Hit %),即 5.1 节「必须按 NAME 去重」的现场。原生报告用于与本文口径交叉核对,不是唯一依据
.\py.cmd run.py diagnose --begin 21 --end 22
真实回显(节选):
快照区间 : 21 -> 22(2026-09-14 16:44:09.195986 → 2026-09-14 16:44:17.655459)
健康评估 : 88.98 / 100(良好)
发现数量 : 7
知识图谱通道: offline(在线通道不可用(HTTP 401 OPEN_API_KEY_INVALID),如实降级为内置知识图谱)
知识条目回流: 5 条
[critical] DML-R13 存在 1 条阻塞链,最长链阻塞 8 个会话(根会话 SESS_ID=140359434824088)
[high ] DML-R03 并发争用:锁等待 0 ms,闩锁 0 ms,互斥体 0 ms,当前等待会话 7 个,阻塞链 1 条
[high ] DML-R12 当前存在 7 个等待事务
[medium ] DML-R02 区间等待 TOP:trxid lock wait(21.712s / 0 次,类=Transaction);已排除 1 类空闲等待
[medium ] DML-R05 解析开销偏高:解析耗时 811 ms 占区间 DB Time 1460 ms 的 55.5%;每执行硬解析 0.70 次
[low ] DML-R04 历史存在死锁痕迹(非本区间,已消退):区间增量 0,去重后历史 1 条(结转副本已剔除)
[info ] DML-R15 监控类参数未成组对齐:MONITOR_SQL_EXEC 未开启(SQL 执行明细监控缺失)
八维健康评分明细(本次实验):
图 8 本次实验八维健康评分(总分 88.98 / 100)与发现分级
| 维度 | 得分 | 权重 | 说明 |
|---|---|---|---|
| 可用性 | 100.0 | 0.15 | 正常 |
| 性能 | 100.0 | 0.20 | 区间跨时 7.57s 过短(< 30s),惰性计数器不具可比性,本项不计分(Δ=18.061ms 仅供参考) |
| SQL | 71.56 | 0.15 | 解析耗时占 DB Time 55.5%;计数口径已登记(二者非包含关系,未参与扣分) |
| 锁 | 60.0 | 0.15 | 当前阻塞链 1 条;等待事务 4 个 |
| 容量 | 100.0 | 0.10 | 正常 |
| 主机IO | 100.0 | 0.10 | 正常 |
| 归档备份 | 100.0 | 0.10 | 正常 |
| 配置 | 85.0 | 0.05 | MONITOR_SQL_EXEC=0 |
设计要点:缺项按权重归一,不用缺失数据制造误判;惰性计数器(
CPU time)与采集噪声(transaction lock waits)不参与扣分。注意"性能"维度虽显示 100.0,但实际不计分——这是"不知道就不说"的诚实做法,详见第 5 章 5.5 节。
核心代码(节选)|区间口径:累计值 → 区间增量(dmlens/awr.py)
脚本作用:达梦 AWR 各
WRH$_*表存的是累计值(随快照单调递增)。
这一段把"累计"翻译成"本区间负载",是所有诊断数字得以成立的地基——
不做差,就会把"实例启动至今"当成"刚才那 8 秒"。
# dmlens/awr.py —— 区间差分(节选)
def interval_sysstat(self, begin, end, names=None) -> Dict[str, Dict[str, int]]:
"""区间口径指标:{name: {raw_begin, raw_end, delta}}。
注意:达梦 AWR 存累计值,`delta` 才是本区间真实负载。
"""
names = names or INTERVAL_METRICS
b = self.sysstat_raw(begin, names)
e = self.sysstat_raw(end, names)
return {n: {"raw_begin": b.get(n, 0), "raw_end": e.get(n, 0),
"delta": max(e.get(n, 0) - b.get(n, 0), 0)} # 单向做差,负值归零
for n in set(list(b.keys()) + list(e.keys()))}
核心代码(节选)|口径风险登记表(dmlens/awr.py)
脚本作用:把"看起来能用、实际不能用"的指标显式登记,让规则引擎按登记结果降级处理,
而不是散落在各个if里凭经验判断。登记表本身就是第 5 章八处陷阱的机器可读版本。
# dmlens/awr.py —— 口径风险登记表(节选)
# verdict 语义:
# constant_artifact 增量与区间时长无关(同一常量),实测为快照自身开销
# lazy_counter 惰性计数器:短区间可能完全不推进,不可用于占比
# non_subset 与同名族指标非包含关系,禁止相除
# gauge 瞬时量,禁止做差
# high_water_mark 高水位:只有"增量"才代表本期是否发生;历史表记录会被结转,须去重
SUSPECT_METRICS = {
M_LOCK_WAITS: "constant_artifact", # transaction lock waits
M_TRX_LOCK_WAIT_MS: "zero_baseline",
M_CPU_US: "lazy_counter", # CPU time(us)
M_SQL_EXEC_CNT: "lazy_counter",
M_HARD_PARSE_CNT: "non_subset", # hard parse count 与 parse count 非包含
M_HARD_PARSE_TIME_MS: "non_subset",
M_DEADLOCK_CNT: "high_water_mark", # transaction deadlock count
M_MEM_POOL_USED_BYTES: "gauge",
}
📷 截图位 5|诊断报告(Markdown / HTML)
复测命令:.\py.cmd run.py diagnose --begin <B> --end <E>(想快跑加--no-knowledge)
截什么:诊断报告首屏——"结论速览 + 发现清单表"与八维健康表
存成:figures/shots/shot-05-diagnosis.png
插入位置:本段下方(即步骤 4 八维健康表之后)
复测证据图 5 诊断报告首屏(2026-09-15 实拍,区间 68 → 69):健康评估 94.82/100(优秀)、发现 6 条(medium 3 / low 2 / info 1)、知识图谱通道 online、耗时 333.46 s——与同区间一键受控执行(证据图 11a,健康 88.79、发现 8 条)对照:同一区间、不同采样时刻,实时类发现的分级会变(R03 此刻已消退为 low),但口径方法完全一致
▶ 上篇到此为止。 下一篇(中篇)将把这份 AWR 报告交给 BIC-QA 在线分析,并走完“定位根因 → 受控解决 → 复测验证 → 审计收尾”的自动闭环;阻塞链 1→0、判定
healed的关键证据将在中篇揭晓。
文章
阅读量
获赞
