CLOVER🍀

That was when it all began.

Apache GeodeでのBucketとキーの配置について(概要/API編)

Apache GeodeにおけるPartitioned Regionでは、Bucketと呼ばれる単位で各メンバーの領域が切り分けられ、管理されるようです。

Partitioned Regionについてのドキュメントは、こちら。

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/chapter_overview.html

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/how_partitioning_works.html

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/managing_partitioned_regions.html

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/configuring_bucket_for_pr.html

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/custom_partitioning_and_data_colocation.html

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/rebalancing_pr_data.html

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/how_pr_ha_works.html

Partitioned Regionは、全メンバーがすべて同じデータを持つのではなく、各メンバーにデータを分散配置します。データへのアクセス時に、どのメンバーにデータが配置されているかについて、利用側は意識する必要はありません。

ですが、どのようにデータが配置されているかは知っておきたいものですし、Functionやトランザクションを使う場合には関連するデータが同じメンバーに集まるように調整した方がよいこともあります。

というわけで、今回このあたりを調べてみました。ちょっと長くなるので2回に分けて書きます。それぞれ、以下のテーマで。

  • Partitioned RegionのBucketとデータの分散状況を確認するためのAPIについて(ただし、単一メンバー
  • クラスタを構成して、複数のメンバー間でデータが分散して配置されている様子を確認する

今回は、概要とAPIについてですね。

追記
後編も書きました。
Apache GeodeでのBucketとキーの配置について(クラスタ動作確認編) - CLOVER

Partitioned Regionについて

Partitioned Regionは、全メンバーが同じデータを複製して持つReplicated Regionと異なり、データはメンバー間で分散配置するものの、データにアクセスする側から見ると仮想的な大きなRegionのように見える形態です。

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/how_partitioning_works.html

クラスタ構成時には、追加したメンバーの数だけ利用できるメモリ、リソースがリニアにスケールする構成です。

各メンバーについては、データを保持する/しないであったり、どのくらいのデータを持つのかを設定することができるようです。データの保存場所を、Data Storageというみたいですね。

データの配置先は、Apache Geodeによって自動的に決定されますが、データの保存場所を「Bucket」と呼ばれる単位に区切って配置します。

データ保存時、作成されたエントリはBucketに割り当てられ、データのリバランスはこのBucket単位で行われます。また、データの配置についてはカスタマイズしてコントロールすることが可能です。

なお、Partitioned Regionでは、オペレーションを行ったメンバーと実際のデータの保存先のメンバーが異なる可能性があるため、その場合はメンバー間の通信が発生します。とはいえ、他のメンバーに実際のデータが存在する場合であっても、1ホップを越える経路にはなりません。

Bucketについて

Bucketは、Partitioned Regionを構成する各メンバーが持つデータ保存領域を区切ったものです。

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/configuring_bucket_for_pr.html

どのくらいのBucket数を持つのかは、設定で決めることができます。初期値は113です。デフォルトでは、ひとつのPartitioned Regionは113個のBucketに分かれていることになります。

Bucketの数をどのくらいにすればよいかですが、少なくとも(データを持つことができる)メンバー数の4倍以上の素数、だそうです。

デフォルトは113バケットなので、メンバー数が30を越えるくらいなったら増やした方がよいのでしょう。

Bucket数を多くすると、各メンバーにより均等に負荷を割り当てることができますが、トレードオフとしてはバランシングやBucketの管理のオーバーヘッドが発生します。

データの移動などは、Bucket単位で行われることになるので。

冗長化について

Partitioned Regionにおける、データのPrimary、Redundant(Secondry)の考え方について。

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/how_pr_ha_works.html

Partitioned Regionでは、メンバーがダウンした時のためにバックアップを保持しておき、Primaryがロストした場合にはバックアップから復旧します。どのくらいの数のバックアップを持つのかは設定することができますが、多くのバックアップを持つとメモリも消費することになるため、クラスタ全体で利用できる総メモリ量が減少することになります。

なお、メンバーダウン時にはバックアップからの自動的な復旧が行われるため、この時にリバランスも行われます。

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/rebalancing_pr_data.html

バックアップがない場合には、メンバーダウン時にはデータは失われます。

バックアップの保持先は、デフォルトではPrimaryと異なる物理マシンとなるようですが、カスタマイズも可能なようです。

データの配置先のコントロールについて

例えば、関連するデータについては同じメンバーに保存するようにコントロールすることができます。

http://geode.docs.pivotal.io/docs/developing/partitioned_regions/custom_partitioning_and_data_colocation.html

が、今回はこちらは省略…。

関連するAPIを使って試してみる

とまあ、ここまで説明を書いてきましたが、ここからは実際にBucketなどについての情報にApache Geode上のAPIでアクセスしてみましょう。

準備

まずは、Maven依存関係。

        <dependency>
            <groupId>org.apache.geode</groupId>
            <artifactId>gemfire-core</artifactId>
            <version>1.0.0-incubating.M1</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.3.0</version>
            <scope>test</scope>
        </dependency>

利用するApache Geodeバージョンは、1.0.0-incubating.M1とします。JUnitとAssertJは、テストコード用です。

テストコードの雛形

ここから先で各テストコードは、以下のクラス定義の中で実装するものとします。
src/test/java/org/littlewings/geode/bucketkey/PartitionedBucketTest.java

package org.littlewings.geode.bucketkey;

import java.util.Set;
import java.util.stream.IntStream;

import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.CacheFactory;
import com.gemstone.gemfire.cache.PartitionAttributesFactory;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.RegionFactory;
import com.gemstone.gemfire.cache.RegionShortcut;
import com.gemstone.gemfire.cache.partition.PartitionRegionHelper;
import com.gemstone.gemfire.distributed.DistributedMember;
import com.gemstone.gemfire.internal.cache.BucketRegion;
import com.gemstone.gemfire.internal.cache.PartitionedRegion;
import com.gemstone.gemfire.internal.cache.PartitionedRegionDataStore;
import com.gemstone.gemfire.internal.cache.PartitionedRegionHelper;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class PartitionedBucketTest {
    // ここに、テストを書く!
}

では、テーマに応じて書いていってみます。

Bucket数を確認する

最初は、Partitioned RegionにどのくらいBucketがあるのか確認してみましょう。

    @Test
    public void defaultNumberOfBucket() {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, String> region =
                    cache.<String, String>createRegionFactory(RegionShortcut.PARTITION_REDUNDANT).create("sampleRegion");

            PartitionedRegion partitionedRegion = (PartitionedRegion) region;
            assertThat(partitionedRegion.getTotalNumberOfBuckets())
                    .isEqualTo(113);
        }
    }

Apache Geodeの内部のクラスですが、PartitionedRegionにキャストすることでBucket数を得ることができます。デフォルトでは、113ですね。

Bucket数を変えてみましょう。今回は、Java APIで変更してみます。

    @Test
    public void configureNumberOfBucket() {
        try (Cache cache = new CacheFactory().create()) {
            RegionFactory regionFactory =
                    cache.createRegionFactory(RegionShortcut.PARTITION_REDUNDANT);
            regionFactory.setPartitionAttributes(new PartitionAttributesFactory().setTotalNumBuckets(257).create());
            Region<String, String> region = regionFactory.create("sampleRegion");

            PartitionedRegion partitionedRegion = (PartitionedRegion) region;
            assertThat(partitionedRegion.getTotalNumberOfBuckets())
                    .isEqualTo(257);
        }
    }

Bucket数を257にしてみました。PartitionedRegion#getTotalNumberOfBucketsの結果にも、反映されていますね。

Cache XMLで設定する場合は、こんな感じみたいです。

<region name="sampleRegion">
  <region-attributes refid="PARTITION">
    <partition-attributes total-num-buckets="257"/>
  </region-attributes> 
</region>

キーに対するPrimaryのOwenerを確認する

次は、あるキーに対して、どのメンバーがPrimaryとなっているかを確認してみます。

こちらを確認するには、PartitionRegionHelperを利用すれば簡単に確認することができます。

    @Test
    public void hasKeyMembers() {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, String> region =
                    cache.<String, String>createRegionFactory(RegionShortcut.PARTITION_REDUNDANT).create("sampleRegion");

            IntStream.rangeClosed(1, 10).forEach(i -> region.put("key" + i, "value" + i));

            Set<DistributedMember> members = PartitionRegionHelper.getAllMembersForKey(region, "key1");
            assertThat(members)
                    .hasSize(1)
                    .containsOnly(cache.getDistributedSystem().getDistributedMember());  // self

            DistributedMember member = PartitionRegionHelper.getPrimaryMemberForKey(region, "key1");
            assertThat(member)
                    .isEqualTo(cache.getDistributedSystem().getDistributedMember());
        }
    }

PartitionRegionHelper#getPrimaryMemberForKeyを使用すれば、キーに対するPrimaryのメンバーを取得することができます。今回はクラスタを構成していないので、どのキーを使用しても必ず自分自身になってしまいますが…。
※そうならないパターンは、次回で確認します

また、PartitionRegionHelper#getAllMembersForKeyを使用することで、指定されたキーに対するエントリを保持している全メンバーを取得することができます。

この方法を、あえてApache Geodeの内部のAPIで確認すると、こういう形になります。

    @Test
    public void hasKeyMembersInternalWay() {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, String> region =
                    cache.<String, String>createRegionFactory(RegionShortcut.PARTITION_REDUNDANT).create("sampleRegion");

            IntStream.rangeClosed(1, 10).forEach(i -> region.put("key" + i, "value" + i));

            PartitionedRegion partitionedRegion = (PartitionedRegion) region;

            // get Bucket by ID
            int bucketIdForKey1 = PartitionedRegionHelper.getHashKey(partitionedRegion, null, "key1", null, null);
            // もしくは
            // int bucketIdForKey1 = PartitionedRegionHelper.getHashKey(partitionedRegion, "key1");

            Set<? extends DistributedMember> members = partitionedRegion.getRegionAdvisor().getBucketOwners(bucketIdForKey1);
            assertThat(members)
                    .hasSize(1)
                    .containsOnly(cache.getDistributedSystem().getDistributedMember());  // self

            DistributedMember member = partitionedRegion.getBucketPrimary(bucketIdForKey1);
            assertThat(member)
                    .isEqualTo(cache.getDistributedSystem().getDistributedMember());
        }
    }

PartitionedRegionHelperを使用すると、キーに対するBucketのIDを取得することができます。
※Partition ed RegionHelperで、先の例ではPartitionRegionHelper

BucketのIDがわかると、そのIDを保持している全メンバーを、PartitionedRegionから取得できるRegionAdvisor#getBucketOwnersを使用して得ることができます。また、BucketのIDが分かっている必要がありますが、PartitionedRegion#getBucketPrimaryを使用することで、Primaryのメンバーを得ることもできます。

キーからBucketRegionを得る

PartitionedRegion#getBucketRegionを使用することで、Bucketに対応するRegion、BucketRegionを取得することができます。

    @Test
    public void bucketForKey() {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, String> region =
                    cache.<String, String>createRegionFactory(RegionShortcut.PARTITION_REDUNDANT).create("sampleRegion");

            IntStream.rangeClosed(1, 10).forEach(i -> region.put("key" + i, "value" + i));

            PartitionedRegion partitionedRegion = (PartitionedRegion) region;

            // get Bucket by ID
            int bucketIdForKey1 = PartitionedRegionHelper.getHashKey(partitionedRegion, null, "key1", null, null);
            // もしくは
            // int bucketIdForKey1 = PartitionedRegionHelper.getHashKey(partitionedRegion, "key1");

            // BucketRegion from key
            BucketRegion bucketRegion = partitionedRegion.getBucketRegion("key1");

            assertThat(bucketIdForKey1)
                    .isEqualTo(bucketRegion.getId());
            assertThat(bucketRegion)
                    .hasSize(1);
        }
    }

とりあえず、PartitionedRegionHelperでキーを指定した場合から得られたBucketのIDと、同じキーを指定して取得したRegionBucketから取得したIDが同じになることを確認してみました…。

Data Store

最後は、Data Storeです。

PartitionedRegion#getDataStoreで、PartitionedRegionDataStoreを得ることができます。

    @Test
    public void dataStore() {
        try (Cache cache = new CacheFactory().create()) {
            Region<String, String> region =
                    cache.<String, String>createRegionFactory(RegionShortcut.PARTITION_REDUNDANT).create("sampleRegion");

            IntStream.rangeClosed(1, 10).forEach(i -> region.put("key" + i, "value" + i));

            PartitionedRegion partitionedRegion = (PartitionedRegion) region;
            PartitionedRegionDataStore dataStore = partitionedRegion.getDataStore();

            BucketRegion bucketRegion = dataStore.getLocalBucketByKey("key1");

            assertThat(dataStore.getSizeOfLocalPrimaryBuckets())
                    .isEqualTo(10);

            dataStore.getAllLocalBucketIds();
            dataStore.getAllLocalBucketRegions();
            dataStore.getAllLocalBuckets();
            dataStore.getAllLocalPrimaryBucketIds();
            dataStore.getAllLocalPrimaryBucketRegions();
        }
    }

ここから、ローカルに保持しているBucketのIDやBucketRegionなどが取得できそうな感じです。Primaryがわかりそうなのもよいですね。

            BucketRegion bucketRegion = dataStore.getLocalBucketByKey("key1");

            assertThat(dataStore.getSizeOfLocalPrimaryBuckets())
                    .isEqualTo(10);

            dataStore.getAllLocalBucketIds();
            dataStore.getAllLocalBucketRegions();
            dataStore.getAllLocalBuckets();
            dataStore.getAllLocalPrimaryBucketIds();
            dataStore.getAllLocalPrimaryBucketRegions();

まとめ

Partitioned Regionに対する内容をちょっと深堀りしたのと、Bucketという単位でデータが管理されていることを調べてみました。また、PartitionedRegionまわりのAPIを使用して、BucketのIDやPrimaryの配置メンバーを確認するためのAPIを試してみました。

今回の動作確認では、クラスタ構成ではなく単一メンバーでの確認だったので、次回にクラスタ構成でこれらのAPIを使用してデータが分散配置されている様子を確認してみたいと思います。

追記
後編も書きました。
Apache GeodeでのBucketとキーの配置について(クラスタ動作確認編) - CLOVER