$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

ScalaでHikariCPを使ってDBのコネクションプールを利用する

ScalaでのJDBCによるDB操作の勉強をした - $shibayu36->blog; の続き。今回はHikariCP を利用して、DBのコネクションプールをScalaで利用してみたのでメモ。DBにはPostgreSQLを利用した。

依存の追加

build.sbtに以下を追加。

libraryDependencies += "org.postgresql" % "postgresql" % "42.1.4"
libraryDependencies += "com.zaxxer" % "HikariCP" % "2.7.1"

単純にDB接続してみる

https://github.com/brettwooldridge/HikariCP#initializationhttps://jyn.jp/java-hikaricp-mysql-sqlite/ あたりを参考にした。

とりあえず接続してINSERTしたりSELECTしたりする例はこんな感じ。

import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }
import scala.util.Random

/**
 $ createdb hikaricp-example
 $ psql hikaricp-example
 CREATE TABLE person (
   id SERIAL NOT NULL PRIMARY KEY,
   name VARCHAR(254) NOT NULL
 );
 */
object HikariCPExample {
  def main(args: scala.Array[String]) = {
    val config = new HikariConfig()
    config.setDriverClassName("org.postgresql.Driver")
    config.setJdbcUrl("jdbc:postgresql:hikaricp-example")
    config.setUsername("dbuser")
    config.setPassword("dbuser")
    val ds = new HikariDataSource(config)

    val conn = ds.getConnection()

    val newName = Random.alphanumeric.take(10).mkString
    println(newName)
    val st1 = conn.prepareStatement("INSERT INTO person (name) VALUES(?)")
    st1.setString(1, newName)
    val rowsInserted = st1.executeUpdate()
    println(rowsInserted + " inserted. name = " + newName)
    st1.close()

    val limit = 3
    val st2 = conn.prepareStatement("SELECT id, name FROM person ORDER BY id DESC LIMIT ?")
    st2.setInt(1, limit)
    val rs2 = st2.executeQuery()
    while (rs2.next()) {
      println("id: " + rs2.getInt(1))
      println("name: " + rs2.getString("name"))
    }
    rs2.close()
    st2.close()

    conn.close()
  }
}
  • HikariConfigで接続などの設定をする
  • HikariDataSourceのインスタンスを作る
  • あとはHikariDataSourceからコネクションを獲得し、利用して、最後にcloseして終了

という感じで利用できる。

並列にコネクションを取得してみる

並列処理でHikariCPを使ってみて、コネクションプールが使われる様子を見てみる。例えば、最大でコネクションプールは3つだけ作るという設定で並列処理してみる。

import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }

object ParallelHikariCPExample {
  def main(args: scala.Array[String]) = {
    val config = new HikariConfig()
    config.setDriverClassName("org.postgresql.Driver")
    config.setJdbcUrl("jdbc:postgresql:hikaricp-example")
    config.setUsername("dbuser")
    config.setPassword("dbuser")
    config.setMaximumPoolSize(3)
    val ds = new HikariDataSource(config)

    (1 to 10).par.foreach { index =>
      val conn = ds.getConnection()
      println(s"Connection $index get connection")
      Thread.sleep(index * 1000)
      conn.close()
      println(s"Connection $index close connection")
    }
  }
}

これを実行してみる。Scalaのスレッド数の上限で処理がブロックしないように適当に設定しておく。

$ sbt -Dscala.concurrent.context.numThreads=16 -Dscala.concurrent.context.maxThreads=16
> runMain ParallelHikariCPExample
[info] Running ParallelHikariCPExample
Connection 8 get connection
Connection 1 get connection
Connection 7 get connection
Connection 1 close connection
Connection 6 get connection
Connection 7 close connection
Connection 5 get connection
Connection 6 close connection
Connection 9 get connection
Connection 8 close connection
Connection 2 get connection
Connection 2 close connection
Connection 3 get connection
Connection 5 close connection
Connection 4 get connection
Connection 3 close connection
Connection 10 get connection
Connection 4 close connection
Connection 9 close connection
Connection 10 close connection

少しわかりづらいが、

  • 最初に三つコネクションを取得でき、そこで上限なので他はgetConnectionでブロックする
  • その後、一つのコネクションがcloseで開放されるたびに、どれかのスレッドがコネクションを獲得できるようになる

という動きをした。コネクションプールを設定した数だけ作り、接続できたことが分かる。

HikariCPのベンチマークを取ってみる

実際にJDBCを直接使っての接続と、HikariCPでコネクションプールを使っての接続で、どの程度速くなるのかベンチマークを取ってみた。ベンチマーク取得にはnanobench を利用する。使い方は以前 nanobenchを使ってJavaのベンチマークを取る - $shibayu36->blog; の記事で書いた。

以下のようにベンチマークコードを書く。

import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }
import java.sql._
import me.geso.nanobench.Benchmark;

object HikariCPBenchmark {
  def main(args: scala.Array[String]): Unit = {
    new Benchmark(HikariCPBenchmarkInner).warmup(1).runByTime(3).timethese().cmpthese()
  }
}

object HikariCPBenchmarkInner {
  @Benchmark.Bench
  def jdbc: Unit = {
    Class.forName("org.postgresql.Driver")

    for (i <- 1 to 100) {
      val conn = DriverManager.getConnection(
        "jdbc:postgresql:hikaricp-example",
        "dbuser",
        "dbuser"
      )
      conn.close()
    }
  }

  @Benchmark.Bench
  def hikaricp: Unit = {
    val config = new HikariConfig()
    config.setDriverClassName("org.postgresql.Driver")
    config.setJdbcUrl("jdbc:postgresql:hikaricp-example")
    config.setUsername("dbuser")
    config.setPassword("dbuser")
    val ds = new HikariDataSource(config)

    for (i <- 1 to 100) {
      val conn = ds.getConnection()
      conn.close()
    }
    ds.close()
  }
}

実行してみる。

> runMain HikariCPBenchmark
[info] Compiling 1 Scala source to /Users/shibayu36/development/src/github.com/shibayu36/scala-playground/target/scala-2.11/classes...
[info] Running HikariCPBenchmark
Warm up: 1

Score:

jdbc: 17 wallclock secs ( 1.16 usr +  2.43 sys =  3.59 CPU) @  9.75/s (n=35)
hikaricp: 10 wallclock secs ( 1.22 usr +  1.89 sys =  3.10 CPU) @ 511.82/s (n=1589)

Comparison chart:

              Rate   jdbc  hikaricp
      jdbc  9.75/s     --      -98%
  hikaricp   512/s  5150%        --
[success] Total time: 43 s, completed Sep 18, 2017 8:16:40 AM

これを見ると、HikariCPを使うと接続が50倍ほど速くなることが分かった。コネクションプール便利。

まとめ

今回はScalaでHikariCPを使ってコネクションプールを利用するのを試してみた。並列でコネクションプールを使うとどのようになるか、実際にどの程度速くなるかなどを試せて勉強になった。