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-RSやJAX-WSはEJBのリモートインタフェースから呼び出す必要はなく、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 2013でemaggameさんが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クライアントコンテキスト管理にJBossのAPIを利用する必要があったのですが、JBoss EAP 6.1からはJNDIのAPIからEJBクライアントコンテキストの管理ができ、JBossのAPIを利用する必要はなくなっています。
サーバサイドは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が付属してはいるのですが、もう少し柔軟性が欲しいところです。
というわけでJRubyでjruby-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
- memcached
- REST
HotRodはInfinispanで使われているバイナリプロトコルで、分散キャッシュのときにデータオーナを識別してリクエストを送信することができるので、パフォーマンス上有利です。
memcachedプロトコルはバイナリプロトコルのほうではなく、テキストプロトコルのサポートです。memcachedは分散だけならできるのですが冗長化や永続化などは標準で提供されていないので、そういうのが欲しい場合に利用されます。
RESTは /rest/
InfinispanおよびInfinispan ServerはRed HatでJBoss Data Gridとして製品化されており、商用サポートが行われています。
とここまで書いたところでThink ITでJBoss Data Gridの連載があることを思い出したのでこの導入記事書かなくても良かったことに気付いた。正直すまんかった。明日これの続きになる本題書きます。あうあう。