LoginSignup
5
7

More than 5 years have passed since last update.

WASチューニングの巻(多重度)

Last updated at Posted at 2018-03-03

今回はWASのチューニング実践として、無意味なアプリケーションを作って多重度パラメタをいじってみます。
たまたまDeveloperエディションが使えるWASを対象にしていますが、どこのJavaEEサーバでも似たようなものかと思います。
某メーカー系のはやたらといろんなところにパラメタを持っていて、カスタマイズの幅が広いのはいいけれども設計する方の身にもなってくれよと常々思いますが。。。

前提

  • WEBサーバは経由せずWASのWebコンテナに直接アクセスする
  • WASのバージョンは9.0(Developerライセンス)
  • OSはVMWare上のCentOS Linux release 7.4.1708
  • CPUは2コア、メモリは充分に割当てる(Javaヒープも充分であるとする)
  • 負荷ツールとしてJMeterを使用
  • WASのパフォーマンスモニタリングツールとして標準提供のTPV(Tivoli Performance Viewer)を使用
  • OSのモニタリングはsarコマンドを使用
  • アプリケーションのチューニングは対象外
  • EJBは使わない(話をシンプルにしたいのとJavaEEプログラミングにあまり馴染みがない:x))
  • DB側の最大同時接続数は充分に確保(DBサイドでは不足なし)
  • 同様にDBのリソースは余裕があるものとする

ゴール

  • CPU使用率80%程度になる同時実行数を探す

WASの多重度(キューイングネットワーク)

WASのキューイングアーキテクチャの概要と今回対象とするパラメタは以下のようになります。EJBがないのでシンプルです。WEBコンテナの最大同時スレッド数とDataSourceの最大接続数をチューニング対象とします。
スクリーンショット 2018-03-03 9.15.00.png
()内の数値はデフォルト(初期値)です。

WEBコンテナのトランスポートチェーン

具体的なトランスポートチェーン名は、「WCInboundDefault」です。
スクリーンショット 2018-03-03 9.05.04.png
デフォルトでは20,000アクセスまでキューイングしてくれます(最大のオープン接続数)。このキューからWEBコンテナの最大スレッド数までリクエストをディスパッチすることになります。20,000もあれば充分なのでここはいじりません。タイムアウト値もデフォルトは60秒ですが、おそらく充分でしょう。ここは必要に応じて設定することにします。

WEBコンテナスレッドプール数

WEBコンテナのスレッド数はそのまんま「WebContainer」です。
スクリーンショット 2018-03-03 9.08.51.png

DataSource最大接続数

DataSource最大接続数は、DataSource単位の設定になります。こちらもそのまんま「最大接続数」です。
スクリーンショット 2018-03-03 9.20.41.png

アプリケーションの概要

テスト用のアプリケーション概要です。
スクリーンショット 2018-03-03 10.06.26.png

  1. クライアントからリクエスト
  2. MainServletが受けてDB参照
  3. 何かCPU負荷がかかる処理を実行
  4. DBを更新
  5. レスポンスを返す

クラスはMainServletだけ作ります。DataSourceのlookupはServlet初期化時にやってしまいましょう。あとはクライアントグループ単位に負荷とDBアクセス時間を変更できるように、無駄なsleepや負荷を調整するためのパラメタを随所に仕込んでおきます。

MainServlet
package net.mognet.was.tuning;

import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Random;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

/**
 * Servlet implementation class MainServlet
 */
@WebServlet("/*")
public class MainServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private DataSource ds = null;
    private int searchTime = 1;
    private int interval   = 1;
    private int duration   = 1;
    private int bit        = 1024;
    private int updateTime = 1;

    public MainServlet() {
        super();
    }

    //サーブレット初期化でDataSourceオブジェクトを取得
    public void init(ServletConfig config) throws ServletException {
        try {
            Context ctx = new InitialContext();
            this.ds = (DataSource) ctx.lookup("jdbc/mysql");
        } catch (NamingException e) {}
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doProcess(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doProcess(request, response);
    }

    //主処理
    protected void doProcess(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        response.setContentType("text/html; charset=utf-8");

        //負荷パラメタ取得
        try { searchTime = Integer.parseInt(request.getParameter("searchTime")); } catch(Exception e) {}
        try { interval   = Integer.parseInt(request.getParameter("interval")); } catch(Exception e) {}
        try { duration   = Integer.parseInt(request.getParameter("duration"));} catch(Exception e) {}
        try { bit        = Integer.parseInt(request.getParameter("bit"));} catch(Exception e) {}
        try { updateTime = Integer.parseInt(request.getParameter("updatTime"));} catch(Exception e) {}

        //総処理時間計測開始
        long startTime = System.currentTimeMillis();
        //DB参照
        doSearch(searchTime);
        //何か重たい処理
        doStress(bit, interval, duration);
        //DB更新
        doUpdate(updateTime);
        //総処理時間計測終了
        long endTime = System.currentTimeMillis();
        long etime = endTime - startTime;
        //レスポンス
        out.println("<pre>");
        out.println("searchTime: " + searchTime + "msec");
        out.println("interval: " + interval + "msec");
        out.println("duration: " + duration + "times");
        out.println("bit: " + bit + "bit");
        out.println("updateTime: " + updateTime + "msec");
        out.println("total elapsed time: " + etime + "msec");
        out.println("</pre>");

    }

    protected void doSearch(int searchTime) {
        doDB(searchTime);
    }

    //無駄に重たい処理
    protected void doStress(int bit, int interval, int duration) {
        BigInteger bi1 = BigInteger.probablePrime(bit, new Random());
        BigInteger bi2 = BigInteger.probablePrime(bit, new Random());
        for (int i = 1; i <= duration; i++) {
            bi1.multiply(bi2);
            try { Thread.sleep(interval); } catch (InterruptedException e) {}
        }
    }

    protected void doUpdate(int updateTime) {
        doDB(updateTime);
    }

    protected void doDB(int msec) {
        try {
            Connection con = ds.getConnection();
            Thread.sleep(msec);
            con.close();
        } catch (SQLException | InterruptedException e) {}
    }
}

DB処理は参照更新ともコネクションを取得して実際にはなにもせず、指定時間眠ってコネクションを破棄します。CPUに負荷をかけるのはdoStress()で、無駄に2つのおそらくは素数である(BigInteger.probablePrime()BigIntegerオブジェクトを作成して掛け算します。本当になんの意味もありません。ビット数と掛け算を繰り返す回数、およびインターバルは引数で指定できるようにしておきます。呼び出し側のメイン処理では、HTTPリクエストパラメタからこれらの実行時パラメタを取得して各メソッドを呼び出します。

負荷グループの設計

JMeterで負荷をかける上で必要なのがシナリオの作成です。負荷テストするうえで一番大変なのはこのシナリオ作成だったりしますが(特に新規サービスでユーザの実際の動きがほとんど予測できない場合など、もうどうにでもなれと言いたくなります)、今回は話をシンプルにするため、ユーザの動きは1パターンしかありません(全リクエストDBアクセスありというとんでもないパターンです)。実際にこんなシンプルな世界だったらどんなによかったことか。。。

それはともかく、今回は以下のようなグループに分けます(JMeterのスレッドグループに相当します)。

DBヘビーユーザ
DB参照10秒、無駄な計算1回だけ、DB更新10秒(ありえないけど)
CPUヘビーユーザ
DB参照20ミリ秒、無駄な計算を500ミリ秒おきに2回、DB更新20ミリ秒

まあ、こういうトランザクションミックスなんだと思っておきましょう。いずれのグループも指定時間リクエストを繰り返します(HTTPのKeep-Aliveは無効)。次回リクエストまでの待ち時間はオフセット500ミリ秒、偏差500ミリ秒のガウス分布タイマーを使います。

これで負荷シナリオは完成です。JMeterに実装するとこうなります。

stress.jmx
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="3.2" jmeter="3.3 r1808647">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="テスト計画" 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="DBヘビーユーザ" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <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">${__P(THREAD_NUMBER)}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1520044481000</longProp>
        <longProp name="ThreadGroup.end_time">1520044481000</longProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${__P(SUSTAIN)}</stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="リクエスト" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="searchTime" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">10000</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">searchTime</stringProp>
              </elementProp>
              <elementProp name="updateTime" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">10000</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">updateTime</stringProp>
              </elementProp>
              <elementProp name="interval" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">100</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">interval</stringProp>
              </elementProp>
              <elementProp name="duration" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">1</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">duration</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain">dev.mognet.net</stringProp>
          <stringProp name="HTTPSampler.port">9080</stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/stress/hoge</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">false</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="ガウス乱数タイマ" enabled="true">
            <stringProp name="ConstantTimer.delay">500</stringProp>
            <stringProp name="RandomTimer.range">500.0</stringProp>
          </GaussianRandomTimer>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="CPUヘビーユーザ" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <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">${__P(THREAD_NUMBER)}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1520044481000</longProp>
        <longProp name="ThreadGroup.end_time">1520044481000</longProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${__P(SUSTAIN)}</stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="リクエスト" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
            <collectionProp name="Arguments.arguments">
              <elementProp name="searchTime" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">20</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">searchTime</stringProp>
              </elementProp>
              <elementProp name="updateTime" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">20</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">updateTime</stringProp>
              </elementProp>
              <elementProp name="interval" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">500</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">interval</stringProp>
              </elementProp>
              <elementProp name="duration" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">2</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
                <boolProp name="HTTPArgument.use_equals">true</boolProp>
                <stringProp name="Argument.name">duration</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain">dev.mognet.net</stringProp>
          <stringProp name="HTTPSampler.port">9080</stringProp>
          <stringProp name="HTTPSampler.protocol">http</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/stress/hoge</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">false</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.BROWSER_COMPATIBLE_MULTIPART">true</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="ガウス乱数タイマ" enabled="true">
            <stringProp name="ConstantTimer.delay">500</stringProp>
            <stringProp name="RandomTimer.range">500</stringProp>
          </GaussianRandomTimer>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ResultCollector guiclass="TableVisualizer" 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>false</xml>
            <fieldNames>true</fieldNames>
            <responseHeaders>false</responseHeaders>
            <requestHeaders>false</requestHeaders>
            <responseDataOnError>false</responseDataOnError>
            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
            <assertionsResultsToSave>0</assertionsResultsToSave>
            <bytes>true</bytes>
            <sentBytes>true</sentBytes>
            <threadCounts>true</threadCounts>
            <idleTime>true</idleTime>
            <connectTime>true</connectTime>
          </value>
        </objProp>
        <stringProp name="filename"></stringProp>
      </ResultCollector>
      <hashTree/>
      <ResultCollector guiclass="ViewResultsFullVisualizer" 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>false</xml>
            <fieldNames>true</fieldNames>
            <responseHeaders>false</responseHeaders>
            <requestHeaders>false</requestHeaders>
            <responseDataOnError>false</responseDataOnError>
            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
            <assertionsResultsToSave>0</assertionsResultsToSave>
            <bytes>true</bytes>
            <sentBytes>true</sentBytes>
            <threadCounts>true</threadCounts>
            <idleTime>true</idleTime>
            <connectTime>true</connectTime>
          </value>
        </objProp>
        <stringProp name="filename"></stringProp>
      </ResultCollector>
      <hashTree/>
    </hashTree>
    <WorkBench guiclass="WorkBenchGui" testclass="WorkBench" testname="ワークベンチ" enabled="true">
      <boolProp name="WorkBench.save">true</boolProp>
    </WorkBench>
    <hashTree/>
  </hashTree>
</jmeterTestPlan>

同時接続ユーザ数を増やしながらCPU使用率が80%くらいになる落とし所を見つける、という作業になりますので、スレッドグループのスレッド数、つまり同時接続ユーザ数の部分は変数にしておいて、コマンド実行時に引数で渡すようにしてあります(${__P(VAR_NAME)})。

モニタリング

OSリソース使用状況

Linuxなのでsarが一番便利なんじゃないでしょうか。ファイルに出力しておいて後で平均値等を出しやすいようにしておきます。

$ sar -o sar.bin 1   # Ctrl+cで終了
$ sar -f sar.bin -s 11:00:00 -e 11:05:00 # 当該時間帯のCPU使用率
$ sar -r -f sar.bin -s 11:00:00 -e 11:05:00 # 当該時間帯のメモリ使用率

WASリソース使用状況

Tivoli Performance Viewer(以下、TPV)を使用します。WAS管理コンソールに実装されています。モニタリング可能な項目はカスタマイズ可能ですが、今回は基本セットでいきます。
スクリーンショット 2018-03-03 12.20.33.png
TPVの画面を開いて対象サーバを選択後、「モニターの開始」を押します。
スクリーンショット 2018-03-03 12.28.49.png
今回見ておきたいのは、DataSourceのコネクションプール数と、WebContainerのスレッドプール数なので、2箇所チェックを入れて「モジュールの表示」を押せば、グラフと表データとしてモニタリング可能です。
スクリーンショット 2018-03-03 12.30.19.png
特に注目しておくべき項目は、DataSourceの"WaitingThreadCount"と"WaitTime"です。今回はDB側リソースが充分という前提なので、基本的にJDBCコネクションの待ちが発生しないようにします。なので、この2項目は常に「0」を目指します。
「ロギング開始」を押すと、レコードをファイルに記録して後から再生できるようにしてくれますが、かなりファイルサイズが大きくなるので、注意が必要です。「設定」からサイズ指定可能ですが、基本セットを取っているだけでも30分くらいで5 GBほどになります。その後圧縮されるのでだいぶ小さくなりますが、とにかくいつの間にかロギング止まっているという状況になりがちです。基本的にはその場で確認するためのツールという印象ですね。

レスポンス・スループット

負荷ツールにJMeterを使うので、JMeterが出力するレポートを見るのが手軽です。グラフ化してくれたりもするのでありがたい存在です。実際に負荷をかけるコマンドは以下のようになります。

$ jmeter -n -t stress.jmx -JTHREAD_NUMBER=1 -JSUSTAIN=300 -l shot1.jtl -e -o ./shot1

-Jでテスト計画内で参照している変数を定義します。-oでレポート(HTML形式)出力先ディレクトリを指定します。
なお、分散環境(JMeterサーバ)を使用して負荷をかける場合は以下のようになります。

$ jmeter -n -t stress.jmx -r -GTHREAD_NUMBER=1 -GSUSTAIN=300 -l shot1.jtl -e -o ./shot1

JMeterサーバを複数使用する場合、jmeter.propertiesremote_hostsを定義しておきます。

jmeter.properties(抜粋)
remote_hosts=10.1.1.50:1099,10.1.1.51:1099,10.1.1.52:1099,10.1.1.53:1099

面白いというか注意しなければいけないのが、ここでTHREAD_NUMBERとして指定した値が、テスト計画中のスレッドグループのスレッド数として使用されますが、リモート実行の場合、全サーバが同じシナリオを実行するので、サーバ台数分の同時アクセス数になるという点でしょうか。

結果

やる前から分かりきっていることですが、このアプリケーション(笑)はWebコンテナのスレッド:DB接続が1:1です。初期設定で50:10ですから同時アクセスユーザ数が10を超えると、DBアクセス待ちが発生します。
スクリーンショット 2018-03-03 13.43.13.png
ユーザ数25(WebContanerのActiveCountが25)のときに、DBコネクション取得待ちのスレッドが14、時間にして平均で約2.5秒待たされていることがわかります。
というわけで、DataSourceのプール数も50に設定します。
スクリーンショット 2018-03-03 13.45.35.png
これで接続数を少しずつ増やしながらCPU使用率を調べると以下のような結果となりました。
スクリーンショット 2018-03-03 17.36.15.png
グラフから同時接続数40程度が限界(CPU使用率80%未満に収まる範囲)であることがわかります(X軸が同時アクセスユーザ数)。
一般的にCPU使用率100%までスループットはリニアに増加するものの、その後は徐々に増加率が低減し、さらに同時アクセス数が増えるとスループットが落ちる(飽和点)という特徴があります。一方で、CPU使用率が80%を超えると、システムの動作が不安定になるという神話のような言い伝えがありますので、大抵のケースではここをポイントにして流量制御することがおおいかと思われます。

流量制御

というわけで、多重度を以下のように40までにして、同時接続数を60とした場合にどうなるかを見てみます。
スクリーンショット 2018-03-03 18.27.45.png
スクリーンショット 2018-03-03 18.28.28.png
なお、WASのWebコンテナスレッド数には「最大スレッド・サイズを超えたスレッド割り振りを許可」という恐ろしいオプションが存在します。負荷に応じてよろしくやってくれるのかもしれませんが、ちょっとおっかないですね。。。ここではOFFにしてありますので、Webコンテナが捌き切れないリクエストはTCPインバウンドチャネルにキューイングされるはずです(TPVで見えればいいんですが残念ながら当該項目は見つけられませんでした)。

スクリーンショット 2018-03-03 18.45.27.png
Webコンテナのスレッド、DataSourceの接続プール(ともに40)を使い切っていますが、JDBCコネクションの取得待ちをしているスレッドはありません。想定通りの結果となりました。最初の図を使えば、インバウンドチャネルのところで20ユーザが待機させられていたということになります。なお、この間のCPU使用率は72.53%でした。
スクリーンショット 2018-03-03 18.49.51.png

というわけで、TPVの使い方をまとめておきました、というお話でした。

おまけ(WASのインバウンドキューを少なくすると)

こうなると、じゃあこのインバウンドチャネルにあるキューがなかったらどうなるのか気になるところです。この仕組みを備えた上で、さらにint listen(int sockfd, int backlog)でリッスンバックログを確保するとも思えないので、おそらくクライアントから見るとソケットエラーになるのではと推測されます。とりあえず40にしてみます。
スクリーンショット 2018-03-03 21.25.04.png
同じく60多重アクセスを5分間で、約7割がエラーになりました(Non HTTP)。
TCPレイヤのバックログ("/proc/sys/net/ipv4/tcp_max_syn_backlog = 512")もありますがこれをあてにするわけにもいきませんね。
さらに、オンプレ環境だとこういった設定を施すのがよくあるデザインパターンでしたが、クラウドでAWSのAuto-Scalingを使ったりするなら、このような流量制御はしないほうがいいですね。もう少し多重度をあげて、飽和点(CPU使用率が100%を超えてさらにスループットが少しずつ増加していってついに低下し始めるポイント)とまでいかなくとも、CPU使用率100%までの前提にしてもいいかもしれません。
で、CPU使用率(ロードアベレージ)が80%を超えたらスケールアウトすると。そう考えれば、余計なことはせずに目一杯開けておくというのも手かもしれません。その方が楽だし。。。

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7