近年のアプリケーションのほとんどがデータを永続化するのにデータベースに依存しています。しばしばデータベース・アクセス・レイヤは性能上の深刻な問題の原因となることがあります。データベースに関する問題が発生した際に多くの人はデータベース自身を調査し始めます。適切なインデックスの作成とデータベースの構造は十分な性能を得るのに不可欠です。しかし、しばしばアプリケーション・レイヤが貧弱な性能や拡張性の原因であることがあるのです。
アプリケーション・レイヤはデータベース・アクセスを制御します。このレイヤにおける問題はデータベース自身で補えるものではありません。従ってデータ・アクセス・ロジックを適切に設計することは性能と拡張性を得るのに不可欠になります。データベース駆動のアプリケーションにはほぼ無限の用途がありますが、その問題はいくつかのアンチ・パターンにまとめることが出来ます。アプリケーションが以下のアンチ・パターンに該当するかどうか分析しそれを解決することでより少ない努力で高性能な拡張性の高いソフトウェアを作ることが出来るでしょう。
O/Rマッパの誤用
O/Rマッパは近代のデータベース・アプリケーションの中心的な要素となりました。O/Rマッパによってオブジェクト指向ソフトウェアはリレーショナル・データベースにアクセスしてデータ変換する苦労から開放されます。またアプリケーションのプログラムからデータ・アクセスに関する複雑さを隠蔽してくれます。これによって開発者がインフラ周りの詳細ではなく実際のアプリケーションのロジックに集中出来ることにより高い生産性が達成されます。背後で何が行われているのかを知ることなく複雑なデータ・グラフをオブジェクト・リレーショナル・レイヤで簡単に生成することが出来るのです。このことはしばしばこれらのフレームワークのおかげでデータ・アクセス・ロジックの設計が不要になるという誤解を招く原因となります。
開発者はよく自分が使っているデータ・アクセス・フレームワークは正しい処理を行っていると考えがちです。ところが、内部の動きを知ることなくO/Rマッパを使うことはしばしば貧弱なアプリケーション性能を招く原因となっています。そこには主に2つの中心的な誤解があります。ロードの振る舞いとロードの頻度です。
O/Rマッパはオブジェクトごとにデータをロードします。これはつまりオブジェクトが要求されるかアクセスされた際に必要なSQL文が生成され実行されると言うことです。この原理はとても一般的で一見するとほとんどの場面でうまく機能するように思えます。と同時に、これが実は性能や拡張性の問題の原因となっていることが多いのです。
簡単な例を見てみましょう。住所録を保管するデータベースで人を保持するテーブルと住所を保持するテーブルがあるとします。ここでもし、各人の名前と住んでいる場所を知りたいと思ったら人についてイテレート処理を行いそれぞれの住所情報を取得しなければなりません。下図は組込まれているクエリ機構を使った場合の結果を示しています。この単純な用途によって多くのデータベース・クエリが発生していることがわかります。
この結果がO/Rマッパに関する2つ目の重大事項を生じさせます。ロードの頻度の問題です。O/Rマッパ指定されない限りなるべく遅いタイミングでデータをロードしようとします。この振る舞いは遅延ロード(レイジー・ロード)と呼ばれます。遅延ロードによってデータは可能な限り遅いタイミングでロードされるので可能な限り最小のクエリ発行と不要なオブジェクトの削減がなされます。一般的にこの方法はよく出来た方法ですが、深刻な性能問題とデータベース・コネクションが存在しない場合にデータにアクセスした際にLazyLoadingExceptionと呼ばれる現象を発生させることがあります。
上で述べたような状況の場合は特化したクエリを使うことでデータのロードの問題と性能の問題を大幅に改善することが出来ます。
つまり、確かにO/Rマッパはデータ・アクセスの開発を大きく補助してくれますが、引き続き適切なデータ・アクセス・ロジックを設計すると言う手間は残っているのです。このような場合、dynaTraceのような動的な検証ツールを使うことでアプリケーションにおける性能上の弱点を特定し、積極的にそれらを解決するのに役立つでしょう。
必要以上のデータをロードする
データベース・アクセスにおけるもう一つのアンチ・パターンでよく見られるのが実際に必要とする以上のデータがロードされているというものです。これにはいくつかの原因があります。RADツールはデータベース構造とユーザ・インタフェース用の部品とを簡単にリンクする方法を提供します。データ・レイヤはドメイン・オブジェクトで構成されるので、しばしば実際に表示されるよりも多くのデータを保持することになります。再び住所録の例を見てみましょう。今回は人の氏名と出身国が表示されるとします。これら3つの情報をロードする代わりにaddressオブジェクトとpersonオブジェクトがロードされます。この結果データベース、ネットワークそしてアプリケーションのレベルで重大なオーバーヘッドが発生します。特化されたクエリを使用することで取得されるデータ量を大幅に削減することができます。しかしそれによって性能が改善される同時に保守の複雑さが増大してしまいます。テーブルに新たなカラムを追加するとデータ・アクセス・レイヤでいくつかの変更が必要となってしまいます。
このアンチ・パターンはしばしばサービス・インタフェースが適切に設計されていない場合にも見られます。サービス・インタフェースはよく多様なユース・ケースに対応するのに十分なほど一般的になるように設計されます。これはサービスが多くのユース・ケースで使えるような少ない制約しかもたないという利点になります。ユース・ケースの追加はバックエンドにあるサービスの実装よりも速いペースで発生するのです。この結果、多くの場合に適切でないサービス・インタフェースを使うことになります。すると開発者はとても効率の悪いデータ・アクセス・ロジックを使うことを強いられるのです。この問題はしばしばデータ駆動のWebサービスで発生します。
これらの問題を解決するためデータ・アクセスのパターンは開発の最中継続的に分析するべきです。アジャイルな開発手法を使う場合、各ユーザ・ストーリーの最後にデータ・アクセス・ロジックを確認するべきです。加えてデータ・アクセスのパターンをアプリケーションの使用方法に対しても分析して、開発の期間中ずっとデータ・アクセス・ロジックが最適可能なようにしなければなりません。
リソースの不適切な使用
データベースはアプリケーションのリソースの中でボトルネックとなりますので、可能な限りその利用は少なくするべきです。にもかかわらずデータベース・コネクションの利用についてはほとんど注意がはらわれていないことがよくあります。他の共有リソース同様このようなコネクションはシステム全体の性能に大きな影響を与えます。とりわけWebアプリケーションやO/Rマッピング・フレームワークの遅延初期化を使ったアプリケーションは必要以上に長い期間データベース・コネクションを保持しておく傾向があります。コネクションは処理の開始時に獲得されレンダリングが完了するかそれ以上のデータ・アクセスが要求されなくなるまで保持されます。O/Rマッパを使ったアプリケーションでは、煩雑な遅延初期化を避けるためにこのコネクションを保持しておきます。データ・アクセス・ロジックを再設計して(レンダリングのような)後続処理と分離することによってアプリケーションの性能と拡張性は劇的に改善されます。
下図のグラフは並行稼働する10個のデータ処理スレッドのレスポンス・タイムを示したものです。最初の部分は1個のデータベース・コネクションを共有した場合です。二つ目の部分は10個のコネクションを使った場合です。三つ目のシナリオでは二つのコネクションを使うものの、2/3の処理はコネクションが戻されてから実行しています。データ・アクセスをうまく設計すれば三つ目のシナリオによって10個のリソースを使った場合と同程度の性能が得られるのです。
十把一からげ
十把一からげというアンチ・パターンは一般的にアジャイルな開発を行うチームでよく見られます。このアンチ・パターンの特徴は、まず主要な機能が開発された後、全てのデータ・アクセスがまるで何の違いもないかのように先行機能と同様に扱われるといったものです。しかし、異なるデータ型やクエリを適宜使うことでアプリケーションの性能と拡張性は飛躍的に改善できるはずです。
データはそのライフサイクルの特徴に応じた分析を行うべきです。どの程度の頻度で変更されるのかあるいは更新可能なのか読み取り専用なのか。データへのアクセス頻度は、そのアクセス・パターンと同様キャッシュを使うかどうか判断する際のヒントとなります。アクセス頻度はさらにどこを最適化するのが効果的であるかというヒントもくれます。これによって早計で不必要な最適化を避けることができ、パフォーマンス・チューニングの効果が最大となることが保証されます。
データの利用パターンを分析することはさらにデータ・アクセス・レイヤのチューニングにも役立ちます。どのデータが使われるのかを知ることは(データの)ロードに関する戦略を最適化するのに役立つのです。ユーザがどのように検索結果を見るかを知ることは、例えば、フェッチ・サイズを最適化するのに役立つでしょう。ユーザが処理内容の詳細を見ているかどうかを知ることは処理状況を遅延ロードとするのか同時にロードするのかを決定するのに役立つでしょう。
データに加えて、クエリも分析し分類しなければなりません。重要な要素はクエリの所要時間、実行頻度そしてそのクエリがユーザとのやり取りで使われるのかバッチ処理内で使われるのかということです。トランザクションの特徴はさらにクエリのトランザクション分離レベルをチューニングするのに役立ちます。
例えばユーザから要求された短時間で終わるクエリとレポート作成のための長時間かかるクエリを同じコネクションで実行するとユーザの操作性は簡単に悪化してしまいます。エンド・ユーザがのクエリを切望しても長時間かかるレポート出力用のクエリはデータベース・コネクションを獲得し続けるのです。異なるクエリのタイプごとに異なるコネクション・プールを使うことでエンド・ユーザの操作性がよくなるということが断言できます。必要のない場面でクエリのトランクザクション分離レベルを下げることも性能と拡張性を格段に改善するでしょう。
悪いテスト方法
最後に、テストの欠如や不適切なテストというのがデータベース接続をするアプリケーションにおける性能と拡張性の問題の主要因の一つとなっています。私は最近この話題について話す機会があり、聴衆にデータベースへのアクセスがアプリケーションの性能上の問題となっていると考えているか訊ねてみました。全員が性能上の問題となっていると認めたにもかかわらず、その場の誰一人としてデータ・アクセスの性能をテストしていませんでした。つまり重要な話題であると考える一方でその部分に注力していないということです。
しかし、例えテストをしていたとしてもテストが適切に行われているとは限りません。コーディングが完了した直後からデータ・アクセス・ロジックには多くの問題が見られるにもかかわらず、テストはテスト・フェーズというずっと後の段階にならないと実行されないのです。このことによって不必要な高いコストを強いられることになるのです。というのも修正は後半になって行われることになり、もしかしたらアーキテクチャの変更によって追加開発やさらなるテストが必要となるかも知れないからです。
さらにテスト・ケースは実世界でのデータ・アクセス・シナリオに沿って設計されなければなりません。データ・アクセスは異なるアクセス・タイプを使って同時実行状態でテストされなければなりません。読み取り/書き込みを組み合わせることによってのみロックや同時実行の問題が特定されます。加えて現実的でないキャッシュへのヒットを避けるために多くの種類の入力データが必要となります。
どのようなデータ・ロードが予想されるか十分な情報が得られないためにどのようなデータ・ロードをテストしたらいいのか分からないといったことがよく起こります。残念ながら私はこのような状況を多く経験しています。しかし、そのことはデータ・ロードと性能の基準を定義しない言い訳にはなりません。どのような基準であれないよりはましなのです。
もし本当に性能特性に関する情報がないのであればデータ・ロードのテストを使ってアプリケーションが飽和状態になるまでロードを増加させていくアプローチが最適でしょう。その結果でアプリケーションの最高性能を定義するのです。この値が適切で現実的なものだと思えばそれでいいでしょう。そうでなければどこまで性能を改善すればいいのか分かっているということです。多くの場合、初期のテストはアプリケーションは期待されているデータ・ロードより少ない要求にしか耐えられない結果を示すでしょう。
結論
データベース・アクセスは近年のアプリケーションの性能と拡張性に最も影響を与える領域のひとつです。フレームワークはデータ・アクセス・ロジックの作成を補助しますが、落とし穴や問題を避けるためには尚もデータ・アクセス・ロジックの設計に十分注意する必要があるのです。重要なのはアプリケーションのデータ・アクセス・レイヤのダイナミックさや特徴について詳細に知るということです。
著者について
Alois Reitbauer氏はdynaTrace Software社でSr. Performance Architectとして働いています。R&D部門の一員としてdynaTrace社の製品戦略に影響を与え、アプリケーションのライフサイクル全体に渡る性能管理ソリューションを提供するために重要顧客に近い位置で働いています。Alois Reitbauer氏はアーキテクトやJava、.NET領域での開発者として10年の経験を有しています。