セッション

このページでは、クライアント ライブラリの作成時、REST API または RPC API の使用時、Google クライアント ライブラリの使用時のベスト プラクティスなど、Spanner でのセッションの高度なコンセプトについて説明します。

セッションの概要

セッションは、Spanner データベース サービスとの通信チャネルを表します。セッションは、Spanner データベースにデータの読み取り、書き込みまたは変更を行うトランザクションで使用されます。各セッションは 1 つのデータベースに適用されます。

セッションで同時に実行されるトランザクションは 1 つだけです。独立した読み取り、書き込み、クエリは内部でトランザクションを使用するため、トランザクションの制限では 1 トランザクションとしてカウントされます。

セッション プールのパフォーマンス上のメリット

セッションの作成には高額なコストがかかります。データベース オペレーションごとのパフォーマンス コストの発生を避けるため、クライアントは使用可能なセッションのプールであるセッション プールを維持する必要があります。プールは既存のセッションを保存し、リクエストされたときに適切なタイプのセッションを返すとともに、未使用セッションのクリーンアップも処理する必要があります。セッション プールを実装する方法の例については、Go クライアント ライブラリJava クライアント ライブラリなど、いずれかの Spanner クライアント ライブラリのソースコードをご覧ください。

セッションは長時間の使用を目的としているため、セッションをデータベース オペレーションに使用した後は、クライアントがセッションを再利用のためプールに戻す必要があります。

gRPC チャネルの概要

gRPC チャネルは、Spanner クライアントが通信に使用します。1 つの gRPC チャネルは 1 つの TCP 接続とほぼ同等です。1 つの gRPC チャネルで最大 100 件の同時リクエストを処理できます。つまり、アプリケーションが実行する同時リクエスト数を 100 で割った数以上の gRPC チャンネルが必要になります。

Spanner クライアントは、作成時に gRPC チャネルのプールを作成します。

Google クライアント ライブラリを使用する際のベストプラクティス

以降は、Spanner に Google クライアント ライブラリを使用する場合のベスト プラクティスについて説明します。

プール内のセッションと gRPC チャネルの数を構成する

クライアント ライブラリのセッション プールにはデフォルトのセッション数があり、チャネルプールにはデフォルトの gRPC チャネル数があります。ほとんどの場合、両方のデフォルト値で十分です。各プログラミング言語におけるデフォルトの最小セッション数、最大セッション数、およびデフォルトの gRPC チャンネル数は以下の通りです。

C++

MinSessions: 100
MaxSessions: 400
NumChannels: 4

C#

MinSessions: 100
MaxSessions: 400
NumChannels: 4

Go

MinSessions: 100
MaxSessions: 400
NumChannels: 4

Java

MinSessions: 100
MaxSessions: 400
NumChannels: 4

Node.js

Node.js クライアントは複数の gRPC チャネルをサポートしていません。そのため、1 つのクライアントで 100 セッションを超えてセッション プールのサイズを大きくするのではなく、複数のクライアントを作成することをおすすめします。

MinSessions: 25
MaxSessions: 100

PHP

PHP クライアントは、構成可能な数の gRPC チャネルをサポートしていません。

MinSessions: 1
MaxSessions: 500

Python

Python は、セッションの管理に使用できる 4 種類のセッション プールタイプをサポートしています。

Ruby

Ruby クライアントは、複数の gRPC チャネルをサポートしていません。そのため、1 つのクライアントで 100 セッションを超えてセッション プールのサイズを大きくするのではなく、複数のクライアントを作成することをおすすめします。

MinSessions: 10
MaxSessions: 100

アプリケーションが使用するセッションの数は、アプリケーションによって実行される同時トランザクションの数と同じです。1 つのアプリケーション インスタンスで、デフォルトのセッション プールよりも多くのトランザクションが同時に実行されると予想される場合にのみ、デフォルトのセッション プールの設定を変更する必要があります。

同時実行数が多いアプリケーションでは、次のことをおすすめします。

  1. MinSessions を、1 つのクライアントが実行すると予想される同時トランザクション数に設定します。
  2. MaxSessions を、1 つのクライアントが実行できる同時トランザクションの最大数に設定します。
  3. MinSessions=MaxSessions は、予想される同時実行がアプリケーション ライフタイム内にあまり変更されない場合に設定します。これにより、セッション プールがスケールアップまたはスケールダウンされなくなります。セッション プールをスケールアップまたはスケールダウンすると、一部のリソースが消費されます。
  4. [NumChannels] を [MaxSessions / 100] に設定します。1 つの gRPC チャネルで最大 100 件のリクエストを同時に処理できます。高いテール レイテンシ(p95/p99 レイテンシ)が見つかった場合は、gRPC チャネルの輻輳を示している可能性があるため、この値を増やします。

アクティブなセッション数を増やすと、Spanner データベース サービスとクライアント ライブラリで追加のリソースが使用されます。アプリケーションの実際のニーズを超えてセッション数を増やすと、システムのパフォーマンスが低下する可能性があります。

セッション プールの増加とクライアント数を増やす

アプリケーションのセッション プールのサイズは、1 つのアプリケーション インスタンスで実行できる同時実行トランザクションの数を決定します。1 つのアプリケーション インスタンスで処理できる最大同時実行数を超えてセッション プールのサイズを増やすことはおすすめしません。プール内のセッション数を超えるリクエストが急増した場合、セッションが使用可能になるのを待つ間、リクエストはキューに入れられます。

クライアント ライブラリで使用されるリソースは次のとおりです。

  1. 各 gRPC チャネルは、1 つの TCP 接続を使用します。
  2. gRPC の呼び出しごとにスレッドが必要です。クライアント ライブラリで使用されるスレッドの最大数は、アプリケーションが実行する同時クエリの最大数と同じです。これらのスレッドは、アプリケーションが独自のビジネス ロジックに使用するスレッドの上位にあります。

1 つのアプリケーション インスタンスが処理できるスレッドの最大数を超えてセッション プールのサイズを増やすことはおすすめしません。代わりに、アプリケーション インスタンスの数を増やしてください。

書き込みセッションの割合を管理する

特定のクライアント ライブラリでは、Spanner は読み取り / 書き込みトランザクションにセッションの一部を予約しています。この予約部分を書き込みセッションの割合といいます。アプリがすべての読み取りセッションを使い切ると、Spanner は読み取り専用トランザクションであっても読み取り / 書き込みセッションを使用します。読み取り / 書き込みセッションでは spanner.databases.beginOrRollbackReadWriteTransaction が必要です。ユーザーに spanner.databaseReader IAM ロールが割り当てられている場合、呼び出しは失敗し、Spanner は次のエラー メッセージを返します。

generic::permission_denied: Resource %resource% is missing IAM permission:
spanner.databases.beginOrRollbackReadWriteTransaction

クライアント ライブラリで書き込みセッションの割合を維持するには、書き込みセッションの割合を設定します。

C++

C++ セッションはすべて同じです。読み取りセッションまたは読み取り / 書き込み専用セッションはありません。

C#

C# のデフォルトの書き込みセッションの割合は 0.2 です。この割合は、SessionPoolOptions の WriteSessionsFraction フィールドを使用して変更できます。

Go

Go セッションはすべて同じです。読み取りセッションまたは読み取り / 書き込み専用セッションはありません。

Java

Java セッションはすべて同じです。読み取りセッションまたは読み取り / 書き込み専用セッションはありません。

Node.js

Node.js セッションはすべて同じです。読み取りセッションまたは読み取り / 書き込み専用セッションはありません。

PHP

PHP セッションはすべて同じです。読み取りセッションまたは読み取り / 書き込み専用セッションはありません。

Python

Python では、読み取りセッションと読み取り / 書き込みセッションの管理に使用できる 4 種類のセッション プールタイプをサポートしています。

Ruby

Ruby のデフォルトの書き込みセッションの割合は 0.3 です。この割合は、クライアントの初期化メソッドを使用して変更できます。

クライアント ライブラリを作成する場合、または REST/RPC を使用する場合のベスト プラクティス

以降は、Spanner のクライアント ライブラリでセッションを実装する場合と、REST API または RPC API でセッションを使用する場合のベスト プラクティスについて説明します。

これらのベスト プラクティスは、クライアント ライブラリを開発している場合、または REST/RPC API を使用している場合にのみ適用されます。Spanner 用の Google クライアント ライブラリのいずれかを使用する場合は、Google クライアント ライブラリを使用する際のベスト プラクティスをご覧ください。

セッション プールの作成とサイズ設定

クライアント プロセスに最適なセッション プール サイズを決定するには、下限は予想される同時実行トランザクションの数に設定し、上限は 100 などの初期テスト数に設定します。上限が十分でない場合は、数を増やします。アクティブなセッション数を増やすと、Spanner データベース サービスで追加のリソースが使用されます。未使用セッションをクリーンアップしないと、パフォーマンスが低下します。RPC API を使用しているユーザーの場合は、gRPC チャネルごとのセッション数を 100 未満にすることをおすすめします。

削除されたセッションの処理

セッションを削除するには、次の 3 つの方法があります

  • クライアントがセッションを削除する。
  • Spanner データベース サービスが、アイドル状態が 1 時間を超えたセッションを削除する。
  • セッションが 28 日を越えた場合、Spanner データベースサービスによってセッションが削除されることがある。

削除されたセッションを使用しようとすると、NOT_FOUND になります。このエラーが発生した場合は、新しいセッションを作成して使用します。新しいセッションをプールに追加して、削除済みのセッションをプールから削除します。

アイドル セッションを維持する

Spanner データベース サービスは未使用のセッションを削除できます。アイドル セッションを維持する必要がある場合(データベースの使用が短期間で増加することが予想される場合など)、セッションが削除されないように設定できます。セッションを維持するには、SQL クエリ SELECT 1 など、低コストなオペレーションを実行します。近い将来使用する予定がないアイドル セッションは Spanner に削除させ、必要になったときに新しいセッションを作成します。

たとえば、データベースの負荷が定期的に高くなる場合にセッションを維持します。 毎日午前 9 時から午後 6 時までデータベースの負荷が高くなる場合、この期間はアイドル セッションを残し、ピーク時の対応に備えます。午後 6 時以降は、Spanner にアイドル セッションを削除させます。毎日、午前 9 時前に、予想される需要に合わせて新しいセッションを作成します。

また、Spanner を使用するアプリケーションがあり、接続のオーバーヘッドを回避する必要がある場合にもセッションを維持します。セッションを維持しておくと、接続のオーバーヘッドを回避できます。

クライアント ライブラリのユーザーにセッションの詳細を隠す

クライアント ライブラリを作成する場合、クライアント ライブラリのユーザーにセッションの詳細を公開しないでください。クライアントがセッションの作成や維持などの複雑な処理を行うことなく、データベースを呼び出せるようにする必要があります。クライアント ライブラリのユーザーにセッションの詳細を隠すクライアント ライブラリについては、Java 用 Spanner クライアント ライブラリをご覧ください。

べき等でない書き込みトランザクションのエラーを処理する

リプレイ保護のない書き込みトランザクションが複数回ミューテーションに適用される場合があります。ミューテーションがべき等でない場合、ミューテーションが複数回適用されると、エラーが発生します。たとえば、書き込み前に行が存在していなくても、ALREADY_EXISTS エラーで挿入が失敗することがあります。バックエンド サーバーがミューテーションを commit していても、クライアントに接続できないと、この問題が発生します。この場合、ミューテーションが再試行され、ALREADY_EXISTS エラーが発生します。

独自のクライアント ライブラリの実装または REST API の使用時にこの問題を回避するには、次の方法が考えられます。

  • 書き込みをべき等にする。
  • リプレイ保護のある書き込みを使用する。
  • upsert ロジックを実行するメソッドを実装する。新規の場合に挿入を行い、既存の場合に更新を行う。
  • クライアントの代わりにエラーを処理する。

安定した接続を維持する

最高のパフォーマンスを得るには、セッションをホストするために使用する接続が安定している必要があります。セッションをホストする接続が変更されると、セッション メタデータの更新中に Spanner がセッションでアクティブなトランザクションを中止し、データベースに余分な負荷がかかる場合があります。いくつかの接続が散発的に変更されても問題ありませんが、多数の接続が同時に変更される状況は避ける必要があります。クライアントと Spanner 間でプロキシを使用する場合は、セッションごとに接続の安定性を維持する必要があります。

アクティブなセッションをモニタリングする

ListSessions コマンドを使用すると、REST API または RPC API により、コマンドラインからデータベース内のアクティブなセッションをモニタリングできます。ListSessions は特定のデータベースのアクティブなセッションを表示します。これは、セッション リークの原因を特定する必要がある場合に便利です。(セッション リークは、セッションは作成中だが再利用のためセッション プールに戻されていないというインシデントです)。

ListSessions を使用すると、セッションが作成されたときやセッションが最後に使用されたときなど、アクティブなセッションに関するメタデータを表示できます。このデータを分析することで、セッションのトラブルシューティングを行う際に、正しい方向へ向かうことができます。ほとんどのアクティブなセッションに最新の approximate_last_use_time がない場合は、アプリケーションでセッションが正しく再利用されていないことを示している可能性があります。approximate_last_use_time フィールドの詳細については、RPC API リファレンスをご覧ください。

ListSessions の使用方法の詳細については、REST API リファレンスRPC API リファレンスgcloud コマンドライン ツール リファレンスをご覧ください。

セッション リークの自動クリーンアップ

セッション プール内のすべてのセッションを使用すると、新しいトランザクションは、セッションがプールに返されるまで待機します。セッションが作成されたものの、再利用のためにセッション プールに戻されなかった場合、これはセッション リークと呼ばれます。セッション リークが発生すると、開いているセッションを待機しているトランザクションが無期限に停止し、アプリケーションをブロックします。セッションのリークは、トランザクションが長時間実行されていて、commit されていない問題が発生しているトランザクションによって発生することがよくあります。

このような非アクティブなトランザクションを自動的に解決するようにセッション プールを設定できます。クライアント ライブラリで非アクティブな移行を自動的に解決できるようになると、セッション リークの原因となる可能性のあるトランザクションが特定され、セッション プールから削除されて、新しいセッションに置き換えられます。

ロギングによって、問題のあるトランザクションを特定することもできます。ロギングが有効になっていて、セッション プールの 95% 以上が使用された場合、デフォルトで警告ログが共有されます。セッションの使用状況が 95% を超える場合は、セッション プールで許可される最大セッションを増やす必要があるか、セッション リークが発生している可能性があります。警告ログには、予想よりも長く実行されるトランザクションのスタック トレースが含まれており、セッション プールの使用率が高い原因を特定できます。警告ログは、ログ エクスポータの構成に応じて push されます。

クライアント ライブラリを有効にして、非アクティブなトランザクションを自動的に解決する

クライアント ライブラリで警告ログを送信して自動的に非アクティブなトランザクションを解決することも、クライアント ライブラリで警告ログの受信のみを有効にすることもできます。

Java

警告ログを受信し、非アクティブなトランザクションを削除するには、setWarnAndCloseIfInactiveTransactions を使用します。

 final SessionPoolOptions sessionPoolOptions = SessionPoolOptions.newBuilder().setWarnAndCloseIfInactiveTransactions().build()

 final Spanner spanner =
         SpannerOptions.newBuilder()
             .setSessionPoolOption(sessionPoolOptions)
             .build()
             .getService();
 final DatabaseClient client = spanner.getDatabaseClient(databaseId);

警告ログのみを受信するには、setWarnIfInactiveTransactions を使用します。

 final SessionPoolOptions sessionPoolOptions = SessionPoolOptions.newBuilder().setWarnIfInactiveTransactions().build()

 final Spanner spanner =
         SpannerOptions.newBuilder()
             .setSessionPoolOption(sessionPoolOptions)
             .build()
             .getService();
 final DatabaseClient client = spanner.getDatabaseClient(databaseId);

Go

警告ログを受信し、非アクティブなトランザクションを削除するには、InactiveTransactionRemovalOptions を指定して SessionPoolConfig を使用します。

 client, err := spanner.NewClientWithConfig(
     ctx, database, spanner.ClientConfig{SessionPoolConfig: spanner.SessionPoolConfig{
         InactiveTransactionRemovalOptions: spanner.InactiveTransactionRemovalOptions{
         ActionOnInactiveTransaction: spanner.WarnAndClose,
         }
     }},
 )
 if err != nil {
     return err
 }
 defer client.Close()

警告ログの受信のみを行うには、customLogger を使用します。

 customLogger := log.New(os.Stdout, "spanner-client: ", log.Lshortfile)
 // Create a logger instance using the golang log package
 cfg := spanner.ClientConfig{
         Logger: customLogger,
     }
 client, err := spanner.NewClientWithConfig(ctx, db, cfg)