$30 off During Our Annual Pro Sale. View Details »

handy-spanner GCPUG

kazegusuri
February 06, 2020

handy-spanner GCPUG

kazegusuri

February 06, 2020
Tweet

More Decks by kazegusuri

Other Decks in Technology

Transcript

  1. handy-spanner
    A Cloud Spanner emulator
    GCPUG Tokyo
    Handy Spanner Day
    February 2020

    View Slide

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

    View Slide

  3. アジェンダ
    handy-spannerの概要
    01
    handy-spannerの作り方
    02

    View Slide

  4. handy-spannerの概要

    View Slide

  5. 突然のSpannerのハードルが高い理由(一般論)
    ● GCPにロックインされる
    ○ ですよね…
    ● ノウハウが少ない
    ○ パラダイムは違う。一般的なノウハウは増えてきた
    ● 高い
    ○ 最近は1node(月約9万円)からでもSLAがある
    ● 開発環境(ローカル)
    ○ それな

    View Slide

  6. 開発環境の問題
    ● やはりクラウドにしかないのは辛い
    ○ 開発のためにCloud Spannerを直接使う必要がある
    ● ローカルかのように爆速なら良いが…
    ○ 通信へのレイテンシ
    ○ データベース作成速度
    ○ スキーマ変更速度
    ● 軽く試すだけならGCPUG Shared Spannerがあるよ!

    View Slide

  7. 神降臨!?
    ● そんな中突如出現したGoogle製のCloud Spannerのfake実装 (spannertest)
    ○ googleapis/google-cloud-go の spanner ライブラリの下にひっそりと爆誕
    ○ gRPCで接続先を切り替えることで本物のSpannerかのように振る舞う
    ○ google-cloud-go のテストでもしっかり使われている
    ● ついでにSpannerのSQLをパースするためのライブラリも誕生 (spansql)
    ● これは我々が求めていたエミュレーターなのか…

    View Slide

  8. 現実は厳しい
    ● 圧倒的な機能不足
    ○ 単純なSELECTのみサポート (以前は * も使えなかった)
    ○ 一部のデータ型のみサポート (以前は TIMESTAMP も使えなかった)
    ○ そもそもSQLをパースできないものが多い
    ● 必要(要望)があれば機能を追加すると明言(Issue1689)
    ○ だが google-cloud-go リポジトリ… (※)
    ● そもそも雰囲気からすると公式エミュレーターとして提供ではない

    View Slide

  9. spannertestの実装
    ● Cloud Spannerが提供するgRPCサーバーのAPIを実装
    ○ googleapis/googleapis の spanner.proto など
    ● SQLのパースは spansql に依存
    ○ フルスクラッチのGo実装
    ● データストアなどのデータベースの機能もフルスクラッチ実装
    ○ データはGoの値としてインメモリ、クエリの評価も自前実装
    ● 結果として外部ライブラリに何も依存しないのでポータブル

    View Slide

  10. コントリビュートして育てたい、が…
    ● だが google-cloud-go リポジトリ… (※)
    ● そもそも Go でフルスクラッチ実装は現実的なのか?
    ○ クエリエンジンをゼロから作るのはかなり大変そう
    ● そもそもまずspansql直すところからスタート
    ○ memefishならフル実装

    View Slide

  11. かっとなって作った
    ● handy-spanner 爆誕
    ○ SQLiteとmemefishを利用したCloud Spannerエミュレーター
    ○ spannertestと同様にCloud SpannerのgRPCのAPIを提供
    ○ 2週間程度(土日)でspannertestと同じ機能を実装
    ● SQLite
    ○ 組み込み型のRDBMS
    ● github.com/MakeNowJust/memefish
    ○ SpannerのSQLを全てパース可能

    View Slide

  12. App
    Google Cloud
    Client Library
    Google Cloud
    Spanner
    handy-spanner
    spannertest
    memory SQLite
    gRPC gRPC gRPC

    View Slide

  13. 機能性の比較(リリース当時 2019/10/04)
    spannertest handy-spanner
    Data型 TIMESTAMP,STRUCT未サポート STRUCT未サポート
    Read Keysのみ、PrimaryKeyのみ
    カラム指定, Limit/Offset
    Keys, KeyRange、SecondaryIndex
    カラム指定、Limit/Offset
    Storing Column
    Query SELECT カラム指定
    Limit/Offset
    カラム指定, *, alias
    Limit/Offset
    Query WHERE ほとんどの比較 ほとんどの比較
    Query ORDER/GROUP 不可 ORDER BY, GROUP BY, HAVING
    Query Others JOIN, Subquery, Unnest
    Transaction 不可 不可
    DML 不可 不可

    View Slide

  14. 人類の欲望に際限はない
    ● これでだいたいのユースケースは満たせると期待
    ○ データ型もクエリのサポートもspannertestよりできている
    ○ ローカル開発時に使ってもらうことを想定
    ○ 動かないものはそれだけテストをスキップ
    ■ 完全にCIでのテストを置き換えることを想定してない
    ● 思ったより使ってもらうのが大変
    ○ 単純にhandy-spannerを使うようにすると動かないテストが多い
    ○ 9割〜10割は動かないとみんな諦める
    ■ むしろそんなクエリ使っているんか!!

    View Slide

  15. 本気を出した
    ● 一部だけじゃなく全部動くようにする
    ○ Subquery全パターン
    ○ JOIN全パターン
    ○ 算術演算, 集合操作(UNION)
    ○ 関数, CAST, リテラル値
    ○ STRUCT型

    View Slide

  16. 機能性の比較(v0.3.0 2019/11/23)
    spannertest handy-spanner
    Data型 STRUCT未サポート 全部
    Mutation REPLACE以外 全部
    Read KeyRange, SecondaryIndex未サポート 全部
    Query かなり限定的 ほぼ全部
    関数 不可 一部
    Transaction 不可 不可
    DML 不可 不可
    DDL 一部(Drop, Alter系含む) 一部(Createのみ)

    View Slide

  17. ほとんどのユースケースで動くように
    ● 複雑なクエリさえ動けば既存のテストはほぼ動く
    ○ JOIN, Subquery, UNNEST, STRUCT
    ○ メルカリ社内での導入が進み始めた (20+ repos)
    ● 足りていない機能も多い
    ○ 関数系, DDL
    ● エッジケースの対応はまだまだ
    ○ STRUCT周り
    ○ Strict Type Check, Coercing

    View Slide

  18. 社外でも使ってもらえるようにしたい
    ● 他社事例ではやはりDMLを使っている
    ○ メルカリではDMLを使っていないので放置していた
    ○ DMLにはトランザクション管理が必須
    ● トランザクション管理するなら本物に近い挙動に
    ○ 分離レベルをSERIALIAZABLEに
    ○ 競合発生時のAbort
    ● また本気出して作ってみた

    View Slide

  19. 機能性の比較(v0.4.0 2020/01/13)
    spannertest handy-spanner
    Data型 STRUCT未サポート 全部
    Mutation REPLACE以外 全部
    Read KeyRange, SecondaryIndex未サポート 全部
    Query かなり限定的 ほぼ全部
    関数 不可 一部
    Transaction 不可 可能
    DML 不可 全部
    DDL 一部(Drop, Alter系含む) 一部(Createのみ)

    View Slide

  20. みんなに使って欲しい!
    ● トランザクションは力技で実装
    ○ SQLiteでは単純には実現できない
    ○ 若干制約はあるもののアプリケーションからの挙動はほぼ本物と変わらない
    ● DMLの処理自体は単純
    ● 試してないけどJavaのJDBC経由でも動くんじゃないかな!

    View Slide

  21. 要望に応じて直します
    ● こういう使い方をすると動かないとかあればすぐ直します
    ○ ほとんどの場合はすぐ修正可能
    ○ エッジケースの考慮が漏れているだけ
    ○ たまにSQLiteの限界を超えていて難しいのもある
    ● 機能的なところも増やす
    ○ DDL, admin API (database/instance)
    ● Strictモード的なの作りたい
    ○ Coercing禁止とか

    View Slide

  22. handy-spannerの使い方
    ● handy-spannerを起動: 方法は大きく2種類
    ○ 独立したプロセスとしてhandy-spannerを起動する (全環境)
    ○ Goのライブラリとしてhandy-spannerを起動する (Go専用)
    ● クライアントライブラリの接続先を切り替え
    ○ Google Cloud Client Library(google-cloud-goなど)なら切り替え方法が用意されているは

    ○ SPANNER_EMULATOR_HOST 環境変数で接続先を指定など
    ● Go 以外でももちろん使えます!

    View Slide

  23. App
    Google Cloud
    Client Library
    Google Cloud
    Spanner
    gRPC
    spanner.googleapis.com:443
    ・google.spanner.v1.Spanner
    ・google.spanner.admin.instance.v1.InstanceAdmin
    ・google.spanner.admin.database.v1.DatabaseAdmin
    gRPC Service
    Cloud Spannerへの接続

    View Slide

  24. App
    Google Cloud
    Client Library
    handy
    spanner
    gRPC
    localhost:9999
    ・google.spanner.v1.Spanner
    ・google.spanner.admin.instance.v1.InstanceAdmin
    ・google.spanner.admin.database.v1.DatabaseAdmin
    gRPC Service
    Step 1
    Step 2
    handy-spannerへの接続
    1. handy-spannerを起動
    2. 接続先を切り替える

    View Slide

  25. handy-spannerの起動
    ● Docker imageをビルド
    ○ $ git clone https://github.com/gcpug/handy-spanner.git
    ○ $ make docker-build
    ● Docker imageを実行(port 9999でListen)
    ○ $ docker run --rm -d -p 9999:9999 handy-spanner
    ○ インスタンスやデータベースなどは空

    View Slide

  26. クライアントの接続先切り替え
    ● クライアントライブラリによって方法が異なります
    ● SPANNER_EMULATOR_HOST 環境変数を設定する
    ○ ライブラリがこの環境変数を見て接続先を切り替えてくれます
    ○ e.g. $ export SPANNER_EMULATOR_HOST=localhost:9999
    ● アプリケーションでgRPCクライアントの接続先を明示的に指定
    ○ 接続先のアドレスを指定 (Cloud Spannerではなくhandy-spannerに接続するため)
    ○ TLS接続を無効化 (gRPCクライアントがデフォルトTLS接続なので)
    ○ 認証処理の無効化 (クライアントライブラリのトークン発行処理)

    View Slide

  27. クライアントの接続先切り替え(Go)

    View Slide

  28. クライアントの接続先切り替え(PHP)
    ● PHPは @castaneai さんがブログにまとめてくれていた
    ○ PHPからhandy-spannerに接続する
    ○ https://castaneai.hatenablog.com/entry/2020/01/30/115837
    ● 他の言語でもやり方わかればまとめたいな!

    View Slide

  29. あとは使うだけ…?
    ● あとはCloud Spannerを使う時と同様にクライアントを使うだけ
    ● ただしいろいろな疑問が…
    ○ プロジェクトやインスタンスの生成ってconsoleからやっていたけどどうするの?
    ○ データベースはどうやって作る?
    ○ スキーマ(テーブルなど)はどうやって作る?
    ○ 動的にスキーマ変更(DDL)できますか?

    View Slide

  30. データベースなどの作成
    ● Spannerはプロジェクト, インスタンス, データベース, テーブルの階層
    ○ 通常は順番に作っていく必要があるが…
    ● handy-spannerではプロジェクト, インスタンス, データベースは自動生成
    ○ アクセスしたときに勝手に作られるので気にしなくて良い
    ○ 厳密には CreateSession でセッション作成時にデータベースまで全て作られる
    ● マニュアルで管理可能が後述

    View Slide

  31. テーブル,インデックスなどの作成
    ● テーブル, インデックスは自身で用意してもらう必要がある
    ○ handy-spannerの初期化機能を使う (簡単)
    ○ DDLを使ってアプリケーションから操作する (複雑)
    ● DDLを使うためには database admin clientを別途用意する必要がある
    ○ まずはCloud SpannerでDDLを使うコードを書くのに慣れよう
    ○ その後に接続先を handy-spanner に切り替えるように

    View Slide

  32. テーブル,インデックスなどの作成
    ● handy-spannerのオプションで起動時にスキーマを指定して作成
    ○ $ handy-spanner -project foo -instance bar -schema -baz -schema schema.sql
    ● Dockerを使う場合はファイルをマウントする必要がある

    View Slide

  33. 動的にテーブルやインデックスを作成
    ● DDL(データ定義言語)を使う
    ● DDLを使うためには database admin clientを別途用意する必要がある
    ○ 通常のSpanner clientではDDLのAPIを利用できない
    ○ まずはCloud SpannerでDDLを使うコードを書くのに慣れよう
    ○ その後に接続先を handy-spanner に切り替えるように
    ● インスタンスなどの操作も instance admin clientを使えば操作可能
    ○ handys-spannerはまだインスタンス操作には未対応
    ● admin client系は SPANNER_EMULATOR_HOST に対応していないので注意
    ○ 必ずアプリケーションで明示的に接続先を指定する必要がある… (Issue)

    View Slide

  34. DDLの利用例

    View Slide

  35. その他たまにある質問
    ● SQLiteを直接操作(参照、変更)できますか
    ○ 今の所できない (やってないだけなので要望があればやる)
    ● 開発でどうやって使う想定ですか(本番以外は全てhandy-spanner使うなど)
    ○ 基本おまかせだが、本番適用前のどこかでCloud Spannerは使う方が良い
    ○ まだまだエッジケースでCloud Spannerと挙動が異なることがある
    ● SQLiteだから永続化可能!本番でもCloud Spannerの代わりに使えるね!
    ○ GO BOLD

    View Slide

  36. handy-spannerの作り方
    ⚠ ここからは Spanner の Deep な解説があります

    View Slide

  37. App
    Google Cloud
    Client Library
    handy
    spanner
    gRPC
    localhost:9999
    ・google.spanner.v1.Spanner
    ・google.spanner.admin.instance.v1.InstanceAdmin
    ・google.spanner.admin.database.v1.DatabaseAdmin
    gRPC Service
    handy-spannerへの接続

    View Slide

  38. gRPCサービスを実装する
    ● Cloud SpannerのgRPC サービス定義は公開されている
    ○ このinterfaceを満たすサービスを実装する
    ● google.spanner.v1.Spanner
    ○ セッション、トランザクション、Read/Writeなどの基本機能
    ● google.spanner.admin.database.v1.DatabaseAdmin
    ○ データベース操作(Create, Drop, DDLなど)
    ● google.spanner.admin.instance.v1.InstanceAdmin
    ○ インスタンス操作(Create, Drop, ノード数変更など)

    View Slide

  39. 挙動を理解する
    ● Cloud SpannerでSDKを使って操作してみる
    ○ e.g. client.Single().Read(ctx, "Sample", spanner.AllKeys(), []string{"Pkey"})
    ● 向き先をhandy-spannerに切り替えて確認する
    ○ どのgRPCメソッドが呼ばれるか
    ○ どんなメッセージが送られてくるか
    ● Cloud SpannerにgRPCで直接つないで操作してみる
    ○ 同じメッセージで送ればSDKを使った場合と同じ結果が得られるはず
    ○ レスポンスメッセージを確認する
    ⚠ 悪用はしないでください

    View Slide

  40. ひたすら仕様を調べる
    ● Cloud Spannerの仕様はドキュメントにかなり詳しく書いてある
    ○ 普通だったら読み飛ばしてしまうけど実は結構書いてある
    ○ Protoのコメントにも有用な情報がある
    ● Cloud Spannerを触ってCloud Spannerの気持ちになる
    ○ SDKのソースコードを読むと"使い方"が見えてくる
    ○ gRPCのメソッドを直接使って異常なケースを試してみる
    ⚠ 悪用はしないでください

    View Slide

  41. セッション
    ● あらゆる操作にはセッションが必要
    ○ データベース単位でセッションを作る
    ● CreateSession または BatchCreateSessions で作成

    View Slide

  42. セッションの仕様
    ● 1セッションでトランザクションを無限に作れる
    ○ RW Txは32個まで
    ● セッションは別コネクションで利用できる
    ○ 実質1セッションを全サーバーで共有も可能!?
    ● セッションにはラベルがつけられる
    ○ ListSessionsでラベルによるフィルターが可能
    ● セッション名には実はルールがある
    ○ projects/xxx/instances/xxx/databases/xxx/sessions/AJSwhARkqNlfkTvz2LsMsJrJf83_1kxUQiAY8uPQcq2NpDEsVt1ylIwPz4Xy

    View Slide

  43. トランザクション
    ● データベースの読み書きにはトランザクションも必要
    ○ ReadOnlyかReadWriteのモードを指定する (PartitionedDMLは忘れる)
    ● ReadOnlyでは Timestamp Boundの指定ができるらしい

    View Slide

  44. Session
    セッションの構造
    ・Active RO Txs
    ・Active RW Txs
    ・Inactive Txs
    Database
    ・Sessions
    Transaction
    ・Mode (RO/RW)
    ・State

    View Slide

  45. トランザクションの終了
    ● トランザクションはRollbackかCommitで終了できる
    ○ ただしReadOnlyトランザクションは終了できない…
    ○ つまりReadOnlyトランザクションは作りっぱなしでかつ無限に作れる…!
    ● Commit時にMutationを渡すことでデータベースを操作できる
    ○ つまりここまでできれば書き込みができる!

    View Slide

  46. ミューテーション
    ● 構造はシンプル
    ○ Insert, Update, InsertOrUpdate, Replace, Deleteの操作
    ○ カラム名の配列とそれに対応する値のリストを渡して書き込む

    View Slide

  47. バリュー
    ● 値をProtoで表現
    ○ Null値, 浮動小数点数
    ○ 文字列, 真偽値
    ○ 構造体, 配列

    View Slide

  48. Spannerのデータ型
    ● Spannerで使えるデータ型は多い
    ○ 真偽値, 整数, 浮動小数点数
    ○ 文字列, バイト列
    ○ 日付, タイムスタンプ
    ○ 構造体, 配列
    ● 単純にValueにマッピングできなさそう

    View Slide

  49. google-cloud-goがどうしているか
    Spannerデータ型 Value データ型 形式 サンプル
    整数 string 文字列化 “1234”
    文字列 string そのまま “foo”
    バイト列 string Base64 “Zm9v” (“foo”)
    日付 string RFC 3339 “2006-01-02”
    タイムスタンプ string RFC 3339 “2006-01-02T15:04:05.999999999Z07:00”

    View Slide

  50. 値の型
    ● Mutationでは値の元の型はわからない
    ○ 値をバリデーションするには型を知る必要がある
    ○ カラム名から本当の型を知るしかない

    View Slide

  51. データベーススキーマ
    ● DDLでテーブルを作成時にテーブル情報を保持する
    ○ 主キー、カラム名や型、Nullableなど
    ○ ユニークインデックス
    ● handy-spannerではなるべくCloud Spannerと同じエラーメッセージを再現
    ○ クエリ実行前に静的解析してエラーをチェック
    ○ 実行時には再現が難しい場合がある
    ○ ユニーク制約などは実行時じゃないと厳しいのでSQLiteのエラーを元に再現

    View Slide

  52. ミューテーション: Insert
    ● SQLiteでのSQL
    INSERT INTO TABLE_NAME (column1, …) VALUES (value1, …)

    View Slide

  53. ミューテーション: Update
    ● SQLiteでのSQL
    ● 主キーに対応するカラムをWHERE句に置くのがポイント
    UPDATE TABLE_NAME SET column1 = value1, …
    WHERE prim_column1 = prim_value1, ...

    View Slide

  54. ミューテーション: Replace
    ● SQLiteでのSQL
    ● Insertとほぼ同じ
    REPLACE INTO TABLE_NAME (column1, …) VALUES (value1, …)

    View Slide

  55. ミューテーション: InsertOrUpdate
    ● SQLiteでのSQL
    ● ON CONFLICT でInsert失敗時にUpdateする
    INSERT INTO TABLE_NAME (column1, ...) VALUES (value1, ...)
    ON CONFLICT (prim_column1, ...) DO UPDATE SET column1 = value1, ...

    View Slide

  56. Spannerのデータ型とSQLiteのデータ型
    Spannerデータ型 SQLiteデータ型
    真偽値 INTEGER
    整数 INTEGER
    浮動小数点数 REAL
    文字列 TEXT
    バイト列 BLOB
    日付 TEXT
    タイムスタンプ TEXT
    ● SQLiteのデータ型は少ない
    ○ INTEGER
    ○ REAL
    ○ TEXT
    ○ BLOB
    ● 日付とタイムスタンプ
    ○ 文字列のままそのまま格納
    ○ 文字列のままでも比較可能!
    ● 配列と構造体をどうする…?

    View Slide

  57. JSON型
    ● SQLiteはextensionでJSONをデータ型として利用可能
    ● 基本的な操作はだいたい揃っている
    ○ JSON() => 文字列をJSON化
    ○ JSON_EXTRACT() => Pathの値を取り出す
    ○ JSON_REMOVE() => Pathの値を削除
    ○ JSON_INSERT(), JSON_SET(), JSON_REPLACE() => Pathの値を更新

    View Slide

  58. 配列と構造体をJSON型として扱う
    ● 配列はJSON ARRAY
    ○ 配列の要素に値がそのまま入る
    ○ e.g. [1, 2, 3] or [“foo“, “bar“]
    ● 構造体はJSON OBJECT
    ○ ただしSpannerの構造体はフィールドに順序性がある
    ○ フィールド名の配列と値の配列をもったオブジェクト
    ○ e.g. {“keys“: [“x”, “y”], “values”: [1, 2]}

    View Slide

  59. 配列や構造体の複雑さ
    ● Spanner固有の操作がSQLiteの限界を超えてくる
    ○ IN UNNEST(array), FROM UNNEST(array)
    ○ SELECT AS STRUCT * FROM table
    ○ SELECT t.struct.* FROM table t
    ○ SELECT t2.* FROM table t, t.struct t2

    View Slide

  60. 型のProto表現
    ● Spannerの型は Type メッセージ
    ● TypeCodeが型の種類
    ○ BOOL, INT64, STRING, ARRAY, STRUCT…
    ● 構造体はフィールドに順序がある
    ○ map[string]Type ではない

    View Slide

  61. 参照系API
    ● Read(StreamingRead)とQuery(ExecuteStreamingSql)の2つのAPI
    ● Read
    ○ テーブル, キーの集合, カラムを指定
    ● Query
    ○ SQLとパラメータを指定
    ● レスポンスは共通
    ○ PartialResultSetメッセージがストリームで返ってくる

    View Slide

  62. 参照系API

    View Slide

  63. クエリの解析
    ● SQLのパースはmemefishにまかせる
    ○ クエリの構造を表現したASTができる
    ● 意味解析しつつSQLite用のクエリを構築
    ○ UNNESTや配列,構造体の扱いを変換する必要がある
    ● 気合で実装していく

    View Slide

  64. クエリの解析
    ● SQLのパースはmemefishにまかせる
    ○ クエリの構造を表現したASTができる
    ● 意味解析しつつSQLite用のクエリを構築
    ○ UNNESTや配列,構造体の扱いを変換する必要がある
    ● 気合で実装していく

    View Slide

  65. レスポンス
    ● PartialResultSetにValueの配列をセット
    ○ ValueがRowに相当するもの
    ○ つまりValue自体が値の配列
    ● metadataにカラム名の情報が入る

    View Slide

  66. 続きは…

    View Slide

  67. Spannerの気持ちになる
    ● データ構造やアルゴリズムがあっていないと実装がおかしくなる
    ○ Spannerの気持ちになることで実装がきれいになる
    ○ 配列,構造体の扱いは何度も実装をかえてやっと気持ちに近づいてきた
    ● 気持ちがわかると実装が見えてくる
    ○ バリデーションのかかるタイミングや順番などから感じる
    ○ なるほどここの処理は別実装になっているんだな〜

    View Slide