Hyperledger CaliperでBESUの性能測定
Hyperledger Caliperの使い方について解説をしていきます。
テクノロジーコラム
- 2021年08月31日公開
はじめに
- NTTテクノクロスの深津颯騎です。
- Hyperledgerプロジェクトの一種であるHyperledger Caliper(以下Caliper)を使って、Hyperledger BESU(以下BESU)の性能測定を実施しました。
- 検証に使用したソースコードはこちらのGithubリポジトリで公開しています。
Caliperとは
- Hyperledgerコミュニティが提供しているブロックチェーンの性能測定ツールです。
- Ethereum, BESU, Fabric, FISCO BCOSを対象に、スマートコントラクトに対するread/writeのリクエストに対して、下記の項目を測定することができます。
- ・スループット
- ・レイテンシー
- ・リソース使用率
- worker processを増やすことで並列度を上げて負荷を掛けることも可能です。
BESUとは
- Hyperledgerコミュニティが提供しているJAVA実装版のEnterprise Ethereumです。
- Enterprise Ethereum Alliance(EEA)の仕様に準拠しており、EVM(Ethereum Virtual Machine)が動作するためEthereumのスマートコントラクトはそのまま移植可能です。
- また、合意形成アルゴリズムはPoW, PoA, IBFTが選択可能です。
- Ethereumの場合はブロックチェーンに記録したデータはすべて公開されることが原則でしたが、BESUの場合は指定したメンバー間でしか情報の共有を行わないPrivacy Groupを使用することができるのが特徴です。
Caliperの使い方
検証環境の構成情報
- OS: Ubuntu20.04
- 台数: 1台
- メモリ: 8GB
- version:
- Nodejs: v12.16.1
- npm: 6.13.4
- docker: Docker version 19.03.12, build 48a66213fe
- BESU: besu/v21.7.0/linux-x8664/oracleopenjdk-java-11
- Caliper: 0.4.0
Caliperのインストール
-
Caliper-benchmarksをgit cloneします。
- Caliperのサンプルコードが含まれています。
$ cd
$ git clone https://github.com/hyperledger/caliper-benchmarks.git
$ cd caliper-benchmarks
- Caliperのサンプルコードが含まれています。
-
Caliperが使用するターゲットとSDKのバージョンを指定します。
- 今回はターゲットにbesu, SDKのバージョンにlatestを指定します。
測定対象のブロックチェーンを切り替えるには、ターゲットをfabric
のように変更すればOKです。$ npx caliper bind --caliper-bind-sut besu:latest
- 今回はターゲットにbesu, SDKのバージョンにlatestを指定します。
-
npmでcaliper-cliをインストールします。
-
$ npm install --only=prod @hyperledger/caliper-cli@0.4.0
-
測定対象環境の起動
- 測定対象のDocker Containerは、Caliper実行時に自動的に起動されるので、事前の起動は不要です。
- 具体的には、
networks/besu/1node-clique/networkconfig.json
のcaliper.command.start
caliper.command.end
が測定開始前と測定完了後に実行されるため、ここでdocker-composeを立ち上げるようにしておけばOKです。
測定実行
BESU1ノード構成での測定
- caliper managerによって負荷を掛けます
$ cd /home/vagrant/caliper-benchmarks
$ npx caliper launch manager \
--caliper-bind-sut besu:latest \
--caliper-benchconfig benchmarks/scenario/simple/config.yaml \
--caliper-networkconfig networks/besu/1node-clique/networkconfig.json \
--caliper-workspace .
-
Name Succ Fail Send Rate (TPS) Max Latency (s) Min Latency (s) Avg Latency (s) Throughput (TPS) open 1000 0 62.3 29.01 2.18 14.40 22.4 query 1000 0 100.2 0.02 0.00 0.00 100.2 transfer 50 0 7.0 5.03 0.04 2.55 5.0
- 測定結果がhtmlでも出力されています。 htmlの中身はコマンドの実行結果とほぼ同じです。
$ ls -la
report.html -rw-rw-r-- 1 vagrant vagrant 11001 Aug 12 09:30 report.html
BESU4台、Tesseraノード4台構成
- 4台構成にするために、独自のdocker-composeでオールインワン環境を立ち上げるようにしています。
-
caliper managerによって負荷を掛けます
$ cd /home/vagrant/caliper-benchmarks
$ npx caliper launch manager \
--caliper-bind-sut besu:latest \
--caliper-benchconfig benchmarks/scenario/simple/config.yaml \
--caliper-networkconfig networks/besu/besu4node/networkconfig_simple.json \
--caliper-workspace . -
Name Succ Fail Send Rate (TPS) Max Latency (s) Min Latency (s) Avg Latency (s) Throughput (TPS) open 1000 0 -2217.3 21.85 8.88 16.63 118.3 query 1000 0 100.4 0.05 0.00 0.00 100.3 transfer 50 0 261.8 10.05 0.70 5.54 5.2
Ethereum1ノード構成での測定
-
caliper managerによって負荷を掛けます
変更するのは--caliper-networkconfig
だけです。
ネットワークの構成設定ファイルを変更するだけで、異なる構成に対して簡単に性能を測ることができます。$ cd /home/vagrant/caliper-benchmarks
$ npx caliper launch manager \
--caliper-bind-sut besu:latest \
--caliper-benchconfig benchmarks/scenario/simple/config.yaml \
--caliper-networkconfig networks/ethereum/1node-clique/networkconfig.json \
--caliper-workspace . -
Name Succ Fail Send Rate (TPS) Max Latency (s) Min Latency (s) Avg Latency (s) Throughput (TPS) open 1000 0 58.2 29.36 2.14 16.38 22.2 query 1000 0 98.1 0.01 0.00 0.00 98.1 transfer 50 0 10.1 6.90 2.08 4.48 5.0 -
測定結果がhtmlでも出力されています。 htmlの中身はコマンドの実行結果とほぼ同じです。
$ ls -la report.html
-rw-rw-r-- 1 vagrant vagrant 11001 Aug 12 09:30 report.html
AWS環境上での測定結果
構成情報
-
- OS: Ubuntu20.04
- 台数: 5台(BESU4台+caliper1台)
- インスタンスタイプ: m5.large
- ストレージ: gp1, 30GB
- ネットワークの構成:
- AZ1: 2台(BESU×2)
- AZ2: 2台(BESU×1+Caliper×1)
- AZ3: 1台
- ブロックチェーンノード
- マイニング方式: IBFT
- ブロック生成間隔: 10秒
- blockGasLimit: 0x1fffffffffffff
- 台数:
- besu: 4台
- orion: 4台
- 負荷ターゲット: AZ1のbesu1号機
- version:
- Nodejs: v12.16.1
- npm: 6.13.4
- docker: Docker version 19.03.12, build 48a66213fe
- BESU: besu/v21.7.0/linux-x8664/oracleopenjdk-java-11
- Caliper: 0.4.0
測定結果
-
- 目標TPS
- open: 200
- query: 500
- transfer: 200
- JVMのメモリ上限: 6GB
-
測定結果
Name Succ Fail Send Rate (TPS) Max Latency (s) Min Latency (s) Avg Latency (s) Throughput (TPS) open 4095 0 598.8 44.78 13.56 33.94 90.5 query 4095 0 500.8 0.01 0.00 0.00 500.7 transfer 4095 0 602.4 35.71 7.34 26.07 117.6 -
目標TPS
- query: 2000
- JVMのメモリ上限: 6GB
- 測定結果
Name Succ Fail Send Rate (TPS) Max Latency (s) Min Latency (s) Avg Latency (s) Throughput (TPS) query 100000 0 909.2 0.07 0.00 0.00 909.2
- 目標TPS
性能測定で得られた知見
-
- Caliperの性能測定ツールを実装する際には、TX発行者のアドレスを動的に変えることはできない。
TX発行者のアドレスはnetworkconfig.json
のfromAddress
fromAddressPrivateKey
で設定したもので固定される。変更は不可。
よって、ERC721トークンを複数の人にmintすることはできるが、A→B→Cへ送付のようなTX発行者が変わるような操作はできない。
- Caliperの性能測定ツールを実装する際には、TX発行者のアドレスを動的に変えることはできない。
Caliperの解説
起動オプションの意味
-
オプション 設定値 意味 --caliper-bind-sut besu:latest 対象の環境とバージョン --caliper-benchconfig benchmarks/scenario/simple/config.yaml 測定するシナリオを定義したファイル --caliper-networkconfig ./networks/networkconfig.json 測定環境を定義したファイル --caliper-workspace . caliperが仕事するときの基点となるパス。
caliper launch managerコマンドの実行した際の大まかな処理の流れ
-
- caliper managerを起動
- docker-composeで環境構築
- 負荷測定開始
- 負荷測定終了
- docker-composeで環境を解体
- コマンドラインに結果が返る
- htmlでレポートが作成される
- caliper managerを起動
設定ファイルの解説
-
-
フォルダ構成
./caliper
├ benchmarks // ベンチマークを測定する負荷ツール
├ networks // 測定対象の環境。docker-composeで作る。
└ src // 測定対象のスマートコントラクト. abiファイルを格納 -
networks
- networks/.../networkconfig.json
- networks/.../data/genesis.json
- besuの初期ブロック。
- networks/.../keys/key
- besuノードの秘密鍵
-
src
- 測定対象のスマートコントラクトのapiファイルを格納する。
- このapiファイルは
networks/.../networkconfig.json
のethereum.contracts.simple.path
で指定する。
-
benchmarks/scenario/simple/
- ベンチマーク測定用の負荷ツールの置き場。スマートコントラクトに合わせて自作する必要あり。
- benchmarks/scenario/simple/config.yaml
- どのような負荷を掛けるかを定義したファイル。
- 目標TPS、実行プログラムが決められる。
- YAML形式のフォーマット
-
rateControl部分はこちらを参照
https://hyperledger.github.io/caliper/v0.4.2/rate-controllers/
-
benchmarks/scenario/simple/open.js, query.js, transfer.js
- 測定対象のプログラム毎にカスタマイズする
- 負荷を掛ける実行プログラムの本体
createWorkloadModule,
createSimpleState,
submitTransaction
の3つの関数から成り立つ。createWorkloadModule
- Caliperによって呼び出される関数
- クラス名くらいを変える程度でOK
createSimpleState
- caliperから渡される変数
config.yaml
のtest.rounds.workload.arguments
で定義した変数を使うことができるようになる。
submitTransaction
- caliperがBCに負荷を掛ける時に呼び出す関数
- 負荷を掛けるプログラム毎にSCの呼び出し関数が変わる
this.createConnectorRequest('SCの関数名', 関数に渡す引数のオブジェクト)
- benchmarks/scenario/simple/utils/operation-base.js
- いじる必要なし。
-
負荷を掛けるプログラムが継承する母体
-
benchmarks/scenario/simple/utils/simple-state.js
- SC毎に変更の必要あり
- SCの関数に渡す引数を調整するプログラム
- ランダムな値が設定された引数をいい感じに作るところ。
-
独自のスマートコントラクトに対して負荷を掛ける時のやり方
-
- スマートコントラクトをtruffleでコンパイルする
- コンパイルされた結果のファイル(build/contracts/**.json)を参考にし、下記の構成で
caliper/src/ethereum/<任意のフォルダ名>/<任意のファイル名>.json
に保存する
"name": "コントラクト名",
"abi": [
ABI情報
],
"bytecode": スマートコントラクトの実行コード,
"gas": TX発行時のGas量
-
性能測定をするインフラ構成のファイルを作成する
networks/besu/1node-clique/geth1node
を参考にし、同じような感じで作る。
-
上記フォルダ内の
networkconfig.json
を編集する- caliper.command.start: 開始時の処理。docker-compose.ymlのパスを変える
- caliper.command.end: 終了時の処理。docker-compose.ymlのパスを変える. ゴミ削除ツールがあるならそれも動かす
- ethereum.contracts."コントラクト名"に変更する
- ethereum.contracts."コントラクト名".path:
src/.../<任意のフォルダ名>/<任意のファイル名>.json
に作ったjsonファイルを指定する - ethereum.contracts."コントラクト名".gas: 関数毎のgas量を指定する
-
ベンチマーク測定用実行プログラムを作成する
benchmarks/scenario/simple/
をコピーし、benchmarks/scenario/MyNFT
とするbenchmarks/scenario/MyNFT/config.yaml
を編集する- simpleArgs: 実行プログラムのプログラムに対して引数を渡す場合には、その引数と値を宣言する
- test.name: コントラクト名
- test.rounds: 測定対象の関数の分だけ用意する
- test.rounds.labels: 任意の名前. 測定対象の関数名と同じにするのが分かりやすい
-
test.rounds.workload.module: 実行対象のプログラムのパスを指定する
-
initializeWorkloadModule
を編集するinitializeWorkloadModule
内のthis.
は、benchmarks/scenario/MyNFT/config.yaml
のsimpleArgs
に合わせること_createEthereumConnectorRequest
のcontract
は 測定対象のコントラクト名を指定すること
benchmarks/scenario/MyNFT/utils/simple-state.js
を編集する- コントラクトの関数で指定するパラメータを動的に変えたいなら、ここで引数部分をいい感じに作るようにする。
benchmarks/scenario/MyNFT/config.yaml
で指定した引数を使う場合は、constructor
部分で変数に格納する
OpenZeppelinのERC721に対して測定するときのサンプルコード
-
- OpenZeppelinから提供されているERC721の実装に対してCaliperを動作させるサンプルコードの例を紹介します。
-
独自アプリへ適用する際の参考にしていただければと思います。
-
フォルダ構成
benchmarks/MyNFT
├── config.yaml
├── mint.js
└── utils
├── operation-base.js
└── simple-state.js -
config.yaml
# 実行プログラム(nodejs)に渡すときの変数
simpleArgs: &simple-args
numberOfTokens: &number-of-tokens 1000
test:
name: MyNFT
description: >-
This is an example benchmark for Caliper, to test the backend DLT's
performance with NTFToken mint and transfer.
workers:
type: local # worker種別
number: 1 # worker台数
rounds:
- label: mint
description: >-
create token with mint function.
txNumber: *number-of-tokens # 測定1ラウンドのTX数
rateControl:
type: fixed-rate
opts:
tps: 50 # 負荷の目標TPS
workload:
module: benchmarks/MyNFT/mint.js # 実行するプログラム
arguments: *simple-args
-
utils/operation-base.js
サンプルコードとの変更点のみ抜粋async initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext) {
await super.initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext);
// config.yamlの引数に合わせて変更する箇所
this.assertConnectorType();
this.assertSetting('numberOfTokens');
this.numberOfTokens = this.roundArguments.numberOfTokens;
this.simpleState = this.createSimpleState();
}
_createEthereumConnectorRequest(operation, args) {
const query = operation === 'query';
return {
contract: 'MyNFT', // コントラクト名に合わせて変える
verb: operation,
args: Object.keys(args).map(k => args[k]),
readOnly: query
};
}
-
utils/simple-state.js
'use strict';
const HDWallet = require('ethereum-hdwallet')
const Web3 = require('web3');
const Dictionary = 'abcdefghijklmnopqrstuvwxyz';
// HDウォレットのシード値
const seed = Buffer.from('efea201152e37883bdabf10b28fdac9c146f80d2e161a544a7079d2ecc4e65948a0d74e47e924f26bf35aaee72b24eb210386bcb1deda70ded202a2b7d1a8c2e', 'hex')
// HDウォレットのインスタンス化
const hdwallet= HDWallet.fromSeed(seed)
const web3 = new Web3(new Web3.providers.HttpProvider("ws://localhost:8546"));
class SimpleState {
constructor(workerIndex, sutContext, accounts = 0) {
this.accountsGenerated = accounts;
this.sutContext = sutContext
this.accountPrefix = this._get26Num(workerIndex);
this.transferCount = 1 //transfer用試験で使うカウンター
this.ownerOfCount = 1 //ownerOf用試験で使うカウンター
}
_get26Num(number){
let result = '';
while(number > 0) {
result += Dictionary.charAt(number % Dictionary.length);
number = parseInt(number / Dictionary.length);
}
return result;
}
_getAccountKey(index) {
return "0x"+hdwallet.derive(`m/44'/60'/${index}'/0/0`).getAddress().toString('hex')
}
_getRandomAccount() {
// choose a random TX/account index based on the existing range, and restore the account name from the fragments
const index = Math.ceil(Math.random() * this.accountsGenerated);
this.accountsGenerated++;
return this._getAccountKey(index);
}
getMintArguments() {
let _to = this._getRandomAccount()
return {
// 変数名をSCの引数名に合わせること
_to: _to,
tokenURI: ""
};
}
}
module.exports = SimpleState;
-
mint.js
'use strict';
const OperationBase = require('./utils/operation-base');
const SimpleState = require('./utils/simple-state');
class Mint extends OperationBase {
constructor() {
super();
}
createSimpleState() {
return new SimpleState(this.workerIndex);
}
/**
* 測定用の関数実行
*/
async submitTransaction() {
let mintArgs = this.simpleState.getMintArguments();
//console.log("mintArgs",mintArgs)
await this.sutAdapter.sendRequests(this.createConnectorRequest('mint', mintArgs));
}
}
function createWorkloadModule() {
return new Mint();
}
module.exports.createWorkloadModule = createWorkloadModule;
まとめ
-
- Hyperledger Caliperを使用すると、複数のブロックチェーンネットワーク構成、異なるマシンスペックの環境に対して、統一的な方法で性能を測定することができるようになるのが特徴です。
測定できるのはEnd to Endではなく、ブロックチェーンのスマートコントラクトに対する読み書きであるため、ブロックチェーン層の純粋なポテンシャルを探ることができ、アプリケーション層との性能のボトルネックを調査するときにも使用することができるようになります。 今までは弊社ではJmeterを使ってアプリ層を通してブロックチェーンの性能測定をしていましたが、ブロックチェーン層のみの性能が把握できるのはとても魅力的でした。 このおかげでトランザクションのキャパシティー設計がやりやすくなりました。 - また、現在運用中の環境に対しても既存のソースコードに手を加えることなく測定が可能なため、導入のハードルが低く済むのも魅力です。
- Hyperledger Caliperを使用すると、複数のブロックチェーンネットワーク構成、異なるマシンスペックの環境に対して、統一的な方法で性能を測定することができるようになるのが特徴です。
デジタルトランスフォーメーション事業部
第一事業ユニット
ブロックチェーン関連のPoC, R&Dを中心に活動中