コネクションプーリングの効果を実測してみる

Java EE アプリケーションサーバを使うとどの製品にもDBコネクションプーリング機能が付いている。DB接続の再接続にはコストがかかることがこの機能の動機だが、いったいどれくらいの『コスト』なのか。あまり実データを見たことないので、実際に測ってみる。

テスト環境

コストを実感しやすいように、DBには接続ごとに対応した子プロセスをfork()するPostgreSQLを使ってみる。

テスト内容

10多重で1回、100回、1000回、10000回の新規接続して SELECT 1 を投げるプログラムを実行する。DBCPを使ったコネクションプーリングの有無で性能を比較する。

テストプログラム

まずはSQLを実行する部分。SELECT 1 を投げるだけのとてもシンプルなタスク。
DBCPを使わない速度を測るときには、コメントアウトされている方のコードを動かす。

public class QueryRunner implements Runnable {
    @Override
    public void run() {
 //       try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/test", "postgres", "postgres")) {
        try (Connection con = DataSourceFactory.getDataSource().getConnection()){
            PreparedStatement ps = con.prepareStatement("SELECT 1");
            ps.executeQuery();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

10多重で動かして、時間を測る部分。Executorフレームワークを使用した。

public class Main {

    private static final Logger logger = Logger.getLogger(Main.class.getName());

    public static void main(String ... args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            executor.submit(new QueryRunner());
        }
        executor.shutdown();
        if (!executor.awaitTermination(120, TimeUnit.SECONDS)) {
            logger.warning("timeout occur. over 120 seconds.");
        }
        long end = System.currentTimeMillis();
        long time = end - start;
        logger.info("time(millsec): " + time);
    }
}

DBCPの初期化をする部分。スタンドアロンのDBCP初期化コードはあまり書いたことがないので自信なし。この部分はもっと良い書き方もあると思う。

public class DataSourceFactory {

    private static final Logger log = Logger.getLogger(DataSourceFactory.class.getName());
    private static DataSource ds = null;

    static {
        try(InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream("dbcp.properties")) {
            Properties props = new Properties();
            props.load(in);
            ds = BasicDataSourceFactory.createDataSource(props);
            log.info("DataSource initialized.");
        } catch (Exception e) {
            log.warning("DataSource initialize failed.");
        }
    }

    public static DataSource getDataSource() {
        if (ds == null) {
            throw new IllegalStateException("DataSource initialize failed.");
        }
        return ds;
    }
}

最後にDBCPのプロパティファイル。アカウントの部分は適宜置き換えて使う。ちょっとハマったのがdefaultAutoCommitがデフォルトでtrueであること。DBCPを使っていると、JDBCを直接使うときと同じように、デフォルトではINSERT/UPDATE/DELETEを実行した時点でコミットされてしまうので、多くのシステムではここをfalseに明示的に設定した方が良いと思う。

driverClassName=org.postgresql.Driver
url=jdbc:postgresql://localhost:5432/test
username=postgres
password=postgres
defaultAutoCommit=false
initialSize=10
maxActive=10
maxIdle=10
maxWait=10000

測定結果

100トランザクションまでは両方とも比較的早い応答があるが、1000トランザクション以降差が広がり、一万トランザクションでは10倍以上の性能差が開く。コネクションプーリングは改めて有効だということがわかった。

またトランザクション数1の時に、DBCPなしの方が早い結果が出ているが、これはDBCPのプロパティを読み込んで初期化する処理が時間の測定範囲内に含まれていることが要因だと思う。

(時間の単位はミリ秒)

トランザクション DBCPなし DBCPあり
1 135 262
100 1328 308
1000 6575 2062
5000 30864 3537
10000 70151 6326

f:id:n_agetsuma:20140216145517p:plain

まとめ

ほんの数回SQLを実行するプログラムではコネクションプーリングをしなくてもなんとかなるが、千〜万単位でgetConnection()してトランザクションを実行するプログラムでは、コネクションプーリングした方が良い。APサーバを使っているとあまり意識しないが、Java SE環境のスタンドアロンアプリケーションではDBコネクションプールを忘れがちなので注意が必要。