CLOVER🍀

That was when it all began.

Infinispan 9で変わったData ContainerとEviction

Infinispanの内部でデータを管理している中心は、DataContainerというインターフェースになります。

https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/DataContainer.java

この実装が、Infinispan 9でけっこう変わった&バリエーションが増えたので、確認してみることにしました。
合わせて、Evictionも変わっています。

Infinispanのブログにも、9のリリース前に紹介されていますね。けっこう詳しく書いているので、最初に
目を通した方がよいかもしれません。

Infinispan: Data Container Changes Part 1

Infinispan: Data Container Changes Part 2

Infinispan 9でデータの持ち方はどうなったか

Infinispanでは、これまで以下のようにデータを管理していました。

  • デフォルト … Java 8から独自にバックポートしたConcurrentHashMap
  • Evictionあり … Java 8から独自にバックポートしたConcurrentHashMap

※このあたりがベースになっています
https://github.com/infinispan/infinispan/blob/8.2.6.Final/commons/src/main/java/org/infinispan/commons/util/concurrent/jdk8backported/BoundedEquivalentConcurrentHashMapV8.java

いずれも、Javaヒープ上でのオブジェクトの管理になります。

これが、Infinispan 9ではこうなりました。

  • デフォルト(ヒープ) … ConcurrentHashMap
  • Evictionあり(ヒープ) … Caffeine
  • Off-Heap … sun.misc.Unsafeを利用したネイティブメモリ管理
  • Off-Heap+Evictionあり … sun.misc.Unsafeを利用したネイティブメモリ管理

ドキュメントは、こちら。

Eviction and Data Container

データをヒープ管理する場合は、オブジェクトのまま持つかMarshallingしてバイナリとして保持するかを選択することが
できます。

独自にバックポートしていたConcurrentHashMapは、標準のものを使用するようになりました。
また、通常のJavaヒープの場合でEvictionを有効化した場合、内部的にはCaffeineを使用するようになります。

GitHub - ben-manes/caffeine: A high performance caching library for Java 8

これにより、Infinispan 9ではライブラリの依存関係にCaffeineが追加され、Evictionの仕組みはCaffeineに
委譲されることとなります。

さらに、これまで搭載していなかったOff-Heapでのデータ管理方法が追加されました。これまでOff-Heapについては
Infinispanはあまり乗り気でなかったイメージがあるのですが、Infinispan 9でついに追加されることになりました。

Off-Heapでデータを管理した場合は、エントリはシリアライズされて保持されることになります。

Evictionはどうなった?

Infinispanを使う時、まずはヒープでデータを管理することになると思いますが、Evictionの定義方法も変わりました。

新しくMemoryというConfigurationが追加され、ここでEvictionの内容を指定することができるようになりました。

MemoryConfiguration (Infinispan JavaDoc All 9.0.3.Final API)

ヒープで管理するのか、Off-Heapで管理するのかもここで決まります。
※ストレージタイプとして、object(ヒープ)/binary(ヒープ)/off-heapの3つから選択します

これにより、Infinispan 8までのEvictionConfigurationは非推奨になりました。

EvictionConfiguration (Infinispan JavaDoc All 9.0.3.Final API)

同様に、XMLによりCache定義を行う場合でも、evictionタグは非推奨になります。

Evictionで指定できる組み合わせは、以下となります。

ストレージタイプ Evictionの有無 Eviction有効時の閾値の決め方
object なし -
object あり エントリ数(COUNT)
binary なし -
binary あり エントリ数(COUNT)
binary あり メモリ使用量(MEMORY)
off-heap なし -
off-heap あり エントリ数(COUNT)
off-heap あり メモリ使用量(MEMORY)

Evictionやストレージタイプに関するドキュメントは、こちら。

Eviction and Data Container / Eviction strategy

Eviction and Data Container / Eviction types

Eviction and Data Container / Storage type

Eviction and Data Container / More defaults

Eviction有効時のパラメーターとして「size」があるのですが、エントリ数(COUNT)かメモリ使用量(MEMORY)のどちらを選ぶかで
意味…というか単位(データ個数/シリアライズ後のバイト数)が変わるので注意が必要です。

EvictionのタイプにMEMORYを選んだ場合は、キーと値はシリアライズされて保存されることになります。

ところで、Evictionのアルゴリズムってどうなるんでしょう?

Off-HeapはLRUのようです。

ヒープの場合は以前はLIRSかLRUでしたが
※他を指定してもLRUになる
https://github.com/infinispan/infinispan/blob/8.2.6.Final/core/src/main/java/org/infinispan/container/DefaultDataContainer.java#L97-L111

Infinispan 9ではCaffeineになったことにより、Window TinyLfuとなります。
Efficiency · ben-manes/caffeine Wiki · GitHub

Eviction is handled by Caffeine utilizing the TinyLFU algorithm with an additional admission window. This was chosen as provides high hit rate while also requiring low memory overhead. This provides a better hit ratio than LRU while also requiring less memory than LIRS.

http://infinispan.org/docs/9.0.x/user_guide/user_guide.html#eviction_strategy

とまあ、こんな感じになりました、と。

使ってみる

では、ここからは実際にこの設定を使って試していってみましょう。

準備

まずは依存関係から。

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-core" % "9.0.0.Final" % Compile,
  "net.jcip" % "jcip-annotations" % "1.0" % Provided,
  "org.scalatest" %% "scalatest" % "3.0.3" % Test
)

まあ、infinispan-coreがあればOKです。あとは、テストコード用にScalaTestが入っているくらいですね。

設定ファイルの雛形としては以下としますが、Cacheの定義は利用するコードを載せる時に書いていきます。
基本的に、今回はDistributed Cacheを扱うようにします。
src/test/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:infinispan:config:9.0 http://www.infinispan.org/schemas/infinispan-config-9.0.xsd"
        xmlns="urn:infinispan:config:9.0">
    <jgroups>
        <stack-file name="udp" path="default-configs/default-jgroups-udp.xml"/>
    </jgroups>
    <cache-container>
        <jmx duplicate-domains="true"/>
        <transport cluster="test-cluster" stack="udp"/>

    <!-- あとで -->

    </cache-container>
</infinispan>

また、テストコードの雛形はこちら。
src/test/scala/org/littlewings/infinispan/datacontainers/DataContainersSpec.scala

package org.littlewings.infinispan.datacontainers

import org.infinispan.Cache
import org.infinispan.commons.marshall.WrappedByteArray
import org.infinispan.configuration.cache.StorageType
import org.infinispan.container.DefaultDataContainer
import org.infinispan.container.entries.{CacheEntrySizeCalculator, ImmortalCacheEntry, InternalCacheEntry, PrimitiveEntrySizeCalculator}
import org.infinispan.container.offheap.{BoundedOffHeapDataContainer, OffHeapDataContainer}
import org.infinispan.eviction.EvictionType
import org.infinispan.manager.DefaultCacheManager
import org.infinispan.marshall.core.WrappedByteArraySizeCalculator
import org.scalatest.{FunSuite, Matchers}

class DataContainersSpec extends FunSuite with Matchers {

  // ここに、テストを書く!

  protected def withCache[K, V](cacheName: String, numInstances: Int = 1)(fun: Cache[K, V] => Unit): Unit = {
    val managers = (1 to numInstances).map(_ => new DefaultCacheManager("infinispan.xml"))
    managers.foreach(_.getCache[K, V](cacheName))

    try {
      val cache = managers(0).getCache[K, V](cacheName)
      fun(cache)
      cache.stop()
    } finally {
      managers.foreach(_.stop())
    }
  }
}

簡単にクラスタを構成できる、ヘルパーメソッド付き。

EvictionにMEMORYを使うケースも試すので、ちょっとEntityっぽいものがあった方がいいかなと書籍をお題に用意。
src/test/scala/org/littlewings/infinispan/datacontainers/Book.scala

package org.littlewings.infinispan.datacontainers

object Book {
  def apply(isbn: String, title: String, price: Int): Book =
    new Book(isbn, title, price)
}

@SerialVersionUID(1L)
class Book(val isbn: String, val title: String, val price: Int) extends Serializable

データは、テストクラスに用意します。

  val books: Array[Book] = Array(
    Book("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 5577),
    Book("978-1785285332", "Getting Started With Hazelcast", 4338),
    Book("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 4815),
    Book("978-1849519205", "Hibernate Search by Example", 3718),
    Book("978-1784392413", "Wildfly Cookbook", 6817),
    Book("978-1783987146", "Mastering Apache Spark", 6615),
    Book("978-1491974292", "Stream Processing With Apache Flink", 5222),
    Book("978-1449358549", "Elasticsearch: The Definitive Guide", 6072),
    Book("978-1617291029", "Solr in Action", 5772),
    Book("978-1782162285", "Lucene 4 Cookbook", 5577)
  )

これらの素材を使って、確認していきましょう。

ストレージタイプ - object

まずはデフォルト

まずは、デフォルトがどうなっているか確認しましょう。Cacheの定義はこれだけ。

        <distributed-cache name="defaultCache"/>

Memoryの設定を取得すると、サイズ制限-1(なし)、ストレージタイプはobject、EvictionのタイプはCOUNT、となっています。

  test("default container") {
    withCache[String, Book]("defaultCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(-1)
      memoryConfiguration.storageType should be(StorageType.OBJECT)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[DefaultDataContainer[_, _]])

      books.foreach(b => cache.put(b.isbn, b))
      cache should have size (10)
    }
  }

address-countについては、現時点では無視してください。

ちなみに、クラスタでのNode数は3にしました。以後のパターンでも、ずっと3にしています。

DataContainerの実装としては、DefaultDataContainerが選択されています。

      cache.getAdvancedCache.getDataContainer should be(a[DefaultDataContainer[_, _]])

データは10個登録しましたが、Evictionは設定していないので10エントリそのまま入っています。

      books.foreach(b => cache.put(b.isbn, b))
      cache should have size (10)

デフォルトだとこんな感じですと。

Evictionタイプ - COUNT

続いて、Evictionを有効にしてエントリ数を制限してみましょう。

ストレージタイプがobjectの場合、sizeのみが指定可能です(Evictionのタイプは暗黙的にCOUNTになるので、エントリ数での制限)。
今回は、3を指定しました。

        <distributed-cache name="objectWithSizeCache">
            <memory>
                <object size="3"/>
            </memory>
        </distributed-cache>

結果は、こちら。

  test("default container, object memory, with size") {
    withCache[String, Book]("objectWithSizeCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(3)
      memoryConfiguration.storageType should be(StorageType.OBJECT)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[DefaultDataContainer[_, _]])

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 3 and be <= 6)
    }
  }

sizeに値が設定されました(-1ではなくなりました)。

で、データを登録するとエントリ数が3から6の間くらいで揺れます。

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 3 and be <= 6)

ここで、クラスタ上のNode数は3なので、これはどう解釈すればよいのだろうということになります。

まず、設定した「size」が「クラスタでのエントリ数」なのか「Node単位でのエントリ数」なのかですが、これは後者、Node単位での
エントリ数となります。

        <distributed-cache name="objectWithSizeCache">
            <memory>
                <object size="3"/>
            </memory>
        </distributed-cache>

となると、3 Nodeにsizeが3で9になるのでは?となりますが、このsizeの計算にはバックアップも含まれるようです。
Distributed Cacheのバックアップ数のデフォルトは1なので、エントリをひとつ登録すると、Cache内にはPrimaryとバックアップの2つの
エントリが存在することになります。
で、Node上のエントリの配置結果によって実行結果が揺れると。

内部的には、Caffeineに最大数を指定しているだけになります。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/DefaultDataContainer.java#L104

            caffeine.maximumSize(thresholdSize);

Eviction - Size-based

以上で、ストレージタイプがobjectの場合はおしまいです。

ストレージタイプ - binary

とりあえずbinaryを指定

続いては、ストレージタイプをbinaryに。

        <distributed-cache name="binaryCache">
            <memory>
                <binary/>
            </memory>
        </distributed-cache>

memoryタグおよびbinaryタグを書いただけです。

で、結果。

  test("default container, binary memory") {
    withCache[String, Book]("binaryCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(-1)
      memoryConfiguration.storageType should be(StorageType.BINARY)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[DefaultDataContainer[_, _]])

      books.foreach(b => cache.put(b.isbn, b))
      cache should have size 10
    }
  }

特にEvictionの設定をしていないので、登録したエントリがそのまま入ります。DataContainerの実装も、DefaultDataContainerのままです。
ストレージタイプがbinaryになったくらいですね、変化があったのは。

Evictionタイプ - COUNT

次に、Evictionを設定してみます。まずはCOUNTから。

        <distributed-cache name="binaryByCountCache">
            <memory>
                <binary size="3"/>
                <!-- 次でも同じ
                <binary size="3" eviction="COUNT"/>
                -->
            </memory>
        </distributed-cache>

コメントでも書いていますが、eviction属性を指定しなかった場合は、COUNTを指定したことと同義です。

結果。

  test("default container, binary memory, by count") {
    withCache[String, Book]("binaryByCountCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(3)
      memoryConfiguration.storageType should be(StorageType.BINARY)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[DefaultDataContainer[_, _]])

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 4 and be <= 6)
    }
  }

COUNTは、エントリ数でのEvictionとなるため、objectでCOUNTを指定した時と、そう結果は変わりません。

Evictionタイプ - MEMORY

続いて、EvictionタイプをMEMORYにしてみます。

        <distributed-cache name="binaryByMemoryCache">
            <memory>
                <binary size="1000" eviction="MEMORY"/>
            </memory>
        </distributed-cache>

sizeは、「1000」としてみました。

結果は、このように。

  test("default container, binary memory, by memory") {
    withCache[String, Book]("binaryByMemoryCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(1000)
      memoryConfiguration.storageType should be(StorageType.BINARY)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.MEMORY)

      cache.getAdvancedCache.getDataContainer should be(a[DefaultDataContainer[_, _]])

      val marshaller = cache.getAdvancedCache.getComponentRegistry.getCacheMarshaller
      val calcurator = new CacheEntrySizeCalculator[String, WrappedByteArray](new WrappedByteArraySizeCalculator(new PrimitiveEntrySizeCalculator))
      val entriesSize =
        books
          .map(b => calcurator
            .calculateSize(b.isbn, new ImmortalCacheEntry(b.isbn, new WrappedByteArray(marshaller.objectToByteBuffer(b))).asInstanceOf[InternalCacheEntry[String, WrappedByteArray]]))

      entriesSize.min should be(264)
      entriesSize.max should be(304)

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 2 and be <= 6)
    }
  }

真ん中の部分はいったん置いておいて、まずは設定が反映されています、と。

      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(1000)
      memoryConfiguration.storageType should be(StorageType.BINARY)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.MEMORY)

保持されるエントリ数も、制限されたようです。

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 2 and be <= 6)

で、sizeを1000に制限したわけですが、シリアライズされたキーと値のサイズを計算してみたのがこちら。

      val marshaller = cache.getAdvancedCache.getComponentRegistry.getCacheMarshaller
      val calcurator = new CacheEntrySizeCalculator[String, WrappedByteArray](new WrappedByteArraySizeCalculator(new PrimitiveEntrySizeCalculator))
      val entriesSize =
        books
          .map(b => calcurator
            .calculateSize(b.isbn, new ImmortalCacheEntry(b.isbn, new WrappedByteArray(marshaller.objectToByteBuffer(b))).asInstanceOf[InternalCacheEntry[String, WrappedByteArray]]))

      entriesSize.min should be(264)
      entriesSize.max should be(304)

データは今回10個用意しましたが、キーと値をシリアライズした時の最小値が264バイトで、最大値が304バイトとなりました。よって、sizeを
1000にするとだいたいまあ3つ分のエントリ数くらいになるでしょう、と。

実際、こんな感じで実装されています。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/DefaultDataContainer.java#L99-L101

            CacheEntrySizeCalculator<K, V> calc = new CacheEntrySizeCalculator<>(new WrappedByteArraySizeCalculator<>(
                  new PrimitiveEntrySizeCalculator()));
            caffeine.weigher((k, v) -> (int) calc.calculateSize(k, v)).maximumWeight(thresholdSize);

これで、binaryは見終わりました、と。

ストレージタイプ - off-heap

とりあえずoff-heapに

最後は、off-heapです。

まずはデフォルトで使ってみましょう。

        <distributed-cache name="offHeapCache">
            <memory>
                <off-heap/>
            </memory>
        </distributed-cache>

こんな感じになります。

  test("off-heap container") {
    withCache[String, Book]("offHeapCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(-1)
      memoryConfiguration.storageType should be(StorageType.OFF_HEAP)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[OffHeapDataContainer])
      cache.getAdvancedCache.getDataContainer should not be a[BoundedOffHeapDataContainer]

      books.foreach(b => cache.put(b.isbn, b))
      cache should have size (10)
    }
  }

ストレージタイプがoff-heapになるのはそうですが、DataContainerの実装クラスがOffHeapDataContainerとなります。
※BoundedOffHeapDataContainerはあとで出てきますが、今回はこちらではありません

Evictionは特に指定していないので、エントリ数は制限されません。

off-heap自体は、また最後に見ていくとしましょう。

Evictionタイプ - COUNT

続いて、Evictionを設定してみます。まずはCOUNTから。

        <distributed-cache name="offHeapByCountCache">
            <memory>
                <off-heap size="3"/>
                <!-- 次でも同じ
                <off-heap size="3" eviction="COUNT"/>
                -->
            </memory>
        </distributed-cache>

Evictionを指定しなかった場合も、COUNTとなります。

off-heapにしたとはいえ、COUNTの場合はエントリ数での制限になるので、結果は他とそう変わりません。

  test("off-heap container, by count") {
    withCache[String, Book]("offHeapByCountCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(3)
      memoryConfiguration.storageType should be(StorageType.OFF_HEAP)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[BoundedOffHeapDataContainer])

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 3 and be <= 6)
    }
  }

ただ、DataContainerの実装クラスは、BoundedOffHeapDataContainerになります。

      cache.getAdvancedCache.getDataContainer should be(a[BoundedOffHeapDataContainer])

Evictionの実装は、Infinispan独自になります(Caffeineではありません)。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/BoundedOffHeapDataContainer.java#L34

         sizeCalculator = i -> 1;
Evictionタイプ - MEMORY

最後は、EvictionタイプをMEMORYで。

        <distributed-cache name="offHeapByMemoryCache">
            <memory>
                <off-heap size="1000" eviction="MEMORY"/>
            </memory>
        </distributed-cache>

sizeは、binaryの時と同じく1000に指定してみました。

結果も、そう変わりません。

  test("off-heap container, by memory") {
    withCache[String, Book]("offHeapByMemoryCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(1000)
      memoryConfiguration.storageType should be(StorageType.OFF_HEAP)
      memoryConfiguration.addressCount should be(1048576)
      memoryConfiguration.evictionType should be(EvictionType.MEMORY)

      cache.getAdvancedCache.getDataContainer should be(a[BoundedOffHeapDataContainer])

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 3 and be <= 7)
    }
  }

サイズ計算は、この部分で行っています。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/BoundedOffHeapDataContainer.java#L37

         sizeCalculator = i -> offHeapEntryFactory.determineSize(i) + 28;

よくよく見ると、その場で実サイズを計算しているわけではありません。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/OffHeapEntryFactoryImpl.java#L174-L183

このあたりは、最後に。

address-point?

ここで、最初からずっと触れてこなかった設定値として、address-pointがあります。

      memoryConfiguration.addressCount should be(1048576)

デフォルト値、1048576ってなっていましたね。

これ、なにかというと、off-heapの時にのみ効果のあるパラメーターです。

こんな感じの説明が、XML Schemaには書かれています。

How many address pointers to use. This number will be rounded up to a power of two. For optimal performance you will want more address pointers than you expect to have entries. This is similar to the size of an array backing a hash map. Without collisions lookups and writes will be constant time. Each pointer will take up 8 bytes of memory thus the default will use 8 MB of off-heap memory.

https://docs.jboss.org/infinispan/9.0/configdocs/infinispan-config-9.0.html

エントリについての情報を持つ、アドレス空間を指定するもので、ここで指定された値はもっとも近い2のべき乗の値に寄せられます(ただし大きい方)。
また、ここで指定した値(2のべき乗で補正されたもの)は、最終的に3ビットほど左シフトされて、その値がネイティブメモリとして保持されます。

デフォルト値の1048576は、8MBになります。

8MBしかエントリを格納できないの?というわけでもなく、あくまでこの値はエントリについての情報を管理するだけのもので、1エントリあたり8バイト
使われます。

実は、この計算で参照していたのは、この領域に格納された値を読み出すことでエントリの大きさが算出できるからです。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/OffHeapEntryFactoryImpl.java#L174-L183

この空間も、ネイティブメモリとして確保されるわけですが、それにはsun.misc.Unsafeが使われます。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/MemoryAddressHash.java#L22

ちなみに、ここまでの説明(2のべき乗が〜)を計算しているのはこのあたりなのですが
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/OffHeapDataContainer.java#L71-L77
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/MemoryAddressHash.java#L22

これを移植して単独で動かすと、こんな感じになります。

  test("address to memory size") {
    def calcMemorySize(desiredSize: Int): Long = {
      val maxLockCount = 1 << 30

      def nextPowerOfTwo(target: Int): Int = {
        var n = target - 1
        n |= n >>> 1
        n |= n >>> 2
        n |= n >>> 4
        n |= n >>> 8
        n |= n >>> 16

        if (n < 0) {
          1
        } else if (n >= maxLockCount) {
          maxLockCount
        } else {
          n + 1
        }
      }

      val lockCount = nextPowerOfTwo(Runtime.getRuntime.availableProcessors) << 1
      var memoryAddresses = if (desiredSize >= maxLockCount) maxLockCount else lockCount

      while (memoryAddresses < desiredSize) {
        memoryAddresses <<= 1
      }

      val pointerCount = nextPowerOfTwo(memoryAddresses)
      println(pointerCount)
      pointerCount.toLong << 3
    }

    calcMemorySize(1048576) should be(8388608L)
    (calcMemorySize(1048576) / 1024 / 1024) should be(8)

    calcMemorySize(1048588) should be(16777216L)
    (calcMemorySize(1048588) / 1024 / 1024) should be(16)
  }

デフォルト値(1048576)の場合は、最終的に8MBになります。

    calcMemorySize(1048576) should be(8388608L)
    (calcMemorySize(1048576) / 1024 / 1024) should be(8)

これをちょっとでも大きくしたりすると、次の2のべき乗の値に寄せられ16MB確保されることになります。

    calcMemorySize(1048588) should be(16777216L)
    (calcMemorySize(1048588) / 1024 / 1024) should be(16)

で、ここまで書くと、「エントリそのもののデータはどこに?」となるわけですが、それについては別途ネイティブメモリを確保して
やっぱりsun.misc.Unsafeでシリアライズ後の値をコピーして保存します。
https://github.com/infinispan/infinispan/blob/9.0.0.Final/core/src/main/java/org/infinispan/container/offheap/OffHeapEntryFactoryImpl.java#L154-L168

とまあ、こんな感じで管理されているわけです。

address-countを指定した例を用意すると、こんな感じに。

        <distributed-cache name="offHeapByCountWithAddressCountCache">
            <memory>
                <off-heap size="3" eviction="COUNT" address-count="2097152"/>
            </memory>
        </distributed-cache>

エントリの情報を保存するアドレス空間が広がるだけで、今回は結果はそんなに変わりませんけどね…。

  test("off-heap container, by count, with address-count") {
    withCache[String, Book]("offHeapByCountWithAddressCountCache", 3) { cache =>
      val memoryConfiguration = cache.getCacheConfiguration.memory
      memoryConfiguration.size should be(3)
      memoryConfiguration.storageType should be(StorageType.OFF_HEAP)
      memoryConfiguration.addressCount should be(2097152)
      memoryConfiguration.evictionType should be(EvictionType.COUNT)

      cache.getAdvancedCache.getDataContainer should be(a[BoundedOffHeapDataContainer])

      books.foreach(b => cache.put(b.isbn, b))
      cache.size should (be >= 3 and be <= 6)
    }
  }

sun.misc.Unsafeについては詳しくなかったので、このあたりを参考にしました。

Java Magic. Part 4: sun.misc.Unsafe

Unsafe Part 2: Using sun.misc.Unsafe to create a contiguous array of objects

まとめ

Infinispan 9で、データの管理方法が変わったので、そこを中心に見ていきました。

大きいところは、ヒープ管理の場合でもEvictionを有効にするとCaffeineが利用されること、あとOff-Heapでの管理が追加されたことですね。
特にOff-Heapについてはあまり知らないので、けっこう調べるのにてこずりました…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-data-containers