詳解 google-cloud-go/spanner — セッション管理編

Yuki Furuyama
google-cloud-jp
Published in
12 min readMay 14, 2019

はじめに

Cloud Spanner では各言語ごとにライブラリが提供されており、アプリケーションはそれを使うことで非常に簡単にデータベースにアクセスすることができます。しかし Cloud Spanner の性能を最大限引き出すためには、クライアント側の設定値をチューニングしたりなど、クライアントライブラリの挙動を知っておくことが不可欠です。そこで本記事では Go 言語のクライアントライブラリ (google-cloud-go) を例に、クライアントライブラリがどのような処理をしているかをじっくりと紐解いてみたいと思います。

全てを一度にカバーすると結構なボリュームになってしまうので、まずは本記事で Spanner の「セッション管理」の部分について説明します。本記事を通して ClientConfigSessionPoolConfig の各値の意味がわかるようになるのが目標です。

尚この記事は google-cloud-go の v0.38.0 時点のものを参照して記述しています。実装に踏み込んだ話もしており、将来に渡って処理内容が変わらない保証はないため、気になる方は最新のバージョンを参照するようにしてください。

ではセッション管理がどのように行われているか詳しく見ていきましょう。

セッションとは

まずそもそも Cloud Spanner のセッションとは何なのでしょうか。

Cloud Spanner ではデータベースに対してクエリを実行したりトランザクションでレコードを更新したりする際には、必ずセッションというものの上で実行されます。これは Web のセッションと似た概念であり、サーバ側で現在のクライアントの状態を保持しているものです。トランザクションを実行する場合は Begin, Query, Commit と複数の RPC が順序立てて実行されることからも、それぞれの RPC を紐付けるためのセッションという概念が必要になることがわかると思います。

このセッションは gRPC の Channel の上に作られます。 gRPC の Channel と Cloud Spanner の Session の関係を図示すると以下のようになります。

gRPC Channel と Cloud Spanner Session の関係

アプリケーションと Cloud Spanner サーバは gRPC の Channel を通して接続されます。この Channel の本数は ClientConfig.NumChannels によって変更可能です。

そして Channel の上に Cloud Spanner の Session が多重化されて作成されます。どの Channel を使うかは Session の作成時にラウンドロビンに選択され、Session が削除されるまで選ばれた Channel を使い続けます。そのため基本的には、それぞれの Channel には均等な数の Session が乗ることになります。

セッションプール

このセッションですが、接続コストがそれなりに高いため、SQL を実行するたびにセッションを新たに作っていては効率が良くありません。そこでセッションプール (sessionPool 構造体)と呼ばれる複数のセッションを管理できるプールが用意されており、一度作ったセッションをそこに入れておくことで何度でも再利用できるようになっています。

Read / Write セッションプール

セッションプールは Read と Write 用の2つに別れています。両者で何が違うかと言うと、Write 用のセッションプールに含まれるセッションにはあらかじめ BeginTransaction が実行されており、すぐにでも Read-Write Transaction が開始できるようになっているという点です。Read-Write Transaction の BeginTransaction はあらかじめ実行しておいても問題ないため、のちに実行されるトランザクションのレイテンシを少しだけ縮めることができます。この最適化はデメリットもあるため後ほど説明します。

Read 用 (Read-Only Transaction) のセッションプールは idleList で、Write 用 (Read-Write Transaction 用) のセッションプールは idleWriteList という名称で管理されています。

但し Read の場合は必ずしも idleList を使わないといけない訳ではありません。idleList にアイドル状態のセッションがない場合は、idleWriteList からセッションが取得されます。逆も然りで、Write のためのセッションが idleWriteList にない場合は idleList からセッションが取得されるようになっています。

セッションのライフサイクル

次にセッションがどのように生まれ、使われ、削除されるか、という一連のライフサイクルを見てみましょう。以下の図はセッションプールとセッションのライフサイクルを図示したものです。

セッションのライフサイクル

図の中の青いボールのようなものがセッションです。ちなみにこの図ではセッションプール内にあるセッション同士に順序関係がないように見えますが、実際は idleList, idleWriteList とも FIFO Queue になっています。

ではライフサイクルを一つずつ見ていきましょう。

1. セッションの作成

セッションが作成されるタイミングは2パターンあります。

  1. Session Pool Maintainer による作成
  2. Read / Write Ops の呼び出しに伴う作成

まず1番目のパターンですが、Spanner Client を作成すると Session Pool Maintainer というものが goroutine で起動されます。この Maintainer はセッションプールに作成してあるセッションの数を調整するものであり、最小限作っておくべきセッションの数に達していなければ新規にセッションを作成し、逆にアイドル状態のものが多すぎればセッションを削除する、という動作をします。

セッションプールに最小限作っておくべきセッション数は SessionPoolConfig.MinOpened で指定でき、最大限許容できるアイドル状態のセッション数は SessionPoolConfig.MaxIdle で指定できます。

次に2番目のパターンです。クライアントが Read / Write 系の処理を実行した際に、全てのセッションが既に使われていてセッションプールが空の場合は、その場で新規にセッションを作ろうとします。ただし既存の作成済みセッション数が SessionPoolConfig.MaxOpened に既に達している場合は、セッションを作るのは諦め、セッションプールにセッションが戻されるまで待ちが発生します。これはパフォーマンス低下に繋がるので防ぎたいところです。

待ちが発生した場合、Go ライブラリ内で OpenCensus の Trace ログが出力されるので、そのログを見かけた場合は MaxOpened の値を上げるのがいいでしょう。また MaxOpened を上げる場合は、NumChannels の値も同時に上げるべきです。なぜなら1つの gRPC Channel につき最大 100 個までのセッションしか保持できないためです (参照)

2. セッションの使用

Cloud Spanner に対して Read や Write を実行したい場合はセッションプールからセッションを取り出して使います。セッションの取り出し順番は FIFO です。

ここで全てのセッションが既に使われていて、取り出せるセッションがない場合は、先程述べたようにセッションが新たに作られます。

3. セッションの返却

Cloud Spanner への API 呼び出しが終わったらセッションをプールに返却します。Write の呼び出しで使ったセッションだとしても、idleList 側のプールに返却されます。

4. セッションのプール間の移動

前述したように、セッションプールは Read 用の idleList と Write 用の idleWriteList の2つに別れています。セッションは初期状態としては idleList に入れられるのですが、Write 用に必要だと判断されると BeginTransaction を実行した上で idleWriteList に移動されます。

この移動は後述する Health Check Worker によって定期的にバックグラウンドで実行される処理です。

セッションをどのくらい idleWriteList に移動すべきかは SessionPoolConfig.WriteSessions の割合を見て判断されます。そのためそれぞれのアプリケーションの Read/Write の比率に応じて、WriteSessions の値を決定するのがいいと思います。ただし、BeginTransaction したセッションが長期間アイドル状態のままだと、いざ使おうとした時にサーバ側でアボートされることもあるため、リトライが発生し逆にレイテンシが上がってしまう可能性があります。Go言語ではデフォルト値は0なので、様子を見つつ少しずつ上げていくのがいいでしょう。

5. セッションの削除

SessionPoolConfig.MaxIdle 以上にアイドルなセッションが存在した場合や、Unhealthy なセッションがあった場合、それらのセッションは削除されます。

アイドルなセッションの削除は、前述の Session Pool Maintainer によって1分置きに実行されます。これがあるおかげで、突発的に増えてしまった分のセッションを減らせるようになっています。

Health Check Worker

次に Health Check Worker というものを見てみたいと思います。

Health Check Worker はその名の通り、セッションが生きているかどうかをチェックする Worker であり、Session Pool Maintainaer と共に goroutine で起動されるものです。 ヘルスチェックする間隔は SessionPoolConfig.HealthCheckInterval で設定でき、デフォルトは5分間隔となっています。実際のチェックは GetSession メソッドが呼ばれ、Cloud Spanner サーバ側でまだセッションが生きているかどうかの確認が行われます。生きていない場合はそのセッションはクライアント側で破棄され、セッションプールからも除外されます。

Health Check Worker はヘルスチェックとは別に、Write 用のセッションを準備するという処理も担っています。前述した、BeginTransaction を実行して idleList → idleWriteList に移動するという処理のことです。

この処理の頻度は SessionPoolConfig.HealthCheckWorkers の設定で Worker 数を変えることで、間接的に変更することができます。デフォルトではこの数は10となっており、およそ 2000 txn/sec を捌ける想定だとライブラリでは記述されています。SessionPoolConfig.WriteSessions と合わせて確認するのがいいと思います。

SessionPoolConfig の設定例

ここまでいくつか SessionPoolConfig の設定値について触れましたが、ここで一度設定値をいじった時の挙動を見てみましょう。ここでは試しに以下の設定にします。

  • MinOpened: 5
  • MaxOpened: 30
  • MaxIdle: 10
  • MaxBurst: 30

MaxBurst はまだ説明していませんでしたが、セッションを一度に大量に作らなければいけなくなった時に、どのくらいの並列数でセッションを作れるかを指定する値です。

上記の設定値を与えた上で、一気に100個のトランザクションを並列実行した時にどのようにセッション数が上下するか見てみます。サンプルコードはこちらです。

このサンプルコードを実行すると以下のようなログが出力されます (表示上いくつかのログは省いています)。

サンプルコードの実行例

ポイントとなるところを見ていきましょう。

① では Session Pool Maintainer によって MinOpened: 5 までバックグラウンドでセッション数が増えていってます。

次に ② でトランザクションが100個並列に実行されますが、セッションプールにある5個のセッションだけでは全てのトランザクションを賄うには足りなく、MaxOpened: 30 までその場でセッションが作られる様子を表しています。MaxBurst: 30 に設定しているので 5 → 30 に一気に増えているのがわかります。また OpenCensus のログを出すようにしているので、”Waiting for read-write session …” とアイドル状態のセッションが足りない状況に陥っていることもわかると思います。

そして ③ の段階では全てのセッションがアイドル状態となっており、Session Pool Maintainer によって MaxIdle: 10 までセッション数が減らされていることがわかります。

まとめ

今回は google-cloud-go のセッション管理の一連の流れを見てみました。セッション管理だけでもクライアントライブラリの中で様々な処理が行われており、SessionPoolConfig の各設定値が密接に使われているのがわかると思います。

今回は Go のクライアントライブラリを見ましたが、他のクライアントライブラリでも似たようなセッションプールの設定値があると思いますので、本記事を参考にして見てみてください。

次回は google-cloud-go/spanner のトランザクション管理について詳しく見ていく予定です。お楽しみに!

--

--

Yuki Furuyama
google-cloud-jp

Technical Solutions Engineer @Google Cloud. Opinions are my own and not the views of my employer.