nekop's blog

OpenShift / JBoss / WildFly / Infinispanの中の人 http://twitter.com/nekop

RESTEasyのクライアントが遅いのを調べる

JBoss / WildFly (全部俺) Advent Calendar 2013の13日目です。

昨日のJBoss EAP 6.2でEJBのnative, IIOP, JAX-WS, JAX-RS呼び出しのパフォーマンスを計測してみるというエントリで速いことを期待していたJAX-RSがなぜか遅かったので調べます。

RESTEasy 2.3.7.Finalのドキュメントのトランスポート層を見ると、やはりデフォルトではApache Http Client 4のSingleClientConnManagerという1つのソケットを使う並列実行できない実装を使う、となっています。

とりあえずClientRequestにURLConnectionClientExecutorを渡してApache Http Client 4ではなく、java.net.HttpURLConnectionを使うように修正してみます。

$ jruby load.rb rs
  3.520000   1.570000   5.090000 (  4.404000)

nativeと遜色ない速さになりました。満足です。みなさんJAX-RS使いましょう。

Apache Http Client 4.2のThreadSafeClientConnManagerとかPoolingClientConnectionManagerも試してみたんですが15秒とか出て大して速くならなかったので見なかったことにします。ApacheのHttpClientは過去にソケットクローズ漏れだとかいろいろやっかいな目に遭っていたりしてあまり好きではありません。

追記: 言及したソケットクローズ漏れはApache Commons HtttpClient 3のReflectionSocketFactory.javaのコードが原因で発生しますが、まだなおっていないようです。もう古いバージョンだし放置でいいかなぁと思っています。Apache Http Client 4では書き直されているのでこの問題は該当しません。

今日は保育園で子供とクリスマス会です。

JBoss EAP 6.2でEJBのnative, IIOP, JAX-WS, JAX-RS呼び出しのパフォーマンスを計測してみる

JBoss / WildFly (全部俺) Advent Calendar 2013の12日目です。

昨日全部入りEJBを作ったので各呼び出しインタフェースのパフォーマンスを軽く測ってみようと思います。未チューニングかつ計測対象も短いStringをSystem.out.println()してechoするだけというシロモノなので、てきとーなものであることに注意してください。実際のペイロードや処理、チューニングなどにより結果は変わります。

20スレッドのスレッドプールに10000回呼び出しを投げて全部終わるまでを計測します。回す対象ですが、EJBはContextの生成後のルックアップから、JAX-WSはServiceが使い回せるのでPortの取得から、JAX-RSは特に初期化はないのでリクエスト発行してるところを回します。

def with_executor(f)
  executor = Executors::newFixedThreadPool(20)
  10000.times do
    executor.execute(f)
  end
  executor.shutdown()
  executor.awaitTermination(30, TimeUnit::SECONDS)
end

計測はRubyのBenchmark::measureを使っています。出力はuser, system, user+system, (elapsed)です。

$ jruby load.rb native
  3.080000   0.780000   3.860000 (  3.468000)
$ jruby load.rb iiop
  9.300000   2.120000  11.420000 (  9.142000)
$ jruby load.rb ws
 11.730000   1.520000  13.250000 (  8.017000)
$ jruby load.rb rs
 32.460000  12.720000  45.180000 ( 16.139000)

nativeが高速でIIOPやJAX-WSが遅いというのは予想通りだと思いますが、JAX-RSの遅さが際立っています。これはたぶん書いたコードが負荷に対応できるような形にはなっていないのだろうと思います。明日のネタにしましょう。

JBoss EAP 6.2で全部入りEJB

JBoss / WildFly (全部俺) Advent Calendar 2013の11日目です。

EJBのリモート呼び出しは通常はコンテナが備えるプロプライエタリプロトコル(native protocol)で呼び出されますが、RMI-IIOPだったりJAX-WS, JAX-RSなどからも呼び出せます。というわけで全部から呼び出せる呼び出しインタフェース豊富なEJBを作ってみようと思います。

最終的に以下の6クラスできました。

  • EJB本体
  • EJB Remote Interface
  • EJB Local Interface (with JAX-RS, JAX-WS annotations)
  • JAX-RS Application
  • EJB2 Home Interface
  • EJB2 Remote Interface

EJBのリモート呼び出しにはリモートインタフェースが必要です。また、JAX-RSJAX-WSEJBのリモートインタフェースから呼び出す必要はなく、EJBローカルインタフェースを使うことになりますから、ローカルインタフェースも必要になります。@LocalBeanを利用したNo-interface viewはローカル限定でJAX-RSなどとは組み合わせ可能ですが、EJBリモートインタフェースとの組み合わせができなくなってしまうため今回は登場しません。

RMI-IIOPはEJB2のビューを要する機能なのでejb-jarじゃないと認識されません。そのため、クラスは全てejb-jarにパッケージし、JAX-RSを有効化するために空のWARと共にEARにしなければなりません。RMI-IIOPなEJBをWARにパッケージングして、META-INF/jboss-ejb3.xmlやWEB-INF/jboss-ejb3.xmlも含めるテストもやってみたのですが当然のごとくIIOP側にバインドされませんでした。普通はRMI-IIOPなんていう超レガシーは使わないのでWAR一個で大丈夫なはずです。

あとWARにした場合にちょっともにょったのですが、JRubyクライアントから参照しようと思った時にWARファイルはクラスパスに含めることができないので、target/classesを参照するようにたり、ライブラリjarを作ったりしないとダメなのがまた微妙です。

ソースのツリーはhttps://github.com/nekop/java-examples/tree/master/ee6-ejb-interfacesに、ファイル一覧はhttps://github.com/nekop/java-examples/tree/master/ee6-ejb-interfaces/src/main/java/com/github/nekop/examplesにあります。すごい適当に書いてます。

クライアントはパラメータにnative, iiop, ws, rsがあり、呼び出しインタフェースを切り換えるようにしました。JAX-RSのクライアントはいかようにも実装できるのですが、とりあえずRESTEasyのふつうのクライアントAPIで実装しています。

require 'java'

JBOSS_HOME="/home/nekop/eap6"

require "./target/ee6-ejb-interfaces.jar"
require "#{JBOSS_HOME}/bin/client/jboss-client.jar"
require "#{JBOSS_HOME}/modules/system/layers/base/org/jboss/resteasy/resteasy-jaxrs/main/resteasy-jaxrs-2.3.7.Final-redhat-2.jar"
require "#{JBOSS_HOME}/modules/system/layers/base/javax/ws/rs/api/main/jboss-jaxrs-api_1.1_spec-1.0.1.Final-redhat-2.jar"
require "#{JBOSS_HOME}/modules/system/layers/base/org/apache/httpcomponents/main/httpclient-4.2.1-redhat-1.jar"
require "#{JBOSS_HOME}/modules/system/layers/base/org/apache/httpcomponents/main/httpcore-4.2.1-redhat-1.jar"
require "#{JBOSS_HOME}/modules/system/layers/base/org/slf4j/jcl-over-slf4j/main/jcl-over-slf4j-1.7.2.redhat-2.jar"
require "#{JBOSS_HOME}/modules/system/layers/base/org/slf4j/main/slf4j-api-1.7.2.redhat-2.jar"

java_import "java.util.Properties"
java_import "javax.naming.Context"
java_import "javax.naming.InitialContext"

def initial_context
  p = Properties.new()
  p.put("remote.connections", "default")
  p.put("remote.connection.default.port", "4447")
  p.put("remote.connection.default.host", "localhost")
  p.put("remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED", "false")
  p.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming")
  p.put("org.jboss.ejb.client.scoped.context", true)
  InitialContext.new(p)
end

def corba_initial_context
  p = Properties.new()
  p.put(Context.INITIAL_CONTEXT_FACTORY,
        "com.sun.jndi.cosnaming.CNCtxFactory")
  p.put(Context.PROVIDER_URL, "corbaloc:iiop:localhost:3528/JBoss/Naming/root")
  InitialContext.new(p)
end

def hello_slsb(ejb_context)
  ear_name = "ee6-ejb-interfaces"
  ejbjar_name = "ee6-ejb-interfaces"
  ejb_name = "Hello"
  interface_name = "com.github.nekop.examples.HelloRemote"
  ejb_context.lookup("#{ear_name}/#{ejbjar_name}/#{ejb_name}!#{interface_name}")
end

type = ARGV.shift
case type
when "native"
  ejb_context = initial_context.lookup("ejb:")
  begin
    bean = hello_slsb(ejb_context)
    bean.hello("world")
  ensure
    begin
      ejb_context.close
    rescue
      # no-op
    end
  end
when "iiop"
  java_import "com.github.nekop.examples.HelloEJB2Home"
  java.lang.System::setProperty("com.sun.CORBA.ORBUseDynamicStub", "true")
  o = corba_initial_context.lookup("Hello")
  home = javax.rmi.PortableRemoteObject.narrow(o, HelloEJB2Home.java_class)
  bean = home.create()
  bean.hello("world")
when "ws"
  java_import "java.net.URL"
  java_import "javax.xml.namespace.QName"
  java_import "javax.xml.ws.Service"
  java_import "com.github.nekop.examples.HelloLocal"
  ejbjar_name = "ee6-ejb-interfaces"
  ejb_name = "Hello"
  wsdlLocation = URL.new("http://127.0.0.1:8080/#{ejbjar_name}/#{ejb_name}?wsdl")
  serviceName = QName.new("http://examples.nekop.github.com/", "#{ejb_name}Service")
  portName = QName.new("http://examples.nekop.github.com/", "#{ejb_name}Port")
  service = Service.create(wsdlLocation, serviceName)
  bean = service.getPort(portName, HelloLocal.java_class)
  bean.hello("world")
when "rs"
  java_import "org.jboss.resteasy.client.ClientRequest"
  war_name = "ee6-ejb-interfaces-web"
  url = "http://localhost:8080/#{war_name}/rest/hello/world"
  request = ClientRequest.new(url)
  request.get(java.lang.String.java_class);  
else
  puts "unko"
end

JBoss EAP 6.2のリモートEJB呼び出しの中身を覗く

JBoss / WildFly (全部俺) Advent Calendar 2013の10日目です。昨日のJava EE Advent Calendar 2013emaggameさんがUndertowについて書いてくれましたUndertowの詳細に関しては以下のビデオなども役立つと思います。

さて、昨日の続きでEJBを並列で2つリモート呼び出しして、クライアントとサーバ共にjstack、およびnetstatを取得してみましょう。EJBは指定したmsだけsleepするだけのメソッドです。

実行してnetstatを取得するとソケットは一つだけです。muxですね。

$ netstat -tn | grep 4447
tcp        0      0 127.0.0.1:33068         127.0.0.1:4447          ESTABLISHED
tcp        0      0 127.0.0.1:4447          127.0.0.1:33068         ESTABLISHED

サーバ側はRemotingのread, write, taskという3種のスレッド群で通信が処理されているようです。通信以外の実際のEJB呼び出しはEJBのスレッドプールに渡されて処理されています。

クライアントは呼び出しの2スレッドの他に、EJBクライアントのスレッドプールのスレッドがいくつかと、Remotingの3種のスレッド群がいくつか、というスレッド構成はサーバ側と大差ないようです。

クライアントは以下のように書き換えました。

require 'java'

JBOSS_HOME="/home/nekop/eap6"

require "#{JBOSS_HOME}/bin/client/jboss-client.jar"
require "./example-as7-ejb.jar"

java_import "java.util.Properties"
java_import "javax.naming.Context"
java_import "javax.naming.InitialContext"

def initial_context
  p = Properties.new()
  p.put("remote.connections", "default")
  p.put("remote.connection.default.port", "4447")
  p.put("remote.connection.default.host", "localhost")
  p.put("remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED", "false")
  p.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming")
  p.put("org.jboss.ejb.client.scoped.context", true)
  InitialContext.new(p)
end

def hello_slsb(ejb_context)
  ear_name = ""
  ejbjar_name = "example-as7-ejb"
  ejb_name = "HelloSLSB"
  interface_name = "jp.programmers.examples.ejb3.slsb.Hello"
  ejb_context.lookup("#{ear_name}/#{ejbjar_name}/#{ejb_name}!#{interface_name}")
end

ejb_context = initial_context.lookup("ejb:")
begin
  t1 = Thread.new do
    hello_slsb(ejb_context).sleep(20000)
  end
  t2 = Thread.new do
    hello_slsb(ejb_context).sleep(20000)
  end
  t1.join
  t2.join
ensure
  begin
    ejb_context.close
  rescue
    # no-op
  end
end

そうそう、UndertowのLive from the living roomというバージョンもあってこっちも結構好きです。

明日は会社おやすみして子供と遊びに行きます。

JBoss EAP 6.2のリモートEJB呼び出し

JBoss / WildFly (全部俺) Advent Calendar 2013の9日目です。

まず最初に注意です。リモートEJB呼び出しとか基本的には過去の遺産なので、できる限りサービスの呼び出しはRESTにしましょう。ポータブルでシンプルなAPIのほうが良い。

さて、JBoss EAP 6のリモートEJBの呼び出し方法がEAP 5までとは異なるので、ちょっと遊んでみます。JBoss EAP 6.2.0を使います。

JBoss EAP 5まではJNDIからルックアップして呼び出すだけだったのですが、このモデルではEJB呼び出しのリソース境界となるAPIがありません。そのため、EJBクライアントがソケットやスレッドをプールできない、プールしたとしてもいつクローズしていいか分からないという暗黙のプール実装を行うしかないというオチになっていました。EJBクライアント側でEJBの呼び出しコンテキストがきちんと管理できれば、より効率的なEJB呼び出しが可能になります。というのを実装したのがJBoss EAP 6のEJB呼び出しモデルです。

JBoss EAP 6.0ではEJBクライアントコンテキスト管理にJBossAPIを利用する必要があったのですが、JBoss EAP 6.1からはJNDIのAPIからEJBクライアントコンテキストの管理ができ、JBossAPIを利用する必要はなくなっています。

サーバサイドはas7-ejbというAS7向けのEJBサンプル実装を前に作ったのでそれをそのまま使います。このプロジェクトはAS7向けにクライアントも実行できるそこそこきちんとしたpom.xmlを書いたのですが、EAP6に対応させるのがかなり面倒そうだったのでやっぱり手抜きしてJRuby使います。

コードがちょっと長めに見えますが、この例ではセットアップ部分が主なのと、わざと冗長に書いてある部分があるのでそのへんは差し引いてください。実用するために共通化とかすれば実際にはgetHelloSLSB().hello()の一行程度になるはずです。

require 'java'

JBOSS_HOME="/home/nekop/eap6"

require "#{JBOSS_HOME}/bin/client/jboss-client.jar"
require "./example-as7-ejb.jar"

java_import "java.util.Properties"
java_import "javax.naming.Context"
java_import "javax.naming.InitialContext"

p = Properties.new()
p.put("remote.connections", "default")
p.put("remote.connection.default.port", "4447")
p.put("remote.connection.default.host", "localhost")
p.put("remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED", "false")
p.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming")
p.put("org.jboss.ejb.client.scoped.context", true)

initial_context = InitialContext.new(p)
ejb_context = initial_context.lookup("ejb:")
begin
  ear_name = ""
  war_name = "example-as7-ejb"
  ejb_name = "HelloSLSB"
  interface_name = "jp.programmers.examples.ejb3.slsb.Hello"
  bean = ejb_context.lookup("#{ear_name}/#{war_name}/#{ejb_name}!#{interface_name}")
  puts bean.hello("world")
ensure
  begin
    ejb_context.close
  rescue
    # no-op
  end
end

一番のポイントはejb_contextの取得とクローズです。このオブジェクトがこのサーバに対するEJBのクライアントコンテキストに対応しています。ejb_context.closeを呼び出すことによって、EJBクライアントのリソースが解放されるようになっています。

JRubyでHotRodクライアントを書く

JBoss / WildFly (全部俺) Advent Calendar 2013の8日目です。

Infinispan Serverのテストをするときに、まず1万エントリ突っ込む、というようなことをよくやります。他にも並列アクセスだとかいろいろなシナリオを実行したいのですが、このようなやたら頻繁に書き直すようなトライアンドエラー系の仕事をJavaでやるというのは罰ゲームっぽいのでスクリプトでやりたい。Infinispan Serverにはispn-cli.shという簡易CLIが付属してはいるのですが、もう少し柔軟性が欲しいところです。

というわけでJRubyjruby-hotrod.rbというのを書きました。

バルクオペレーションのテスト前提なのでキー末尾にはカウントがくっつく仕様です。一旦こんなスクリプト書いてしまえばあとは煮るなり焼くなりマルチスレッドから負荷かけるようにするなりある程度自由にできます。

jruby jruby-hotrod.rb put -c 10000

手元の環境だと2.5秒くらいで1万エントリのputできます。

JRubyで書くとDir.glob each requireできるのでMavenとかclasspathとか最初の面倒なところすっとばしてコード書き始められるので良いですね。

Infinispan Server

JBoss / WildFly (全部俺) Advent Calendar 2013の7日目です。

分散インメモリキーバリューストアのInfinispanですが、Infinispan Serverというものもダウンロードできます。通常のInfinispanは単なるJavaのライブラリですが、こちらはこれは起動シェルなどが付属するキャッシュサーバで、memcachedと同じような位置付けのものです。JBoss AS7がベースになっています。

起動シェルは2種類、standalone.shとclustered.shがあり、前者が非分散キャッシュサーバ、後者が分散キャッシュサーバとして動作するようになっています。前者の設定ファイルのデフォルトがstandalone.xml、後者がclustered.xmlとなっているだけでシェル自体にはそれ以外の差はありません。

以下の3種のプロトコルをサポートしています。

HotRodはInfinispanで使われているバイナリプロトコルで、分散キャッシュのときにデータオーナを識別してリクエストを送信することができるので、パフォーマンス上有利です。

memcachedプロトコルはバイナリプロトコルのほうではなく、テキストプロトコルのサポートです。memcachedは分散だけならできるのですが冗長化や永続化などは標準で提供されていないので、そういうのが欲しい場合に利用されます。

RESTは /rest// の形式でGETで取得、POST/PUTで更新が行えます。

InfinispanおよびInfinispan ServerはRed HatJBoss Data Gridとして製品化されており、商用サポートが行われています。

とここまで書いたところでThink ITでJBoss Data Gridの連載があることを思い出したのでこの導入記事書かなくても良かったことに気付いた。正直すまんかった。明日これの続きになる本題書きます。あうあう。