1. 先搞清楚 cursor pin s wait on x 到底卡在哪
cursor: pin S wait on X这个等待事件,第一次看到的人很容易被名字唬住。拆开看其实不复杂:S 是共享(Shared),X 是排他(Exclusive),一个会话想以共享方式去 pin 住某个游标,结果发现另一个会话正拿着这个游标的排他锁不放,于是只能排队等。它本质是一个 mutex 层面的争用,不是 IO,也不是锁(enqueue),所以你在v$lock里翻半天往往什么都找不到。
我见过太多人一上来就盯着这个等待事件本身调参数,结果方向全错。要记住一句话:cursor: pin S wait on X是症状,不是根因。真正要回答的问题是——谁在持有 X 锁,它为什么持有那么久。绝大多数情况下,持有者正在做硬解析(hard parse),或者在做游标失效后的重新编译。硬解析本身要拿 library cache 上的 mutex,解析越慢,别人等得越久,等待就像滚雪球一样堆起来。
那为什么硬解析会频繁发生?常见几类:绑定变量没用好导致大量不可共享的游标;high version count让一个 SQL 生成成百上千个子游标;统计信息或 DDL 导致游标失效;还有少数是版本 bug。这些原因在 AWR 和 ADDM 里其实都有痕迹,只是需要你知道去哪一栏看。
这篇是系列第一篇,只讲首次排查路径:怎么用 AWR 快照对比锁定异常时段,怎么用 ADDM 报告拿到 Oracle 自己的判断,再用 system state dump 把 holder 和 waiter 进程抓出来。全程给可复制的脚本和命令,你照着敲就行。适合已经能登数据库、会看基本等待事件的 DBA 和运维同学。如果你手上正好有一套采集端点,后面我也会说怎么把诊断数据统一收口,避免每次排查都在几台机器之间来回拷 trace 文件。
先明确排查顺序,别乱:第一步确认现象和时段,第二步 AWR 对比找异常,第三步 ADDM 看建议,第四步 dump 抓现场,第五步定位 blocker 和它的 SQL。顺序反了,你会在 dump 文件里迷路。
2. 排查前的准备:AWR、ADDM 与统一采集端点
动手之前先把工具链理清楚。AWR 是 Oracle 自带的历史性能仓库,默认每小时一个快照,保留期看你的配置。ADDM 是建立在 AWR 之上的自动诊断,它会在每个快照间隔跑一次,直接告诉你"这段时间数据库认为最大的问题是啥"。这两个是官方给的、不用额外装的东西,排查cursor: pin S wait on X一定要先用它们,而不是一上来就 dump。
你需要确认几件事。第一,当前用户有DBA角色或者至少能执行awrrpt.sql、addmrpt.sql。第二,知道$ORACLE_HOME在哪,因为脚本都在$ORACLE_HOME/rdbms/admin/下。第三,确认 AWR 快照间隔和保留策略,如果间隔太长(比如 1 小时),短时间的争用高峰可能被平均掉,这时候要结合v$active_session_history做细粒度看。
-- 查看当前 AWR 配置 SELECT snap_interval, retention FROM dba_hist_wr_control; -- 查看最近的快照,确定异常时段对应的 snap_id SELECT snap_id, begin_interval_time, end_interval_time FROM dba_hist_snapshot ORDER BY snap_id DESC FETCH FIRST 20 ROWS ONLY;关于采集端点,这里说一个实际痛点。排查这类问题经常要跨多套库、多个时间点收集 AWR 文本、ADDM 文本、trace 文件,散落在不同机器上,事后复盘很难对齐。我的做法是把这些诊断产物的采集和归档统一走一个通道,用 TaoToken 的 API 端点做集中管理,把每次排查的 AWR/ADDM 报告、dump 摘要按事件归档,后面写复盘或者做基线对比时直接调。它的接入地址是https://taotoken.net/api,控制台在https://taotoken.net/console,API Key 在https://taotoken.net/api-keys生成。注意这里只是把"诊断数据的采集与归档"这件事收口,数据库本身的 AWR 还是 Oracle 原生的,两者不冲突。
如果你只是想先把这次排查做完,可以跳过归档,直接用原生脚本。但如果你是要做长期性能治理,建议一开始就把端点配好,省得后面补。配置方式在下一节给。
3. 可复制配置:AWR/ADDM 脚本与采集端点设置
这一节全是能直接抄的东西。先做 AWR 报告,交互式脚本会问你 snap 范围,正常时段和异常时段各出一份,用来做基线对比。
-- 交互式生成 AWR 报告(文本格式) SQL> @$ORACLE_HOME/rdbms/admin/awrrpt.sql -- 提示选择 report_type: text -- 提示输入天数: 1 -- 提示输入 begin_snap 和 end_snap: 选异常时段如果你不想交互,可以用awrrpti.sql指定实例,或者直接查dba_hist视图自己算。下面这段是我常用的,直接定位异常时段里cursor: pin S wait on X的等待占比和 top SQL:
-- 异常时段内该等待事件的总体情况 SELECT event, total_waits, time_waited_micro/1000000 AS wait_sec, average_wait_micro/1000 AS avg_ms FROM dba_hist_system_event WHERE event = 'cursor: pin S wait on X' AND snap_id BETWEEN &begin_snap AND &end_snap ORDER BY snap_id; -- 该时段 top SQL(按 elapsed time) SELECT sql_id, executions, elapsed_time/1000000 AS elapsed_sec, parse_calls, version_count FROM dba_hist_sqlstat WHERE snap_id BETWEEN &begin_snap AND &end_snap ORDER BY elapsed_time DESC FETCH FIRST 20 ROWS ONLY;ADDM 报告同样用官方脚本,它会直接给出"硬解析过多""library cache 争用"这类结论:
SQL> @$ORACLE_HOME/rdbms/admin/addmrpt.sql -- 选择异常时段的 begin_snap / end_snap接下来是采集端点的配置。TaoToken 的接入用标准 API Key 方式,把诊断产物的归档请求指向https://taotoken.net/api。下面是一个 JSON 配置片段,路径按你实际的归档脚本目录放,比如/opt/diag/collector/config.json:
{ "endpoint": "https://taotoken.net/api", "api_key": "sk-你的key", "model_id": "diagnostic-archive", "base_url": "https://taotoken.net/api", "archive": { "awr_dir": "/opt/diag/awr", "addm_dir": "/opt/diag/addm", "trace_dir": "/opt/diag/trace", "tag": "cursor_pin_s_wait_on_x" } }三件套要写全:Base URL 是https://taotoken.net/api,Key 从https://taotoken.net/api-keys拿,Model ID 按你归档任务的定义填。如果你用的是 Cline 这类带 MCP 的客户端,配置里同样要保证这三项齐全,缺一个就会报local proxy failed或者 401。Codex 用户如果走auth.json,也要把 base_url 和 key 对齐,别只填一半。
配好之后,每次排查产出的 AWR/ADDM 文本、dump 摘要都往这个端点推,tag 统一用cursor_pin_s_wait_on_x,后面按 tag 检索就能把同一类问题的历史现场全捞出来。这一步不是必须,但做过一次你就知道多省事。
4. 验证请求与成功结果:dump 抓现场并定位 blocker
AWR 和 ADDM 给的是"面",system state dump 给的是"点"。当 AWR 没抓到异常 SQL,或者你需要精确知道谁持有 X 锁时,就得 dump。先看非 RAC 环境:
-- 非 RAC,连续三次 dump,间隔 90 秒,观察变化 SQL> oradebug setmypid SQL> oradebug unlimit SQL> oradebug dump systemstate 266 -- 等待 90 秒 SQL> oradebug dump systemstate 266 -- 等待 90 秒 SQL> oradebug dump systemstate 266 SQL> oradebug tracefile_name SQL> quitRAC 环境用 hanganalyze 加 systemstate,注意-g all是对所有实例:
$ sqlplus '/ as sysdba' SQL> oradebug setmypid SQL> oradebug unlimit SQL> oradebug setinst all SQL> oradebug -g all hanganalyze 4 SQL> oradebug -g all dump systemstate 267 SQL> oradebug tracefile_name SQL> quitdump 出来之后,怎么快速定位 blocker?其实不用每次都翻巨大的 trace。v$session里的P2RAW列直接给出了阻塞会话。10g 和 11g 的解析方式不同,注意位数:
-- 10g 32bit SELECT p2raw, TO_NUMBER(SUBSTR(TO_CHAR(RAWTOHEX(p2raw)),1,4),'XXXX') AS sid FROM v$session WHERE event = 'cursor: pin S wait on X'; -- 10g 64bit SELECT p2raw, TO_NUMBER(SUBSTR(TO_CHAR(RAWTOHEX(p2raw)),1,8),'XXXXXXXX') AS sid FROM v$session WHERE event = 'cursor: pin S wait on X';拿到 sid 后确认阻塞会话:
SELECT sid, serial#, sql_id, blocking_session, blocking_session_status, event FROM v$session WHERE sid = &blocker_sid;11g 更省事,BLOCKING_SESSION直接可用:
SELECT sid, serial#, sql_id, blocking_session, blocking_session_status, event FROM v$session WHERE event = 'cursor: pin S wait on X';成功的结果长这样:BLOCKING_SESSION_STATUS显示VALID,BLOCKING_SESSION有具体 sid,顺着这个 sid 查它的sql_id,就能看到那条正在硬解析的 SQL。再查 waiter 侧:
SELECT s.sid, t.sql_text FROM v$session s, v$sql t WHERE s.event LIKE '%cursor: pin S wait on X%' AND t.sql_id = s.sql_id;如果已经确定了 blocker 进程,还想看它到底卡在解析的哪一步,用 errorstack:
SQL> oradebug setospid <blocker_spid> SQL> oradebug dump errorstack 3 -- 等待 1 分钟 SQL> oradebug dump errorstack 3 -- 等待 1 分钟 SQL> oradebug dump errorstack 3 SQL> exit三次 errorstack 是为了看调用栈有没有推进。如果三次栈顶几乎一样,说明它真的卡住了,不是慢而是死等。到这一步,holder、waiter、SQL、执行计划基本都齐了,可以进入根因分析。
5. 本篇常见报错排查清单
排查过程中最容易撞的几个坑,我按真实报错列一下。
第一个,ORA-00054: resource busy或者 dump 时提示权限不足。这通常是oradebug需要sysdba权限,普通 DBA 账号不行。确认你用sqlplus "/ as sysdba"登录,或者有SYSDBA角色。另外oradebug setmypid必须在当前会话先执行,顺序错了后面全废。
第二个,local proxy failed。这个多半出现在你用带 MCP 的客户端去连采集端点时,配置里 Base URL 或 Key 没写全。检查三件套:Base URL 是不是https://taotoken.net/api,Key 是不是从https://taotoken.net/api-keys拿的最新值,Model ID 有没有填。Cline 的 MCP 配置里这三项缺一不可,Codex 的auth.json同理,只填 key 不填 base_url 一样会失败。
第三个,401 未授权。Key 过期、复制时带了空格、或者用了别的环境的 key。重新生成一个,注意别把换行符带进去。如果是在settings.json里配的,检查 JSON 有没有语法错误,一个多余的逗号就会让整个配置失效。
第四个,reading choices相关报错。这通常发生在解析返回结果时,端点返回的不是预期结构,可能是请求体格式不对,或者 model_id 写错了。对照文档https://taotoken.net/doc检查请求字段。
第五个,dump 文件巨大导致磁盘告警。system state dump 在进程多的时候能到几个 G,这就是为什么前面说"不是特别建议无脑 dump"。优先用P2RAW定位 blocker,实在需要 dump 再 dump,并且 dump 完及时清理。RAC 环境用-g all更要小心,四个节点一起 dump 磁盘压力翻倍。
第六个,AWR 里根本看不到cursor: pin S wait on X。可能是快照间隔太长把峰值平均掉了,或者等待时间太短没进 top。这时候去查v$active_session_history,按秒级采样看:
SELECT sample_time, session_id, sql_id, event FROM v$active_session_history WHERE event = 'cursor: pin S wait on X' AND sample_time > SYSDATE - 1/24 ORDER BY sample_time;第七个,BLOCKING_SESSION_STATUS显示UNKNOWN或NO HOLDER。说明 holder 在采样瞬间已经释放了,或者你查的时机不对。这种情况要连续多次查询,或者直接上 dump 抓瞬时状态。别指望一次查询就抓到,争用是动态的。
把这几条对照着过一遍,大部分首次排查的卡点都能解开。真正难的不是命令,是判断"这次到底是硬解析、version count 还是 bug",那需要结合 AWR 的 parse 相关指标和v$sql的version_count一起看,这个留到系列后面讲。
6. 把诊断数据收口,下次排查快一半
第一次排查cursor: pin S wait on X最耗时的往往不是分析,而是找数据:AWR 在哪台机器、trace 文件叫什么、上次类似问题的现场还在不在。我现在的习惯是每次排查完,把 AWR 文本、ADDM 文本、dump 摘要、blocker 的 SQL 和执行计划,按统一 tag 归档到采集端点。下次再遇到,先按 tag 捞历史,基线直接就有了。
归档走https://taotoken.net/api,Key 在https://taotoken.net/api-keys,控制台https://taotoken.net/console能看归档任务状态。如果你要长期做性能治理,尤其是多套库、多实例的场景,建议把 Coding Plan 也用上,把采集脚本、分析脚本、报告模板统一管理,省得每次手敲。模型对话入口在https://taotoken.net/models,调试归档请求格式的时候可以直接在那试。
最后给一个实用技巧:给每次排查建一个目录,命名用日期_事件_tag,比如20240923_cursorpin_tag,AWR、ADDM、dump 摘要、结论各一个文件。归档时整个目录推上去。三个月后你回头看,这套结构能让你在十分钟内重建任何一次现场。排查这件事,快不快取决于你上次有没有留好底。