使用请求代码和交易代码排查问题

Spanner 提供了一组内置的统计信息表,可帮助您深入了解查询、读取和事务。为了将统计信息与应用代码相关联并改进问题排查效果,您可以在应用代码中的 Spanner 读取、查询和事务操作中添加标记(自由格式字符串)。这些标记将填充在统计信息表中,以帮助您根据标记进行关联和搜索。

Spanner 支持两种类型的标记:request 标记和 transaction 标记。顾名思义,您可以向事务添加事务标记,向各个查询和读取 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
从订单中选择 *; 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 秒)。如果未分配标记,则按查询汇总统计信息(例如,第三行中的查询的平均延迟时间为 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 汇总(例如,第三行中的 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 读写事务
提交 读写事务

1 对于使用 Apache Beam SpannerIO Dataflow 连接器执行的变更数据流查询,REQUEST_TAG 包含 Dataflow 作业名称。

限制

向读取、查询和事务添加标记时,请考虑以下限制:

  • 标记字符串的长度上限为 50 个字符。超出此限制的字符串会被截断。
  • 标记中只允许使用 ASCII 字符 (32-126)。任意 Unicode 字符会被替换为下划线。
  • 任何前导下划线 (_) 字符都会从字符串中移除。
  • 标记区分大小写。例如,如果您将请求标记 APP=cart,ENV=dev 添加到一组查询,并将 app=cart,env=dev 添加到另一组查询,则 Spanner 会分别汇总每个标记的统计信息。
  • 在以下情况下,统计信息表中可能缺少标记:

    • 如果 Spanner 无法存储在相应时间间隔内的表内运行的所有带标记操作的统计信息,则系统会在指定的时间间隔内优先处理资源消耗最多的操作。

标记命名

为数据库操作分配标记时,请务必考虑要在每个标记字符串中传达的信息。您选择的惯例或模式可使您的标记更有效。例如,正确的标记命名可以更轻松地将统计信息与应用代码相关联。

您可以在规定的限制中选择所需的任何标记。不过,我们建议您将标记字符串构建为一组以英文逗号分隔的键值对。

例如,假设您将 Spanner 数据库用于电子商务用例。您可能希望在要分配给特定查询的请求标记中包含有关应用、开发环境以及查询会执行的操作的信息。您可以将键值对格式的标记字符串考虑为 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 时,具有相同标记字符串的多个读取的统计信息会在读取统计信息表中分组为一行。读取的所有列集都会添加到 READ_COLUMNS 字段。
  • 当您分配 TRANSACTION_TAG 时,具有相同标记字符串的事务的统计信息会在事务统计信息表中分组为一行。通过事务写入的所有列集都会添加到 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;

下表列出了我们的查询返回的示例数据,其中有三个应用(即购物车产品前端),它们拥有或者查询同一数据库。

识别出遇到高延迟的事务之后,您可以使用关联的标记来识别应用代码的相关部分,并使用事务统计信息进一步排查问题。

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

同样,请求标记可用于从查询统计信息表中查找有问题的查询的来源,从读取统计信息表中查找有问题的读取的来源。

查找特定应用或微服务中事务的延迟时间和其他统计信息

如果您在标记字符串中使用了应用名称或微服务名称,则可帮助按包含该应用名称或微服务名称的标记过滤事务统计信息表。

假设您向付款应用添加了新事务,并想要查看这些新事务的延迟时间和其他统计信息。如果您在标记中使用了付款应用的名称,则可以过滤交易统计信息表以仅显示包含 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

从这个结果表中,我们可以看到键为 SingerId=32Singers 表中发生了冲突。Singers.SingerInfoReaderSharedWriterShared 之间发生锁定冲突的列。您还可以确定遇到冲突的相应事务(app=cart,service=orderapp=cart,service=redis)。

在查明导致锁冲突的事务之后,您现在可以使用事务统计信息关注这些事务,以便更好地了解这些事务正在执行的操作,您是否可以避免冲突或缩短锁定的持有时间。如需了解详情,请参阅减少锁争用的最佳做法

后续步骤