Gethによるプライベートイーサリアムブロックチェーンを断念した話

## はじめに

ERC20等のトークンではなく、実際に意味のあるデータ（例えば企業間の取引の証左とか）を連動させるという実証を行った。
最初に気になったのはやはり

- **手数料問題をどう扱うか？**
- **秒間１２～１５程度のスループットで実用できるのか？**

だったが、実証なのに手数料を払ってパブリックネットワークにブロックを積むのも微妙だし
テストネットワークを利用するところでやはりスケーラビリティ問題は気になる。

なので、（コンソーシアム想定の）プライベートチェーンで試すことに決めた。
また、プライベートではマイニングコスト自体が余計だし、スケーラビリティを確保するためにもPoWは実質無効化させた（後述）。
PoWを外したイーサリアムに何の意味があるの？ という向きもあろうが、以下の流れを想定していたので。

1. **今後のバージョンアップでCasperやシャーディングが適用されればスケーラビリティ問題は解決に向かい、ついでにGasFeeもある程度抑えられると予測（期待）**

2. **でも、今すぐ準備は進めたいし、知見も手に入れておきたい**

4. **なら、PoWを外したイーサリアムをプライベートで動作させて準備しておけば良い**

5. **実証で用意し利用した（スマートコントラクトコード、トランザクションデータの設計、クライアントプログラム等）は、本家がバージョンアップした際にはそのまま使える（改めてコード登録し、データを連携しなおす必要はあるが）。**

### gethを改造

入れポンである程度動き、情報が豊富なGo Ethereum（geth）を利用することにした。
今回はソースコードを改修し、以下の部分に手を加えた。

- **MiningのDifficultyを"1"に固定**
    - PoAに変更するだけでよかったのだが、当時は単純にそこに気付かずDifficultyを1で返すように変更することでPoWを実質外した
- **ブロック生成時の高負荷対策として１ブロックのTX数の上限を100TXに設定**
    - 挙動的にはTransaction Pool から 必ず100TX単位でTXを取り出してブロック生成を行うように変更
    - 但し、Transaction Pool内に 100TX未満しかない状態が一定時間続いた場合は強制的にブロック生成を行う

### その他環境に関して

大体、以下のような感じ
典型的なトランザクションであるeth送金の150倍程度のデータを詰めていることになる。
**まあ、これが致命的な問題を引き起こすわけになるのだが。**

| 環境 | 設定値 | 備考 |
|:---:|:---:|:---:|
| ノード数 | 2 |  |
| Syncモード  | Full | 可用性を考慮 |
| Cache | 4096 | キャッシュ(MB) |
| Cache.Database | 75% | 規定値。CacheのうちLevelDBが使う割合 |
| gas量/1TX | 3,000,000gas | eth送金の約150倍 |
| データサイズ/1TX | 約4KB | 厳密にはInputData部分の容量 |
| GasLimit | - | 上記InputDataを含むTXを受け入れられる値に変更済 |
| OS | CentOS 7.4.1 と Redhat Linux 7.6 | 両方試した |

ノードのスペックは以下の通り。

|項目|値|
|:---:|:---:|
| CPU | 8core Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz |
| Mem | 32GB |
| ストレージ | HDD（AWSの汎用SSD (gp2)と同程度のIO性能） |

## 結果として深刻な性能問題に遭遇した

最初は良好なパフォーマンスだったがデータ量が増えるにつれて劇的に性能が悪化した。
以下はざっくりとした目安となる。

| 積んだブロック数 | 積んだトランザクション数 | その状態での秒間TXスループット |
|:---:|:---:|:---:|
| 100 | 10,000 | 100TX/1sec |
| 1000 | 100,000 | 30TX/1sec |
| 2000 | 200,000 | 7TX/1sec |
| 3000 | 300,000 | 1.5TX/1sec |
| 3500 | 350,000 | 0.7TX/1sec |

※ なお、実際の投入時には問題を切り分けるため、TXを投入するクライアントプログラムにて
Transaction Poolには 1000TX以上は溜まらないように調整して負荷をかけた。

2000ブロックあたりからは、そもそもパブリックのイーサリアム（秒間 12~15/txといったところだろうか）より遅い。

## 性能問題が発生した原因は突き止めたが・・・

#### gethの仕組み上の問題
gethが内部で利用しているLevelDBの（検索ではなく登録の）性能劣化が著しい。
データが少ないうちは登録も高速だが、データが増えると指数関数的に遅くなる。
なお、LevelDB単体はキーがソートされるという特性を踏まえて利用すれば、大量データでも必ずしも遅くならないだろう。

#### TXあたりのデータ量（≒gas量）が多すぎる
InputDataにJson形式で情報を詰めているのだが、それが4KBくらいある。消費されるgas量はeth送金の150倍必要だ。
ここを削れという話なのだが、取引でこれだけは必要という属性情報を考えると大きく削るのは結構難しい。

#### 確認してみたらある意味当然だったのかも
[Ethereum Blog](https://blog.ethereum.org/2019/07/10/geth-v1-9-0/)にあるとおりgethではFullモードで同期すると
今回の実証よりだいぶ高いスペックのノード（CPUコア性能が高い、メモリも倍、SSDもおそらくより高速）で１週間弱かかっている。

容量をみると本家のデータサイズは340GBだ。実はこの検証のプライベートイーサリアムでは、
性能測定をやめた（50万TX程度を登録した）段階でデータサイズはざっくり200GBくらいだった。
そして、そこに至るまで大体2週間くらいかかった。

データサイズとノードのスペックの差を鑑みるとまず、
Ethereum Blogで公表されているものと比較してもそこまで違和感ない当然の結果ということになる。

そして、その上でざっくり計算すると以下となる。

**50万TX/2週間 ≒ 4TX/sec**

実際は最初は秒間100TX以上出ていて、最後は秒間1TXもおぼつかなくなっているので単なる平均で
この後更に10万20万TXを積んでいけば平均は更に下がっていくだろう。

### 色々試したみたが・・・・

結局ダメだった。

##### ◆ トランザクション処理非同期化
パラメータを指定しても何故か必ず1コアのみ使用されていたので並列化してみた。
並列化することでスループットは10倍程度になったが、やはり急改造では伝搬エラーも発生した。
Gethのトランザクション処理は直列実行である事を想定した処理になっているためだと思われる。

##### ◆ LevelDBのファイル数削減
leveldbではファイルが2MBごとに分割されるため、データ量が増えるとファイル数が数万となり、
Linux上ではボトルネックになり得ると考え、ソースを修正して200MBごとにファイルを分割するように修正した。

が、逆にパフォーマンスが悪化した。
恐らくLevelDBは2MBごとにファイル分割するのが最適であるように実装されているのだろう。

##### ◆ 起動時のキャッシュオプションを有効化
最初は冒頭の環境どおり（--cache 4096）の値ではなかったのだが、キャッシュを増やして動作も確認してみた。

ブロック数が少ないうちはパフォーマンスの向上が多少見られたが
ブロック数が多くなると、パフォーマンス劣化する現象は変わらなかった。

## 考えてみれば・・・

そもそも、マイニングでほとんどの時間（処理能力）を取られるイーサリアムでは
本来のPoWで動作している限りは専用ノードで秒間数十トランザクションを裁ければ現状の仕組みとしては十分なはずだ。

実証では採取的に0.7TX/sec程度まで性能が劣化しているが、TXを裁く処理がTXのデータサイズに比例して遅くなるのであれば
トランザクションの殆どを占めるeth送金のgas量が1/150なので、単純計算でもeth送金なら100TX/sec以上は捌けていることになる。

コンセンサスアルゴリズムがCasperになり、スマートコントラクトがさらに活用されてある程度大きなTXを受け入れる段階になって
始めてネックとなる実装であり、その時にはgethもきっと何かしら対応されてくるのだろう。

今回は残念な結果になったが得た知見も多かった。設計も見直すが、タイミングをみて再度トライしようと思う。
そして、今は現在はEOSで同じようなデータを投入して問題ないかを実証している。

## まとめ

##### 1TXに詰め込むデータ量（≒gas量）を極力抑える
例えば、契約や証左としての役割を持たせたいなら、
原本情報ではなく原本のデータのハッシュなどに絞って登録させるのが良いのだろう。

##### 連続運用テストは重要
今回のケースだと、ブロック（投入済TX）数が少ないうちはパフォーマンスは良好だった。
PoAへの変更を行わなくても、プライベートではDifficultyもほぼ一定に当面は保たれるので
当初は安定したパフォーマンスが出るケースも多いと思われる。

実証段階や初期リリース時にはサクサク動作しても、
本格動作や利用者の増大と時間に伴って深刻な影響が出る可能性は多いにある。

ブロックチェーンに関わらずだが、連続運用テストは重要だな、と改めて痛感した。

##### ちなみに
HyperLedger Fabricでは同じような問題には遭遇せず、パフォーマンスは安定している。
プライベート・コンソーシアムチェーンならこちらも選択肢には入ると思う。
ただ、そもそも性質の違うブロックチェーンを単純に今回の事象だけで比較するのには難があるし
今回はイーサリアムの問題というより、特殊なユースケースやこちらの使い方の問題の可能性が高く、一概にFabricが良いと言うつもりはない。

Fabricも内部ではLevelDBを選択可能で、それを選択しているのだが
恐らく全部をLevelDBに入れるgethと、あくまでワールドステートのみをLevelDBに保持するFabricの差かな、と。