$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を使ってコネクションプールを利用するのを試してみた。並列でコネクションプールを使うとどのようになるか、実際にどの程度速くなるかなどを試せて勉強になった。

ScalaでのJDBCによるDB操作の勉強をした

ScalaでPlay+Slickの勉強をし始めたのだけど、DIを使ってDBオブジェクトを取得したり、コネクションプールを使っていたり、クエリ発行はDSLだったりと、色々なことをやっていて全く理解できなかった。そのため、とりあえず地道に下の方から学習して、分からないところを減らしていこうと思ったので、まずはJDBCによるDB操作の勉強をした。

とりあえず使い方をさっと理解するために https://www.postgresql.jp/document/7.4/html/jdbc.html が参考になった。

JDBCとは

PerlDBIみたいなやつとおぼえた。ドライバの組み合わせもDBD::mysqlやDBD::pgみたいなものとおぼえた。

依存の追加

今回はPostgreSQLを使って操作をしようと思ったので、そのドライバを依存に追加する。build.sbtに以下のように記述する。

libraryDependencies += "org.postgresql" % "postgresql" % "42.1.4"

DB操作のサンプルコードの実装

https://www.postgresql.jp/document/7.4/html/jdbc.html を参考にしながら、自分でサンプルコードを書いてみると以下のようになった。

import java.sql._
import scala.util.Random

/**
 $ createdb jdbc-example
 $ psql jdbc-example
 CREATE TABLE person (
   id SERIAL NOT NULL PRIMARY KEY,
   name VARCHAR(254) NOT NULL
 );
 */
object JdbcExample {
  def main(args: scala.Array[String]) = {
    Class.forName("org.postgresql.Driver")
    val db = DriverManager.getConnection(
      "jdbc:postgresql:jdbc-example",
      "dbuser",
      "dbuser"
    )

    val newName = Random.alphanumeric.take(10).mkString
    val st1 = db.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 = db.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()

    db.close()
  }
}
  • まず、Class.forNameでJDBCで使うドライバを読み込む
  • 次にgetConnectionでDB名やユーザー名、パスワードを指定して、DBのコネクションを取得
  • prepareStatementとsetStringでDBのプレースホルダーを使ってクエリ発行ができるので、これでランダムなnameを作ってpersonテーブルにINSERT
  • 次にpersonテーブルに入っているデータをSELECT。LIMITは3件にしてみる。
    • ResultSetが返ってくるので、nextで一行ずつ見ながら、getIntやgetStringを使ってカラムのデータを取得していける

これで実行してみると以下のような出力を得られる。

> runMain JdbcExample
fGdQ97AoFu
1 inserted. name = fGdQ97AoFu
id: 1
name: fGdQ97AoFu
> runMain JdbcExample
JnS9X2nt2b
1 inserted. name = JnS9X2nt2b
id: 2
name: JnS9X2nt2b
id: 1
name: fGdQ97AoFu
> runMain JdbcExample
M5ZGqRLjqA
1 inserted. name = M5ZGqRLjqA
id: 3
name: M5ZGqRLjqA
id: 2
name: JnS9X2nt2b
id: 1
name: fGdQ97AoFu
> runMain JdbcExample
KARBx9lGLG
1 inserted. name = KARBx9lGLG
id: 4
name: KARBx9lGLG
id: 3
name: M5ZGqRLjqA
id: 2
name: JnS9X2nt2b

これでひとまず簡単なDB操作ができるようになった。

まとめ

今回はひとまずJDBCをそのまま使ってDB操作を行う方法について学習した。コードは https://github.com/shibayu36/scala-playground/blob/master/src/main/scala/JdbcExample.scala においた。

さらにJDBCの使い方について学ぶには https://jdbc.postgresql.org/documentation/93/ あたりを読むのが良さそう。

他にもコネクションプールを使うとか、いろいろアドバンスドな使い方はあると思うけど、一旦JDBCを使った操作の学習はここまでにしておく。

手元からpsqlでログインする時にパスワード入力せずにログインできてしまう理由

sudo brew install postgresql

Macpostgresqlを入れて、

$ psql postgres
postgres=# create role dbuser createdb login inherit password 'dbuser';

のようにroleを作ってみたのだけど

psql --username=dbuser postgres

でパスワード指定せずにログインできてしまった。


???となって少し調べてみたら、pg_hba.conf*1にデフォルトで以下のように設定されていて、この時localhostからpsqlでログインするときにはパスワードは不要になるようだった。ちょっと怖い。

# "local" is for Unix domain socket connections only
local   all             all                                     trust
# IPv4 local connections:
host    all             all             127.0.0.1/32            trust
# IPv6 local connections:
host    all             all             ::1/128                 trust

ちなみに https://stackoverflow.com/questions/4328679/how-to-configure-postgresql-so-it-accepts-loginpassword-auth を参考にして、trustじゃなくてmd5を指定するとパスワード入力を必須にできた。しかし、ちゃんと調べていないので、本当にこの指定方法が正しかったのかはよく分からない。

*1:brewで入れた場合/usr/local/var/postgres/以下ににある