CLOVER🍀

That was when it all began.

Hazelcastのインデックス利用時の挙動を調べてみる

ちょっと興味がありまして、Hazelcastのインデックスの利用の様子と、クエリ実行時の挙動を調べてみました。

Hazelcastは、Distributed Mapに格納するオブジェクトのプロパティに対して、インデックスを張ることができます。

Indexing
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#indexing

またクエリ実行時の挙動の確認には、SQL Queryを使うことにします。

Distributed SQL Query
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#distributed-sql-query

では、見ていってみましょう。

準備

ビルド定義。
pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.llittlewings.hazelcast.indexing</groupId>
    <artifactId>hazelcast-indexing</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>hazelcast-indexing</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.5</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.0.0</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
</project>

Hazelcast 3.5がリリースされていたので、今回利用してみました。テストコード用にJUnitとAssertJも利用します。

Distributed Mapの値として格納するクラスは、以下とします。
src/main/java/org/llittlewings/hazelcast/indexing/Book.java

package org.llittlewings.hazelcast.indexing;

import java.io.Serializable;
import java.util.Objects;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    private String isbn;

    private String title;

    private int price;

    public Book(String isbn, String title, int price) {
        this.isbn = isbn;
        this.title = title;
        this.price = price;
    }

    public String getIsbn() {
        return isbn;
    }

    public String getTitle() {
        return title;
    }

    public int getPrice() {
        return price;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Book) {
            Book other = (Book) o;

            return isbn.equals(other.isbn) && title.equals(other.title) && price == other.price;
        }

        return false;
     }

    @Override
    public int hashCode() {
        return Objects.hash(isbn, title, price);
    }
}

また、Hazelcastの設定はこのようにしました。
src/test/resources/hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.5.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <group>
        <name>my-cluster</name>
        <password>my-cluster-pass</password>
    </group>

    <network>
        <port auto-increment="true" port-count="100">5701</port>
        <join>
            <multicast enabled="true">
                <multicast-group>224.2.2.3</multicast-group>
                <multicast-port>54327</multicast-port>
            </multicast>
        </join>
    </network>

    <map name="default">
        <indexes>
            <index ordered="false">title</index>
            <index ordered="true">price</index>
        </indexes>
    </map>
</hazelcast>

デフォルトのDistributed Mapに対する設定ですが、titleとpriceにインデックスを張っています。また、priceについては順序を意識する形でインデックス定義しています。

    <map name="default">
        <indexes>
            <index ordered="false">title</index>
            <index ordered="true">price</index>
        </indexes>
    </map>

テストコードで利用する、簡単なクラスタ構築用のクラス。
src/test/java/org/llittlewings/hazelcast/indexing/HazelcastTestSupport.java

package org.llittlewings.hazelcast.indexing;

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;

public abstract class HazelcastTestSupport {
    protected void withHazelcast(int numInstances, Consumer<HazelcastInstance> f) {
        int initialPort = 5701;
        List<HazelcastInstance> instances =
                IntStream
                        .rangeClosed(1, numInstances)
                        .mapToObj(i -> {
                            ClasspathXmlConfig config = new ClasspathXmlConfig("hazelcast.xml");
                            config.setInstanceName("MyHazelcastInstance-" + (initialPort + i - 1));
                            return Hazelcast.newHazelcastInstance(config);
                        })
                        .collect(Collectors.toList());

        try {
            f.accept(instances.get(0));
        } finally {
            instances
                    .stream()
                    .forEach(h -> h.getLifecycleService().shutdown());
            Hazelcast.shutdownAll();
        }
    }
}

指定数分だけ、JavaVM内でHazelcastのNodeを起動することができます。HazelcastInstanceには、わかりやすいようにポート番号をインスタンス名に含めるようにしました。

インデックスは、どこで、誰が保持するのか?

まずは、インデックスがどこで保持されるのかというところを確認したいと思います。

実装からいくと、以下のクラスで持つことになるようです。

順序付けを意識しない場合。
https://github.com/hazelcast/hazelcast/blob/v3.5/hazelcast/src/main/java/com/hazelcast/query/impl/UnsortedIndexStore.java

順序を意識する場合。
https://github.com/hazelcast/hazelcast/blob/v3.5/hazelcast/src/main/java/com/hazelcast/query/impl/SortedIndexStore.java

順序不要の場合はConcurrentHashMap、順序付けが必要な場合はConcurrentSkipListMap+ConcurrentHashMapで持つようになっています。クラスタ化されているわけでもなく、ローカルのメモリのみに持っているという感じですね。

あとは、これのインデックスを誰が持っているのかということですね。

この確認のために、こんなテストクラスを用意しました。
src/test/java/org/llittlewings/hazelcast/indexing/IndexingTest.java

package org.llittlewings.hazelcast.indexing;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import com.hazelcast.core.IMap;
import com.hazelcast.core.PartitionService;
import org.junit.Test;

public class IndexingTest extends HazelcastTestSupport {
    @Test
    public void indexingTest() {
        List<Book> books =
                Arrays.asList(
                        new Book("978-4774169316", "Javaエンジニア養成読本", 2138),
                        new Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536),
                        new Book("978-4873117188", "Javaパフォーマンス", 4212)
                );

        withHazelcast(3, hazelcast -> {
            IMap<String, Book> map = hazelcast.getMap("default");

            books.stream().forEach(b -> map.put(b.getIsbn(), b));

            try {
                System.out.println("Sleeping...");
                TimeUnit.SECONDS.sleep(10L);
            } catch (InterruptedException e) { }

            PartitionService ps = hazelcast.getPartitionService();
            System.out.printf(
                    "%s:%s => %s%n",
                    "978-4774169316",
                    "Javaエンジニア養成読本",
                    ps.getPartition("978-4774169316").getOwner()
            );
            System.out.printf(
                    "%s:%s => %s%n",
                    "978-4798124605",
                    "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
                    ps.getPartition("978-4798124605").getOwner()
            );
            System.out.printf(
                    "%s:%s => %s%n",
                    "978-4873117188",
                    "Javaパフォーマンス",
                    ps.getPartition("978-4873117188").getOwner()
            );
        });
    }
}

テストはしていませんけど…。

Distributed Mapにデータ登録後、少しスリープしてから、実際にキーに対応するエントリがどのNodeに配置されているかを表示しています。

とはいえ、これだけだとわからないのでBytemanのスクリプトを書いて、インデックスへの登録時にコンソール出力するようにしてみました。
※複数Nodeいるからか、デバッガーだとちょっと苦しかったです
indexing-trace.btm

RULE trace UnsortedIndexStore
CLASS com.hazelcast.query.impl.UnsortedIndexStore
METHOD newIndex
AT ENTRY
IF TRUE
  DO traceln("[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = " + $1 + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

RULE trace SortedIndexStore
CLASS com.hazelcast.query.impl.SortedIndexStore
METHOD newIndex
AT ENTRY
IF TRUE
  DO traceln("[Trace SortedIndexStore] SortedIndexStore, newValue = " + $1 + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

このBytemanスクリプトを含めて、テストコードを動かしてみます。

$ mvn test -Dtest=*IndexingTest -DargLine=-javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:indexing-trace.btm

今回は3 Nodeになるようにしているので、クラスタが3つのNodeで構成されます。

Members [3] {
	Member [192.168.254.129]:5701 this
	Member [192.168.254.129]:5702
	Member [192.168.254.129]:5703
}

6 20, 2015 6:59:33 午後 com.hazelcast.cluster.ClusterService
情報: [192.168.254.129]:5702 [my-cluster] [3.5] 

Members [3] {
	Member [192.168.254.129]:5701
	Member [192.168.254.129]:5702 this
	Member [192.168.254.129]:5703
}

6 20, 2015 6:59:33 午後 com.hazelcast.cluster.ClusterService
情報: [192.168.254.129]:5703 [my-cluster] [3.5] 

Members [3] {
	Member [192.168.254.129]:5701
	Member [192.168.254.129]:5702
	Member [192.168.254.129]:5703 this
}

そして、インデックス登録時のログが出力されます。

[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Javaエンジニア養成読本 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Javaエンジニア養成読本 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-0]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4536 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-0]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4536 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-0]
[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Javaパフォーマンス :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-6]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4212 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-6]
Sleeping...
[Trace SortedIndexStore] SortedIndexStore, newValue = 4212 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-6]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4536 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-0]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4212 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-6]

最後にスレッド名を出力していて、スレッド名にHazelcastのインスタンス名が入るため、どのNodeにインデックスが入ったかが少しは見やすいと思います。
※実行ごとに、結果は変わります

また、キーの配置状況は、このようになっています。

978-4774169316:Javaエンジニア養成読本 => Member [192.168.254.129]:5702
978-4798124605:Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava => Member [192.168.254.129]:5703
978-4873117188:Javaパフォーマンス => Member [192.168.254.129]:5703

ということは、データのオーナーとなったNodeにインデックスが作成されたということですね。

それにしても、SortedIndexStoreにはやたらインデックス登録のリクエストが発行されるんですねぇ…。

また、この後クラスタ内のNodeが減っていきますが

Members [2] {
	Member [192.168.254.129]:5702 this
	Member [192.168.254.129]:5703
}

6 20, 2015 7:03:42 午後 com.hazelcast.cluster.ClusterService
情報: [192.168.254.129]:5703 [my-cluster] [3.5] 

Members [2] {
	Member [192.168.254.129]:5702
	Member [192.168.254.129]:5703 this
}

それに伴い再度インデックス作成のログが出力されます。

情報: [192.168.254.129]:5703 [my-cluster] [3.5] Removing Member [192.168.254.129]:5702
[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Javaエンジニア養成読本 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5703.partition-operation.thread-1]

これは、Nodeが減った時にデータの再配置が発生するのですが、減ってしまったNodeがそのエントリのオーナーだった場合には別のNodeがオーナーになった際にインデックスが再作成されるということですね。

この例だと、5702ポートを使用していたNodeがダウンしたわけですが、「Javaエンジニア養成読本」のエントリのオーナーとなっていたのはこのNodeでした。

クエリを実行してみる

それでは続いて、クエリを投げてみましょう。

今度は、このようなテストコードを用意しました。
src/test/java/org/llittlewings/hazelcast/indexing/QueryTest.java

package org.llittlewings.hazelcast.indexing;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import com.hazelcast.core.IMap;
import com.hazelcast.core.PartitionService;
import com.hazelcast.query.SqlPredicate;
import org.junit.Test;

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

public class QueryTest extends HazelcastTestSupport {
    @Test
    public void testQuery() {
        List<Book> books =
                Arrays.asList(
                        new Book("978-4774169316", "Javaエンジニア養成読本", 2138),
                        new Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536),
                        new Book("978-4873117188", "Javaパフォーマンス", 4212)
                );

        withHazelcast(3, hazelcast -> {
            IMap<String, Book> map = hazelcast.getMap("default");

            books.stream().forEach(b -> map.put(b.getIsbn(), b));

            PartitionService ps = hazelcast.getPartitionService();
            System.out.printf(
                    "%s:%s => %s%n",
                    "978-4774169316",
                    "Javaエンジニア養成読本",
                    ps.getPartition("978-4774169316").getOwner()
            );
            System.out.printf(
                    "%s:%s => %s%n",
                    "978-4798124605",
                    "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
                    ps.getPartition("978-4798124605").getOwner()
            );
            System.out.printf(
                    "%s:%s => %s%n",
                    "978-4873117188",
                    "Javaパフォーマンス",
                    ps.getPartition("978-4873117188").getOwner()
            );

            SqlPredicate titleQuery = new SqlPredicate("title = 'Javaエンジニア養成読本'");
            Collection<Book> booksByTitleQuery = map.values(titleQuery);

            assertThat(booksByTitleQuery)
                    .hasSize(1)
                    .containsOnly(new Book("978-4774169316", "Javaエンジニア養成読本", 2138));

            SqlPredicate titleWithLikeQuery = new SqlPredicate("title LIKE '%Java%' AND title LIkE '%養成読本'");
            Collection<Book> booksByTitleWithLikeQuery = map.values(titleWithLikeQuery);

            assertThat(booksByTitleWithLikeQuery)
                    .hasSize(1)
                    .containsOnly(new Book("978-4774169316", "Javaエンジニア養成読本", 2138));

            SqlPredicate priceQuery = new SqlPredicate("price > 4000");
            Collection<Book> booksByPriceQuery = map.values(priceQuery);

            assertThat(booksByPriceQuery)
                    .hasSize(2)
                    .containsSequence(
                            new Book("978-4873117188", "Javaパフォーマンス", 4212),
                            new Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536)
                    );
        });
    }
}

今度は、テストも入っています(笑)。

先ほどと同様、エントリのオーナーの状況も表示した後、クエリを3回実行します。この時の様子をトレースしたいと思います。

Bytemanスクリプトも、クエリ実行時の様子が追えるように追加しました。
indexing-trace.btm

RULE trace UnsortedIndexStore
CLASS com.hazelcast.query.impl.UnsortedIndexStore
METHOD newIndex
AT ENTRY
IF TRUE
  DO traceln("[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = " + $1 + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

RULE trace SortedIndexStore
CLASS com.hazelcast.query.impl.SortedIndexStore
METHOD newIndex
AT ENTRY
IF TRUE
  DO traceln("[Trace SortedIndexStore] SortedIndexStore, newValue = " + $1 + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

RULE trace QueryOperation
CLASS com.hazelcast.map.impl.operation.QueryOperation
METHOD run
AT ENTRY
IF TRUE
  DO traceln("[Trace Query] " + $0.predicate + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

RULE trace IndexService
CLASS com.hazelcast.query.impl.IndexService
METHOD query
AT EXIT
IF TRUE
  DO traceln("[Trace IndexService] " + $1 + ", " + $! + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

RULE trace BasicMapContextQuerySupport
CLASS com.hazelcast.map.impl.BasicMapContextQuerySupport
METHOD queryOnPartition
AT EXIT
IF TRUE
  DO traceln("[Trace BasicMapContextQuerySupport] " + $2 + ", " + $! + " :: Thread[" + Thread.currentThread().getName()  + "]")
ENDRULE

追加したクラスについては、また後で。

では、実行してみます。

$ mvn test -Dtest=*QueryTest -DargLine=-javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:indexing-trace.btm

クラスタが構成された後、インデックス登録とエントリの配置状況がこのように表示されます。

[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Javaエンジニア養成読本 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava :: Thread[hz.MyHazelcastInstance-5701.partition-operation.thread-0]
[Trace SortedIndexStore] SortedIndexStore, newValue = 2138 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-1]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4536 :: Thread[hz.MyHazelcastInstance-5701.partition-operation.thread-0]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4536 :: Thread[hz.MyHazelcastInstance-5701.partition-operation.thread-0]
[Trace UnsortedIndexStore] UnsortedIndexStore, newValue = Javaパフォーマンス :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-6]
[Trace SortedIndexStore] SortedIndexStore, newValue = 4212 :: Thread[hz.MyHazelcastInstance-5702.partition-operation.thread-6]


978-4774169316:Javaエンジニア養成読本 => Member [192.168.254.129]:5702
978-4798124605:Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava => Member [192.168.254.129]:5701 this
978-4873117188:Javaパフォーマンス => Member [192.168.254.129]:5702

今度は、5702に寄りました…。

最初のクエリ。「title = 'Javaエンジニア養成読本'」です。

            SqlPredicate titleQuery = new SqlPredicate("title = 'Javaエンジニア養成読本'");
            Collection<Book> booksByTitleQuery = map.values(titleQuery);

            assertThat(booksByTitleQuery)
                    .hasSize(1)
                    .containsOnly(new Book("978-4774169316", "Javaエンジニア養成読本", 2138));

この時のログは、このようになります。なお、titleは順序付けについてのインデックス定義はしていません。

[Trace Query] title=Javaエンジニア養成読本 :: Thread[main]
[Trace IndexService] title=Javaエンジニア養成読本, [] :: Thread[main]
[Trace Query] title=Javaエンジニア養成読本 :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-0]
[Trace Query] title=Javaエンジニア養成読本 :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-3]
[Trace IndexService] title=Javaエンジニア養成読本, [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-0]
[Trace IndexService] title=Javaエンジニア養成読本, [com.hazelcast.query.impl.QueryEntry@eba933f0] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-3]

ここで、mainスレッドは実行した本人のようです(5701ポートのNodeと同義)。

クエリは、各Nodeで分散実行されるみたいですね。

こう見ると、3つのNodeで検索が実行され、うち5702のポートを使用するNodeで結果が見つかったようです(オーナーですし)。

[Trace IndexService] title=Javaエンジニア養成読本, [com.hazelcast.query.impl.QueryEntry@eba933f0] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-3]

続いて、LIKEの場合。

            SqlPredicate titleWithLikeQuery = new SqlPredicate("title LIKE '%Java%' AND title LIkE '%養成読本'");
            Collection<Book> booksByTitleWithLikeQuery = map.values(titleWithLikeQuery);

            assertThat(booksByTitleWithLikeQuery)
                    .hasSize(1)
                    .containsOnly(new Book("978-4774169316", "Javaエンジニア養成読本", 2138));

こちらは、かなり膨大なログが出力されます。

[Trace Query] (title LIKE '%Java%' AND title LIKE '%養成読本') :: Thread[main]
[Trace IndexService] (title LIKE '%Java%' AND title LIKE '%養成読本'), null :: Thread[main]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[main]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[main]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[main]

〜省略〜

[Trace Query] (title LIKE '%Java%' AND title LIKE '%養成読本') :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace IndexService] (title LIKE '%Java%' AND title LIKE '%養成読本'), null :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace Query] (title LIKE '%Java%' AND title LIKE '%養成読本') :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace IndexService] (title LIKE '%Java%' AND title LIKE '%養成読本'), null :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [com.hazelcast.query.impl.QueryEntry@eba933f0] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]

〜省略〜

[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-2]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]
[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]

よ〜く見ると、ここでエントリを見つけています。

[Trace BasicMapContextQuerySupport] (title LIKE '%Java%' AND title LIKE '%養成読本'), [com.hazelcast.query.impl.QueryEntry@eba933f0] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-0]

最初にインデックスから探して、その後にフルスキャンしているみたいですね。
※持っているデータ数の割には、フルスキャン時のログがやたら多いのが気になりますが…。

実際、このような実装になっているようです。最初にインデックスから探し、そこで結果が得られなければフルスキャンに移行します。
https://github.com/hazelcast/hazelcast/blob/v3.5/hazelcast/src/main/java/com/hazelcast/map/impl/operation/QueryOperation.java#L92

この時、インデックスを使った検索時には以下のクラスが使用され、
https://github.com/hazelcast/hazelcast/blob/v3.5/hazelcast/src/main/java/com/hazelcast/query/impl/IndexService.java

フルスキャンの時には以下のクラスが使用されるようです。
https://github.com/hazelcast/hazelcast/blob/v3.5/hazelcast/src/main/java/com/hazelcast/map/impl/BasicMapContextQuerySupport.java

なるほど。

なお、フルスキャンも各Nodeで実行されます、と。

追記
デフォルトはNode単位のスキャンはシングルスレッドで動作し、「hazelcast.query.predicate.parallel.evaluation」をtrueにすることで並列実行になるらしいです。デフォルトはfalseらしいのですが、今回見ているとスレッド名が途中で変わっていたようにも見えましたが…?

System Properties
http://docs.hazelcast.org/docs/3.5/manual/html-single/hazelcast-documentation.html#system-properties

最後は、「price > 4000」。

            SqlPredicate priceQuery = new SqlPredicate("price > 4000");
            Collection<Book> booksByPriceQuery = map.values(priceQuery);

            assertThat(booksByPriceQuery)
                    .hasSize(2)
                    .containsSequence(
                            new Book("978-4873117188", "Javaパフォーマンス", 4212),
                            new Book("978-4798124605", "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", 4536)
                    );

出力されたログ。

[Trace Query] price>4000 :: Thread[main]
[Trace IndexService] price>4000, [com.hazelcast.query.impl.QueryEntry@b8dbfd77] :: Thread[main]
[Trace Query] price>4000 :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-2]
[Trace IndexService] price>4000, [com.hazelcast.query.impl.QueryEntry@5a52fd10] :: Thread[hz.MyHazelcastInstance-5702.generic-operation.thread-2]
[Trace Query] price>4000 :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-1]
[Trace IndexService] price>4000, [] :: Thread[hz.MyHazelcastInstance-5703.generic-operation.thread-1]

こちらは、インデックスを利用した検索で完結するみたいですね。

なんとなく、挙動が見えた感じですね。

まとめ

Hazelcastのインデックスの持ち方と、クエリ実行時の挙動を追ってみました。

インデックスは、まとめると以下のような感じですね。

  • 順序を意識しないのであれば、UnsortedIndexStoreで保持される(ConcurrentHashMapで保持)
  • 順序を意識するのであれば、SortedIndexStoreで保持される(ConcurrentSkipListMap+ConcurrentHashMapで保持)
  • インデックスそのものは、データのオーナーとなるNodeがローカルメモリに保持する
  • オーナーのNodeがいなくなった場合は、データのマイグレーション時に再インデックスされる

クエリ実行時については、以下の感じですね。

  • クエリは、各Nodeで分散実行
  • 最初に、インデックスを使って検索、見つかればそこで終了
  • インデックスを使って見つからなかった場合は、フルスキャンへ移行
  • フルスキャンは各Nodeでそれぞれ実行される
  • Node内で並列実行したい場合は、「hazelcast.query.predicate.parallel.evaluation」をtrueにする

※また、「hazelcast.query.result.size.limit」でクエリが戻す最大件数も制御できるようです(デフォルトは「-1で、制限なし)

今回、ソースや挙動から追ってみましたが、よい勉強になりました。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-indexing