Java 開発 2.0: Hibernate Shards によるシャーディング

リレーショナル・データベースの水平スケーラビリティー

シャーディングはすべての人にとって有効な手段というわけではありませんが、リレーショナル・システムでサイズの大きいデータを扱う必要がある場合に有効な手段の 1 つです。場合によってはシャーディングを実行するということは、すなわちデータのスケーラビリティーやシステムのパフォーマンスを犠牲にすることなく、信頼性の高い RDBMS を維持できることを意味します。連載「Java 開発 2.0」の今回の記事では、シャーディングが有効な場合とそうでない場合を説明し、シャーディングを実際に利用してテラバイト規模のデータを処理できる単純なサンプル・アプリケーションを作成します。

Andrew Glover, Author and developer, Beacon50

Andrew GloverAndrew Glover は、ビヘイビア駆動開発、継続的インテグレーション、アジャイル・ソフトウェア開発に情熱を持つ開発者であるとともに、著者、講演者、起業家でもあります。また、easyb BDD (Behavior-Driven Development) フレームワークの創始者、そして『継続的インテグレーション入門 開発プロセスを自動化する47の作法』、『Groovy in Action』、『Java Testing Patterns』の 3 冊の本の共著者でもあります。詳細は彼のブログにアクセスするか、Twitter で彼をフォローしてください。



2010年 8月 31日

リレーショナル・データベースが数テラバイトのデータを 1 つのテーブルに保管しようとすると、通常は全体的なパフォーマンスが劣化します。これほど膨大なデータのすべてに索引を付けるとなると、読み取り操作だけでなく、書き込み操作にもコストがかかります。大量のデータを保存するには NoSQL データ・ストアが特に適していますが (Google の Bigtable を考えてみてください)、NoSQL は明らかに非リレーショナル手法です。リレーショナル・データベースの ACID 性 (つまり、アトミック性、一貫性、独立性、永続性) と堅牢な構造を優先する開発者にとって、あるいはリレーショナル・データベースを使用する必要があるプロジェクトにとって、シャーディングは大量のデータを保存する素晴らしい手段となります。

シャーディングはデータベース・パーティショニングから派生した手法ですが、データベース固有の手法ではなく、アプリケーション・レベルで適用されます。さまざまなシャーディング実装のうち、Java™ 技術の世界で最もよく使われているのはおそらく Hibernate Shards でしょう。この気の利いたプロジェクトでは、論理データベースにマッピングされた POJO を使用して、シャーディングによって分割されたデータ・セットをほぼシームレスに処理することができます (「ほぼ」という部分については後ほど説明します)。Hibernate Shards を使用する際に、POJO を特別な方法でシャード (分割された区画) にマッピングする必要はありません。マッピングは、通常のリレーショナル・データベースの場合のように Hibernate 方式で行います。下位レベルのシャーディングに関しては、Hibernate Shards が管理してくれます。

この連載ではこれまで、さまざまなデータ・ストレージ技術を具体的に説明するために、レースとランナーの例に基づく単純なドメインを使用してきました。今月は、このお馴染みのサンプル・アプリケーションを使用して実際的なシャーディング・ストラテジーを紹介した後、このアプリケーションを Hibernate Shards で実装します。シャーディング関連の作業で困難な部分は、必ずしも Hibernate に関連しているとは限らないことに注意してください。実のところ、Hibernate Shards のコードを作成するのは難しい作業ではありません。真の仕事は、何をどのように分割するかを考えるところにあります。

この連載について

Java 技術が初めて登場してから現在に至るまでに、Java 開発の様相は劇的に変化しました。成熟したオープンソースのフレームワーク、そしてサービスとして提供される信頼性の高いデプロイメント・インフラストラクチャーを利用できる (借りられる) おかげで、今では Java アプリケーションを短時間かつ低コストでアセンブルし、テスト、実行、保守することが可能になっています。この連載では Andrew Glover が、この新たな Java 開発パラダイムを可能にする多種多様な技術とツールを詳しく探ります。

シャーディングの概要

データベース・パーティショニングは、テーブルの行を論理的なデータごとに分割し、複数の小さなグループにするという本質的にリレーショナルなプロセスです。例えば、タイムスタンプを基準に foo という巨大なテーブルをパーティショニングする場合、2010年 8月のすべてのデータはパーティション A に集め、それ以降のデータはパーティション B に集めるといった具合になります。パーティショニングには、読み取り操作と書き込み操作を高速化する効果があります。それは、操作のターゲットが個別のパーティションに含まれる小さなデータ・セットになるからです。

パーティショニングは常に使用できるとは限りません (MySQL ではバージョン 5.1 になるまではパーティショニングをサポートしていませんでした)。また、商用システムでパーティショニングを行うにはコストがかかり過ぎることもあります。その上、ほとんどのパーティショニング実装ではデータを同じ 1 つの物理マシンに保管するため、パーティショニングを利用するとしても、ハードウェアの制限には相変わらず縛られます。さらに、パーティショニングによってハードウェアの信頼性 (すなわち信頼性の欠如) が解決されることにもなりません。このような理由から、さまざまな賢い人々が新しいスケーリング方法を探すようになりました。

シャーディングとは基本的に、データベース・レベルでパーティショニングを行うことです。シャーディングではテーブルの行をデータごとに分割するのではなく、データベース自体を何らかの論理データ要素を基準に分割します (通常は複数のマシンに分割します)。つまり、テーブルを小さな塊に分割するのではなく、データベース全体を小さな塊に分割するということです。

シャーディングの典型例は、世界中の顧客データを保管する大規模なデータベースを地域別に分割することによるもので、米国の顧客データはシャード A に、アジアの顧客データはシャード B に、欧州の顧客データはシャード C に格納するなどです。シャード自体はそれぞれ異なるマシンにあり、シャードのそれぞれが顧客の好みや注文履歴などの関連するすべてのデータを格納します。

シャーディングの利点は (パーティショニングと同じく)、サイズの大きなデータをコンパクトにできることです。各シャードに格納される個々のテーブルは小さいことから、読み取り、書き込み操作の時間が短縮され、パフォーマンスが向上します。また、シャーディングは信頼性を改善することにもなり得ます。1 つのシャードに突然障害が発生したとしても、他のシャードは引き続きデータを提供できるからです。さらにシャーディングはアプリケーション層で行われるため、通常のパーティショニングをサポートしないデータベースにも適用可能です。シャーディングを利用することで、コストを低く抑えられる可能性もあります。


シャーディングとストラテジー

ほとんどの技術と同じく、シャーディングにもトレードオフが伴います。シャーディングはデータベース固有の手法ではないため (つまり、アプリケーションに実装する必要があるため)、あらかじめ、そのストラテジーを綿密に計画しておかなければなりません。シャーディングでは、主キー、そして複数のシャードに跨るクエリーの両方が大きく影響します。それは主に、この 2 つが制約要因となるためです。

主キー
シャーディングでは複数のデータベースを使用しますが、そのすべてが他のデータベースを認識せずに自律的に機能します。したがって、データベース・シーケンスに依存する場合 (主キーを自動生成する場合など)、一連のデータベースでまったく同じ主キーが生成される可能性があります。1 つの分散データベースを介してシーケンスを調整することも可能ですが、そうすると、システムの複雑さが増してきます。主キーが重複することがないようにする最も安全な方法は、アプリケーションにキーを生成させることです (いずれにしても、アプリケーションがシャードに分割されたシステムを管理することになります)。

複数のシャードに跨るクエリー
ほとんどのシャーディング実装 (Hibernate Shards を含む) では、複数のシャードに跨るクエリーを実行することはできません。これは、2 つの異なるシャードそれぞれのデータのセットを使用するとなると、必要以上の苦労を要することを意味します (面白いことに、Amazon の SimpleDB でも複数のドメインに跨るクエリーを禁止しています)。例えば、米国の顧客をシャード 1 に保管しているとすると、米国の顧客に関連するすべてのデータもシャード 1 に保管しなければなりません。そのデータをシャード 2 に保管しようとすると事態は複雑になり、システム・パフォーマンスに悪影響が出てくることになります。この状況は、前に指摘した点にも関係します。つまり、シャード間の結合がどうしても必要になった場合には、重複する可能性を排除するような方法でキーを管理したほうがよいでしょう。

以上のことから、データベースを構築する前に、十分にシャーディング・ストラテジーを検討する必要があることは明らかです。そして方向性を具体的に決定した後は、事実上その方向性を維持しなければならなくなります。シャードに分割された後のデータの移動は簡単ではないからです。

シャーディングは時期尚早に行わないこと

シャーディングは、最後の手段として採り入れるのが最善です。時期尚早に行う最適化と同じく、データ増加の予想に基づくシャーディングは最悪の事態になりかねません。シャーディング実装が成功するかどうかは、アプリケーションの長期的なデータ増加をある程度理解した上で立てた推定にかかっています。データをいったんシャードに分割すると、データの移動は極めて困難になります。

ストラテジーの一例

シャーディングは線形データ・モデルを強いることになるため (つまり、異なるシャード間でデータを簡単に結合できないということです)、データがシャードごとに論理的に編成されるように、どのようなデータ編成にするかを明確にすることから始めなければなりません。通常はドメインの主要ノードに重点を置くと、データの編成を最も簡単に行えるようになります。e-コマース・システムを例に挙げると、主要ノードは「注文」または「顧客」のいずれかです。顧客をシャーディング・ストラテジーの基礎として選んだ場合、顧客に関連するすべてのデータはそれぞれの顧客のシャードに格納することになりますが、それでもまだ、そのデータをどのシャードに移動するかを選択する必要があります。

顧客のシャードは、例えば顧客の居住地域 (欧州、アジア、アフリカなど) を基準に分割することができます。あるいはその他の基準で分割しても構いませんが、シャーディング・ストラテジーには、すべてのシャードで均一にデータを分散する何らかの手段を取り込まなければなりません。シャーディング本来の趣旨は、サイズの大きなデータ・セットを小さなデータ・セットに分割することです。したがって特定の e-コマース・ドメインで、欧州の顧客データは膨大にあり、米国の顧客データは比較的少ないというのであれば、顧客の居住地域に基づいて分割する意味はありません。


レースに参加する – 今回はシャーディングを使用します!

お馴染みのレース参加アプリケーションではレース別またはランナー別のシャードに分割することができますが、このドメインはレースに参加するランナーで編成されているので、この例ではレース別のシャードに分割することにします。したがって、レースが私のドメインのルートとなります。さらに、レースの距離ごとのシャードにも分割するつもりです。このレース参加アプリケーションでは無数のランナーと併せ、距離の異なる数え切れないほどのレースを扱うためです。

以上を決定するにあたって、私はトレードオフを前提としている点に注意してください。1 人のランナーがそれぞれに異なるシャードに置かれた複数のレースに参加するという場合を考えてみてください。Hibernate Shards は (ほとんどのシャーディング実装と同じく) シャード間の結合をサポートしません。したがって、この多少不都合な制約を我慢して、ランナーを複数のシャードに存在させることにします。つまり、ランナーが参加するそれぞれのレースが置かれたシャードに、そのランナーを再作成するということです。

説明を単純にするため、ここでは 2 つのシャードだけを作成します。1 つは距離が 10 マイル以下のレース用、もう 1 つは 10 マイルを超える距離のレース用です。


Hibernate Shards の実装

Hibernate Shards は既存の Hibernate プロジェクトとほぼシームレスに連動するように作成されていますが、唯一の難点として、Hibernate Shards には特定の情報と振る舞いを指定する必要があります。具体的には、シャードへのアクセス・ストラテジー、シャードの選択ストラテジー、そしてシャードの解決ストラテジーです。このサンプル・アプリケーションではこの 3 つのインターフェースを実装する必要がありますが、場合によってはデフォルトのインターフェースを使用することもできます。以降のセクションで、それぞれのインターフェースについて個別に説明します。

ShardAccessStrategy

クエリーの実行時に Hibernate Shards に必要となるのは、最初にアクセスするシャード、次にアクセスするシャードというように、アクセスするシャードの順番を決定するためのメカニズムです。Hibernate Shards は必ずしもクエリーの検索対象を明らかにするわけではありませんが (これは Hibernate Core とデータベースの仕事です)、答えを得るには複数のシャードに対してクエリーを実行しなければならないことは認識します。そこで、Hibernate Shards ではそのまますぐに使用できる 2 つの論理実装を提供しています。その 1 つは、答えが返されるまで、あるいはすべてのシャードに対してクエリーが実行されるまで、1 度に 1 つのシャードに対して順番にクエリーを実行するシーケンシャル・ストラテジーです。もう 1 つの並列アクセス・ストラテジーではスレッド化モデルを使用してすべてのシャードに同時にアクセスします。

説明が複雑にならないように、ここではその名も SequentialShardAccessStrategy というシーケンシャル・ストラテジーを使用します。このストラテジーの構成については後で説明します。

ShardSelectionStrategy

新しいオブジェクトが作成される際には (つまり、Hibernate を介して新規 Race または Runner が作成される際)、Hibernate Shards はそのオブジェクトに対応するデータを書き込む対象となるシャードを知る必要があります。したがって、このインターフェースを実装し、シャーディング・ロジックをコーディングします。デフォルトの実装をお望みの場合には、データをラウンド・ロビン・ストラテジーでシャードに書き込む RoundRobinShardSelectionStrategy と呼ばれる実装があります。

レース参加アプリケーションには、レースの距離別のシャードに分割する振る舞いを提供する必要があります。そこで、ShardSelectionStrategy インターフェースを実装し、selectShardIdForNewObject メソッドで Race オブジェクトの distance を基準としてシャードに分割する単純なロジックを提供します (Race オブジェクトについては、この後記載します)。

実行時にドメイン・オブジェクトで save のようなメソッドが呼び出されると、Hibernate のコア内の奥深くではこのインターフェースの振る舞いが利用されます。

リスト 1. 単純なシャード選択ストラテジー
import org.hibernate.shards.ShardId;
import org.hibernate.shards.strategy.selection.ShardSelectionStrategy;

public class RacerShardSelectionStrategy implements ShardSelectionStrategy {

 public ShardId selectShardIdForNewObject(Object obj) {
  if (obj instanceof Race) {
   Race rce = (Race) obj;
   return this.determineShardId(rce.getDistance());
  } else if (obj instanceof Runner) {
   Runner runnr = (Runner) obj;
   if (runnr.getRaces().isEmpty()) {
    throw new IllegalArgumentException("runners must have at least one race");
   } else {
    double dist = 0.0;
    for (Race rce : runnr.getRaces()) {
     dist = rce.getDistance();
     break;
    }
    return this.determineShardId(dist);
   }
  } else {
   throw new IllegalArgumentException("a non-shardable object is being created"); 
 }
}

 private ShardId determineShardId(double distance){
  if (distance > 10.0) {
   return new ShardId(1);
  } else {
   return new ShardId(0);
  }
 }
}

リスト 1 を見るとわかるように、永続化対象のオブジェクトが Race の場合にはその距離が判別され、距離に応じたシャードが選択されます。この例のシャードには 0 と 1 の 2 つがあります。シャード 1 には 10 マイルより長い距離のレースが格納されます。それ以外のレースはすべて、シャード 1 に格納されます。

Runner やその他のオブジェクトを永続化する場合には、もう少し複雑になってきます。私は以下の 3 つを規定する論理ルールをコーディングしました。

  • Runner は、対応する Race がなければ存在できない。
  • 複数の Race が関連付けられた Runner が作成された場合、その Runner は最初に検出された Race のシャードに永続化される (ちなみに、このルールは将来的にマイナス要素となります)。
  • 他のドメイン・オブジェクトが保存される場合には、差し当たり例外がスローされる。

上記のルールが用意できたら、ほっと一息ついてください。これで、困難な作業の大部分は終わったことになります。私がコーディングしたロジックは、レース参加アプリケーションを拡張するには柔軟性に欠けるかもしれませんが、この記事のデモには申し分ありません!

ShardResolutionStrategy

オブジェクトをそのキーで検索する場合、Hibernate Shards にはどのシャードを最初に検索するかを決定するための手段が必要です。この決定を行うための手引きとして、SharedResolutionStrategy インターフェースを使用します。

前述したように、シャーディングでは自分で主キーを管理することになるため、主キーのことは常に意識しておかなければなりません。幸い、Hibernate はすでにキーの提供、つまり UUID の生成を得意としています。そのことから、Hibernate Shards には最初から UUID 自体にシャード ID 情報を組み込む賢い ID ジェネレーター、ShardedUUIDGenerator が用意されています。

ShardedUUIDGenerator を使用してキーを生成することにした場合には (この記事ではそうします)、Hibernate Shards に付属の ShardResolutionStrategy 実装を使用することも可能になります。AllShardsShardResolutionStrategy と呼ばれるこの実装は、特定のオブジェクトの ID に基づいて、検索対象とするシャードを決定することができます。

これで、Hibernate Shards が適切に機能するために必要な 3 つのインターフェースの構成は完了しました。次は、サンプル・アプリケーションのシャーディングに取り掛かります。早速、Hibernate の SessionFactory を起動してください。


Hibernate Shards の構成

Hibernate のコアとなっているインターフェース・オブジェクトの 1 つは、SessionFactory です。Hibernate の魔法のすべては、例えばマッピング・ファイルや構成をロードして Hibernate アプリケーションを構成するこの小さなオブジェクトを介して行われます。アノテーションまたは Hibernate の由緒ある .hbm ファイルを使用する場合でも、どのオブジェクトが永続化可能でどこに永続化すべきかを Hibernate が把握するためには、SessionFactory が必要になります。

したがって Hibernate Shards では、複数のデータベースを構成できるように補強された SessionFactory 型を利用します。これには ShardedSessionFactory というふさわしい名前が付けられており、型はもちろん SessionFactory です。ShardedSessionFactory を作成するときには、前に構成した 3 つのシャード実装型 (ShardAccessStrategyShardSelectionStrategy、および ShardResolutionStrategy) を提供します。また、使用している POJO に必要なマッピング・ファイルを提供する必要もあります (アノテーション・ベースの Hibernate POJO 構成を使用する場合は多少異なります)。そして最後に、ShardedSessionFactory インスタンスには、使用する各シャードに対応する複数の Hibernate 構成ファイルが必要となります。

Hibernate 構成を作成する

私が作成した ShardedSessionFactoryBuilder 型には、createSessionFactory という主要なメソッドが 1 つだけあります。このメソッドが、適切に構成された SessionFactory を作成します。後ですべての構成要素を Spring で関連付けるので (最近では誰もが IOC コンテナーを利用します)、まずはリスト 2 に記載する ShardedSessionFactoryBuilder の主要な関数を見てください。これは、Hibernate の Configuration を作成する関数です。

リスト 2. Hibernate の Configuration の作成
private Configuration getPrototypeConfig(String hibernateFile, List<String> 
  resourceFiles) {
 Configuration config = new Configuration().configure(hibernateFile);
 for (String res : resourceFiles) {
  configs.addResource(res);
 }
 return config;
}

リスト 2 に示されているように、Hibernate 構成ファイルから 1 つの単純な Configuration が作成されます。このファイルに、使用されているデータベースのタイプ、ユーザー名、パスワードなどの情報だけでなく、必要なすべてのリソース・ファイル (POJO の場合は .hbm ファイル) が格納されます。このように、複数のデータベース構成を使用するシャーディングの場合、Hibernate Shards では 1 つの hibernate.cfg.xml ファイルだけを使用するように単純化することができます (ただし、リスト 4 を見るとわかるように、このファイルは使用するシャードごとに 1 つ必要です)。

次にリスト 3 で、すべてのシャード構成を 1 つの List にまとめます。

リスト 3. シャード構成のリスト
List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
for (String hibconfig : this.hibernateConfigurations) {
 shardConfigs.add(buildShardConfig(hibconfig));
}

Spring 構成

リスト 3hibernateConfigurations の参照は、Hibernate 構成ファイルの名前がそれぞれに含まれる String からなる List を指しています。この List が、Spring の Autowire 機能によって自動的に関連付けられることになります。リスト 4 に、私の Spring 構成ファイルから該当する部分を抜粋します。

リスト 4. Spring 構成ファイルからの抜粋
<bean id="shardedSessionFactoryBuilder" 
  class="org.disco.racer.shardsupport.ShardedSessionFactoryBuilder">
    <property name="resourceConfigurations">
        <list>
            <value>racer.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateConfigurations">
        <list>
            <value>shard0.hibernate.cfg.xml</value>
            <value>shard1.hibernate.cfg.xml</value>
        </list>
    </property>
</bean>

リスト 4 からわかるように、ShardedSessionFactoryBuilder に関連付けられているのは、1 つの POJO マッピング・ファイル、そして 2 つのシャード構成ファイルです。前者の POJO ファイルのスニペットをリスト 5 に記載します。

リスト 5. Race の POJO マッピング
<class name="org.disco.racer.domain.Race" table="race"dynamic-update="true"
   dynamic-insert="true">

 <id name="id" column="RACE_ID" unsaved-value="-1">
  <generator class="org.hibernate.shards.id.ShardedUUIDGenerator"/>
 </id>

 <set name="participants" cascade="save-update" inverse="false" table="race_participants"
    lazy="false">
  <key column="race_id"/>
  <many-to-many column="runner_id" class="org.disco.racer.domain.Runner"/>
 </set>

 <set name="results" inverse="true" table="race_results" lazy="false">
  <key column="race_id"/>
  <one-to-many class="org.disco.racer.domain.Result"/>
 </set>

 <property name="name" column="NAME" type="string"/>
 <property name="distance" column="DISTANCE" type="double"/>
 <property name="date" column="DATE" type="date"/>
 <property name="description" column="DESCRIPTION" type="string"/>
</class>

リスト 5 の POJO マッピングで唯一特異な点は、ID のジェネレーター・クラスが ShardedUUIDGenerator となっていることです。このジェネレーターは前にも説明したように、シャード ID 情報をその UUID 自体に組み込みます。これが唯一、私の POJO マッピングでのシャーディングに固有な点です。

シャード構成ファイル

次のリスト 6 では、シャードの一方を構成しています。これはシャード 0 の構成ファイルですが、シャード ID と接続情報を除けば、シャード 1 の構成ファイルも同じ内容になります。

リスト 6. Hibernate の Shards 構成ファイル
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory name="HibernateSessionFactory0">
        <property name="dialect">org.hibernate.dialect.HSQLDialect</property>
        <property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="connection.url">
            jdbc:hsqldb:file:/.../db01/db01
        </property>
        <property name="connection.username">SA</property>
        <property name="connection.password"></property>
        <property name="hibernate.connection.shard_id">0</property>
        <property name="hibernate.shard.enable_cross_shard_relationship_checks">true
        </property>
    </session-factory>
</hibernate-configuration>

その名前からわかるように、enable_cross_shard_relationship_checks プロパティーはシャード間の関係をチェックします。Hibernate Shards の資料によると、このプロパティーにはかなりのコストがかかるため、本番環境では無効にしてください。

いよいよ ShardedSessionFactoryBuilderShardStrategyFactory を作成し、3 つの型 (そのうちの 1 つはリスト 1RacerShardSelectionStrategy) を追加することによって、すべてを 1 つに統合します (リスト 7 を参照)。

リスト 7. ShardStrategyFactory の作成
private ShardStrategyFactory buildShardStrategyFactory() {
 ShardStrategyFactory shardStrategyFactory = new ShardStrategyFactory() {
  public ShardStrategy newShardStrategy(List<ShardId> shardIds) {
   ShardSelectionStrategy pss = new RacerShardSelectionStrategy();
   ShardResolutionStrategy prs = new AllShardsShardResolutionStrategy(shardIds);
   ShardAccessStrategy pas = new SequentialShardAccessStrategy();
   return new ShardStrategyImpl(pss, prs, pas);
  }
 };
 return shardStrategyFactory;
}

これで、あの巧みな createSessionFactory メソッドを実行すると、ShardedSessionFactory が作成されます (リスト 8 を参照)。

リスト 8. ShardedSessionFactory の作成
public SessionFactory createSessionFactory() {
 Configuration prototypeConfig = this.getPrototypeConfig
  (this.hibernateConfigurations.get(0), this.resourceConfigurations);

 List<ShardConfiguration> shardConfigs = new ArrayList<ShardConfiguration>();
 for (String hibconfig : this.hibernateConfigurations) {
  shardConfigs.add(buildShardConfig(hibconfig));
 }

 ShardStrategyFactory shardStrategyFactory = buildShardStrategyFactory();
 ShardedConfiguration shardedConfig = new ShardedConfiguration(
  prototypeConfig, shardConfigs,shardStrategyFactory);
 return shardedConfig.buildShardedSessionFactory();
}

Spring でドメイン・オブジェクトを相互に関連付ける

まずは深呼吸してください。ここから、仕上げの段階に入ります。これまでの作業で、ShardedSessionFactory を適切に構成するビルダー・クラスを作成しました。ShardedSessionFactory は、実際には Hibernate の随所で目にする SessionFactory 型の実装にすぎませんが、シャーディングの魔法はすべてここで行われます。つまり、リスト 1 に記載したシャード選択ストラテジーを使用して、私が構成した 2 つのシャードでデータの書き込み、読み取り操作を行うということです (リスト 6 にシャード 0 の構成を記載しましたが、シャード 1 の構成もほとんど同じです)。

残る作業は、ドメイン・オブジェクトを相互に関連付けることだけです。この例の場合、これらのオブジェクトは Hibernate に依存することから SessionFactory 型を機能させなければなりませんが、それには単に、ShardedSessionFactoryBuilder を使用して、SessionFactory 型を提供すればよいのです (リスト 9 を参照)。

リスト 9. Spring での POJO の関連付け
<bean id="mySessionFactory"
 factory-bean="shardedSessionFactoryBuilder"
 factory-method="createSessionFactory">
</bean>

<bean id="race_dao" class="org.disco.racer.domain.RaceDAOImpl">
 <property name="sessionFactory">
  <ref bean="mySessionFactory"/>
 </property>
</bean>

リスト 9 ではまず、ファクトリー風の Bean を Spring で作成します。つまり、RaceDAOImpl 型に sessionFactory という名前の SessionFactory 型のプロパティーを含めます。その結果、リスト 4 で定義した ShardedSessionFactoryBuildercreateSessionFactory メソッドをmySessionFactory 参照が呼び出すと、SessionFactory のインスタンスが作成されます。

Spring (基本的に、私はこれを事前構成されたオブジェクトを返すための巨大なファクトリーとして使用しています) に Race オブジェクトのインスタンスを要求すれば、すべてが整うことになります。ここには記載していませんが、RaceDAOImpl 型は Hibernate テンプレートを使用してデータを保管および取得するオブジェクトです。Race 型には RaceDAOImpl のインスタンスを含めるようにし、あらゆるデータ・ストア関連のアクティビティーをこのインスタンスに任せます。賢い仕組みだと思いませんか?

DAO は、コードでは Hibernate Shards に結び付けられていませんが、構成によって結び付けられています。リスト 5 の構成によって DAO はシャーディング固有の UUID 生成スキームに結び付けられているので、シャードに分割する必要が出てきた場合には、既存の Hibernate 実装のドメイン・オブジェクトを再利用できるというわけです。


シャーディング: easyb での試運転

次に行うべき作業として、シャーディング実装が機能することを検証しなければなりません。データベースは 2 つあり、距離別にシャードに分割していることから、例えばレースとしてマラソン (10 マイルよりも長い距離) を作成した場合には、その Race インスタンスはシャード 1 になければなりません。それよりも短い、例えば 5 キロのレース (3.1 マイル) は、シャード 0 に見つかることになります。Race を作成したら、それぞれのデータベースでそこに保管されているレコードを確認することができます。

リスト 10 ではまずマラソンを作成し、そのレコードが実際にシャード 0 ではなく、シャード 1 にあることを確認しています。このテストをさらに面白く (そして簡単に) するために、ここでは easyb を使用しました。この Groovy ベースのビヘイビア駆動開発フレームワークは、自然言語の検証を容易にします。また、easyb は Java コードでも簡単に機能します。Groovy や easyb についての知識がないとしても、リスト 10 のコードは理解できるはずなので、すべてが計画通りに機能していることを確認することができます (easyb の作成には私も関与しました。developerWorks に、easyb に関する私の記事が掲載されています)。

リスト 10. シャードの正しさを検証する easyb のスニペット
scenario "races greater than 10.0 miles should be in shard 1 or db02", {
  given "a newly created race that is over 10.0 miles", {
    new Race("Leesburg Marathon", new Date(), 26.2,
            "Race the beautiful streets of Leesburg!").create()
  }
  then "everything should work fine w/respect to Hibernate", {
    rce = Race.findByName("Leesburg Marathon")
    rce.distance.shouldBe 26.2
  }
  and "the race should be stored in shard 1 or db02", {
    sql = Sql.newInstance(db02url, name, psswrd, driver)
    sql.eachRow("select race_id, distance, name from race where name=?", 
      ["Leesburg Marathon"]) { row ->
      row.distance.shouldBe 26.2
    }
    sql.close()
  }
  and "the race should NOT be stored in shard 0 or db01", {
    sql = Sql.newInstance(db01url, name, psswrd, driver)
    sql.eachRow("select race_id, distance, name from race where name=?", 
      ["Leesburg Marathon"]) { row ->
      fail "shard 0 contains a marathon!"
    }
    sql.close()
  }
}

もちろん私の役目はこれで終わったわけではありません。短い距離のレースを作成して、それがシャード 1 ではなくシャード 0 に置かれることを検証する必要がまだ残っています。この検証の演習については、この記事に付属のコードをダウンロードして確認してください。


シャーディングの利点と欠点

シャーディングは、アプリケーションの読み取り、書き込み操作を高速に行えるようにします。この効果は、アプリケーションが例えばテラバイト単位の膨大な量のデータを格納している場合や、Google または Facebook のように、とめどもなく増大し続けるドメインでは、一層顕著に現れます。

シャーディングを適用する前に、アプリケーションのサイズとその増大がシャーディングを適用するに値することを確認してください。シャーディングに伴うコスト (あるいは欠点) には、データをどのように保管および取得するかに関してアプリケーション固有のロジックをコーディングしなければならないという厄介な作業が含まれます。また、いったんシャーディングを適用すると、そのシャーディング・モデルに多かれ少なかれ束縛されることになります。シャードを分割し直すのは、簡単なことではないためです。

適切な状況であれば、シャーディングは従来の RDBMS に伴う、スケーリングおよび速度の制約を解放する鍵となり得ます。組織がリレーショナル・インフラストラクチャーに縛られていて、極めてスケーラビリティーの高いデータ・ストレージの必要性を満たすためにハードウェアをアップグレードし続けることはできないという場合には、シャーディングは特にコスト効果の高い決定となります。


ダウンロード

内容ファイル名サイズ
Sample code for this articlej-javadev2-11.zip15KB

参考文献

学ぶために

  • Java 開発 2.0」: この developerWorks の連載では、NoSQL (2010年5月)、SimpleDB (2010年6月)、CouchDB (2009年11月) をはじめ、Java 開発の様相を塗り替える技術とツールを詳しく探っています。
  • Sharding with Max Ross」(JavaWorld podcast、2008年7月): Andy Glover が Hibernate Shards の創始者たちと、このプロジェクトの背後にある動機、そしてシャードを効果的に利用する方法について討議しています。
  • Think twice before sharding」(Andrew Glover 著、thediscoblog.com、2008年6月): シャーディングにはリスクが伴わないわけではありません。シャーディングを使用する前に、この手法について詳しく調べてください。
  • An Unorthodox Approach to Database Design : The Coming of the Shard」(Todd Hoff 著、HighScalability.com、2009年8月): 膨大なデータの管理を専門とするこのサイトで、シャーディングについて分かりやすく概説しています。
  • Building Scalable Databases: Pros and Cons of Various Database Sharding Schemes」(Dare Obasanjo 著、25HoursADay.com、2009年1月): 実世界の例として、Facebook の場合のシャーディング・ストラテジーについて詳しく探っています。
  • Rethinking the traditional DAO pattern」(Andrew Glover 著、thediscoblog.com、2008年6月): DAO を通常のドメイン・オブジェクトに畳み込む Grails および Rails 式の方法について詳しく学んでください。
  • Drive development with easyb」(Andrew Glover 著、developerWorks、2008年11月): easyb の作成者自らが、このフレームワークの使い方を一から説明します。
  • Java Technology bookstore で、この記事で取り上げた技術やその他の技術に関する本を探してください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

  • Hibernate Shards: Hibernate Core に水平パーティショニングのサポートを追加することにより、データの複雑さをカプセル化して最小限にするよう設計されています。
  • easyb: ドメイン特化言語ベースの仕様を使用する easyb は、実行可能でありながら、読んで理解できる文書を実現することを目的としています。

議論するために

  • My developerWorks コミュニティーに加わってください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者が主導するブログ、フォーラム、グループ、ウィキを調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=548932
ArticleTitle=Java 開発 2.0: Hibernate Shards によるシャーディング
publish-date=08312010