[GCP] Spanner のアーキテクチャを図解する

[GCP] Spanner のアーキテクチャを図解する

こんばんは。七色メガネです。今日は、GCPにて提供されるSpannerのアーキテクチャを俯瞰してみたいと思います。

 

Spanner とは?

Spanner は、GCP で提供される分散型のRDBです。次の記事で Spanner についての紹介を行なっていますので、よろしければご参照ください。

[ GCP ] メガネと学ぶ GCP (8) ストレージ・サービスの特徴を知る

Spanner のアーキテクチャを俯瞰する

では早速、Spanner のアーキテクチャを確認しましょう。Spanner は次のような内部構成を持っています。

Client

Client は、Spanner で実行するコードなどが保存されているサーバーです。1つのインスタンスに1つ存在しますが、開発者が Client の存在を意識することはあまりありません。

Spanner のクライアント・ライブラリを使用してリクエストを行う時、そのリクエストはこの Client に届けられます。

Node

Node は、データの read/write を実行する Spanner におけるコンピュート・サーバーです。

1つのインスタンスには 1 ~ x 個 の Node を用意することが出来ます。
Node を増やすことのメリットは、主に次の通りです。

・管理できるデータ容量が増える。

・スループット性能が向上する。

1ノードが管理することができるデータサイズは、2TB までです。
従って、扱うデータ容量に応じて適切な Node 数を設定する必要があります。ですが Node の数は後から変更してスケールさせることもできるため、必ずしも最初から十分なノード数を設定する必要はありません。ただし、インスタンス内の Node が自動的にスケーリングされることはありません。StackDriver などを使用して使用率を監視し、適切なタイミングで Node の追加を指示する必要があります。

データは Node に保存されるわけではありません。データそのものは、Split という単位で分散保存されています。Node は、Split を管理し、ネットワーク経由でアクセスするためのものです。
つまり1 Node における 2TB の上限とは、1 Node が管理する Split のデータサイズの合計が 2TB まで、ということを意味します。

また、 Node を増やすことでスループット性能を向上させることが出来ます。これは単純に、データの read/write を分散処理できるようになるためです。
仮に Node の Read 性能が 1GB/sec であり読み取るべきデータが 4GB 存在したとします。このRead処理を 1Node で行えば、分散のしようがありませんから 4sec かかってしまいます。しかしデータが分散されている状態の 2Node で行えば処理へ並列実行され、2sec で処理が終了すると見込めます。

Split

Split は、Spanner におけるデータの保存単位を意味します。Split は Node とは別のサーバーで保存されます。1つの Split は必ず1つの Node により管理され、その Split への read/write リクエストは紐づく Node を経由して実行されます。

テーブルのデータは、自動的に各 Split に振り分けられます。どの Split にデータが保存されたかは、観測することが出来ません。
しかし、データを Split に振り分ける基本的なルールが存在しています。それは PK のキー範囲です。

Spanner は、PKのキー範囲が隣接するデータを同じ Split に保存しようとします。下記のようなイメージです。

これは一般的に避けるべき状態とされています。なぜなら、特定の Node 下に特定のデータが集中してしまうと、そのデータに対するオペレーションを分散処理出来なくなってしまうからです。
このようにデータが特定ノード下に集中してしまう現象を、「ホットスポットの発生」と言います。回避するためには、PK をシーケンシャルな値ではなく、UUID などランダムな値にするなどの方法があります。

Split のデータ保存上限は 4GB です。4GB を超過しそうな場合、Split は分割されます。分割は自動で行われるため、分割されたデータがどの Split に格納されるかは分かりません。別の Node 下の Split に移動することも考えられます。

 

スキーマ設計時の注意事項

Spanner の設計において最も注意すべきことが、データの保存が局所的になってしまうことで発生するホットスポットを回避することです。
またホットスポットを回避することとは真逆の観点ですが、必ず結合しなければいけないデータ群などはあえて局所的に保存し、分散させないことでスループットを上げるという方法(インターリーブ)も存在します。

ホットスポットの回避

ホットスポットとは何かについては、Split の項で言及したので説明は省きます。

Google が推奨するのは、PK を UUID にすることでデータが保存される Split を分散させるという方法です。
逆にアンチパターンとされるのは、シーケンシャルな番号やタイムスタンプなどを PK にしてしまう場合です。

新規に Spanner でテーブルを作成するときは上記点に留意して設計するだけで良いですが、既存のDBからデータ移行などを行う際には、既存データの PK が Spanner でのアンチパターンに引っかかっていないかを確認する必要があります。

インターリーブの適用

ホットスポットではデータの局所性を回避しました。しかし、データに局所性を持たせて置くのが望ましい場合もあります。

例えば、「Singer」に関するデータと、「Song」に関するデータが存在するとし、この2つは常に結合されて Select されるものとします。

ここでのデータ保存パターンは3つ考えられます。すなわち、

パターンA・同じ Node 下の 同じ Split 内に Singer と Song のデータが保存されている。

パターンB・同じ Node 下の 異なる Split 内に Singer と Song のデータが保存されている。

パターンC・異なる Node 下の Split 内に Singer と Song のデータが保存されている。

結論から言うと、パターンC の場合には処理速度が低下します。パターンAとBではそれほど処理速度は変わりません。

パターンC の場合で処理速度が低下する原因は、データの取得・結合処理に、2つのノードにアクセスするための処理時間が加わるという点です。パターンA/Bの場合には、いずれも同じノード内の Split なのでパターンCのような余計な処理時間は発生しません。

しかしホットスポットを回避するために PK を UUID にしているとデータが分散されるため、どの Node 下の Split にデータが保存されるかをコントロールすることが出来ません。

そこで使用するのが、インターリーブです。

今回の例で言えば、Song テーブルを create するときにインターリーブを適用することで、明示的にパターンAの状況を作ることが出来ます。これにより、処理速度の低下を防ぐことが出来ますね。

インターリーブを適用するDDLの解説は、本記事では行いません。

 

参考

https://medium.com/google-cloud-jp/cloud-spanner-%E3%81%AE%E3%83%8F%E3%82%A4%E3%83%AC%E3%83%99%E3%83%AB%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3%E8%A7%A3%E8%AA%AC-fee62c17f7ed

https://docs.google.com/presentation/d/1XKaOrex3WS8xZ0TsjsTQKBgxjVSGxkyqdcIqG66zd64/htmlpresent

https://cloud.google.com/spanner/docs/schema-design?hl=ja

https://cloud.google.com/spanner/docs/instances?hl=ja

 

書籍紹介

Google Cloud Platform エンタープライズ設計ガイド

Google Cloud Platform 実践Webアプリ開発 ストーリーで学ぶGoogle App Engine

プログラマのためのGoogle Cloud Platform入門 サービスの全体像からクラウドネイティブアプリケーション構築まで

GoogleCloudPlatformカテゴリの最新記事