Clojure を使用したデータベース接続プール
この記事の英語版に更新があります。ご覧の翻訳には含まれていない変更点があるかもしれません。
最終更新日 2024年05月09日(木)
接続プールは、事前に作成された一連の再利用可能な接続オブジェクトを使用してデータベースに接続するために、ソフトウェアアプリケーションによって使用されるパターンです。接続が必要になった場合は、このプールから既存の接続が取得されます。接続を使用しているスレッドが完了すると、その接続は、別のスレッドで使用できるようにプールに戻されます。このパターンにより、ネットワークトラフィックが減少し、新しい接続を作成するコストが抑えられ、さらにガーベジコレクターへの負荷が軽減されるためデータベースへの接続のオーバーヘッドが削減されます。
この記事では、Java Database Connectivity (JDBC) API と c3p0 ライブラリを使用してデータベース接続プールを作成する方法について学習します。
アプリケーションの作成
すでに Clojure アプリケーションがある場合は、それをこの例で使用できます。それ以外の場合は、先に進む前に「Heroku スターターガイド (Clojure)」の記事から単純なアプリケーションを作成します。この例のソースコードから始めて、順番に進めていくこともできます。
c3p0 の使用
c3p0 は、同名のキャラクターと同様に、プロトコルを主に扱います。ただし、イウォークと話す代わりにデータベースプロトコルを変換します。c3p0 は、従来の DriverManager ベースの JDBC ドライバーを、JNDI バインド可能な DataSource で拡張します。これには、接続とステートメントのプーリングを実装する DataSource が含まれます。
この例では、対応する clojure/java.jdbc 拡張を持つ c3p0 を使用します。これらの依存関係を追加するには、プロジェクトの project.clj
ファイルを開き、次のエントリを :dependencies
リストに追加します。
[org.clojure/java.jdbc "0.3.6"]
[clojure.jdbc/clojure.jdbc-c3p0 "0.3.1"]
ファイルを保存し、Leiningen を実行して、ライブラリがダウンロードされていることを確認します。
$ lein install
Retrieving org/clojure/java.jdbc/0.3.6/java.jdbc-0.3.6.pom from central
Retrieving org/clojure/java.jdbc/0.3.6/java.jdbc-0.3.6.jar from central
Retrieving clojure/jdbc/clojure.jdbc-c3p0/0.3.1/clojure.jdbc-c3p0-0.3.1.pom from clojars
Retrieving clojure/jdbc/clojure.jdbc-c3p0/0.3.1/clojure.jdbc-c3p0-0.3.1.jar from clojars
...
Installed jar and pom into local repo.
次に、db.clj
ファイルをプロジェクトに作成し、下記のコードを先頭に記述します (ただし、名前空間はアプリケーションに適した名前空間に置き換えます)。
(ns ticks.db
(:import com.mchange.v2.c3p0.ComboPooledDataSource)
(require [clojure.java.jdbc :as jdbc]
[jdbc.pool.c3p0 :as pool]))
この下に、環境からデータベース URL を取得するための次の関数を追加します。
(def db-uri
(java.net.URI. (or
(System/getenv "DATABASE_URL")
"postgresql://localhost:5432/ticks")))
この関数は DATABASE_URL 環境変数の値を取得しようとしますが、見つからない場合はデフォルトで localhost 接続文字列を使用します。これはローカル開発に役立ちます。
ここで、データベース URL を解析してユーザー名とパスワードを割り出す次の関数を追加します。
(def user-and-password
(if (nil? (.getUserInfo db-uri))
nil (clojure.string/split (.getUserInfo db-uri) #":")))
そして最後に、このモジュール用に、データベース接続を定義するための関数を追加します。
(def spec
(pool/make-datasource-spec
{:classname "org.postgresql.Driver"
:subprotocol "postgresql"
:user (get user-and-password 0)
:password (get user-and-password 1)
:subname (if (= -1 (.getPort db-uri))
(format "//%s%s" (.getHost db-uri) (.getPath db-uri))
(format "//%s:%s%s" (.getHost db-uri) (.getPort db-uri) (.getPath db-uri)))}))
この関数は、clojure.jdbc ライブラリの make-datasource-spec
関数を利用して接続プールを初期化します。これにより、spec
関数を使用してプールから接続を取得できます。
データベースを使用するコードがすでにアプリケーションにある場合、既存のコードの代わりに spec
関数の使用を開始できます。次のセクションでは、その使用方法の簡単な例を示します。
接続プールの使用
プロジェクトのメインクラス (スターターガイドの例)を使用した場合は web.clj
ファイル) を開き、少なくとも次の require ステートメントがあることを確認します。
(ns ticks.web
(:require
[clojure.java.jdbc :as jdbc]
[ticks.db :as db]
[compojure.core :refer [defroutes GET]]
[compojure.handler :refer [site]]
[compojure.route :as route]
[ring.adapter.jetty :as jetty]
[environ.core :refer [env]]))
ここで、データベーススキーマを移行する次の関数を追加します。
(defn migrated? []
(-> (jdbc/query db/spec
[(str "select count(*) from information_schema.tables "
"where table_name='ticks'")])
first :count pos?))
(defn migrate []
(when (not (migrated?))
(print "Creating database structure...") (flush)
(jdbc/db-do-commands db/spec
(jdbc/create-table-ddl
:ticks
[:id :serial "PRIMARY KEY"]
[:body :varchar "NOT NULL"]
[:tick :timestamp "NOT NULL" "DEFAULT CURRENT_TIMESTAMP"]))
(println " done")))
これらの関数では、"ticks" という名前の単純なテーブルがまだ存在しない場合、作成します。jdbc
関数の引数として db/spec
関数をどのように使用しているかに注目してください。
ここで、新しいテーブルをクエリする次の関数を追加します。
(defn tick []
(jdbc/insert! db/spec :ticks [:body] ["hello"]))
次に、tick
関数を呼び出すルートを定義してから、別のクエリを実行してティック数を表示します。
(defroutes app
(GET "*" []
(tick)
{:status 200
:headers {"Content-Type" "text/plain"}
:body (str "Ticks: " (first (jdbc/query db/spec ["select count(*) from ticks"])))}))
最後に、アプリケーションの起動時に migrate
関数が必ず呼び出されるようにします。これは次のようになります。
(defn -main [& [port]]
(migrate)
(let [port (Integer. (or port (env :port) 5000))]
(jetty/run-jetty (site #'app) {:port port :join? false})))
これで、アプリを実行できます。
Heroku へのアプリのデプロイ
アプリにまだデータベースがない場合、次のコマンドを実行して追加できます。たとえば、Essential-0 Heroku Postgres データベースを追加するには、次のようにします。
$ heroku addons:create heroku-postgresql:essential-0
次に、すべての変更を Git に追加し、アプリケーションをデプロイします。
$ git add .
$ git commit -m "adding a database connection pool"
$ git push heroku master
次に、ログを表示して、プールが正しく作成されていることを確認します。
$ heroku logs
2014-12-21T22:12:04.756975+00:00 heroku[web.1]: Starting process with command `java $JVM_OPTS -cp target/ticks-standalone.jar clojure.main -m ticks.web`
2014-12-21T22:12:05.262632+00:00 app[web.1]: Picked up JAVA_TOOL_OPTIONS: -Xmx384m -Djava.rmi.server.useCodebaseOnly=true
2014-12-21T22:12:07.341079+00:00 app[web.1]: INFO: MLog clients using java 1.4+ standard logging.
2014-12-21T22:12:07.341072+00:00 app[web.1]: Dec 21, 2014 10:12:07 PM com.mchange.v2.log.MLog
2014-12-21T22:12:08.358960+00:00 app[web.1]: Dec 21, 2014 10:12:08 PM com.mchange.v2.c3p0.C3P0Registry
2014-12-21T22:12:08.358965+00:00 app[web.1]: INFO: Initializing c3p0-0.9.5-pre9 [built 08-October-2014 03:06:08 -0700; debug? true; trace: 10]
2014-12-21T22:12:10.579168+00:00 app[web.1]: Dec 21, 2014 10:12:10 PM com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource
2014-12-21T22:12:10.579174+00:00 app[web.1]: INFO: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> 1bqs26q96urtp65v8d9bo|3276732, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> org.postgresql.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceUseNamedDriverClass -> false, identityToken -> 1bqs26q96urtp65v8d9bo|3276732, idleConnectionTestPeriod -> 800, initialPoolSize -> 0, jdbcUrl -> jdbc:postgresql://ec2-54-204-42-119.compute-1.amazonaws.com:5432/ddan7chsh1kg3g, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 3600, maxIdleTime -> 1800, maxIdleTimeExcessConnections -> 120, maxPoolSize -> 15, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {user=******, password=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
2014-12-21T22:12:10.594560+00:00 app[web.1]: Dec 21, 2014 10:12:10 PM com.mchange.v2.resourcepool.BasicResourcePool
2014-12-21T22:12:10.594564+00:00 app[web.1]: WARNING: Bad pool size config, start 0 < min 3. Using 3 as start.
2014-12-21T22:12:10.760360+00:00 app[web.1]: 2014-12-21 22:12:10.759:INFO:oejs.Server:jetty-7.x.y-SNAPSHOT
2014-12-21T22:12:10.801601+00:00 app[web.1]: 2014-12-21 22:12:10.800:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:34833
2014-12-21T22:12:10.960005+00:00 heroku[web.1]: State changed from starting to up
“Bad pool size config” (プールサイズの設定が正しくない) という警告に気付くかもしれません。ライブラリによって適切なデフォルトが選択されるため、これは問題ありません。ただし、実際のアプリケーションでは、最小プールサイズやその他の多くのオプションも設定する必要があります。
プールの設定
プール内でウォーム状態に維持されるアイドル接続の数は、アプリケーションのサイズや性質によって異なります。多くのユーザーが、HTTP リクエストを処理するスレッドあたり 1 つの接続で十分であることに気付いています (HTTP リクエストを処理するスレッドが接続を使用する唯一のスレッドである場合)。アプリケーションが、接続を新しいスレッドにすぐに引き継げないほど非常に高いスループットを処理している場合は、さらに多くを必要とする可能性があります。または、すべての HTTP リクエストがデータベースへのアクセスを必要としているわけでない場合は、少なくて済む可能性があります。最終的には、本番環境の負荷の下でのアプリケーションのプロファイリングが適切なプールパラメータを決定するための最善の方法です。
開発環境では、データベースをチェックすることにより、アプリケーションによって使用される接続の数を確認できます。
$ psql -h localhost
psql (9.3.2)
Type "help" for help.
jkutner=# \q
これにより、開発データベースへの接続が開かれます。その後、次を実行して Postgres への接続の数を確認できます。
select count(*) from pg_stat_activity where pid <> pg_backend_pid() and usename = current_user;
これにより、そのデータベース上の接続の数が返されます。
count
-------
5
(1 row)
シミュレートされた本番環境の負荷の下で、これは必要なプールのサイズを適切に示しています。ただし、いくつかの制約があります。
データベース接続の最大数
Heroku では、マネージド Postgres データベースが提供されます。階層型データベースごとにさまざまな接続制限があります。これは、Heroku Postgres アドオンのドキュメントにある一覧で見つけることができます。低い層のデータベースでは、高い層のデータベースより少ない接続が許可されます。データベースは、アクティブな接続の最大数に達すると、新しい接続を受け付けなくなります。これにより、アプリケーションが接続タイムアウトになり、例外が発生する可能性があります。
スケールアウトするときは、アプリケーションに必要なアクティブな接続の数に注意することが重要です。各 dyno で 5 つのデータベース接続が許可されている場合は、より堅牢なデータベースのプロビジョニングが必要になるまでに、4 つの dyno にしかスケールアウトできません。
これで、接続プールを設定する方法や、データベースで処理できる接続の数を見つける方法がわかったので、各 dyno に必要な接続の適切な数を計算する必要があります。
PgBouncer を使用した接続の制限
データベース接続の制限に達するまで、追加の dyno を使用して引き続きアプリケーションをスケールアウトできます。この時点に達する前に、PgBouncer buildpack を使用して、各 dyno に必要な接続の数を制限することをお勧めします。
PgBouncer では、データベーストランザクションで共有される接続のプールが保持されます。これにより、Postgres への接続 (通常は開いており、アイドル状態) が最小限に維持されます。ただし、トランザクションプーリングでは、名前付きプリペアドステートメント、セッションアドバイザリロック、リッスン/通知、またはセッションレベルで操作するその他の機能を使用できなくなります。詳細は、「PgBouncer buildpack FAQ for full list of limitations」(制限の完全なリストに関する PgBouncer buildpack の FAQ) を参照してください。
多くのフレームワークでは、PgBouncer を使用するためにプリペアドステートメントを無効にする必要があります。その後、PgBouncer buildpack を使用するようにアプリを設定します。
JDBC の場合、これには接続文字列への prepareThreshold=0
の追加が必要です。ただし、JDBC ドライバへのパッチの適用も必要になることがあります。
プリペアドステートメントを無効にするか、またはフレームワークでそれが使用されていないことを確認する前に続行しないでください。
$ heroku buildpacks:add heroku/pgbouncer
次に、アプリケーションを確実に実行可能にする必要があるため、言語固有の buildpack を追加する必要があります。Clojure を使用しているため、次のようになります。
$ heroku buildpacks:add heroku/clojure
ここで、PgBouncer を起動するように Procfile
を変更する必要があります。Procfile
で、コマンド bin/start-pgbouncer-stunnel
を web
エントリの先頭に追加します。そのため、Procfile
が
web: java $JVM_OPTS -cp target/ticks-standalone.jar clojure.main -m ticks.web
であった場合は、次のようになります。
web: bin/start-pgbouncer-stunnel java $JVM_OPTS -cp target/ticks-standalone.jar clojure.main -m ticks.web
結果を Git にコミットし、ステージングアプリでテストした後、本番環境にデプロイします。
デプロイ時、出力には次の内容が表示されます。
=====> Detected Framework: pgbouncer-stunnel
Clojure、JDBC、c3p0 を使用した接続プーリングについての詳細は、clojure-doc.org の java.jdbc セクションを参照してください。
この記事で使用されている例のソースコードは、GitHub で見つけることができます。