Cloud Spanner のハイレベルアーキテクチャ解説

Google Cloud でゲーム担当をやっているサミールです。

本日のトピックは私が大好きな Cloud Spanner となります。Cloud Spanner は GCP のフルマネージド・グローバルスケール・リレーショナルデータベース・サービスです。Cloud Spanner は裏では NoSQL でよくある分散データベースですので、NoSQL の特性を提供しております。

  • HA (高可用性)
  • 水平方向のスケーラビリティ(動的にダウンタイムなしにノードの追加・削除が可能)

なお MySQL のようなリレーショナルDB と同じ特性も提供しております。

  • スキーマ
  • 強整合性
  • SQL クエリ(ANSI 2011)

言い過ぎかもしれないが、マルチマスターの MySQL というイメージで良いと思います。

注意点:Cloud Spanner は MySQL 互換ではありません。gRPC/REST の API を通じてメソッドを呼び出して操作します。ソフトウェアからは7つの言語で提供されているクライアントライブラリを利用することができます。

以前 Slideshare に Cloud Spanner の落とし穴やベストプラクティスを説明する資料を公開しました。こちらをご参考にしていただければ、新しいテクノロジーを使い始める時にハマるポイントを回避できるのかと思います。そして、今日はその資料にも説明されている Cloud Spanner のスケールアウトの際になぜリシャーディングがあっと言う間に完了するのかを Cloud Spanner のハイレベルアーキテクチャを解説しながら説明します。

Cloud Spanner のハイレベルアーキテクチャは以下となります。

上記の図を解説していきましょう。

クライアント

クライアントは Cloud Spanner に読み書きするコードが実行されているサーバやコンテナになります。Cloud Spanner のクライアントライブラリーを利用すると gRPC で通信します。REST API という選択肢もあります。クライアントライブラリーのメリットとしては、gRPC による通信、リトライ処理や gRPC セッションの使い回しを行ってくれます。

注意点:現状では Google Compute Engine (GCE)、Google Kubernetes Engine (GKE)、Google App Engine (GAE) Flexible Environmentや Cloud Functions からはクライアントライブラリーを利用して gRPC で通信を行うことができますが、GAE Standard Environmentの場合は現状では GAE Standard + Java 8 のみから gRPC 通信が行えます。他の言語を GAE Standard で使う場合は REST API をご利用いただく必要があります。
参考:https://cloud.google.com/spanner/docs/integrate-google-cloud-platform

ノード

ノードは Cloud Spanner への読み書き処理を実行するコンピュートリソースです。ノードには直接ローカルストレージ(ディスク)はアタッチされていません。格納されたデータ自体は分散ファイルシステム(インターナルネーム Colossus)に保存され、ノードはネットワーク経由でデータにアクセスします。

注意点:Cloud Spanner のドキュメントによると、1ノードごとのストレージ容量の上限が 2TB と記載されています。上記に説明しましたように、ノードにはディスクがアタッチされていないため、その 2TB はストレージ容量の上限ではありません。データ自体は分散ファイルシステムに保存されているため 2TB の上限はストレージの制約でなく、ノードのコンピュートリソースの制約となります。ノードはデータの単位となる Split を管理しており、データにアクセスする際に使用されるリソースです。

GCP コンソールからシングルリージョンモデルで1ノードを設定すると、実はそのリージョンの各ゾーンに1レプリカが立ち上がります

1ノードごとに3つのレプリカ

コンピュートとストレージを疎結合にすることで高可用性を実現しております。あるゾーンのレプリカがダウンしても、他のゾーンにあるレプリカがリーダーと選択され、アクセス処理が継続します。そして、ゾーン間のレプリケーションは Paxos プロトコルに基づいて行われます。MySQLと比較して若干レイテンシーが高いのは高整合性を担保しながらレプリケーションを Paxos で行っているからです。高可用性のコストですね。

マルチリージョンで Cloud Spanner を展開すると、1つの大陸(US)または3つの大陸(US/EU/Asia)でデプロイすることができます。現時点では US のみが Read Write (RW) リージョンとなります。他のリージョンは Read Only (RO)リージョンとなります。以下は3つの大陸での例です。

RW レプリカは US のみ、RO は EU と Asia
注意点:RO レプリカは約15秒ごとに RW レプリカと同期しますので、Strong Read を行いますと RW レプリカにアクセスする必要があり、RO レプリカにアクセスするメリットを感じなくなると思います。近いリージョンにある RO レプリカを低レイテンシーでアクセスするには、Stale Read で過去のデータを読む必要があります。15秒以上古いデータを読めば、確実に RO レプリカからクエリの結果が返されます。

ストレージ

Colossus は Google が開発した分散ファイルシステムです。GFS (Google File System)の後任となるテクノロジーです。Spanner のノードはネットワークを通して、Colossus にアクセスします。ネットワークスタックとしてはグーグルが開発したフルメッシュネットワーク Jupiter を利用しております。

グーグルのデータセンターネットワークにご興味ございましたら、弊社の中井が書いた非常にわかりやすい記事をオススメします。Google File System についても中井からのグレート記事がございます!是非ご覧ください。

Split

Split は Cloud Spanner でのデータの単位です。Cloud Spanner は複数のノードをフル活用するためにテーブルを自動的に Split に分割します。Split はあるノードから”管理”されます。Split に格納されているデータを書き込み・読み込みしようとすると、Split をリードするノード経由でアクセスします。

この例では最初の3行が1つの Split、最後の2行がもう1つの Split となります。実際に Split には数千行が格納されます。

上記のテーブルを定義するには、以下の DDL を利用することができます。

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
FirstName  STRING(1024),
LastName   STRING(1024),
SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

Split の容量上限は数 GBです。(以前エンジニアリングに確認した時、2GBと聞きましたが、今後変わる可能性がございますのでご了承ください m(_ _ )m)

注意点:上記の DDL には PK として INT64 を利用して、そして以下の例では簡略化するためにインクリメンタルな値になっているが、本番ではこのようなスキーマはホットスポットを発生する可能性がありますので、積極的に UUID を PK としてご利用いただくようお勧めしております。UUID は 128 bit のため INT64 に治りませんので、STRING[36] に十六進法で UUID をご利用ください。

Cloud Spanner がテーブル分割を自動的に行いますので、どのデータがどの Split に格納されるかを識別することは難しいです。2つのテーブルを JOIN したクエリを行うと2の Split から読む可能性が高いということです。2の Split が同じノードから管理されていれば、特にパフォーマンスに影響はございませんが、その2つの Split が違うノードから管理されていたら、2ノードにアクセスするレイテンシーが追加されるため、確実にパフォーマンスに影響がございます。その際、テーブルをインターリーブ(親子関係)することで、関連するデータを同じ Split に格納することをコントロールすることができます。以下の例で説明します。

上記のテーブルをインターリーブなしで定義します。

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
FirstName  STRING(1024),
LastName   STRING(1024),
SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);
CREATE TABLE Albums (
SingerId     INT64 NOT NULL,
AlbumId      INT64 NOT NULL,
AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);

インターリーブなしでテーブルを定義すると、Cloud Spanner は適当にこの2つのテーブルのデータを Split に分割します。

Split がどの行に発生するかはコントロールできない。

アルバム1と歌手1を JOIN してクエリをしたら、2つの Split から読み込むことになるだろう。

ではインターリーブをすると、どう変わるのでしょう?まずは DDL をご覧ください、最後の1行だけがわかっています。

CREATE TABLE Singers (
SingerId   INT64 NOT NULL,
FirstName  STRING(1024),
LastName   STRING(1024),
SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);
CREATE TABLE Albums (
SingerId     INT64 NOT NULL,
AlbumId      INT64 NOT NULL,
AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId);
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

その結果、物理的のデータレイヤーを確認すると以下のようになります。

インターリーブをしましたので、Split は親レコード間飲みに発生します。

今度はアルバム1と歌手1を JOIN してクエリする1つの Split から読み込むことになり、より早いクエリを行えます。

最後に

Cloud Spanner ではデータはノードに確認されていないため、高可用性を提供しています。だが、データを複数のノードからアクセスすると、パフォーマンスに影響がある可能性もあります。そのために、インターリーブという仕組みで関連するデータを同じ Split にコロケーションして、よりパフォーマンス良く Cloud Spanner をご利用いただくことができます。

本日はハイレベルのアーキテクチャから、各コンポーネントを開設しました。そして、軽くデータモデルのお話もできたかと思います。今後も Cloud Spanner に関する記事を書きたいと思いますので、楽しみにしてください!