Slide 1

Slide 1 text

go-sqlite3を使って Cloud Spannerエミュレータを つくってみた mercari.go #13

Slide 2

Slide 2 text

@kazegusuri ● Merpay Backend Engineer ● Architect Team 2015/11 2017/01 2018/01 SRE & Platform Platform Architect

Slide 3

Slide 3 text

トークの概要 handy-spannerの説明をしながら 01 SQLiteの話をして Goでどうやっているのか 03 02

Slide 4

Slide 4 text

Cloud Spanner ● Google Cloud Platformが提供するフルマネージドデータベース ○ スキーマ,トランザクション,強整合性,スケーラブル ● Google Cloud Client Libraryを使ってアクセス ○ https://github.com/googleapis/google-cloud-go ● gRPCでAPIを提供 ○ gRPCの定義も公開されている ○ https://github.com/googleapis/googleapis

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

SQLite ● "SQLデータベースエンジンを実装したインプロセスライブラリ" ○ Full-Featured SQL ○ Extensionによる機能拡張 ● mattn/go-sqlite3 ○ https://github.com/mattn/go-sqlite3 ○ Goのsqlite3用driver ○ database/sqlから操作可能

Slide 7

Slide 7 text

Cloud Spannerエミュレーター ● gRPCのAPIを実装してCloud Spannerの挙動を再現する ● Google Cloud Client Libraryからアクセス先を切り替えて利用する

Slide 8

Slide 8 text

App Google Cloud Client Library Google Cloud Spanner handy-spanner SQLite gRPC gRPC

Slide 9

Slide 9 text

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のトランザクションとして管理

Slide 10

Slide 10 text

handy-spanner StreamingRead “SELECT FirstName, LastName FROM Singers WHERE FirstName = ‘foo’” SQLite SQLite SQL ReadRequest Table = Singers Columns = FirstName, LastName KeySet = ‘foo’ クエリ生成

Slide 11

Slide 11 text

handy-spanner ExecuteStreamingSql “SELECT * FROM Singers WHERE FirstName = ‘foo’” SELECT * FROM WHERE Singers = FirstName ‘foo’ “SELECT * FROM Singers WHERE FirstName = ‘foo’” SQLite Spanner SQL AST SQLite SQL クエリ生成 クエリパース

Slide 12

Slide 12 text

何がSQLiteで難しいの? ● SQLiteにないデータ型(配列, 構造体) ● SQLiteにない操作の実現 (UNNESTなど) ● Cloud Spannerの関数 ● トランザクション

Slide 13

Slide 13 text

配列と構造体の実装 ● SQLiteのJSON型を利用 ● 配列はJSON ARRAY ○ e.g. [1, 2, 3], ["foo", "bar"] ● 構造体はJSON OBJECT ○ ただしフィールドに順序があるのでkey, valueを配列で扱う ○ e.g. {"keys": ["x", "y"], "values": [1, 2]}

Slide 14

Slide 14 text

配列と構造体は組み合わせ可能 ● 配列の構造体の配列の構造体みたいなのが可能 ○ もちろん配列の要素型や構造体のフィールドは任意 ● 読み書き前に型の定義は正確にわかっている ○ e.g. ARRAY>> ● DataValue型 ⇔ JSONの相互変換を実現したい ○ DataValueは型定義と値を保持したstruct []struct { x int64 y []string } type DataValue struct { Type DataType Value interface{} } type DataType struct { // INT64, STRING... }

Slide 15

Slide 15 text

JSONとの相互変換 ● json.Marshalerとjson.Unmarshalerを実装する ○ DataValue型からJSONへの変換処理をMarshaler ○ JSONからDataValue型への変換処理をUnmarshaler ● 型の定義を見ながら再帰的に展開していく func (v *DataValue) MarshalJSON() ([]byte, error) { } func (v *DataValue) UnmarshalJSON([]byte) error { }

Slide 16

Slide 16 text

データベースから透過的に扱いたい ● 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)) }

Slide 17

Slide 17 text

関数 ● Cloud Spannerには大量の関数がある ○ 集計関数, 数学関数, 文字列関数, 配列関数, タイムスタンプ関数... ○ https://cloud.google.com/spanner/docs/functions-and-operators ● 全ての関数がSQLiteに存在する訳ではない ○ ABS() など基本的なものはある ○ 配列関数や, 文字列関数などほとんどの関数はない

Slide 18

Slide 18 text

関数 ● リテラルやパラメータなどは入力値がわかるのでクエリ変換可能 ● データベースの値に対する操作はできることに制限がある… 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 SQL SQLite SQL

Slide 19

Slide 19 text

SQLiteのデータを操作したい ● SQLite上のデータはアプリケーションから直接操作できない ○ 出力時にScanner, 入力時にValuerを通すことで加工はできる ○ JSONのようにExtensionによりSQLite側に直接ロジックも追加可能 Extension SQLite handy-spanner JSON driver Scanner Valuer

Slide 20

Slide 20 text

SQLiteのカスタム関数 ● SQLiteのクエリからGoの関数が呼び出せる ○ 任意の値を受け取って、任意の値を返す関数 Extension SQLite handy-spanner JSON driver Scanner Valuer Function

Slide 21

Slide 21 text

カスタム関数の実装 ● Goの関数の引数と戻り値がカスタム関数と一致 ○ interface{}で任意の型も可能 ○ 可変長引数も可能 ● エラーも返せる SIGN() 関数の実装 SELECT SIGN(3) // => 1 SELECT SIGN(-2) // => -1

Slide 22

Slide 22 text

EXTRACT() 関数の実装 カスタム関数の実装

Slide 23

Slide 23 text

カスタム関数の登録 ● SQLiteドライバーを再登録する ○ github.com/mattn/go-sqlite3.SQLiteConnのRegisterFuncで関数を登録 ● database/sql.Openで登録したドライバを指定

Slide 24

Slide 24 text

カスタム関数を使えば ● 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 SQL SQLite SQL SELECT 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);

Slide 25

Slide 25 text

カスタム関数を使えば ● 仮想的にデータ型も定義可能なはず! ● 例えば、位置情報型(Geometry) ○ SQLiteには"(100, 200)"のような文字列で保存 ○ 比較(=)などのオペレーターは関数に置き換える SELECT tbl.pos1 = tbl.pos2 クエリ変換 SELECT GEO_EQUAL(tbl.pos1, tbl.pos2)

Slide 26

Slide 26 text

カスタム関数を使う場合の注意 ● rows.Err()のチェックは必須 ○ カスタム関数でエラーが発生時にScanやク エリはエラーにならない

Slide 27

Slide 27 text

時間があればトランザクションの難しさを… ● SQLiteはスキーマレベル Lock ○ 誰かがロックを取るの他の全員がロック待ちになる ● Shared Cache Modeにするとテーブルレベル Lock ○ 誰かがテーブルを読み込むとRead Lock ○ 誰かがテーブルを書き込むとWrite Lock ○ Read Lock中はReadはできるがWriteはできない ○ Write中は他の誰もReadできない ● ずっとReadしているやつがいるといつまでもWriteできない…!

Slide 28

Slide 28 text

handy-spannerのトランザクション管理 ● SQLiteのトランザクションがどのテーブルをどうロックしているか全部管理 ● トランザクションの優先度をつける ○ 必要に応じて任意のトランザクションを終了させる ○ 通常ならデッドロックするケースでも動くようになる

Slide 29

Slide 29 text

まとめ ● エミュレーションは面白い ○ エミュレーション対象のことがわかる ● SQLiteのハックは面白い ○ Goの拡張だけでなくC拡張も使えばもっとできることは広がる ● 開発デバッグ目的だけじゃなくプロダクション用途にも発展可能 ○ S3互換ストレージのような ○ CQL(Cassandra)とかJQL(Jira) みたいなクエリベース ○ Redisみたいなコマンドベース