注册
【#达梦数据库 #达梦同行者征文】《 DMLens_让 DM9 的 AWR 说真话:八处口径陷阱、BIC-QA 在线印证与一条命令跑完的锁争用闭环(上)》
专栏/技术分享/ 文章详情 /

【#达梦数据库 #达梦同行者征文】《 DMLens_让 DM9 的 AWR 说真话:八处口径陷阱、BIC-QA 在线印证与一条命令跑完的锁争用闭环(上)》

尚雷 2026/09/18 374 0 0
摘要

《 DMLens_让 DM9 的 AWR 说真话:八处口径陷阱、BIC-QA 在线印证与一条命令跑完的锁争用闭环(上)》

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 实例的真实复测。

本篇目录

  • 写在前面:本文要让你照着做一遍
  • 第 1 章 理论基础
    • 1.0 总体架构:AWR 是证据,BIC-QA 是知识,闭环是手脚
    • 1.1 达梦 AWR 的观测模型:快照、累计值与区间差分
    • 1.2 锁与等待的基本原理:阻塞链是怎么形成的
    • 1.3 三种口径:区间 / 累计 / 高水位
    • 1.4 从"证据"到"知识":为什么需要 BIC-QA 印证
    • 1.5 自治闭环的安全模型:六道防线与一条归因红线
  • 第 2 章 实验环境与准备
    • 2.1 环境清单
    • 2.2 准备步骤
  • 第 3 章 实操步骤(核心)
    • 步骤 1 环境体检
    • 步骤 2 人为制造性能问题(热点行锁争用)
    • 步骤 3 采集 AWR(快照对 + 原生报告)
    • 步骤 4 规则诊断(区间口径 + 八维健康)

写在前面:本文要让你照着做一遍

本文不是"读懂了就好"的原理综述,而是一次可完整复测的实验记录。笔者在一台 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.0 总体架构:AWR 是证据,BIC-QA 是知识,闭环是手脚

在展开原理之前,先给出全局视图。本方案分五层,每层职责单一、可独立复算(图 1)。

fig1_architecture.png

图 1 总体架构:达梦 AWR 采集 × BIC-QA 知识印证 × 受控闭环

层 职责 关键设计
数据源层 读取 DM9 的 AWR 快照与动态视图 只读;WRH$_* 走快照对,实时状态走 V$*
口径层 把"原始计数"变成"可信指标" 区间差分、单位换算、计数器语义登记表
规则诊断层 规则引擎 + 八维健康评分 每条发现自带证据 SQL 与口径声明
知识印证层 BIC-QA 官方 Open API + 内置图谱兜底 三通道;识别错误载荷 → 降级 + 熔断
受控闭环层 双闸门 → 白名单 → 身份复核 → 执行 → 复测 → 回流 全程哈希链审计

这五层合起来,正是 DM9“DB+AI 融合”在运维侧的一次具体落地:
数据库负责把数据采准、存住(AWR 快照体系与动态视图),AI 负责把现象解释成结论(BIC-QA 在线分析 + 内置图谱),
而中间那层口径与最后那层受控闭环,是让 AI 结论可以被采信、可以被执行的关键——
少了口径层,AI 会基于失真数字给出看似专业的错误结论;少了闭环层,AI 的建议永远停留在“建议”。
本文的全部工作,就是把这两层补齐,并用实测证明它们有效。

1.1 达梦 AWR 的观测模型:快照、累计值与区间差分

达梦的 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 节)。

1.2 锁与等待的基本原理:阻塞链是怎么形成的

一个典型的阻塞链是这样长出来的:

事务 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)。这一点在闭环设计里必须如实说明,不能做虚假承诺。

1.3 三种口径:区间 / 累计 / 高水位

口径 含义 典型指标 禁止操作
interval 区间增量 等待时间、解析耗时与次数 不可直接取值
cumulative 实例级累计 缓冲池命中率 RAT_HIT 不可做差(累计比率无意义)
high_water_mark 高水位(只增不减且结转) transaction deadlock count 不可当作"本区间发生了"

一句话记忆:引用任何 AWR 指标前,先问三句——这是区间还是累计?单位是页还是字节?会不会结转?

1.4 从"证据"到"知识":为什么需要 BIC-QA 印证

规则引擎能回答"发生了什么"(锁等待 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(走内置图谱)

fig3_knowledge_verify.png

图 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() 一个函数里,可单测、可审计。

1.5 自治闭环的安全模型:六道防线与一条归因红线

自动化处置必须"经得起审问"。本方案设六道防线(见图 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。


第 2 章 实验环境与准备

2.1 环境清单

项目 实测值
实例名 DMSERVER
版本 DM Database Server x64 V9
构建号 03151060506-20260417-322930-20218
状态 / 模式 OPEN / NORMAL
页大小 16384(16 KB)
AWR 快照间隔 1800 秒
AWR 保留天数 8 天
TOPNSQL 30

2.2 准备步骤

① 部署 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 节末)

shot01check.png

复测证据图 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)与自检输出的"实例画像"数字一致——两份只读检查互为旁证。


第 3 章 实操步骤(核心)

以下每一步都给出命令 → 真实回显 → 说明,并在关键处预留截图位。整套流程可被第 4 章的一条命令替代。

步骤 1 环境体检

.\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;

步骤 2 人为制造性能问题(热点行锁争用)

为什么要人为制造:故障不可控、不可复现,无法作为教学与验证素材。用受控事务构造争用,既能复现,又能随时收尾清零。构造原理应 1.2 节):

  1. 建一张锁表DMLENS.LOCK_T,灌入若干基线行(本实验实测为 8 行); *持锁会话**:UPDATE … WHERE ID = 1` 后不提交,从而持有该行锁;
  2. 等待会话:多线程各自申请同一行 → 全部进入等待,形成以持锁会话为根节点的阻塞链;
  3. 等待方必须带重试(见下方"关键实现")。

关键实现:等待方必须处理 -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):

fig6_lock_scenarios.png

图 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 四场景表之后)

shot02contention.png

复测证据图 2 争用现场:locklab 四场景连续运行(单行锁 / 热点行 / 三级级联),可见根事务、链上每个会话的 SESS_ID 与根 SQL(图为第二次复测拍)/p>

📷 截图位 3|动态视图旁证(可选,增强说服力)
复测命令:在争用持续期间另开一个 disql 会话,执行下方 SQL
截什么:V$TRXWAIT 的等待关系明细 + WRH$_LOCK.BLOCKED &lt;> 0 行数
存成:figures/shots/shot-03-trxwait.png
插入位置:紧接上图之后

shot03trxwait.png

复测证据图 3 动态视图旁证:V$TRXWAIT 的等待关系(独立于本工具的第二条证据源)(图为第二次复测实拍:7 行等待关系,最长一条链 65658 → 65657 → 65656 → 65655 → 65649 前后跨越 5 个事务——级联阻塞链在动态视图里的原貌;WRH$_LOCK 中 BLOCKED &lt;> 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 &lt;> 0;

步骤 3 采集 AWR(快照对 + 原生报告)

方法:在争用开始前打"开始快照",在争用持续期间打"结束快照",两者组成负载区间。

.\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 &lt;B> --end &lt;E>(导出原生报告);&lt;B>/&lt;E> 用当次回显里的两个 SNAP_ID
截什么:① 快照列表(能看清区间终点的 SNAP_ID 与 END_INTERVAL_TIME);② 浏览器打开 dm_awr_&lt;B>_&lt;E>.html 的首屏(实例信息 + Top 等待事件)
存成:figures/shots/shot-04-snapshots.png、figures/shots/shot-04-awr-report.png
插入位置:本段下方(两张图上下相邻)

shot04snapshots.png

复测证据图 4a 两次手工快照的回显(上:开始快照 SNAP_ID=49;下:结束快照 SNAP_ID=50——区间由这两个点"夹"出来)(图为第二次复测实拍,两次快照之间执行了四场景锁实验)

shot04awrreport.png

复测证据图 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 去重」的现场。原生报告用于与本文口径交叉核对,不是唯一依据

步骤 4 规则诊断(区间口径 + 八维健康)

.\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 执行明细监控缺失)

八维健康评分明细(本次实验):

fig8_health_dimensions.png

图 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 &lt;B> --end &lt;E>(想快跑加 --no-knowledge)
截什么:诊断报告首屏——"结论速览 + 发现清单表"与八维健康表
存成:figures/shots/shot-05-diagnosis.png
插入位置:本段下方(即步骤 4 八维健康表之后)

shot05diagnosis.png

复测证据图 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 的关键证据将在中篇揭晓。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服