InitLedgerで既存のRDBからState DBの初期化を実施します
Hyperledger Fabric(以下HF)のDB初期化には色んな方法があると思います。ちなみに、サンプルプログラムで有名なFabcarでは、10レコード分のデータをハードコーディングでState DBへ登録しています:-(
ここでは、レガシーなRDBであるPostgreSQLからデータを読み出し、State DBへ登録する方法を紹介します。
#環境について
動作環境については次の通りです。
- Ubuntu 18.04.4 LTS
- docker-compose 1.26.0
- docker 19.03.11
- HF 2.1.1
- go 1.14.4
- PostgreSQL(Dockerイメージ) 12.3(latest)
#データ構造を考える
これから自動運転の社会になってくると、車は資産として自動車メーカーが管理するのではないかと予想しています。車自体のテクノロジーが高度になりすぎて町工場では対応できないでしょう…
ということで、車の状態を最低限管理できる項目を並べたデータ構造とします。サンプルだし。
type Asset struct {
Year string `json:"year"` // 初度登録年
Month string `json:"month"` // 初度登録月
Mileage int `json:"mileage"` // 走行距離(km)
Battery int `json:"battery"` // バッテリーライフ(%)
}
type QueryResult struct {
Key string `json:"Key"` // 車台番号
Record *Asset
}
バッテリーライフは充電状態ではなくバッテリー自体の劣化状態を意味しています。
(初年度登録年・月は製造年・月の方がより良いかもしれません)
下準備
下準備をします。PostgreSQLのテーブル定義は端折るのでGoogle先生に聞いてください;-)
ダミーデータの作成
何はなくともダミーデータ。
1万台分のデータをPostgreSQLに準備します。ダミーデータをでっち上げるgoプログラムを書いてPostgreSQLのDBへ流し込みました。
- 1万台分のデータをCSVファイルに出力(goプログラム)
- PostgreSQLのコンテナへCSVファイルをコピー(/tmpなど)
- PostgreSQLのコンテナへ入り、
\copy
コマンドでインポート
もちろん、予めPostgreSQLのDBへテーブルを定義しておく必要があります。
CSVファイルの内容は次の通り。
Key | 初年度登録年 | 初年度登録月 | 走行距離 | バッテリーライフ |
---|---|---|---|---|
"JXT4EGSP0AX100001" | "2010" | "04" | 33880 | 78 |
"JXT5EGSP0AX100002" | "2010" | "02" | 6360 | 96 |
"JXT4EGSP0AX100003" | "2010" | "09" | 41040 | 73 |
: | : | : | : | : |
このKey値は、VIN(Vehicle Identification Number)コードと呼ばれる17桁の文字列です。上記はダミーデータなのでもちろん架空の値です。その他の値もrand
を使って生成しています。初年度登録年はVINと関係してくるのでそれなりのロジックで生成しました。走行距離もそれっぽく。
chaincodeでPostgreSQLを使えるようにする
goのchaincodeモジュールにPostgreSQLを追加します。
いつからかgoはgo.modというファイルでモジュールを管理しているようです。周回遅れ過ぎです。
当初、ローカルな$GOHOMEでchaincodeを書いてビルドできていたのに、HF上へデプロイするとできないのでハマりました。ローカルな環境ではgo get github.com/lib/pq
をしていたのでビルドできていたのです。
HFのchaincode置き場でも同様にgo get github.com/lib/pq
する必要がありました。asset
という名前でchaincodeを作ったので、ディレクトリfabric-samples/chaincode/asset/go
を作ってfabcarからgo.mod
とgo.sum
とvendor
をコピーしてきます。その際、go.modのmodule
をasset
へ変更します。
# pwd
/root/fabric/fabric-samples/chaincode/asset/go
# ls
asset.go go.mod go.sum vendor
# go get github.com/lib/pq
これでgo.modとgo.sumが自動的に更新されます。
module asset
go 1.13
require (
github.com/hyperledger/fabric-contract-api-go v1.1.0
github.com/lib/pq v1.7.0
)
go.modにgithub.com/lib/pq v1.7.0
が追加されました。
PostgreSQLのdocker-composeファイルを作成する
HFのpeerからPostgreSQLのコンテナへアクセスできるようにするcomposeファイルを定義します。
docker-compose-pgsql.yaml
としてfabric-samples/test-network/docker
ディレクトリへ置きます。
定義内容は次の通り。
- HFと同じネットワークで稼働させる
- コンテナ名を定義する(これがホスト名になります)
version: '2'
networks:
test:
services:
pgsql:
image: postgres:latest
environment:
POSTGRES_PASSWORD: secret
volumes:
- /tank/pgsql-data:/var/lib/postgresql/data
ports:
- 5432:5432
container_name: pgsql
networks:
- test
test
というネットワークにpgsql
という名前でデプロイします。
察しの良い方は気づくかもしれませんが、DBはZFS上に永続化しています。
###fabcarの環境をassetの環境へ書き換える
今回はサンプルコードのfabcar環境を流用しています。
書き換えるファイルは次の2つです。
- fabric-samples/test-network/network.sh
- fabric-samples/test-network/scripts/deployCC.sh
network.sh
はfabcar
をasset
へ文字列置換して、PostgreSQL
のupとdownの記述を追加します。
deployCC.sh
は文字列置換のみでOKです。chaincodeのFunction名も必要に応じて変更します※。
※queryAllCars
-> QueryAllAssets
みたいな
# diff network.sh network.sh.org
27c27
< echo " - 'deployCC' - deploy the asset chaincode on the channel"
---
> echo " - 'deployCC' - deploy the fabcar chaincode on the channel"
244,250d243
< echo "############################"
< echo "##### Start PostgreSQL #####"
< echo "############################"
<
< IMAGE_TAG=${PGSQL_IMAGETAG} docker-compose -f $COMPOSE_FILE_PGSQL up -d 2>&1
<
< echo
407d399
< docker-compose -f $COMPOSE_FILE_PGSQL down --volumes --remove-orphans
425c417
< rm -rf channel-artifacts log.txt asset.tar.gz asset
---
> rm -rf channel-artifacts log.txt fabcar.tar.gz fabcar
452,453d443
< # docker-compose.yaml file if you are using postgresql
< COMPOSE_FILE_PGSQL=docker/docker-compose-pgsql.yaml
作業ディレクトリは、fabric-samples/fabcar
をfabric-samples/asset
でコピーします。
中のシェルはchaincodeに依存していないので、そのまま使えます。
- startFabric.sh:HFを開始するシェルです。HFコンテナ群のデプロイからchaincodeのデプロイ、初期化・全件クエリまで自動で実施します。
- networkDown.sh:全てを掃除してなかったことにしてくれます。
fabcar
ディレクトリには他にもディレクトリがありますが、不要なので削除しても構いません。
InitLedgerの実装
先ずは必要なライブラリをインポートします。
import (
"bytes"
"encoding/json"
:
"database/sql"
_ "github.com/lib/pq"
)
"database/sql"
と_ "github.com/lib/pq"
を追加しています。
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
fmt.Println(time.Now())
db, err := sql.Open("postgres", "host=pgsql port=5432 user=postgres password=secret dbname=asset sslmode=disable")
defer db.Close()
if err != nil {
return fmt.Errorf("sql.Open: %s", err.Error())
}
rows, err := db.Query("SELECT * FROM asset;")
if err != nil {
return fmt.Errorf("sql.Query: %s", err.Error())
}
var id string
for rows.Next() {
var asset Asset
rows.Scan(&id, &asset.Year, &asset.Month, &asset.Mileage, &asset.Battery)
assetAsBytes, _ := json.Marshal(asset)
ctx.GetStub().PutState(id, assetAsBytes)
}
fmt.Println(time.Now())
return nil
}
恐ろしくシンプルなコードです。ホスト名・ユーザ名・パスワード、諸々がベタ打ちなのはサンプルなのでご愛嬌。goがわからなくても読むとなんとなく理解できると思います。
- DBをオープン
- 全レコードのクエリ(
"SELECT * FROM asset;"
) - 1レコードずつ読み取って(
rows.Next()
)、カラムに分解(rows.Scan
) -
rows.Scan
と同時にAsset
構造体へ設定 -
json.Marshal
してState DBへPutState
InitLedgerを実行
1万台分のデータの登録にどのくらいの時間がかかるのでしょう?
実行環境は4C/4TのPentiumにメモリ16GBを積んだマイクロサーバー(LIVA Z)です。
InitLedgerにtime.Now()
をプリントするコードを仕込んであるのでpeerのログを覗いてみます。
# docker logs 3575482a43e3
2020-06-20 10:05:34.977106747 +0000 UTC m=+5.210858066
2020-06-20 10:05:42.026225952 +0000 UTC m=+12.259977077
logsに続く文字列はpeerのコンテナIDです。
State DBへの登録に8秒かかっていますね。1万件なら早いのではないでしょうか。HW環境的にも。
全件クエリにはどのくらいかかるでしょうか。
# time ./QueryAllAssets.sh
途中省略
"Record":{"year":"2019","month":"09","mileage":2565,"battery":99}},{"Key":"JXT5EGSP0KX100979","Record":{"year":"2019","month":"01","mileage":353,"battery":100}},{"Key":"JXT5EGSP0KX100980","Record":{"year":"2019","month":"10","mileage":7060,"battery":96}},{"Key":"JXT5EGSP0KX100982","Record":{"year":"2019","month":"02","mileage":1806,"battery":99}},{"Key":"JXT5EGSP0KX100984","Record":{"year":"2019","month":"10","mileage":9240,"battery":94}},{"Key":"JXT5EGSP0KX100985","Record":{"year":"2019","month":"03","mileage":36,"battery":100}},{"Key":"JXT5EGSP0KX100991","Record":{"year":"2019","month":"04","mileage":1160,"battery":100}},{"Key":"JXT5EGSP0KX100992","Record":{"year":"2019","month":"09","mileage":6219,"battery":96}},{"Key":"JXT5EGSP0KX100995","Record":{"year":"2019","month":"08","mileage":3792,"battery":98}},{"Key":"JXT5EGSP0KX100996","Record":{"year":"2019","month":"11","mileage":10868,"battery":93}},{"Key":"JXT5EGSP0KX100998","Record":{"year":"2019","month":"09","mileage":5967,"battery":97}}]
real 0m8.896s
user 0m0.175s
sys 0m0.051s
画面出力にほとんどの時間を使っています。処理的にはアッと言う間ですね。
比較のために、peer
コマンドを使ってシェル内ループで1万件を登録してみました。
# time ./CreateAllAssets.sh
途中省略
2020-06-21 12:33:00.825 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:00.951 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:01.072 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:01.195 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:01.319 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
real 20m44.886s
user 17m9.930s
sys 3m46.809s
#
差は歴然でした。
最後に
HFとレガシーRDBとの連携情報があまりなかったので書いてみました。RDBからのReadができればWriteもできそうです。個人情報などはRBDへ保存して都度HFと連携すると良いと思います。また、データ構造にLocation情報も追加して、ブロックチェーンの特徴でもある取引履歴を応用したトレーサビリティ機能が実装できそうです。
RBDとの連携は興味深いのでまた投稿しようと思います。
みなさんのお役に立てたら幸いです。