EclipseLinkでデータベースのパーティショニングをするぞ

データベースにおいてパーティショニング(Partitioning)という負荷分散手法をご存知でしょうか。

先日のエントリ「JPAでマスター/スレーブ構成のMySQLを使うぞ」で紹介した方法ではスレーブをスケールアウトさせることでSELECTなど参照系のクエリに対してはある程度まで負荷分散ができました。しかしINSERTやUPDATE、DELETEなど更新系のクエリはスケールアウトすることができません

パーティショニングはこのような問題を解決する手法の1つで、テーブルを一定のルールに基づいて複数のデータベースに分割し、データの格納先を分散させることで更新系のクエリの負荷を軽減させる仕組みです。シャーディング(Sharding)と呼ばれることもあります。

EclipseLinkはJPAの実装ですが、パーティショニングを簡単に扱うことができるように独自の拡張が施されています。今回はその方法をご紹介します。サンプルプロジェクトはこちらです。

アノテーションで楽々パーティショニング

EclipseLinkのパーティショニングは非常に簡単で、

  • エンティティに対してアノテーションを付加
  • 接続先のデータベース情報の定義

これだけで実現できます。まずはエンティティをみてみましょう。ユーザーのエンティティをIDでパーティショニングする例です。

@Entity
@Data
@HashPartitioning(
    name = "hashPartitioningById",
    partitionColumn = @Column(name = "id"),
    connectionPools = { "partition0", "partition1", "partition2", "partition3" },
    unionUnpartitionableQueries = true)
@Partitioned("hashPartitioningById")
public class User implements Serializable {
  private static final long serialVersionUID = 9138208428859166924L;

  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  private Long id;

  @Column(nullable = false, unique = true)
  private String name;

  @Column(nullable = false)
  private String password;
}

複数のデータベースを使うため@GeneratedValueで指定するIDの生成戦略は自動採番(GenerationType.IDENTITY)ではなくシーケンステーブル(GenerationType.TABLE)を指定します。

@HashPartitioningと@PartitionedがEclipseLinkのパーティショニング用アノテーションです。

@HashPartitioningはEclipseLinkのパーティショニング戦略のひとつで、partitionColumnプロパティで指定したカラムのハッシュ値に基づいてクエリを発行するデータベースを決定します。上記の例の場合はidカラムを指定しています。

unionUnpartitionableQueriesプロパティにtrueを指定すると、SELECTクエリにパーティションのキーが含まれない場合、全てのデータベースに対してクエリを発行し結果をマージするようになります。

connectionPoolsプロパティは分散させるデータベースのコネクションプール名の配列を指定します。上記の例ではpartition0からpartition3までの4つが指定されています。コネクションプールの定義はpersistence.xml内で行います。

<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
  version="2.1">

  <persistence-unit name="sample-eclipselink-partitioning" transaction-type="JTA">
    <jta-data-source>jdbc/sample-eclipselink-partitioning</jta-data-source>
    <class>com.github.nagaseyasuhito.sample.eclipselink.partitioning.entity.User</class>

    <properties>
      <property name="eclipselink.connection-pool.partition0.jtaDataSource" value="jdbc/sample-eclipselink-partitioning0" />
      <property name="eclipselink.connection-pool.partition1.jtaDataSource" value="jdbc/sample-eclipselink-partitioning1" />
      <property name="eclipselink.connection-pool.partition2.jtaDataSource" value="jdbc/sample-eclipselink-partitioning2" />
      <property name="eclipselink.connection-pool.partition3.jtaDataSource" value="jdbc/sample-eclipselink-partitioning3" />
    </properties>
  </persistence-unit>
</persistence>

このようにeclipselink.connection-pool.[コネクションプール名].[プロパティ]のように定義します。上記はJTAのデータソースを使う例です。JTAではなくRESOURCE_LOCALの場合は、jtaDataSourceの代わりにurlやuser、passwordなどを指定します。

実行するぞ

早速テストを実行してみましょう。

public class PartitioningTest {
 @Test
 public void partitioningSuccess() throws Exception {
 @Cleanup
 EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("sample-eclipselink-partitioning-test");

 @Cleanup
 EntityManager entityManager = entityManagerFactory.createEntityManager();

 entityManager.getTransaction().begin();
 for (int i = 0; i < 16; i++) {
 User user = new User();
 user.setName(String.format("name%03d", i));
 user.setPassword("password");
 entityManager.persist(user);
 }
 entityManager.getTransaction().commit();

 entityManagerFactory.getCache().evictAll();
 entityManager.clear();

 entityManager.getTransaction().begin();
 entityManager.createQuery("from User u where u.id = :id", User.class).setParameter("id", 3L).getSingleResult();
 entityManager.createQuery("from User u where u.name = :name", User.class).setParameter("name", "name004").getSingleResult();
 entityManager.getTransaction().commit();
 }
}

このように何の変哲もないJPAのクエリです。ログを見てみましょう。

[EL Fine]: sql: 2015-02-05 18:48:18.586--ClientSession(16868310)--Connection(980697799)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [1, name000, password]
[EL Fine]: sql: 2015-02-05 18:48:18.589--ClientSession(16868310)--Connection(2075809815)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [2, name001, password]
[EL Fine]: sql: 2015-02-05 18:48:18.59--ClientSession(16868310)--Connection(2045036434)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [3, name002, password]
[EL Fine]: sql: 2015-02-05 18:48:18.592--ClientSession(16868310)--Connection(2135449562)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [4, name003, password]
[EL Fine]: sql: 2015-02-05 18:48:18.593--ClientSession(16868310)--Connection(980697799)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [5, name004, password]
[EL Fine]: sql: 2015-02-05 18:48:18.594--ClientSession(16868310)--Connection(2075809815)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [6, name005, password]
[EL Fine]: sql: 2015-02-05 18:48:18.595--ClientSession(16868310)--Connection(2045036434)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [7, name006, password]
[EL Fine]: sql: 2015-02-05 18:48:18.598--ClientSession(16868310)--Connection(2135449562)--INSERT INTO USER (ID, NAME, PASSWORD) VALUES (?, ?, ?)
	bind => [8, name007, password]
.
.
.

まずはpersistのログはこのようになります。

INSERTに使われている4つのConnectionのオブジェクトIDがエンティティのidによって変わっていることから、上手く複数のデータベースに分散されていることが分かります。

[EL Fine]: sql: 2015-02-05 18:48:18.842--ServerSession(254749889)--Connection(2045036434)--SELECT ID, NAME, PASSWORD FROM USER WHERE (ID = ?)
	bind => [3]

次にパーティショニングのキーにしたidによる検索ですが、1つのデータベースに対してのみクエリが発行されていることが分かります。

[EL Fine]: sql: 2015-02-05 18:48:18.845--ServerSession(254749889)--Connection(2135449562)--SELECT ID, NAME, PASSWORD FROM USER WHERE (NAME = ?)
	bind => [name004]
[EL Fine]: sql: 2015-02-05 18:48:18.845--ServerSession(254749889)--Connection(980697799)--SELECT ID, NAME, PASSWORD FROM USER WHERE (NAME = ?)
	bind => [name004]
[EL Fine]: sql: 2015-02-05 18:48:18.846--ServerSession(254749889)--Connection(2075809815)--SELECT ID, NAME, PASSWORD FROM USER WHERE (NAME = ?)
	bind => [name004]
[EL Fine]: sql: 2015-02-05 18:48:18.846--ServerSession(254749889)--Connection(2045036434)--SELECT ID, NAME, PASSWORD FROM USER WHERE (NAME = ?)
	bind => [name004]

それに対しnameによる検索は4つのデータベースすべてに対してクエリが発行されています。

このように扱い方は通常のJPAのまま透過的にパーティショニングの恩恵を受けることができます。

まとめ

いかがでしょうか。EclipseLink縛りになりますが大規模ソーシャルゲームなどには欠かせないパーティショニングをとても手軽に扱うことができます。

HibernateにはHibernate ShardsというEclipseLinkと同じようなパーティショニングの拡張がありましたが、現在はメンテナンスが行われていないようです。

今回のサンプルにはMavenのCargoプラグインでGlassFishへのデプロイや、JMeterを使った分散負荷テスト、JAX-RS / CDI / JPAの参考にもなると思います。ぜひご自身の環境にクローンしてmvn clean verifyしてみてください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>