Sessions

This page describes the advanced concept of sessions in Spanner, including best practices for sessions when creating a client library, using the REST or RPC APIs, or using the Google client libraries.

Overview of Sessions

A session represents a communication channel with the Spanner database service. A session is used to perform transactions that read, write, or modify data in a Spanner database. Each session applies to a single database.

Sessions can execute only one transaction at a time. Standalone reads, writes, and queries use a transaction internally, and count toward the one transaction limit.

Performance benefits of a session pool

Creating a session is expensive. To avoid the performance cost each time a database operation is made, clients should keep a session pool, which is a pool of available sessions that are ready to use. The pool should store existing sessions and return the appropriate type of session when requested, as well as handle cleanup of unused sessions. For an example of how to implement a session pool, see the source code for one of the Spanner client libraries, such as the Go client library or the Java client library.

Sessions are intended to be long-lived, so after a session is used for a database operation, the client should return the session to the pool for reuse.

Overview of gRPC channels

gRPC channels are used by the Spanner client for communication. One gRPC channel is roughly equivalent to a TCP connection. One gRPC channel can handle up to 100 concurrent requests. This means that an application will need at least as many gRPC channels as the number of concurrent requests the application will execute, divided by 100.

The Spanner client will create a pool of gRPC channels when it is created.

Best practices when using Google client libraries

The following describes best practices when using the Google client libraries for Spanner.

Configure the number of sessions and gRPC channels in the pools

The client libraries have a default number of sessions in the session pool and a default number of gRPC channels in the channel pool. Both defaults are adequate for most cases. The following are the default minimum and maximum sessions and the default number of gRPC channels for each programming language.

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

The Node.js client does not support multiple gRPC channels. It is therefore recommended to create multiple clients instead of increasing the size of the session pool beyond 100 sessions for a single client.

MinSessions: 25
MaxSessions: 100

PHP

The PHP client does not support a configurable number of gRPC channels.

MinSessions: 1
MaxSessions: 500

Python

Python supports four different session pool types that you can use to manage sessions.

Ruby

The Ruby client does not support multiple gRPC channels. It is therefore recommended to create multiple clients instead of increasing the size of the session pool beyond 100 sessions for a single client.

MinSessions: 10
MaxSessions: 100

The number of sessions that your application uses is equal to the number of concurrent transactions that your application executes. You should modify the default session pool settings only if you expect a single application instance to execute more concurrent transactions than the default session pool can handle.

For high concurrency applications the following is recommended:

  1. Set MinSessions to the expected number of concurrent transactions that a single client will execute.
  2. Set MaxSessions to the maximum number of concurrent transactions that a single client can execute.
  3. Set MinSessions=MaxSessions if the expected concurrency does not change much during the application lifetime. This prevents the session pool from scaling up or down. Scaling the session pool up or down also consumes some resources.
  4. Set NumChannels to MaxSessions / 100. One gRPC channel can handle up to 100 requests concurrently. Increase this value if you observe a high tail latency (p95/p99 latency), because this could be an indication of gRPC channel congestion.

Increasing the number of active sessions uses additional resources on the Spanner database service and in the client library. Increasing the number of sessions beyond the actual need of the application could degrade the performance of your system.

Increase the session pool versus increasing the number of clients

The session pool size for an application determines how many concurrent transactions a single application instance can execute. Increasing the session pool size beyond the maximum concurrency that a single application instance can handle is not recommended. If the application receives a burst of requests that goes beyond the number of sessions in the pool, the requests are queued while waiting for a session to become available.

The resources that are consumed by the client library are the following:

  1. Each gRPC channel uses one TCP connection.
  2. Each gRPC invocation requires a thread. The maximum number of threads that is used by the client library is equal to the maximum number of concurrent queries that the application executes. These threads come on top of any threads that the application uses for its own business logic.

Increasing the size of the session pool beyond the maximum number of threads that a single application instance can handle is not recommended. Instead, increase the number of application instances.

Manage the write-sessions fraction

For some client libraries, Spanner reserves a portion of the sessions for read-write transactions, called the write-sessions fraction. If your app uses up all the read sessions, then Spanner uses the read-write sessions, even for read-only transactions. Read-write sessions require spanner.databases.beginOrRollbackReadWriteTransaction. If the user is in the spanner.databaseReader IAM role, then the call fails and Spanner returns this error message:

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

For the client libraries that maintain a write-sessions fraction, you can set the write-sessions fraction.

C++

All C++ sessions are the same. There are no read or read-write only sessions.

C#

The default write-sessions fraction for C# is 0.2. You can change the fraction using the WriteSessionsFraction field of SessionPoolOptions.

Go

All Go sessions are the same. There are no read or read-write only sessions.

Java

All Java sessions are the same. There are no read or read-write only sessions.

Node.js

All Node.js sessions are the same. There are no read or read-write only sessions.

PHP

All PHP sessions are the same. There are no read or read-write only sessions.

Python

Python supports four different session pool types that you can use to manage read and read-write sessions.

Ruby

The default write-sessions fraction for Ruby is 0.3. You can change the fraction using the client initialize method.

Best practices when creating a client library or using REST/RPC

The following describes best practices for implementing sessions in a client library for Spanner, or for using sessions with the REST or RPC APIs.

These best practices only apply if you are developing a client library, or if you are using REST/RPC APIs. If you are using one of the Google client libraries for Spanner, refer to Best practices when using Google client libraries.

Create and size the session pool

To determine an optimal size of the session pool for a client process, set the lower bound to the number of expected concurrent transactions, and set the upper bound to an initial test number, such as 100. If the upper bound is not adequate, increase it. Increasing the number of active sessions uses additional resources on the Spanner database service, so failing to clean up unused sessions can degrade performance. For users working with the RPC API, we recommend having no more than 100 sessions per gRPC channel.

Handle deleted sessions

There are three ways to delete a session:

  • A client can delete a session.
  • The Spanner database service can delete a session when the session is idle for more than 1 hour.
  • The Spanner database service may delete a session if the session is more than 28 days old.

Attempts to use a deleted session result in NOT_FOUND. If you encounter this error, create and use a new session, add the new session to the pool, and remove the deleted session from the pool.

Keep an idle session alive

The Spanner database service reserves the right to drop an unused session. If you definitely need to keep an idle session alive, for example, if a significant near-term increase in database use is expected, then you can prevent the session from being dropped. Perform an inexpensive operation such as executing the SQL query SELECT 1 to keep the session alive. If you have an idle session that is not needed for near-term use, let Spanner drop the session, and then create a new session the next time a session is needed.

One scenario for keeping sessions alive is to handle regular peak demand on the database. If heavy database use occurs daily from 9:00 AM to 6:00 PM, you should keep some idle sessions alive during that time, since they are likely required for the peak usage. After 6:00 PM, you can let Spanner drop idle sessions. Prior to 9:00 AM each day, create some new sessions so they will be ready for the expected demand.

Another scenario is if you have an application that uses Spanner but must avoid the connection overhead when it does. You can keep a set of sessions alive to avoid the connection overhead.

Hide session details from the client library user

If you are creating a client library, don't expose sessions to the client library consumer. Provide the ability for the client to make database calls without the complexity of creating and maintaining sessions. For an example of a client library that hides the session details from the client library consumer, see the Spanner client library for Java.

Handle errors for write transactions that are not idempotent

Write transactions without replay protection may apply mutations more than once. If a mutation is not idempotent, a mutation that is applied more than once could result in a failure. For example, an insert may fail with ALREADY_EXISTS even though the row did not exist prior to the write attempt. This could occur if the backend server committed the mutation but was unable to communicate the success to the client. In that event, the mutation could be retried, resulting in the ALREADY_EXISTS failure.

Here are possible ways to address this scenario when you implement your own client library or use the REST API:

  • Structure your writes to be idempotent.
  • Use writes with replay protection.
  • Implement a method that performs "upsert" logic: insert if new or update if exists.
  • Handle the error on behalf of the client.

Maintain stable connections

For best performance, the connection that you use to host a session should remain stable. When the connection that hosts a session changes, Spanner might abort the active transaction on the session and cause a small amount of extra load on your database while it updates the session metadata. It is OK if a few connections change sporadically, but you should avoid situations that would change a large number of connections at the same time. If you use a proxy between the client and Spanner, you should maintain connection stability for each session.

Monitor active sessions

You can use the ListSessions command to monitor active sessions in your database from the command line, with the REST API, or with the RPC API. ListSessions shows the active sessions for a given database. This is useful if you need to find the cause of a session leak. (A session leak is an incident where sessions are being created but not returned to a session pool for reuse.)

ListSessions lets you view metadata about your active sessions, including when a session was created and when a session was last used. Analyzing this data will point you in the right direction when troubleshooting sessions. If most active sessions don't have a recent approximate_last_use_time, this could indicate that sessions aren't being reused properly by your application. See the RPC API reference for more information about the approximate_last_use_time field.

See the REST API reference, the RPC API reference, or the gcloud command-line tool reference for more information on using ListSessions.

Automatic cleanup of session leaks

When you use all the sessions in your session pool, each new transaction waits until a session is returned to the pool. When sessions are created but not returned to the session pool for reuse, this is called a session leak. When there is a session leak, transactions waiting for an open session get stuck indefinitely and block the application. Session leaks are often caused by problematic transactions that are running for an extremely long time and aren't committed.

You can setup your session pool to automatically resolve these inactive transactions. When you enable your client library to automatically resolve inactive transition, it identifies problematic transactions that might cause a session leak, removes them from the session pool, and replaces them with a new session.

Logging can also help identify these problematic transactions. If logging is enabled, warning logs are shared by default when more than 95% of your session pool is in use. If your session usage is greater than 95%, then you either need to increase the max sessions allowed in your session pool, or you might have a session leak. Warning logs contain stack traces of transactions that run for longer than expected and can help identify the cause of high session pool utilization. Warning logs are pushed depending on your log exporter configuration.

Enable client library to automatically resolve inactive transactions

You can either enable client library to send warning logs and automatically resolve inactive transactions, or enable client library to only receive warning logs.

Java

To receive warning logs and remove inactive transactions, use setWarnAndCloseIfInactiveTransactions.

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

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

To only receive warning logs, use setWarnIfInactiveTransactions.

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

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

Go

To receive warning logs and remove inactive transactions, use SessionPoolConfig with InactiveTransactionRemovalOptions.

 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()

To only receive warning logs, use 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)