レベル: 中級 Lan Vuong, Technical Evangelist, IBM
2009年 6月 03日 WebSphere® eXtreme Scale をデータベースとアプリケーションの間の仲介として活用し、アプリケーションのパフォーマンスを最適化する方法を学びましょう。この記事では、ライトビハインド・キャッシュを使用したソリューションと JPA ローダーの概念について、その理論と実装を概説します。次に、ビジネス事例をサンプル・コードと併せて紹介し、これらの機能の利用方法について説明します。
はじめに
アプリケーションは通常、パフォーマンスを高めるために、データ・キャッシュを使用します。特に、読み取り専用トランザクションを主とするアプリケーションの場合には、その傾向が顕著です。こうしたアプリケーションでは、データベースを直接更新してデータの変更を行います。この手法の問題点は、負荷が増大するにつれ、更新における応答時間が長くなることです。データベースは、少数のレコードを扱うトランザクションを大量に並列実行するには適格ではありません。バッチ化したトランザクションを実行する方が、はるかにデータベースに適しています。負荷が増えるにつれ、最終的に、データベースは、CPUやディスクが飽和し、応答時間が長くなります。従来のメモリー内キャッシュにも、JVM の空きメモリーに格納可能な量しかデータを保存できないという限界があります。この量を超えるデータのキャッシングが必要になると、他のデータのスペースを確保しようとキャッシュが連続的にデータを除去する、スラッシングが発生します。そうすると、必要なレコードを絶え間なく読み取らなければならず、キャッシュが無意味になり、データベースには常に読み取り負荷がかかることになります。
この記事では、WebSphere eXtreme Scale を使用することで、JVM クラスターの個々の JVM の空きメモリーを、1 つのキャッシュとして使用する手法を紹介します。この手法を使用すると、JVM の追加に応じてキャッシュの容量を直線的に増加させることができます。JVM の増加が、CPU、メモリー、ネットワークを備えた物理的なサーバーの追加に伴うものである場合には、それによって、直線的なスケーラビリティー、一定した応答時間、および読み取り要求へのサービス提供を実現できます。また、eXtreme Scale のライトビハインド技術を活用することによっても、同様の改善を図ることができます。WebSphere eXtreme Scale は、XTP (エクストリーム・トランザクション処理) シナリオにとって理想的な、直線的なスケーラビリティーを実現します。Gartner は XTP を次のように定義しています。
「パフォーマンス、スケーラビリティー、可用性、セキュリティー、管理容易性、信頼性に関して特別に厳しい要件を持つ分散トランザクション処理アプリケーションの、設計、開発、デプロイメント、管理、保守をサポートするためのアプリケーション・スタイル」
この記事では、データベースとアプリケーションの間の仲介として WebSphere eXtreme Scale を活用し、アプリケーションのパフォーマンスを最適化する方法について説明します。WebSphere eXtreme Scale は可用性に優れた分散型のメモリー内キャッシュであり、アプリケーションのパフォーマンスを高めるための高度な機能を数多く持っています。ライトビハインド機能は、ユーザーの指定する時間間隔で非同期に、バックエンドのデータベースに対する更新をバッチ処理します。このシナリオによる明らかなメリットとして、データベース呼び出しの回数を減少させることができます。それにより、トランザクション負荷が軽減され、グリッド内のオブジェクトへのアクセスが高速になります。またこのシナリオの方が、ライトスルー・キャッシュを使用するよりも高速に応答を得ることができます (ライトスルー・キャッシュのシナリオでは、キャッシュが更新されるとデータベースも即座に更新されます)。ライトビハインドの場合には、トランザクションがデータベースへの書き込み操作の終了を待つ必要がありません。また、変更がデータベースに反映されるまで、メモリー複製によってライトビハインド・バッファーに変更が保持されるため、データベースに障害が起きてもアプリケーションは保護されます。
このインライン・データベース・バッファーには、グリッドとバックエンド・データベースとの間でデータを同期化するためのローダーが必要です。ユーザーが作成した任意のローダーでライトビハインド機能を実現することもできますが、この記事では WebSphere eXtreme Scale に組み込まれている JPA ローダーを使ってこの機能を説明します。JPA (Java Persistence API) 仕様を利用すると、Java オブジェクトとリレーショナル・データベースとをマッピングすることができます。WebSphere eXtreme Scale 6.1.0.3 以降には JPA ローダーが組み込まれています。この JPA ローダーは、JPA 仕様を使ってキャッシュ・データをデータベースのリレーショナル・データに自動的にマッピングします。このローダーと、OpenJPA や Hibernate のようなJPA 準拠のオブジェクト・リレーショナル・マッパーとを組み合わせて使うことができます。
この記事では、ライトビハインド・キャッシュを使用したソリューションと JPA ローダーの概念について、その理論と実装を概説します。次に、ビジネス事例をサンプル・コードと併せて紹介し、これらの機能の利用方法について説明します。
重要な概念と構成
「ライトビハインド」キャッシュとは何か
ライトビハインド・キャッシュでは、データの読み取りと更新のすべてを行います。ただしライトスルー・キャッシュとは異なり、更新が即座にデータ・ストアに伝搬されるわけではありません。ライトビハインド・キャッシュでは更新がキャッシュの中で行われ、ダーティー (コミットされていない) 更新のリストをキャッシュが追跡し、そしてダーティー・レコードの最新セットを定期的にデータ・ストアに書き込みます。さらにパフォーマンスを高めるために、キャッシュは、これらのダーティー・レコードをまとめます。まとめる理由は、バッファリング期間中に同じレコードが何度も更新される場合、つまり何度もダーティー・レコードになる場合には、キャッシュは最後の更新のみを保持するからです。この手法によって、非常に頻繁に値が変更されるシナリオ、例えば金融マーケットでの株価などでのパフォーマンスを大幅に改善することができます。例えば株価が 1 秒間に 100 回変化する場合、通常であればローダーを 30 秒ごとに 30 x 100 回更新する必要があります。しかしダーティー・レコードをまとめることによって、更新は 1 回に減少します。
ダーティー更新のリストは、JVM が終了しても必ず残るように、複製されます。複製レベルは指定可能であり、基本的に同期型と非同期型の 2 つの選択肢があります。同期複製では JVM が終了してもデータが失われる恐れはありませんが、変更を受信したことを複製側が確認するまでプライマリー側が待つ必要があるため、低速です。非同期複製は、はるかに (通常は少なくとも 6 倍) 高速ですが、トランザクションが複製される前に JVM が終了すると、最新のトランザクションは失われる可能性があります。
ダーティー・レコードのリストは大規模なバッチ・トランザクションを使ってデータ・ソースに書き込まれます。データ・ソースが利用できない場合でも、グリッドは要求の処理を続行し、後で再度書き込みを試みます。グリッドは変更にスケーラブルに対応しながら、一定した応答時間を維持できます。これは、変更がグリッドに対してのみコミットされるため、たとえデータベースがダウンしていてもトランザクションをコミットできるからです。これらのダーティー・レコードがバッファリングされている状態でグリッドの JVM に障害が起きた場合には、グリッドは自動的にフェイルオーバーし、バックアップ・サーバー側で書き込みを再試行します。また、1 回目の障害発生後にグリッドは追加の複製を作成し、2 回目の障害が起きた際にダーティー・レコードのリストが失われるリスクを軽減します。この手法で主な課題は、データベースが常時最新に保たれるわけではない点ですが、このスタイルのグリッドを使って大量のデータをグリッドの速度で前処理しておき、データ・ソースに書き込んでから後で処理やレポート作成を行う、という場合には、問題にならないでしょう。
ライトビハインド・キャッシュはすべての状況に適しているわけではありません。ライトビハインドの性質として、ある短期間、ユーザーにはコミットされたと見える変更がデータベースには反映されていないということが起こります。この時間遅延は、キャッシュ書き込み遅延 (cache write latency) またはデータベースの失効 (database staleness) と呼ばれます。データベースの変更と、その変更を反映して更新される (または無効化される) キャッシュの間の遅延は、キャッシュ読み取り遅延 (cache read latency) またはキャッシュの失効 (cache staleness) と呼ばれます。システムのすべての部分がキャッシュを介して (例えば共通のインターフェースを使って) データにアクセスする場合は、キャッシュは常に正しく最新のレコードを保持するため、ライトビハインドの使用に適しています。ライトビハインドを使用するシステムでは、すべての変更がキャッシュを使って行われ、その他のパスがないことが前提です。
ライトビハインド機能では、スパース・キャッシュまたは完全キャッシュのいずれかを使用します。スパース・キャッシュはデータのサブセットのみを保管し、データの追加は遅れて行われます。スパース・キャッシュは通常、キーを使ってアクセスされます。これはキャッシュの中のすべてのデータを利用できるわけではないからです。そのため、このキャッシュを使ってクエリーを実行することはできません。完全キャッシュはすべてのデータを含みますが、最初のロードに長い時間がかかる場合があります。この 2 つの選択肢の中間に位置する、第 3 の方法があります。この方法では、データのサブセットを短時間でキャッシュにプリロードし、その後、遅れて残りのデータをロードします。プリロードされるサブセットはレコード総数の約 20% ですが、このサブセットで 80% の要求に対応することができます。
通常、このような方法で WebSphere eXtreme Scale を使用するアプリケーションは、パーティション化が可能なデータ・モデルを、単純な CRUD (Create、Read、Update、Delete) パターンを使ってアクセスするシナリオでのみ使用されます。
ライトビハインド機能を構成する
ライトビハインド機能を有効にするためには、objectgrid.xml 構成の中で、下記のように backingMap 要素に writeBehind 属性を追加します。このパラメーターの値には、[T(time)][;][C(count)] という構文を使用して、データベースの更新をいつ行うかを指定します。指定された秒数が経過するか、またはキュー・マップでの変更の数が count の値に達すると、永続ストアに更新が書き込まれます。
リスト 1. ライトビハインドの構成例
<objectGrid name="UserGrid">
<backingMap name="Map" pluginCollectionRef="User" lockStrategy="PESSIMISTIC"
writeBehind="T180;C1000"/>
|
JPA ローダーとは何か
WebSphere eXtreme Scale をメモリー内キャッシュとして使用する場合には、データベースとの間でデータを読み書きするためのローダーが必要です。WebSphere eXtreme Scale 6.1.0.3 以降のバージョンには 2 つのローダー (JPALoader と JPAEntityLoader) が提供されています。これらのローダーが JPA プロバイダーとやり取りし、リレーショナル・データを ObjectGrid マップにマッピングします。JPALoader は POJO を保管するキャッシュで使用され、JPAEntityLoader は ObjectGrid エンティティーを保管するキャッシュで使われます。
JPA ローダーの構成
JPA ローダーを構成するには、objectgrid.xml を変更し、そして META-INF ディレクトリーに persistence.xml ファイルを追加する必要があります。
また、トランザクション・コールバックも定義する必要があります。トランザクション・コールバックはトランザクションのコミット・イベントまたはロールバック・イベントを受信し、それらのイベントを JPA レイヤーに送信します。トランザクション・コールバックを構成するには、objectGrid 定義に JPATxCallback という Bean を追加します。persistenceUnitName プロパティーは、persistence.xml の中のJPA エンティティーのメタデータの場所を指定します。ローダーを構成するには、JPALoader または JPAEntityLoader という Bean を追加します。JPA ローダーには entityClassName プロパティーが必須となります。
リスト 2 は objectgrid.xml
の例を示しています。
リスト 2. objectgrid.xml の例
<?xml version="1.0" encoding="UTF-8"?>
<objectGridConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ibm.com/ws/objectgrid/config ../objectGrid.xsd"
xmlns="http://ibm.com/ws/objectgrid/config">
<objectGrids>
<objectGrid name="UserGrid" txTimeout="30">
<bean id="TransactionCallback"
className="com.ibm.websphere.objectgrid.jpa.JPATxCallback">
<property name="persistenceUnitName" type="java.lang.String"
value="userPUDB2"/>
</bean>
<backingMap name="Map" pluginCollectionRef="User" lockStrategy="PESSIMISTIC"
writeBehind="T180;C1000"/>
</objectGrid>
</objectGrids>
<backingMapPluginCollections>
<backingMapPluginCollection id="User">
<bean id="Loader" className="com.ibm.websphere.objectgrid.jpa.JPALoader">
<property name="entityClassName" type="java.lang.String"
value="com.ibm.personalization.model.User"/>
</bean>
</backingMapPluginCollection>
</backingMapPluginCollections>
</objectGridConfig>
|
ローダーを構成するには persistence.xml ファイルを使います。このファイルはアプリケーションの META-INF フォルダーの中に保存されている必要があります。この構成ファイルによって、パーシスタンス単位に対する特定の JPA プロバイダーを、プロバイダー特有のプロパティーと併せて指定します。
リスト 3 は OpenJPA プロバイダーを使った persistence.xml の例です。
リスト 3. persistence.xml の例
<?xml version="1.0"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="1.0">
<persistence-unit name="userPUDB2">
<provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
<class>com.ibm.personalization.model.User</class>
<class>com.ibm.personalization.model.UserAccount</class>
<class>com.ibm.personalization.model.UserTransaction</class>
<properties>
<property name="openjpa.ConnectionURL" value="jdbc:db2://myserver:50001/userdb" />
<property name="openjpa.ConnectionDriverName" value="com.ibm.db2.jcc.DB2Driver" />
<property name="openjpa.ConnectionUserName" value="db2inst1" />
<property name="openjpa.ConnectionPassword" value="mypassword" />
<property name="openjpa.jdbc.DBDictionary" value="db2"/>
<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema"/>
<property name="openjpa.Log" value="DefaultLevel=WARN, MetaData=INFO,
Runtime=INFO, Tool=INFO, JDBC=INFO, SQL=WARN, Enhance=INFO"/>
</properties>
</persistence-unit>
</persistence>
|

 |
ビジネス事例を検証する
ある架空のオンライン・バンキング用 Web サイトでは、ユーザーの増加に伴い、応答時間の悪化や、環境のスケーラビリティーの問題が発生しています。この銀行では、既存のハードウェアを使って顧客ユーザーをサポートする方法を必要としています。ここからは、この事例を検証し、このサイトの抱える問題を解決する上でライトビハインド・キャッシュ機能がどのように役立つかを見てみましょう。
事例: ポータルをパーソナライズする
この銀行では、ユーザー・プロファイルの情報をデータベースから直接取り出す代わりに、データベースにあるプロファイルをキャッシュにプリロードしておくようにします。これは、キャッシュがデータベースに代わって読み取り要求をサービスするということです。中には、こうしたシナリオで 100 Gb をはるかに超えるレコードをキャッシュにロードしているお客様も存在します。従来のシステムでは、プロファイルの更新もデータベースに直接書き込んでいました。この手法ではデータベース・マシンが飽和してしまうため、適切な応答時間で 1 秒間に並列に更新できる件数が制限されていました。
新しいシステムでは、プロファイルの変更をグリッドに書き込んでおき、これらの変更を後から、ライトビハインド技術を使ってデータベースに書き込みます。この手法では、こうした変更に対し、グリッドによる通常のサービス品質とパフォーマンスが提供され、単一インスタンスのデータベースがプロファイルの読み書き操作から完全に分離されます。そうするとこの銀行は、単純に JVM またはサーバーをグリッドに追加するだけでプロファイル・サービスをスケールアップすることができ、しかも応答時間を一定に維持しながらスループットを直線的に増加させることができます。バックエンドに送信されるトランザクションは大幅に減少するため、もはやデータベースがボトルネックとなることはありません。応答が速くなることによってページのロードが速くなり、ユーザー・エクスペリエンスが改善される上、プロファイル・サーバーをコスト効率よくスケールアップできます。また、データベースが SPOF (Single Point of Failure) ではなくなるため、可用性も向上します。グリッドは一般的な障害からであれば 1 秒以内に回復することができます。また、それにより影響を受けるのは、障害の発生したサーバー上のデータのサブセットのみにとどまるため、他のデータはそのまま利用することができます。
この事例では、DB2® データベースを OpenJPA プロバイダーと組み合わせて使っています。このシナリオのデータ・モデルは User であり、UserAccounts および UserTransactions とOneToManyの関係を含んでいます。この関係を示す User クラスの部分をリスト 4 に示します。
リスト 4: この事例のエンティティーの関係
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = { CascadeType.ALL })
private Set<UserAccount> accounts = new HashSet<UserAccount>();
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = { CascadeType.ALL })
private Set<UserTransaction> transactions = new HashSet<UserTransaction>();
|
ステップ 1. データベースにデータを追加する
サンプル・コードには、ユーザー・データをデータベースにロードする PopulateDB クラスが含まれています。DB2 データベースへの接続情報は、先ほど示した persistence.xml の中で定義されています。persistence.xml の中に記述されたパーシスタンス単位名を使って EntityManagerFactory という JPA を作成します。次に User オブジェクトを作成し、これらのオブジェクトをバッチでデータベースに永続化します。
リスト 5 は、このフローを示す、コードの抜粋です。
リスト 5: データベースにデータを追加する例
javax.persistence.EntityManagerFactory emf = null;
synchronized (PopulateDB.class) {
emf = Persistence.createEntityManagerFactory(puName);
}
javax.persistence.EntityManager em = emf.createEntityManager();
for (int i = start; i < end; i++) {
if ((i - start) % BATCH_SIZE == 0) {
em.getTransaction().begin();
}
User user = createUser(i, totalUsers);
em.persist(user);
if (((i - start) + 1) % BATCH_SIZE == 0) {
em.getTransaction().commit();
em.clear();
}
}
if (em.getTransaction().isActive()) {
em.getTransaction().commit();
em.clear();
}
|
次に、下記のコマンドを使ってデータベースにデータを追加します。
$JAVA_HOME/bin/java -Xms1024M -Xmx1024M -verbose:gc -classpath $TEST_CLASSPATH ogdriver.PopulateDB 1000000 5
ステップ 2. キャッシュをウォームアップする
データベースにデータを追加したら、データ・グリッド・エージェントを使ってキャッシュにデータをプリロードします。クライアントとサーバー間の往復を極力減らすため、キャッシュへのレコードの書き込みはバッチで行います。また、ウォームアップ時間を短縮するには複数のクライアントを使用する必要があります。すべてのレコードのサブセットである「ホットな」データ・セットでキャッシュをウォームアップしておき、残りのデータは遅延ロードします。キャッシュへのプリロードによって、キャッシュ・ヒット率が高まり、バックエンド層からデータを取得する必要性が低下します。この例では、実行時間を短縮するためにデータベース・レコードに一致するデータをキャッシュに挿入しており、データベースからはロードしていません。
リスト 6 はグリッドへのバッチ挿入を示しています。
リスト 6: キャッシュにデータをプリロードする例
public void putAll(Map<K,V> batch, BackingMap bmap) throws Exception {
Map<Integer, Map<K,V>> pmap = convertToPartitionEntryMap(bmap, batch);
Iterator<Map<K,V>> items = pmap.values().iterator();
ArrayList<Future<Boolean>> results = new ArrayList<Future<Boolean>>();
while(items.hasNext()) {
Map<K,V> perPartitionEntries = items.next();
// we need one key for partition routing
// so get the first one
K key = perPartitionEntries.keySet().iterator().next();
// invoke the agent to add the batch of records to the grid
InsertAgent<K,V> ia = new InsertAgent<K,V>();
ia.batch = perPartitionEntries;
Future<Boolean> fv = threadPool.submit(new
InserterThread(bmap.getName(), key, ia));
results.add(fv);
}
Iterator<Future<Boolean>> iter = results.iterator();
while(iter.hasNext()) {
Future<Boolean> fv = iter.next();
Boolean r = fv.get();
if(r.booleanValue() == false) {
throw new RuntimeException("Put failed");
}
}
}
|
キャッシュへのプリロードを行うには、下記のサンプル・コマンドを使います。
$JAVA_HOME/bin/java -Xms1024M -Xmx1024M -verbose:gc -classpath $TEST_CLASSPATH ogdriver.ClientDriver –load -m Map -n 1000000 -g UserGrid -nt 5 -r 1000 -t 200000 -c $CATALOG_1
ステップ 3. グリッドに対する負荷を生成する
サンプル・コードに含まれているクライアント・ドライバーは、グリッドへの操作を再現することで、ライトビハインド・キャッシュ機能によってパフォーマンスがどのように向上するかを示します。このクライアントには、負荷を調整するためのオプションがいくつか用意されています。下記のコマンドは、スレッド当たりの要求数 200 の割合で 10 本のスレッドを使って、50 万件のレコードを「UserGrid」というグリッドにロードします。
$JAVA_HOME/bin/java -Xms1024M -Xmx1024M -verbose:gc -classpath $TEST_CLASSPATH ogdriver.ClientDriver -m Map -n 500000 -g UserGrid -nt 10 -r 200 -c $CATALOG_1
利用可能なオプションはすべて、Options クラスにドキュメント化されています。
結果を検証する
ライトビハインド機能を使用することによって、確実にパフォーマンスを改善することができます。ここではサンプル・コードを実行し、ライトスルーを使った場合とライトビハインドを使った場合の応答時間とデータベースの CPU 使用率を比較しました。そのために、データベースの中のレコードと一致するデータをキャッシュに挿入しています。こうすることで、ウォームアップ時間を不要にして読み取り応答時間を一定にし、書き込み応答時間を比較できるようにしています。図 1 と図 2 は、ライトスルーとライトビハインドそれぞれの読み取り応答時間と書き込み応答時間を示しています。この 2 つの図を見ると、ライトスルー・シナリオでは更新に対する応答時間が長く、一方ライトビハインド・シナリオでは、更新時間は読み取り時間とほとんど同じです。さらに JVM を追加することによってキャッシュの容量を増加させることができ、その場合でも応答時間が変化することはありません。これは、もはやデータベースにボトルネックが存在しなくなったためです。
図 1. ライトスルー・キャッシュのシナリオでの応答時間のチャート
図 2. ライトビハインド・キャッシュのシナリオでの応答時間のチャート
図 3 と図 4 はデータベースの CPU 使用率を示しています。この 2 つの図から、ライトビハインドを使用した場合にはバックエンドの負荷が軽減されていることがわかります。ライトスルー・シナリオではバックエンドに負荷がかかったままになっているのに対し、ライトビハインドの場合には CPU 使用率が低く抑えられており、バックエンドには、バッファーがフラッシュされる時間間隔でのみ負荷がかかります。書き込みトランザクションの比率、同一レコードを更新する頻度、そしてデータベース更新の遅延を考慮し、環境にとって最適となるようにライトビハインドの構成を調整する必要があります。
図 3. ライトスルー・キャッシュのシナリオでのデータベースの CPU 使用率を示すチャート
図 4. ライトビハインド・キャッシュのシナリオでのデータベースの CPU 使用率を示すチャート
まとめ
この記事では、ライトビハインド・キャッシュのシナリオ、JPA ローダー、バッチ・エージェントによるプリロードについて概説し、こうした WebSphere eXtreme Scale の機能を利用することで XTP (エクストリーム・トランザクション処理) ソリューションを実現する方法を紹介しました。ライトビハインド・キャッシュ機能によって、バックエンドの負荷の軽減、トランザクションの応答時間の短縮、およびバックエンドの障害とアプリケーションとの分離が可能となります。こうした利点、そして構成の単純さから、ライトビハインド・キャッシュは非常に強力な機能と言うことができます。
謝辞
この記事の執筆にご協力いただいた、Billy Newport、Jian Tang、Thuc Nguyen、Tom Alcott、Art Jolin の各氏に感謝いたします。
サンプル・コードを使って作業を開始するために
ダウンロード・ファイルの内容
必要なライブラリー
- WebSphere eXtreme Scale の試用版のダウンロード
- OpenJPA
- args4j JAR
- DB2
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sample code for this article | OGClientDriver.zip | 70KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Lan は Penn State University をコンピューター・サイエンスの学位で卒業し、WebSphere Extended Deployment 開発チームのメンバーとして IBM に入社しました。現在は、XTP の技術エバンジェリストとして活動しています。 |
記事の評価
|