2013年08月12日

DBCP より 50倍早い!? BoneCP を使って高速コネクション・プーリングを実現する方法

DB 殺すにゃ刃物はいらぬ、Connection の3つも短時間で get すれば良い、なんて言ったか言わないかはわかりませんが DB サーバーにとってコネクションの生成はコストの掛かる処理で、この負荷を軽減する為にアプリケーション側で DB コネクションのプーリングを実装することは多々あります。

このコネクション・プーリングは JDBC ドライバの一部として提供されることもありますが一般的には依存性を回避する為に DBCP , C3PO などのミドルウェアを利用しますよね。

今日ご紹介するのは高速、ハイ・パフォーマンスを特徴とする第三の JDBC コネクション・プーリング実装 - BoneCP です。

performance


BoneCP のインストールと設定


BoneCP のプロジェクト・ページはこちら。

BoneCP - The fast Java JDBC Connection Pool


Download リンクを辿り Binary (bonecp-x.x.x.RELEASE.jar)をダウンロード。

この他に外部ライブラリとして参照する Guava ( Google Core Libraries for Java ) , SLF4J , JDBC ドライバ(ここでは MySQL を利用)を入手し、クラスパスに追加しましょう。

BoneCP - external libraries


これで準備はおしまい。


BoneCP による JDBC コネクション・プーリングの利用


今回用意したサンプルコードがこちら。

import java.sql.Connection;
import java.sql.SQLException;

import com.jolbox.bonecp.BoneCP;
import com.jolbox.bonecp.BoneCPConfig;

public class MyBoneCPApp {


  private BoneCP bcp = null;
  private String driverClassName = "com.mysql.jdbc.Driver";
  private String username = "root";
  private String password = "";
  private String url = "jdbc:mysql://localhost/test";
  
  public void init() throws ClassNotFoundException, SQLException {
    Class.forName(driverClassName);
     BoneCPConfig config = new BoneCPConfig();
     config.setJdbcUrl(url);
    config.setUsername(username);
    config.setPassword(password);
    config.setMinConnectionsPerPartition(5);
    config.setMaxConnectionsPerPartition(10);
    config.setPartitionCount(1);
    config.setDefaultAutoCommit(false);
    config.setDefaultReadOnly(false);
    config.setDefaultTransactionIsolation("READ_COMMITTED");
    
    bcp = new BoneCP(config);    
  }
  
  public Connection getConnection() throws SQLException {
    Connection conn = null;
    if (bcp != null) {
      conn = bcp.getConnection();
      System.out.println("Created("+bcp.getTotalCreatedConnections() + "), " 
            + "Free(" + bcp.getTotalFree() + "), "
            + "Leased(" + bcp.getTotalLeased()+")");
    }
    
    return conn;
  }
  
  public void shutdown() {
    if (bcp != null) {
      bcp.shutdown();
    }
  }
  
  /**
   * 
   * @param args
   */
  public static void main(String[] args) {
    MyBoneCPApp app =  new MyBoneCPApp();
    try {
      app.init();
      long start = System.currentTimeMillis();
      for (int i=0; i<100; i++) {
        Connection conn = app.getConnection();        
        conn.close();
      }
      long end = System.currentTimeMillis();      
      System.out.println((end - start) + " millis");
    } catch (SQLException e) {
      e.printStackTrace();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      app.shutdown();
    }
  }

}

init() メソッドで BoneCP コネクション・プーリングを初期化(プーリング数は最低 5、最大 10)、JDBC URL , Min/MaxConnectionsPerPartition , PartitionCount を設定しています。
 
ここで聞き慣れない Partition という用語が出てきますが、これは BoneCP でコネクションとそれを管理するスレッドのグループを指します。

1つのプールに対して複数スレッドでコネクションの入出力を行う場合、その入出力を行うメソッドは synchronized で同期・スレッド・セーフにする必要がありますが、このロックを共有するスレッド数が増えるほど性能面では不利になるた為、ある単位でプールとスレッドを複数のグループ(パーティション)に分けて高速化しようという試み。

ただし、ベンチマーク結果を見ると複数パーティションによる大きなメリットは見受けられないようで、基本的に1パーティションで利用すれば問題無いようです。

bonecp multithread


最後に BoneCPConfig オブジェクトをコンストラクタに渡して BoneCP インスタンスを生成すれば準備完了。

getConnection() メソッドでは後述する DBCP との違いを比較出来るようプーリングのステータスを標準出力しています。


DBCP による JDBC コネクション・プーリングの利用


DBCP は Apache Commons プロジェクトで再利用可能な Java コンポーネントとして開発されているコネクション・プーリング用ライブラリ。

DBCP - Overview


詳しい説明は割愛しますが、数多くのアプリケーションで利用されていますよね。

commons-dbcp-1.4.jar, commons-pool-1.6.jar の他に JDBC ドライバ を要すればOK。

今回 BoneCP と比較する DBCP コードはこちら。

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbcp.BasicDataSource;

public class MyDbcpApp {

  private BasicDataSource db = null;
  private String driverClassName = "com.mysql.jdbc.Driver";
  private String username = "root";
  private String password = "";
  private String url = "jdbc:mysql://localhost/test";
  
  public void init() {
    db = new BasicDataSource();
    db.setDriverClassName(driverClassName);
    db.setUsername(username);
    db.setPassword(password);
    db.setUrl(url);
    db.setMaxActive(10);
    db.setMaxIdle(10);
    db.setMinIdle(5);
    db.setDefaultAutoCommit(false);
    db.setDefaultReadOnly(false);
    db.setDefaultTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
  }
  
  public Connection getConnection() throws SQLException {
    Connection conn = null;
    if (db != null) {
      conn = db.getConnection();
      System.out.println(
          "Active(" + db.getNumActive() + "), Idle(" + db.getNumIdle() + ")");
    }
    
    return conn;
  }
  
  public void shutdown() {
    if (db != null && db.getNumIdle() > 0) {
      try {
        db.close();
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
  }
  
  /**
   * @param args
   */
  public static void main(String[] args) {
    MyDbcpApp app = new MyDbcpApp();
    app.init();
    try {
      long start = System.currentTimeMillis();
      for (int i=0; i<100; i++) {
        Connection conn = app.getConnection();
        conn.close();
      }
      long end = System.currentTimeMillis();      
      System.out.println((end - start) + " millis");
    } catch (SQLException e) {
      e.printStackTrace();
    } finally {
      app.shutdown();
    }
  }

}

BoneCP と同様 init() メソッドを用意、この中で BasicDataSource オブジェクトを生成、初期化しています。

getConnection() メソッドでは Active, Idle 数を標準出力。

これで BoneCP と DBCP を比較する準備が出来ました。


BoneCP vs. DBCP - DBコネクション生成とその性能比較


2つのコードはシーケンシャル(非並列)に100コネクションを取得、都度クローズしてその間に要した時間をミリ秒単位で計測しています。

まずは、DBCP を使ったコードを実行してみます。

・・・
Active(1), Idle(0)
Active(1), Idle(0)
Active(1), Idle(0)
375 millis

合計 375 ミリ秒(3.75 millis/conn)。

続いて BoneCP を使ったコード。

・・・
Created(5), Free(4), Leased(1)
Created(5), Free(4), Leased(1)
Created(5), Free(4), Leased(1)
7 millis

何と、衝撃(?)の 合計 7 ミリ秒(0.07 milis/conn)。 約 50 倍強の短縮ですね。

秒オーダーで数千、数万のリクエストを消化するアプリケーションでは強力な武器になりそうです。

さて、何故 BoneCP は早いのでしょうか。 これには幾つかの工夫があるのですが、まず1つが初期化時のコネクション生成。

BoneCP では BoneCP インスタンスを生成した瞬間、今回のコードで言えば init() メソッドを実行した瞬間に設定した最大プーリング数の 20 % 程のコネクションを生成しています。
これにより初期化は遅い(DBCP ならば数十ミリ秒が BoneCP では数百ミリ秒)ですが、コネクション取得は高速なります。

工夫の2つ目はコネクション管理のスレッド化。

コネクションの状態を見ると DBCP では常に次のような標準出力となり close されたコネクションは必ず次回には再利用されれている、つまり close と 再利用が同期していることが分かります。

Active(1), Idle(0)
Active(1), Idle(0)
Active(1), Idle(0)

これに対して BoneCP では close と再利用が別スレッドで実行され、今回のようなシーケンシャルなコードであっても Leased が2つ以上になることあります。

Created(5), Free(4), Leased(1)
Created(5), Free(4), Leased(1)
Created(5), Free(3), Leased(2)
Created(5), Free(4), Leased(1)
Created(5), Free(3), Leased(2)

実際には最大値には達していないにも関わらず getConnection() で例外が発生する可能性・デメリットがあることになりますが、そもそも”ほぼ最大値”に達している状態ですから、50倍高速になった方がメリットは大きいとも言えそうです。

Bone CP は開発活動も活発ですし、次世代 JDBC コネクション・プーリング実装として期待して良さそうです。

それでは、より早いプーリング・ライフを!

Javaデータアクセス実践講座 (DB Magazine SELECTION)
松信 嘉範
翔泳社
売り上げランキング: 246,808

JDBCによるJavaデータベースプログラミング 第2版
ジョージ リース
オライリー・ジャパン
売り上げランキング: 714,471

Posted by netbuffalo at 20:00│Comments(0)TrackBack(0)Java | プログラミング


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4550713

コメントする

名前
URL
 
  絵文字