泄漏排查)
1. 從一次 ORA-01000 報錯說起游標(biāo)到底被誰占滿了應(yīng)用日志里突然刷出ORA-01000: maximum open cursors exceeded通常意味著某個會話打開的游標(biāo)數(shù)量超過了open_cursors參數(shù)允許的上限。這個報錯本身不復(fù)雜麻煩的是它只告訴你“超了”不告訴你“誰沒關(guān)”。如果只是把open_cursors從 300 調(diào)到 3000可能撐幾天又爆因?yàn)楦蛲谴a里Statement/ResultSet沒關(guān)或者循環(huán)里反復(fù)創(chuàng)建游標(biāo)卻不釋放。我處理這類問題的思路是先用 Errorstack 在報錯瞬間把會話的游標(biāo)現(xiàn)場 dump 下來看清是哪些 SQL、哪些游標(biāo)狀態(tài)卡在 BOUND再結(jié)合v$open_cursor和open_cursors參數(shù)判斷是配置偏小還是泄漏。下面按“復(fù)現(xiàn)報錯 → 設(shè)置 Errorstack → 抓 trace → 分析游標(biāo) → 調(diào)整參數(shù) → 驗(yàn)證修復(fù)”走一遍你可以直接照著在測試庫上做一次。適合誰看Oracle DBA、Java/中間件開發(fā)、需要定位連接池游標(biāo)泄漏的運(yùn)維同學(xué)。核心檢索詞就是 Errorstack、ORA-01000、open_cursors、游標(biāo)泄漏。2. 前置準(zhǔn)備TaoToken 與排查環(huán)境排查過程中如果要用大模型幫你讀 trace、解釋游標(biāo)狀態(tài)或者讓 coding agent 輔助寫診斷腳本可以先把 TaoToken 的接入配好。它提供 OpenAI 兼容接口模型對話、API Key、Coding Plan 都有獨(dú)立入口按需選即可。官網(wǎng)入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址https://taotoken.net/api模型對話驗(yàn)證模型是否可用https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan長期編碼/Agent 場景https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制臺https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keyshttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文檔https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteOracle 側(cè)需要一個可復(fù)現(xiàn)的測試庫11g/12c/19c 均可、能執(zhí)行alter system的權(quán)限、能訪問diag/rdbms/db/inst/trace目錄。Java 側(cè)準(zhǔn)備一個故意不關(guān)游標(biāo)的 demo用來制造泄漏。3. 可復(fù)制配置復(fù)現(xiàn) ORA-01000 并掛上 Errorstack3.1 先把 open_cursors 調(diào)小制造報錯為了快速復(fù)現(xiàn)把參數(shù)臨時調(diào)小。生產(chǎn)上不要這么干測試庫隨意。-- 查看當(dāng)前值 show parameter open_cursors; -- 臨時調(diào)小到 15方便復(fù)現(xiàn) alter system set open_cursors15 scopeboth; -- 確認(rèn) show parameter open_cursors;scopeboth表示內(nèi)存和 spfile 同時生效重啟后仍是 15。復(fù)現(xiàn)完記得改回去。3.2 設(shè)置 Errorstack 事件Errorstack 的作用是當(dāng)指定錯誤號出現(xiàn)時自動 dump 錯誤棧、進(jìn)程棧和會話游標(biāo)信息。針對 ORA-01000事件號就是 1000。-- 實(shí)例級出現(xiàn) ORA-01000 時 dump level 3 alter system set events 1000 trace name errorstack level 3; -- 排查結(jié)束后關(guān)閉 alter system set events 1000 trace name context off;Errorstack 的級別含義Level內(nèi)容1錯誤堆棧 函數(shù)調(diào)用堆棧2Level 1 ProcessState3Level 2 Context area顯示所有 cursors重點(diǎn)顯示當(dāng)前 cursor排查游標(biāo)泄漏用 level 3因?yàn)橹挥兴鼤褧挻蜷_的游標(biāo)列表打出來。也可以只在某個會話上設(shè)置alter session set events 1000 trace name errorstack level 3;3.3 用 Java demo 觸發(fā)泄漏下面這段代碼在循環(huán)里反復(fù)createStatement和executeQuery但只在 finally 里關(guān)最后一次的rset/stmt前面的游標(biāo)全部泄漏。循環(huán) 300 次open_cursors15必然報 ORA-01000。import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class TestCursor { public static void main(String args[]) throws Exception { Connection con null; Statement stmt null; ResultSet rset null; try { Class.forName(oracle.jdbc.driver.OracleDriver); String url jdbc:oracle:thin:127.0.0.1:1521:ora11; con DriverManager.getConnection(url, test, test); for (int i 0; i 300; i) { stmt con.createStatement(); rset stmt.executeQuery(select * from test); while (rset.next()) { rset.getString(1); } // 注意這里沒有 close游標(biāo)持續(xù)累積 } } catch (Exception e) { e.printStackTrace(); } finally { try { if (rset ! null) rset.close(); if (stmt ! null) stmt.close(); if (con ! null) con.close(); } catch (Exception e) { e.printStackTrace(); } } } }運(yùn)行后會看到類似堆棧java.sql.SQLException: ORA-00604: 遞歸 SQL 級別 1 出現(xiàn)錯誤 ORA-01000: 超出打開游標(biāo)的最大數(shù) ORA-00604: 遞歸 SQL 級別 1 出現(xiàn)錯誤 ORA-01000: 超出打開游標(biāo)的最大數(shù) ORA-01000: 超出打開游標(biāo)的最大數(shù) at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:445) ... at TestCursor.main(TestCursor.java:19)4. 驗(yàn)證請求與成功結(jié)果讀 trace 定位泄漏游標(biāo)4.1 找到 trace 文件報錯后alert 日志里會出現(xiàn)類似OS Pid: 8588 executed alter system set events 1000 trace name errorstack level 3 Errors in file f:\app\administrator\diag\rdbms\ora11\ora11\trace\ora11_ora_8764.trc: ORA-01000: 超出打開游標(biāo)的最大數(shù)打開ora11_ora_8764.trc重點(diǎn)看Session Cursor Dump和Session Open Cursors兩段。4.2 解讀游標(biāo) dump----- Session Cursor Dump ----- Current cursor: 0, pgadep0 Open cursors(pls, sys, hwm, max): 15(0, 3, 15, 15) NULL0 SYNTAX0 PARSE0 BOUND15 FETCH0 ROW0Open cursors(...): 15(0, 3, 15, 15)說明當(dāng)前會話打開了 15 個游標(biāo)其中 3 個是系統(tǒng)遞歸游標(biāo)高水位和上限都是 15。BOUND15表示 15 個游標(biāo)全部處于 BOUND 狀態(tài)——已經(jīng)綁定但沒關(guān)閉典型的泄漏特征。繼續(xù)往下看----- Session Open Cursors ----- Cursor#1(0x000000001BD91998) stateBOUND curiob0x000000001BDAD5B0 Cursor#5(0x000000001BD91BD8) stateBOUND curiob0x000000001CBD94E8 ----- Dump Cursor sql_idc99yw1xkb4f1u xsc0x000000001CBD94E8 cur0x000000001BD91BD8 ----- ObjectName: Nameselect * from test Cursor#6(0x000000001BD91C68) stateBOUND curiob0x000000001CBD8958 ----- Dump Cursor sql_idc99yw1xkb4f1u xsc0x000000001CBD8958 cur0x000000001BD91C68 ----- ObjectName: Nameselect * from test Cursor#9(0x000000001BD91E18) stateBOUND curiob0x000000001CBD66A8 ----- Dump Cursor sql_idc99yw1xkb4f1u xsc0x000000001CBD66A8 cur0x000000001BD91E18 ----- ObjectName: Nameselect * from test多個游標(biāo)指向同一個sql_idc99yw1xkb4f1uSQL 文本都是select * from test。這說明應(yīng)用在循環(huán)里反復(fù)執(zhí)行同一條 SQL每次新建游標(biāo)卻不關(guān)閉。到這里根因就清楚了不是open_cursors太小而是代碼泄漏。4.3 用 v$open_cursor 交叉驗(yàn)證在報錯會話還活著的時候可以查-- 按會話統(tǒng)計(jì)打開的游標(biāo)數(shù) select s.sid, s.serial#, s.username, count(*) as cursor_cnt from v$open_cursor o, v$session s where o.sid s.sid group by s.sid, s.serial#, s.username order by cursor_cnt desc; -- 看具體是哪些 SQL select sid, sql_id, sql_text from v$open_cursor where sid 問題會話SID order by sql_id;如果某個 SID 的游標(biāo)數(shù)接近open_cursors且 SQL 高度重復(fù)基本可以確認(rèn)泄漏點(diǎn)。5. 本篇常見錯排查5.1 Errorstack 設(shè)了但沒生成 trace先確認(rèn)事件是否真的生效select name, value from v$parameter where name event; -- 或 show parameter event;如果沒看到1000 trace name errorstack level 3可能是alter system沒執(zhí)行成功或者被其他 event 覆蓋。另外 trace 目錄權(quán)限不足也會導(dǎo)致寫不進(jìn)去檢查background_dump_dest和user_dump_dest。5.2 報錯是 ORA-01000 但 trace 里沒有 Session Open Cursors大概率是 level 設(shè)成了 1 或 2。只有 level 3 才包含 Context area。改成 level 3 重新觸發(fā)。5.3 調(diào)大 open_cursors 后不報錯了但連接池還是異常open_cursors是會話級上限調(diào)大只是延后爆發(fā)。如果v$open_cursor里某個會話游標(biāo)數(shù)持續(xù)增長不回落說明泄漏仍在。正確做法是修代碼Statement、PreparedStatement、ResultSet用完即關(guān)推薦 try-with-resources。try (Connection con DriverManager.getConnection(url, user, pwd); PreparedStatement ps con.prepareStatement(select * from test); ResultSet rs ps.executeQuery()) { while (rs.next()) { rs.getString(1); } }5.4 參數(shù)改了沒生效open_cursors是動態(tài)參數(shù)scopeboth立即生效。但如果用scopespfile需要重啟。另外 RAC 環(huán)境要在每個實(shí)例上確認(rèn)或者用sid*。alter system set open_cursors1000 scopeboth sid*;5.5 排查完忘記關(guān) Errorstack事件會一直掛在實(shí)例上每次 ORA-01000 都 dumptrace 目錄可能被撐爆。排查結(jié)束務(wù)必執(zhí)行alter system set events 1000 trace name context off;6. 收尾與后續(xù)接入整個鏈路走下來調(diào)小open_cursors復(fù)現(xiàn) → 掛 Errorstack level 3 → 跑泄漏 demo → 讀 trace 看到多個 BOUND 游標(biāo)指向同一 sql_id → 用v$open_cursor確認(rèn) → 修代碼用 try-with-resources → 把open_cursors調(diào)回合理值。根因是游標(biāo)泄漏不是參數(shù)太小這一點(diǎn)在 trace 里看得很清楚。如果你想讓 coding agent 幫你批量掃描項(xiàng)目里沒關(guān)的Statement或者用模型解釋 trace 里的游標(biāo)狀態(tài)可以走 Coding Plan 和模型對話入口API Key 和接入文檔在下面API Keyshttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文檔https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite模型對話https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite最后提醒一句生產(chǎn)庫上設(shè) Errorstack 前先確認(rèn) trace 目錄空間level 3 的 dump 在游標(biāo)多的時候文件會比較大別把磁盤寫滿。