ベンチマーク (JdbcRunner) を DB2 for IBM i でやってみる (1) - 基本的な使い方 - (V7R1)

データベース負荷テストツールに JdbcRunner というものがあります。

どういうものかというと、紹介サイトの「Overview」に↓のように書いてあります。

JdbcRunnerは各種データベースを対象とした負荷テストツールです。
スクリプトでトランザクションを定義して多重実行し、スループットとレスポンスタイムを測定することができます。

さらに、

また、JdbcRunnerにはOracle、MySQL、PostgreSQLを対象とした以下のテストキットが付属しており、
ユーザが独自にスクリプトを作成する以外に、これらを用いた負荷テストを行うことも可能となっています。

ともあります。

マニュアルを見てみると、

JdbcRunnerはJavaで実装されているため、JDBCドライバが提供されているRDBMSであれば製品を問わず使用することができます。

ともあります。

そこで、今回はこのテストキット(SysBench/TPC-B/TPC-C)のそれぞれとチュートリアルを DB2 for IBM i で実行してみて、上記データベース以外での考慮点などを見てみよう、と思いたってやってみての結果のご紹介です。

結論から言うと、テストキットのいずれもチュートリアルもすべて、きちんと動きました。
ただし、データベースの実装に対する考え方の違いや JDBC ドライバーの実装などの違いで、スクリプトの方で考慮した方がいい面がいくつかありました。そのあたりをテストの記録としてここでに紹介してみようと思います。


チュートリアル

まず、チュートリアルを実行してみましょう。

テストデータの作成

2.1. データベースの準備」にならってテストデータを作成します。

↑の例では、テーブル作成に先立って JR という名前のスキーマを作成し、それを使用するようにしています。

JDBC ドライバーの設定

JDBC ドライバーは JTOpen を使用します。JDBC で DB2 for IBM i に接続するときの定番ですね。

JDBC ドライバーの含まれている jt400.jar というファイルがすでにクラスパスに存在していることを前提(;%classpath%)で、↓のように JDBC Runner を展開したディレクトリで jdbcrunner-1.2.jar をクラスパスに追加します。

C:\jdbcrunner-1.2>set classpath=jdbcrunner-1.2.jar;%classpath%

これで準備完了です。

シンプルなテストの実行例

2.3 スクリプトの作成」で作成している test.js を実行してみましょう。

function run() {
    var param = random(1, 5);
    query("SELECT data FROM tutorial WHERE id = $int", param);
}

これはそのまま実行できます。が、終了時に↓のようなエラーが出ることがあります。

JTOpen の JDBC ドライバーの場合、SQL 実行中に切断しようとするとこのようなエラーが出るんですね。
JTOpen の JDBC ドライバーはマルチスレッドで実装されています。他スレッドがまだ実行中に切断処理が行われようとするような時に↑のようなエラーが出る、ということらしいです。

SQL の実行も終了していますし、続けて実行も可能なので実害はほぼありません。
が、やはりエラーが出るのは気持ちがいいものではありませんよね。

Java の例外は Catch すればいいので、できるだけシンプルに紹介したいというチュートリアルの意図には反してしまうかもしれませんが、処理を try 〜 catch ブロックで囲んで、目的の例外をきちんと catch すれば↑のエラーは出ないようになります。

このツール、ちゃんと try 〜 catch ブロックをスクリプトに埋め込めるようになっているので、↓のように変更してみましょう。

function run() {
  try {

    var param = random(1, 5);
    query("SELECT data FROM tutorial WHERE id = $int", param);

  } catch (e) {
      if (isThreadProtectError(e)) {
          // just ignore.
      } else {
          error(e + getScriptStackTrace(e));
      }
    }

}

function isThreadProtectError(exception) {
    var javaException = exception.javaException;
    
    if (javaException instanceof java.sql.SQLException) {
        if (getDatabaseProductName() == "DB2 UDB for AS/400"
            && javaException.getMessage() == "Internal driver error. (class java.lang.InterruptedException)") {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

シンプルなテストの実行例(修正後)

実行の仕方

エラーの対応もできたので、あらためて実行の仕方から見ていきましょう。

JDBC ドライバーの設定」での set classpath ができた状態で

java JR test_400.js -jdbcUrl jdbc:as400://hostname/JR -jdbcDriver com.ibm.as400.access.AS400JDBCDriver

と実行することで、test_400.js は実行されます。("JR" はテストデータの存在するスキーマ名です)

データベースの挙動の設定

DB2 for IBM i の JDBC ドライバーについては、↓のように URL の一部としてオプションを追加することができます。

java JR test_400.js -jdbcUrl jdbc:as400://hostname/JR; "database name"="IASPNAME";"transaction isolation"="read committed";"concurrent access resolution"="1" -jdbcDriver com.ibm.as400.access.AS400JDBCDriver

IASPの場合、database= でデータベース名を指定できます。

"transaction isolation"= は分離レベルの指定ですね。

"transaction isolation" でデフォルトのトランザクション分離を指定します。 "none" "read uncommitted"(dft) "read committed" "repeatable read" "serializable" の 5種類が指定可能です。

"concurrent access resolution"= については、V7R1 から使用できるようになったロックの制御の仕方についての指定です。read committed という指定の場合に意味があります。

値 1は currently committed というオプションの指定で、同様の状況の時、まったく待たずに更新されてしまうかもしれない以前の値を取ってくるという動きです。他トランザクションで今まさに更新中の行が読み込み対象となった時、その他トランザクションのコミットをちょっと待って最新の値を手に入れようとするのが通常の DB2 の動きなわけです。
“今まさに更新中“ということはコミットが行われれば更新されてしまうわけで、DB2の挙動は”待ってより安全な値を入手しよう“という考えなんですね。

よりギャンブル的というか、”ロールバックするかもしれないし、とにかく待つ時間が惜しい“という考え方もあるわけです。これが値 2 にあたります。実はこれはOracleと同様の動きなんですね。

さらに、値 3 というのがあり、これは更新中の行のみをスキップする、というオプションです。

ANSI の read committed の規定では、「コミット済みのデータを読む」 = 「未コミットの行は読まない」ということになっています。
つまり、あるトランザクションの read がまさに更新中の行がアクセス範囲に入ってきたときに、論理的には 3つのオプションがあり得るわけなんですね。

  1. 更新が終わるまで待ってあげる
  2. 今まさしく更新中なのはわかっているし、古いのも承知だが、コミット済みのデータを取ってくる
  3. 該当の行をスキップする

DB2 for IBM iは、この3つの挙動を、この"concurrent access resolution"= というオプションの値を 1, 2, 3 と指定することで切り替えることができるんですね。

この 3つの挙動についてはインフォメーション・センターの「ロック待機の回避による並行性の改善」などにも記述があります。

更新も含めた基本的な例の実行例

3.3. 負荷テストの3つのフェーズ」にある例を実行してみたのが↓の例です。

シンプルなテストの実行例」での修正と同様な try 〜 catch ブロックの追加と例外の catch を行っています。

// CREATE TABLE JR/ACCOUNT 
//             ("ID" INT NOT NULL WITH DEFAULT, 
//              NAME VARCHAR (128 ) NOT NULL WITH DEFAULT, 
//              BALANCE INT NOT NULL WITH DEFAULT)                                                       


function init() {
    if (getId() == 0) {
        execute("UPDATE account SET balance = 0");
        commit();
    }
}

function run() {
  // try 〜 catch block 追加
  try {

    var accountId = random(1, 100);
    // var accountId = random(1, 5);
    var amount = random(-10000, 10000);

    // query("SELECT name, balance FROM account WHERE id = $int FOR UPDATE", accountId);
    // 標準SQLでは"FOR UPDATE"は不要・分離レベルで指定
    query("SELECT name, balance FROM account WHERE id = $int", accountId);
    execute("UPDATE account SET balance = balance + $int WHERE id = $int", amount, accountId);
    commit();

  // try 〜 catch block 追加
  } catch (e) {
      if (isThreadProtectError(e)) {
          // just ignore it
      } else {
          error(e + getScriptStackTrace(e));
      }
    }

}

// Exception Catch 追加
function isThreadProtectError(exception) {
    var javaException = exception.javaException;
    
    if (javaException instanceof java.sql.SQLException) {
        if (getDatabaseProductName() == "DB2 UDB for AS/400"
            && javaException.getMessage() == "Internal driver error. (class java.lang.InterruptedException)") {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }


}

function fin() {
    if (getId() == 0) {
        info("Total : " + fetchAsArray("SELECT sum(balance) FROM account")[0][0]);
        commit();
    }
}

try 〜 catch ブロックの追加と例外の catch 以外に、SQL 文の修正も行っています。

    // query("SELECT name, balance FROM account WHERE id = $int FOR UPDATE", accountId);
    // 標準SQLでは"FOR UPDATE"は不要・分離レベルで指定
    query("SELECT name, balance FROM account WHERE id = $int", accountId);
    execute("UPDATE account SET balance = balance + $int WHERE id = $int", amount, accountId);
    commit();

意外と知られていないことのような気がしますが、「標準 SQL」には SELECT 〜 FOR UPDATE という SELECT 文は存在しません。
FOR UPDATE というのは ANSI の SELECT 文の文法にはないんですね。

”カーソル”というものの定義の一部なんです。(『標準SQL ガイド』 p.129,p.131 および『SQL:1999 リレーショナル言語詳解』 p.456)
つまり、基本的には埋め込み SQL などでカーソルを使用する時にしか使えないものなんですね。決して SELECT 〜 FOR UPDATE という SELECT 文があるわけではないんです。

更新のためのロックは分離レベルでコントロールします。EJB などと同様に、コードはそのままでプロパティでコントロールするイメージです。今回の例で言えば transaction isolation でセットした値でコントロールするわけですね。

コードを変更することなしに分離レベルの指定だけでロック時の挙動を変えることができるわけで、慣れてしまえばこちらの方が使い勝手がいいという考え方もあると思います。
EJB や JPA の”宣言型トランザクション”と同じものといっていいと思いますので、ある意味、現代的な考え方なんですよね。

では、実行してみましょう。

java JR test_DB2.js -jdbcUrl jdbc:as400://hostname/JR;"database name"="IASPNAME";"transaction isolation"="read committed";"concurrent access resolution"="1" -jdbcDriver com.ibm.as400.access.AS400JDBCDriver

せっかくなので、ちょっと設定などを変えてみましょうか。

たとえば、"concurrent access resolution"の値を 2 に変えてみましょう。スループットなどに変化があることがわかります。

java JR test_DB2.js -jdbcUrl jdbc:as400://hostname/JR;"database name"="IASPNAME";"transaction isolation"="read committed";"concurrent access resolution"="2" -jdbcDriver com.ibm.as400.access.AS400JDBCDriver

こんなふうに挙動の違い等を観察するにもいいツールですよね。

ちょっとしたまとめ

だいたい基本的な使い方の紹介はできたかと思います。

考慮点は

といったところでしょうか。

上記修正はあくまで”スクリプト”に対してのみで、ツール自体にはなんら変更は必要ありません。

他データベースとの比較にも使えるとは思いますが、いろいろな条件をきちんとあわせておかないと比較にならないと思いますので、あまり安易にそういう用途では使わない方がいいかもしれませんね。ネットワークやファイアーウォールなど、いろんなものの影響を受けることがありますので。

むしろ、同じデータベースに対して、この記事の中で紹介しているように、設定の違いでどのくらいパフォーマンスが違うのか、といった使い方からはじめてみるといいのではないでしょうか。

[Top Pageに戻る]

Ads by TOK2