Upgrade to Pro — share decks privately, control downloads, hide ads and more …

go-sqlite3を使ってCloud Spannerエミュレーターを作ってみた / Cloud Spanner emulator with go-sqlite3

59c0cc69a8ad4ca8d26d752b3b795b55?s=47 kazegusuri
February 10, 2020

go-sqlite3を使ってCloud Spannerエミュレーターを作ってみた / Cloud Spanner emulator with go-sqlite3

59c0cc69a8ad4ca8d26d752b3b795b55?s=128

kazegusuri

February 10, 2020
Tweet

Transcript

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

  2. @kazegusuri • Merpay Backend Engineer • Architect Team 2015/11 2017/01

    2018/01 SRE & Platform Platform Architect
  3. トークの概要 handy-spannerの説明をしながら 01 SQLiteの話をして Goでどうやっているのか 03 02

  4. Cloud Spanner • Google Cloud Platformが提供するフルマネージドデータベース ◦ スキーマ,トランザクション,強整合性,スケーラブル • Google

    Cloud Client Libraryを使ってアクセス ◦ https://github.com/googleapis/google-cloud-go • gRPCでAPIを提供 ◦ gRPCの定義も公開されている ◦ https://github.com/googleapis/googleapis
  5. 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
  6. SQLite • "SQLデータベースエンジンを実装したインプロセスライブラリ" ◦ Full-Featured SQL ◦ Extensionによる機能拡張 • mattn/go-sqlite3

    ◦ https://github.com/mattn/go-sqlite3 ◦ Goのsqlite3用driver ◦ database/sqlから操作可能
  7. Cloud Spannerエミュレーター • gRPCのAPIを実装してCloud Spannerの挙動を再現する • Google Cloud Client Libraryからアクセス先を切り替えて利用する

  8. App Google Cloud Client Library Google Cloud Spanner handy-spanner SQLite

    gRPC gRPC
  9. 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のトランザクションとして管理
  10. handy-spanner StreamingRead “SELECT FirstName, LastName FROM Singers WHERE FirstName =

    ‘foo’” SQLite SQLite SQL ReadRequest Table = Singers Columns = FirstName, LastName KeySet = ‘foo’ クエリ生成
  11. 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 クエリ生成 クエリパース
  12. 何がSQLiteで難しいの? • SQLiteにないデータ型(配列, 構造体) • SQLiteにない操作の実現 (UNNESTなど) • Cloud Spannerの関数

    • トランザクション
  13. 配列と構造体の実装 • SQLiteのJSON型を利用 • 配列はJSON ARRAY ◦ e.g. [1, 2,

    3], ["foo", "bar"] • 構造体はJSON OBJECT ◦ ただしフィールドに順序があるのでkey, valueを配列で扱う ◦ e.g. {"keys": ["x", "y"], "values": [1, 2]}
  14. 配列と構造体は組み合わせ可能 • 配列の構造体の配列の構造体みたいなのが可能 ◦ もちろん配列の要素型や構造体のフィールドは任意 • 読み書き前に型の定義は正確にわかっている ◦ e.g. ARRAY<STRUCT<x

    INT64, y ARRAY<STRING>>> • DataValue型 ⇔ JSONの相互変換を実現したい ◦ DataValueは型定義と値を保持したstruct []struct { x int64 y []string } type DataValue struct { Type DataType Value interface{} } type DataType struct { // INT64, STRING... }
  15. JSONとの相互変換 • json.Marshalerとjson.Unmarshalerを実装する ◦ DataValue型からJSONへの変換処理をMarshaler ◦ JSONからDataValue型への変換処理をUnmarshaler • 型の定義を見ながら再帰的に展開していく func

    (v *DataValue) MarshalJSON() ([]byte, error) { } func (v *DataValue) UnmarshalJSON([]byte) error { }
  16. データベースから透過的に扱いたい • 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)) }
  17. 関数 • Cloud Spannerには大量の関数がある ◦ 集計関数, 数学関数, 文字列関数, 配列関数, タイムスタンプ関数...

    ◦ https://cloud.google.com/spanner/docs/functions-and-operators • 全ての関数がSQLiteに存在する訳ではない ◦ ABS() など基本的なものはある ◦ 配列関数や, 文字列関数などほとんどの関数はない
  18. 関数 • リテラルやパラメータなどは入力値がわかるのでクエリ変換可能 • データベースの値に対する操作はできることに制限がある… 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
  19. SQLiteのデータを操作したい • SQLite上のデータはアプリケーションから直接操作できない ◦ 出力時にScanner, 入力時にValuerを通すことで加工はできる ◦ JSONのようにExtensionによりSQLite側に直接ロジックも追加可能 Extension SQLite

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

    Scanner Valuer Function
  21. カスタム関数の実装 • Goの関数の引数と戻り値がカスタム関数と一致 ◦ interface{}で任意の型も可能 ◦ 可変長引数も可能 • エラーも返せる SIGN()

    関数の実装 SELECT SIGN(3) // => 1 SELECT SIGN(-2) // => -1
  22. EXTRACT() 関数の実装 カスタム関数の実装

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

  24. カスタム関数を使えば • 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);
  25. カスタム関数を使えば • 仮想的にデータ型も定義可能なはず! • 例えば、位置情報型(Geometry) ◦ SQLiteには"(100, 200)"のような文字列で保存 ◦ 比較(=)などのオペレーターは関数に置き換える

    SELECT tbl.pos1 = tbl.pos2 クエリ変換 SELECT GEO_EQUAL(tbl.pos1, tbl.pos2)
  26. カスタム関数を使う場合の注意 • rows.Err()のチェックは必須 ◦ カスタム関数でエラーが発生時にScanやク エリはエラーにならない

  27. 時間があればトランザクションの難しさを… • SQLiteはスキーマレベル Lock ◦ 誰かがロックを取るの他の全員がロック待ちになる • Shared Cache Modeにするとテーブルレベル

    Lock ◦ 誰かがテーブルを読み込むとRead Lock ◦ 誰かがテーブルを書き込むとWrite Lock ◦ Read Lock中はReadはできるがWriteはできない ◦ Write中は他の誰もReadできない • ずっとReadしているやつがいるといつまでもWriteできない…!
  28. handy-spannerのトランザクション管理 • SQLiteのトランザクションがどのテーブルをどうロックしているか全部管理 • トランザクションの優先度をつける ◦ 必要に応じて任意のトランザクションを終了させる ◦ 通常ならデッドロックするケースでも動くようになる

  29. まとめ • エミュレーションは面白い ◦ エミュレーション対象のことがわかる • SQLiteのハックは面白い ◦ Goの拡張だけでなくC拡張も使えばもっとできることは広がる •

    開発デバッグ目的だけじゃなくプロダクション用途にも発展可能 ◦ S3互換ストレージのような ◦ CQL(Cassandra)とかJQL(Jira) みたいなクエリベース ◦ Redisみたいなコマンドベース