複数のCatapultノードで4,000TPSに挑戦してみた(瞬間4,000TPS)

先週(1/20)に行われたNEM Bootcaptの中の「秒間4,000トランザクション記録にチャレンジ！」をチャレンジしてみた記事です。

* [NEM Bootcamp \- Hello, Blockchain\! \- NEM JAPAN](https://nemjapan.jp/nem-bootcamp1/)

結論としては最大瞬間風速的に4,000TPSを達成という感じです。
だいぶ右往左往していてちょっとわかりにくいかもしれませんが、
よりシンプルに、もしくはより複雑に発展させていく参考になれば幸いです。
とにかくブロックに詰め込んでみました。


# ネットワーク構成

オリジナルからはちょっとアレンジをしています。
あれこれイジっていてこの構成で達成しました。

<img width="565" alt="スクリーンショット 2019-01-28 9.38.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/2353/b27d04ff-1b6a-a850-064f-b798dae2459f.png">

APIエンドポイントに投げつけるペイロードのスループットがイマイチだせなくて、
雑ですが単純に数を増やしていったという感じです。

入り口が複数あって、後ろのPeerノードが通信してネットワークを構築しているという状態です。
更に各ノードごとにペイロードを投げつけるJMeterを実行するインスタンスを配置して、
そこからぶつけに行きます。

## スペック

アリババクラウドのECSを利用して構築してみました。

* ecs.se1ne.2xlarge (インスタンスタイプ)
    * アジア東北1(東京)
    * 8 vCPU
    * 64 GiB
    * Intel Xeon E5-2682v4 / Intel Xeon(Skylake) Platinum 8163 2.5 GHz
    * 2 Gbps1000,000 PPS
    * SSD クラウドディスク
    * 69.20JPY/時間
    * ３台でおよそ 14,9472JPY/月(30日)

* Catapult Service Bootstrap (Bison `2348d31` がベース)
    * [44uk/catapult\-service\-bootstrap at prevent\-overwriting\-configs](https://github.com/44uk/catapult-service-bootstrap/tree/prevent-overwriting-configs)
    * 起動時に設定ファイルが上書きされないように修正

* Apache JMeter 5.0
    * ペイロードを投げつけるために使用する

このインスタンスタイプを選択したのは、とりあえず使える中で一番いいやつだったからです。
月額課金のやつやエンタープライズを申請したらもっといいのが借りられるっぽいですが
ちょっとよくわかりません。


# 環境構築

各インスタンスでCatapultが`docker-compose -f docker-compose-with-explorer.yml up`でノードが立ち上がるようにします。

[catapult\-service\-bootstrap のノードを別々のサーバに立ち上げてネットワークを構築する \- Qiita](https://qiita.com/44uk_i3/items/56e2a5ba2a8796b2e7a3)

こちらの記事は異なるリージョンに配置していますが、今回は同じリージョン内に配置します。
やはり異なるリージョンをインターネットでつなげると、4,000TPSは無理なんじゃないかと思います。
(がやってみる価値はあるかと)


## ノードを相互に接続

各サーバで`peer-0`では`7900`を、`peer-1`では`7910`を晒すように`docker-compose.yml`を編集します。

```yaml
peer-node-0:
    ports:                                                                      
    - "7900:7900"

peer-node-1:
    ports:                                                                      
    - "7910:7900"
```

異なるインスタンス間のノードをそれぞれ通信するように、Peerノードを追加します。

* `api-node-0/userconfig/resources/peers-p2p.json`
* `peer-node-0/userconfig/resources/peers-p2p.json`
* `peer-node-1/userconfig/resources/peers-p2p.json`

`"knownPeers"` に追加していきます。

```json
    {
      "publicKey": "29E45877930EE3FFB87AA4D2B7FCA062716BFA57029A0EBF14C40736A4305B63",
      "endpoint": {
        "host": "192.168.0.143",
        "port": 7900
      },
      "metadata": {
        "name": "peer-node-2",
        "roles": "Peer"
      }
    },
    {
      "publicKey": "4843C9559B1EECB8CE5C6ABBBE33B05C6542BBCC967F024B61FA114B55BDB974",
      "endpoint": {
        "host": "192.168.0.143",
        "port": 7910
      },
      "metadata": {
        "name": "peer-node-3",
        "roles": "Peer"
      }
    },
    {
      "publicKey": "29E45877930EE3FFB87AA4D2B7FCA062716BFA57029A0EBF14C40736A4305B63",
      "endpoint": {
        "host": "192.168.0.144",
        "port": 7900
      },
      "metadata": {
        "name": "peer-node-4",
        "roles": "Peer"
      }
    },
    {
      "publicKey": "4843C9559B1EECB8CE5C6ABBBE33B05C6542BBCC967F024B61FA114B55BDB974",
      "endpoint": {
        "host": "192.168.0.144",
        "port": 7910
      },
      "metadata": {
        "name": "peer-node-5",
        "roles": "Peer"
      }
    }
```

プライベートIPアドレスで互いのサーバを追記していきます。

本当は別のアカウントを設定すべきかもしれませんが、手抜きで同じアカウントを使ったので、元からある設定をコピペして、IPアドレスとポートと名前だけ書き換えました。

これで各サーバにて`docker-compose -f docker-compose-with-explorer.yml up`を実行してサーバを立ち上げます。

(何度かやり直したせいでしょうか、たまに同期できなかったので、どうもおかしいと思ったら`./clean-data`を実行して、一旦チェーンを削除してやりなおしてみてください)

相互に接続できていれば、エクスプローラで見たときに、各サーバで同じブロックが生成されていることが確認できます。

どのサーバでもいいので、試しにトランザクションを投げてみて、
承認されたブロックのハッシュや内容を他のサーバのエクスプローラから確認できるか確かめるとわかりやすいです。


# トランザクションをぶちかます準備

最初はお手本どおり、 @planethouki さんによるトランザクションの生成スクリプトを利用させてもらっていたのですが、

* [プライベートチェーンのカタパルトで秒間4000トランザクションを目指してみる \- Qiita](https://qiita.com/planethouki/items/9733aa83096a988ee57a#%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B)
* [planethouki/yonsen: A transaction making tool for NEM2 catapult to confirm 4,000 tx/s in private network\.](https://github.com/planethouki/yonsen)

ちょっと問題が発生。

* 事前のトランザクションペイロード作成がおわんねぇ…(私のマシンで6,7時間かかった)
* トランザクションの送信がメモリ不足でコケてしまいトランザクションを投げきれない

ので、ここもアレンジしました。以下、下準備です。
以下で作ったファイルはこのリポジトリにコミットしてあります。

* [44uk/catapult\-loadtest](https://github.com/44uk/catapult-loadtest)


## 事前にペイロードを生成

* [proximax\-storage/nem2\-sdk\-go: Official Go SDK for ProximaX/NEM Catapult](https://github.com/proximax-storage/nem2-sdk-go)

ProximaXのリポジトリにGo実装のSDKがあったので、これを利用してGoで書きました。

使い方はほぼ同じで、`nemesis_addresses`の`private` (秘密鍵) を集めたjson配列から、
ランダムに送り合うトランザクションを生成します。

```console
$ cat addresses.yaml | yq '[.nemesis_addresses[0:20][].private]' > privateKeys.json
```

秘密鍵のjsonは`yq`コマンドでザクッと作りました。便利ですねこれ。
初期分配は先頭22件に行っているようだったので、先頭20件程度を取ってます。
[catapult\-service\-bootstrap/template\_bindings\.rb at master · tech\-bureau/catapult\-service\-bootstrap](https://github.com/tech-bureau/catapult-service-bootstrap/blob/master/ruby/lib/catapult/config/nemesis_properties_file/template_bindings.rb#L20)

```console
$ time go run payloadGenerator.go -f ./privateKeys.json -n 10000 > payloads-10k.txt
```

* [catapult\-loadtest/payloadGenerator\.go at master · 44uk/catapult\-loadtest](https://github.com/44uk/catapult-loadtest/blob/master/payloadGenerator.go)

とりあえず10,000万行生成。コンパイル時間を入れても爆速でした。
(ちゃんと数値で比較してないですが、大量に生成したときにn時間が秒単位になります)
これでカジュアルに数十万単位で生成できるようになったので、
やり直したり、繰り返し大量のトランザクションを用意できるようになりました。


## JMeterを設定

負荷テストで鉄板のJMeterを利用することにしました。

* [catapult\-loadtest/catapultLoadTest\.jmx at master · 44uk/catapult\-loadtest](https://github.com/44uk/catapult-loadtest/blob/master/catapultLoadTest.jmx)

`CSV Data Set Config`を使うと、ファイルに羅列したデータを次々とリクエストとして投げられそうです。
形式は先に作ったペイロードの羅列を利用できます。区切られてないだけですからね。

<img width="994" alt="スクリーンショット 2019-01-28 0.14.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/2353/aabfb8f8-36a7-dce2-d3bb-58667ca07958.png">

リクエストボディとして、`{"payload": "<ここにペイロードの内容>"}`が飛ぶように設定。
`${payload}`の部分に行が流れ込んでいきます。

<img width="999" alt="スクリーンショット 2019-01-28 0.15.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/2353/0168c9b2-7a3c-d496-f24a-b75eddcec2c6.png">

JMeter Serverによるクラスタを構築してやってみようとも思ったのですが、いまいちうまく動かなかったので、
愚直に3台用意してそれぞれのAPIへぶつけることに。

スクリプトの作成はGUIで行いますが、実行は後述にあるようにCLIで叩きます。

* [お手軽JMeterクラスター 〜 フルボッコ編｜アドカレ2013 : CFn \#1 ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/aws-cfn-advent-calendar-2013-fullbok/)

これはAWSで動かすものですが、参考までにリンクを。
これをうまく使えたらもっと楽かも。


## JMeterを叩くインスタンスを作る

更にもう3つインスタンスを立ち上げて、それぞれのインスタンスのAPIサーバにトランザクションを投げ込んでいくようにしました。

ペイロードの生成をサーバ上でできるように`golang`もインストールしてしまいました。

```console
$ apt update && apt install openjdk-8-jre golang git
```

```console
$ wget https://www-eu.apache.org/dist/jmeter/binaries/apache-jmeter-5.0.tgz 
$ tar zxf apache-jmeter-5.0.tgz
$ cd apache-jmeter-5.0
```
このサーバへ`payloadGenerator.go`も`catapultLoadTest.jmx`もアップロードしてしまいます。

`catapultLoadTest.jmx`中のファイルの場所や、APIエンドポイントのIPアドレスを書き換える必要があります。
アップロード前でもサーバ上でもいいので、編集してください。(xmlなのでテキストエディタでもできます)

```xml:catapultLoadTest.jmx
<stringProp name="HTTPSampler.domain">192.168.0.142</stringProp>
```


# トランザクションをぶちかます

以下のような形でペイロードを流しまくってみます。

<img width="565" alt="スクリーンショット 2019-01-28 9.38.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/2353/b27d04ff-1b6a-a850-064f-b798dae2459f.png">

まずは各サーバでペイロードを生成。

```console
$ time go run payloadGenerator.go -f ./privateKeys.json -n 480000 > payloads-480k.txt
```

生成できたら、各サーバ同じタイミングで流しましょう。

```console
$ ./bin/jmeter -n -t ./catapultLoadTest.jmx
```

流れおわるのをしばし待ちましょう…


# 集計

<img width="1110" alt="スクリーンショット 2019-01-27 23.47.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/2353/a0c5fe62-265a-6ad4-bf6d-9ec1cf29a924.png">

コピペでもいいですが、めんどいのでスクリプトで取得。

```console
$ node blocks.js http://xxx.xxx.xxx.xxx:3000 > data.csv
```

直近100ブロックを拾ってきてCSVな感じで標準出力にだします。

そしてそれをスプレッドシートにて貼り付けて集計しました。

https://docs.google.com/spreadsheets/d/1cYfLGcoHCsE9q86EaffjR33kS7YHAZi_eYotEB-wj0Y/edit?usp=sharing

ちょ〜っと(だいぶ)波がありますが、瞬間的には4,000TPSを達成していました！

<img width="521" alt="スクリーンショット 2019-01-28 1.01.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/2353/20aa7b23-5d1e-2e05-1333-448ec2409250.png">


# まとめ

やたら複雑に、3台もインスタンスを使いましたが、1台のサーバに複数のノードがいるという状況なので、
実際の運用とはちょっと違う感じかと思います。(全体ではAPI3つ、Peer6つだけど同居している)
作業中、ちょいちょい不安定でうまく同期しなかったり、漏らすトランザクションがあったので、
若干スペック不足だったかもしれません。(が、従量課金で選べる一番いいやつがそれだった)

値段的にもこのインスタンスの利用において、計算上では15万円程度/月になってしまい、
あまり旨味が無い微妙な結果に…。もうちょっと真面目に構築したら違う結果になるかも。

受け口であるAPIサーバのスペックが低いとそもそもトランザクションを吐ききれなくて落としてしまったりするし、
APIサーバだけ強くても`Failure_Chain_Unconfirmed_Cache_Too_Full`のエラーがでていて、結局トランザクションを処理しきれないなどということがあるみたいです。
(後者はトランザクションが乗り切ればその後はうまいこと行くのだろうか？)

というわけで、何に一番苦労したかというと、
スループットがでなくてブロックに詰め込むのが間に合わないなど、
負荷をかける環境構築のほうが大変だったなというオチでした。
(JMeterには多少詳しくなれた)

JMeterからのリクエストがいまいち投げきれてない部分があったので、
テストのやりかたにボトルネックがあるっぽいです。
JMeterをもっとチューニングしてあげてください…。

まぁ、ネックだった事前トランザクション生成が秒または分程度で作り出せるようになったし、
JMeterのGUIからでもぶちかませるようになったので、こんなにたくさんノードを用意しなくとも、
またはローカル環境でもいいので、大量のトランザクションを投げ込むのを気軽に試してみてください。


# 追記

トランザクションを受け始めるとCPUが張り付いてLAが6くらいまであがるので
イマイチスループットが出きらないのはノード側のCPU力不足かもしれません…。

んが、今のアカウントではこれ以上いいインスタンスが借りられなかったので、
安定させるためにはもうちょっと性能の良いCPUが必要みたいです。

アリババクラウドでは6つまでしかインスタンスが持てませんでした。
多分申請すればその辺解除されていくんでしょうが、最初からもっと借りられたり、これよりも高性能なインスタンスがつかえるサービスでやったほうが手っ取り早そうです。