Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

handy-spannerの概要

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

機能性の比較(リリース当時 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 不可 不可

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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への接続

Slide 24

Slide 24 text

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. 接続先を切り替える

Slide 25

Slide 25 text

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 ○ インスタンスやデータベースなどは空

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

動的にテーブルやインデックスを作成 ● 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)

Slide 34

Slide 34 text

DDLの利用例

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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への接続

Slide 38

Slide 38 text

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, ノード数変更など)

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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”

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

配列や構造体の複雑さ ● 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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

参照系API

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

続きは…

Slide 67

Slide 67 text

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