この前、Javaで使えるキャッシュライブラリについてまとめたエントリを書きましたが、やっぱりキャッシュといえばパフォーマンスが気になるところですよね…。
NoSQLでのベンチマークといえば、YCSBですが、なんかInfinispanの扱いが微妙だったので、パスしていました。
YCSB(Yahoo! Cloud Serving Benchmark)
https://github.com/brianfrankcooper/YCSB/wiki
それで、ちょっと前にInfinispanのブログでInfinispanとHazelcastのパフォーマンス比較をしていて、なおかつ簡易的にやっていたので、それを利用させてもらうことにしました。
http://infinispan.blogspot.jp/2013/05/infinispan-vs-hazelcast-performance.html
https://bitbucket.org/ssmoot/scala-map-benchmarks
元のソースでは、Infinispan 5.2.5.FinalとHazelcast 2.5のベンチマークを取っています。やっていることは、Cacheに対する単純なputとgetをそれぞれ1,000回、10,000回、1000,000回やっています。設定は全部デフォルトで、Embedded Cacheモード。で、これをちょっと改造して、以下を比較してみました。なお、コードはScalaで書かれています…。こういうエントリだとJavaで書いておきたいところですが、まあ仕方がありません。
- Infinispan 5.2.7.Final
- Hazelcast 2.6
- Ehcache 2.7.2
- Google Guava 14.0.1
まあ、上2つは本来Data Gridだし、Guavaは機能面が全然違うので、こういう単純な比較だけをしても微妙なところですが、やっぱり気になるところでもありますよね。
Infinispanは、とりあえずある程度元の記事に合わせようということで。また、元のソースがjava.util.Mapインターフェースを使ったアクセスが前提になっていたので、Mapを実装していないEhcacheとGuavaのために、簡単なMapの実装を用意してそれ経由でアクセスさせることにしました。
*ソースは、後で載せます
ベンチマークを取るためのフレームワークは、GoogleのCaliperを使用しています。
Google Caliper
http://code.google.com/p/caliper/
最新の開発版は1.0-beta-1ですが、今回は元のソースが0.5-rc1を使用していので、それに合わせました。安定版というものはありません。また、0.5と1.0の間でクラス構成が変わっているのも1.0-beta-1を使わなかった理由ですね。でも、そのうちこれの使い方を覚えてもいいかもしれません。こういうベンチマークのためのフレームワーク、他にあるのかな?
あと、InfinispanだとRadarGunというData Gridのためのベンチマークを取るためのフレームワークもあるようですよ。
RadarGun
https://github.com/radargun/radargun
こちらは、ご参考までに。
計測環境は、Core2 Duo/2.53GHz、メモリ4.5GのUbunt Linux(VMware上)です…。
ちなみに、元の記事の結果は
On an iMac (i7 @ 3.4GHz) these are my results:
[info] service length benchmark us linear runtime [info] Infinispan 1000 Set 375.8 = [info] Infinispan 1000 Get 76.8 = [info] Infinispan 10000 Set 3989.3 = [info] Infinispan 10000 Get 774.1 = [info] Infinispan 100000 Set 50001.2 = [info] Infinispan 100000 Get 6165.3 = [info] Hazelcast 1000 Set 11838.6 = [info] Hazelcast 1000 Get 8098.7 = [info] Hazelcast 10000 Set 119332.3 == [info] Hazelcast 10000 Get 79783.9 = [info] Hazelcast 100000 Set 1290150.0 ============================== [info] Hazelcast 100000 Get 825389.0 ===================
でした…。あ、単位はマイクロ秒で、1回のオペレーションの平均値みたいです。
では、とりあえず、実行。
sbt run
で動きます。
[info] length service benchmark us linear runtime [info] 1000 Infinispan Set 1091.4 = [info] 1000 Infinispan Get 225.3 = [info] 1000 Hazelcast Set 27577.2 = [info] 1000 Hazelcast Get 14645.3 = [info] 1000 Ehcache Set 649.0 = [info] 1000 Ehcache Get 247.3 = [info] 1000 Guava Set 271.6 = [info] 1000 Guava Get 98.3 = [info] 10000 Infinispan Set 13594.6 = [info] 10000 Infinispan Get 2116.7 = [info] 10000 Hazelcast Set 269421.3 == [info] 10000 Hazelcast Get 136887.0 = [info] 10000 Ehcache Set 7152.0 = [info] 10000 Ehcache Get 2134.1 = [info] 10000 Guava Set 3170.5 = [info] 10000 Guava Get 912.5 = [info] 100000 Infinispan Set 141752.6 = [info] 100000 Infinispan Get 21254.5 = [info] 100000 Hazelcast Set 3013198.5 ============================== [info] 100000 Hazelcast Get 1385884.8 ============= [info] 100000 Ehcache Set 98762.9 = [info] 100000 Ehcache Get 22453.1 = [info] 100000 Guava Set 47946.0 = [info] 100000 Guava Get 9423.9 =
えーと、うちのPCが遅いですね…。Infinispan、Hazelcastを見比べると、3倍近く遅くなっています。なので、あんまり参考にならないかも。自分で計測したい人は、別途試してみてくださいな。
で、結果のほどなのですが、Guavaが最速ですね。Ehcacheの3倍くらい速い。1番軽量で機能も少ないので、ちょっとフェアじゃないかな。
とはいえ、Ehcacheも十分速いんじゃないかと。読み込みはInfinispanよりわずかに遅く、書き込みはEhcacheの方が速いという結果になっています。Hazelcastは…なんか件数が増えると速度劣化が激しいですね。
繰り替えしますが、ここでは機能面の比較はしていないので、これだけで優劣は決めない方がよいです。Guavaとそれ以外のライブラリでは、メモリに載せきらないエントリをディスクに逃したり、Data Gridのものであれば分散キャッシュなども組めたりするので、実際に使う時は用途に合わせて機能も見ていくことになるでしょう。
それに、あくまで単純なベンチマークですから。実際に使う時、特にWebアプリとかだと、並列アクセスされたらどうなるんだーとかあるでしょうからね。ここではやりませんが。
では、ちょっとプラスアルファ。
Infinispan 5.3.0.Finalと、Hazelcast 3.0-RC2の比較。Infinispanの最新の開発版は6.0.0.Alpha1なのですが、Maven Centralにあるものがちょっと???な感じだったので…。
[info] service length benchmark us linear runtime [info] Infinispan 1000 Set 934 = [info] Infinispan 1000 Get 202 = [info] Infinispan 10000 Set 11604 = [info] Infinispan 10000 Get 1805 = [info] Infinispan 100000 Set 122891 = [info] Infinispan 100000 Get 17604 = [info] Hazelcast 1000 Set 29408 = [info] Hazelcast 1000 Get 11954 = [info] Hazelcast 10000 Set 290449 == [info] Hazelcast 10000 Get 122187 = [info] Hazelcast 100000 Set 3199994 ============================== [info] Hazelcast 100000 Get 1167398 ==========
Infinispanは、5.2.7.Finalの時よりも読み書き共に速くなりました。Hazelcastは、読み込みは改善したものの書き込みは遅くなっています…。
Infinispan 5.3.0.FinalとEhcache 2.7.2の、1,000件、10,000件、100,000件、1,000,000件での比較。
[info] length benchmark service us linear runtime [info] 1000 Set Infinispan 952 = [info] 1000 Set Ehcache 629 = [info] 1000 Get Infinispan 210 = [info] 1000 Get Ehcache 231 = [info] 10000 Set Infinispan 10878 = [info] 10000 Set Ehcache 6672 = [info] 10000 Get Infinispan 1722 = [info] 10000 Get Ehcache 2094 = [info] 100000 Set Infinispan 126222 == [info] 100000 Set Ehcache 88981 = [info] 100000 Get Infinispan 17842 = [info] 100000 Get Ehcache 21668 = [info] 1000000 Set Infinispan 1496974 ============================== [info] 1000000 Set Ehcache 1017425 ==================== [info] 1000000 Get Infinispan 200249 ==== [info] 1000000 Get Ehcache 226731 ====
やってみて、こんな貧弱な環境で試すんじゃなかったと、強く思いました…。
その他、気になることがあればお手元の環境で動かしていただければと。
では、作成というか、修正したコードです。
build.sbt
よろしくありませんが、ライブラリの依存関係がごちゃ混ぜです…。
name := "cache-benchmarks" version := "littlewings" scalaVersion := "2.10.2" organization := "littlewings" resolvers += "JBoss Public Maven Repository Group" at "http://repository.jboss.org/nexus/content/groups/public-jboss" fork in run := true javaOptions in run <++= (fullClasspath in Runtime) map { cp => Seq("-cp", sbt.Build.data(cp).mkString(":")) } libraryDependencies ++= Seq( // Infinispan //"org.infinispan" % "infinispan-core" % "5.2.7.Final", "org.infinispan" % "infinispan-core" % "5.3.0.Final", "net.jcip" % "jcip-annotations" % "1.0", // Hazelcast //"com.hazelcast" % "hazelcast" % "2.6", "com.hazelcast" % "hazelcast" % "3.0-RC2", // Ehcache "net.sf.ehcache" % "ehcache" % "2.7.2", //"javax.transaction" % "jta" % "1.1", // Guava "com.google.guava" % "guava" % "14.0.1", "com.google.caliper" % "caliper" % "0.5-rc1" )
Ehcacheだけは、ほぼデフォルトですが設定ファイルを用意しました。
src/main/resources/ehcache.xml
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <diskStore path="java.io.tmpdir" /> <cache name="defaultCache" maxEntriesLocalHeap="0" /> </ehcache>
src/main/scala/benchmarks/CacheRunner.scala
package benchmarks import com.google.caliper.Runner object CacheRunner { // main method for IDEs, from the CLI you can also run the caliper Runner directly // or simply use SBTs "run" action def main(args: Array[String]) { // we simply pass in the CLI args, // we could of course also just pass hardcoded arguments to the caliper Runner Runner.main(classOf[CacheBenchmark], args) } }
src/main/scala/benchmarks/SimpleScalaBenchmark.scala
package benchmarks import com.google.caliper.SimpleBenchmark trait SimpleScalaBenchmark extends SimpleBenchmark { // helper method to keep the actual benchmarking methods a bit cleaner // your code snippet should always return a value that cannot be "optimized away" def repeat[@specialized A](reps: Int)(snippet: => A) = { val zero = 0.asInstanceOf[A] // looks weird but does what it should: init w/ default value in a fully generic way var i = 0 var result = zero while (i < reps) { val res = snippet if (res != zero) result = res // make result depend on the benchmarking snippet result i = i + 1 } result } }
src/main/scala/benchmarks/CacheBenchmark.scala
package benchmarks import scala.annotation.tailrec import com.google.caliper.Param abstract class PutOrGetMapSupport[K, V] extends java.util.Map[K, V] { def shutdown(): Unit = () private def notSupport: Nothing = throw new UnsupportedOperationException("Not Implemented.") def clear(): Unit = notSupport def containsKey(key: Any): Boolean = notSupport def containsValue(value: Any): Boolean = notSupport def entrySet: java.util.Set[java.util.Map.Entry[K, V]] = notSupport def isEmpty: Boolean = notSupport def keySet: java.util.Set[K] = notSupport def putAll(m: java.util.Map[_ <: K, _ <: V]): Unit = notSupport def remove(key: Any): V = notSupport def size: Int = notSupport def values: java.util.Collection[V] = notSupport } object Maps { import scala.collection.JavaConversions._ val infinispanCache: java.util.Map[String, String] = new PutOrGetMapSupport[String, String] { import org.infinispan.Cache import org.infinispan.manager.DefaultCacheManager val underlying: Cache[String, String] = new DefaultCacheManager().getCache("example") def get(key: Any): String = underlying.get(key) def put(key: String, value: String): String = underlying.put(key, value) } val hazelcastCache: java.util.Map[String, String] = new PutOrGetMapSupport[String, String] { import com.hazelcast.core.Hazelcast import com.hazelcast.config.{Config, MapConfig} val underlying: java.util.Map[String, String] = Hazelcast.newHazelcastInstance(new Config).getMap("example") def get(key: Any): String = underlying.get(key) def put(key: String, value: String): String = underlying.put(key, value) } val ehcacheCache: java.util.Map[String, String] = new PutOrGetMapSupport[String, String] { import net.sf.ehcache.Cache import net.sf.ehcache.CacheManager import net.sf.ehcache.config.CacheConfiguration import net.sf.ehcache.Element val cacheManager: CacheManager = CacheManager.newInstance(this.getClass.getResourceAsStream("/ehcache.xml")) val underlying: Cache = cacheManager.getCache("defaultCache") def get(key: Any): String = { val v = underlying.get(key) if (v != null) v.getObjectValue.asInstanceOf[String] else null } def put(key: String, value: String): String = { underlying.put(new Element(key, value)) null } //override def shutdown(): Unit = cacheManager.shutdown() } val guavaCache: java.util.Map[String, String] = new PutOrGetMapSupport[String, String] { import com.google.common.cache.{Cache, CacheBuilder} val underlying: Cache[String, String] = CacheBuilder.newBuilder.build.asInstanceOf[Cache[String, String]] def get(key: Any): String = underlying.getIfPresent(key) def put(key: String, value: String): String = { underlying.put(key, value) null } } import scala.util.hashing.MurmurHash3 def uuid(i:Int) = new java.util.UUID(0, MurmurHash3.stringHash(i.toString)).toString val store: Map[String, java.util.Map[String, String]] = Map("Infinispan" -> infinispanCache, "Hazelcast" -> hazelcastCache, "Ehcache" -> ehcacheCache, "Guava" -> guavaCache) } class CacheBenchmark extends SimpleScalaBenchmark { import java.util.UUID import scala.collection.mutable.MutableList @Param(Array("1000", "10000", "100000")) //@Param(Array("1000", "10000", "100000", "1000000")) val length: Int = 0 @Param(Array("Infinispan", "Hazelcast", "Ehcache", "Guava")) //@Param(Array("Infinispan", "Hazelcast")) //@Param(Array("Infinispan", "Ehcache")) val service:String = "" var cache:java.util.Map[String, String] = _ val ids = MutableList[String]() val lorem = """ Sed blandit augue non augue cursus sodales. Morbi suscipit arcu vitae nulla porta eleifend. Fusce scelerisque faucibus mi tempus suscipit. Praesent dignissim egestas convallis. Praesent tellus urna, lobortis nec pellentesque vel, venenatis placerat lorem. Fusce cursus fermentum tempor. Curabitur convallis est id urna faucibus id ullamcorper massa porta. Morbi id iaculis orci. Fusce pellentesque lectus dui, non pellentesque lectus. Maecenas ornare justo vel elit dapibus et tincidunt eros aliquam. Curabitur imperdiet eros vitae arcu euismod sed dictum dui sollicitudin. Nam consectetur pretium dolor, sit amet tempor ligula ultricies a. Suspendisse sem diam, vulputate at tincidunt eu, aliquam sit amet purus. Nam facilisis, neque id pharetra euismod, diam velit aliquet lacus, elementum tincidunt arcu turpis at metus. Suspendisse egestas diam a libero placerat sed pharetra risus cursus. Fusce viverra orci ut ipsum elementum nec commodo eros viverra. """ override def setUp() { cache = Maps.store.get(service).get /* cache = service match { case "Infinispan" => Maps.infinispanCache case "Hazelcast" => Maps.hazelcastCache case "Ehcache" => Maps.ehcacheCache case "Guava" => Maps.guavaCache } */ for(i <- 0 to length) ids += Maps.uuid(i) } def timeSet(reps:Int) = repeat(reps) { for(uuid <- ids) cache.put(uuid, lorem) cache } def timeGet(reps:Int) = repeat(reps) { for(uuid <- ids) cache.get(uuid) cache } override def tearDown() { } }