BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル OpenTerracottaの紹介

OpenTerracottaの紹介

Javaアプリケーションは、書くのもテストするのも、単一のJVM上で動かす場合が一番楽です。しかし、スケーラビリティと高い可用性が要求される場合、Javaアプリケーションを複数のJVM上で走らせる必要が出てきます。この記事では、エンタープライズクラス、かつオープンソースな、JVMレベルのクラスタリングソリューションであるOpenTerracottaをご紹介しようと思います。

JVMレベルのクラスタリングを使用すると、複数のJVMにデプロイされたJavaアプリケーションが、まるで同じJVM上で動作しているかのように相互作用するので、エンタープライズJavaアプリケーションを非常にシンプルにします。

単一のJVMでは、スレッド同士のやり取りは、ヒープ上のオブジェクトに対する変更と言語レベルの並行プリミティブ('synchronized'キーワードとObjectクラスのwait、notify、notifyAllメソッド)を通して行われます。Open Terracottaは、スレッド同士がJVMの境界を越えて相互作用できるよう、言語レベルの機能をクラスタレベルに拡張します。こうしたクラスタリングの機能は、実行時にアプリケーションクラスのバイトコードに対して差し込まれるため、特別なクラスタリング用のAPIを用いてコーディングする必要はありません。

こうしたクラスタリングの機能を使い、Terracottaは以下のようなシナリオで最もよく使用されます。

  • HTTPセッションレプリケーション
  • 分散キャッシュ
  • POJOクラスタリング / Springとの統合
  • コラボレーション、協調、イベント

シンプルな例

では、具体的に説明するために、いくつかのサンプルコードに取り組んでみるとしましょう。サンプルが対象とする領域はショッピングカート通販の小売システムです。アクティブなショッピングカートはいつでも、例えば管理コンソールやレポート用コンソールなどから閲覧することができます。

サンプルコードは単純なJavaのデータ構造を用いて記述されています。いくつかの問題領域は、サンプルを単純にするために理想化されています。例えば、商品、カタログ、顧客、注文などのクラスにカプセル化されたビジネスデータは、現実のシステムではリレーショナルデータベースに保存され、恐らく何らかのオブジェクト/リレーショナルシステムをフロントに配置することでしょう。とはいえ、今回のシステムではショッピングカートのデータは本当にシンプルな Javaオブジェクトで表されており、バックエンドのシステムに保存するといったことは行いません。

Productクラスは、商品の明細に関するデータを内包しています。商品名、SKU、そして価格です。

package example;
import java.text.NumberFormat;
public class ProductImpl implements Product {
private String name;
private String sku;
private double price;

public ProductImpl(String sku, String name, double price) {
this.sku = sku;
this.name = name;
this.price = price;
}

public String getName() {
return this.name;
}

public String getSKU() {
return this.sku;
}

public synchronized void increasePrice(double rate) {
this.price += this.price * rate;
}
}

商品は、カタログ内で商品のSKUをキー、Productのオブジェクトを値としたマップに保持されます。カタログは商品を複数保持し、SKUで商品を見つけ出すことができます(そして恐らくそれらの商品はショッピングカートに入れられます)。以下がカタログのコードです。

package example;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Catalog {

private final Map catalog;

public Catalog() {
this.catalog = new HashMap();
}

public Product getProductBySKU(String sku) {
synchronized (this.catalog) {
Product product = this.catalog.get(sku);
if (product == null) {
product = new NullProduct();
}
return product;
}
}

public Iterator getProducts() {
synchronized (this.catalog) {
return new ArrayList>(this.catalog.values()).iterator();
}
}

public int getProductCount() {
synchronized (this.catalog) {
return this.catalog.size();
}
}

public void putProduct(Product product) {
synchronized (this.catalog) {
this.catalog.put(product.getSKU(), product);
}
}

}

ShoppingCartクラスは利用者が見て、買おうと考えている商品のリストを保持します。

package example;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ShoppingCartImpl implements ShoppingCart {

private List products = new LinkedList();

public void addProduct(final Product product) {
synchronized (products) {
this.products.add(product);
}
}
}

ActiveShoppingCartsと呼ばれるShoppingCartに関連したクラスがあり、アクティブなショッピングカートを保持します。

package example;
import java.util.LinkedList;
import java.util.List;

public class ActiveShoppingCarts {

private final List activeShoppingCarts
= new LinkedList();

public void addShoppingCart(ShoppingCart cart) {
synchronized (activeShoppingCarts) {
this.activeShoppingCarts.add(cart);
}
}

public List getActiveShoppingCarts() {
synchronized (this.activeShoppingCarts) {
List carts
= new LinkedList(this.activeShoppingCarts);
return carts;
}
}
}

カプセル化のためRootsと呼ばれるクラスが存在し、このアプリケーションで使用されるクラスタ化されたオブジェクトグラフへの参照を保持します。このようにオブジェクトグラフのルートを特別なクラスに置くのは必須ではありませんが、このサンプルにおいては便宜的に行います。

package example;

import
java.util.concurrent.CyclicBarrier;
public class Roots {
private final CyclicBarrier barrier;
private final Catalog catalog;
private final ActiveShoppingCarts activeShoppingCarts;

public Roots(CyclicBarrier barrier, Catalog catalog,
ActiveShoppingCarts activeShoppingCarts) {
this.barrier = barrier;
this.catalog = catalog;
this.activeShoppingCarts = activeShoppingCarts;
}

public ActiveShoppingCarts getActiveShoppingCarts() {
return activeShoppingCarts;
}

public CyclicBarrier getBarrier() {
return barrier;
}

public Catalog getCatalog() {
return catalog;
}
}

以下のコードは、これらのクラスがマルチスレッド環境でどのように使用され得るかを表しています。二つのスレッドがrun()メソッドに入ったと仮定すると、様々なポイントでCyclicBarrierを使用してお互い協調しあいます。このサンプルの使用法としては非常に不自然ですが、現実のアプリケーション内でコードがどう動作するかを想像することはできるでしょう。

package example;

import java.util.Iterator;
import java.util.concurrent.CyclicBarrier;

public class Main implements Runnable {

private final CyclicBarrier barrier;
private final int participants;
private int arrival = -1;
private Catalog catalog;
private ShoppingCartFactory shoppingCartFactory;
private ActiveShoppingCarts activeCarts;

public Main(int participants, CyclicBarrier barrier, Catalog catalog, ActiveShoppingCarts activeCarts,
ShoppingCartFactory shoppingCartFactory) {
this.barrier = barrier;
this.participants = participants;
this.catalog = catalog;
this.activeCarts = activeCarts;
this.shoppingCartFactory = shoppingCartFactory;
}

public void run() {
try {
display("Step 1: Waiting for everyone to arrive. I'm expecting " + (participants - 1) + " other thread(s)...");
this.arrival = barrier.await();
display("We're all here!");

String skuToPurchase;
String firstname, lastname;

display();
display("Step 2: Set Up");
boolean firstThread = arrival == (participants - 1);

if (firstThread) {
display("I'm the first thread, so I'm going to populate the catalog...");
Product razor = new ProductImpl("123", "14 blade super razor", 12);
catalog.putProduct(razor);

Product shavingCream = new ProductImpl("456", "Super-smooth shaving cream", 5);
catalog.putProduct(shavingCream);

// I'm going to be John Doe and I'm going to buy the razor
skuToPurchase = "123";
firstname = "John";
lastname = "Doe";
} else {
// I'm going to be Jane Doe and I'm going to buy the shaving cream...
skuToPurchase = "456";
firstname = "Jane";
lastname = "Doe";
}

// wait for all threads.
barrier.await();

display();
display("Step 3: Let's do a little shopping...");
ShoppingCart cart = shoppingCartFactory.newShoppingCart();

Product product = catalog.getProductBySKU(skuToPurchase);
display("I'm adding \"" + product + "\" to my cart...");
cart.addProduct(product);
barrier.await();
display();
display("Step 4: Let's look at all shopping carts in all JVMs...");
displayShoppingCarts();

display();
if (firstThread) {
display("Step 5: Let's make a 10% price increase...");
for (Iterator i = catalog.getProducts(); i.hasNext();) {
Product p = i.next();
p.increasePrice(0.1d);
}
} else {
display("Step 5: Let's wait for the other JVM to make a price change...");
}
barrier.await();
display();
display("Step 6: Let's look at the shopping carts with the new prices...");
displayShoppingCarts();

} catch (Exception e) {
// You wouldn't really do this here.
throw new RuntimeException(e);
}
}

// ... setup and convenience code omitted

public static void main(String[] args) throws Exception {
int participants = 2;
if (args.length > 0) {
participants = Integer.parseInt(args[0]);
}

Roots roots = new Roots(new CyclicBarrier(participants), new Catalog(), new ActiveShoppingCarts());
if (args.length > 1 && "run-locally".equals(args[1])) {
// Run 'participants' number of local threads. This is the non-clustered
// case.
for (int i = 0; i < participants; i++) {
new Thread(new Main(participants, roots.getBarrier(), roots.getCatalog(), roots.getActiveShoppingCarts(),
new ShoppingCartFactory(roots.getActiveShoppingCarts()))).start();
}
} else {
// Run a single local thread. This is the clustered case. It is assumed that main() will be called
// participants - 1 times in other JVMs
new Main(participants, roots.getBarrier(), roots.getCatalog(), roots.getActiveShoppingCarts(),
new ShoppingCartFactory(roots.getActiveShoppingCarts())).run();
}

}

}

今までのところ、このコードは単一のJVM上では正しく動作します。複数のスレッドがCatalog、Product、ShoppingCart、 Customer、Orderといった単純なPOJOと共に相互作用し、必要に応じて標準Javaライブラリに含まれる java.util.concurrent.CyclicBarrierを用いてお互いと協調します。

しかし、もしこれが単なるサンプルアプリケーション以上の重要なコードであったら、恐らく、高い可用性を得るため、そして単位時間当たりの処理量としてのスケーラビリティを向上させるためさらなるサーバを追加するという選択肢のために、二つ以上の物理的なサーバにデプロイしたいと考えるでしょう。サーバを追加すると、単一のJVMへのデプロイ時には存在さえしていなかったたくさんの要件が露見します。

  • 全てのアクティブなショッピングカートが、全てのJVM上で利用可能である必要があります。従って顧客のショッピングカートは、カート内のアイテムが失われること無くどのサーバからも見えます。
  • 全てのアクティブなショッピングカートを見ると言うことは、全てのJVM上の全てのアクティブなカートにアクセスする必要があります。.
  • CyclicBarrierを用いた例のコードで見たようなスレッドの相互作用は、複数のJVM上のスレッドに拡張される必要があります。
  • カタログのデータが大きくなりすぎた場合、RAM内に収まりきらなくなるかも知れません。必要に応じて商品データベースから検索することができますが、データベースがボトルネックになるでしょう。データベースのボトルネックを軽減するためにキャッシュが用いられたとすると、各々のJVMがキャッシュにアクセスする必要が生じます。データベースへのクリティカルな負荷を避けるためには、キャッシュはデータベースから一度だけ読み込まれ(JVMごとではなく)、 JVM間で共有されるべきです。

OpenTerracottaを使用すれば、少量のコンフィグレーションとコードの変更なしで、アプリケーションをクラスタにデプロイすることによって発生する全ての要件が満たされます。では、どんなコンフィグレーションを行えばよいのかを少し見てみましょう。

サンプルに対してのコンフィグレーション

コンフィグレーションの最初のステップは、アプリケーション内のどのオブジェクトをクラスタにより共有するのかを決定することです。共有されるオブジェクトグラフは、"ルート"内で宣言された変数として指定されています。ルートのオブジェクトから参照により辿れる全てのオブジェクトは、クラスタ間の全ての JVMで共有され、利用できるようになります。例では、三つのルートがあり、その全てがRootsクラスで宣言されています。これはTerracotta の設定において以下のようになります。

<roots>
<root>
<field-name>example.Roots.barrier</field-name>
</root>
<root>
<field-name>example.Roots.catalog</field-name>
</root>
<root>
<field-name>example.Roots.activeShoppingCarts</field-name>
</root>
</roots>

コンフィグレーションの次のステップは、ロード時にバイトコードインストゥルメントを行うクラスを決定することです。共有されるオブジェクトグラフの一部となるオブジェクトのクラスは全て、クラスのロード時にTerracottaによるバイトコードインストゥルメントを施される必要があります。このインストゥルメンテーション処理により、Teracottaによる透過的なクラスタリング能力がアプリケーションに付与されます。この例で私たちがすべきことは、example.*パッケージ内の全てをインクルードすることです。CyclicBarrierクラスは、インストゥルメントが必要とされるJava ライブラリクラスのコアセットの一部なため、Terracottaにより自動的にインクルードされます。設定のinstrumented-classes セクションは以下のようになります。

<instrumented-classes>
<include>
<!--include all classes in the example package for bytecode instrumentation-->
<class-expression>example..*</class-expression>
</include>
</instrumented-classes>

コンフィグレーションの最後のステップは、どのメソッドに対して、クラスタ内での並行セマンティックスを与えるかを決定することです。この例では、インクルードされた全てのクラスが持つ全てのメソッドに対して"自動ロック"を使用するために、正規表現を使用します。

<locks>
<autolock>
<method-expression>void *..*(..)</method-expression>
<lock-level>write</lock-level>
</autolock>
</locks>

この設定によりTerracottaは、インストゥルメントされた全てのクラスのメソッドの中で全てのsynchronizedメソッドとブロックを探し出し、wait()とnotify()の呼び出しを挿入することで、その意味(synchronized)をクラスタのレベルにまで広げます。

サンプルを実行する

このサンプルのソースコードは以下のリンクからダウンロードできます。

http://wiki.terracotta.org/confluence/display/labs/CatalogExample(英語) 

一度設定が済んでしまえば、コマンドラインかEclipseのプラグインから、クラスタ内でアプリケーションを実行できます。この記事ではEclipseを使いますが、オンラインチュートリアル(サイト・英語)やダウンロードキットにはコマンドラインからTerracottaを実行するサンプルが複数あります。最初のステップは、Terracottaサーバを開始することです。Eclipseでは、Terracottaメニューからこれを行うことができます。

一度サーバが開始されれば、あなたのアプリケーションインスタンス(複数)をスタートすることができます。この例では、Mainクラスを二回実行しなければなりません。

最初のアプリケーションインスタンスが開始したら、コンソールに以下のような出力を見て取れることでしょう。

2007-01-18 15:49:42,204 INFO - Terracotta, version 2.2 as of 20061201-071248.
2007-01-18 15:49:42,811 INFO - Configuration loaded from the file at
'/Users/orion/Documents/workspace/TerracottaExample/tc-config.xml'.
2007-01-18 15:49:42,837 INFO - Log file:
'/Users/orion/Documents/workspace/TerracottaExample/terracotta/client-logs/terracotta-client.log'.
Waiting for everyone to arrive. I'm expecting 1 other thread(s)...

これは、最初のインスタンスのメインスレッドはbarrier.await()でブロックしていることを表しています。barrierは共有されているオブジェクトなため、メインスレッドは他のスレッド(同じJVM、もしくはこのTerracottaクラスタ内にいる他のJVMかは問いません)がbarrier.await()をコールするまでブロックしているのです。その後、アプリケーションインスタンスをもう一つスタートすると、両方のスレッドが先に進めるようになります。二番目のアプリケーションインスタンスを開始すると、コンソールに以下のような出力が行われるはずです(バリアポイントにたどり着くスレッドの順番によっては、多少違う出力になるかもしれません)。

Step 1: Waiting for everyone to arrive. I'm expecting 1 other thread(s)... We're all here!

Step 2: Set Up
I'm the first thread, so I'm going to populate the catalog...

Step 3: Let's do a little shopping...
I'm adding "Price: $12.00; Name: 14 blade super razor" to my cart...

Step 4: Let's look at all shopping carts in all JVMs...

==========================

Shopping Cart
item 1: Price: $12.00; Name: 14 blade super razor
==========================
Shopping Cart
item 1: Price: $5.00; Name: Super-smooth shaving cream

Step 5: Let's make a 10% price increase...

Step 6: Let's look at the shopping carts with the new prices...

==========================

Shopping Cart
item 1: Price: $13.20; Name: 14 blade super razor
==========================
Shopping Cart
item 1: Price: $5.50; Name: Super-smooth shaving cream

他のアプリケーションインスタンスのコンソール出力として、以下のようなメッセージが出力されるはずです。

Step 1: Waiting for everyone to arrive. I'm expecting 1 other thread(s)... We're all here!

Step 2: Set Up

Step 3: Let's do a little shopping...
I'm adding "Price: $5.00; Name: Super-smooth shaving cream" to my cart...

Step 4: Let's look at all shopping carts in all JVMs...

==========================

Shopping Cart
item 1: Price: $12.00; Name: 14 blade super razor
==========================
Shopping Cart
item 1: Price: $5.00; Name: Super-smooth shaving cream

Step 5: Let's wait for the other JVM to make a price change...

Step 6: Let's look at the shopping carts with the new prices...

==========================

Shopping Cart
item 1: Price: $13.20; Name: 14 blade super razor
==========================
Shopping Cart
item 1: Price: $5.50; Name: Super-smooth shaving cream

ステップ1では、異なるアプリケーションインスタンス内の二つのスレッドが、お互いが開始し、同じランデブーポイントに到着するのを待ちます。ステップ2では、最初のスレッドがProductオブジェクトを作成し、クラスタ化されたCatalogに追加します。ステップ3では、各スレッドは、クラスタ化されたCatalogからSKUを用いてProductを取り出します。Productは、一つのJVM上で最初のスレッドがカタログに追加したものですが、自動的に他のJVMからも利用可能になっていることに注意してください。ステップ4では、それぞれのスレッドが、全てのJVM内のアクティブなショッピングカートを走査し、その中身をプリントしています。ステップ5では、最初のスレッドは二番目のスレッドがブロックしている間に、カタログ内の全てのプロダクトを走査しながら、価格を10%値上げします。ステップ6では、両方のスレッドが全てのアクティブなショッピングカートを表示します - 注意すべきは、値上げは一つのスレッドによりCatalogを操作することによって行われ、新しい価格は両方のJVM内の全てのショッピングカートに自動的に反映されていることです。

全てのクラスタ化されたオブジェクトは、Terracotta管理コンソール内でリアルタイムに閲覧することが可能です。各ルートは、プリミティブな値、もしくは参照のツリー構造として見る事ができます。Catalogは以下のように見えます。

'catalog'というHashMapの中のProductオブジェクトも見ることができます。ショッピングカートから参照されている同じProductオブジェクトについても同様です。

このビューは、ShoppingCart内のLinkedListから参照されているProductについての表示です。

Terracottaはどのように動作しているか?

今我々はTerracottaの透過的クラスタリングが動作するところを見てきました。それがどう動作しているのかについて詳しく論じていきます。

アーキテクチャ

Terracotta は、ハブ-アンド-スポーク式(中央が全て管理、制御を行う)のアーキテクチャを用いています。クラスタ化されたアプリケーションが走るJVMは、スタート時に中央のTerracottaサーバに接続します。Terracottaサーバはオブジェクトデータを蓄えておき、JVM間のスレッド並行処理を調停します。アプリケーションJVM内のTerracotta DSOライブラリは、クラスがロードされるときにバイトコードインストゥルメンテーションを行って、オブジェクトのデータや、同期境界におけるロックとアンロック(アプリケーションJVMとTerracottaサーバの間でのwait()notify()の呼び出しも同様に)を転送します。

クラスタのインジェクションとバイトコードインストゥルメンテーション

クラスタリングの振る舞いは、アプリケーションクラスがJVMにロードされるとき、バイトコードインストゥルメントを行うことによってアプリケーションにインジェクトされます。TerracottaはAspectJやAspectWerkzなどの多くのアスペクト指向プログラミングフレームワークと同様のバイトコードインジェクション技術を使用しています。クラスのバイトコードはロード時に横取りされ、Terracottaによって検証されます。バイトコードはJVMによってクラスとして扱われる前に、コンフィグレーションに従って修正されます。

オブジェクトの変更を管理するため、PUTFIELDとGETFIELDバイトコード命令はオーバーロードされます。PUTFIELD命令はクラスタ化されたオブジェクトのフィールドへの変更を保存するようトラップされます。GETFIELD命令は必要に応じてサーバからオブジェクトのデータを検索するようトラップされます。対象のフィールドから参照されているオブジェクトがすでに検索済みで、ヒープ上でインスタンス化されている場合は、サーバからのオブジェクトの検索は行われません。

スレッドの調停を行うために、MONITORENTERとMONITOREXITバイトコード命令はオーバーロードされます。様々なObject.wait()Object.notify()へのINVOKEVIRTUAL命令も同様です。MONITORENTERはオブジェクトのモニタに対するスレッドの要求を意味します。スレッドは、そのオブジェクトに対するロックが取得できるまで、この命令の地点でロックされます。ロックを取得すると、MONITOREXIT命令がそのオブジェクトに対して実行されるまで、スレッドはオブジェクトに対する独占的なロックを保持し続けます。もしそのロック対象のオブジェクトがクラスタ化されていた場合、 Terracottaはそのオブジェクトに対するローカルのロック要求に加えて、そのオブジェクトに対するクラスタワイドの独占的ロックが取得できるまでスレッドがブロックすることを保証します。スレッドがローカルロックを手放す時、クラスタワイドのロックもまた開放します。

サンプルコードでは、example.*パッケージ内の全てのsynchronizedメソッドとsynchronizedブロックは "autolocking"に設定されました。その意味は、MONITORENTERとMONITOREXIT命令が追加されるということです。クラスタリングのために、明示的な同期を行っていないアプリケーションコードをTerracottaのコンフィグレーション内で同期されるメソッドとして宣言することも可能と言うことです。

wait()notify()の呼び出しもインストゥルメントされます。共有オブジェクトにおいてwait()メソッドが呼び出されると、Terracottaサーバは呼び出し元のスレッドを、クラスタ全体でそのオブジェクトについて待っているスレッドの集合に加えます。notify()メソッド(系のどれか)が呼ばれたら、サーバはクラスタ内の適切な数のスレッドに通知が行われることを保証します。notify()が一つのJVMで呼び出された際には、(もしあれば)ウェイトしているスレッドのうち一つを選択し、そのスレッドが通知を受け取った状態にします。notifyAll()が呼び出された際には、全てのJVM上でそのオブジェクトをウェイトしている全てのスレッドに対して通知を行います。

ルート、そしてクラスタ化されたオブジェクトグラフ

クラスタ化されたオブジェクトは、共有されたオブジェクトグラフのルートから始まります。そのルートは、一つまたはそれ以上のフィールドの集合によって識別され、Terracottaコンフィグレーション内で一意な名前を付けられます。例においては、ルートの全てはRootsクラス内で宣言されていました。これは便宜上のことで、あらゆるフィールドはルートとして宣言できます。

ルートが最初にインスタンス化されたとき、ルートオブジェクトと、トップレベルのルートオブジェクトから参照によって到達可能な全てのオブジェクトはクラスタ化されます。それらのフィールドデータはTerracottaサーバに送られ、保持されます。ルートがどこかのJVMで生成されると、フィールドに割り当てられた他の全ての値は無視され、クラスタ化されたルートオブジェクトの値が代わりにそのフィールドに割り当てられます。サンプルでは、二つ目のアプリケーションインスタンスがRootsオブジェクトを生成したときにこれが発生しています。ルートはすでに一番目のアプリケーションインスタンスにより生成されているので、(二番目の)Rootsコンストラクタによって割り当てられたルートフィールドへの代入は無視されます。ソースコードに書かれているとおりに、引数の値をルートフィールドに代入する代わりに、Terracottaの透過的なライブラリがサーバからルートを検索し、ローカルヒープ内でインスタンス化し、その参照を問題となっているルートフィールドに割り当てます。これは、Terracottaの透過的なメカニズムによって生じる、アプリケーションのセマンティクスに対する唯一大きな変更です。

クラスタ化されていないオブジェクトが、クラスタ化されたオブジェクトから参照可能になった際には、その新しいオブジェクトと、そこから参照可能な全てのオブジェクトグラフがクラスタ化されます。オブジェクトはクラスタ化される際、クラスタワイドの一意なオブジェクトIDを割り当てられ、ライフサイクルが終わるまでクラスタ化され続けます。オブジェクトがどのルートグラフからも到達不能になり、かつクラスタに参加しているJVM内のいずれかでインスタンスが消失した場合、サーバ内のクラスタガベージコレクタによって除去される対象となります。

同期化、クラスタ化されたロック、オブジェクトの変更

synchronized メソッド、synchronizedブロック、そしてTerracottaコンフィグレーション内でロック対象であると宣言されたメソッドには、 Terracottaトランザクションの境界が存在します。Terracottaトランザクションの考え方は、JTAトランザクションとは少し違っています。どちらかというとJavaのメモリモデルで使用されているトランザクションのほうにより近似しています。

前述の通り、共有オブジェクトにおけるMONITORENTER命令はクラスタ化されたロックのリクエストを行うよう拡張され、そのオブジェクトに対するローカルロックとクラスタ内でのロックが与えられるまで、呼び出し元のスレッドをブロックするようになります。MONITORENTER命令と対応する MONITOREXIT命令の間で発生したオブジェクトの変更は、Terracottaによりローカルトランザクションに記録・収集されます。 Terracottaにより、クラスタ内の全てのJVMにおいて、特定のオブジェクトロックに関するトランザクション内で行われた全ての変更は、次にいずれかのスレッドがそのロックを取得する(MONITORENTER命令をパスする)前に、ローカルにもその変更が適用されていることが保証されます。トランザクションは、ロック対象となっているオブジェクトに対する変更のみを含むのではなく、あらゆる(共有)オブジェクトが対象となります。

オブジェクトの変更に対するきめの細かいレプリケーション

 オブジェクトに対する変更を保持するトランザクションは、変更されたフィールドのデータのみを含みます。これらのトランザクションは、クラスタの一貫性を保つため、サーバと他のクラスタ化されたJVMに対して送信されます。サーバは、トランザクションに参加しているオブジェクトをヒープ内に保持している他の JVMにのみ、そのトランザクションを送信します。同様に、それらのJVMに対して、トランザクションの一部分のみを必要なだけ送信します。例えば、スレッドがオブジェクト'a'内のフィールド'p'と、オブジェクト'b'内のフィールド'q'を変更したとすると、a.pb.pに関するフィールドのデータだけがトランザクションに投入され、サーバに送られます。サーバはその他のJVM内で、、どのJVMがaもしくはbをインスタンス化しているかを確定します。もしあるJVMがaのオブジェクトはインスタンス化しているがbはしていない場合、そのJVMはa.pのフィールドデータは受信しますが、b.qに関しては受信しません。

我々のサンプルコードでは、Productオブジェクトの価格が更新されたとき、priceフィールドのみがクラスタに向かって送出され、nameやSKUなど変更されていないフィールドについては送られません。

オブジェクトのアイデンティティとシリアライゼーション

Terracotta は、オブジェクトの変更を複製するときにJavaのシリアライゼーションを使用しません。なぜなら、オブジェクトの変更はフィールドレベルで追跡され、トランザクションはオブジェクトグラフの全てではなくオブジェクトの断片のみを含むからです。例では、Productオブジェクトの価格を値上げしたときクラスタに送り出さねばならないものは以下のものだけです - 変更されたオブジェクトのID、オブジェクト内で変更されたフィールドの識別子、そしてフィールドpriceが保持するバイトです。Productオブジェクトの残りの部分は無視されます。JavaのシリアライゼーションはProductオブジェクトの全てのフィールドと、Productオブジェクトが持つ深いオブジェクトグラフ(他のオブジェクトへの参照を多く持ち、さらにそれらが他のオブジェクトを参照している)の全てをシリアライズしてしまいます - 変更の全てはdouble値になります。

Terracotta のアプローチは、シリアライズされるオブジェクトのグラフ全てではなく、変更のあったデータだけをクラスタ全体に伝えればよいため、(Javaの)シリアライゼーションよりも非常に効率的です。しかし、効率性に加えて、オブジェクトのフィールドを変更の単位とすることはアーキテクチャ的に決定的な利益をもたらします:それは、オブジェクトのアイデンティティが維持され続けることです。

もしJavaシリアライゼーションが、クラスタ内の変更通知に使われていたとしたら、変更されたオブジェクトはクラスタ化されたアプリケーションのJVM内でデシリアライズされ、存在しているオブジェクトのインスタンスとどうにかして入れ替える必要があるでしょう。これが、他の多くのクラスタリング/キャッシングテクノロジーがGET/PUT APIを必要とする理由です。クラスタ化されたオブジェクトは、何らかの"GET"呼び出しを使用してクラスタから検索される必要があり、オブジェクトに対する変更があった場合、何らかの"PUT"呼び出しによりクラスタに戻す必要があります。

Terracotta はそうした制限がありません。クラスタ化されたオブジェクトは、他の全てのオブジェクトと同じようにヒープ内に生存しています。ローカルでオブジェクトに対して変更が加えられた場合、それらはヒープ上のオブジェクトに対して変更が行われます。そのオブジェクトに対してリモートで変更が行われた場合、ローカルJVMによりトランザクションを受信し、既にヒープ内に存在しているオブジェクトに対して直接変更が適用されます。これは、ある時点でヒープに存在するクラスタ化されたオブジェクトは、ただ一つのインスタンスしか存在しないと言うことを意味しています(複数のクラスローダーが存在する状況ではもう少し複雑になりますが、それはこの記事の範囲外です)。

Terracotta を使用すれば、オブジェクトの新鮮なコピーを取得するよう気をつける必要はありませんし、オブジェクトに対して何かしたらそれをクラスタに戻すことを念頭においておく必要もありません。そして、コピーが存在しない - クラスタ化されたオブジェクトは、ヒープ上の単なるプレーンなオブジェクトです - ため、クラスタ化されたオブジェクトはその他のあらゆるオブジェクトと同様の振る舞いをします:クラスタ化されたオブジェクトに対して行ったあらゆる変更は、その変更されたオブジェクトへの参照を持つ全てのオブジェクトにとっても有効です。同様に、オブジェクト'bar'への参照'foo'と、同じオブジェクト'bar'への参照'baz'があった場合、foo.equals(baz)のみならず、foo == bazもtrueとなります。

我々の例でも、カタログ内のProductオブジェクトの価格を変更した際、ショッピングカート内のProductオブジェクトも同様に変更されたということからも、オブジェクトのアイデンティティが維持されているのを見ることができました。これは、ヒープ上でCatalog内のProductオブジェクトとショッピングカート内のProductオブジェクトが同一のオブジェクトだからです。

このオブジェクトアイデンティティの維持により、クラスタ化されたマルチJVMアプリケーションが、通常のシングルJVMアプリケーションとほとんど変わらず振舞うようになります。この単純さと、オブジェクトアイデンティティがクラスタを横断して維持されることによるパワーにより、クラスタリングに関する問題と、アプリケーションのデザインや実装の問題とが直交する(混じらない)ようになります。クラスタリングの振る舞いは、JVMレベルで Terracottaの層に押し込められ、インフラに溶け込んでしまいます。ガベージコレクションがメモリ管理をアプリケーションコードから見えなくするのと同様に、Terracottaはクラスタリングや分散コンピューティングの振る舞いを見えないものにします。

仮想ヒープ/ネットワーク接続メモリ

JVM 間でのオブジェクトの共有とスレッド間通信に加えて、Terracottaは非常に大きなオブジェクトグラフのための、ローカルJVMヒープの効率的な利用も行うことができます。共有されたオブジェクトグラフが成長すると、そのうち単一のJVMヒープでは収まりきらなくなることでしょう。 Terracottaは、インスタンスが使用されるパターンに従って、共有されたオブジェクトグラフのローカルインスタンスを切り詰めることで、この問題に対処します。Terracottaはクラスタ化されたオブジェクトグラフに対する設定可能な枠を持っており、ヒープの特定なパーセンテージに収まりきらない部分を、キャッシュポリシーに従って追い出してしまいます。そうして失われてしまった部分が必要になったとき、それらはJVM内でフォルトし、自動的にサーバから取得されます。Terracottaクラスタは、任意に大きな仮想ヒープ、もしくはネットワークに接続されたメモリと考えることができます。.

この機能は、任意の大きなオブジェクトグラフを標準的なヒープサイズに合わせることができます。これはまた、柔軟な実行時のデータ分割が可能と言うことです。我々のサンプルコートにおいて、カタログが非常に大きくなり商品が数十万個になれば、恐らく商品のデータは数ギガバイトに達し、そうしたら何が起こるのかは想像に難くありません。そんな巨大なカタログをメモリに割り当てるだけで数分、もしくは数時間かかってしまうかもしれません。Terracotta を使用しない場合、その全てを単一のJVMが持つヒープに割り当てようとすれば、64ビットOSと4GB以上のRAMを必要とするでしょう。そして、高い可用性のためにそうしたアプリケーションサーバマシンを少なくとも二台は必要とすることでしょう。スケーラビリティを得るため、そうしたアプリケーションサーバマシンをさらに追加する必要が生じることでしょう。Terracottaなしでは、追加されたアプリケーションサーバマシンはそれぞれが Catalogを独立して読み込む必要が生じるでしょう。

Terracotta がネットワーク接続メモリとして動作するおかげで、カタログの全てを、それがいかに大きいかを問題とせず、単一のクラスタ化されたオブジェクトグラフとして扱う事ができます。カタログの生成は一度しか行う必要が無いため、アプリケーションインスタンスを追加するときのスタートアップ時間が劇的に削減され、しかもクラスタ内の全てのメンバが全てすぐに利用可能になります。

いつ、どのようにTerracottaを使うのか?

Terracottaがもっとも有効な四つの主なユースケースがあります。

  • HTTPセッションの複製
  • 分散キャッシュ
  • POJOクラスタリング
  • コラボレーション、協調、イベント

HTTPセッションレプリケーション

恐らく、最も馴染み深いユースケースはスティッキーなロードバランサをフロントに持つ、複数アプリケーションサーバ環境におけるHTTPセッションレプリケーションです。アプリケーションサーバ(複数)のうちの一つが異常終了してもセッションを有効に保ち続けることは、難しく、お金のかかる問題でした。アプリケーションデザインにおける現在のトレンドは、アプリケーションの状態 - 我々のショッピングカートのような - をセッションに保存するのではなく、"ステートレス"なアプリケーションデザインと呼ばれる、アプリケーションの状態をデータベースのようななんらかの外部システムに保存するというものです。

この"ステートレス"なアプローチは、実際にはステートレスではありません - アプリケーションのステートは姿を消しましたが、単にアプリケーションの外に出されただけです。これは、二つのまずい副作用をもたらします。一つはパフォーマンスです:アプリケーションの状態をデータベースに書くことによりデータベースがボトルネックになり、アプリケーションサーバを追加してクラスタの規模を大きくしても、リターンが減少すると言う苦しみを味わうことになります。二つ目は、アプリケーションのプログラミングアーキテクチャが、アプリケーションの状態を外部化するためのAPIで汚染されてしまうことです。

それよりは、セッションスコープのデータをHTTPセッションに単純なJavaオブジェクトとして突っ込むほうがよっぽど楽です。もともとそのようにデザインされているものなのですから。Terracottaによるセッションレプリケーションは、アプリケーションのセッションデータをどこに、どういう形で置いたとしても状態を保ってくれるため、Webアプリケーションのソフトウェアアーキテクチャを単純にしてくれます。

Terracotta によるセッションレプリケーションは、アプリケーションサーバが全てのアクティブなセッションに到達でき、またどこで(セッションが)生成されたかを気にかける必要もないことから、高い可用性をもたらします。セッション内の変更されたデータのみ複製され、必要な場合にのみ送信されるため、よくスケールします。もしセッション内のデータに変更がなければ、データはどこにも書かれることがありません。もしセッション内で一つのバイトに変更があったら、セッショングラフ全体ではなく、そのバイトのみがTerracottaサーバに送られます。もしそのセッションをヒープに持つアプリケーションサーバが他にいなかったら(一般的なケースで、スティッキーなロードバランサをフロントに持つクラスタのような)、他のアプリケーションサーバに変更が送られることはありません。アプリケーションサーバが異常終了した場合のみ、セッションデータは他のアプリケーションサーバに複製されます。それは必要な場合にのみです。

Terracotta は通常のJavaクラスをクラスタ化するため、セッションデータをわざと断片化したアトリビュートに細切れにする必要がありません。セッションオブジェクトは、アプリケーションにあわせてシンプルにも、複雑にもして良いのです。あなたは深いオブジェクトグラフに対して更新を行うことができ、その変更は SessionのsetAttribute()を呼ぶことなく、クラスタで利用可能になります。TerracottaはJavaシリアライゼーションを使用せずにオブジェクトをクラスタ化しているので、セッション内に Serializableを実装していないオブジェクトも入れることができます。例えば、ショッピングカートはそのままセッションに格納することができます。効率のよいレプリケーションのことを考えて、ばらばらのアトリビュートに分割する必要がありません。アプリケーションのどこか、もしくはクラスタのどこかで行われたショッピングカートに対する変更は、setAttribute()の呼び出しなしに自動的に反映されます。

クラスタが動作中の間、クラスタ全体における全てのセッションの内容を見ることができます。開発時には、不慮の事故により、セッショングラフに入るべきではないオブジェクトが入ってしまう、といったセッションの不正利用を見つけるのに役立ちます。本番運用時には、アクティブなセッションがどれだけあるのかをすぐに見ることができ、その変更をリアルタイムに監視することができます。例えば、人々がショッピングカートに入れているものをTerracottaコンソールから見ることができます。

Terracottaは、Struts、Spring Web Flow、Wicketといった人気のあるWebフレームワークと共に動作します。

POJO、Springクラスタリング

サンプルアプリケーション内の全てのオブジェクトは単純なPOJO(Plain Old Java Objects)です。Terracottaは、クラスタでPOJOを扱うのを、単一のJVMでPOJOを扱うのと同じくらいシンプルにします。これは、あなたのアプリケーションがSpringビーンを使用しているときは特に真です。

Terracotta for Springは、Springビーンをクラスタ化するにあたって、Springフレームワークを侵食しません。あなたは、単一のJVMにおける Springアプリケーションを普通に作ったあと、どのSpringアプリケーションコンテキストを、そしてそのコンテキスト内のどのビーンをクラスタ化するか、を定義することができます。Terracottaはそれらのビーンとアプリケーションコンテキストイベントを、透過的に、かつクラスタ上にも関わらず単一JVM時と同じセマンティックスを以ってクラスタ化します。Terracotta for Springはまた、Spring Web Flowとコンティニュエーションについてもサポートしています。この機能は、アプリケーションがTerracottaクラスタ上で動作している場合、 Spring webアプリケーションの対話ステートをフェールオーバーにすることができます。

分散キャッシュ

クラスタ化されたオブジェクトグラフはそのまま優れた分散キャッシュとなります。If 我々の例でカタログデータがデータベースからロードされた場合、クラスタ内のアプリケーションインスタンスごとに一度ではなく、クラスタ全体で一度だけCatalogの割り当てが行われるため、開始時のデータベース負荷を減らします。キャッシュ全体がクラスタ化されたデータ構造と合致しているため、アプリケーションインスタンスは、ヒープに乗らないからといってキャッシュの一部を捨てたり、それがまた必要になったときにデータをデータベースからリロードする、といった必要がありません。

コラボレーション、コーディネーション、イベント

Terracottaのクラスタ化された並行処理機能は、JVM間の理想的なシグナル機能を実現します。サンプルコード内で、CyclicBarrier クラスを異なるJVM上でスレッド間の協調を行うのに使用しました。クラスタ化されたイベントメカニズムも、同様の単純なやり方で実装できるでしょう。

並行度を高める一般的な方法は、一つのマネージャーエンティティが並行動作する複数のワーカにタスクを行わせ、結果を収集することです。このマスタ・ワーカパターンは、実践の用意は、最も一般的な方法の一つです。

このマスタ・ワーカパターンは二つの論理的なエンティティからなります:一つのマスタと、一つ以上のワーカインスタンスです。マスタはタスクの集合を生成して計算処理を開始し、それらをいくつかの共有スペースにおき、ワーカによりそれらのタスクが完了されるのを待ちます。

共有スペースには通常、何らかの共有キューが使用されます。このパターンを使う利点の一つは、アルゴリズムにより自動的に負荷が分散されることです。行うべき仕事の集合が共有されており、行うべき仕事がなくなるまでワーカはその集合から仕事を取り出し続けます。このアルゴリズムは通常、タスクの数がワーカの数をはるかに上回り、かつタスクが完了までに大体同じ時間を要するとき、良いスケーラビリティを得られるという特色を持ちます。.

このパターンを用いた一般的なアプリケーションには、例えば金融リスク分析などのシミュレーション、大きなデータ集合の検索と集約、販売注文のパイプライン処理などがあります。

Java には現在、マスタ・ワーカパターンをベースにした、ワーク/マネージャーシステムの構築を手助けするライブラリが存在します。Terracottaはそうしたシステムをスケールアウトされたクラスタにデプロイすることを可能にし、単一JVM上のワーカだけでなく、JVMクラスタ全体のワーカに作業させることができるようになります。単純に、仕事のキューと結果のキューをクラスタリングするだけで、ワーカが動作するマシンの全てのプロセッサをフルに活用でき、その上、アプリケーションのアーキテクチャに大きな混乱をもたらすことなく、必要に応じてマシンを追加することも可能です。

Terracotta上でワーク/マネージャーアプリケーションを構築するための情報がもっと必要なら、terracotta.orgにあるマスタ・ワーカ チュートリアルを見てください。http://wiki.terracotta.org/confluence/display/orgsite/TutorialTerracottaDsoWorkManager1(英語) 

結論

Open Terracottaは、新しい軽量ソフトウェアスタックの基盤です。JVMレベルのクラスタリングは、Tomcat、Spring、Geronimoのようなオープンソースコンポーネントとオープンソースアプリケーションフレームワークのホスト環境が、共に組み合わさり、エンタープライズクラスの可用性とスケーラビリティを持って配備されることを実現します。そして使うのも容易です。

著者について

Orion LetiziはTerracottaの共同設立者であり、ソフトウェアエンジニアです。JVMレベルクラスタリングのプロジェクトであるOpen Terracottaに従事しています。彼はエンタープライズJavaにここ10年携わっています。Terracotta以前は、Walmart.com のソフトウェアアーキテクトでした。

ダウンロード、更に知りたい、もしくはOpen Terracottaのコントリビュータになりたい等の場合は、http://www.terracotta.org(英語)を訪れてみてください。

原文はこちらです:http://www.infoq.com/articles/open-terracotta-intro

(このArticleは2007年2月12日にリリースされました)

この記事に星をつける

おすすめ度
スタイル

BT