コネクションプールの落とし穴

 

 

 

 

 

 

 

 

 

システムインフィニティ 武石


はじめに

コネクションプールを使用した環境において実際に発生した落とし穴です。


環境

APサーバ  Resin 2.1.14

JAVA    J2SDK 1.4.2_07

 

DBサーバ(Microsoft SQL Server 2000 SP3)は別マシン環境にありAPサーバ(Resin)のコネクションプールを利用してアプリケーションからJDBCで接続している。


問題点の解説

コネクションプールが一杯になりデータベース接続が遅延・滞留してしまう原因の1つとして以下のコネクションの存在が判明。

 

コネクション

import com.caucho.sql.DBPool;

import java.sql.Connection;

import java.sql.SQLException;

角丸四角形吹き出し: @コネクションプールを利用してデータベース接続を行う。

 


                   ・・・・・

                   ・・・・・

                   ・・・・・

                   Connection conn = null;

                   try {

                            conn = DBPool.getPool([データソース名]).getConnection();

                            ・・・・・

                            conn = DBPool.getPool([データソース名]).getConnection();

角丸四角形吹き出し: Aデータベース接続を開放(クローズ)せずに再びコネクションプールを利用してデータベース接続を行う。                            ・・・・・

                   } catch (SQLException e) {

                            ・・・・・

                            ・・・・・

                            ・・・・・

                   } finally {

                            if (conn != null) {

角丸四角形吹き出し: Bデータベース接続を開放(クローズ)してコネクションプールへリソースを返却するが、余計に取得(最初に取得した)データベース接続は開放されずコネクションプールが1つ消費されたままになる。                                     try {

                                               conn.close();

                                     } catch (SQLException e) {

                                               // 例外は無視する

                                     }

                                     conn = null;

                            }

                   }

                   ・・・・・

                   ・・・・・

                   ・・・・・

 


コネクションの問題点

 

今回コネクションの障害は同一のオブジェクト(Connection)に対して2回データベース接続を行ったことが原因であるが、このパターンの問題は通常あまり見られずまた見落としがちである。

データベース接続に対する開放(close)は多少の経験があれば必ず行う必要のある処理であることは理解できまた正しく確実に開放(close)されているか確認することであろう。

 

しかしながら今回の問題はデータベース接続を開放(close)する通常フォーカスしているのとはまったく違う意味で実装(不具合の実装)が可能でなおかつ見つけにくい問題であったが、ソース解析によってこの問題点にフォーカスできさらにこの問題を障害として再現可能であったため取り除くことが可能であった。

 

大げさだと思われるかもしれないがプログラマーのスキル(知識や経験)、そしてちょっとした処理フローの見落としで難なく発生することが可能になる。

また特定が難しい場合も多くある。特にオブジェクト(Connection)を広範囲のスコープで利用可能にしている場合は注意が必要であるしこういったオブジェクトは限られたスコープで用いるべきであろう。

発生するのは当たり前だと思っている貴方も今までのプログラムを注意深く観察してみては如何であろう。

 

先ほど「問題の障害を再現可能であったため」と言ったがコネクションプールを利用していたのが障害発生の諸原因であり逆にコネクションプールを利用することで障害の確認が容易に可能であった。

もしコネクションプールを使用していなかったら障害が発生していなかったかもしれない、あるいは違った形の障害となっていた可能性もある。しかしその場合その障害を確認するのは困難を極めたであろう。

 

    コネクションプールを利用すべきであると説いているわけではないので誤解しないように。


問題点の確認

もしこの問題を実際に試してみたければ以下の内容を参考にAPサーバでご確認いただきたい。

なおAPサーバには Resin のバージョン 2 を想定しているのでご了承願いたい。

また Resin のバージョン 3 でも確認は出来ると思うので試してみても良いでしょう。

 

但しこの動作確認における責任は各自で負う事。

 

1.Resinのコンフィグレーションファイル

resin.conf(%RESIN_HOME%/conf/resin.conf)をテキストエディターで開くと40行目あたりに以下の記述が見つかる。

 

<resin.conf>

・・・・・

・・・・・

・・・・・

<resource-ref>

  <res-ref-name>jdbc/test</res-ref-name>

  <res-type>javax.sql.DataSource</res-type>

  <init-param driver-name="com.caucho.jdbc.mysql.Driver"/>

  <init-param url="jdbc:mysql_caucho://localhost:3306/test"/>

  <init-param user=""/>

  <init-param password=""/>

  <init-param max-connections="20"/>

  <init-param max-idle-time="30"/>

</resource-ref>

・・・・・

・・・・・

・・・・・

 


 

参考に以下の様にリソース定義を追加する。

なお追加する内容は動作可能な環境に合わせて記述願う。

<resource-ref>

  <res-ref-name>jdbc/pool-test</res-ref-name>                                                @

  <res-type>javax.sql.DataSource</res-type>                                                  A

  <init-param driver-name="com.microsoft.jdbc.sqlserver.SQLServerDriver"/>                   B

  <init-param url="jdbc:microsoft:sqlserver://C:D;databasename=E"/>                       C〜E

  <init-param user="F"/>                                                                    F

  <init-param password="G"/>                                                                G

  <init-param max-connections="20"/>                                                         H

  <init-param max-idle-time="30"/>                                                           I

</resource-ref>

 

@      リソース名(プーリングのデータソース名)を定義する。

A      リソース(データソース)のタイプを設定。

B      Microsoft SQL Server JDBCドライバを使用する場合。
他のデータベース及びJDBCドライバを使用する場合は合わせてください。

C      接続先のデータベースサーバ名またはIPアドレスを指定。

D      接続先のデータベースサーバのポート番号を定義
Microsoft SQL Server
の場合通常は 1433 になる。

E      データベースアカウントの既定のデータベース以外に接続する場合や明示的に指定する場合、データベース名を指定。
データベースアカウントの既定のデータベースを使用する場合や明示的に指定をしない場合 'databasename=' 部分は省略可能である。

F      データベースアカウントのユーザー

G      データベースアカウントのパスワード

H      コネクションプールの最大プール数を指定。
確認テストであれば多くは要らない。

I      コネクションプールからプールを取得する最大待ち時間と思われる(単位は分)
しかし1分に設定しても1分で帰ってこないので違うかも知れない。
知っている人は情報ください。

 

    データベースに接続した後、テーブルなどのオブジェクトに対して操作はしないのでバックアップなどは不要である。

    次ページから確認用プログラム(JSP)が3本出てくるが、その3本のプログラムの配置場所はサーブレットコンテナ上で実行するのに有効な場所であればどこに配置しても問題は無い。ぜひ試していただきたい。


2.コネクションのプログラムで確認

 

コネクションプールを消費させるコネクションのサンプルソースを以下に載せる。

resin.confのリソース(データソース)定義に合わせればこのまま動くはずである。

 

<trouble_dbpool_resin.jsp>

<%@ page contentType="text/html;charset=MS932" pageEncoding="MS932" %>

<%@ page import="com.caucho.sql.DBPool" %>

<%@ page import="java.sql.Connection" %>

<%@ page import="java.sql.SQLException" %>

<%!

         private static final String DATA_SOURCE_NAME = "jdbc/pool-test";

%>

<%

         DBPool dbpool = DBPool.getPool(DATA_SOURCE_NAME);

         String resule = "";

         if (request.getParameter("connect") != null) {

                   Connection conn = null;

                   try {

                            // 1回目のデータベース接続

                            conn = dbpool.getConnection();

                            // 2回目のデータベース接続

                            conn = dbpool.getConnection();

                            resule = "2回接続しました";

                   } catch (SQLException e) {

                            throw e;

                   } finally {

                            if (conn != null) {

                                     try {

                                               conn.close();

                                     } catch (SQLException e) {

                                               // 例外は無視する

                                     }

                                     conn = null;

                            }

                            resule += "<br>1回開放しました";

                   }

         }

         resule += "<br>現在のアクティブなコネクションプール数は";

         resule += dbpool.getActiveConnections();

         resule += "です";

%>

<html>

<head>

<title>WEBアプリケーションにおけるデータベースプール使用時の障害事例</title>

</head>

<body bgcolor="white">

<form action="<%= request.getRequestURI() %>" method="post" target="_top">

<center>

<hr>

<font size="5">

  接続ボタンを押してください。<br>

  押す度にコネクションが1つずつ消費されていきます。

</font>

<hr>

<input type="submit" name="connect" value=" 接 続 ">

<hr>

<%= resule %>

<hr>

</center>

</form>

</body>

</html>

 


3.コネクション未開放のプログラムで確認

 

コネクションを1回だけ行い開放(クローズ)しない場合でも確認してしてはどうなるであろう。聞くまでも無いだろう。

 

<noclose_dbpool_resin.jsp>

<%@ page contentType="text/html;charset=MS932" pageEncoding="MS932" %>

<%@ page import="com.caucho.sql.DBPool" %>

<%@ page import="java.sql.Connection" %>

<%@ page import="java.sql.SQLException" %>

<%!

         private static final String DATA_SOURCE_NAME = "jdbc/pool-test";

%>

<%

         DBPool dbpool = DBPool.getPool(DATA_SOURCE_NAME);

         String resule = "";

         if (request.getParameter("connect") != null) {

                   Connection conn = null;

                   try {

                            // 1回目のデータベース接続

                            conn = dbpool.getConnection();

                            resule = "1回接続しました";

                   } catch (SQLException e) {

                            throw e;

                   }

         }

         resule += "<br>現在のアクティブなコネクションプール数は";

         resule += dbpool.getActiveConnections();

         resule += "です";

%>

<html>

<head>

<title>WEBアプリケーションにおけるデータベースプール使用時の障害事例</title>

</head>

<body bgcolor="white">

<form action="<%= request.getRequestURI() %>" method="post" target="_top">

<center>

<hr>

<font size="5">

  接続ボタンを押してください。<br>

  押す度にコネクションが1つずつ消費されていきます。

</font>

<hr>

<input type="submit" name="connect" value=" 接 続 ">

<hr>

<%= resule %>

<hr>

</center>

</form>

</body>

</html>

 


4.コネクションプールのリソース確認プログラム

 

コネクションプールの各情報とresinが使用するJVMヒープメモリなど情報を表示するツールを以下に載せる。

Resinにはコネクションプールを利用するためのライブラリ(com.caucho.sql.DBPool)が標準で付いているためコネクションプールを利用するのがとても楽である。

 

環境に合わせて本ソースを修正する場合は5行目〜16行目のみの修正でよいはずである。

 

<admin_dbpool_resin.jsp>

<%@ page contentType="text/html;charset=MS932" pageEncoding="MS932" %>

<%@ page import="java.text.DecimalFormat" %>

<%@ page import="com.caucho.sql.DBPool" %>

<%!

         // ------------------------------------------------------------

         // resin.conf の設定に併せて以下を変更してください。

         //   sDB_POOL_MANES : DBプール名

         // ------------------------------------------------------------

         static final String[] sDB_POOL_MANES = { "jdbc/ pool-test"

                                                };

 

         // ------------------------------------------------------------

         // 横に表示するDPプールの情報数(既定値は1列)

         //   iDISP_COLS : DBプール表示数()

         // ------------------------------------------------------------

         static final int iDISP_COLS = 1;

%>

<%

// ------------------------------------------------------------

// ここから下のソースは変更しないでください!!

// ------------------------------------------------------------

         int      iDB_PoolCnt    = sDB_POOL_MANES.length;

         int[]    iDB_Pool_Max   = new int[iDB_PoolCnt];

         int[]    iDB_Pool_Used  = new int[iDB_PoolCnt];

         int[]    iDB_Pool_Total = new int[iDB_PoolCnt];

         DBPool[] dbpool         = new DBPool[iDB_PoolCnt];

 

         // リロード時間の初期値(3000ミリ秒)

         int re = 3000;

         if (request.getParameter("re") != null) {

                   re = Integer.parseInt(request.getParameter("re"));

         }

 

         // JVMの総ヒープメモリ取得、空きメモリ取得

         DecimalFormat df = new DecimalFormat("###,###,###,###,###");

         String freeMemory = df.format(Runtime.getRuntime().freeMemory());

         String totalMemory = df.format(Runtime.getRuntime().totalMemory());

 

         // 初期化

         for (int i = 0; i < iDB_PoolCnt; i++) {

                   // DBプールのMAX数

                   iDB_Pool_Max[i] = -1;

                   // DBプールの使用数

                   iDB_Pool_Used[i] = -1;

                   // DBプールの合計数

                   iDB_Pool_Total[i] = -1;

                   // DBプール

                   dbpool[i] = null;

         }

 

         // JDBCドライババージョン・初期化

         String strVersion           = "--";

         // JSPバージョン・初期化

         JspFactory    jsFact        = null;

         JspEngineInfo jsEinf        = null;

         String strJspVersion        = "--";

         // サーブレットAPIバージョン・初期化

         String strServletApiVersion = "--";

 

         // 例外時のエラー文字列・初期化

         String errorStr = "";

 

         try {

 

                   // DBプール情報取得

                   for (int i = 0; i < iDB_PoolCnt; i++) {

                            dbpool[i]         = DBPool.getPool(sDB_POOL_MANES[i]);

                            iDB_Pool_Max[i]   = dbpool[i].getMaxConnections();

                            iDB_Pool_Used[i]  = dbpool[i].getActiveConnections();

                            iDB_Pool_Total[i] = dbpool[i].getTotalConnections();

                   }

 

                   // JDBCドライババージョン情報取得

                   strVersion = dbpool[0].getMajorVersion() + "." + dbpool[0].getMinorVersion();

 

                   // JSPバージョン取得

                   jsFact = JspFactory.getDefaultFactory();

                   jsEinf = jsFact.getEngineInfo();

                   strJspVersion = jsEinf.getSpecificationVersion();

 

                   // サーブレットAPIバージョン取得

                   strServletApiVersion = getServletContext().getMajorVersion() + "." + getServletContext().getMinorVersion();

 

         } catch (Exception e) {

                   errorStr = e.toString();

                   e.printStackTrace();

         } finally {

                   jsFact = null;

                   jsEinf = null;

         }

%>

<html>

<head>

<!-- meta http-equiv="refresh" content="<%= (re / 1000) %>;url=<%= request.getRequestURI() %>" -->

<title>Audit</title>

 

<SCRIPT LANGUAGE="JavaScript">

<!--

  setTimeout("re()", <%= re %>);

  function re() {

    location.reload();

  }

//-->

</SCRIPT>

</head>

<body bgcolor="white">

<center>

<table border="0" style="font-size:12px">

  <tr>

    <td align="center">

      <b>DBプールおよび、サーブレットコンテナ(Tomcat/Resin)監視画面<br></b>

      <font color="gray"><%= (re / 1000) %>秒おきにリロードします。</font>

    </td>

  </tr>

  <tr>

    <td>

      <table border="0">

<%

         int iDispCols = iDISP_COLS;

         if (iDispCols < 1) {

                   iDispCols = 1;

         }

         for (int cnt = 0; cnt < iDB_PoolCnt;) {

%>

        <tr>

<%

                   for ( int cols = 0; cols < iDispCols && cnt < iDB_PoolCnt; cols++, cnt++) {

%>

          <td>

            <table border="1" width="270" style="font-size:12px">

              <tr>

                <td align="center" colspan="2" bgcolor="#FFCCCC"><%= sDB_POOL_MANES[cnt] %></td>

              </tr>

              <tr>

                <td align="left" width="200">&nbsp;DBプールのMAX数</td>

                <td align="center"><%= iDB_Pool_Max[cnt] %></td>

              </tr>

              <tr>

                <td align="left">&nbsp;コネクションの合計数</td>

                <td align="center"><%= iDB_Pool_Total[cnt] %></td>

              </tr>

              <tr>

                <td align="left">&nbsp;現在、使用中のプール数</td>

<%

                            if (((iDB_Pool_Used[cnt] * 10) / iDB_Pool_Max[cnt]) >= 4 &&

                                ((iDB_Pool_Used[cnt] * 10) / iDB_Pool_Max[cnt]) <= 7) {

%>

                <td align="center" bgcolor="#FFFF99"><%= iDB_Pool_Used[cnt] %></td>

<%

                            } else if ((( iDB_Pool_Used[cnt] * 10) / iDB_Pool_Max[cnt] ) > 7) {

%>

                <td align="center" bgcolor="#FFCCCC"><%= iDB_Pool_Used[cnt] %></td>

<%

                            } else {

%>

                <td align="center"><%= iDB_Pool_Used[cnt] %></td>

<%

                            }

%>

              </tr>

            </table>

          </td>

<%

                   }

%>

        </tr>

        <tr>

          <td></td>

        </tr>

<%

         }

%>

      </table>

    </td>

  </tr>

  <tr>

    <td>pool数の背景が黄色、または赤色になりましたらシステム管理者に連絡して下さい。</td>

  </tr>

  <tr>

    <td>&nbsp;(黄色:40-70%使用中、赤色:70-100%使用中)</td>

  </tr>

  <tr>

    <td>・情報取得に失敗した場合は、-1が表示されます。</td>

  </tr>

  <tr>

    <td>

      &nbsp;

    </td>

  </tr>

  <tr>

    <td>

      <table border="1" width="270" style="font-size:12px">

        <tr>

          <td align="center" colspan="2" bgcolor="#FFCCCC">その他、JAVA環境の情報</td>

        </tr>

        <tr>

          <td align="left" width="200">&nbsp;JDBCドライババージョン</td>

          <td align="center"><%= strVersion %></td>

        </tr>

        <tr>

          <td align="left">&nbsp;JSPバージョン</td>

          <td align="center"><%= strJspVersion %></td>

        </tr>

        <tr>

          <td align="left">&nbsp;サーブレットAPIバージョン</td>

          <td align="center"><%= strServletApiVersion %></td>

        </tr>

        <tr>

          <td align="left">&nbsp;JVM総ヒープメモリ(バイト)</td>

          <td align="right"><%= totalMemory %></td>

        </tr>

        <tr>

          <td align="left">&nbsp;JVM空きメモリ(バイト)</td>

          <td align="right"><%= freeMemory %></td>

        </tr>

      </table>

    </td>

  </tr>

</table>

<%= errorStr %>

</center>

</body>

</html>