CLOVER🍀

That was when it all began.

Infinispan/Hazelcast/Ehcache/Guavaでベンチマーク

この前、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() {
  }
}