Infinispan 9.2では、Evictionの戦略として「ExceptionベースのEviction」をサポートしたということが、リリースについて
書かれたブログエントリに記載があります。
Infinispan: Infinispan 9.2.0.Final
Exception based evictionA new "eviction" that instead of removing old entries prevents new entries being inserted (supported by all memory storage and eviction types)
http://blog.infinispan.org/2018/02/infinispan-920final.html
ただこのEviction Strategyという設定自体、Infinispan 9.2で新しく設定できるようになった項目(新規の設定項目)
となります。
全部で次の4つのStrategyがあり、
- NONE
- MANUAL
- REMOVE
- EXCEPTION
Infinispan 9.1までの挙動で考えると、MANUALとREMOVEが相当するという感じでしょうか。
せっかくなので、見ていってみましょう。
環境
Java。
$ java -version openjdk version "1.8.0_151" OpenJDK Runtime Environment (build 1.8.0_151-8u151-b12-0ubuntu0.16.04.2-b12) OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)
Infinispanは、9.2.1を使用します。
準備
依存関係は、こちら。
libraryDependencies ++= Seq( "org.infinispan" % "infinispan-core" % "9.2.1.Final" % Compile, "net.jcip" % "jcip-annotations" % "1.0" % Compile, "org.scalatest" %% "scalatest" % "3.0.5" % Test )
Eviction Strategyは、「infinispan-core」があれば十分確認できます。
お題
Eviction Strategyの各戦略に対して、Node数が3のDistributed Cacheについて動きを確認してみたいと思います。Evictionの閾値を設ける場合は、
オブジェクト数(count)を10とします。
テストコードと設定ファイルの雛形
テストコードの雛形は、このように用意します。
src/test/scala/org/littlewings/infinispan/evictionstrategy/EvictionStrategySuite.scala
package org.littlewings.infinispan.evictionstrategy import org.infinispan.Cache import org.infinispan.commons.CacheException import org.infinispan.configuration.cache.StorageType import org.infinispan.eviction.EvictionStrategy import org.infinispan.interceptors.impl.ContainerFullException import org.infinispan.manager.DefaultCacheManager import org.infinispan.remoting.RemoteException import org.scalatest.{FunSuite, Matchers} class EvictionStrategySuite extends FunSuite with Matchers { // ここに、テストを書く!! protected def withCache[K, V](cacheName: String, numInstances: Int = 3)(fun: Cache[K, V] => Unit): Unit = { val managers = (1 to numInstances).map(_ => new DefaultCacheManager("infinispan.xml")) managers.foreach(_.startCache(cacheName)) try { val cache = managers(0).getCache[K, V](cacheName) fun(cache) cache.stop() } finally { managers.foreach(_.stop()) } } }
簡単にクラスタを構成できる、ヘルパーメソッド付きです。
続いて、設定ファイルの雛形。
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.2 http://www.infinispan.org/schemas/infinispan-config-9.2.xsd" xmlns="urn:infinispan:config:9.2"> <jgroups> <stack-file name="udp" path="default-configs/default-jgroups-udp.xml"/> </jgroups> <cache-container> <transport cluster="test-cluster" stack="udp"/> <!-- 後で --> </cache-container> </infinispan>
この中に、Cacheの定義を埋めていきます。
デフォルト?
最初は、Eviction Strategyをなにも設定しない状態で動かしてみましょう。
<distributed-cache name="defaultEvictionStrategyCache" owners="1"/>
ほぼデフォルトのDistributed Cacheで、ownersのみ1にしてあります。ownersを1にしている理由については、Eviction有効時に触れることとしましょう。
テストコードは、こちら。
test("default eviction strategy") { withCache[String, String]("defaultEvictionStrategyCache", 3) { cache => val configuration = cache.getCacheConfiguration configuration.memory.evictionStrategy should be(EvictionStrategy.NONE) configuration.memory.storageType should be(StorageType.OBJECT) (1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(50L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should (be >= 33 or be <= 34) } }
クラスタ内のNodeは、3つです。
Cacheの定義で確認すると、デフォルトのEviction StrategyはNONEのようです。
val configuration = cache.getCacheConfiguration
configuration.memory.evictionStrategy should be(EvictionStrategy.NONE)
configuration.memory.storageType should be(StorageType.OBJECT)
NONEとは、Evictionが有効になっていない状態で、直接Cache#evictは呼び出されない想定となっているようです。Passivationが有効だと、警告されるそうな。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/EvictionConfigurationBuilder.java#L116-L118
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/MemoryConfigurationBuilder.java#L179-L182
データを50個放り込んで全部をCache#evictすると、33〜34個くらいのエントリが残ります。
*例が悪いのですが、Eviction StratetyとCache#evictには関係はあるものの同時に使って説明するべきではなかったなぁと思います
(1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(50L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should (be >= 33 or be <= 34)
ちょっと、ん?って感じですね。ですが、ここは流しておきます。
なお、この状態でCache#evictを呼び出すと、実は警告されていたりします。
3 31, 2018 12:59:36 午前 org.infinispan.cache.impl.CacheImpl evict WARN: ISPN000419: Eviction of an entry invoked without an explicit eviction strategy for cache defaultEvictionStrategyCache
Evictionが有効でないのにCache#evictを呼び出しているよ、と言われるようですね。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/cache/impl/CacheImpl.java#L873-L875
ここで言う「Evictionが有効でない」というのは、MemoryConfiguration#isEvictionEnabledがtrueを返すことであり
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/MemoryConfiguration.java#L87
EvictionStrategy#isRemovableBasedがtrueを返す時です。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L66-L68
つまりREMOVEの時だけですね、警告されないの…。
ここで、Eviction StrategyはNONEのままで、閾値をオブジェクト数10にしてみましょう。
<distributed-cache name="noneEvictionStrategyCache" owners="1"> <memory> <object strategy="NONE" size="10"/> </memory> </distributed-cache>
テストコードは、こちら。
test("none eviction strategy, but select remove strategy") { withCache[String, String]("noneEvictionStrategyCache", 3) { cache => // REMOVE!! cache.getCacheConfiguration.memory.evictionStrategy should be(EvictionStrategy.REMOVE) cache.getCacheConfiguration.memory.size should be(10L) (1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(30L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should be(20L) } }
なんと、敷居値を設定すると、設定ファイル上はNONEを指定しているにも関わらず、REMOVEにされてしまいました。
// REMOVE!! cache.getCacheConfiguration.memory.evictionStrategy should be(EvictionStrategy.REMOVE) cache.getCacheConfiguration.memory.size should be(10L)
先ほどと同様に50個のエントリを放り込むと、Evictionを有効にしているので30個しか残りません。
(1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(30L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should be(20L)
Cache#evict後は、20個です。
ここで、InfinispanのEvictionにおける閾値の考え方は、「Node単位の閾値」でした。
Infinispan 9で変わったData ContainerとEviction - CLOVER
つまり、ownersを1にしているがために、Node数が3なので50個のエントリを放り込んでもCacheに残るのは10×3とわかりやすい結果になっているわけですね。
デフォルトのownersは2(バックアップがひとつで、これもEvictionの対象数に含まれる)なので、そのままだとCacheに残るエントリ数がもっと揺れる
結果となります。
で、Cache#evictすると残るのは20個なのですが…どうもデータの操作したCacheのエントリにしか影響していなさそうな…。この点は、また後で。
MANUAL
続いて、MANUAL。
MANUALの意味は、Cache#evictを利用してよいこと以外は、NONEと同じです。こちらを使う場合は、Passivationを有効にしても警告は出ないようです。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/EvictionConfigurationBuilder.java#L116-L118
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/MemoryConfigurationBuilder.java#L179-L182
Cacheの定義はこちら。
<distributed-cache name="manualEvictionStrategyCache" owners="1"> <memory> <object strategy="MANUAL" size="10"/> </memory> </distributed-cache>
Eviction Strategyを「MANUAL」にしている以外、先ほどのNONEと同じです。
結果も、実は一緒だったりします。Eviction StrategyがREMOVEとなってしまうところまで、同じです。
test("manual eviction strategy, but select remove strategy") { withCache[String, String]("manualEvictionStrategyCache", 3) { cache => // REMOVE!! cache.getCacheConfiguration.memory.evictionStrategy should be(EvictionStrategy.REMOVE) cache.getCacheConfiguration.memory.size should be(10L) (1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(30L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should be(20L) } }
Passivationの警告を除いてMANUALとNONEが同じというのは、EvictionStrategyのソースコードを見るとわかります。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L20-L27
Enumのコンストラクタとしてbooleanの引数exceptionおよびremoveを取るのですが(Exceptionベースなのか、Removeなのかを示す値)、NONEもMANUALも
両方falseで、定義上ほぼ違いがありません。
また、EvictionStrategy#isEnabledでNONEとMANUALが同じように扱われているところからも、そこは伺えることでしょう。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L50-L52
ちなみにですね、Eviction StrategyをMANUALにしてサイズ制限などをしない場合、MANUALのままにはなるのですが、Cache#evictを
直接使用するとやっぱり警告されたりします…。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/cache/impl/CacheImpl.java#L873-L875
だって、この判定条件ってRemovableBasedなStrategyかどうかで見てますからねぇ。
REMOVE
続いて、REMOVE。とはいっても、先ほどsize指定時にNONEが強制的にREMOVEに変更された結果を見ているので、結果はすでにわかっていることになります。
Cacheの定義。
<distributed-cache name="removeEvictionStrategyCache" owners="1"> <memory> <object strategy="REMOVE" size="10"/> </memory> </distributed-cache>
テストコード。
test("remove eviction strategy") { withCache[String, String]("removeEvictionStrategyCache", 3) { cache => cache.getCacheConfiguration.memory.evictionStrategy should be(EvictionStrategy.REMOVE) cache.getCacheConfiguration.memory.size should be(10L) (1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(30L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should be(20L) } }
ここでは、NONEなどがなぜREMOVEに強制変更されるかを見てみましょう。
Evictionが無効な時に、sizeが0より大きい値で指定されていると、強制的にREMOVEが設定されるようになっています。ここで、Evictionが無効な時というのは
EvictionStrategy#isEnabledがfalseを返す時。
つまり、NONEとMANUAL以外の時です。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L51
EXCEPTION
最後は、EXCEPTION。こちらは、他と少し毛色が変わります。
まずは、Cacheの定義から。
<distributed-cache name="exceptionEvictionStrategyCache" owners="1"> <memory> <object strategy="EXCEPTION" size="10"/> </memory> <!-- NON_DURABLE_XA または FULL_XA である必要がある --> <transaction mode="NON_DURABLE_XA"/> </distributed-cache>
コメントにも書いていますが、トランザクショナルなCacheである必要があり、かつXAが有効であることを要求します。「XAが有効」とは、トランザクションの
モードが「NON_DURABLE_XA」(リカバリを無効にしたXA)か、「FULL_XA」のどちらかであることを指します。
これを守らない場合、もれなく例外が飛んできてエラーになります。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/MemoryConfigurationBuilder.java#L187-L195
今回はAuto Commitなノリで使いましょう。
テストコード。
test("exception eviction strategy") { withCache[String, String]("exceptionEvictionStrategyCache", 3) { cache => cache.getCacheConfiguration.memory.evictionStrategy should be(EvictionStrategy.EXCEPTION) cache.getCacheConfiguration.memory.size should be(10L) val thrown = the[CacheException] thrownBy (1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) thrown.getMessage should be("Could not commit implicit transaction") thrown.getCause.getCause.getCause match { case cause: RemoteException => cause.getMessage should include("ISPN000217: Received exception from") val remoteCause = cause.getCause remoteCause should be(a[ContainerFullException]) remoteCause.getMessage should be("ISPN000514: Container eviction limit 10 reached, write operation(s) is blocked") case cause: ContainerFullException => cause.getMessage should be("ISPN000514: Container eviction limit 10 reached, write operation(s) is blocked") case _ => fail(s"unknown Exception[${thrown.getCause}]") } cache.size should (be >=24 or be <= 30) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(cache.get(key))) println("size = " + cache.size) cache.size should (be >= 14 or be <= 20) } }
他の例に習って50個のエントリを追加していますが、途中で失敗しています。
val thrown = the[CacheException] thrownBy (1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) thrown.getMessage should be("Could not commit implicit transaction")
例外の原因はLocal Node発生したかRemote Nodeで発生したかで見え方が変わりますが、もとをたどると「Containerがいっぱいになった」という例外が
投げられていることがわかります。
thrown.getCause.getCause.getCause match { case cause: RemoteException => cause.getMessage should include("ISPN000217: Received exception from") val remoteCause = cause.getCause remoteCause should be(a[ContainerFullException]) remoteCause.getMessage should be("ISPN000514: Container eviction limit 10 reached, write operation(s) is blocked") case cause: ContainerFullException => cause.getMessage should be("ISPN000514: Container eviction limit 10 reached, write operation(s) is blocked") case _ => fail(s"unknown Exception[${thrown.getCause}]") }
もう少し、EXEPTIONなStrategyを追ってみましょう。
Eviction StrategyをEXCEPTIONとすると、InfinispanのInterceptorを構築する際にTransactionalExceptionEvictionInterceptorが追加されます。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L241-L246
このTransactionalExceptionEvictionInterceptorで、Evictionで指定した上限を越えていないか確認し、上限に達すると例外をスローするわけです。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/impl/TransactionalExceptionEvictionInterceptor.java#L112-L127
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/impl/TransactionalExceptionEvictionInterceptor.java#L193-L195
各種Strategyの雰囲気は、だいたいわかったのではないでしょうか?
もう少し中身を
それでは、もう少し中身に踏み込んでみましょう。
EvictionStrategyというのは、RemovalBaseかExceptionBaseかのフラグを設定したEnumにすぎないことがわかります。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L20-L36
NONE(false, false), MANUAL(false, false), REMOVE(false, true), EXCEPTION(true, false),
EvictionStrategyとして、Evictionが有効かどうかはNONEかMANUAL以外だということは、前に書きました。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L50-L52
とはいうものの、MemoryConfigurationとして扱った時に、Evictionが有効になっているかどうかというのは、RemovableBasedとなっているかどうかという
判定となるので、この点には注意です。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/configuration/cache/MemoryConfiguration.java#L86-L88
また、Deprecatedとなっていますが、既存のEviction StrategyであるUNORDERERDやFIFO、LRU、LIRSについても宣言としては残っていますが、REMOVEと同じ意味に
なっていますね。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/eviction/EvictionStrategy.java#L11-L18
@Deprecated UNORDERED(false, true), @Deprecated FIFO(false, true), @Deprecated LRU(false, true), @Deprecated LIRS(false, true),
そして、これらのEviction StrategyがDataContainer構築時に利用されます。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/factories/DataContainerFactory.java#L34-L56
Heapを使うデフォルトのDataContainerであれば、Caffeineの設定に反映されるという感じになります。
*Remove Strategyでsizeやcountでの閾値指定の場合
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/container/DefaultDataContainer.java#L101-L112
Off-Heapの場合は、自前でした。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/container/offheap/BoundedOffHeapDataContainer.java#L44-L53
Exception Strategyの場合は、Interceptorが例外をスローする、でしたね。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/impl/TransactionalExceptionEvictionInterceptor.java#L193-L195
まとめると、こんなところでしょうか。
Cache#evictされるのが、Local Nodeのエントリだけなのはどうして?
Eviction Strategyとは直接関係ありませんが、Cache#evictした時の挙動の話です。
これまで言っていた、Eviction StrategyはあくまでデータをCache#putした時に裏で動くEvictionの仕組みであって、明示的にCache#evictを呼び出した時は
また別の話となります。
これは、最初に全エントリをCache#evictしたのに、けっこうな数のエントリが残ってしまったことが最初どうしてかわからず、途中でPrimary Ownerのデータ以外は
evictしても消えていないという事実になんとなく気付きました。
こういうやつですね。
(1 to 50).foreach(i => cache.put(s"key${i}", s"value${i}")) cache.size should be(30L) println("size = " + cache.size) cache.forEach((key, _) => cache.evict(key)) cache.forEach((key, _) => println(key)) println("size = " + cache.size) cache.size should be(20L)
ownersが1のDistributed Cacheに50エントリ放り込み、残るエントリ数が30、これで全キーに対してCache#evictしても、消えているのは1 Node分。
なお、このCache#evictをCache#removeにすると、全エントリがキレイになくなります。
内部的にはCache#removeとCache#evictは、似たような関係にあります。
なにが似てるかって、Cache#removeとCache#evictの内部表現であるRemoveCommandとEvictCommandは継承関係にあるからです。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/commands/write/RemoveCommand.java
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/commands/write/EvictCommand.java#L19
では、この差はなにかというとトランザクション系のInterceptorにあります。
関連するのは、このあたりのInterceptor構築のところですね。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L288-L329
ここで登場する各種Interceptor…TriangleDistributionInterceptor、VersionedDistributionInterceptor、TxDistributionInterceptorなどいろいろありますが、
これらのInterceptorでRemoveCommandとEvictCommandでは扱いに差があります。
ベースとなるInterceptorの実装に対して、RemoveCommandについてはオーバーライドされており
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/distribution/TriangleDistributionInterceptor.java#L117-L120
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/distribution/TxDistributionInterceptor.java#L134-L137
Remote Nodeに対しても処理を行うように実装されています。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/distribution/TriangleDistributionInterceptor.java#L380-L404
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/distribution/TxDistributionInterceptor.java#L389-L416
これに対して、EvictCommandについてはデフォルト実装となっており、Commandを直接実行する流れになります。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/DDAsyncInterceptor.java#L93-L96
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/interceptors/BaseAsyncInterceptor.java#L51-L61
Cache#evict効果が、Local Nodeでしか反映されなかったのはこのためでしょう。
なお蛇足ですが、SyncなDistributed Cache、Replicated Cacheの場合にバージョン管理が必要だったら…という分岐があり、そうでなかったらAsyncな
Distributed Cache、Replicated Cacheにフォールスルーする記述があるのですが
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L293-L303
ここでいう「バージョン管理が必要だったら」というのは、トランザクションが有効なことを指しています。
https://github.com/infinispan/infinispan/blob/9.2.1.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L112-L113
なんか、Evictionについていろいろ深堀りすることになりました…。
まとめ
Infinispan 9.2で整理された、Eviction Strategyについて試しつつ、実装を追ってみました。
これまであんまり気にしていなかった、Cache#evictの処理の裏側も見れてちょっと勉強になりました。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-eviction-strategy