CLOVER🍀

That was when it all began.

Hazelcastの設定をしてみよう

今まで、Hazelcastを使ってプログラムを書いている時はずっとデフォルトの設定だったのですが、そろそろ設定ファイルを書いてみようと思います。

Configuration
http://www.hazelcast.com/docs/3.1/manual/single_html/#Config

Hazelcastの設定には、XMLファイルによる設定とAPIによる設定があるのですが、それなりに項目が多かったり複雑な場合は設定ファイルで書く方が好きなので、XMLファイルで書きます。

設定ファイルのロード場所は、「-Dhazelcast.config」システムプロパティでファイルパスを指定するか、以下のクラスで指定する方法があるようです。

  • XmlConfigBuilder
  • ClasspathXmlConfig
  • FileSystemXmlConfig
  • UrlXmlConfig
  • InMemoryXmlConfig

今回は、ClasspathXmlConfigを使用します。

そもそも、デフォルトの設定ファイルは?

Hazelcastのデフォルトの設定ファイルは、こちらになります。

https://github.com/hazelcast/hazelcast/blob/master/hazelcast/src/main/resources/hazelcast-default.xml

いろいろ設定が書かれていますし、コメントもあるのでまずはこちらを読むのがよいでしょう。

では、いくつか以下のXML内に設定を書きつつ、内容を見ていきましょう。

src/test/resources/hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.1.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</hazelcast>

ルートタグは「hazelcast」、今回はテストコードを書きつつ確認するので、依存関係は以下とします。

libraryDependencies ++= Seq(
  "com.hazelcast" % "hazelcast" % "3.1.3",
  "org.scalatest" %% "scalatest" % "2.0" % "test"
)

ついに、Scalaを持ち出しました…。

ちなみに、設定はもちろん網羅できないので、基本的なところから押さえていこうと思います。

クラスタの設定

クラスタ分割設定ですが、クラスタに設定する名前が重要みたいです。まあ、割とよくある設定ですね。

  <group>
    <!-- クラスタを分ける場合は、クラスタごとに名前を設定する -->
    <name>my-cluster</name>
    <password>my-cluster-password</password>
  </group>

今回は、クラスタの名前を「my-cluster」、パスワードを「my-cluster-password」としました。

ネットワークの設定

続いて、ネットワークの設定。

  <network>
    <!-- auto-incrementをfalseにした場合は、各クラスタメンバーに
         portを個別に指定する -->
    <port auto-increment="true">5701</port>
    <join>
    <!-- マルチキャストやTCPの設定 -->
    </join>
  </network>

portタグでは、Hazelcastがリッスンするポートを指定します。auto-increment属性をtrueにしていれば、同じポートが使用されていた場合はインクリメントして別のポートを使用します。もちろん、falseにした場合は同じポートを使用すると、起動に失敗します。

続いて、マルチキャストの設定。

      <multicast enabled="true">
        <multicast-group>224.2.2.3</multicast-group>
        <multicast-port>54327</multicast-port>
      </multicast>

Hazelcastは、Nodeの探索にはマルチキャストかTCPを使用します。この例ではmulticastタグのenabled属性をtrueにしているので、マルチキャストを使用します。falseにした場合は、後述するTCPの設定を行いましょう。

TCPで設定する場合は、クラスタに参加するNodeを列挙する形になります。

      <tcp-ip enabled="false">
        <member>localhost:5701</member>
        <member>localhost:5702</member>
        <!-- こういうのも可能らしい
             <member>machine1</member>
             <member>machine2</member>
             <member>machine3:5799</member>
             <member>192.168.1.0-7</member>
             <member>192.168.1.21</member>
        -->
      </tcp-ip>

今回は、tcp-ipタグのenabled属性をfalseにしているので、TCPは使用しません。

ネットワークインターフェースの指定も、可能みたいですよ。

      <!-- バインドするネットワークインターフェースの指定も可能
      <interfaces enabled="true">
        <interface>10.3.16.*</interface>
        <interface>10.3.10.4-18</interface>
        <interface>192.168.1.3</interface>       
      </interfaces>
      -->

IMapを設定してみよう

Hazelcastはサポートしているデータ構造がいろいろあるので、今回は最も使いそうなIMapの設定を行ってみます。

  <!-- IMapのデフォルトの設定 -->
  <map name="default">
    <!-- ここに、IMapの設定を書く -->
  </map>

mapタグのname属性で、指定された名前のIMapに対する設定を行うようなのですが、「default」は特別な名前みたいで、明示的に定義していない名前でIMapを作成した場合は、この「default」の設定を引き継ぐみたいです。

以下の設定は、こちらに載っている内容を主に書いていく感じです。

Distributed Map
http://www.hazelcast.com/docs/3.1/manual/single_html/#Map

IMap内での、データの保存形式。

    <!-- データの保持形式。BINARY、OBJECT、OFFHEAPから選ぶ -->
    <in-memory-format>BINARY</in-memory-format>

デフォルトは、BINARYです。

クラスタ内での、データのバックアップ数。

    <!-- バックアップ数。デフォルトは1。0はバックアップなし -->
    <backup-count>1</backup-count>

タイムアウトの設定。

    <!-- 有効期限の設定 -->
    <!-- TTL。秒で指定。デフォルトは0で、タイムアウトしない -->
    <time-to-live-seconds>5</time-to-live-seconds>
    <!-- アイドルタイムアウト。秒で指定。デフォルトは0で、タイムアウトしない -->
    <max-idle-seconds>0</max-idle-seconds>

TTLとアイドル時間による有効期限を、秒単位で指定できます。デフォルトは0で、有効期限切れしません。

エビクションの設定。

    <!-- エビクションの設定 -->
    <!-- デフォルトはNONEで、エビクションしない。他に、LRU、LFUを指定可能 -->
    <eviction-policy>NONE</eviction-policy>
    <!-- 保持可能な最大数を指定。policyを指定でき、PER_NODEはNode単位。また、0はInteger.MAX_VALUEを意味する -->
    <max-size policy="PER_NODE">0</max-size>
    <!-- その他、指定例 -->

    <!-- パーティション単位
    <max-size policy="PER_PARTITION">27100</max-size>
    -->
    <!-- ヒープサイズ単位(MB)
    <max-size policy="USED_HEAP_SIZE">4096</max-size>
    -->
    <!-- ヒープのパーセンテージ
    <max-size policy="USED_HEAP_PERCENTAGE">75</max-size>
    -->

まずはポリシーで、エビクションの戦略を指定します。デフォルトは「NONE」で、エビクションしません。エビクションを有効にする場合は、どの程度エントリを保持できるかを、様々な単位で設定することができます。

さらに、max-sizeに達した場合に、どのくらいエビクションするのかも指定可能です。

    <!-- max-Sizeに到達した際に、エビクションする比率をパーセンテージで指定。
         25%なら、max-sizeに到達した後に25%のエントリがエビクションされる
    -->
    <eviction-percentage>25</eviction-percentage>

ちょっと面白いのが、分断されたクラスタをマージする時のポリシーを指定できます。

    <!-- 分断されたクラスタがマージされる時に、同じキーで違うバージョンのエントリを持っていた場合、
         どうマージするかを指定する。ビルトインで提供されているポリシーは、全部で4つ。
         com.hazelcast.map.merge.PassThroughMergePolicy:
             PassThroughMergePolicy causes the merging entry to be merged from source to destination map
             if source entry has updated more recently than the destination entry
         com.hazelcast.map.merge.PutIfAbsentMapMergePolicy:
             PutIfAbsentMapMergePolicy causes the merging entry to be merged from source to destination map
             if it does not exist in the destination map.
         com.hazelcast.map.merge.HigherHitsMapMergePolicy:
             HigherHitsMapMergePolicy causes the merging entry to be merged from source to destination map
             if source entry has higher hits than the destination one.
         com.hazelcast.map.merge.LatestUpdateMapMergePolicy:
             LatestUpdateMapMergePolicy causes the merging entry to be merged from source to destination map
             if source entry has updated more recently than the destination entry.

         その他
           http://www.hazelcast.com/docs/3.1/manual/single_html/#NetworkPartitioning
    -->
    <merge-policy>com.hazelcast.map.merge.PassThroughMergePolicy</merge-policy>

4つほど指定できるのですが、コメントからはPassThroughMergePolicyとLatestUpdateMapMergePolicyの違いがわからないです…。それに、どっちがsourceでどっちがdestinationになるんでしょうかねぇ?

IMapの設定自体は、ここまでにします…。他、以下のような設定がありますが

  • Persistence(データの永続化)
  • Indexing(Query実行のための、インデックスの作成)
  • Near Cache(他のNodeのデータのキャッシュ)

今回はパス…。詳しくは、こちらへ。

Distributed Map
http://www.hazelcast.com/docs/3.1/manual/single_html/#Map

あと、明示的に名前を指定してIMapを定義する場合ですが、こんな感じになります。

  <map name="double-backup-map">
    <backup-count>2</backup-count>
  </map>

指定しなかった項目は、「default」の名前で定義したIMapとは関係なく、MapConfigのデフォルト値になる感じ?

http://www.hazelcast.com/docs/3.1/javadoc/com/hazelcast/config/MapConfig.html

ワイルドカードを使用して、定義を作成することもできるので、面白いです。

  <map name="wildcard-map-*">
    <max-idle-seconds>10</max-idle-seconds>
  </map>

Advanced Configuration Properties

その他、propertiesタグとpropertyタグで、いくつか設定を行うことができます。

  <!-- Advanced Configuration Properties
       http://www.hazelcast.com/docs/3.1/manual/single_html/#ConfigurationProperties
  -->
  <properties>
    <property name="hazelcast.memcache.enabled">false</property>
  </properties>

設定項目のリストは、こちら。

Advanced Configuration Properties
http://www.hazelcast.com/docs/3.1/manual/single_html/#ConfigurationProperties

今回紹介した設定以外にも、ログとかいろいろあるので、興味のある方はマニュアルへ…。

ちょっと試してみる

それでは、ここまで書いてきた設定ファイルを使って、簡単なテストプログラムを書いてみましょう。

まずは、設定ファイル全体。
src/test/resources/hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.1.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-password</password>
  </group>

  <network>
    <!-- auto-incrementをfalseにした場合は、各クラスタメンバーに
         portを個別に指定する -->
    <port auto-increment="true">5701</port>
    <join>
      <multicast enabled="true">
        <multicast-group>224.2.2.3</multicast-group>
        <multicast-port>54327</multicast-port>
      </multicast>
      <tcp-ip enabled="false">
        <member>localhost:5701</member>
        <member>localhost:5702</member>
        <!-- こういうのも可能らしい
             <member>machine1</member>
             <member>machine2</member>
             <member>machine3:5799</member>
             <member>192.168.1.0-7</member>
             <member>192.168.1.21</member>
        -->
      </tcp-ip>

      <!-- バインドするネットワークインターフェースの指定も可能
      <interfaces enabled="true">
        <interface>10.3.16.*</interface>
        <interface>10.3.10.4-18</interface>
        <interface>192.168.1.3</interface>       
      </interfaces>
      -->
    </join>
  </network>

  <!-- IMapのデフォルトの設定 -->
  <map name="default">
    <!-- データの保持形式。BINARY、OBJECT、OFFHEAPから選ぶ -->
    <in-memory-format>BINARY</in-memory-format>
    
    <!-- バックアップ数。デフォルトは1。0はバックアップなし -->
    <backup-count>1</backup-count>

    <!-- 有効期限の設定 -->
    <!-- TTL。秒で指定。デフォルトは0で、タイムアウトしない -->
    <time-to-live-seconds>5</time-to-live-seconds>
    <!-- アイドルタイムアウト。秒で指定。デフォルトは0で、タイムアウトしない -->
    <max-idle-seconds>0</max-idle-seconds>

    <!-- エビクションの設定 -->
    <!-- デフォルトはNONEで、エビクションしない。他に、LRU、LFUを指定可能 -->
    <eviction-policy>NONE</eviction-policy>
    <!-- 保持可能な最大数を指定。policyを指定でき、PER_NODEはNode単位。また、0はInteger.MAX_VALUEを意味する -->
    <max-size policy="PER_NODE">0</max-size>
    <!-- その他、指定例 -->

    <!-- パーティション単位
    <max-size policy="PER_PARTITION">27100</max-size>
    -->
    <!-- ヒープサイズ単位(MB)
    <max-size policy="USED_HEAP_SIZE">4096</max-size>
    -->
    <!-- ヒープのパーセンテージ
    <max-size policy="USED_HEAP_PERCENTAGE">75</max-size>
    -->

    <!-- max-Sizeに到達した際に、エビクションする比率をパーセンテージで指定。
         25%なら、max-sizeに到達した後に25%のエントリがエビクションされる
    -->
    <eviction-percentage>25</eviction-percentage>

    <!-- 分断されたクラスタがマージされる時に、同じキーで違うバージョンのエントリを持っていた場合、
         どうマージするかを指定する。ビルトインで提供されているポリシーは、全部で4つ。
         com.hazelcast.map.merge.PassThroughMergePolicy:
             PassThroughMergePolicy causes the merging entry to be merged from source to destination map
             if source entry has updated more recently than the destination entry
         com.hazelcast.map.merge.PutIfAbsentMapMergePolicy:
             PutIfAbsentMapMergePolicy causes the merging entry to be merged from source to destination map
             if it does not exist in the destination map.
         com.hazelcast.map.merge.HigherHitsMapMergePolicy:
             HigherHitsMapMergePolicy causes the merging entry to be merged from source to destination map
             if source entry has higher hits than the destination one.
         com.hazelcast.map.merge.LatestUpdateMapMergePolicy:
             LatestUpdateMapMergePolicy causes the merging entry to be merged from source to destination map
             if source entry has updated more recently than the destination entry.

         その他
           http://www.hazelcast.com/docs/3.1/manual/single_html/#NetworkPartitioning
    -->
    <merge-policy>com.hazelcast.map.merge.PassThroughMergePolicy</merge-policy>
  </map>

  <map name="double-backup-map">
    <backup-count>2</backup-count>
  </map>

  <map name="wildcard-map-*">
    <max-idle-seconds>10</max-idle-seconds>
  </map>

  <!-- Advanced Configuration Properties
       http://www.hazelcast.com/docs/3.1/manual/single_html/#ConfigurationProperties
  -->
  <properties>
    <property name="hazelcast.memcache.enabled">false</property>
  </properties>
</hazelcast>

ちょっと、長い…。

テストプログラムには、ScalaTestを使用します。
src/test/scala/org/littlewings/hazelcast/configurationtest/HazelcastConfigurationSpec.scala

package org.littlewings.hazelcast.configurationtest

import com.hazelcast.core.{Hazelcast, HazelcastInstance}
import com.hazelcast.config.ClasspathXmlConfig

import org.scalatest.{BeforeAndAfterAll,FunSpec}
import org.scalatest.Matchers._

class HazelcastConfigurationSpec extends FunSpec with BeforeAndAfterAll {
  // テストの開始から終了まで、浮いていてもらうサーバ
  val server: HazelcastInstance =
    Hazelcast
      .newHazelcastInstance(new ClasspathXmlConfig("hazelcast.xml"))

  describe("Hazelcast Configuration Spec") {
    // ここで、withHazelcastメソッドを使って、テストを書く!
  }
  def withHazelcast(fun: (HazelcastInstance => Unit)): Unit = {
    val hazelcast =
      Hazelcast
        .newHazelcastInstance(new ClasspathXmlConfig("hazelcast.xml"))

    try {
      fun(hazelcast)
    } finally {
        hazelcast.getLifecycleService.shutdown()
    }
  }

  override def afterAll: Unit = {
    // 全HazelcastInstanceのシャットダウン
    Hazelcast.shutdownAll()
  }
}

テスト実行中は、常にサーバがひとつ浮いていてもらう形にして、テストケースごとにNodeが参加・離脱していくものにします。もちろん、サーバもテストケース単位で起動・停止するNodeも、先ほどのhazelcast.xmlを読み込むようにしています。

では、テストケースを。

クラスタ名の確認。

    it("cluster group spec") {
      withHazelcast { hazelcast =>
        hazelcast.getConfig.getGroupConfig.getName should be ("my-cluster")
        // 浮いているサーバが別個いるので、クラスタのメンバーは2になる
        hazelcast.getCluster.getMembers.size should be (2)
      }
    }

TTLのテスト。

    it("ttl spec") {
      withHazelcast { hazelcast =>
        val map = hazelcast.getMap[String, String]("default")

        map.put("key1", "value1")

        // TTLを5秒に設定しているので、6秒待てば有効期限切れ
        map.get("key1") should be ("value1")
        Thread.sleep(6 * 1000L)
        map.get("key1") should be (null)

        // 他のIMapも、defaultの定義を引き継ぐ
        val otherMap = hazelcast.getMap[String, String]("other-map")

        otherMap.put("key1", "value1")

        otherMap.get("key1") should be ("value1")
        Thread.sleep(6 * 1000L)
        otherMap.get("key1") should be (null)
      }
    }

明示的に、定義を指定したIMapの確認。

    it("double-backup-map spec") {
      // 明示的に、default以外のIMapの設定を定義した場合
      withHazelcast { hazelcast =>
        val config = hazelcast.getConfig

        config.getMapConfig("default").getBackupCount should be (1)
        config.getMapConfig("double-backup-map").getBackupCount should be (2)

        // 明示しなかった値は、MapConfigのデフォルト値が使用される模様
        config.getMapConfig("default").getTimeToLiveSeconds should be (5)
        config.getMapConfig("double-backup-map").getTimeToLiveSeconds should be (0)

        // 明示的な定義をしなかった場合は、「default」の内容で作成される模様
        config.getMapConfig("other-map-1").getTimeToLiveSeconds should be (5)
        config.getMapConfig("other-map-2").getTimeToLiveSeconds should be (5)
      }
    }

ワイルドカードを使った定義の確認。

    it("wildcard-map spec") {
      withHazelcast { hazelcast =>
        val config = hazelcast.getConfig

        // ワイルドカードにマッチするものは、同じ定義を使用する
        config.getMapConfig("default").getMaxIdleSeconds should be (0)
        config.getMapConfig("wildcard-map-1").getMaxIdleSeconds should be (10)
        config.getMapConfig("wildcard-map-2").getMaxIdleSeconds should be (10)
      }
    }

こんなところで、どうでしょうか?これで、少しは使えるようになりそうです(自分が)。

今回のソースコードは、こちらにアップしています。

https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-configuration