クローズ漏れの個所も大体の見当が付いたので、アプリケーション開発チームに確認・修正を依頼した。
アプリケーションが直るまでの間、removeAbandonedを付けたままで負荷試験を継続することにした。目的はremoveAbandonedのオーバヘッドを測定することである。logAbandonedを指定しなければstackTraceは生成されない。そのため、ある程度の性能は出るように思われた。
しかし、実際に測定してみたところ、思ったように性能は出なかった。CPUがほとんど使い切れなかったのだ。
ここまできたら、ついでに解析してしまうのが、トラブルシューティング屋の務めだろう。負荷を掛けている最中にスレッドダンプを取得すると、多数のスレッドが以下の2つの処理の直前でtraceリストのロック取得待ちとなっていた。
該当するAbandonedObjectPoolのソースコードをチェックすると、リスト6のようになっている。
リスト6 removeAbandonedのtraceリストロック |
private void removeAbandoned() { |
synchronizedブロックで囲われた中で、それぞれIteratorによるリストの走査処理と、ArrayList#remove(obj)を呼び出している。
どちらも十分速い処理ではあるが、リスト内の要素数が増えるにつれ、どうしてもある程度の時間を要してしまう。この2つの処理は、コネクションの使用に伴い毎回呼ばれる処理のため、無視できない同期待ち時間になってしまったのだ。
removeAbandonedのオーバヘッドは予想以上に大きかった。やはり、コネクションのクローズ漏れに関しては、アプリケーションを修正せざるを得ない。
「このような基本的なミスをして申しわけない」。アプリケーション開発のリーダーから修正版のアプリケーションを受け取ると、今度こそ性能が出ることを確認できた。
今回の事例からは、Tomcatを使用するうえではDBCPについての知識も必要となることが分かる。そこでコネクションプールの性能に影響を与えるパラメータについて解説しておこう。
パラメータ | 内容 | デフォルト値 | |
---|---|---|---|
maxActive | プールから貸し出すコネクションの最大数 | 8 | |
minIdle | プール内のコネクション数をminIdle以上に保つ | 0 | |
maxIdle | プール内のコネクション数をmaxIdle以下に保つ | 8 | |
initialSize | プールの初期状態に確保するコネクション数 | 0 | |
maxWait | プール内のコネクションが不足したときの最大待ち時間(ミリ秒) | −1(無限大) | |
コネクション数の管理において、一番注意が必要となるのが、「maxActive」だ。DBサーバ側にも最大プロセス数等の制限があるため、その値以下にしなくてはならない。
また、アプリケーションサーバが複数台あるときには、その分も考慮しなくてはならないだろう。0以下の値を指定した場合、最大値の制限はなくなる。
「maxWait」はプール内のコネクションが不足したとき、最大何ミリ秒待つかを指定する。指定した時間内にコネクションが確保できなかった場合、SQLExceptionが投げられる。エラーとして応答を返せば、アプリケーションが無応答になることを防ぐことができる。
コネクション数はTomcatのスレッド数よりも少ない値を指定することも可能だ。しかし、Object#wait()で待っている複数のスレッドはObject#notifyAll()によってすべて起動させられる。
コネクションを取得できるのは、OSのスケジューリングにより最初にコネクションの再取得にたどり着いたスレッドのみだ。そのため、運が悪いスレッドはいつまでもコネクションが取得できないこともあり得る。コネクション数を絞り込む場合、適切な「maxWait」の値を設定しタイムアウトさせた方がよいだろう。
パラメータ | 内容 | デフォルト値 | |
---|---|---|---|
validationQuery | コネクションが有効かどうかを検証するためのSQL | なし | |
testOnBorrow | コネクションをプールから取り出すときに検証するかどうか | true | |
testOnReturn | コネクションをプールに返すときに検証するかどうか | false | |
testWhileIdle | プール内でアイドル状態のコネクションを検証するかどうか | false | |
timeBetweenEvictionRunsMillis | アイドル状態のコネクションの検証間隔 | −1 | |
numTestsPerEvictionRun | 1回の検証当たり何個のコネクションを対象とするか | 3 | |
minEvictableIdleTimeMillis | 何ミリ秒アイドルだったら検証するか | 1000 * 60 * 30 | |
validationQueryパラメータでは、コネクションが有効かどうかを検証するためのSQLを指定する。通常は、負荷の軽いSQLを指定する。
testWhileIdle以降はプール内でアイドル状態のコネクションを検証するかどうかのパラメータだ。この機能を使用するときには、validationQueryパラメータが設定されている必要がある。
パラメータ | 内容 | デフォルト値 | |
---|---|---|---|
poolPreparedStatements | PreparedStatementをキャッシュするかどうか | false | |
maxOpenPreparedStatements | 1コネクション当たりキャッシュするPreparedStatementの最大数 | 0(無限大) | |
PreparedStatementキャッシュは、作成したPreparedStatementをDBコネクションごとにキャッシュする機能だ。SQLのパース、実行計画の作成などのDBサーバの処理を減らし、負荷を下げる効果がある。
パラメータ | 内容 | デフォルト値 | |
---|---|---|---|
removeAbandoned | クローズ漏れとなったコネクションを回収するかどうか | false | |
removeAbandonedTimeout | コネクションが最後に使用されてから回収対象となるまでの時間(秒) | 300 | |
logAbandoned | コネクションがプールから取り出されたときの時間とスタックトレースを回収時に出力するかどうか | false | |
removeAbandonedはクローズ漏れとなったコネクションを回収する機能だ。しかし、本文中でも触れたとおり、非効率な処理が多く含まれている。本番環境で使用することは避けた方がよいだろう。
本稿では、試験環境におけるDBCPに関するトラブル事例を紹介した。DBCPにはクローズ漏れを実行時に回収する手段や呼び出しのstackTraceを表示する機能が存在する。
しかし、フレームワークにコネクションの処理を委譲したり、コーディング規約やチェックツールにより確実にクローズしていることを確認したりするなど、クローズ漏れに関してはそのほかの手段を用いる方がよいだろう。
あくまで緊急の手段であることをお忘れなく。
金子 崇之(かねこ たかゆき)
NTTデータ先端技術株式会社 オープンソース事業部所属。
入社よりJavaを用いたWebシステムの開発支援にかかわる。最近では、主にオープンソースのアプリケーションサーバに関する検証や技術支援、トラブルシューティングに明け暮れている
Copyright © ITmedia, Inc. All Rights Reserved.