Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
handy-spanner GCPUG
kazegusuri
February 06, 2020
Technology
4
1.2k
handy-spanner GCPUG
kazegusuri
February 06, 2020
Tweet
Share
More Decks by kazegusuri
See All by kazegusuri
go-sqlite3を使ってCloud Spannerエミュレーターを作ってみた / Cloud Spanner emulator with go-sqlite3
kazegusuri
5
5.8k
Open SKT: メルペイ開発の裏側 / builderscon tokyo 2019 Open SKT
kazegusuri
22
24k
Keep watching and extending features of gRPC
kazegusuri
3
2.1k
Testing with microservices in merpay
kazegusuri
10
9.8k
Real World Mercari API Architecture
kazegusuri
1
5.7k
gRPC and REST with gRPC in practice
kazegusuri
19
6.8k
Fluentdで始めるPrometheus / Prometheus Tokyo Meetup #1
kazegusuri
1
1.4k
GRPCの実践と現状での利点欠点 / Go Conference 2016 Spring
kazegusuri
44
30k
OutputとBufferedOutputの間の何か
kazegusuri
2
2.8k
Other Decks in Technology
See All in Technology
ユーザーテストガイドライン VERSION 2.0
kouzoukaikaku
0
1.3k
Oracle Transaction Manager for Microservices Free 22.3 製品概要
oracle4engineer
PRO
5
100
FlexScan HD2452Wの 後継を探して
tring
0
6.3k
日本ディープラーニング協会主催 NeurIPS 2022 技術報告会講演資料
tdailab
0
1.1k
エアドロップ for オープンソースプロジェクト
epicsdao
0
390
証明書って何だっけ? 〜AWSの中間CA移行に備える〜
minorun365
3
2.1k
Deep Neural Networkの共同学習
hf149
0
270
ラズパイとGASで加湿器の消し忘れをLINEでリマインド&操作
minako__ph
0
150
Oktaの管理者権限を適切に移譲してみた
shimosyan
2
270
Kaggleシミュレーションコンペの動向
nagiss
0
270
地方自治体業務あるある ーアナログ最適化編-
y150saya
1
270
NGINXENG JP#2 - 3-NGINX Plus・プロダクトのアップデート
hiropo20
0
240
Featured
See All Featured
For a Future-Friendly Web
brad_frost
166
7.8k
A better future with KSS
kneath
230
16k
The Pragmatic Product Professional
lauravandoore
21
3.4k
How to train your dragon (web standard)
notwaldorf
66
4.3k
Web Components: a chance to create the future
zenorocha
304
40k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
351
21k
Into the Great Unknown - MozCon
thekraken
2
290
Reflections from 52 weeks, 52 projects
jeffersonlam
338
18k
Build your cross-platform service in a week with App Engine
jlugia
221
17k
GitHub's CSS Performance
jonrohan
1020
430k
Bash Introduction
62gerente
601
210k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
217
21k
Transcript
handy-spanner A Cloud Spanner emulator GCPUG Tokyo Handy Spanner Day
February 2020
@kazegusuri • Merpay Backend Engineer • Architect Team 2015/11 2017/01
2018/01 SRE & Platform Platform Payment & Architect
アジェンダ handy-spannerの概要 01 handy-spannerの作り方 02
handy-spannerの概要
突然のSpannerのハードルが高い理由(一般論) • GCPにロックインされる ◦ ですよね… • ノウハウが少ない ◦ パラダイムは違う。一般的なノウハウは増えてきた •
高い ◦ 最近は1node(月約9万円)からでもSLAがある • 開発環境(ローカル) ◦ それな
開発環境の問題 • やはりクラウドにしかないのは辛い ◦ 開発のためにCloud Spannerを直接使う必要がある • ローカルかのように爆速なら良いが… ◦ 通信へのレイテンシ
◦ データベース作成速度 ◦ スキーマ変更速度 • 軽く試すだけならGCPUG Shared Spannerがあるよ!
神降臨!? • そんな中突如出現したGoogle製のCloud Spannerのfake実装 (spannertest) ◦ googleapis/google-cloud-go の spanner ライブラリの下にひっそりと爆誕
◦ gRPCで接続先を切り替えることで本物のSpannerかのように振る舞う ◦ google-cloud-go のテストでもしっかり使われている • ついでにSpannerのSQLをパースするためのライブラリも誕生 (spansql) • これは我々が求めていたエミュレーターなのか…
現実は厳しい • 圧倒的な機能不足 ◦ 単純なSELECTのみサポート (以前は * も使えなかった) ◦ 一部のデータ型のみサポート
(以前は TIMESTAMP も使えなかった) ◦ そもそもSQLをパースできないものが多い • 必要(要望)があれば機能を追加すると明言(Issue1689) ◦ だが google-cloud-go リポジトリ… (※) • そもそも雰囲気からすると公式エミュレーターとして提供ではない
spannertestの実装 • Cloud Spannerが提供するgRPCサーバーのAPIを実装 ◦ googleapis/googleapis の spanner.proto など •
SQLのパースは spansql に依存 ◦ フルスクラッチのGo実装 • データストアなどのデータベースの機能もフルスクラッチ実装 ◦ データはGoの値としてインメモリ、クエリの評価も自前実装 • 結果として外部ライブラリに何も依存しないのでポータブル
コントリビュートして育てたい、が… • だが google-cloud-go リポジトリ… (※) • そもそも Go でフルスクラッチ実装は現実的なのか?
◦ クエリエンジンをゼロから作るのはかなり大変そう • そもそもまずspansql直すところからスタート ◦ memefishならフル実装
かっとなって作った • handy-spanner 爆誕 ◦ SQLiteとmemefishを利用したCloud Spannerエミュレーター ◦ spannertestと同様にCloud SpannerのgRPCのAPIを提供
◦ 2週間程度(土日)でspannertestと同じ機能を実装 • SQLite ◦ 組み込み型のRDBMS • github.com/MakeNowJust/memefish ◦ SpannerのSQLを全てパース可能
App Google Cloud Client Library Google Cloud Spanner handy-spanner spannertest
memory SQLite gRPC gRPC gRPC
機能性の比較(リリース当時 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 不可 不可
人類の欲望に際限はない • これでだいたいのユースケースは満たせると期待 ◦ データ型もクエリのサポートもspannertestよりできている ◦ ローカル開発時に使ってもらうことを想定 ◦ 動かないものはそれだけテストをスキップ ▪
完全にCIでのテストを置き換えることを想定してない • 思ったより使ってもらうのが大変 ◦ 単純にhandy-spannerを使うようにすると動かないテストが多い ◦ 9割〜10割は動かないとみんな諦める ▪ むしろそんなクエリ使っているんか!!
本気を出した • 一部だけじゃなく全部動くようにする ◦ Subquery全パターン ◦ JOIN全パターン ◦ 算術演算, 集合操作(UNION)
◦ 関数, CAST, リテラル値 ◦ STRUCT型
機能性の比較(v0.3.0 2019/11/23) spannertest handy-spanner Data型 STRUCT未サポート 全部 Mutation REPLACE以外 全部
Read KeyRange, SecondaryIndex未サポート 全部 Query かなり限定的 ほぼ全部 関数 不可 一部 Transaction 不可 不可 DML 不可 不可 DDL 一部(Drop, Alter系含む) 一部(Createのみ)
ほとんどのユースケースで動くように • 複雑なクエリさえ動けば既存のテストはほぼ動く ◦ JOIN, Subquery, UNNEST, STRUCT ◦ メルカリ社内での導入が進み始めた
(20+ repos) • 足りていない機能も多い ◦ 関数系, DDL • エッジケースの対応はまだまだ ◦ STRUCT周り ◦ Strict Type Check, Coercing
社外でも使ってもらえるようにしたい • 他社事例ではやはりDMLを使っている ◦ メルカリではDMLを使っていないので放置していた ◦ DMLにはトランザクション管理が必須 • トランザクション管理するなら本物に近い挙動に ◦
分離レベルをSERIALIAZABLEに ◦ 競合発生時のAbort • また本気出して作ってみた
機能性の比較(v0.4.0 2020/01/13) spannertest handy-spanner Data型 STRUCT未サポート 全部 Mutation REPLACE以外 全部
Read KeyRange, SecondaryIndex未サポート 全部 Query かなり限定的 ほぼ全部 関数 不可 一部 Transaction 不可 可能 DML 不可 全部 DDL 一部(Drop, Alter系含む) 一部(Createのみ)
みんなに使って欲しい! • トランザクションは力技で実装 ◦ SQLiteでは単純には実現できない ◦ 若干制約はあるもののアプリケーションからの挙動はほぼ本物と変わらない • DMLの処理自体は単純 •
試してないけどJavaのJDBC経由でも動くんじゃないかな!
要望に応じて直します • こういう使い方をすると動かないとかあればすぐ直します ◦ ほとんどの場合はすぐ修正可能 ◦ エッジケースの考慮が漏れているだけ ◦ たまにSQLiteの限界を超えていて難しいのもある •
機能的なところも増やす ◦ DDL, admin API (database/instance) • Strictモード的なの作りたい ◦ Coercing禁止とか
handy-spannerの使い方 • handy-spannerを起動: 方法は大きく2種類 ◦ 独立したプロセスとしてhandy-spannerを起動する (全環境) ◦ Goのライブラリとしてhandy-spannerを起動する (Go専用)
• クライアントライブラリの接続先を切り替え ◦ Google Cloud Client Library(google-cloud-goなど)なら切り替え方法が用意されているは ず ◦ SPANNER_EMULATOR_HOST 環境変数で接続先を指定など • Go 以外でももちろん使えます!
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への接続
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. 接続先を切り替える
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 ◦ インスタンスやデータベースなどは空
クライアントの接続先切り替え • クライアントライブラリによって方法が異なります • SPANNER_EMULATOR_HOST 環境変数を設定する ◦ ライブラリがこの環境変数を見て接続先を切り替えてくれます ◦ e.g.
$ export SPANNER_EMULATOR_HOST=localhost:9999 • アプリケーションでgRPCクライアントの接続先を明示的に指定 ◦ 接続先のアドレスを指定 (Cloud Spannerではなくhandy-spannerに接続するため) ◦ TLS接続を無効化 (gRPCクライアントがデフォルトTLS接続なので) ◦ 認証処理の無効化 (クライアントライブラリのトークン発行処理)
クライアントの接続先切り替え(Go)
クライアントの接続先切り替え(PHP) • PHPは @castaneai さんがブログにまとめてくれていた ◦ PHPからhandy-spannerに接続する ◦ https://castaneai.hatenablog.com/entry/2020/01/30/115837 •
他の言語でもやり方わかればまとめたいな!
あとは使うだけ…? • あとはCloud Spannerを使う時と同様にクライアントを使うだけ • ただしいろいろな疑問が… ◦ プロジェクトやインスタンスの生成ってconsoleからやっていたけどどうするの? ◦ データベースはどうやって作る?
◦ スキーマ(テーブルなど)はどうやって作る? ◦ 動的にスキーマ変更(DDL)できますか?
データベースなどの作成 • Spannerはプロジェクト, インスタンス, データベース, テーブルの階層 ◦ 通常は順番に作っていく必要があるが… • handy-spannerではプロジェクト,
インスタンス, データベースは自動生成 ◦ アクセスしたときに勝手に作られるので気にしなくて良い ◦ 厳密には CreateSession でセッション作成時にデータベースまで全て作られる • マニュアルで管理可能が後述
テーブル,インデックスなどの作成 • テーブル, インデックスは自身で用意してもらう必要がある ◦ handy-spannerの初期化機能を使う (簡単) ◦ DDLを使ってアプリケーションから操作する (複雑)
• DDLを使うためには database admin clientを別途用意する必要がある ◦ まずはCloud SpannerでDDLを使うコードを書くのに慣れよう ◦ その後に接続先を handy-spanner に切り替えるように
テーブル,インデックスなどの作成 • handy-spannerのオプションで起動時にスキーマを指定して作成 ◦ $ handy-spanner -project foo -instance bar
-schema -baz -schema schema.sql • Dockerを使う場合はファイルをマウントする必要がある
動的にテーブルやインデックスを作成 • 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)
DDLの利用例
その他たまにある質問 • SQLiteを直接操作(参照、変更)できますか ◦ 今の所できない (やってないだけなので要望があればやる) • 開発でどうやって使う想定ですか(本番以外は全てhandy-spanner使うなど) ◦ 基本おまかせだが、本番適用前のどこかでCloud
Spannerは使う方が良い ◦ まだまだエッジケースでCloud Spannerと挙動が異なることがある • SQLiteだから永続化可能!本番でもCloud Spannerの代わりに使えるね! ◦ GO BOLD
handy-spannerの作り方 ⚠ ここからは Spanner の Deep な解説があります
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への接続
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, ノード数変更など)
挙動を理解する • Cloud SpannerでSDKを使って操作してみる ◦ e.g. client.Single().Read(ctx, "Sample", spanner.AllKeys(), []string{"Pkey"})
• 向き先をhandy-spannerに切り替えて確認する ◦ どのgRPCメソッドが呼ばれるか ◦ どんなメッセージが送られてくるか • Cloud SpannerにgRPCで直接つないで操作してみる ◦ 同じメッセージで送ればSDKを使った場合と同じ結果が得られるはず ◦ レスポンスメッセージを確認する ⚠ 悪用はしないでください
ひたすら仕様を調べる • Cloud Spannerの仕様はドキュメントにかなり詳しく書いてある ◦ 普通だったら読み飛ばしてしまうけど実は結構書いてある ◦ Protoのコメントにも有用な情報がある • Cloud
Spannerを触ってCloud Spannerの気持ちになる ◦ SDKのソースコードを読むと"使い方"が見えてくる ◦ gRPCのメソッドを直接使って異常なケースを試してみる ⚠ 悪用はしないでください
セッション • あらゆる操作にはセッションが必要 ◦ データベース単位でセッションを作る • CreateSession または BatchCreateSessions で作成
セッションの仕様 • 1セッションでトランザクションを無限に作れる ◦ RW Txは32個まで • セッションは別コネクションで利用できる ◦ 実質1セッションを全サーバーで共有も可能!?
• セッションにはラベルがつけられる ◦ ListSessionsでラベルによるフィルターが可能 • セッション名には実はルールがある ◦ projects/xxx/instances/xxx/databases/xxx/sessions/AJSwhARkqNlfkTvz2LsMsJrJf83_1kxUQiAY8uPQcq2NpDEsVt1ylIwPz4Xy
トランザクション • データベースの読み書きにはトランザクションも必要 ◦ ReadOnlyかReadWriteのモードを指定する (PartitionedDMLは忘れる) • ReadOnlyでは Timestamp Boundの指定ができるらしい
Session セッションの構造 ・Active RO Txs ・Active RW Txs ・Inactive Txs
Database ・Sessions Transaction ・Mode (RO/RW) ・State
トランザクションの終了 • トランザクションはRollbackかCommitで終了できる ◦ ただしReadOnlyトランザクションは終了できない… ◦ つまりReadOnlyトランザクションは作りっぱなしでかつ無限に作れる…! • Commit時にMutationを渡すことでデータベースを操作できる ◦
つまりここまでできれば書き込みができる!
ミューテーション • 構造はシンプル ◦ Insert, Update, InsertOrUpdate, Replace, Deleteの操作 ◦
カラム名の配列とそれに対応する値のリストを渡して書き込む
バリュー • 値をProtoで表現 ◦ Null値, 浮動小数点数 ◦ 文字列, 真偽値 ◦
構造体, 配列
Spannerのデータ型 • Spannerで使えるデータ型は多い ◦ 真偽値, 整数, 浮動小数点数 ◦ 文字列, バイト列
◦ 日付, タイムスタンプ ◦ 構造体, 配列 • 単純にValueにマッピングできなさそう
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”
値の型 • Mutationでは値の元の型はわからない ◦ 値をバリデーションするには型を知る必要がある ◦ カラム名から本当の型を知るしかない
データベーススキーマ • DDLでテーブルを作成時にテーブル情報を保持する ◦ 主キー、カラム名や型、Nullableなど ◦ ユニークインデックス • handy-spannerではなるべくCloud Spannerと同じエラーメッセージを再現
◦ クエリ実行前に静的解析してエラーをチェック ◦ 実行時には再現が難しい場合がある ◦ ユニーク制約などは実行時じゃないと厳しいのでSQLiteのエラーを元に再現
ミューテーション: Insert • SQLiteでのSQL INSERT INTO TABLE_NAME (column1, …) VALUES
(value1, …)
ミューテーション: Update • SQLiteでのSQL • 主キーに対応するカラムをWHERE句に置くのがポイント UPDATE TABLE_NAME SET column1
= value1, … WHERE prim_column1 = prim_value1, ...
ミューテーション: Replace • SQLiteでのSQL • Insertとほぼ同じ REPLACE INTO TABLE_NAME (column1,
…) VALUES (value1, …)
ミューテーション: InsertOrUpdate • SQLiteでのSQL • ON CONFLICT でInsert失敗時にUpdateする INSERT INTO
TABLE_NAME (column1, ...) VALUES (value1, ...) ON CONFLICT (prim_column1, ...) DO UPDATE SET column1 = value1, ...
Spannerのデータ型とSQLiteのデータ型 Spannerデータ型 SQLiteデータ型 真偽値 INTEGER 整数 INTEGER 浮動小数点数 REAL 文字列
TEXT バイト列 BLOB 日付 TEXT タイムスタンプ TEXT • SQLiteのデータ型は少ない ◦ INTEGER ◦ REAL ◦ TEXT ◦ BLOB • 日付とタイムスタンプ ◦ 文字列のままそのまま格納 ◦ 文字列のままでも比較可能! • 配列と構造体をどうする…?
JSON型 • SQLiteはextensionでJSONをデータ型として利用可能 • 基本的な操作はだいたい揃っている ◦ JSON() => 文字列をJSON化 ◦
JSON_EXTRACT() => Pathの値を取り出す ◦ JSON_REMOVE() => Pathの値を削除 ◦ JSON_INSERT(), JSON_SET(), JSON_REPLACE() => Pathの値を更新
配列と構造体をJSON型として扱う • 配列はJSON ARRAY ◦ 配列の要素に値がそのまま入る ◦ e.g. [1, 2,
3] or [“foo“, “bar“] • 構造体はJSON OBJECT ◦ ただしSpannerの構造体はフィールドに順序性がある ◦ フィールド名の配列と値の配列をもったオブジェクト ◦ e.g. {“keys“: [“x”, “y”], “values”: [1, 2]}
配列や構造体の複雑さ • 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
型のProto表現 • Spannerの型は Type メッセージ • TypeCodeが型の種類 ◦ BOOL, INT64,
STRING, ARRAY, STRUCT… • 構造体はフィールドに順序がある ◦ map[string]Type ではない
参照系API • Read(StreamingRead)とQuery(ExecuteStreamingSql)の2つのAPI • Read ◦ テーブル, キーの集合, カラムを指定 •
Query ◦ SQLとパラメータを指定 • レスポンスは共通 ◦ PartialResultSetメッセージがストリームで返ってくる
参照系API
クエリの解析 • SQLのパースはmemefishにまかせる ◦ クエリの構造を表現したASTができる • 意味解析しつつSQLite用のクエリを構築 ◦ UNNESTや配列,構造体の扱いを変換する必要がある •
気合で実装していく
クエリの解析 • SQLのパースはmemefishにまかせる ◦ クエリの構造を表現したASTができる • 意味解析しつつSQLite用のクエリを構築 ◦ UNNESTや配列,構造体の扱いを変換する必要がある •
気合で実装していく
レスポンス • PartialResultSetにValueの配列をセット ◦ ValueがRowに相当するもの ◦ つまりValue自体が値の配列 • metadataにカラム名の情報が入る
続きは…
Spannerの気持ちになる • データ構造やアルゴリズムがあっていないと実装がおかしくなる ◦ Spannerの気持ちになることで実装がきれいになる ◦ 配列,構造体の扱いは何度も実装をかえてやっと気持ちに近づいてきた • 気持ちがわかると実装が見えてくる ◦
バリデーションのかかるタイミングや順番などから感じる ◦ なるほどここの処理は別実装になっているんだな〜