レベルエンター山本大のブログ

面白いプログラミング教育を若い人たちに

BLOCKVROCKリファレンス目次はこちら

UsingTerracottaDSO via TSS

TheServerSide.comに掲載された「UsingTerracottaDSO」の記事の翻訳

Terracottaとは

複数のJavaVM上で同じJavaオブジェクトを共用できるオープンソース製品。
2005年の製品登場時期より150台を超えるWEBコンテナのセッションをクラスタリングする事例が出るなどしていた。2006年12月4日にオープンソース化。

January 2007 (2007年 1月)

Terracotta DSO is an open source technology created by Terracotta, meant to provide clustering to Java at the virtual machine level. It does so by weaving code around specific classes, which will communicate with a specific server process to retrieve and update data as needed.

Terracotta DSOは、JavaVMレベルでクラスタリングする機能を持ったTerracottaを利用したオープンソーステクノロジーです。
この機能は特定のクラスに、必要なデータの取得と更新を独自のサーバープロセスを使って通信するコードを織り込むことによって、動作します。

If this sounds somewhat like what JavaSpaces does... it's not. For one thing, DSO doesn't rely on an API to manipulate the clustered data. For another, DSO is configured slightly differently, with participating JVMs needing to know where the DSO hub is. DSO is also far easier to test from a client-side perspective, as we'll see later in the article.

JavaSpacesの動きとどこか似ていると思われるかもしれませんが、違うのです。一つ挙げれば、DSOは分散するデータが、操作のためのAPIに依存しません。他にも、DSOのハブがどこにあるのかを知る必要があるためにJVMに参加する事など、DSOでは少し異なった設定がされています。DSOはクライアントサイドからのテストがとても容易でもあります。それについては、この後の記事で紹介します。

DSO is deployed as a set of modules that run atop a regular Sun JVM1. A set of scripts to manage the cluster and start the JVM are provided as part of the distribution. The first step DSO users should execute after downloading DSO is running the $DISTRIBUTION/dso/bin/make-boot-jar.bat, where $DISTRIBUTION is the home of the Terracotta installation. This will create a set of modules for use with the current JVM. (Any update to your JVM will require re-running this file.)

DSOは、通常のSun JVM1の上で稼働する1セットのモジュールとして配布されます。クラスタの管理やJVMの軌道のためのスクリプトのセットは、ディストリビューションの一部として提供されます。DSOをダウンロードした後のDSOの最初のステップは、「$DISTRIBUTION/dso/bin/make-boot-jar.bat」を実行することです。(このコマンドの「$DISTRIBUTION」とは、Terracottaをインストールした際のHomeディレクトリです」
このコマンドは、現在実行中のJVMを使って1セットのモジュールを作成します。(JVMをアップデートしたときは必ず、このファイルを再実行してください。)

The next step is to have something worth clustering, which will also show off one of the biggest advantages of DSO: codeless clustering.

次のステップは、クラスタリングの対象を何か作っておくことです。また、これはDSOの最大のメリット(コード不要のクラスタリング)を見せ付けるための対象でもあります。

Reference data isn't a problem for most applications, until it turns out to be megabytes in size. Once it's large enough, there's a huge advantage in preloading it, and calculating slices of cached data at runtime to cut down on how long it takes to access it2. However, in typical deployments, each runtime application has to calculate the reference data and appropriate subsets of information.

ガバイト級のサイズになるまで、参照するデータはほとんどのアプリケーションで問題がありません。サイズが大きくなったときのために、事前ローディングという大きなメリットがあります。そして実行時にキャッシュされたデータを削除するために、どれぐらいの期間アクセスがあるかを計算します。しかしながらほとんどの場合、それぞれのランタイムアプリケーションは参照するデータと情報のサブセットについて計算しなければなりません。

What would be really useful is if we could load the data in one application and have it available in all of the JVMs -- and if one JVM memoized a structure, it'd be nice if that memoization was available to all of the other JVMs, too.

本当に便利なのは、一つのアプリケーションでデータをロードできたとき、すべてのJVMで利用可能にできるかどうかということです。そして、一つのJVMに構造をインスタンス化(memoized)すれば、そのインスタンスはすべてのJVMでも利用可能となるなら、なお良しです。

The best feature of DSO for this structure is that we can code with our normal, non-clustered, JVM, with our regular suite of tools and tests... in other words, as if we weren't clustering the application at all.

DSOの最も優れた機能は、クラスタリングされてないノーマルのJVMでコードを記述することで動作することです。
通常使うツールが使え、通常使うテスト方法が利用出来ます。言い換えれば、アプリケーションのクラスタリングを意識しなくてもよいと言うことです。

What we'll do is fairly simple. This isn't useful in the real world, but we'll build an application that returns sets of words based on strings passed in. At the start, it'll query a URL for the list of words (which is copied from /usr/share/dict/words on my Linux server), then ask for various subsets of words, which it will manually calculate and cache as they're requested.

やるべき事はとてもシンプルです。現実的には便利ではありませんが、渡された文字列から文字の集まりを返すアプリケーションをビルドします。
初めに、単語のリストのURLのアドレスを検索します。(私のリナックスサーバーの/usr/share/dict/wordsからコピーしました)
それから、単語の色々なサブセットを問い合わせます。それらが要求されたら、手作業で演算子してキャッシュします。

We'll then run this process in more than one JVM, to show the clustering in action. The second JVM to run as a DSO process won't have to hit the URL, and won't have to build up every sublist of data, either.

このプロセスを1つ以上のJVMクラスタリングで実行して、走らせてみせましょう。DSOプロセスとして走らせる2つ目のJVMはURLをたたく必要はありません。しかもすべてのサブリストのデータをビルドする必要もありません。

The basic test harness looks like this:

基本的なテストの実装は以下のようになります。

	public static void main(String[] args) {
		WordList list=WordList.getWordlist();
		System.out.println(list.getList("").size());

		Random r=new Random();

		runTest(list, r);
		runTest(list, r);
	}

	private static void runTest(WordList list, Random r) {
		List<String> l=new ArrayList<String>();
		for(char i='a';i<'z'+1;i++) {
			l.add(Character.toString(i));
		}
		while(l.size()>0) {
			String prefix=l.remove(r.nextInt(l.size()));
			long starttime=System.currentTimeMillis();
			int s=list.size(prefix);
			long endtime=System.currentTimeMillis();
			System.out.printf("%s: %d (%d ms)\n", prefix, s, (endtime-starttime));
		}
	}

This isn't horribly exciting: it basically runs two loops, selecting letters at random and showing how long it took to retrieve the sublists from the WordList object.

これは、そんなにエキサイティングなコードではないですね。基本的な2回のループが実行されます。ランダムで文字が選択されます。そして単語リストオブジェクトからサブリストを取得するために、どのぐらいの時間がかかったかを表示します。

The WordList's relevant code looks like this:

単語リストの該当するコードは以下のようなものです。

    public List<String> getList(String prefix) {
        List<String> list = null;
        boolean hasKey;
        synchronized (wordlists) {
            hasKey = wordlists.containsKey(prefix);
        }
        if (hasKey) {
            System.err.println("'" + prefix
                    + "' was requested, and it's been built already.");
        } else {
            buildList(prefix);
        }

        synchronized (wordlists) {
            list = wordlists.get(prefix);
        }
        return list;
    }

There's nothing earth-shattering about this code, either. The synchronization needs a little bit of explanation, though. For one thing, we need to synchronize wordlists to see if it has a key -- simple enough, and very fast (relatively speaking.) If it doesn't find the key, it builds the list, which is a slow process. The buildList() function updates the word map like this:

「synchronization 」の部分は、ちょっとした説明が必要だとは言うものの、このコードでも、あっと驚くようなコードはどこにもありません。
「synchronization」について、一つにはキーを持っているかどうか確認するのにwordlistsを同期する必要があります。
(これは十分簡単で、非常に速い(比較的)処理です) キーを見つけられないならリストを生成します。(これは遅い処理です)。 buildList()機能は以下のように単語Mapをアップデートします:

            synchronized (wordlists) {
                if (!wordlists.containsKey(prefix)) {
                    wordlists.put(prefix, w);
                }
            }

So what's happening here? If two threads look for the key at the same moment, they both might discover that the word list doesn't have their prefix, so they both build it. When they try to add the list to the map, they check to make sure the prefix is still missing. If it's not missing, then we had a thread contention; both processes built the word list, but only one stores it into the map. This has a lot of advantages over being more aggressive about synchronization -- and the cost is relatively small.
We could have built more precise thread control, but it's not necessary. (Well, not for this explanation. In production, you'd probably want better mutex control.)

さて、ここでは何が行われているのでしょう?2つのスレッドが同時にキーを参照しに来たとしたら、どちらのスレッドも単語リストがそれらのプレフィックスを保持していないことを発見します。そして、両方のスレッドが単語リストを生成します。そのリストにMapを追加しようとしたときに、両方のスレッドは、プレフィックスがまだ見つかっていないことを確認しようとします。もし、欠落していなければ
消えていないなら、スレッドの競合がということです。両方のプロセスが単語リストを作成します。しかし、1つだけにMapが格納されます。
これは、沢山のメリットがあります。これには同期に関して、肯定的な意味で多くの利点があります。そしてコストが現実的なほど小さいです。
比較的わずかにですが、より正確なスレッドに制御させるように組み立てることができました。しかし、それは必要ではありません。
(さて、このような説明ではなく、製品では、あなたはたぶんより良いミューテックス(相互排除)コントロールが欲しいでしょう。)

The truly interesting part about this code is that there's absolutely nothing making it look like it's clusterable. It's a simple, single-JVM memoization application. It can be tested in a single JVM, with nothing else involved. Junit or TestNG tests could be written to exercise the cache and the word lists themselves. At no point is any real care taken, beyond the simplest thread safety.

このコードに関して本当に興味深い部分は、まったくクラスタリングがされているように見えないことです。簡単です。一つのJVMがアプリケーションをインスタンス化しているのです。
それは、複雑な事はなしに単一のJVMでテストする事が出来ます。JunitTestNGのテストは、キャッシュを実行し文字リストを自分自身に持つようにかいてださい。
スレッドセーフのための注意は、本当にこれっぽっちも払われてません。

However, if two of these are run at the same time, each will have to build its cache manually, which isn't acceptable for data that can and should be shared between tasks. This is where DSO comes in, by providing a transparent cache mechanism.

しかしながら、それぞれが手動でキャッシュを組み立てる2つのスレッドが同時に走ったとしたら、タスクの間で共有できず、してもいけないデータにはアクセス出来ません。
これは、DSOの透過的なキャッシュメカニズムが機能しています。

DSO requires a "host process," the actual DSO server itself. DSO clients run using a specific batch file provided by Terracotta DSO, "dso-java.bat". So we have four tests ahead of us: one is to run in single JVMs, the next is to run under a single client JVM with the DSO instance, then a run with multiple clients at one time, and then -- lastly -- a run with two clients, running sequentially.

DSOは実際のDSOサーバ自体である「ホストプロセス」を必要とします。 DSOクライアントは、Terracotta DSOによって提供された特定のバッチファイル"dso-java.bat"を使用することで走ります。したがって、4つのテストがあります。一つは単一のJVMで実行することになっていること。次にDSOインスタンスが単一のクライアントJVMの下で動作すること。それから、一度に複数のクライアントが実行する場合。最後にその時は2つのクライアントが連続して動作することです。

Before we can do that, though, we need to build our DSO client configuration. DSO configuration is through a very simple XML file that lists servers and ports used, client log location, instrumented classes, the paths of variable instances that are managed by DSO, and lock conditions.

それをやるまえに、DSOクライアントの設定を行う必要があります。DSOの設定は、利用するサーバーとポートのリスト、クライアントログの場所を記したとてもシンプルなXMLファイルです。

The relevant (client-specific) section of the configuration file for this article looks like the following:

設定ファイルの中で、この記事に該当する(クライアントで特定する)セクションは以下のようになります。

  <application>
    <dso>
      <instrumented-classes>
        <include>
          <class-expression>com.wordcalc.data.WordList</class-expression>
        </include>
      </instrumented-classes>
      <roots>
        <root>
          <field-name>com.wordcalc.data.WordList.instance</field-name>
        </root>
      </roots>
      <locks>
        <autolock>
          <method-expression>java.util.List com.wordcalc.data.WordList.getList(java.lang.String)</method-expression>
          <lock-level>write</lock-level>
        </autolock>
        <autolock>
          <method-expression>void com.wordcalc.data.WordList.buildBaseWordlist()</method-expression>
          <lock-level>write</lock-level>
        </autolock>
        <autolock>
          <method-expression>void com.wordcalc.data.WordList.buildList(java.lang.String)</method-expression>
          <lock-level>write</lock-level>
        </autolock>
      </locks>
    </dso>
  </application>

Most of this is very simple, with only the autolock using "special syntax," and in this case, even the autolock uses something straightforward, mapping to the signatures of the methods that update the word list (and thus need locks that are cluster-aware.) An autolock is "is meant for the case were the code is written with multiple threads in mind, using the synchronized keyword to demarcate protected areas. A synchronized block or method denotes a section of control flow that will be serialized with respect to thread access. Only a single thread can enter the block at a time." The autolock syntax uses AspectWerkz syntax for method specifications.

このうちのほとんどは、とてもシンプルです。ただ「autolock」が"特殊な構文"を使っているだけです。そしてこのケースでは、「autolock」でさえ、直接的な使われ方をしています。それはメソッドのシグネチャマッピングすることです。そのメソッドは単語リストを更新するものです。(したがって意識してクラスタをロックする必要があります)
「autolock」は、"マルチスレッドを念頭においてコードが書かれている場合、保護された領域を区別するためにsynchronized キーワードを使用することを意味"します。
synchronizedブロックまたはメソッドが、スレッドアクセスに関してシリアライズされるコントロールフローのセクションを表します。 「一度に一つのスレッドだけがブロックに入ることができます。」autolock構文はメソッド仕様にAspectWerkz構文を使用します。

Back to our code! The first test has already run; on a single (non-DSO) JVM on my workstation, it reports a runtime of 49587 milliseconds.

ではコードにもどりましょう。シングルでDSOのない最初のテストは私のワークステーションJVMで実行済みです。実行時間は、49,587ミリ秒と報告しました。

Now, let's start up the DSO server ("start-tc-server.bat") and run the Main class under DSO (with "dso-java.bat -cp . -Dtc.config=../tc-config.xml com.wordcalc.Main"): this affects the runtime, boosting it up to a whopping 49667 ms. (Note the sarcasm; this is just as likely to be attributed to network issues retrieving the word list, or task priority issues. This is not a significant difference.)

さて、DSOサーバーを起動しましょう。("start-tc-server.bat"を実行します)、そしてメインクラスをDSOの下で実行します。( "dso-java.bat -cp . -Dtc.config=../tc-config.xml com.wordcalc.Main"というコマンドを実行してください)。この効果は、実行時に49667ミリ秒というとてつもないところまで押し上げました。

We can now say our second test is complete. Now we need to shut down the DSO server, and repeat the process with two client instances. (We need to shut down the server for reasons that you'll see in our fourth test, which is two client instances running sequentially.)

これで、2つ目のテストが終わったと言えます。ここでDSOサーバーをシャットダウンする必要があります。そして、2つのクライアントインスタンスを繰り返します。(2つのクライアントインスタンスが連続して動作するという、4つ目のテストを試みるためにサーバーをシャットダウンする必要があるのです。)

To run the third test, we start up the DSO server again (remember, we killed it after the second test), and then start up two DSO clients as quickly together as possible. The runtime sped up to forty-one and forty-two seconds (including the time to fetch the base list itself for both processes, because they both initialized at the same time), a decent improvement - and the most interesting thing is that only one of the processes showed a message building the list for any given prefix in most cases. (During the tests, finding a collision where the same list was built twice was uncommon.)

3つ目のテストを行うために、DSOサーバーを再び立ち上げましょう。(覚えていますか?2つ目のテストの後に落としました)そして、2つのDSOクライアントを出来るだけ2つともすばやく立ち上げます。
実行環境は、41から42秒までスピードアップしました。(両方のプロセスがベースリストにフェッチする時間を含んでいます。なぜなら両方とも同じ時間に初期化されるからです。)なかなかの向上です。それに最も興味深いことは、プロセスの内の一つだけが、 多くの場合で、どんな与えられたプレフィックスのためのリストも造るメッセージを表したことです。(テストの間、同じリストが2度生成されたという稀な競合が見られました。)

Now for the fourth test: shut down the DSO server again, and run only one client. After it finishes, run one more client before shutting down the DSO server. The first client run takes 49597 ms, which is equivalent to our original run. The second run, though: 1432 ms. This is a huge increase over the first run, and we had a 100% hit rate on our memoization, even though we had no memoization code for the first set of strings checked.

そして、4つ目のテストです。DSOサーバーを再びシャットダウンして、一つのクライアントだけを実行します。それが終わったら、もう一つのクライアントをDSOサーバーがシャットダウンするまえに実行します。初めのクライアントは元の実行時間と同じぐらいの49597ミリセカンドかかりました。2つ目の実行は1432ミリセカンドです。これは1回目の実行からとんでもなく向上しています。そして、100%の割合でメモリがヒットしました。最初のセットのストリングチェックのためのインスタンス化コードは全くありませんでした。

So what happened? It's pretty simple. The configuration file for DSO specified that the object root for the word list was to be managed by DSO, so when the word list is used by a client JVM, it checks to see if it has the current data in "local view." If it doesn't, it pulls what it needs from the DSO server; if the DSO server doesn't have it, then the local client builds it, and updates the DSO-managed instance.

さて何が起こったのでしょう?それはとても簡単です。DSOの設定ファイルには、単語リストのオブジェクトルートはDSOによって管理されることが書かれていました。そのためクライアントJVMによって単語リストが使われたとき、"ローカルヴュー"に現在のデータががあるかどうかをチェックします。もしなければ、DSOサーバーから引き出す必要があります。もしDSOサーバーにもなければ、ローカルクライアントが生成します。そして、DSOが管理するインスタンスを更新します。

This makes the data available to every JVM that attaches to that DSO server -- even if the original JVM that built the data isn't running. It only pulls the data it needs, so even if our list is large, the check to see if the word list contains a given prefix is pretty small (and depending on the implementation of the size() method, might continue to be small, checking only the size of the keyset.) Every DSO client is a peer with every other, so standard JVM multithreading semantics apply to the client code -- there's absolutely nothing in our word list client that has to change to leverage DSO as a cache.

これは、DSOサーバーに接続したすべてのJVMでデータを有効化します。データの生成が実行されていないオリジナルのJVMまでも対象とします。必要なデータを引き出すだけですので、リストが大きくても
単語リストが与えられたプレフィックスを含んでいるならチェックがかなり小さくて(keysetのサイズだけをチェックするというsize()メソッドの実装によって小さくあり続けられるかもしれません。)
すべてのDSOクライアントは、他のすべてと同等です。そのため通常のJVMマルチスレッドの意味は、クライアントコードにも適用されます。DSOをキャッシュとして利用するために変更するべきことは、単語リストクライアントの中には、完全に何もありません。

This can be very useful. In one case, an application used 31MB of raw data for a rules engine, which it then sliced into memoized pieces (much as our test has, except in more dimensions and with far more data.) Each application startup used roughly a minute and a half on initializing this data, with more time spent at runtime memoizing common requests.

これはとても便利です。一つの例で言うと、アプリケーションが31MBの規則エンジンのための生データを利用していたとき、メモリの破片の中に分割されてしまったとき(より沢山のデータや大きなサイズでも、テストは期待値とほとんど同じです)それぞれのアプリケーションの起動は、このデータの初期化におおよそ1分から30秒掛かり、実行時に共通のリクエストを記憶するにはさらに多くの時間を使います。

If this were to be made DSO-aware, initialization would happen one time. Updates could be made on the fly, storing the changes to a database as well as directly to the runtime dataset, where clients could automagically use the correct (updated) data. There are other distributed models in which a specific, dedicated client could be the only one that accesses the entire dataset at once (or slices could be pulled from the database, instead of generating subsets of data from an in-memory cache), which would lower end-process memory requirements.

DSOを意識してつくられたなら、初期化処理は1度だけ起こります。更新は大急ぎで行えます。データベースへ変更を格納することも実行時データセットへ直接するようにできます。クライアントは自動的に更新データを使うことが出来ます。他の分散モデルは、特定の、そしてひたむきなクライアントがすぐに全体のデータセットにアクセスする唯一のものであるかもれません。
(または、データの部分集合を生成することの代わりに、データベースから部分を引くことが出来ました)より低い終了プロセスのメモリ要件によって。

This is one of the primary focuses of DSO -- providing clustered cache capabilities with no client-code alterations -- and it does it very well.

クライアントコードを変えずに、クラスタ化され可用性のあるキャッシュ提供することは、DSOの一つの主要な点です。そしてとてもうまく動きます。

Footnotes

  1. Given the nature of what DSO does, it's not very surprising that not only is it currently bound to a specific JVM vendor, but should also be bound to the specific JVM version before running.
  2. This is called “memoization;” see http://en.wikipedia.org/wiki/Memoization for more details.
  3. Note that it's silly to expect that every situation is the same! This code works, on the assumption that building a list has no side-effects, and with the knowledge that building the list once per process (instead of "just once") is an acceptable cost. This is probably a correct assumption most of the time, but your mileage may vary.
  4. Page 39, http://terracottatech.com/product-docs/TerracottaDSOGuide.pdf .
  5. AspectWerkz' site: http://aspectwerkz.codehaus.org/
  6. This can still be improved, but this is left as an exercise to the reader. Note that DSO is fully able to leverage the concurrency capabilities of the JVM, so as your code gains fine-grained control for multiple threads in a single JVM, it'll gain the same kinds of benefits for multiple DSO clients.

フッターノート

  1. DSOが行うことを自然に考えると、現在特定のJVMベンダーに縛られるだけではなく、実行する前に特定のJVMバージョンに縛られるということも、それほど驚くべきものではありません。
  2. "memoization"について詳しくは、http://en.wikipedia.org/wiki/Memoizationをみてください。
  3. あらゆる状況は同じではありません。このコードはリストを造るのにおいて副作用が全くないという前提、および過程(「ただ一度」の代わりに)に一度リストを造るのが、許容できるコストであるという知識で働いています。 たいていこれはたぶん正しい仮定ですが、あなたの工程数は異なるかもしれません。
  4. PDF( http://terracottatech.com/product-docs/TerracottaDSOGuide.pdf )の39ページ
  5. AspectWerkz'というサイト: http://aspectwerkz.codehaus.org/
  6. まだこれを改良することができますが、これは宿題として読者に残しておきます。 DSOは、JVMの並行処理制御の能力を完全に利用できることに注意してください。そのため、あなたのコードは、単一のJVMのきめの細かいマルチスレッドの制御装置を獲得します。複数のDSOクライアントでも同じ種類の利益が得られます。