📚 本文为系列连载(中篇) 原稿《 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 实例的真实复测。
batchId 问答awr_bicqa_analysis.md)(接续上篇) 上篇已完成“环境体检 → 人为制造锁争用 → 采集 AWR → 规则诊断”,并产出一份达梦原生 AWR 报告。本篇把它交给 BIC-QA 在线分析,走完“定位根因 → 受控解决 → 复测验证 → 审计收尾”的自动闭环。
这一步是全文的主线目标:把步骤 3 采集到的达梦原生 AWR 报告,通过 BIC-QA 官方 Open API 在线交给达梦知识库分析,拿到"成因 + 优化建议"。
本节所有回显均为 2026-09-14 17:23–17:26 一次成功调用的真实留痕(output/awranalyze_20260914_172340/)。
batchId 问答如 1.4 节 所述,官方 Open API 没有独立的 AWR 分析端点,正确姿势是三步组合。
① 建会话(顺带完成鉴权自检)
POST /open-api/v1/session/create
Authorization: Bearer <API_KEY>
{"title": "DMLens AWR 分析"}
{"status":"success","data":{"conversationId":"63cf6f5a-2c6d-492e-bf11-551c3b72f4ce",
"chatTitle":"DMLens AWR 分析"},"message":"会话创建成功"}
② 上传 AWR 报告(multipart)——原生 AWR HTML 约 290 KB,一次上传即可
POST /open-api/v1/chat/attachments # Content-Type: multipart/form-data
batchId = 6ab03448-d866-4ea7-86cd-67dc746714b2
file = dm_awr_21_22.html
{"status":"success","data":{
"batchId":"6ab03448-d866-4ea7-86cd-67dc746714b2",
"attachmentId":"a79a901d-ec57-4db2-bdda-47d3f4e3eb17",
"name":"dm_awr_21_22.html","size":294225,"mime":"text/html","type":"text",
"status":"READY","expiresAt":"2026-09-15T17:23:40.548"},
"message":"附件上传成功"}
③ 携 batchId 提问,SSE 流式返回分析
POST /open-api/v1/chat # Accept: text/event-stream
{"question":"请只依据报告内容,给出…(见 5.4 的提问模板)",
"conversationId":"63cf6f5a-…","batchId":"6ab03448-…"}
本次调用的 SSE 统计:{'meta': 45, 'delta': 14900, 'file': 1, 'done': 1},totalTokens = 2,701,275,耗时 174.3 秒 —— 14 900 个 delta 帧逐字流式返回,最终通道判定 online。
⚠️ 实测最硬的教训(务必保留):第 ② 步
status=READY只证明"通道可用",不证明"分析可用"。同一 Key 在配额耗尽时,上传依旧返回HTTP 200 / READY;真正的阻塞发生在第 ③ 步——它以HTTP 200+ 正常 SSE 帧序返回错误帧:data:{"type":"error","content":"Token余额不足,请先充值","done":true, "errorCode":"OPEN_API_TOKEN_INSUFFICIENT"} data:{"type":"done","done":true}若只看状态码,就会把"余额不足"当成 BIC-QA 的分析结论写进报告——这正是本工程设"伪成功识别"的原因。校验的是载荷内容,不是 HTTP 状态。
核心代码(节选)|dmlens/bicqa.py:伪成功识别(本工程最关键的一段)
脚本作用:判断在线返回究竟是不是"错误文案"。它只有一个函数,却决定了报告里
"在线分析结论"与"错误提示"会不会被混为一谈——这是可信度的分界线。
# dmlens/bicqa.py —— 伪成功识别(节选,保留原注释)
ERROR_MARKERS = (
"余额不足", "请先充值", "配额", "quota", "insufficient",
"unauthorized", "invalid api key", "鉴权失败", "认证失败",
"无权限", "权限不足", "rate limit", "请求过于频繁", "too many requests",
"服务异常", "系统繁忙", "internal error",
)
def looks_like_error(text: str, errors=None) -> Optional[str]:
"""判断在线返回是否为错误文案;是则返回命中的标记,否则 None。"""
if errors:
return "SSE error 事件:" + str(errors[0])[:120]
t = (text or "").strip()
if not t:
return None
# 仅在前 200 字内匹配,避免把正常回答里顺带提到的"权限"等词误判
head = t.lower()[:200]
for m in ERROR_MARKERS:
if m in head:
return f"命中错误标记「{m}」:{t[:120]}"
if len(t) <= 40 and any(k in t.lower() for k in ("error", "fail", "exception")):
return f"短文本疑似错误:{t[:80]}"
return None
核心代码(节选)|上传附件与 SSE 流式问答
脚本作用:把官方文档里"AWR / 巡检报告分析"的两步组合落地——
先multipart上传报告拿到batchId,再携batchId提问,让模型"看着报告"回答。
全程只依赖 Python 标准库(urllib),零第三方依赖,便于在客户内网复现。
# dmlens/bicqa.py —— 上传附件(节选):POST /open-api/v1/chat/attachments
def upload_attachment(self, batch_id: str, filename: str, content: bytes) -> Dict[str, Any]:
"""契约:multipart/form-data,字段 batchId + file。
实测(2026-09-14):上传**恒为 HTTP 200 且 status=READY**,即使账户配额已耗尽——
因此"上传成功"不能作为"分析可用"的证据,必须看随后 chat 的返回。"""
boundary = "----DMLens" + uuid.uuid4().hex
body = (f"--{boundary}\r\n"
'Content-Disposition: form-data; name="batchId"\r\n\r\n'
f"{batch_id}\r\n"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
"Content-Type: text/html\r\n\r\n").encode()
body += content + f"\r\n--{boundary}--\r\n".encode()
req = urllib.request.Request(self.base_url + API_PREFIX + "/chat/attachments",
data=body, method="POST")
req.add_header("Authorization", "Bearer " + self.api_key)
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
...
# dmlens/bicqa.py —— SSE 流式问答(节选):POST /open-api/v1/chat
req.add_header("Accept", "text/event-stream") # ★ 缺了它会被网关按 application/json 处理(实测 406)
payload = {"question": question, "conversationId": conversation_id}
if batch_id:
payload["batchId"] = batch_id # ★ 带上附件批次,模型才会"看着报告"作答
while "\n\n" in buf: # SSE:按空行拆帧
frame, buf = buf.split("\n\n", 1)
for line in frame.splitlines():
if not line.startswith("data:"):
continue
evt = json.loads(line[5:].strip()) # data: 之后是完整 JSON
d = evt.get("delta")
if isinstance(d, dict): # delta.content 可能是嵌套对象,
_absorb("content", d.get("content")) # 也可能是裸字符串 —— 两种都要兼容
_absorb("thinking", d.get("thinking"))
if str(evt.get("type")) == "error": # 错误帧以正常帧序返回
errors.append(str(evt.get("content") or evt.get("errorCode")))
if evt.get("totalTokens") is not None:
tokens = evt.get("totalTokens")
# 最终判定:正文非空且未命中错误标记,才允许标 online
err = looks_like_error(text, errors)
return {"text": text[:max_chars], "tokens": tokens, "events": counts,
"errors": errors, "error_kind": err, "ok": err is None and bool(text)}
以上三步已被封装,一条命令跑完(核心代码见上文两段;脚本职责与索引见附录 D.1):
# 从 DM9 取回原生 AWR 报告 → 上传 → 请求分析 → 落盘(JSON + Markdown)
.\py.cmd run.py awr-analyze --begin 21 --end 22
# 也可直接指定既有报告文件;或用 --question 定制提问
.\py.cmd run.py awr-analyze --begin 21 --end 22 --file output/e2e_20260914_164409/dm_awr_21_22.html
真实回显(本次成功调用,节选):
DMLens AWR → BIC-QA 在线分析 区间 21 → 22
输出目录:output/awranalyze_20260914_172340
BIC-QA :https://api.bic-qa.com 知识库 dataset_code=2103
[0] 从 DM9 取回原生 AWR 报告
达梦原生 AWR 报告(1825 行)→ …/dm_awr_21_22.html
[1] 建会话 POST /open-api/v1/session/create
HTTP 200 conversationId = 8…(回显从略)
[2] 上传 AWR 报告 POST /open-api/v1/chat/attachments
HTTP 200 batchId = 6ab03448-d866-4ea7-86cd-67dc746714b2
attachmentId = a79a901d-ec57-4db2-bdda-47d3f4e3eb17
name/size = dm_awr_21_22.html / 294225 字节
status = READY expiresAt = 2026-09-15T17:23:40.548
[3] 携 batchId 请求分析 POST /open-api/v1/chat (SSE)
SSE 事件计数 = {'meta': 45, 'delta': 14900, 'file': 1, 'done': 1}
tokens = 2701275 耗时 174.3s
√ 在线分析成功(正文非错误载荷)
[4] 留痕
JSON :output/awranalyze_20260914_172340/awr_bicqa_analysis.json
MD :output/awranalyze_20260914_172340/awr_bicqa_analysis.md
第二次复测又跑了两轮(区间
49 → 50,同一条命令):
①awr-analyze --begin 49 --end 50(不带提问,输出目录awranalyze_20260914_223401):报告 1853 行、
上传 296,133 字节 →batchId = 9035c9e3-…,SSE{'meta': 19, 'delta': 11780, 'file': 1, 'done': 1},
tokens = 99,053 / 耗时 124.8 s;
②awr-analyze --begin 49 --end 50 --question "重点看锁与解析开销"(输出目录awranalyze_20260914_223705):
上传 296,213 字节 →batchId = 323dfc2f-…,SSE{'meta': 31, 'delta': 13141, 'file': 1, 'done': 1},
tokens = 1,609,954 / 耗时 150.9 s,正文首屏与 HTML 分析报告链接见下方证据图 7 / 7b。
两轮都是√ 在线分析成功(正文非错误载荷)——同一天、同一命令、只差一个--question,
消耗与产出都随提问而变;证据图 6 / 6b 即第 ① 轮的真实回显。
一个容易漏掉的产物:SSE 里出现了 1 个
file事件 —— BIC-QA 除了返回分析正文,还生成了一份 HTML 分析报告(在content中以 Markdown 链接给出)。要把它取回本地,须调POST /open-api/v1/files/download,且file参数不能直接用file事件里的原始路径,而要用正则扫会话详情得到的值。
awr_bicqa_analysis.md)以下为 BIC-QA 返回的分析原文(为版面计仅作极少量删减,未改任何数据):
一、本区间最突出的性能现象
区间口径:Snap 21 = 2026-09-14 16:44:10.086806 → Snap 22 = 16:44:17.655459,Elapsed 0.12 分钟(约 7.57 秒),DB Time 0.03 分钟(1,460 ms),AAS ≈ 0.25。数据库 DMDB / 实例 DMSERVER / DM V9 / Multi-machine Type = LOCAL(单机,非 DSC)。
按区间差分(System Statistics 的 Max − Min = Total)解读:
现象 区间差分值 占比 DB time(ms) 1,460 100% hard parse time(ms) 887 60.75% of DB time parse time(ms) 811 55.55% CPU time(us) 18,061 us(≈18 ms) 仅 1.23% hard parse count / parse count 978 / 686 — sql executed count 1,400 — transaction 总数 / commit / rollback 246 / 215 / 0 — transaction deadlock count 0(Max 1 − Min 1) — redo log size 14,557,166 B(≈14.56 MB) — Top 等待事件:
network idle wait56 次 / 12.766711 s(占 DB time 100%,但属空闲等待,非网络故障);rlog flush wait43 次 / 0.0065 s;dbfile extend1 次 / 0.0010 s。主机:CPU_IDLE 99.36% → 94.72%,Load 1/5/15 = 0 / 0.01 / 0.05,FREE_PHY 7,734.84 MB。主机完全未饱和。
⚠️ 口径冲突(须以差分口径为准):Load Profile 的
DB CPU(s) = 2.59/秒与 Time ModelCPU time = 18 ms相互矛盾;Redo size = 0与 redo 差分 14.56 MB 矛盾;hard parse count(978) > parse count(686)亦不自洽。故本分析统一以区间差分为准。
二、可能的根因链路(按可能性排序)
- 链 1(probable)|计划复用不足 → 硬解析主导 DB Time:依据 hard parse time 887 ms 占 60.75%、Soft Parse % = 0.00、Execute to Parse % = 51、Library Hit % = 81.95、Library Cache 淘汰 16 次 / 34.6 MB、
% plan with executions>1仅 17.59%。反证:CPU time 仅 18 ms(1.23%),不是 CPU 瓶颈。- 链 2(suspicion)|DDL / 对象失效 → 字典与计划缓存失效:Dictionary Cache
DDL Discards区间 +556,DDL in pl/sql count差分 236。反证:字典缓存 Used % 仅 1.16,容量极空闲。- 链 3(suspicion)|短连接 / 会话抖动:Logons 9.3/秒、Logoff 6/秒;session alloc 14 / free 9;来源
python.exe(192.168.3.46,Windows 11)——即本文的压测客户端。- 链 4(区间外·confirmed)|历史死锁:2026-09-14 14:59:56,TRX 28295,SQL
UPDATE DMLENS.LOCK_T SET N = N + 1, HOLDER='B→R1' WHERE ID = ?—— 该时间早于区间起点,且 deadlock 计数差分为 0 → 本区间无死锁,仅作实例级风险跟踪。- 已排除:主机 CPU/内存/IO 饱和、REDO/提交同步、UNDO 空间压力、排序落盘、区间内锁阻塞、网络故障(
network idle wait为空闲等待)、DSC(单机不适用)。总体判定:低风险窗口,唯一局部矛盾是解析开销占比过高。
三、可执行优化建议
参数级(当前值取自
dm.ini;本窗口证据不足,不建议直接改值):
参数 当前值 建议 USE_PLN_POOL 1(已启用计划重用) 保持不变;仅在确认字面量 SQL 主导硬解析后再评估 2(常量参数化) CACHE_POOL_SIZE 200 MB 暂不调整(重启生效) DICT_BUF_SIZE 128 不调整,Used % 仅 1.16 MEMORY_TARGET 1000 不调整,内存池使用率仅 40.84% BIND_PARAM_OPT_FLAG / BATCH_PARAM_OPT 3 / 0 先做应用侧绑定变量核查,暂不调整 SQL 级:定位高硬解析语句(只读)——
SELECT SQL_TXT, PARSE_CNT, HARD_PARSE_CNT, PARSE_TIME, HARD_PARSE_TIME, EXEC_TIME FROM V$SQL_STAT ORDER BY HARD_PARSE_CNT DESC, HARD_PARSE_TIME DESC;统一使用绑定变量 / 参数化查询;避免在 PL/SQL 中频繁执行 DDL(区间 236 次),DDL 集中到维护窗口。
应用级:客户端改用连接池 / 长连接;按业务高峰采集 15~30 分钟窗口再评估。
四、需进一步采集的数据
完整 AWR SQL 榜单(本报告 SQL 排名章节为 0,是最主要证据缺口)、
Session_state_Statistics、区间内 DDL 审计、V$DEADLOCK_HISTORY全量、应用侧 SQL 与连接池配置、更长窗口 AWR。
结论(原文):本区间为低负载短窗口,总体低风险;核心矛盾是硬解析占 DB Time 60.75%,机制指向计划复用不足(叠加 DDL/短连接传播),但受限于 SQL 榜单缺失与窗口代表性不足,结论为 probable 而非 confirmed。当前不建议直接修改任何参数,应先补齐长窗口采集与 SQL 榜单。
这段分析里,有三点值得单独说:
hard parse count > parse count、Redo size = 0 与 redo 差分矛盾)——与本文第 5 章由规则引擎独立抓到的陷阱互相印证。"达梦 AWR 里有若干指标不能直接相除/直接取用"是客观事实,不是单一实现的臆断。network idle wait 标注为"属空闲等待,非网络故障"),与 5.6 节的规则处理一致。probable 而非 confirmed,点出"SQL 榜单缺失"这一证据缺口,还明确劝阻直接改参数。这种"知道自己站在什么证据水平上"的回答,正是运维场景最需要的。提问方式直接影响回答质量。本工程默认使用结构化提问,要求模型"只依据报告内容 + 引用具体数字 + 分层给建议":
这是一份达梦数据库 DM9 的原生 AWR 报告(自动工作负载仓库)。
请只依据报告内容,输出结构化分析:
1) 本区间最突出的性能现象(引用报告中的具体数字,如 DB Time、Top 等待事件、
Load Profile 中的每秒指标);
2) 可能的根因链路(按可能性排序,并说明判断依据);
3) 可执行的优化建议(分参数级 / SQL 级 / 应用级三层,给出具体参数名或 SQL 形态);
4) 需要进一步采集哪些数据才能确认。
注意:报告中的部分统计量是累计值,请以区间差分后的口径解读。
为什么要加最后一句:达梦 AWR 报告里的部分统计量是累计值(详见第 5 章)。若不提示,模型可能把"累计"当作"本区间"来解读——这属于口径级错误,比数字错更隐蔽。本次分析中模型主动写了"按区间差分(System Statistics 的 Max − Min = Total)解读",说明这句提示生效了。
| 状态 | 触发条件 | 判定依据 | 报告标注 |
|---|---|---|---|
online |
鉴权 200 且 正文非错误载荷 | session/create 通过 + chat SSE 返回有效回答 |
采用在线分析结论 |
degraded |
鉴权 200,但 chat 返回错误载荷 |
looks_like_error() 命中 OPEN_API_TOKEN_INSUFFICIENT 等 |
熔断 + 退回内置图谱 |
offline |
session/create 返回 401 |
OPEN_API_KEY_INVALID |
熔断 + 退回内置图谱 |
同一实例、同一区间,Key 处于三种状态时的实测对照:
| 时间 | Key 状态 | session/create |
chat |
判定 |
|---|---|---|---|---|
| 14:44 | 有效但配额耗尽 | 200 | 200 + 错误帧"Token余额不足" | degraded |
| 16:44 | 已失效/被轮换 | 401 | 未调用 | offline |
| 17:23 | 有效(已换新 Key) | 200 | 200 + 14 900 个 delta 帧 |
online |
关键工程认知:配额是账户级、不是 Key 级。新建或更换 Key 不会带来新额度——只要
session/create返回 200,就说明 Key 本身没问题,此时chat被拒必然是账号余额问题,换 Key、关知识库、换模型都无解,只能充值。这条判别规则能省下大量排障时间。
通道探测命令(可单独验证三态):
.\py.cmd run.py bicqa
本文第 4 章的"发现清单"来自规则引擎(可复算、口径固定、不依赖网络);本节的正文来自 BIC-QA 在线知识库(成因解释与建议、会引用报告原文)。两者不是替代关系:
本次两者在关键结论上一致(解析开销偏高、无区间内锁阻塞、无死锁),并且都指向同一个证据缺口(SQL 榜单为空)。这正是"规则 + 知识"双通道的价值:一个保证数字可信,一个保证解释可用。
📷 截图位 6|把 AWR 报告交给 BIC-QA(成功调用)
复测命令:.\py.cmd run.py awr-analyze --begin <B> --end <E>(<B>/<E>用当次回显里的两个SNAP_ID)
截什么:完整回显的四段——“建会话 / 上传附件 / 请求分析 / 留痕”,以及末尾的√ 在线分析成功
存成:figures/shots/shot-06-awr-bicqa.png
插入位置:本段下方(步骤 5 末尾,即 5.6 节结束时)

复测证据图 6 AWR → BIC-QA 在线分析调用链([0] 取报告 1853 行 → [1] 建会话 → [2] 上传 296,133 字节拿 batchId → [3] 请求分析;需看清 attachmentId、status=READY)(图为第二次复测实拍,区间 49 → 50)
📷 截图位 6b|BIC-QA 在线通道返回(成功回执)
复测命令:.\py.cmd run.py awr-analyze --begin <B> --end <E>(回显的[3]段)
截什么:SSE 事件计数一行与√ 在线分析成功(正文非错误载荷)
存成:figures/shots/shot-06b-bicqa.png
插入位置:紧接上图之后

复测证据图 6b 在线通道成功回执:SSE 事件计数 = {'meta': 19, 'delta': 11780, 'file': 1, 'done': 1},tokens = 99,053 / 耗时 124.8 s(图为第二次复测实拍——载荷非错误才标 online,这是 5.5 节伪成功识别的落地:状态码与帧序都正常,不看正文就会误判成功)
📷 截图位 7|在线分析正文首屏
复测命令:打开output/awranalyze_<时间戳>/awr_bicqa_analysis.md
截什么:正文首屏,含"重点结论"段落(引用报告中的具体数字:等待事件次数 / 秒数 / 占比)
存成:figures/shots/shot-07-bicqa-online.png
插入位置:紧接上图之后

复测证据图 7 在线分析正文(--question "重点看锁与解析开销" 定向提问):模型直接引用了报告中的具体数字——trxid lock wait 10 次等待 / 164.10 秒 / 单次平均 16.41 秒 / 占 DB Time 99.12%、死锁实例累计 4 次其中 1 次落在本区间、transaction lock waits 区间内增 400(1.73/秒),并识别出解析计数器"打架"(hard parse count 997 > parse count 774)——证明模型确实"读懂了"这份 AWR(图为第二次复测实拍,tokens = 1,609,954 / 耗时 150.9 s)
📷 截图位 7b|BIC-QA 生成的 HTML 分析报告(可选)
复测命令:用POST /open-api/v1/files/download取回file事件对应的 HTML 报告后打开;或直接看正文末尾的"分析报告下载"链接与[4] 留痕段
存成:figures/shots/shot-07b-bicqa-report.png
插入位置:紧接上图之后

复测证据图 7b 在线分析正文收尾:本区间最突出现象(trxid lock wait 占 DB Time 99.00%)、可能根因链路排序("确认与加锁之间隔了一小段时间、但并发更新同一行"为 1 号候选)、[4] 留痕 JSON/MD 路径(图为第二次复测实拍)
命令(查看会话 / 锁 / 阻塞链):
.\py.cmd run.py sessions
本次实测的根因(来自诊断报告 R13 的关键指标):
| 项 | 值 |
|---|---|
| 阻塞链数量 | 1 |
| 根事务 | root_trx = 56100 |
| 根会话 | SESS_ID = 140359434824088 |
| 根用户 | SYSDBA |
| 根语句 | UPDATE DMLENS.LOCK_T SET N = N + 1, HOLDER = ? WHERE ID = 1 |
| 链上阻塞会话数 | 8(waiter_count = 8) |
定位用的核心 SQL(可直接执行):
-- ① 等待关系:谁在等谁(还原阻塞链)
SELECT ID, WAIT_FOR_ID, WAIT_TIME, LOCK, WAIT_FOR_LOCK FROM V$TRXWAIT;
-- ② 当前会话与状态
SELECT SESS_ID, USER_NAME, STATE, RUN_STATUS, CLNT_IP, SQL_TEXT FROM V$SESSIONS WHERE STATE <> 'IDLE';
-- ③ 被阻塞的锁(AWR 快照侧)
SELECT COUNT(*) AS BLOCKED_LOCKS FROM SYS.WRH$_LOCK WHERE BLOCKED <> 0;
核心代码(节选)|dmlens/sessions.py:阻塞链还原
脚本作用:
V$TRXWAIT只给出一堆"谁在等谁"的边,不是链。这段代码把边还原成树:
以"被等待、但自己不等待任何人"的事务为根,沿WAIT_FOR_ID递归下钻,
得到"根持锁会话 → 下游等待者"的层级关系(本文实测:根 1 个、级联阻塞 8 个会话)。
根节点找对了,处置才不会打错人。
# dmlens/sessions.py —— 阻塞链还原(节选,保留原注释)
def blocking_chains(conn) -> List[Dict[str, Any]]:
"""还原阻塞链:以"无人等待的持锁事务"为根,向下展开等待者。"""
waits = trx_waits(conn) # SELECT ID, WAIT_FOR_ID, WAIT_TIME, LOCK ... FROM V$TRXWAIT
if not waits:
return []
trx_map = {int(t["ID"]): t for t in transactions(conn)}
sess_map = {int(s["SESS_ID"]): s for s in iconn)}
wait_of = {int(w["ID"]): w for w in waits}
holders = {int(w["WAIT_FOR_ID"]) for w in waits}
def walk(holder: int) -> List[Dict[str, Any]]:
out = []
for w in waits:
if int(w["WAIT_FOR_ID"]) == holder: # 谁在等 holder
out.append({"waiter_trx": int(w["ID"]), "holder_trx": holder,
"wait_time": w.get("WAIT_TIME"),
"waiter_session": sess_of(int(w["ID"])),
"holder_session": sess_of(holder)})
out.extend(walk(int(w["ID"]))) # 继续往下钻(支持多级级联)
return out
# ★ 根 = 出现在 WAIT_FOR_ID 里(被等待)、但自己不在等待集合中(不等待任何人)
roots = [h for h in holders if h not in wait_of]
chains = []
for r in roots:
edges = walk(r)
if edges:
chains.append({"root_trx": r, "root_session": sess_of(r), "chain": edges,
"waiter_count": len(edges), "max_depth": max(len(edges), 1)})
chains.sort(key=lambda c: c["waiter_count"], reverse=True) # 阻塞面最大的链排在最前
return chains
# dmlens/sessions.py —— 会话级终止与身份复核(节选)
# 作用:达梦提供 SP_CLOSE_SESSION 终止会话;★ 本构建**没有**语句级取消,
# 故闭环只能到"会话级",不做超出能力的承诺。
def close_session(conn, sess_id: int) -> bool:
cur = conn.conn.cursor()
try:
cur.execute("CALL SP_CLOSE_SESSION(?)", (int(sess_id),))
finally:
cur.close()
return True
def session_exists(conn, sess_id, sess_seq=None, user=None):
"""复核目标会话身份,用于防误杀。返回当前会话画像(不存在则 None)。
比对 SESS_SEQ 与 USER_NAME:会话 ID 会被复用,只认 ID 有误杀风险。
"""
rows = conn.query(f"SELECT {SESS_COLS} FROM V$SESSIONS WHERE SESS_ID=?", [int(sess_id)])
if not rows:
return None
cur = rows[0]
if sess_seq is not None and int(cur.get("SESS_SEQ") or -1) != int(sess_seq):
return None
if user is not None and str(cur.get("USER_NAME") or "").upper() != str(user).upper():
return None
return cur
📷 截图位 8|阻塞链定位
复测命令:.\py.cmd run.py sessions
截什么:输出里的"阻塞链"段落(或用附录 B 的证据 SQL ① 查出的等待关系明细)
存成:figures/shots/shot-08-blocking-chain.png
插入位置:本段下方(步骤 6 末)

复测证据图 8 阻塞链定位——"链"段逐行给出根事务/根会话与每个等待者的 SESS_ID、等待时长、双方 SQL(图为第二次复测实拍:热点行轮次根事务 trx-65470(SESS_ID=1403594348143560)阻塞 8 个会话,级联轮次链 trx-66240 → trx-66242 → … 多级展开——根找对了,处置才不会打错人)
命令(默认干跑;加 --execute 才真实处置):
# 干跑:只检测与规划,不动实例
.\py.cmd run.py auto
# 受控执行:需双闸门同时满足
.\py.cmd run.py auto --execute
本次实测的处置过程(真实回显):
[5] 受控修复-规划 :: 计划 1 个动作,放行 1,拦截 0
[5] 受控修复-执行 :: 目标 SESS_ID=140359434824088 结果=ok :: SP_CLOSE_SESSION(140359434824088) 已执行
核心代码(节选)|dmlens/autoheal.py:门禁检查(六道防线的前五道)
脚本作用:在真正动手之前,把"谁授权、能动谁、动几个"逐条算清。
它的输出不是布尔值,而是两份清单:放行动作与拦截记录——
被拦截的每一个动作都带明确原因并同样入审计,不允许静默丢弃。
这是"自动化能上生产"与"自动化是隐患"的分水岭。
# dmlens/autoheal.py —— 门禁检查(节选;为版面计,拦截原因字符串做了压缩)
ACTION_WHITELIST = {"close_session"} # 动作白名单:只允许达梦 SP_CLOSE_SESSION
def preflight(self, actions, cli_gate) -> Tuple[List[Action], List[Dict[str, Any]]]:
"""返回 (放行动作, 拦截记录)。"""
allowed, blocked = [], []
# ① 双闸门:配置 [safety].auto_execute=true 且 CLI --execute,缺一不执行
gate_cfg = bool(self.cfg.safety.auto_execute)
if not (gate_cfg and cli_gate):
return [], [{"action": a.to_dict(),
"block_reason": "双闸门未同时满足(需 auto_execute=true 且 CLI --execute)"}
for a in actions]
# ② 全局开关:allow_terminate=false 时一律不动
if not self.cfg.safety.allow_terminate:
return [], [{"action": a.to_dict(),
"block_reason": "allow_terminate=false(配置禁止终止会话)"} for a in actions]
deny = {u.upper() for u in (self.cfg.safety.deny_users or [])}
taken = 0
for a in actions:
if a.target_user and a.target_user.upper() in deny: # ③ 保护名单 deny_users
blocked.append({"action": a.to_dict(),
"block_reason": f"目标用户 {a.target_user} 在保护名单中"}); continue
if int(a.target_sess_id) == int(self.self_sess): # ④ 自身排除:永不动自己
blocked.append({"action": a.to_dict(),
"block_reason": "目标为本工具自身会话(自身排除)"}); continue
if a.kind not in ACTION_WHITELIST: # ⑤ 白名单
blocked.append({"action": a.to_dict(),
"block_reason": f"动作 {a.kind} 不在白名单中"}); continue
if taken >= int(self.cfg.safety.max_terminate_per_round): # ⑥ 单轮上限
blocked.append({"action": a.to_dict(),
"block_reason": "超出单轮上限 max_terminate_per_round"}); continue
# ⑦ 身份复核:执行前后各做一次,缩小竞态窗口,防 SESS_ID 复用误杀
cur = sessmod.session_exists(self.conn, a.target_sess_id,
sess_seq=a.target_sess_seq, user=a.target_user)
if cur is None:
blocked.append({"action": a.to_dict(),
"block_reason": "身份复核不通过(会话已消失或 SESS_SEQ/USER 已变)"}); continue
a.detail["identity_recheck"] = {"SESS_ID": cur.get("SESS_ID"),
"SESS_SEQ": cur.get("SESS_SEQ"),
"USER_NAME": cur.get("USER_NAME")}
allowed.append(a)
taken += 1
self._audit("autoheal.preflight", allowed=len(allowed), blocked=len(blocked))
return allowed, blocked
⚠️ 安全声明(务必如实交代):仓库提交的配置保持安全默认(
auto_execute = false、deny_users = SYSDBA)。本次演示中"放行 1"是通过环境变量临时放开双闸门实现的,演示完毕即恢复默认;生产环境请保留保护名单。
📷 截图位|受控解决的"规划 + 执行"两行(并入截图位 11,不单独出图)
复测命令:.\py.cmd run.py auto --execute(需临时放开双闸门,放开与恢复方法见 8.3)
截什么:[5] 受控修复-规划 :: 计划 1 个动作,放行 1,拦截 0与[5] 受控修复-执行 :: 目标 SESS_ID=… 结果=ok
插入位置:这两行已包含在一键复测的完整回显中,随截图位 11 一起插入(第 4 章末)
业务侧根治建议(工具只能治标,治本在应用):
复测的核心问题:阻塞链是否消失?消失是否可归因于本工具?
真实回显:
[6] 复测验证 :: 阻塞链 1 -> 0,等待会话 8 -> 0,判定 = healed(可归因于本工具)
[6] 旁路复核 :: 独立查询 V$TRXWAIT 行数 = 0
[8] 16:44:23 复测验证 —— 阻塞链 1->0 等待会话 8->0 独立旁证:4/4 等待事务成功获锁
上述"旁证"是独立于工具自证的:根会话被终止后其行锁确被释放,因此 4 个等待事务全部成功获锁——这比工具自己说"修好了"更有说服力。
"8 → 0"与"4/4"的口径差异:
V$TRXWAIT的 8 行是等待边(含等待方在-608超时重试过程中先后建立的等待事务);“4/4"按本轮注入的 4 个等待线程计——只回答"被注入的等待方是否都拿到了锁”。两个口径各答一个问题,前者衡量"现场是否清空",后者衡量"处置是否真实生效"。
三种模式的对照(图 5)——尤其关注"不冒领功劳"那一行:
图 5 同一条真实阻塞链 · 三模式对照
| 模式 | 命令 | 复测结果 | 判定 | 归因本工具 |
|---|---|---|---|---|
| 干跑(默认) | .\py.cmd run.py demo |
1 → 1 | no_action_chain_persists |
否 |
| 链自行消失 | .\py.cmd run.py demo --race |
1 → 0 | no_action_chain_self_cleared |
否(不冒领) |
| 受控执行 | .\py.cmd run.py demo --execute |
1 → 0 | healed |
是 |
| 现场已干净(第二次复测实拍) | .\py.cmd run.py auto --execute |
0 → 0(无链可修) | no_action_nothing_to_do |
否(不冒领) |
中间一行是本方案的关键差异:阻塞链从 1 → 0,看起来"修好了",但工具老老实实报告"未执行任何动作,链自行消失,不归因于本工具"。
第四行不是演示脚本,而是生产路径的现场证据:第二次复测在四场景锁实验结束后运行
auto --execute
(双闸门已临时放开:DMLENS_SAFETY_AUTO_EXECUTE=1+DMLENS_SAFETY_DENY_USERS=NONE),此时实验事务已收尾、
阻塞链为 0,工具如实输出"执行动作 0 个 → 判定 = no_action_nothing_to_do(因本工具未动作)… 不可归因于本工具",
并照常完成审计(发现 4 条,短路阻断 1 条)。归因红线不是"某个特殊分支",而是每次执行都要过的一道闸
——证据见下方复测证据图 9b。
核心代码(节选)|dmlens/autoheal.py:复测判定与归因约束
脚本作用:判定"问题是否真的解决了",并且回答一个更尖锐的问题——“算谁的功劳”。
归因红线只有一条:这一轮没执行任何处置动作,阻塞链的任何变化都不得归因于本工具。
否则会把"等待方超时退出(-608)“这类自发现象误报成"修复成功”——
这正是自动化工具最容易犯、也最伤可信性的错误。
# dmlens/autoheal.py —— 复测判定(节选,保留原注释)
def verify(self, before, after, executed: int = 0) -> Dict[str, Any]:
"""由阻塞链数据判定 healed / partial / unhealed —— 拒绝"自称成功"。
★ 归因约束(重要):若本轮**未执行任何处置动作**(干跑 / 全被拦截),
则阻塞链的任何变化都**不可归因于本工具**。此时不得输出 healed,
只能输出 no_action_*,否则会把"链自行消失"误报成"修复成功"。
"""
b, a_ = len(before), len(after)
b_waiters = sum(int(c.get("waiter_count") or 0) for c in before)
a_waiters = sum(int(c.get("waiter_count") or 0) for c in after)
if executed <= 0: # 没动手 → 一律不归因
if b > 0 and a_ == 0:
judgement = "no_action_chain_self_cleared"
note = ("本轮未执行任何处置动作,阻塞链已自行消失(常见原因:等待方请求超时 -608 / "
"持锁方主动提交回滚);该变化**不可归因于本工具**")
elif b == 0:
judgement = "no_action_nothing_to_do"
note = "本轮未发现阻塞链,也无需处置"
else:
judgement = "no_action_chain_persists"
note = "本轮未执行任何处置动作,阻塞链仍存在"
return {"before_chains": b, "after_chains": a_,
"before_waiters": b_waiters, "after_waiters": a_waiters,
"executed": executed, "judgement": judgement,
"attributed_to_tool": False, "note": note}
# 动手了:才有资格谈归因,但判定仍以**复测数据**为准
if a_ == 0:
judgement = "healed"
elif a_ < b or a_waiters < b_waiters:
judgement = "partial"
else:
judgement = "unhealed"
return {"before_chains": b, "after_chains": a_, "judgement": judgement,
"attributed_to_tool": True,
"note": "本轮执行处置动作 %d 个,判定基于阻塞链复测数据" % executed}
📷 截图位 9|复测对比与归因(本文最有说服力的一张)
复测命令:受控执行.\py.cmd run.py e2e --execute(或.\py.cmd run.py demo --execute);对照图用.\py.cmd run.py demo --race
截什么:①[6] 复测验证 :: 阻塞链 1 -> 0 … 判定 = healed(可归因于本工具)与[8] … 独立旁证:4/4 等待事务成功获锁;② 对照图:争用结束后auto --execute输出no_action_nothing_to_do(不可归因)——第二次复测已按 ② 落图(见下)
存成:figures/shots/shot-09-verify.png、figures/shots/shot-09-race.png
插入位置:本段下方(两张并列,先verify后race)
复测证据图 9a 受控执行下的复测判定(2026-09-15 实拍,区间 70 → 71):[5] 受控修复-执行 :: 目标 SESS_ID=140359790075568 结果=ok、[6] 复测验证 :: 阻塞链 1 -> 0 … 判定 = healed(可归因于本工具)、[7] 执行动作=1 判定=healed 归因本工具=是、[8] 独立旁证:4/4 等待事务成功获锁、[9] 哈希链 ok=True 已校验记录=110——处置、判定、旁证、审计四层证据同框
复测证据图 9b 归因红线的现场证据:第二次复测在争用结束后以受控模式运行 auto --execute(双闸门已放开,见顶部两行 $env: 赋值),工具检测到阻塞链 0 条,如实输出 [5] 执行动作 0 个 → 判定 = no_action_nothing_to_do(因本工具未动作)、不可归因于本工具,并照常留痕审计——"不是我修的"与"我修好了"同样值得写进报告
.\py.cmd run.py audit --verify # 校验哈希链完整性
.\py.cmd run.py sessions # 确认实例已干净
真实回显:
[9] 16:44:23 审计校验 —— 哈希链 ok=True 已校验记录=56
[9] 16:44:23 收尾清零 —— 残余阻塞链 0 条 (清理语句:DROP TABLE DMLENS.LOCK_T;)
收尾核对:阻塞链 0 条 / 被阻塞锁 0 / 等待事务 0;实验锁表可删除(DROP TABLE DMLENS.LOCK_T;)。
核心代码(节选)|dmlens/audit.py:哈希链审计
脚本作用:让"做过什么"不可被悄悄改写。每条记录携带前一条记录的哈希,
形成单向链:任何一处内容改动都会让后续所有哈希不匹配,verify()一次即可检出。
对破坏性动作而言,"可追溯"不是锦上添花,而是能否被批准上生产的前提。
# dmlens/audit.py —— 哈希链(节选,保留原注释)
GENESIS = "0" * 64
def _hash(prev: str, payload: Dict[str, Any]) -> str:
blob = prev + json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str)
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
def append(self, action: str, **fields) -> Dict[str, Any]:
"""只追加:记录写入即定稿,不提供任何"修改/删除"接口。"""
prev = self._last_hash()
payload = {"action": action, "ts": time.strftime("%Y-%m-%d %H:%M:%S"), **fields}
rec = dict(payload)
rec["prev"] = prev
rec["hash"] = _hash(prev, payload)
with open(self.path, "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
return rec
def verify(self) -> Dict[str, Any]:
"""逐条重算哈希;任一处不符即报出断链位置与原因。"""
prev, n = GENESIS, 0
for rec in self.read():
payload = {k: v for k, v in rec.items() if k not in ("prev", "hash")}
if rec.get("prev") != prev:
return {"ok": False, "broken_at": n + 1, "reason": "prev 链接不连续", "checked": n}
if rec.get("hash") != _hash(prev, payload):
return {"ok": False, "broken_at": n + 1,
"reason": "内容哈希不匹配(疑似被篡改)", "checked": n}
prev = rec["hash"]
n += 1
return {"ok": True, "checked": n, "head": prev}
📷 截图位 10|审计与收尾
复测命令:.\py.cmd run.py audit --verify,随后.\py.cmd run.py sessions
截什么:审计输出(含"ok": true与校验条数),以及复核的阻塞链:0 条
存成:figures/shots/shot-10-audit.png
插入位置:本段下方(步骤 9 末)
复测证据图 10 审计哈希链校验:{"ok": true, "checked": 94, "head": "0d4a34a9…82dd6"}(图为第二次复测实拍;第一次受控执行实测 ok=true / 已校验 56 条——两次运行的审计记录条数不同属预期,ok=true 才是判据)
图 4 自治闭环七阶段与六道防线
| 阶段 | 时间 | 详情 |
|---|---|---|
| 1 监控发现 | 16:44:20 | 阻塞链 1 条 / 等待事务 7 个 / 等待中 4 个 / 锁行 41 |
| 2 证据固化 | 16:44:20 | AWR 快照区间 21 → 22;AWR 可用 = True,页大小 = 16384 |
| 3 规则诊断 | 16:44:20 | 发现 7 条 {critical: 1, high: 2, medium: 2, low: 1, info: 1} |
| 4 知识印证 | 16:44:20 | 通道 = offline(鉴权失效被识别,熔断退回内置图谱) |
| 5 受控修复 | 16:44:20 | 计划 1 个动作,放行 1,拦截 0;SP_CLOSE_SESSION(...) → ok |
| 6 复测验证 | 16:44:23 | 阻塞链 1 → 0,等待会话 8 → 0,判定 = healed |
| 7 知识回流 | 16:44:23 | 回流 5 条;另存达梦原生 AWR 报告 |
上述 9 个步骤已被封装为一条命令,避免人工"掐秒表"导致的时序不可复现:
# ① 干跑:完整走一遍"制造问题→采集 AWR→诊断→定位→复测",但不动实例
.\py.cmd run.py e2e
# ② 受控执行:在双闸门放行时真实处置根锁会话
.\py.cmd run.py e2e --execute --waiters 4 --hold 60
本次运行的完整回显(真实,节选):
====================================================================================
DMLens 端到端实验编排 等待者=4 保持=60s 模式=受控执行(真实处置)
闸门:auto_execute=True allow_terminate=True deny_users=[] 单轮上限=3
输出目录:output/e2e_20260914_164409
====================================================================================
[0] 16:44:09 环境体检 —— AWR 可用=True 页大小=16384 快照数=20
[1] 16:44:11 开始快照 —— SNAP_ID=21(区间起点)
锁表准备:schema 新建=True 基线行=8
[2] 16:44:15 制造问题 —— 热点行争用已形成:根 trx=56100 会话=140359434824088 阻塞 8 个会话
[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 行)→ .../dm_awr_21_22.html
[4] 16:44:20 规则诊断 —— 健康 88.98/100(良好) 发现 7 条
[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 执行明细监控缺失)
[5] 16:44:20 知识印证 —— 通道=offline 在线通道不可用(HTTP 401 :: {"status":"error","data":null,"message":"未登录或登录已过期","code":"OPEN_API_KEY_INVALID"}),如实降级为内置知识图谱
[6] 16:44:20 定位根因 —— 根持锁会话 SESS_ID=140359434824088 用户=SYSDBA
根 SQL:UPDATE DMLENS.LOCK_T SET N = N + 1, HOLDER = ? WHERE ID = 1
[5] 受控修复-规划 :: 计划 1 个动作,放行 1,拦截 0
[5] 受控修复-执行 :: 目标 SESS_ID=140359434824088 结果=ok :: SP_CLOSE_SESSION(140359434824088) 已执行
[6] 复测验证 :: 阻塞链 1 -> 0,等待会话 8 -> 0,判定 = healed(可归因于本工具)
[6] 旁路复核 :: 独立查询 V$TRXWAIT 行数 = 0
[7] 知识回流 :: 条目 5 条 -> .../dmlens_knowledge_export.json
[7] 16:44:23 受控解决 —— 执行动作=1 判定=healed 归因本工具=是
[8] 16:44:23 复测验证 —— 阻塞链 1->0 等待会话 8->0 独立旁证:4/4 等待事务成功获锁
[9] 16:44:23 审计校验 —— 哈希链 ok=True 已校验记录=56
[9] 16:44:23 收尾清零 —— 残余阻塞链 0 条 (清理语句:DROP TABLE DMLENS.LOCK_T;)
====================================================================================
[完成] 全过程留痕 → output/e2e_20260914_164409/e2e_story.json
====================================================================================
口径注(回显里三个"等待数"为什么不一样):
DML-R13的"阻塞 8 个会话"按V$TRXWAIT等待边计(等待事务 → 被等待事务的每一条关系算一条边);DML-R03的"当前等待会话 7 个"与DML-R12的"7 个等待事务"是诊断采样时刻的实时值——与造压阶段相隔数秒,期间已有等待方经历了一轮-608超时重试(超时后旧等待事务退出、重试建立新等待事务),所以瞬时值在 7~8 之间波动;锁汇总里的waiting_trx: 4则按V$TRX.WAITING口径计(见 5.4 节)。三个数字口径不同、时刻不同,都真实——正因如此,本文的判定从不依赖单一计数,而是用"阻塞链还原 + 等待时间 + 复测前后对比"三条互相独立的证据(这也是 5.4 节"计数噪声"的现场版本)。
留痕产物(output/e2e_20260914_164409/):
| 文件 | 内容 |
|---|---|
e2e_story.json |
全过程结构化留痕(时间线 / 争用详情 / 诊断 / 闭环 / 审计) |
DMLens_diagnosis_*.md / .html |
诊断报告(含每条证据 SQL) |
dm_awr_21_22.html |
达梦原生 AWR 报告(1825 行,交叉核对与在线分析用) |
pipeline_*/dmlens_knowledge_export.json |
知识回流条目 |
另有
output/awranalyze_*/:AWR → BIC-QA 在线分析的留痕(awr_bicqa_alysis.on/.md与导出报告),对应步骤 5。
📷 截图位 11|一键复测全流程(建议截两张)
复测命令:.\py.cmd run.py e2e --execute --waiters 4 --hold 60(放开双闸门的方法见 8.3)
截什么:① 前半段——“环境体检 → 开始快照 → 制造问题 → 采集 AWR → 固化证据 → 规则诊断”;② 后半段——“定位根因 → 受控修复(规划/执行)→ 复测验证 → 审计校验 → 收尾清零”
存成:figures/shots/shot-11-e2e-a.png、figures/shots/shot-11-e2e-b.png
插入位置:本段下方(即第 4 章"一键复测"末尾,两张上下相邻)
复测证据图 11a 一键复测(前半段,2026-09-15 实拍,区间 70 → 71):环境体检(AWR 可用=True 页大小=16384)→ 开始快照 70 → 热点行争用形成(根 trx=72604,阻塞 8 个会话)→ 结束快照 71 → 原生报告 1858 行 → 规则诊断(健康 88.79/100,发现 8 条,其中 DML-R07 缓冲池命中率 94.69% 与 5.1 节口径互证)→ 知识通道 offline 如实降级
复测证据图 11b 一键复测(后半段,2026-09-15 实拍):定位根因(根持锁会话 SESS_ID=140359790075568)→ 闭环流水线(放行 1,拦截 0)→ SP_CLOSE_SESSION(...75568) 已执行 → [6] 复测验证 :: 阻塞链 1 -> 0 … healed → 旁路复核 V$TRXWAIT 行数 = 0 → 知识回流 6 条 → [9] 收尾清零:残余阻塞链 0 条
📷 截图位 12|留痕目录
复测命令:查看output/e2e_<时间戳>/(文件管理器或ls -la)
截什么:本目录内文件列表(e2e_story.json/ 诊断报告.md+.html/dm_awr_<B>_<E>.html/pipeline_*/)
存成:figures/shots/shot-12-artifacts.png
插入位置:紧接上图之后
复测证据图 12 一次运行留下的全部证据文件(2026-09-15 实拍,output/e2e_20260915_054616/):e2e_story.json(全过程结构化留痕)、诊断报告 .md + .html、dm_awr_70_71.html(原生 AWR 报告 1858 行)、pipeline_20260915_054626/(闭环流水线留痕,含知识回流 JSON)
▶ 中篇到此为止。 下一篇(下篇)将逐条拆解 AWR 的八处口径陷阱,并给出实验数据总表、独立复测取证单,以及全部证据图的索引与插入位置。
文章
阅读量
获赞
