ブロックチェーン・アプリケーションの初期データ登録
**この記事の需要があるのかは分からない…**備忘録的に残しておく。
ブロックチェーンでchaincodeを書き、いざ初期データを登録しようとするとchaincode経由でしか登録できないことに気がついた。登録するデータが少量なら問題無いが、数万~数百万もデータがあると1件ずつ登録するには時間がかかりすぎてしまう。
当初はPostgreSQLと連携して登録するコードをGOで書いたが、ローカルだとビルドできるがchaincodeのインストール時ビルドが通らずコケてしまう。相性が悪いようだ。
そこで1つの文字列引数に可能な限りのデータを詰め込むことを考えた。つまり、
CSVファイル→zlib(compress)で圧縮→BASE64で文字列化
を行うツールで入力引数を作成し、chaincodeで逆を行えば良い。
このロジックでは、zlibで約1/5に圧縮されBASE64で約10%ほどサイズが増えるようだ。
エンコードツールのコード
package main
import (
b64 "encoding/base64"
"fmt"
"flag"
"bytes"
"os"
"io/ioutil"
"compress/zlib"
)
func main() {
// コマンドライン引数をパース
_ = flag.Int("flag1", 0, "flag 1")
flag.Parse()
// CSVファイルのオープン
f, err := os.Open(flag.Args()[0])
if err != nil {
fmt.Println("os.Open:error")
}
defer f.Close()
// CSVファイルを読み込みzlibで圧縮
b, err := ioutil.ReadAll(f)
var sbuff bytes.Buffer
zlibw := zlib.NewWriter(&sbuff)
zlibw.Write(b)
zlibw.Close()
// zlibで圧縮したデータをBASE64で文字列化
sEnc := b64.StdEncoding.EncodeToString(sbuff.Bytes())
// 標準出力へ出力
fmt.Println(sEnc)
}
使い方は、CSVファイルを引数に呼ぶだけ。結果は標準出力へ。
ls -l data.csv
-rw-r--r-- 1 root root 405488 12月 27 22:02 data.csv #元データ
./encode data.csv > data.b64z
ls -l data.b64z
-rw-r--r-- 1 root root 99525 2月 7 10:59 data.b64z #zlib圧縮+BASE64
chaincodeでデコード
func (s *SmartContract) createAssetForB64Z(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
fmt.Println("createAssetForB64Z:START")
// 入力引数チェック
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
// 入力引数の文字列をBASE64デコード
argAsBytes, err := b64.StdEncoding.DecodeString(args[0])
if err != nil {
return shim.Error("b64.StdEncoding.DecodeString:error")
}
// BASE64デコードしたバイト列をzlibでデコード
var sbuff bytes.Buffer
sbuff.Write(argAsBytes)
var dbuff bytes.Buffer
zlibr , err := zlib.NewReader(&sbuff)
if err != nil {
return shim.Error("zlib.NewWriter:error")
}
io.Copy(&dbuff, zlibr)
zlibr.Close()
// CSVデータとして取り出す
recs := csv.NewReader(strings.NewReader(string(dbuff.Bytes())))
var asset Asset
for {
rec, err := recs.Read()
if err == io.EOF {
break
}
if err != nil {
fmt.Println("csv.NewReader:error")
break
}
asset.Year = rec[1]
asset.Month = rec[2]
asset.Mileage, _ = strconv.Atoi(rec[3])
asset.Battery, _ = strconv.Atoi(rec[4])
assetAsBytes, _ := json.Marshal(asset)
// ステートDBへPUT
APIstub.PutState(rec[0], assetAsBytes)
}
fmt.Println("createAssetForB64Z:END")
return shim.Success(nil)
}
入力引数として可能なサイズとは
xargs --show-limits
で表示できる。
xargs --show-limits
Your environment variables take up 583 bytes
POSIX upper limit on argument length (this system): 2094521
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2093938
Size of command buffer we are actually using: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647
2,093,938バイトがMAXのように書いてあるが、実際には131,072バイトまでのようだ。
また、構築している環境が非力な場合※はchaincode処理でタイムアウトが発生するので、split -l
コマンド等でCSVファイルを分割し調整する必要がある。AWS等のパブリッククラウドを使いXeon2CPU、4GBメモリなら131,072バイトを超えなければ問題なく実行できる。
※自宅オンプレ:Pentium+Atom混在の非力なKubernetes構成みたいな
実際に呼び出してみる(cliを利用)
- Docker ComposeでHyperledger Fabricを構築している場合
# !/bin/bash
files="data/*.b64z"
for file in $files; do
echo $file
read line < $file
docker exec cli peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n asset -c '{"Args":["createAssetForB64Z","'${line}'"]}'
done
- KubernetesでHyperledger Fabricを構築している場合
# !/bin/bash
files="data/*.b64z"
for file in $files; do
echo $file
read line < $file
kubectl exec -i cli -- bash -c "peer chaincode invoke -o orderer-example-com:30050 -C mychannel -n asset -c '{\"Args\":[\"createAssetForB64Z\",\"${line}\"]}'"
done
実際の効果はどうだったの?
1万件のデータを1件毎にcliバッチを使ってcreateAssetすると次の日になりそうな勢いだったのがこの方法だと、非力オンプレ・AWSの両方で数十秒のオーダーで完了した(両環境共にKubernetes上にHyperledger Fabricを構築している)。
やりたいことが実現できて満足だ。