一、報錯現象

這是一個在使用 DB2資料庫過程中比較常見的錯誤, 報錯資訊如下

Exception stack trace:
com.ibm.db2.jcc.am.SqlException: DB2 SQL Error: SQLCODE=-805, SQLSTATE=51002, SQLERRMC=NULLID.SYSLH203 0X5359534C564C3031, DRIVER=3.66.46

二、關鍵知識點

先說明幾個知識點:

[Packages]

DB2 中的包是一組資訊,其可以控制任何靜態SQL語句的編譯,部分控制著任何動態SQL語句的編譯 以及可以影響在其範圍內發出的任何SQL請求的執行。

包資訊包括編譯期間使用的優化級別、執行期間是否對符合條件的遊標使用阻塞以及執行期間使用的並行度等專案。

對於靜態SQL語句,包還有一個和每個SQL語句相關聯的section

[Section]

因為應用程式可以有許多不同的靜態和動態性質的 SQL 語句,所以包也一樣。DB2 UDB 將包細分為更小的單元,稱為section。 section包含有關 SQL 語句本身(如果存在)以及有關在應用程式中找到 SQL 語句的上下文的資訊。

section是 SQL 語句的實際可執行體現。 它包含 DB2 UDB 產生指定結果所需的邏輯和資料訪問方法。 一個section由一系列運算子和任何關聯的運算元組成,這些運算元概述了資料訪問的執行順序和最佳操作。

section是 SQL 語句編譯的最終結果。 SQL 編譯器確定滿足 SQL 語句的最有效方法,並生成一個section來實現該計劃。

[DB2 CLI Packages]

DB2 呼叫級介面 (DB2 CLI) 是 DB2 系列資料庫伺服器的可呼叫 SQL 介面。 可呼叫 SQL 介面是用於資料庫訪問的應用程式介面 (API),它使用函式呼叫來呼叫動態 SQL 語句。在建立或遷移資料庫時,或者給資料庫服務端打補丁時,DB2 CLI 包會自動繫結到資料庫。

在 CLI 應用程式中分配的每個語句控制代碼將佔用 CLI 包中的一個section。

預設的:

  • DB2 CLI包在NULLID集合中建立
  • 為每個隔離級別(4 個隔離級別)和遊標保持性 (2種) 建立了三個小包和三個大包。 (3*4*2 + 3*4*2=共48包
  • 每個小包允許每個連線最多 65 個語句控制代碼,每個大包每個連線最多允許 385 個語句,其中大包和小包各有2個控制代碼是提供給update/delete語句和execute immediate語句(同一個連線中這兩種控制代碼可以被多次複用),所以每個連線中其他所有語句可以使用的控制代碼數初始預設為(3 * 63) + (3 * 383) = 1338 個。

CLI包的命名方式:SYSS[H|N]xyy 和 SYSL[H|N]xyy

  • 'S'代表小包,'L'代表大包
  • 'H' 代表 WITH HOLD,'N' 代表 NOT WITH HOLD
  • 'x'是隔離級別:0=NC, 1=UR, 2=CS, 3=RS, 4=RR
  • 'yy' 是包迭代 00 到 FF

預設下面這些包會在資料庫中預先建立好:

db2 "select pkgname from syscat.packages where pkgSchema='NULLID' and pkgname like '%SYS%'"

三、解決辦法

3.1. Case A

如果提示的缺少的package是上面提到的預先建立好的package其中之一,則說明那個package因為某些未知原因在資料庫中丟失了。

可以重新bind一次來建立包

cd ~db2inst1/sqllib/bnd
db2 "bind @db2cli.lst blocking all sqlerror continue grant public "

3.2. Case B

如果提示缺少的包的號碼大於上面提到的預先建立好的任何package號碼,則說明當前存在的包不夠用了,程式在申請新的package。

比如應用報805錯誤說找不到NULLID.SYSLH203這個包,則說明應用已經使用了SYSSH200, SYSSH201, SYSSH202 (3個小包) 和 SYSLH200, 201, 202 (3個大包) 並且正在尋找下一個大包。

首先需要知道,單次應用連線中可使用的CLI Package的控制代碼數量是有上限的,所以一般有2種情況會導致這種場景:

  • 應用程式程式碼中存在未正常釋放已經不需要使用的語句控制代碼。
  • 如果程式不存在上述控制代碼未釋放的情況,則可能是發生報錯的時間點應用承載了過高的併發壓力,而當前單次連線的語句控制代碼上限滿足不了業務需求了

對於程式碼層的原因,需要排查程式碼來解決問題根本原因。比較常見的出現問題的語句為prepareStatement, DECLARE CURSORS, 或者嵌入式SQL(靜態SQL)等,每一個獨立的這種語句都會佔用一個控制代碼,使用完畢後需要呼叫Statement.close()方法釋放控制代碼。

對於第二種情況,則需要手動增加CLI 包的數量

cd ~db2inst1/sqllib/bnd
db2 "bind @db2cli.lst blocking all sqlerror continue grant public CLIPKG X"

這裡的"X"是指可以建立的大包的數量,可以指定的範圍為3-30。

四、實驗

4.1. 錯誤復現

這裡有一個Java Demo,用來複現SQL0805N錯誤。

其中通過呼叫prepareStatement語句但不正常釋放來模擬控制代碼數耗盡。

import java.sql.*;
import java.io.*;
import java.math.*;
import java.util.*; public class db2805_1
{
public static void main(String[] args)
{
try{
Class.forName("com.ibm.db2.jcc.DB2Driver").getDeclaredConstructor().newInstance();
Connection conn= DriverManager.getConnection("jdbc:db2://10.211.55.10:60000/sample","db2inst1","db2inst1");
if (conn==null)
{
System.out.println("Can not connect to database");
} PreparedStatement pstmt = null;
ResultSet rs=null; String sql = "select count(*) from emp"; int i=1;
while(i<10000){
pstmt = conn.prepareStatement(sql); /*每次呼叫佔一個section*/
pstmt.execute();
System.out.println("i="+i);
i++;
} } catch(Exception e){
e.printStackTrace();
}
}
}

資料庫端的包的情況如下(預設):

-- 通過SQL檢查
$ db2 connect to sample Database Connection Information Database server = DB2/LINUXX8664 10.1.4
SQL authorization ID = DB2INST1
Local database alias = SAMPLE
$ db2 "select substr(PKGNAME,1,20),TOTAL_SECT,CREATE_TIME,ISOLATION,BOUNDBYTYPE,PKG_CREATE_TIME,LASTUSED from syscat.packages where PKGNAME like 'SYSSH20%' or PKGNAME like 'SYSLH20%'" 1 TOTAL_SECT CREATE_TIME ISOLATION BOUNDBYTYPE PKG_CREATE_TIME LASTUSED
-------------------- ---------- -------------------------- --------- ----------- -------------------------- ----------
SYSLH202 385 2021-06-18-18.18.44.883818 CS S 2021-06-18-18.18.44.883818 01/01/0001
SYSLH201 385 2021-06-18-18.18.44.874181 CS S 2021-06-18-18.18.44.874181 01/01/0001
SYSLH200 385 2021-06-18-18.18.44.865003 CS S 2021-06-18-18.18.44.865003 01/01/0001
SYSSH202 65 2021-06-18-18.18.44.818740 CS S 2021-06-18-18.18.44.818740 01/01/0001
SYSSH201 65 2021-06-18-18.18.44.816721 CS S 2021-06-18-18.18.44.816721 01/01/0001
SYSSH200 65 2021-06-18-18.18.44.814545 CS S 2021-06-18-18.18.44.814545 01/01/0001 6 record(s) selected. -- 也可以這樣檢查
$ db2 connect to sample Database Connection Information Database server = DB2/LINUXX8664 10.1.4
SQL authorization ID = DB2INST1
Local database alias = SAMPLE [db2inst1@db01] [~]
$ db2 "list packages for all"|grep -i NULLID|egrep -i 'SYSSH2|SYSLH2'
SYSLH200 NULLID SYSIBM 385 Y 3 CS B
SYSLH201 NULLID SYSIBM 385 Y 3 CS B
SYSLH202 NULLID SYSIBM 385 Y 3 CS B
SYSSH200 NULLID SYSIBM 65 Y 3 CS B
SYSSH201 NULLID SYSIBM 65 Y 3 CS B
SYSSH202 NULLID SYSIBM 65 Y 3 CS B

執行程式報錯:

可知,在申請第1339個section時失敗,這裡和上面說明的單次連線1338個控制代碼上限一致。

4.2. 錯誤修復

這裡給Demo程式碼新增正常釋放語句控制代碼的邏輯,如下

...
while(i<10000){
pstmt = conn.prepareStatement(sql);
pstmt.execute();
pstmt.close(); /*釋放控制代碼*/
System.out.println("i="+i);
i++;
}
...

再次執行成功

4.3. APP佔用section數的監控

在開啟了例項級別的監控開關後,可以通過採集應用的snapshot來獲取其對於section的佔用情況,例如:

Section number = 35
Application creator = NULLID
Package name = SYSLH206

則應用已經佔用的控制代碼數可以由上面的資訊計算出來。因為APP現在已經用到SYSLH206包的35個section,則其已經使用過了SYSSH200, SYSSH201, SYSSH202 (3個小包) 和 SYSLH200, 201, 202, 203, 204, 205(6個大包) 的所有section,具體數目為:3*64 + 6*384 + 35 = 2531

正常釋放控制代碼的APP

這裡我們來觀察下正常的APP在獲取CLI包section時的情況,demo程式為

while(i<10000){
pstmt = conn.prepareStatement(sql);
pstmt.execute();
pstmt.close(); /*釋放控制代碼*/
System.out.println("i="+i);
i++;
}

可知:控制代碼每次不需要使用後都正常釋放的APP,由於這裡只存在prepareStatement的呼叫和釋放,所以使用的section是固定的,為第一個小包SYSSH200的1個section。

未正常釋放控制代碼的APP

這裡為了方便觀察,給demo程式後面加了一層模擬休眠的SQL,從而模擬程式處於未提交狀態,另外prepareStatement語句每次迴圈使用完後並未釋放控制代碼

String sql = "SELECT * FROM sysibm.systables where fid=?";
String sql1 = "call dbms_alert.sleep(1000000)"; /*休眠*/ int i=1;
while(i<1338){
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1,5);
pstmt.execute();
System.out.println("i="+i);
i++;
}
pstmt = conn.prepareStatement(sql1);
pstmt.execute();

執行程式,可以看到成功執行了1338個語句(含一個sleep語句呼叫)

再採集DB2 snapshot觀察下section的佔用

可以計算下:3*63 + 3*383 = 1338

可知:未正常釋放控制代碼時,單次連線中控制代碼的佔用是逐漸遞增的,直到達到上限為止

4.4. 控制代碼未釋放是否影響其他併發連線

以上一小節agentid=562的應用為對比,再執行另一段未正常釋放控制代碼的程式,來觀察section的未釋放是否不會影響其他併發的連線

顯而易見,是無影響的。

五、思考總結

5.1. 最開始的思考誤區

一開始以為DB2 CLI包是一組由多個應用連線共享的資源,每個連線對於section的申請按照先到先得的原則,佔用控制代碼不釋放的異常應用程式最終會消耗光總的section從而產生805報錯。

此種思考結論,不能解釋應用人員提出來的:出現報錯後再次重試可以繼續執行而未出現報錯,以及別的一些應用訪問資料庫正常的現象。

5.2. DB2記憶體結構

這裡主要說明下DB2代理私有記憶體。

每個DB2 代理程序都有自己的私有記憶體工作區域,以執行任務。代理程序將代表應用程式使用記憶體來優化、構建和執行訪問計劃、執行排序、記錄遊標資訊,收集統計資訊等。

5.3. SQL語句工作流程

可知,SQL的執行是依賴於最終的編譯結果section的獲取。

對於CLI 包的呼叫,也應該是遵循這個過程,通過JDBC呼叫DB2 CLI介面時,程式中包含的PrepareStatement、Execute Immediate等語句都需要申請section,最終從CLI Package中獲取section並載入到自身代理的私有記憶體中。但是,在同一個應用連線中,CLI Package所包含的section個數是有上限的,如果存在已佔用的語句控制代碼在執行完並未正常釋放時,最終將導致達到上限而報錯。

並且,不同的應用連線在資料庫連線層的連線代理負責自己那一部分的包和section的獲取和載入到私有記憶體,即代理間是獨立的非共享的,所以不存在最開始提到的那個思考誤區。

六、參考文章

https://www.ibm.com/support/pages/75-ways-demystify-db2-9-tech-tip-db2-cli-packages-demystified

https://www.ibm.com/support/pages/sql0805n-package-nullidsyslh21e-was-not-found

https://www.ibm.com/support/pages/how-many-concurrently-running-statements-allowed-db2-java-application-and-how-increase-it