go-sqlite3を使ってCloud Spannerエミュレータをつくってみたmercari.go #13
View Slide
@kazegusuri● Merpay Backend Engineer● Architect Team2015/11 2017/01 2018/01SRE & Platform Platform Architect
トークの概要handy-spannerの説明をしながら01SQLiteの話をしてGoでどうやっているのか0302
Cloud Spanner● Google Cloud Platformが提供するフルマネージドデータベース○ スキーマ,トランザクション,強整合性,スケーラブル● Google Cloud Client Libraryを使ってアクセス○ https://github.com/googleapis/google-cloud-go● gRPCでAPIを提供○ gRPCの定義も公開されている○ https://github.com/googleapis/googleapis
handy-spanner● Cloud Spannerの(非公式)エミュレーター (Go)○ 現状では一番再現率が高いはず○ https://github.com/gcpug/handy-spanner○ https://speakerdeck.com/kazegusuri/handy-spanner-gcpug● SQLパーサーはmemefish (Go)○ Cloud SpannerのSQLを正確にパース可能○ https://github.com/MakeNowJust/memefish● ストレージはSQLite
SQLite● "SQLデータベースエンジンを実装したインプロセスライブラリ"○ Full-Featured SQL○ Extensionによる機能拡張● mattn/go-sqlite3○ https://github.com/mattn/go-sqlite3○ Goのsqlite3用driver○ database/sqlから操作可能
Cloud Spannerエミュレーター● gRPCのAPIを実装してCloud Spannerの挙動を再現する● Google Cloud Client Libraryからアクセス先を切り替えて利用する
AppGoogle CloudClient LibraryGoogle CloudSpannerhandy-spannerSQLitegRPC gRPC
Spannerの機能は大きく3つ● RPCによるデータの参照・更新○ e.g. StreamingRead, Mutation(Commit)○ SQLiteのRead/Write(クエリ)に変換● SQLによるデータの参照・更新○ e.g. ExecuteStreamingSql, ExecuteSql(DML)○ SQLiteのRead/Write(クエリ)に変換● トランザクション管理○ e.g. BeginTransaction, Commit, Rollback○ SQLiteのトランザクションとして管理
handy-spannerStreamingRead“SELECT FirstName, LastName FROM Singers WHEREFirstName = ‘foo’”SQLiteSQLite SQLReadRequestTable = SingersColumns = FirstName, LastNameKeySet = ‘foo’クエリ生成
handy-spannerExecuteStreamingSql“SELECT * FROM Singers WHERE FirstName = ‘foo’”SELECT* FROM WHERESingers =FirstName ‘foo’“SELECT * FROM Singers WHERE FirstName = ‘foo’”SQLiteSpanner SQLASTSQLite SQLクエリ生成クエリパース
何がSQLiteで難しいの?● SQLiteにないデータ型(配列, 構造体)● SQLiteにない操作の実現 (UNNESTなど)● Cloud Spannerの関数● トランザクション
配列と構造体の実装● SQLiteのJSON型を利用● 配列はJSON ARRAY○ e.g. [1, 2, 3], ["foo", "bar"]● 構造体はJSON OBJECT○ ただしフィールドに順序があるのでkey, valueを配列で扱う○ e.g. {"keys": ["x", "y"], "values": [1, 2]}
配列と構造体は組み合わせ可能● 配列の構造体の配列の構造体みたいなのが可能○ もちろん配列の要素型や構造体のフィールドは任意● 読み書き前に型の定義は正確にわかっている○ e.g. ARRAY>>● DataValue型 ⇔ JSONの相互変換を実現したい○ DataValueは型定義と値を保持したstruct[]struct {x int64y []string}type DataValue struct {Type DataTypeValue interface{}}type DataType struct {// INT64, STRING...}
JSONとの相互変換● json.Marshalerとjson.Unmarshalerを実装する○ DataValue型からJSONへの変換処理をMarshaler○ JSONからDataValue型への変換処理をUnmarshaler● 型の定義を見ながら再帰的に展開していくfunc (v *DataValue) MarshalJSON() ([]byte, error) {}func (v *DataValue) UnmarshalJSON([]byte) error {}
データベースから透過的に扱いたい● database/sql.Scannerとdatabase/sql/driver.Valuerを実装する○ Scannerはデータベースから値を取り出す(Scan)するとき○ Valuerはデータベースに値を書き込むとき● 実際の値(JSON)への変換はMarshal/Unmarshalにまかせるfunc (v *DataValue) Value() (driver.Value, error) {b, err := v.MarshalJSON()if err != nil { … }return driver.Value(string(b)), nil}func (v *DataValue) Scan(src interface{}) error {s, _ := src.(string)return v.UnmarshalJSON([]byte(s))}
関数● Cloud Spannerには大量の関数がある○ 集計関数, 数学関数, 文字列関数, 配列関数, タイムスタンプ関数...○ https://cloud.google.com/spanner/docs/functions-and-operators● 全ての関数がSQLiteに存在する訳ではない○ ABS() など基本的なものはある○ 配列関数や, 文字列関数などほとんどの関数はない
関数● リテラルやパラメータなどは入力値がわかるのでクエリ変換可能● データベースの値に対する操作はできることに制限がある…SELECT DATE_ADD(DATE "2008-12-25",INTERVAL 5 DAY);SELECT DATE "2008-12-30";クエリ変換SELECT DATE_ADD(@date,INTERVAL 5 DAY);@date = "2008-12-25"SELECT DATE "2008-12-30";クエリ変換SELECT DATE_ADD(tbl.created_at,INTERVAL 5 DAY);クエリ変換Spanner SQLSQLite SQL
SQLiteのデータを操作したい● SQLite上のデータはアプリケーションから直接操作できない○ 出力時にScanner, 入力時にValuerを通すことで加工はできる○ JSONのようにExtensionによりSQLite側に直接ロジックも追加可能ExtensionSQLitehandy-spannerJSONdriverScanner Valuer
SQLiteのカスタム関数● SQLiteのクエリからGoの関数が呼び出せる○ 任意の値を受け取って、任意の値を返す関数ExtensionSQLitehandy-spannerJSONdriverScanner Valuer Function
カスタム関数の実装● Goの関数の引数と戻り値がカスタム関数と一致○ interface{}で任意の型も可能○ 可変長引数も可能● エラーも返せるSIGN() 関数の実装SELECT SIGN(3) // => 1SELECT SIGN(-2) // => -1
EXTRACT() 関数の実装カスタム関数の実装
カスタム関数の登録● SQLiteドライバーを再登録する○ github.com/mattn/go-sqlite3.SQLiteConnのRegisterFuncで関数を登録● database/sql.Openで登録したドライバを指定
カスタム関数を使えば● DATE_ADDの機能をX_DATE_ADDと登録した場合SELECT DATE_ADD(DATE "2008-12-25",INTERVAL 5 DAY);クエリ変換SELECT DATE_ADD(@date,INTERVAL 5 DAY);@date = "2008-12-25"クエリ変換SELECT DATE_ADD(tbl.created_at,INTERVAL 5 DAY);クエリ変換Spanner SQLSQLite SQLSELECT X_DATE_ADD(DATE "2008-12-25",INTERVAL 5 DAY);SELECT X_DATE_ADD(@date,INTERVAL 5 DAY);@date = "2008-12-25"SELECT X_DATE_ADD(tbl.created_at,INTERVAL 5 DAY);
カスタム関数を使えば● 仮想的にデータ型も定義可能なはず!● 例えば、位置情報型(Geometry)○ SQLiteには"(100, 200)"のような文字列で保存○ 比較(=)などのオペレーターは関数に置き換えるSELECT tbl.pos1 = tbl.pos2クエリ変換SELECT GEO_EQUAL(tbl.pos1, tbl.pos2)
カスタム関数を使う場合の注意● rows.Err()のチェックは必須○ カスタム関数でエラーが発生時にScanやクエリはエラーにならない
時間があればトランザクションの難しさを…● SQLiteはスキーマレベル Lock○ 誰かがロックを取るの他の全員がロック待ちになる● Shared Cache Modeにするとテーブルレベル Lock○ 誰かがテーブルを読み込むとRead Lock○ 誰かがテーブルを書き込むとWrite Lock○ Read Lock中はReadはできるがWriteはできない○ Write中は他の誰もReadできない● ずっとReadしているやつがいるといつまでもWriteできない…!
handy-spannerのトランザクション管理● SQLiteのトランザクションがどのテーブルをどうロックしているか全部管理● トランザクションの優先度をつける○ 必要に応じて任意のトランザクションを終了させる○ 通常ならデッドロックするケースでも動くようになる
まとめ● エミュレーションは面白い○ エミュレーション対象のことがわかる● SQLiteのハックは面白い○ Goの拡張だけでなくC拡張も使えばもっとできることは広がる● 開発デバッグ目的だけじゃなくプロダクション用途にも発展可能○ S3互換ストレージのような○ CQL(Cassandra)とかJQL(Jira) みたいなクエリベース○ Redisみたいなコマンドベース