リクエスト タグとトランザクション タグによるトラブルシューティング

Spanner には、クエリ、読み取り、トランザクションに関する分析情報の取得に役立つ、一連の組み込み統計テーブルが用意されています。統計情報とアプリケーション コードを関連付けてトラブルシューティングの改善を図るため、アプリケーション コードで Spanner の読み取り、クエリ、トランザクション オペレーションにタグ(自由形式の文字列)を追加できます。これらのタグは、統計情報テーブルに入力され、タグに基づいて関連付けと検索を行えます。

Spanner では、2 種類のタグ(リクエスト タグと トランザクション タグ)がサポートされています。その名前が示すように、トランザクションにはトランザクション タグを、個々のクエリ API や読み取り API にはリクエストタグを追加できます。トランザクション タグはトランザクション スコープで設定でき、個々のリクエストタグはトランザクション内の該当する API リクエストごとに設定できます。アプリケーション コードで設定されるリクエストタグとトランザクション タグは、次の統計情報テーブルの列に入力されます。

統計情報テーブル 統計情報テーブルに入力されるタグの種類
TopN クエリ統計情報 リクエストタグ
TopN 読み取り統計情報 リクエストタグ
TopN トランザクション統計情報 トランザクション タグ
TopN ロック統計情報 トランザクション タグ

リクエストタグ

クエリや読み取りリクエストには、オプションのリクエストタグを追加できます。Spanner は、リクエストタグ別に統計情報をグループ化します。これは、クエリ統計情報読み取り統計情報両方の REQUEST_TAG フィールドで確認できます。

リクエストタグを使用する場合

リクエストタグの使用によりメリットを得られるシナリオの例を次に示します。

  • 問題のあるクエリや読み取りの発生源を見つける: Spanner では、組み込み統計テーブル内の読み取りとクエリに関する統計情報が収集されます。統計情報テーブルで時間がかかるクエリや CPU 使用率の高い読み取りが見つかったときに、すでにタグが割り当てられていれば、タグの情報に基づいて、こうしたオペレーションを呼び出しているソース(アプリケーション / マイクロサービス)を特定できます。
  • 統計情報テーブルの読み取りやクエリを特定する: リクエストタグを割り当てると、関心のあるタグに基づいて、統計情報テーブルの行をフィルタリングできます。
  • 特定のアプリケーションやマイクロサービスからのクエリが遅いかどうかを判断する: リクエストタグを使用すると、特定のアプリケーションやマイクロサービスからのクエリがレイテンシが高いかどうかを見分けるのに役立ちます。
  • 一連の読み取りやクエリに関する統計情報をグループ化する: リクエストタグを使用して、一連の類似した読み取りやクエリのパフォーマンスを追跡、比較、報告できます。たとえば、同じアクセス パターンを持つテーブル / 一連のテーブルに複数のクエリがアクセスしている場合は、そうしたクエリのすべてに同じタグを追加して、それらを一緒に追跡することを検討できます。

リクエストタグを割り当てる方法

次のサンプルでは、Spanner クライアント ライブラリを使用してリクエストタグを設定する方法を示します。

C++

void SetRequestTag(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
  using RowType = std::tuple<std::int64_t, std::int64_t, std::string>;

  auto opts = google::cloud::Options{}.set<spanner::RequestTagOption>(
      "app=concert,env=dev,action=select");
  auto rows = client.ExecuteQuery(std::move(select), std::move(opts));
  for (auto& row : spanner::StreamOf<RowType>(rows)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
}

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class RequestTagAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> RequestTagAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        using var cmd = connection.CreateSelectCommand(
            $"SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
        // Sets the request tag to "app=concert,env=dev,action=select".
        // This request tag will only be set on this request.
        cmd.Tag = "app=concert,env=dev,action=select";

        var albums = new List<Album>();
        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            var album = new Album
            {
                SingerId = reader.GetFieldValue<int>("SingerId"),
                AlbumId = reader.GetFieldValue<int>("AlbumId"),
                AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
            };
            albums.Add(album);
            Console.WriteLine($"SingerId: {album.SingerId}, AlbumId: {album.AlbumId}, AlbumTitle: {album.AlbumTitle}");
        }
        return albums;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

// queryWithTag reads from a database with request tag set
func queryWithTag(w io.Writer, db string) error {
	// db = `projects/<project>/instances/<instance-id>/database/<database-id>`
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := client.Single().QueryWithOptions(ctx, stmt, spanner.QueryOptions{RequestTag: "app=concert,env=dev,action=select"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}
}

Java

static void setRequestTag(DatabaseClient databaseClient) {
  // Sets the request tag to "app=concert,env=dev,action=select".
  // This request tag will only be set on this request.
  try (ResultSet resultSet = databaseClient
      .singleUse()
      .executeQuery(
          Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"),
          Options.tag("app=concert,env=dev,action=select"))) {
    while (resultSet.next()) {
      System.out.printf(
          "SingerId: %d, AlbumId: %d, AlbumTitle: %s\n",
          resultSet.getLong(0),
          resultSet.getLong(1),
          resultSet.getString(2));
    }
  }
}

Node.js

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

async function queryTags() {
  // Gets a reference to a Cloud Spanner instance and database.
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  // Execute a query with a request tag.
  const [albums] = await database.run({
    sql: 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums',
    requestOptions: {requestTag: 'app=concert,env=dev,action=select'},
    json: true,
  });
  albums.forEach(album => {
    console.log(
      `SingerId: ${album.SingerId}, AlbumId: ${album.AlbumId}, AlbumTitle: ${album.AlbumTitle}`
    );
  });
  await database.close();
}
queryTags();

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Executes a read with a request tag.
 * Example:
 * ```
 * spanner_set_request_tag($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function set_request_tag(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $snapshot = $database->snapshot();
    $results = $snapshot->execute(
        'SELECT SingerId, AlbumId, AlbumTitle FROM Albums',
        [
            'requestOptions' => [
                'requestTag' => 'app=concert,env=dev,action=select'
            ]
        ]
    );
    foreach ($results as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

with database.snapshot() as snapshot:
    results = snapshot.execute_sql(
        "SELECT SingerId, AlbumId, AlbumTitle FROM Albums",
        request_options={"request_tag": "app=concert,env=dev,action=select"},
    )

    for row in results:
        print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client = spanner.client instance_id, database_id

client.execute(
  "SELECT SingerId, AlbumId, MarketingBudget FROM Albums",
  request_options: { tag: "app=concert,env=dev,action=select" }
).rows.each do |row|
  puts "#{row[:SingerId]} #{row[:AlbumId]} #{row[:MarketingBudget]}"
end

統計情報テーブルでリクエストタグを確認する方法

次のクエリでは、10 分間隔のクエリの統計情報が返されます。

SELECT t.text,
       t.request_tag,
       t.execution_count,
       t.avg_latency_seconds,
       t.avg_rows,
       t.avg_bytes
FROM SPANNER_SYS.QUERY_STATS_TOP_10MINUTE AS t
LIMIT 3;

クエリから返される結果の例として、以下のデータを見てみましょう。

テキスト request_tag execution_count avg_latency_seconds avg_rows avg_bytes
SELECT SingerId, AlbumId, AlbumTitle FROM Albums app=concert,env=dev,action=select 212 0.025 21 2365
select * from orders; app=catalogsearch,env=dev,action=list 55 0.02 16 33.35
SELECT SingerId, FirstName, LastName FROM Singers; [empty string] 154 0.048 42 486.33

この結果の表から、クエリに REQUEST_TAG を割り当てた場合は、統計テーブルに値が入力されることがわかります。リクエストタグが割り当てられていない場合は、空の文字列として表示されます。

タグ付きクエリの場合、統計情報はタグごとに集計されます(リクエストタグ app=concert,env=dev,action=select の平均レイテンシは 0.025 秒など)。タグが割り当てられていない場合、統計情報はクエリごとに集計されます(3 行目のクエリの平均レイテンシは 0.048 秒など)。

トランザクション タグ

オプションのトランザクション タグは、個々のトランザクションに追加できます。Spanner は、トランザクション タグ別に統計情報をグループ化します。これは、トランザクションの統計情報テーブルの TRANSACTION_TAG フィールドで確認できます。

トランザクション タグを使用する場合

トランザクション タグの使用によりメリットを得られるシナリオの例を次に示します。

  • 問題のあるトランザクションの発生源を見つける: Spanner では、トランザクションの統計情報テーブル内の読み取り / 書き込みトランザクションに関する統計情報が収集されます。トランザクションの統計情報テーブルで遅いトランザクションが見つかり、すでにタグが割り当てられている場合は、そのタグの情報に基づいて、トランザクションを呼び出しているソース(アプリケーション / マイクロサービス)を特定できます。
  • 統計テーブル内のトランザクションを区別する: トランザクション タグを割り当てると、関心があるタグに基づいてトランザクション統計情報テーブルの行をフィルタリングできます。トランザクション タグを使用しない場合は、統計で表されるオペレーションを見つけるのことが、煩雑なプロセスになる可能性があります。たとえば、トランザクションの統計情報の場合、タグ付けされていないトランザクションを特定するためには、関連するテーブルと列を調べる必要があります。
  • 特定のアプリケーションやマイクロサービスからのトランザクションが遅いかどうかを判断する確認: トランザクション タグを使用すると、特定のアプリケーションやマイクロサービスからのトランザクションのレイテンシが高いかどうかを見分けるのに役立ちます。
  • 一連のトランザクションに関する統計情報をグループ化する: トランザクション タグを使用して、一連の類似したトランザクションのパフォーマンスを追跡、比較、報告できます。
  • ロックの競合に関係する列にアクセスしているトランザクションを見つける: トランザクション タグを使用すると、ロックの統計情報テーブルでロックの競合を引き起こしている個々のトランザクションを特定できます。
  • 変更ストリームを使用して Spanner からユーザー変更データをストリーミングする: 変更ストリーム データレコードには、ユーザーデータを変更したトランザクションのトランザクション タグが含まれます。これにより、変更ストリームの読み取り側は、タグに基づいて変更をトランザクション タイプに関連付けることができます。

トランザクション タグを割り当てる方法

次のサンプルでは、Spanner クライアント ライブラリを使用してトランザクション タグを設定する方法を示します。クライアント ライブラリを使用する際、トランザクション呼び出しの先頭には、トランザクション内のすべてのオペレーションに適用されるトランザクション タグを設定できます。

C++

void SetTransactionTag(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::StatusOr;

  // Sets the transaction tag to "app=concert,env=dev". This will be
  // applied to all the individual operations inside this transaction.
  auto commit_options =
      google::cloud::Options{}.set<spanner::TransactionTagOption>(
          "app=concert,env=dev");
  auto commit = client.Commit(
      [&client](
          spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        spanner::SqlStatement update_statement(
            "UPDATE Venues SET Capacity = CAST(Capacity/4 AS INT64)"
            "  WHERE OutdoorVenue = false");
        // Sets the request tag to "app=concert,env=dev,action=update".
        // This will only be set on this request.
        auto update = client.ExecuteDml(
            txn, std::move(update_statement),
            google::cloud::Options{}.set<spanner::RequestTagOption>(
                "app=concert,env=dev,action=update"));
        if (!update) return std::move(update).status();

        spanner::SqlStatement insert_statement(
            "INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue, "
            "                    LastUpdateTime)"
            " VALUES (@venueId, @venueName, @capacity, @outdoorVenue, "
            "         PENDING_COMMIT_TIMESTAMP())",
            {
                {"venueId", spanner::Value(81)},
                {"venueName", spanner::Value("Venue 81")},
                {"capacity", spanner::Value(1440)},
                {"outdoorVenue", spanner::Value(true)},
            });
        // Sets the request tag to "app=concert,env=dev,action=insert".
        // This will only be set on this request.
        auto insert = client.ExecuteDml(
            txn, std::move(insert_statement),
            google::cloud::Options{}.set<spanner::RequestTagOption>(
                "app=concert,env=dev,action=select"));
        if (!insert) return std::move(insert).status();
        return spanner::Mutations{};
      },
      commit_options);
  if (!commit) throw std::move(commit).status();
}

C#


using Google.Cloud.Spanner.Data;
using System.Threading.Tasks;

public class TransactionTagAsyncSample
{
    public async Task<int> TransactionTagAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";
        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        return await connection.RunWithRetriableTransactionAsync(async transaction =>
        {
            // Sets the transaction tag to "app=concert,env=dev".
            // This transaction tag will be applied to all the individual operations inside
            // the transaction.
            transaction.Tag = "app=concert,env=dev";

            // Sets the request tag to "app=concert,env=dev,action=update".
            // This request tag will only be set on this request.
            var updateCommand =
                connection.CreateDmlCommand("UPDATE Venues SET Capacity = DIV(Capacity, 4) WHERE OutdoorVenue = false");
            updateCommand.Tag = "app=concert,env=dev,action=update";
            updateCommand.Transaction = transaction;
            int rowsModified = await updateCommand.ExecuteNonQueryAsync();

            var insertCommand = connection.CreateDmlCommand(
                @"INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue, LastUpdateTime)
                    VALUES (@venueId, @venueName, @capacity, @outdoorVenue, PENDING_COMMIT_TIMESTAMP())",
                new SpannerParameterCollection
                {
                    {"venueId", SpannerDbType.Int64, 81},
                    {"venueName", SpannerDbType.String, "Venue 81"},
                    {"capacity", SpannerDbType.Int64, 1440},
                    {"outdoorVenue", SpannerDbType.Bool, true}
                }
            );
            // Sets the request tag to "app=concert,env=dev,action=insert".
            // This request tag will only be set on this request.
            insertCommand.Tag = "app=concert,env=dev,action=insert";
            insertCommand.Transaction = transaction;
            rowsModified += await insertCommand.ExecuteNonQueryAsync();
            return rowsModified;
        });
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

// readWriteTransactionWithTag executes the update and insert queries on venues table with appropriate transaction and requests tag
func readWriteTransactionWithTag(w io.Writer, db string) error {
	// db = `projects/<project>/instances/<instance-id>/database/<database-id>`
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	_, err = client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		stmt := spanner.Statement{
			SQL: `UPDATE Venues SET Capacity = CAST(Capacity/4 AS INT64) WHERE OutdoorVenue = false`,
		}
		_, err := txn.UpdateWithOptions(ctx, stmt, spanner.QueryOptions{RequestTag: "app=concert,env=dev,action=update"})
		if err != nil {
			return err
		}
		fmt.Fprint(w, "Venue capacities updated.")
		stmt = spanner.Statement{
			SQL: `INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue, LastUpdateTime) 
                   VALUES (@venueId, @venueName, @capacity, @outdoorVenue, PENDING_COMMIT_TIMESTAMP())`,
			Params: map[string]interface{}{
				"venueId":      81,
				"venueName":    "Venue 81",
				"capacity":     1440,
				"outdoorVenue": true,
			},
		}
		_, err = txn.UpdateWithOptions(ctx, stmt, spanner.QueryOptions{RequestTag: "app=concert,env=dev,action=insert"})
		if err != nil {
			return err
		}
		fmt.Fprint(w, "New venue inserted.")
		return nil
	}, spanner.TransactionOptions{TransactionTag: "app=concert,env=dev"})
	return err
}

Java

static void setTransactionTag(DatabaseClient databaseClient) {
  // Sets the transaction tag to "app=concert,env=dev".
  // This transaction tag will be applied to all the individual operations inside this
  // transaction.
  databaseClient
      .readWriteTransaction(Options.tag("app=concert,env=dev"))
      .run(transaction -> {
        // Sets the request tag to "app=concert,env=dev,action=update".
        // This request tag will only be set on this request.
        transaction.executeUpdate(
            Statement.of("UPDATE Venues"
                + " SET Capacity = CAST(Capacity/4 AS INT64)"
                + " WHERE OutdoorVenue = false"),
            Options.tag("app=concert,env=dev,action=update"));
        System.out.println("Venue capacities updated.");

        Statement insertStatement = Statement.newBuilder(
            "INSERT INTO Venues"
                + " (VenueId, VenueName, Capacity, OutdoorVenue, LastUpdateTime)"
                + " VALUES ("
                + " @venueId, @venueName, @capacity, @outdoorVenue, PENDING_COMMIT_TIMESTAMP()"
                + " )")
            .bind("venueId")
            .to(81)
            .bind("venueName")
            .to("Venue 81")
            .bind("capacity")
            .to(1440)
            .bind("outdoorVenue")
            .to(true)
            .build();

        // Sets the request tag to "app=concert,env=dev,action=insert".
        // This request tag will only be set on this request.
        transaction.executeUpdate(
            insertStatement,
            Options.tag("app=concert,env=dev,action=insert"));
        System.out.println("New venue inserted.");

        return null;
      });
}

Node.js

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

async function transactionTag() {
  // Gets a reference to a Cloud Spanner instance and database.
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  // Run a transaction with a transaction tag that will automatically be
  // included with each request in the transaction.
  try {
    await database.runTransactionAsync(
      {requestOptions: {transactionTag: 'app=cart,env=dev'}},
      async tx => {
        // Set the request tag to "app=concert,env=dev,action=update".
        // This request tag will only be set on this request.
        await tx.runUpdate({
          sql: 'UPDATE Venues SET Capacity = DIV(Capacity, 4) WHERE OutdoorVenue = false',
          requestOptions: {requestTag: 'app=concert,env=dev,action=update'},
        });
        console.log('Updated capacity of all indoor venues to 1/4.');

        await tx.runUpdate({
          sql: `INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue, LastUpdateTime)
                VALUES (@venueId, @venueName, @capacity, @outdoorVenue, PENDING_COMMIT_TIMESTAMP())`,
          params: {
            venueId: 81,
            venueName: 'Venue 81',
            capacity: 1440,
            outdoorVenue: true,
          },
          types: {
            venueId: {type: 'int64'},
            venueName: {type: 'string'},
            capacity: {type: 'int64'},
            outdoorVenue: {type: 'bool'},
          },
          requestOptions: {requestTag: 'app=concert,env=dev,action=update'},
        });
        console.log('Inserted new outdoor venue');

        await tx.commit();
      }
    );
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    await database.close();
  }
}
transactionTag();

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;

/**
 * Executes a transaction with a transaction tag.
 * Example:
 * ```
 * spanner_set_transaction_tag($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function set_transaction_tag(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) {
        $t->executeUpdate(
            'UPDATE Venues SET Capacity = CAST(Capacity/4 AS INT64) WHERE OutdoorVenue = false',
            [
                'requestOptions' => ['requestTag' => 'app=concert,env=dev,action=update']
            ]
        );
        print('Venue capacities updated.' . PHP_EOL);
        $t->executeUpdate(
            'INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue, LastUpdateTime) '
            . 'VALUES (@venueId, @venueName, @capacity, @outdoorVenue, PENDING_COMMIT_TIMESTAMP())',
            [
                'parameters' => [
                    'venueId' => 81,
                    'venueName' => 'Venue 81',
                    'capacity' => 1440,
                    'outdoorVenue' => true,
                ],
                'requestOptions' => ['requestTag' => 'app=concert,env=dev,action=insert']
            ]
        );
        print('New venue inserted.' . PHP_EOL);
        $t->commit();
    }, [
        'requestOptions' => ['transactionTag' => 'app=concert,env=dev']
    ]);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

def update_venues(transaction):
    # Sets the request tag to "app=concert,env=dev,action=update".
    #  This request tag will only be set on this request.
    transaction.execute_update(
        "UPDATE Venues SET Capacity = CAST(Capacity/4 AS INT64) WHERE OutdoorVenue = false",
        request_options={"request_tag": "app=concert,env=dev,action=update"},
    )
    print("Venue capacities updated.")

    # Sets the request tag to "app=concert,env=dev,action=insert".
    # This request tag will only be set on this request.
    transaction.execute_update(
        "INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue, LastUpdateTime) "
        "VALUES (@venueId, @venueName, @capacity, @outdoorVenue, PENDING_COMMIT_TIMESTAMP())",
        params={
            "venueId": 81,
            "venueName": "Venue 81",
            "capacity": 1440,
            "outdoorVenue": True,
        },
        param_types={
            "venueId": param_types.INT64,
            "venueName": param_types.STRING,
            "capacity": param_types.INT64,
            "outdoorVenue": param_types.BOOL,
        },
        request_options={"request_tag": "app=concert,env=dev,action=insert"},
    )
    print("New venue inserted.")

database.run_in_transaction(update_venues, transaction_tag="app=concert,env=dev")

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client = spanner.client instance_id, database_id

client.transaction request_options: { tag: "app=cart,env=dev" } do |tx|
  tx.execute_update \
    "UPDATE Venues SET Capacity = CAST(Capacity/4 AS INT64) WHERE OutdoorVenue = false",
    request_options: { tag: "app=concert,env=dev,action=update" }

  puts "Venue capacities updated."

  tx.execute_update \
    "INSERT INTO Venues (VenueId, VenueName, Capacity, OutdoorVenue) " \
    "VALUES (@venue_id, @venue_name, @capacity, @outdoor_venue)",
    params: {
      venue_id: 81,
      venue_name: "Venue 81",
      capacity: 1440,
      outdoor_venue: true
    },
    request_options: { tag: "app=concert,env=dev,action=insert" }

  puts "New venue inserted."
end

トランザクション統計情報テーブルでトランザクション タグを確認する方法

次のクエリでは、10 分間隔のトランザクションの統計情報が返されます。

SELECT t.fprint,
       t.transaction_tag,
       t.read_columns,
       t.commit_attempt_count,
       t.avg_total_latency_seconds
FROM SPANNER_SYS.TXN_STATS_TOP_10MINUTE AS t
LIMIT 3;

クエリから返される結果の例として、以下のデータを見てみましょう。

fprint transaction_tag read_columns commit_attempt_count avg_total_latency_seconds
40015598317 app=concert,env=dev [Venues._exists,
Venues.VenueId,
Venues.VenueName,
Venues.Capacity]
278802 0.3508
20524969030 app=product,service=payment [Singers.SingerInfo] 129012 0.0142
77848338483 [empty string] [Singers.FirstName, Singers.LastName, Singers._exists] 5357 0.048

この結果のテーブルから、トランザクションに TRANSACTION_TAG を割り当てると、トランザクションの統計情報テーブルに値が入力されることがわかります。トランザクション タグが割り当てられていない場合は、空の文字列として表示されます。

タグ付けされたトランザクションの場合、統計情報はトランザクション タグごとに集計されます(トランザクション タグ app=concert,env=dev の平均レイテンシは 0.3508 秒など)。タグが割り当てられていない場合、統計情報は FPRINT ごとに集計されます(3 行目の 77848338483 は平均レイテンシが 0.048 秒など)。

ロック統計情報テーブルでトランザクション タグを表示する方法

次のクエリでは、10 分間隔のロック統計情報が返されます。

CAST() 関数は、row_range_start_key BYTES フィールドを STRING に変換します。

SELECT 
   CAST(s.row_range_start_key AS STRING) AS row_range_start_key,
   s.lock_wait_seconds,
   s.sample_lock_requests
FROM SPANNER_SYS.LOCK_STATS_TOP_10MINUTE s
LIMIT 2;

クエリから返される結果の例として、以下のデータを見てみましょう。

row_range_start_key lock_wait_seconds sample_lock_requests
Songs(2,1,1) 0.61 LOCK_MODE: ReaderShared
COLUMN: Singers.SingerInfo
TRANSACTION_TAG: app=product,service=shipping

LOCK_MODE: WriterShared
COLUMN: Singers.SingerInfo
TRANSACTION_TAG: app=product,service=payment
albums(2,1+) 0.48 LOCK_MODE: ReaderShared
COLUMN: users._exists1
TRANSACTION_TAG: [empty string]

LOCK_MODE: WriterShared
COLUMN: users._exists
TRANSACTION_TAG: [empty string]

この結果のテーブルから、トランザクションに TRANSACTION_TAG を割り当てると、ロックの統計情報テーブルに値が入力されることがわかります。トランザクション タグが割り当てられていない場合は、空の文字列として表示されます。

API メソッドとリクエスト / トランザクション タグのマッピング

リクエストタグとトランザクション タグは、トランザクション モードが読み取り専用トランザクションか読み取り / 書き込みトランザクションかに応じて、特定の API メソッドに適用されます。通常、トランザクション タグは読み取り / 書き込みトランザクションに適用され、リクエストタグは読み取り専用トランザクションに適用されます。次の表では、API メソッドから該当するタグの種類へのマッピングを示します。

API メソッド トランザクション モード リクエストタグ トランザクション タグ
Read、
StreamingRead
読み取り専用トランザクション ×
読み取り / 書き込みトランザクション
ExecuteSql、
ExecuteStreamingSql1
読み取り専用トランザクション1 1 ×
読み取り / 書き込みトランザクション
ExecuteBatchDml 読み取り / 書き込みトランザクション
BeginTransaction 読み取り / 書き込みトランザクション ×
commit 読み取り / 書き込みトランザクション ×

1 Apache Beam SpannerIO Dataflow コネクタを使用して実行される変更ストリーム クエリの場合、REQUEST_TAG には Dataflow ジョブ名が含まれます。

制限事項

読み取り、クエリ、トランザクションにタグを追加する際は、次の制限事項を考慮してください。

  • タグ文字列の長さは、50 文字までに制限されています。この上限を超える文字列は、切り捨てられます。
  • タグに使用できる文字は、ASCII 文字(32~126)のみです。任意の Unicode 文字は、アンダースコアに置き換えられます。
  • 先頭のアンダースコア(_)は、文字列から削除されます。
  • タグでは、大文字と小文字が区別されます。たとえば、あるクエリセットにリクエストタグ APP=cart,ENV=dev を追加し、別のクエリセットに app=cart,env=dev を追加すると、Spanner では、統計情報がタグごとに分けて集計されます。
  • 次のような状況では、統計情報テーブルにタグがない可能性があります。

    • 期間中に実行されたすべてのタグ付きオペレーションの統計情報を Spanner がテーブルに保存できない場合は、指定された期間で最もリソース消費量の多いオペレーションが優先されます。

タグの命名

データベース オペレーションにタグを割り当てる場合は、各タグ文字列で伝えたい情報を検討することが重要です。選択した規則やパターンによって、タグの効果が高まります。たとえば、タグ適切な名前付けを行うと、統計情報とアプリケーション コードの関連付けがより簡単になります。

タグは、前述の制限事項の範囲内で自由に選択できます。とはいえ、タグ文字列は、カンマで区切られた Key-Value ペアのセットとして構成することをおすすめします。

たとえば、Spanner データベースを e コマースのユースケースで使用しているとします。特定のクエリに割り当てるリクエストタグに、アプリケーション、開発環境、クエリによるアクションに関する情報を含めたい場合があります。この場合、Key-Value 形式のタグ文字列を app=cart,env=dev,action=update として割り当てることを検討してください。つまり、クエリは開発環境内のカート アプリケーションから呼び出され、カートの更新に使用されます。

カタログ検索アプリケーションからの別のクエリがあり、タグ文字列を app=catalogsearch,env=dev,action=list として割り当てる場合を考えます。ここで、これらのクエリのいずれかがクエリ統計テーブルに高レイテンシ クエリとして現れる場合は、タグを使用してその原因を簡単に特定できます。

以下の例では、タグ付けパターンが、運用統計の整理にどのように使用されるかを示します。この例は網羅的なものではありません。タグ文字列内で、カンマなどの区切り文字を使用してそれらを組み合わせることもできます。

タグキー タグと値のペアの例 説明
アプリケーション app=cart
app=frontend
app=catalogsearch
オペレーションを呼び出しているアプリケーションの特定に役立ちます。
環境 env=prod
env=dev
env=test
env=staging
オペレーションに関連付けられている環境を特定する際に有用です。
フレームワーク framework=spring
framework=django
framework=jetty
オペレーションに関連付けられているフレームワークの特定に役立ちます。
アクション action=list
action=retrieve
action=update
ユーザーが行った操作を特定する際に有用です。
サービス service=payment
service=shipping
オペレーションを呼び出すマイクロサービスの特定に役立ちます。

留意点

  • REQUEST_TAG を割り当てると、同じタグ文字列を持つ複数のクエリの統計情報が、クエリ統計情報テーブルの単一の行にグループ化されます。TEXT フィールドには、これらのクエリのいずれかのテキストのみが現れます。
  • REQUEST_TAG を割り当てると、同じタグ文字列を持つ複数の読み取りの統計情報が、読み取り統計情報テーブルの 1 行にグループ化されます。読み取られた一連の列は、すべて READ_COLUMNS フィールドに追加されます。
  • TRANSACTION_TAG を割り当てると、同じタグ文字列を持つトランザクションの統計情報が、トランザクション統計情報テーブルの 1 行にグループ化されます。トランザクションによって書き込まれた一連のすべての列は WRITE_CONSTRUCTIVE_COLUMNS フィールドに追加され、読み取られた一連のすべての列は READ_COLUMNS フィールドに追加されます。

タグを使用したトラブルシューティングのシナリオ

問題のあるトランザクションの原因を見つける

次のクエリにより、選択した期間における上位トランザクションの元データが返されます。

SELECT
 fprint,
 transaction_tag,
 ROUND(avg_total_latency_seconds,4) as avg_total_latency_sec,
 ROUND(avg_commit_latency_seconds,4) as avg_commit_latency_sec,
 commit_attempt_count,
 commit_abort_count
FROM SPANNER_SYS.TXN_STATS_TOP_10MINUTE
WHERE interval_end = "2020-05-17T18:40:00"
ORDER BY avg_total_latency_seconds DESC;

次の表に、アプリケーション自身へのクエリ、または同じデータベースに対するクエリの実行によって返されたサンプルデータを示します。ここで、アプリケーションは、3 つ(cartproductfrontend)あります。

レイテンシが大きなトランザクションを特定したら、関連するタグを使用して、アプリケーション コードの関連部分を特定し、トランザクションの統計情報を使用してさらにトラブルシューティングを行います。

fprint transaction_tag avg_total_latency_sec avg_commit_latency_sec commit_attempt_count commit_abort_count
7129109266372596045 app=cart,service=order 0.3508 0.0139 278802 142205
9353100217060788102 app=cart,service=redis 0.1633 0.0142 129012 27177
9353100217060788102 app=product,service=payment 0.1423 0.0133 5357 636
898069986622520747 app=product,service=shipping 0.0159 0.0118 4269 1
9521689070912159706 app=frontend,service=ads 0.0093 0.0045 164 0
11079878968512225881 [empty string] 0.031 0.015 14 0

同様に、Request Tag は、クエリ統計分析テーブルから問題のあるクエリの原因を見つけるためや、読み取り統計情報テーブルから問題のある読み取りの原因を見つけるために使用できます。

特定のアプリケーションやマイクロサービスからのトランザクションのレイテンシや他の統計情報を確認する

タグ文字列でアプリケーション名やマイクロサービス名を使用している場合は、そのアプリケーション名やマイクロサービス名を含むタグでトランザクション統計情報テーブルをフィルタリングするのに役立ちます。

支払いアプリに新しいトランザクションを追加して、それらの新しいトランザクションのレイテンシや他の統計情報を確認したいとします。タグ内で支払いアプリケーションの名前を使用した場合は、app=payment を含むタグに対してのみトランザクション統計情報テーブルをフィルタリングできます。

次のクエリでは、10 分間隔の支払いアプリのトランザクションの統計情報を返します。

SELECT
  transaction_tag,
  avg_total_latency_sec,
  avg_commit_latency_sec,
  commit_attempt_count,
  commit_abort_count
FROM SPANNER_SYS.TXN_STATS_TOP_10MINUTE
WHERE STARTS_WITH(transaction_tag, "app=payment")
LIMIT 3;

出力例を次に示します。

transaction_tag avg_total_latency_sec avg_commit_latency_sec commit_attempt_count commit_abort_count
app=payment,action=update 0.3508 0.0139 278802 142205
app=payment,action=transfer 0.1633 0.0142 129012 27177
app=payment, action=retrieve 0.1423 0.0133 5357 636

同様に、リクエストタグを使用して、クエリ統計情報テーブルや読み取り統計情報テーブルで特定のアプリケーションからのクエリや読み取りを見つけられます。

ロックの競合に関係するトランザクションの検出

ロックの待機時間が長いトランザクションと行キーを探し出すには、ロックの競合に関連する行キー、列、対応するトランザクションが列挙された LOCK_STAT_TOP_10MINUTE テーブルをクエリします。

SELECT CAST(s.row_range_start_key AS STRING) AS row_range_start_key,
       t.total_lock_wait_seconds,
       s.lock_wait_seconds,
       s.lock_wait_seconds/t.total_lock_wait_seconds frac_of_total,
       s.sample_lock_requests
FROM spanner_sys.lock_stats_total_10minute t, spanner_sys.lock_stats_top_10minute s
WHERE
  t.interval_end = "2020-05-17T18:40:00" and s.interval_end = t.interval_end;

クエリからの出力例を次に示します。

row_range_start_key total_lock_wait_seconds lock_wait_seconds frac_of_total sample_lock_requests
Singers(32) 2.37 1.76 1 LOCK_MODE: WriterShared
COLUMN: Singers.SingerInfo
TRANSACTION_TAG:
app=cart,service=order

LOCK_MODE: ReaderShared
COLUMN: Singers.SingerInfo
TRANSACTION_TAG:
app=cart,service=redis

このテーブルの結果から、Singers テーブルのキー SingerId=32 で競合が発生していることがわかります。Singers.SingerInfo は、ReaderSharedWriterShared の間でロックの競合が発生した列です。対応するトランザクションで、競合が発生しているもの(app=cart,service=orderapp=cart,service=redis)を特定することもできます。

ロックの競合を引き起こしているトランザクションを特定したら、トランザクションの統計情報を使用して、これらのトランザクションに絞ってトランザクションのより詳細な動作と、競合を回避可能か、ロックが保持される時間を短くできるかを把握します。詳細については、ロック競合を減らすためのベスト プラクティスをご覧ください。

次のステップ