CLOVER🍀

That was when it all began.

InfinispanのstoreAsBinaryとは?

Infinispanの設定の中で、storeAsBinaryというものがありますが、これについてちょっと調べてみました。

設定的には、XML中に

  <default>
    <storeAsBinary />

    <clustering mode="distribution">
      <hash numOwners="2" />
    </clustering>
  </default>

みたいな感じであるだけなので。

以下のドキュメントの、/infinispan/defaultまたはnamedCache/storeAsBinaryのところを読むと、こんなことが書かれています。

Configuration reference
http://docs.jboss.org/infinispan/5.3/configdocs/infinispan-config-5.3.html

Controls whether when stored in memory, keys and values are stored as references to their original objects, or in a serialized, binary format. There are benefits to both approaches, but often if used in a clustered mode, storing objects as binary means that the cost of serialization happens early on, and can be amortized. Further, deserialization costs are incurred lazily which improves throughput. It is possible to control this on a fine-grained basis: you can choose to just store keys or values as binary, or both.

要は、キーと値を保存する時に、オブジェクトのリファレンスにするか、バイナリフォーマットにするか、ということです。双方利点はあるのですが、クラスタリングモードの時にバイナリで保存するということは、シリアライズのコストが早い段階で発生するということになります。さらに、デシリアライズのコストの発生を遅らせることで、スループットを改善できます。

キーと値、またはその両方をバイナリで保存するかどうかを選択することができますよ、と。

で、storeAsBinaryの設定可能な属性としては、以下の3つが存在します。

属性名 デフォルト値 意味
enabled false 有効にすると、キーと値の両方をバイナリで保存するようにします
storeKeysAsBinary true キーを保存する時に、バイナリにするかそうでないかを指定します。「enabled」属性をtrueにした場合は、デフォルトで有効になります
storeValuesAsBinary true 値を保存する時に、バイナリにするかそうでないかを指定します。「enabled」属性をtrueにした場合は、デフォルトで有効になります

が、ドキュメントには載っていない属性がもうひとつあります。

StoreAsBinaryConfigurationBuilder#defensive
http://docs.jboss.org/infinispan/5.3/apidocs/org/infinispan/configuration/cache/StoreAsBinaryConfigurationBuilder.html#defensive%28boolean%29

属性名 デフォルト値 意味
defensive false 無効の場合、オブジェクト参照を保持し、キーのマーシャリングをレイジーに行います

Javadocには、こんなことが書いていました。

When defensive copying is disabled, Infinispan keeps object references around and marshalls keys lazily. So clients can modify entries via original object references, and marshalling only happens when entries are to be replicated/distributed, or stored in a cache store. Since client references are valid, clients can make changes to entries in the cache using those references, but these modifications are only local and you still need to call one of the cache's put/replace... methods in order for changes to replicate. When defensive copies are enabled, Infinispan marshalls objects the moment they're stored, hence changes made to object references are not stored in the cache, not even for local caches.

要は、防御的コピーのことですね。

キーはレイジーにマーシャリングされるので、レプリケーションまたは分散される時にマーシャリングが発生しますよ、と。クライアントはキャッシュ内にあるエントリと同じ参照を保持していて変更することができますが、それはローカルの参照が変わるのみです。それをキャッシュのレプリケーションに反映するには、キャッシュのputやreplaceメソッドを呼び出す必要がありますよ、と。

defensiveを有効にした場合は、オブジェクトを保存した時にすぐにマーシャリングします。オブジェクトの参照に対する変更は、ローカルキャッシュを含めて反映されなくなります。

まあ、試してみましょう。

ちょっとわざとらしいですが、キャッシュのキーと値用のクラスを用意します。

@SerialVersionUID(1L)
class KeyEntry(var value: String) extends Serializable {
  override def toString: String =
    s"This Key Value is [$value]"

  override def equals(other: Any): Boolean =
    other match {
      case o: KeyEntry => value == o.value
      case _ => false
    }

  override def hashCode: Int =
    value.hashCode * 31
}

@SerialVersionUID(1L)
class ValueEntry(var value: String) extends Serializable {
  override def toString: String =
    s"This Entry Value is [$value]"
}

そして、一応クラスタリングも使用するので、浮いていてもらうサーバを用意します。

import org.infinispan.manager.DefaultCacheManager

object EmbeddedCacheServer {
  def main(args: Array[String]): Unit = {
    new DefaultCacheManager("infinispan.xml").getCache[KeyEntry, ValueEntry]()
  }
}

あとは、Cacheに対してエントリを保存するクラスと、参照して変更するクラスを用意します。共に、mainメソッドを持つものとして作成。

import org.infinispan.Cache
import org.infinispan.manager.DefaultCacheManager

trait CacheSupport {
  def usingCache[A](body: Cache[KeyEntry, ValueEntry] => A): A = {
    val manager = new DefaultCacheManager("infinispan.xml")
    val cache = manager.getCache[KeyEntry, ValueEntry]()

    try {
      body(cache)
    } finally {
      cache.stop()
      manager.stop()
    }
  }
}

object InfinispanStoreAs extends CacheSupport {
  def main(args: Array[String]): Unit = usingCache { cache =>
    val key = new KeyEntry("1")
    val value = new ValueEntry("firstValue")

    cache.put(key, value)

    println("first => " + cache.get(key))
  }
}

object QueryAndUpdate extends CacheSupport {
  def main(args: Array[String]): Unit = usingCache { cache =>
    val key = new KeyEntry("1")
    val value = cache.get(key)

    println("Get Value => " + value)

    value.value = "update"

    println("Local Updated, In Cache => " + cache.get(key))

    println("Reference Equals => " + (cache.get(key) eq cache.get(key)))
  }
}

JGroupsの設定は省略します。あとは、Infinspanの設定を。

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:5.3 http://www.infinispan.org/schemas/infinispan-config-5.3.xsd"
    xmlns="urn:infinispan:config:5.3">

  <global>
    <transport clusterName="sample-cluster">
      <properties>
        <property name="configurationFile" value="jgroups.xml" />
      </properties>
    </transport>
    <globalJmxStatistics
        enabled="true"
        jmxDomain="org.infinispan"
        cacheManagerName="DefaultCacheManager"
        />
  </global>

  <default>
    <storeAsBinary />

    <clustering mode="distribution">
      <hash numOwners="2" />
    </clustering>
  </default>
</infinispan>

この

    <storeAsBinary />

の場所をいろいろ変えていこうということで。

あとは、

object InfinispanStoreAs extends CacheSupport {
  def main(args: Array[String]): Unit = usingCache { cache =>
    val key = new KeyEntry("1")
    val value = new ValueEntry("firstValue")

    cache.put(key, value)

    println("first => " + cache.get(key))
  }
}

で、エントリを登録し、

object QueryAndUpdate extends CacheSupport {
  def main(args: Array[String]): Unit = usingCache { cache =>
    val key = new KeyEntry("1")
    val value = cache.get(key)

    println("Get Value => " + value)

    value.value = "update"

    println("Local Updated, In Cache => " + cache.get(key))

    println("Reference Equals => " + (cache.get(key) eq cache.get(key)))
  }
}

で、浮いてるサーバからエントリを取得して、書き換えてみようといった感じですね。最後に、キャッシュから2つエントリを取得して、参照を比較しています。


まずは、デフォルトのまま。

  • enabled=false
  • storeKeysAsBinary=true
  • storeValuessAsBinary=true
  • defensive=false

サーバを起動。

$ sbt "run-main EmbeddedCacheServer"

エントリ登録。

> run-main InfinispanStoreAs
[info] first => This Entry Value is [firstValue]

取得とローカル参照の更新、参照比較。

> run-main QueryAndUpdate
[info] Get Value => This Entry Value is [firstValue]
[info] Local Updated, In Cache => This Entry Value is [update]
[info] Reference Equals => true

Cacheの中の参照も、見事に変わりました。

が、もう1度同じプログラムを実行すると

> run-main QueryAndUpdate
[info] Get Value => This Entry Value is [firstValue]
[info] Local Updated, In Cache => This Entry Value is [update]
[info] Reference Equals => true

となるので、やっぱりオブジェクト参照のみが書き換わったことになり、クラスタ内のキャッシュには変更はありません。

では、今度は

    <storeAsBinary defensive="true" />

としてみます。

エントリを登録するところは端折って、取得、更新と参照比較だけ載せます。

> run-main QueryAndUpdate
[info] Get Value => This Entry Value is [firstValue]
[info] Local Updated, In Cache => This Entry Value is [update]
[info] Reference Equals => true

実は、何も変わりません。

なら、enabled属性をtrueにすると?

    <storeAsBinary enabled="true" defensive="true" />

実行。

> run-main QueryAndUpdate
[info] Get Value => This Entry Value is [firstValue]
[info] Local Updated, In Cache => This Entry Value is [firstValue]
[info] Reference Equals => false

動きが変わりました。ローカルの参照を更新しても、再度Cacheから取得すると値は変更されていませんし、Cacheから2つエントリを取得して参照を比較しても、falseになっています。

つまり、enabled属性をtrueにしないと、defensive属性は効果がないということですね。

ここでさらに、storeValuesAsBinary属性をfalseにしてみます。

    <storeAsBinary enabled="true" storeValuesAsBinary="false" defensive="true" />

実行。

> run-main QueryAndUpdate
[info] Get Value => This Entry Value is [firstValue]
[info] Local Updated, In Cache => This Entry Value is [update]
[info] Reference Equals => true

enabled属性をfalseにした時と、同じ挙動になりましたね…。

ちなみに、

    <storeAsBinary enabled="true" />

も同じ挙動でした。

とりあえず、Infinispanでは(storeKeysAsBinaryとstoreValuesAsBinaryはtrueですが)すぐにはバイナリにされず、ローカル参照の形で保持されていると。マーシャリングは必要な時に行いますよ〜ってことみたいですね。

ソースを追いかけつつ、Interceptorの増減を見てみましたが、enabledがtrueで、少なくともstoreKeysAsBinaryかstoreValuesAsBinaryのどちらかがtrueであれば、IsMarshallableInterceptorとMarshalledValueInterceptorが追加されるようです。
*defensiveがtrueの場合は、MarshalledValueInterceptorがDefensiveMarshalledValueInterceptorになります

ということは、少なくともenabledをtrueにしない限りは効果がない??

つか、enabledってどういう時に使うんでしょう…?storeKeysAsBinaryとstoreValuesAsBinaryをfalseにするケースって??

後で、この辺りの議論を読んでみた方がいいかな…。
https://issues.jboss.org/browse/ISPN-1267
https://community.jboss.org/thread/169285
http://infinispan-developer-list.980875.n3.nabble.com/infinispan-dev-JBoss-Libra-td3702716.html
http://java.dzone.com/articles/infinispan-memory-overhead