投稿日
Corda JMeterの使い方
もくじ
はじめに
こんにちは。ブロックチェーン推進室の鈴木です。
私の所属するブロックチェーン推進室では、企業間取引に特化したエンタープライズブロックチェーンプラットフォーム:Cordaに関して機能調査を行っております。
Cordaは世界で350社を超える金融機関、規制当局、中央銀行、業界団体、システム・インテグレーターやソフトウェアベンダーにより構成されるR3エコシステムから、エンドユーザー目線で設計・開発されています。
また、4半期に1度のバージョンアップにより機能追加がされていますが、その機能がお客様に活用できるものなのか、ドキュメントだけではわからないことも多く、ブロックチェーン推進室独自で調査を進めています。
このブログでは、そういったCordaの機能調査をした結果を機能の使い方含めて紹介していきます。
Cordaの導入方法についてはこちらのドキュメントをご参照ください。
今後以下の内容で紹介していきます。
(以降、バージョンアップの都度、新機能を紹介予定)
第1回目は「Corda JMeterの使い方」について紹介します。
導入
システムを導入する際、一般的にはパフォーマンステストの実施が必要です。
ブロックチェーンは新しい技術であり、パフォーマンステスト用ツールが充実していない部分もありますが、Corda Enterpriseには「Corda JMeter」が用意されており、Cordaに特化したパフォーマンステストの実施が可能です。
今回はCorda JMeterの使い方をご紹介します。
開発環境
- Windows10
- IntelliJ
- Kotlin 1.2 / Java 1.8
実行環境
- Corda Enterprise 4.3(2021年8月時点では最新版は4.8)
- Corda JMeter 4.2 …Cordaの処理に特化したJMeter。Enterpriseのモジュールに含まれています。レポート出力に一部不備があったため、Apache JMeter 5.2.1も併用
- AWS EC2…インスタンスタイプは、パフォーマンステストケースに応じて変更。Cordaのノード、ノード用のDBは同一サーバに相乗りさせます。
- PostgreSQL 9.6
- Corda側ノード環境…NodeA、NodeB、Notaryの3台構成(CENMはNotaryノードと相乗り)
- Corda側で利用するFlow(ソースコードは最下部の付録に記載)
-Create:自ノードに対して発行を実施する。Notaryは関与しない。
-Send:他ノードに送信する。Notaryが関与する。 - JMeter側設定…同時(0秒)に100スレッド作成し、10回ループする
-スレッド数:100
-Ramp-up:0
-ループ回数:10
Corda JMeter設定方法
ポイントをおさえ、設定方法・使用方法を紹介していきます。
▼モジュール取得
モジュール配置用のフォルダ(A)を作成します。 Apache JMeterをこちらからダウンロードし、解凍後、Aに配置します。
Corda Enterpriseのモジュールから「jmeter-corda-X.Y-capsule.jar」をJMeterのbinフォルダ下に配置します(X.Yはバージョンを表す) CordaのGithub(こちら)より、Samplerを取得し、Aに配置します。 自身で作成されたCordappを、Aに配置します。
フォルダ構成のイメージ
▼Samplerの修正
Cordappの依存関係を解決するため、修正が必要になります。また、実行するFlowに合わせて、Samplerの修正や新規作成を行う必要があります。
(1)jmeter-samplerフォルダに「lib」フォルダを新規作成し、Cordappのjarを配置します。
jmeter-samplerにlibを配置
(2)build.gradleのdependenciesで、配置したjarを読み込むように指定します。
dependencies {
ext.jmeterVersion = "3.3"
ext.cordaVersion = "4.3"
ext.cordaOsVersion = "4.3"
compile files("lib/workflows.jar","lib/contracts.jar")
(3)Samplerの修正/新規作成をします(今回利用したソースコード全量は最下部の付録に記載)
【Samplerの修正ポイント】
a.パラメータを定義する…JMeterPropertiesに、JMeterで使用するパラメータを定義する。
companion object JMeterProperties {
// JMeterのパラメータで使用する変数を定義
val tokenId = Argument("tokenID","","<meta>","tokenID")
val type = Argument("Type","","<meta>","type")
val value = Argument("value","0","<meta>","value")
}
b.パラメータの値を取得する…setupTestでパラメータ名と値を取得する。
override fun setupTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) {
// パラメータから値を受け取る
// パラメータ名(XXX.name)と値(XXX.value)
// 値が文字列以外の場合は、toXXX()で変換をかける
tID = testContext.getParameter(tokenId.name,tokenId.value)!!
typ = testContext.getParameter(type.name,type.value)!!
vlu = testContext.getParameter(value.name,value.value)!!.toInt()
}
c.パラメータを使用可能にする…additionalArgsでパラメータを指定する。
override val additionalArgs: Set<Argument>
// ここで指定(setOf)した変数がGUI上でパラメータとして設定できるようになる
get() = setOf(tokenId, type, value)
d.Flowを使用可能にする。
override fun createFlowInvoke(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext): FlowInvoke<*> {
// 実行するFlow(のクラス)を指定する
// 第2引数には、実行するFlowの引数を配列で渡す
// tokenID,Type,value
return FlowInvoke<TokenCreateFlow.Initiator>(TokenCreateFlow.Initiator::class.java, arrayOf(tID,typ,vlu))
}
▼SamplerJarの作成
前述した修正点を反映したjarファイルを作成します。
gradlew.bat jar
build/libsにSNAPSHOT.jarが作成されます。
custom-sampler-1.0-SNAPSHOT.jarの作成
▼Corda JMeter GUI起動
実際にノードに対してトランザクションを投げるため、事前にCorda Nodeを起動しておきます。
Corda JMeterを起動しますが、引数(XadditionalSearchPaths)に、上記のSNAPSHOT.jarとFlowのjarを指定する必要があります。
SNAPSHOT.jarを指定しないと後述のクラス指定が行えず、Flowのjarを指定しないとエラー(ClassNotFoundException)が発生します。
Corda JMeterのjarファイルがあるフォルダに移動し、Corda JMeterを起動します(各jarはフルパスを指定。区切りは;(セミコロン)環境によりパスは適宜変更してください)。
cd ApacheJMeter/bin
java -jar jmeter-corda-4.2-capsule.jar -XadditionalSearchPaths=/A/jmeter-sampler/build/libs/custom-sampler-1.0-SNAPSHOT.jar;/A/jmeter-sampler/lib/workflows.jar
▼テスト計画の作成
基本的な使い方は、Apache JMeterと同じですが、Javaリクエストで任意のFlowを呼び出せることが相違点です。
(1)テスト計画(右クリック)>追加>Threads>スレッドグループを選択します。
(2)スレッドグループ(右クリック)>追加>サンプラー>Javaリクエストを選択します。
(3)クラス名のプルダウンから対象のクラスを選択します。
(4)パラメータを設定します。実際のパラメータはnode.confを参照して設定してください。
Javaリクエストのパラメータ設定
なお、ノードを指定する場合は略称ではなく、PartyFullNameを指定してください。
例)
誤:PartyB
正:O=PartyB,L=New York,C=US
ノード指定時はPartyFullNameを指定
パラメータの名前が意図したものが表示されていない場合、Sampler側で適切に実装されていない可能性があります。
JMeterPropertiesで定義した変数をadditionalArgsのパラメータとして指定してください。
override val additionalArgs: Set<Argument>
// ここで指定(setOf)した変数がGUI上でパラメータとして設定できるようになる
get() = setOf(tokenId, otherParty, value)
(5)実行結果用のリスナー設定をします(CLI実行のみであれば不要)
(6)スレッドグループを選択し、スレッド数やループの回数といったプロパティを設定します。
- スレッド数:100
-Ramp-up:0
-ループ回数:10
なお、CordaはJavaベースということもあり、初回アクセスの反応が悪いため、下図のような試運転用のスレッドグループも作成しています。
試運転用のスレッドグループ設定
(7)スレッドグループではなく、「テスト計画」を保存してください。拡張子は「jmx」です。
「テスト計画」を選択し右クリック、別名で保存
テスト計画を保存
(8)下図の▶をクリックすることでテストが実行されます。なお、GUIで実行すると、表示等にメモリを消費する為、性能を正しく測定する場合は、CLIから実行することが推奨されています。
テストの実行
実行方法
両Flowを別のテスト計画として実施しています。また、前述の通り、適切に性能検証を行うためコマンドライン(CLI)で実施しています。
テストの進め方としては、テスト実行→インスタンス停止→インスタンスタイプ変更→インスタンス起動→テスト実行→…のように実施していきました。
▼Corda JMeterの実行
実行するFlowの説明と、実行方法は以下の通りです。Corda JMeter GUIを起動したコマンドにオプションを追加しています。
● -n…Non-GUI-Mode
● -t…テスト計画のjmxファイルを指定します(相対パスでも可)
● -l…レポート出力用のjtlファイル名を指定します。出力先に同名のファイルがある場合は出力できません。
(1)Create NodeAがCreateのFlowを実行
java -jar jmeter-corda-4.2-capsule.jar -XadditionalSearchPaths=/A/jmeter-sampler/build/libs/custom-sampler-1.0-SNAPSHOT.jar;/A/jmeter-sampler/lib/workflows.jar -- -n -t create.jmx -l 001_create.jtl
(2)Send NodeAがNodeBに向けてSendのFlowを実行
java -jar jmeter-corda-4.2-capsule.jar -XadditionalSearchPaths=/A/jmeter-sampler/build/libs/custom-sampler-1.0-SNAPSHOT.jar;/A/jmeter-sampler/lib/workflows.jar -- -n -t send.jmx -l 002_send.jtl
▼レポートHTML出力
Apache JMeterであれば、上記のコマンド実行の際にオプションをつけることでそのままレポート出力が可能ですが、Corda JMeterだとうまく出力されなかったため、Apache JMeterを使用してレポートを出力します。
使用するオプションは以下の通りです。
● -g…レポート出力対象のjtlファイルを指定します(相対パスでも可)
● -o…レポート出力先のフォルダを指定します(相対パスでも可)このときフォルダは空でなければいけません。
(1)Createのレポート出力
jmeter -g 001_create.jtl -o 001_create_test_h2db_report
(2)Sendのレポート出力
jmeter -g 002_send.jtl -o 002_send_test_h2db_report
▼レポート確認
テスト計画が正常に完了した場合、指定したフォルダにいくつかのフォルダやファイルが出力されています。そのうちの「index.html」をブラウザで開くことにより、グラフィカルにレポートを参照できます。
レポート出力後
index.html
以上が実施までの手順です。
今回、JMeter初心者ということもあり、実施するまでに、多少の時間がかかってしまいました。
手順内に記載しておりますが、改めて以下のポイントを注意していれば比較的早く実施できます。
注意するポイント
(1)JMeterでノードの指定方法 略称ではなく、PartyFullNameを指定する。
誤:PartyB
正:O=PartyB,L=New York,C=US
(2)JMeterでクラス名に表示されない(Flowが実行対象に無い)時の対処法 JMeterを起動する時に「-XadditionalSearchPaths」で、対象となるSamplerのjarを指定する必要がある。また、Samplerが別ディレクトリのCordapp jarを参照している場合は、そのjarも含める必要がある。
(3)JMeterで名前に表示されない(パラメータとして存在しない)時の対処法 Samplerで「JMeterProperties」でパラメータを定義して、「additionalArgs」でパラメータを登録する。
(4)JMeter起動時にjarを指定する 上記の通りFlowのjarと、Samplerのjarを指定する必要がある。
(5)レポート(html)出力がされない時の対処法 Corda JMeterだと出力されない。こちら側の設定ミスの可能性も考えられるが、対処法として実行結果ファイル(jtl)をCorda JMeterで作成し、そのファイルをApache JMeterで読み込ませてレポートを出力する。
上記ポイントをおさえれば、Corda JMeterでの実行はできます。
次回はこのCorda JMeterを使い、ノードスペックを変化させることで性能にどのように影響が出るかを見ていきます。
付録
CreateFlow.kt
package com.example.flow
import co.paralleluniverse.fibers.Suspendable
import com.example.contract.TokenContract
import com.example.state.TokenState
import net.corda.core.contracts.Command
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
object TokenCreateFlow {
@InitiatingFlow
@StartableByRPC
class Initiator(
val tokenID: String,
val Type: String,
val value: Int
) : FlowLogic() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val outputState = TokenState(tokenID, Type, serviceHub.myInfo.legalIdentities.first(), value)
val txCommand = Command(TokenContract.Commands.Create(), outputState.participants.map { it.owningKey })
val txBuilder = TransactionBuilder(notary)
.addOutputState(outputState, TokenContract.ID)
.addCommand(txCommand)
txBuilder.verify(serviceHub)
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
return subFlow(
FinalityFlow(
partSignedTx, emptyList(),
TokenExchange.Initiator.Companion.FINALISING_TRANSACTION.childProgressTracker()
)
)
}
}
}
SendFlow.kt
package com.example.flow
import co.paralleluniverse.fibers.Suspendable
import com.example.contract.TokenContract
import com.example.schema.TokenSchemaV1
import com.example.state.TokenState
import net.corda.core.contracts.Command
import net.corda.core.contracts.requireThat
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.builder
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
object TokenSendFlow {
@InitiatingFlow
@StartableByRPC
class Initiator(
val tokenID: String,
val to: Party,
val value: Int
) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryIdentities[0]
val generalCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED)
val states = builder {
val condition1 = TokenSchemaV1.PersistentToken::tokenid.equal(tokenID)
val q1 = QueryCriteria.VaultCustomQueryCriteria(condition1)
val criteria = generalCriteria.and(q1)
serviceHub.vaultService.queryBy<TokenState>(criteria).states
}
val inputState = states.first().state.data;
val outputState1 =
TokenState(inputState.tokenId, inputState.type, inputState.owner, inputState.value - value);
val outputState2 = TokenState(inputState.tokenId + "_2", inputState.type, to, value)
val list = arrayListOf(to.owningKey, inputState.owner.owningKey);
list.distinct();
val txCommand = Command(TokenContract.Commands.Send(), list)
val txBuilder = TransactionBuilder(notary)
.addInputState(states.first())
.addOutputState(outputState1, TokenContract.ID)
.addOutputState(outputState2, TokenContract.ID)
.addCommand(txCommand)
txBuilder.verify(serviceHub)
val partSignedTx = serviceHub.signInitialTransaction(txBuilder)
val otherPartySession = initiateFlow(to)
val fullySignedTx = subFlow(
CollectSignaturesFlow(
partSignedTx,
setOf(otherPartySession),
TokenExchange.Initiator.Companion.GATHERING_SIGS.childProgressTracker()
)
)
return subFlow(
FinalityFlow(
fullySignedTx,
setOf(otherPartySession),
TokenExchange.Initiator.Companion.FINALISING_TRANSACTION.childProgressTracker()
)
)
}
}
@InitiatedBy(Initiator::class)
class Acceptor(val otherPartySession: FlowSession) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val signTransactionFlow = object : SignTransactionFlow(otherPartySession) {
override fun checkTransaction(stx: SignedTransaction) = requireThat {}
}
val txId = subFlow(signTransactionFlow).id
return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId = txId))
}
}
}
example-sampler.kt
package net.corda.jmetersampler
import com.example.flow.TokenCreateFlow
import com.r3.corda.jmeter.AbstractSampler
import net.corda.core.messaging.CordaRPCOps
import org.apache.jmeter.config.Argument
import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext
import org.apache.jmeter.samplers.SampleResult
class ExampleSamplerTokenCreate : AbstractSampler() {
companion object JMeterProperties {
// JMeterのパラメータで使用する変数を定義
val tokenId = Argument("tokenID","","<meta>","tokenID")
val type = Argument("Type","","<meta>","type")
val value = Argument("value","0","<meta>","value")
}
// パラメータから値を受け取る変数
var tID : String = ""
var typ : String = ""
var vlu : Int = 0
override fun setupTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) {
// パラメータから値を受け取る
// パラメータ名(XXX.name)と値(XXX.value)
// 値が文字列以外の場合は、toXXX()で変換をかける
tID = testContext.getParameter(tokenId.name,tokenId.value)!!
typ = testContext.getParameter(type.name,type.value)!!
vlu = testContext.getParameter(value.name,value.value)!!.toInt()
}
override fun createFlowInvoke(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext): FlowInvoke<*> {
// 実行するFlow(のクラス)を指定する
// 第2引数には、実行するFlowの引数を配列で渡す
// tokenID,Type,value
return FlowInvoke<TokenCreateFlow.Initiator>(TokenCreateFlow.Initiator::class.java, arrayOf(tID,typ,vlu))
}
override fun teardownTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) {}
override val additionalArgs: Set<Argument>
// ここで指定(setOf)した変数がGUI上でパラメータとして設定できるようになる
get() = setOf(tokenId, type, value)
override fun additionalFlowResponseProcessing(context: JavaSamplerContext, sample: SampleResult, response: Any?) {}
}
example-sampler_send.kt
package net.corda.jmetersampler
import com.example.flow.TokenSendFlow
import com.r3.corda.jmeter.AbstractSampler
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import org.apache.jmeter.config.Argument
import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext
import org.apache.jmeter.samplers.SampleResult
class ExampleSamplerTokenSend : AbstractSampler() {
companion object JMeterProperties {
// JMeterのパラメータで使用する変数を定義
val tokenId = Argument("tokenID","","<meta>","tokenID")
val otherParty = Argument("to","","<meta>","type")
val value = Argument("value","0","<meta>","value")
}
// パラメータから値を受け取る変数
var tID : String = ""
lateinit var counterParty: Party
var vlu : Int = 0
override fun setupTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) {
// パラメータから値を受け取る
// パラメータ名(XXX.name)と値(XXX.value)
// 値が文字列以外の場合は、toXXX()で変換をかける
tID = testContext.getParameter(tokenId.name,tokenId.value)!!
counterParty = getIdentity(rpcProxy,testContext, otherParty)
vlu = testContext.getParameter(value.name,value.value)!!.toInt()
}
override fun createFlowInvoke(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext): FlowInvoke<*> {
// 実行するFlow(のクラス)を指定する
// 第2引数には、実行するFlowの引数を配列で渡す
// tokenID,Type,value
return FlowInvoke<TokenSendFlow.Initiator>(TokenSendFlow.Initiator::class.java, arrayOf(tID,counterParty,vlu))
}
override fun teardownTest(rpcProxy: CordaRPCOps, testContext: JavaSamplerContext) {
}
override val additionalArgs: Set<Argument>
// ここで指定(setOf)した変数がGUI上でパラメータとして設定できるようになる
get() = setOf(tokenId, otherParty, value)
override fun additionalFlowResponseProcessing(context: JavaSamplerContext, sample: SampleResult, response: Any?) {
}
}