요청 태그 및 트랜잭션 태그를 사용한 문제 해결

Spanner는 쿼리, 읽기, 트랜잭션에 대한 유용한 정보를 얻는 데 도움이 되는 기본 제공 통계 테이블 집합을 제공합니다. 통계와 애플리케이션 코드의 상관관계를 파악하고 문제 해결을 개선하기 위해 애플리케이션 코드에서 Spanner 읽기, 쿼리, 트랜잭션 작업에 태그(자유 형식 문자열)를 추가할 수 있습니다. 이 태그는 통계 테이블에 채워지므로 태그를 기반으로 상관관계를 파악하고 검색할 수 있습니다.

Spanner는 두 가지 유형의 태그, 즉 요청 태그와 트랜잭션 태그를 지원합니다. 이름에서 알 수 있듯이 트랜잭션 태그를 트랜잭션에 추가하고 요청 태그를 개별 쿼리 및 읽기 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;

쿼리에서 반환되는 결과의 예시로 다음 데이터를 살펴보겠습니다.

text 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초임). 할당된 태그가 없으면 쿼리별로 통계가 집계됩니다(예: 세 번째 행에 있는 쿼리의 평균 지연 시간은 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 메서드 트랜잭션 모드 요청 태그 트랜잭션 태그
읽기,
StreamingRead
읽기 전용 트랜잭션 아니요
읽기-쓰기 트랜잭션
ExecuteSql,
ExecuteStreamingSql1
읽기 전용 트랜잭션1 1 아니요
읽기-쓰기 트랜잭션
ExecuteBatchDml 읽기-쓰기 트랜잭션
BeginTransaction 읽기-쓰기 트랜잭션 아니요
커밋 읽기-쓰기 트랜잭션 아니요

1 Apache Beam SpannerIO Dataflow 커넥터를 사용하여 실행되는 변경 스트림 쿼리의 경우 REQUEST_TAG에 Dataflow 작업 이름이 포함됩니다.

제한사항

읽기, 쿼리, 트랜잭션에 태그를 추가할 때는 다음 제한사항을 고려하세요.

  • 태그 문자열의 길이는 50자로 제한됩니다. 이 제한을 초과하는 문자열은 잘립니다.
  • ASCII 문자(32~126)만 태그에 허용됩니다. 임의의 유니코드 문자는 밑줄로 대체됩니다.
  • 모든 선행 밑줄(_)은 문자열에서 삭제됩니다.
  • 태그는 대소문자를 구분합니다. 예를 들어 요청 태그 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

마찬가지로 요청 태그를 사용하여 쿼리 통계 또는 읽기 통계 테이블에서 특정 애플리케이션의 쿼리 또는 읽기를 찾을 수 있습니다.

잠금 충돌과 관련된 트랜잭션 검색

잠금 대기 시간이 긴 트랜잭션과 row key를 확인하기 위해 잠금 충돌과 관련된 row key, 열, 해당 트랜잭션이 나열된 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=32 키의 Singers 테이블에서 충돌이 발생한 것을 확인할 수 있습니다. Singers.SingerInfoReaderSharedWriterShared 사이에 잠금 충돌이 발생한 열입니다. 또한 충돌이 발생한 해당 트랜잭션(app=cart,service=orderapp=cart,service=redis)을 식별할 수 있습니다.

잠금 충돌을 유발하는 트랜잭션이 식별되면 이제 트랜잭션 통계를 사용하여 이러한 트랜잭션에 집중하여 트랜잭션이 수행하는 작업과 충돌을 방지하거나 잠금이 유지되는 시간을 줄일 수 있는지 파악할 수 있습니다. 자세한 내용은 잠금 경합을 줄이기 위한 권장사항을 참조하세요.

다음 단계