OpenSSO Benchmark by JMeter.

Jakarta Apache JMeter を使用して、OpenSSOをシングルサインオン性能を計測した。

観点

OpenSSOに対してLogin / Logout の操作を複数のユーザーが同時に行った際の同時実効性能、スループット。及び、セッションが増したときのWeb Container側のメモリの使用率など、他のマシンとの比較対象となる数値を計測する、また、その計測方法のサンプルを作成する。

測定環境


サーバー: WhiteBox
CPU: AMD Phenom 9350e 2.0GHz Quad-Core
RAM: 2GB
OS: Fedora 12 32bit
JDK 1.6
GlassFish v3 Preview / Sun Web Server 7.0 u6
OpenSSO Enterprise 8.0
Sun Directory Server Enterprise Edition 7.0

環境概要

OpenSSOのConfig Data Store / User Data Storeには、Sun Directory Server 7.0を使用。Directory Server 7.0はFedoraを正式にサポートしておらず、セットアップには多少のウォークアラウンドを要した。( # mv lib/private/libfreebl3.so lib/private/libfreebl3.so- )
Directory Server 6.xの操作知識があれば、7.0は問題なく操作できた。
ユーザーデータとして、10万件のエントリーを作成。サンプルデータの作成には、OpenDSに付属してくる make-ldif ユーティリティを使用し、LDIFを作成しインポートした。
Web Containerとして、まずは、GlassFish v3 Preview(Preludeでは無く)を使用。特別な設定なしにOpenSSO Enterprise 8.0のコンフィギュレーションが完了する。Sun Web Server 7.0 u6でも同様にテストを行う。Web Server 7.0 u6でもインストール時にDirectory Serverと同様のウォークアラウンドを要した。

JMeterテストケースの作成

JMeter 2.3.4を使用。
テストケースとして次の定義を用いる。
基本的なシナリオ

  1. /opensso/UI/Login?IDToken1=user.${__Random(0,100000,)}&IDToken2=password で、ログインする。
  2. 数秒間、待つ。
  3. /opensso/UI/Logout で、ログアウトする。

上記を、同時に数十スレッドから数百スレッド実行し、スループットとサーバー側のCPUやメモリの使用率を測定する。

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.1">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="amtest-plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="スレッドグループ" enabled="true">
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="ループコントローラ" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1000</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1258881060000</longProp>
        <longProp name="ThreadGroup.end_time">1258884000000</longProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <stringProp name="ThreadGroup.duration">60</stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP リクエスト初期値設定" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain">redcube.example.com</stringProp>
          <stringProp name="HTTPSampler.port">8080</stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path"></stringProp>
        </ConfigTestElement>
        <hashTree/>
        <CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="HTTP クッキーマネージャ" enabled="true">
          <collectionProp name="CookieManager.cookies"/>
          <boolProp name="CookieManager.clearEachIteration">false</boolProp>
          <stringProp name="CookieManager.policy">rfc2965</stringProp>
        </CookieManager>
        <hashTree/>
        <HTTPSampler2 guiclass="HttpTestSampleGui2" testclass="HTTPSampler2" testname="login" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="IDToken1" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">user.${__Random(0,100000,)}</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">IDToken1</stringProp>
              </elementProp>
              <elementProp name="IDToken2" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">password</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">IDToken2</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/opensso/UI/Login</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.FILE_NAME"></stringProp>
          <stringProp name="HTTPSampler.FILE_FIELD"></stringProp>
          <stringProp name="HTTPSampler.mimetype"></stringProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
        </HTTPSampler2>
        <hashTree/>
        <GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="シンプルコントローラ" enabled="true"/>
        <hashTree>
          <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="wait time" enabled="true">
            <stringProp name="ConstantTimer.delay">1</stringProp>
          </ConstantTimer>
          <hashTree/>
          <HTTPSampler2 guiclass="HttpTestSampleGui2" testclass="HTTPSampler2" testname="logout" enabled="true">
            <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
              <collectionProp name="Arguments.arguments"/>
            </elementProp>
            <stringProp name="HTTPSampler.domain"></stringProp>
            <stringProp name="HTTPSampler.port"></stringProp>
            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
            <stringProp name="HTTPSampler.response_timeout"></stringProp>
            <stringProp name="HTTPSampler.protocol"></stringProp>
            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
            <stringProp name="HTTPSampler.path">/opensso/UI/Logout</stringProp>
            <stringProp name="HTTPSampler.method">GET</stringProp>
            <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
            <boolProp name="HTTPSampler.auto_redirects">true</boolProp>
            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
            <stringProp name="HTTPSampler.FILE_NAME"></stringProp>
            <stringProp name="HTTPSampler.FILE_FIELD"></stringProp>
            <stringProp name="HTTPSampler.mimetype"></stringProp>
            <boolProp name="HTTPSampler.monitor">false</boolProp>
            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          </HTTPSampler2>
          <hashTree/>
        </hashTree>
        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="結果をツリーで表示" enabled="false">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>true</xml>
              <fieldNames>false</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="GraphVisualizer" testclass="ResultCollector" testname="グラフ表示" enabled="false">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>true</xml>
              <fieldNames>false</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="統計レポート" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>true</xml>
              <fieldNames>false</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="ガウス乱数タイマ" enabled="true">
          <stringProp name="ConstantTimer.delay">300</stringProp>
          <stringProp name="RandomTimer.range">100.0</stringProp>
        </GaussianRandomTimer>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

テストケース

同時実行数、50, 100, 200, 500, 1000と上げていく。 Glassfish: サーバー側のJVMのヒープには1GB割り当てた。( -Xmx1g ) それ以外はデフォルト。 Web Server 7.0u6: サーバー側のJVMのヒープに、-Xms512m -Xmx1gを指定。スレッドプールのキューサイズを1024に、キープアライブの最大接続数を1024に設定。 また、双方、ファイルディスクリプターの上限に達し、テストが継続できなくなったため、ulimit -n 5000を設定した。 測定時間:60秒間

結果(上段 GlassFish v3 / 下段 Sun Web Server 7.0 u6 )

Threads Complete Count Avg(ms) Median(ms) 90% Line(ms) Min(ms) Max(ms) through put( count/sec )
50 thread 7321
9583
105
10
34
9
210
16
6
4
299
80
121.32
159.33
100 thread 14695
18588
102
15
50
12
211
31
5
4
376
295
243.22
307.8
200 thread 21477
24555
248
171
201
55
548
479
5
4
871
1176
355.39
404.59
500 thread 19885
22826
1203
990
861
892
1929
1471
6
5
2390
13632
321.82
366.54
1000 thread 21072
20927
2547
2553
2400
2520
3550
3235
6
611
5787
5632
327.47
327.5
GlassfishとWeb Serverを比較すると、甲乙つけがたい結果であるが、同時接続の数が少ないときはWeb Serverが、同時接続数が多くなってくるとGlassfishが勝っている。GlassfishとWeb Serverでは、デフォルトのJVMのオプション設定が異なり、GCの動作がかなり異なっていた。基本的なHTTP接続処理性能はネイティブで動作するWeb Serverが有利であろう。Web Serverでリクエスト数が増えた際の性能を向上させるのであれば、JVMのGCの設定をGlassfishから流用すると良いかもしれない。 Web Serverで、500 thread実行時に、最大レスポンスタイムが13632ミリ秒と極端に悪い結果がひとつある。これは、JVMのGCが実行されたためである。GCが実行されるタイミングは予測が難しい。このような状態を避ける意味でも、GCのパラメーターは、適当なものを選ぶ必要がありそうである。とはいうものの、頻発するわけではないので状況によっては無視できるかも知れない。 Sun Web Serverは、GUI(BUI)に優れており、また、PHPやSHTML(SSI)も組み込め、導入が容易であるため良い選択肢だと思う。 両者、サーバー側のCPUは、75%程度を推移し健全な状態状態であった。また、1GB確保したヒープの領域は、500MB程度の使用で収まっている。本テストでは、ログインしたのちログアウトするので、サーバー側に保持されるセッションは同時実行スレッド数とほぼ同じであり、多くても最大1000程度のセッションが保持されるに留まる。 一方、JMeterを動させせたマシン側では、スレッド数が500程度で、CPUの使用率が100%に近づいていた。ボトルネックがJMeterを実行しているマシンとなっている可能性があり、テストの信憑性を下げている。(サーバー側の性能はもっとよい結果が予測できる) また、OpenSSOとDirectory Serverが同一マシンで稼働しており、これを別マシンと分離することでスループットが上がるかどうかも、検証が必要ではないかと考える。 もし、ログイン、ログアウトにかかる時間を2秒以下でサイトに対して約束するのであれば、一秒間に500クライアントまでという結果となった。 一分間の処理数としては、500同時アクセスで1万9千程度は処理できる。1000同時アクセスでも2万は処理できるが、一リクエストの処理時間の平均が2秒を越えてしまっているため、ユーザーの待ち時間が指摘される可能性がある。 防御的に考えると、健全に動作させるには、「500リクエスト/秒以下の使用状況で、一秒間に300程度の認証処理を平均1.2秒、概ね(90%)は、2秒以内(最大待ち時間2.4秒)で実行可能であり、一分間に1万8千程度の処理が期待できる」と、言うことができそうである