handy-spanner GCPUG

59c0cc69a8ad4ca8d26d752b3b795b55?s=47 kazegusuri
February 06, 2020

handy-spanner GCPUG

59c0cc69a8ad4ca8d26d752b3b795b55?s=128

kazegusuri

February 06, 2020
Tweet

Transcript

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

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

    2018/01 SRE & Platform Platform Payment & Architect
  3. アジェンダ handy-spannerの概要 01 handy-spannerの作り方 02

  4. handy-spannerの概要

  5. 突然のSpannerのハードルが高い理由(一般論) • GCPにロックインされる ◦ ですよね… • ノウハウが少ない ◦ パラダイムは違う。一般的なノウハウは増えてきた •

    高い ◦ 最近は1node(月約9万円)からでもSLAがある • 開発環境(ローカル) ◦ それな
  6. 開発環境の問題 • やはりクラウドにしかないのは辛い ◦ 開発のためにCloud Spannerを直接使う必要がある • ローカルかのように爆速なら良いが… ◦ 通信へのレイテンシ

    ◦ データベース作成速度 ◦ スキーマ変更速度 • 軽く試すだけならGCPUG Shared Spannerがあるよ!
  7. 神降臨!? • そんな中突如出現したGoogle製のCloud Spannerのfake実装 (spannertest) ◦ googleapis/google-cloud-go の spanner ライブラリの下にひっそりと爆誕

    ◦ gRPCで接続先を切り替えることで本物のSpannerかのように振る舞う ◦ google-cloud-go のテストでもしっかり使われている • ついでにSpannerのSQLをパースするためのライブラリも誕生 (spansql) • これは我々が求めていたエミュレーターなのか…
  8. 現実は厳しい • 圧倒的な機能不足 ◦ 単純なSELECTのみサポート (以前は * も使えなかった) ◦ 一部のデータ型のみサポート

    (以前は TIMESTAMP も使えなかった) ◦ そもそもSQLをパースできないものが多い • 必要(要望)があれば機能を追加すると明言(Issue1689) ◦ だが google-cloud-go リポジトリ… (※) • そもそも雰囲気からすると公式エミュレーターとして提供ではない
  9. spannertestの実装 • Cloud Spannerが提供するgRPCサーバーのAPIを実装 ◦ googleapis/googleapis の spanner.proto など •

    SQLのパースは spansql に依存 ◦ フルスクラッチのGo実装 • データストアなどのデータベースの機能もフルスクラッチ実装 ◦ データはGoの値としてインメモリ、クエリの評価も自前実装 • 結果として外部ライブラリに何も依存しないのでポータブル
  10. コントリビュートして育てたい、が… • だが google-cloud-go リポジトリ… (※) • そもそも Go でフルスクラッチ実装は現実的なのか?

    ◦ クエリエンジンをゼロから作るのはかなり大変そう • そもそもまずspansql直すところからスタート ◦ memefishならフル実装
  11. かっとなって作った • handy-spanner 爆誕 ◦ SQLiteとmemefishを利用したCloud Spannerエミュレーター ◦ spannertestと同様にCloud SpannerのgRPCのAPIを提供

    ◦ 2週間程度(土日)でspannertestと同じ機能を実装 • SQLite ◦ 組み込み型のRDBMS • github.com/MakeNowJust/memefish ◦ SpannerのSQLを全てパース可能
  12. App Google Cloud Client Library Google Cloud Spanner handy-spanner spannertest

    memory SQLite gRPC gRPC gRPC
  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 不可 不可
  14. 人類の欲望に際限はない • これでだいたいのユースケースは満たせると期待 ◦ データ型もクエリのサポートもspannertestよりできている ◦ ローカル開発時に使ってもらうことを想定 ◦ 動かないものはそれだけテストをスキップ ▪

    完全にCIでのテストを置き換えることを想定してない • 思ったより使ってもらうのが大変 ◦ 単純にhandy-spannerを使うようにすると動かないテストが多い ◦ 9割〜10割は動かないとみんな諦める ▪ むしろそんなクエリ使っているんか!!
  15. 本気を出した • 一部だけじゃなく全部動くようにする ◦ Subquery全パターン ◦ JOIN全パターン ◦ 算術演算, 集合操作(UNION)

    ◦ 関数, CAST, リテラル値 ◦ STRUCT型
  16. 機能性の比較(v0.3.0 2019/11/23) spannertest handy-spanner Data型 STRUCT未サポート 全部 Mutation REPLACE以外 全部

    Read KeyRange, SecondaryIndex未サポート 全部 Query かなり限定的 ほぼ全部 関数 不可 一部 Transaction 不可 不可 DML 不可 不可 DDL 一部(Drop, Alter系含む) 一部(Createのみ)
  17. ほとんどのユースケースで動くように • 複雑なクエリさえ動けば既存のテストはほぼ動く ◦ JOIN, Subquery, UNNEST, STRUCT ◦ メルカリ社内での導入が進み始めた

    (20+ repos) • 足りていない機能も多い ◦ 関数系, DDL • エッジケースの対応はまだまだ ◦ STRUCT周り ◦ Strict Type Check, Coercing
  18. 社外でも使ってもらえるようにしたい • 他社事例ではやはりDMLを使っている ◦ メルカリではDMLを使っていないので放置していた ◦ DMLにはトランザクション管理が必須 • トランザクション管理するなら本物に近い挙動に ◦

    分離レベルをSERIALIAZABLEに ◦ 競合発生時のAbort • また本気出して作ってみた
  19. 機能性の比較(v0.4.0 2020/01/13) spannertest handy-spanner Data型 STRUCT未サポート 全部 Mutation REPLACE以外 全部

    Read KeyRange, SecondaryIndex未サポート 全部 Query かなり限定的 ほぼ全部 関数 不可 一部 Transaction 不可 可能 DML 不可 全部 DDL 一部(Drop, Alter系含む) 一部(Createのみ)
  20. みんなに使って欲しい! • トランザクションは力技で実装 ◦ SQLiteでは単純には実現できない ◦ 若干制約はあるもののアプリケーションからの挙動はほぼ本物と変わらない • DMLの処理自体は単純 •

    試してないけどJavaのJDBC経由でも動くんじゃないかな!
  21. 要望に応じて直します • こういう使い方をすると動かないとかあればすぐ直します ◦ ほとんどの場合はすぐ修正可能 ◦ エッジケースの考慮が漏れているだけ ◦ たまにSQLiteの限界を超えていて難しいのもある •

    機能的なところも増やす ◦ DDL, admin API (database/instance) • Strictモード的なの作りたい ◦ Coercing禁止とか
  22. handy-spannerの使い方 • handy-spannerを起動: 方法は大きく2種類 ◦ 独立したプロセスとしてhandy-spannerを起動する (全環境) ◦ Goのライブラリとしてhandy-spannerを起動する (Go専用)

    • クライアントライブラリの接続先を切り替え ◦ Google Cloud Client Library(google-cloud-goなど)なら切り替え方法が用意されているは ず ◦ SPANNER_EMULATOR_HOST 環境変数で接続先を指定など • Go 以外でももちろん使えます!
  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への接続
  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. 接続先を切り替える
  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 ◦ インスタンスやデータベースなどは空
  26. クライアントの接続先切り替え • クライアントライブラリによって方法が異なります • SPANNER_EMULATOR_HOST 環境変数を設定する ◦ ライブラリがこの環境変数を見て接続先を切り替えてくれます ◦ e.g.

    $ export SPANNER_EMULATOR_HOST=localhost:9999 • アプリケーションでgRPCクライアントの接続先を明示的に指定 ◦ 接続先のアドレスを指定 (Cloud Spannerではなくhandy-spannerに接続するため) ◦ TLS接続を無効化 (gRPCクライアントがデフォルトTLS接続なので) ◦ 認証処理の無効化 (クライアントライブラリのトークン発行処理)
  27. クライアントの接続先切り替え(Go)

  28. クライアントの接続先切り替え(PHP) • PHPは @castaneai さんがブログにまとめてくれていた ◦ PHPからhandy-spannerに接続する ◦ https://castaneai.hatenablog.com/entry/2020/01/30/115837 •

    他の言語でもやり方わかればまとめたいな!
  29. あとは使うだけ…? • あとはCloud Spannerを使う時と同様にクライアントを使うだけ • ただしいろいろな疑問が… ◦ プロジェクトやインスタンスの生成ってconsoleからやっていたけどどうするの? ◦ データベースはどうやって作る?

    ◦ スキーマ(テーブルなど)はどうやって作る? ◦ 動的にスキーマ変更(DDL)できますか?
  30. データベースなどの作成 • Spannerはプロジェクト, インスタンス, データベース, テーブルの階層 ◦ 通常は順番に作っていく必要があるが… • handy-spannerではプロジェクト,

    インスタンス, データベースは自動生成 ◦ アクセスしたときに勝手に作られるので気にしなくて良い ◦ 厳密には CreateSession でセッション作成時にデータベースまで全て作られる • マニュアルで管理可能が後述
  31. テーブル,インデックスなどの作成 • テーブル, インデックスは自身で用意してもらう必要がある ◦ handy-spannerの初期化機能を使う (簡単) ◦ DDLを使ってアプリケーションから操作する (複雑)

    • DDLを使うためには database admin clientを別途用意する必要がある ◦ まずはCloud SpannerでDDLを使うコードを書くのに慣れよう ◦ その後に接続先を handy-spanner に切り替えるように
  32. テーブル,インデックスなどの作成 • handy-spannerのオプションで起動時にスキーマを指定して作成 ◦ $ handy-spanner -project foo -instance bar

    -schema -baz -schema schema.sql • Dockerを使う場合はファイルをマウントする必要がある
  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)
  34. DDLの利用例

  35. その他たまにある質問 • SQLiteを直接操作(参照、変更)できますか ◦ 今の所できない (やってないだけなので要望があればやる) • 開発でどうやって使う想定ですか(本番以外は全てhandy-spanner使うなど) ◦ 基本おまかせだが、本番適用前のどこかでCloud

    Spannerは使う方が良い ◦ まだまだエッジケースでCloud Spannerと挙動が異なることがある • SQLiteだから永続化可能!本番でもCloud Spannerの代わりに使えるね! ◦ GO BOLD
  36. handy-spannerの作り方 ⚠ ここからは Spanner の Deep な解説があります

  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への接続
  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, ノード数変更など)
  39. 挙動を理解する • Cloud SpannerでSDKを使って操作してみる ◦ e.g. client.Single().Read(ctx, "Sample", spanner.AllKeys(), []string{"Pkey"})

    • 向き先をhandy-spannerに切り替えて確認する ◦ どのgRPCメソッドが呼ばれるか ◦ どんなメッセージが送られてくるか • Cloud SpannerにgRPCで直接つないで操作してみる ◦ 同じメッセージで送ればSDKを使った場合と同じ結果が得られるはず ◦ レスポンスメッセージを確認する ⚠ 悪用はしないでください
  40. ひたすら仕様を調べる • Cloud Spannerの仕様はドキュメントにかなり詳しく書いてある ◦ 普通だったら読み飛ばしてしまうけど実は結構書いてある ◦ Protoのコメントにも有用な情報がある • Cloud

    Spannerを触ってCloud Spannerの気持ちになる ◦ SDKのソースコードを読むと"使い方"が見えてくる ◦ gRPCのメソッドを直接使って異常なケースを試してみる ⚠ 悪用はしないでください
  41. セッション • あらゆる操作にはセッションが必要 ◦ データベース単位でセッションを作る • CreateSession または BatchCreateSessions で作成

  42. セッションの仕様 • 1セッションでトランザクションを無限に作れる ◦ RW Txは32個まで • セッションは別コネクションで利用できる ◦ 実質1セッションを全サーバーで共有も可能!?

    • セッションにはラベルがつけられる ◦ ListSessionsでラベルによるフィルターが可能 • セッション名には実はルールがある ◦ projects/xxx/instances/xxx/databases/xxx/sessions/AJSwhARkqNlfkTvz2LsMsJrJf83_1kxUQiAY8uPQcq2NpDEsVt1ylIwPz4Xy
  43. トランザクション • データベースの読み書きにはトランザクションも必要 ◦ ReadOnlyかReadWriteのモードを指定する (PartitionedDMLは忘れる) • ReadOnlyでは Timestamp Boundの指定ができるらしい

  44. Session セッションの構造 ・Active RO Txs ・Active RW Txs ・Inactive Txs

    Database ・Sessions Transaction ・Mode (RO/RW) ・State
  45. トランザクションの終了 • トランザクションはRollbackかCommitで終了できる ◦ ただしReadOnlyトランザクションは終了できない… ◦ つまりReadOnlyトランザクションは作りっぱなしでかつ無限に作れる…! • Commit時にMutationを渡すことでデータベースを操作できる ◦

    つまりここまでできれば書き込みができる!
  46. ミューテーション • 構造はシンプル ◦ Insert, Update, InsertOrUpdate, Replace, Deleteの操作 ◦

    カラム名の配列とそれに対応する値のリストを渡して書き込む
  47. バリュー • 値をProtoで表現 ◦ Null値, 浮動小数点数 ◦ 文字列, 真偽値 ◦

    構造体, 配列
  48. Spannerのデータ型 • Spannerで使えるデータ型は多い ◦ 真偽値, 整数, 浮動小数点数 ◦ 文字列, バイト列

    ◦ 日付, タイムスタンプ ◦ 構造体, 配列 • 単純にValueにマッピングできなさそう
  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”
  50. 値の型 • Mutationでは値の元の型はわからない ◦ 値をバリデーションするには型を知る必要がある ◦ カラム名から本当の型を知るしかない

  51. データベーススキーマ • DDLでテーブルを作成時にテーブル情報を保持する ◦ 主キー、カラム名や型、Nullableなど ◦ ユニークインデックス • handy-spannerではなるべくCloud Spannerと同じエラーメッセージを再現

    ◦ クエリ実行前に静的解析してエラーをチェック ◦ 実行時には再現が難しい場合がある ◦ ユニーク制約などは実行時じゃないと厳しいのでSQLiteのエラーを元に再現
  52. ミューテーション: Insert • SQLiteでのSQL INSERT INTO TABLE_NAME (column1, …) VALUES

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

    = value1, … WHERE prim_column1 = prim_value1, ...
  54. ミューテーション: Replace • SQLiteでのSQL • Insertとほぼ同じ REPLACE INTO TABLE_NAME (column1,

    …) VALUES (value1, …)
  55. ミューテーション: InsertOrUpdate • SQLiteでのSQL • ON CONFLICT でInsert失敗時にUpdateする INSERT INTO

    TABLE_NAME (column1, ...) VALUES (value1, ...) ON CONFLICT (prim_column1, ...) DO UPDATE SET column1 = value1, ...
  56. Spannerのデータ型とSQLiteのデータ型 Spannerデータ型 SQLiteデータ型 真偽値 INTEGER 整数 INTEGER 浮動小数点数 REAL 文字列

    TEXT バイト列 BLOB 日付 TEXT タイムスタンプ TEXT • SQLiteのデータ型は少ない ◦ INTEGER ◦ REAL ◦ TEXT ◦ BLOB • 日付とタイムスタンプ ◦ 文字列のままそのまま格納 ◦ 文字列のままでも比較可能! • 配列と構造体をどうする…?
  57. JSON型 • SQLiteはextensionでJSONをデータ型として利用可能 • 基本的な操作はだいたい揃っている ◦ JSON() => 文字列をJSON化 ◦

    JSON_EXTRACT() => Pathの値を取り出す ◦ JSON_REMOVE() => Pathの値を削除 ◦ JSON_INSERT(), JSON_SET(), JSON_REPLACE() => Pathの値を更新
  58. 配列と構造体をJSON型として扱う • 配列はJSON ARRAY ◦ 配列の要素に値がそのまま入る ◦ e.g. [1, 2,

    3] or [“foo“, “bar“] • 構造体はJSON OBJECT ◦ ただしSpannerの構造体はフィールドに順序性がある ◦ フィールド名の配列と値の配列をもったオブジェクト ◦ e.g. {“keys“: [“x”, “y”], “values”: [1, 2]}
  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
  60. 型のProto表現 • Spannerの型は Type メッセージ • TypeCodeが型の種類 ◦ BOOL, INT64,

    STRING, ARRAY, STRUCT… • 構造体はフィールドに順序がある ◦ map[string]Type ではない
  61. 参照系API • Read(StreamingRead)とQuery(ExecuteStreamingSql)の2つのAPI • Read ◦ テーブル, キーの集合, カラムを指定 •

    Query ◦ SQLとパラメータを指定 • レスポンスは共通 ◦ PartialResultSetメッセージがストリームで返ってくる
  62. 参照系API

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

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

    気合で実装していく
  65. レスポンス • PartialResultSetにValueの配列をセット ◦ ValueがRowに相当するもの ◦ つまりValue自体が値の配列 • metadataにカラム名の情報が入る

  66. 続きは…

  67. Spannerの気持ちになる • データ構造やアルゴリズムがあっていないと実装がおかしくなる ◦ Spannerの気持ちになることで実装がきれいになる ◦ 配列,構造体の扱いは何度も実装をかえてやっと気持ちに近づいてきた • 気持ちがわかると実装が見えてくる ◦

    バリデーションのかかるタイミングや順番などから感じる ◦ なるほどここの処理は別実装になっているんだな〜