פתרון בעיות באמצעות תגי בקשות ותגי עסקאות

‫Spanner מספק קבוצה של טבלאות נתונים סטטיסטיים מובנות שעוזרות לקבל תובנות לגבי השאילתות, הקריאות והעסקאות. כדי ליצור קורלציה בין נתונים סטטיסטיים לבין קוד האפליקציה ולשפר את פתרון הבעיות, אפשר להוסיף תג (מחרוזת חופשית) לפעולות קריאה, שאילתה ועסקאות ב-Spanner בקוד האפליקציה. התגים האלה מאוכלסים בטבלאות סטטיסטיות, ועוזרים לכם ליצור קורלציה ולחפש על סמך תגים.

‫Spanner תומך בשני סוגים של תגים: תגי בקשה ותגי עסקה. כפי שהשמות שלהם מרמזים, אפשר להוסיף תגי עסקאות לעסקאות, ותגי בקשות לממשקי API של שאילתות וקריאות בודדות. אפשר להגדיר תג עסקה בהיקף העסקה ולהגדיר תגי בקשה נפרדים לכל בקשת API רלוונטית במסגרת העסקה. תגי בקשות ותגי עסקאות שמוגדרים בקוד האפליקציה מאוכלסים בעמודות של טבלאות הנתונים הסטטיסטיים הבאות.

טבלת הנתונים הסטטיסטיים סוג התגים שמופיעים בטבלת הנתונים הסטטיסטיים
נתונים סטטיסטיים של שאילתות TopN בקשת תגים
TopN Read Statistics בקשת תגים
TopN Transaction Statistics תגי עסקאות
TopN Lock Statistics תגי עסקאות

בקשת תגים

אפשר להוסיף תג בקשה אופציונלי לשאילתה או לבקשת קריאה. ב-Spanner, הנתונים הסטטיסטיים מקובצים לפי תג בקשה, שמופיע בשדה REQUEST_TAG של הטבלאות query statistics ו-read statistics.

מתי כדאי להשתמש בתגי בקשות

ריכזנו כאן כמה תרחישים שבהם כדאי להשתמש בתגי בקשות.

  • איתור המקור של שאילתה או קריאה בעייתיות: ‫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 אוסף נתונים סטטיסטיים של עסקאות קריאה-כתיבה בטבלת הנתונים הסטטיסטיים של העסקאות. אם אתם מוצאים עסקאות איטיות בטבלת נתוני העסקאות, ואם כבר הקציתם להן תגים, תוכלו לזהות את המקור (האפליקציה או המיקרו-שירות) שקורא לעסקאות האלה על סמך המידע בתג.
  • זיהוי טרנזקציות בטבלאות נתונים סטטיסטיים: הקצאת תגי טרנזקציות עוזרת לסנן שורות בטבלת הנתונים הסטטיסטיים של הטרנזקציות על סמך התגים שמעניינים אתכם. בלי תגי עסקאות, יכול להיות שתצטרכו לעבור תהליך ארוך ומייגע כדי להבין אילו פעולות מיוצגות על ידי נתון מסוים. לדוגמה, כדי לזהות עסקה לא מתויגת בנתונים הסטטיסטיים של העסקאות, צריך לבדוק את הטבלאות והעמודות שקשורות לעסקה.
  • איך בודקים אם טרנזקציות מאפליקציה או ממיקרו-שירות מסוימים מתבצעות לאט: תגי טרנזקציות יכולים לעזור לזהות אם לטרנזקציות מאפליקציה או ממיקרו-שירות מסוימים יש השהיות ארוכות יותר.
  • קיבוץ נתונים סטטיסטיים של קבוצת טרנזקציות: אתם יכולים להשתמש בתגי טרנזקציות כדי לעקוב אחרי הביצועים של קבוצת טרנזקציות דומות, להשוות ביניהם ולדווח עליהם.
  • איך מוצאים את העסקאות שגורמות לבעיות נעילה בעמודות: תגי עסקאות יכולים לעזור לזהות עסקאות ספציפיות שגורמות לבעיות נעילה בטבלאות Lock statistics.
  • הזרמת נתוני שינויים של משתמשים מ-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.TransactionOptions.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 a יש זמן אחזור ממוצע של 0.3508 שניות). אם לא הוקצה תג, הנתונים הסטטיסטיים מצטברים לפי FPRINT (למשל, 77848338483 בשורה השלישית הוא זמן האחזור הממוצע של 0.048 שניות).

איך מציגים תגי עסקאות בטבלת הנתונים הסטטיסטיים של הנעילה

השאילתה הבאה מחזירה את הנתונים הסטטיסטיים של הנעילה במרווחי זמן של 10 דקות.

הפונקציה CAST() ממירה את השדה row_range_start_key BYTES למחרוזת.

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
אלבומים(2 ומעלה) 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 Methods מצבי עסקה בקשת תג תג עסקה
קריאה,
סטרימינגקריאה
עסקה עם הרשאת קריאה בלבד כן לא
עסקת קריאה וכתיבה כן כן
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 לא מצליח לאחסן בטבלאות נתונים סטטיסטיים של כל הפעולות המתויגות שמופעלות במהלך המרווח, המערכת נותנת עדיפות לפעולות שצורכות הכי הרבה משאבים במהלך המרווח שצוין.

מתן שמות לתגים

כשמקצים תגים לפעולות במסד הנתונים, חשוב לחשוב איזה מידע רוצים להעביר בכל מחרוזת תגים. המוסכמה או התבנית שתבחרו ישפרו את היעילות של התגים. לדוגמה, מתן שמות מתאימים לתגים מקל על התאמת נתונים סטטיסטיים לקוד האפליקציה.

אתם יכולים לבחור כל תג שתרצו במסגרת המגבלות שצוינו. עם זאת, מומלץ ליצור מחרוזת תגים כקבוצה של צמדי מפתח-ערך שמופרדים באמצעות פסיקים.

לדוגמה, נניח שאתם משתמשים במסד נתונים של Spanner לתרחיש לדוגמה של מסחר אלקטרוני. כדאי לכלול מידע על האפליקציה, על סביבת הפיתוח ועל הפעולה שהשאילתה מבצעת בתג הבקשה שאתם מתכוונים להקצות לשאילתה מסוימת. אפשר להקצות את מחרוזת התג בפורמט של זוג מפתח/ערך כ-app=cart,env=dev,action=update.המשמעות היא שהשאילתה נקראת מאפליקציית העגלה בסביבת הפיתוח, והיא משמשת לעדכון העגלה.

נניח שיש לכם שאילתה אחרת מאפליקציית חיפוש בקטלוג, ואתם מקצים את מחרוזת התג כ-app=catalogsearch,env=dev,action=list. אם אחת מהשאילתות האלה מופיעה בטבלת הנתונים הסטטיסטיים של השאילתות כשאילתה עם זמן אחזור גבוה, תוכלו לזהות בקלות את המקור באמצעות התג.

הנה כמה דוגמאות לשימוש בתבנית תיוג כדי לארגן את נתוני הפעילות. הדוגמאות האלה לא ממצות. אפשר גם לשלב אותן במחרוזת התג באמצעות תו מפריד כמו פסיק.

Tag keys דוגמאות לצמדי תג-ערך תיאור
בקשת הצטרפות app=cart
app=frontend
app=catalogsearch
עוזר לזהות את האפליקציה שמפעילה את הפעולה.
סביבה env=prod
env=dev
env=test
env=staging
עוזר לזהות את הסביבה שמשויכת לפעולה.
Framework framework=spring
framework=django
framework=jetty
הפרמטר עוזר לזהות את המסגרת שמשויכת לפעולה.
פעולה action=list
action=retrieve
action=update
עוזר לזהות את הפעולה שבוצעה על ידי הפעולה.
שירות service=payment
service=shipping
עוזר לזהות את המיקרו-שירות שקורא לפעולה.

הערות

  • כשמקצים REQUEST_TAG, הנתונים הסטטיסטיים של כמה שאילתות עם אותו מחרוזת תגים מקובצים בשורה אחת בטבלה query statistics. רק הטקסט של אחת מהשאילתות האלה מוצג בשדה TEXT.
  • כשמקצים REQUEST_TAG, הנתונים הסטטיסטיים של כמה קריאות עם אותו מחרוזת תגים מקובצים בשורה אחת בטבלת read statistics. קבוצת כל העמודות שנקראות מתווספת לשדה READ_COLUMNS.
  • כשמקצים TRANSACTION_TAG, הנתונים הסטטיסטיים של עסקאות עם אותו מחרוזת תגים מקובצים בשורה אחת בטבלה transaction statistics. קבוצת כל העמודות שנכתבות על ידי העסקאות מתווספת לשדה 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;

בטבלה הבאה מפורטים נתונים לדוגמה שמוחזרים מהשאילתה שלנו, שבה יש שלוש אפליקציות – cart,‏ product ו-frontend – שבבעלותן או שהן שולחות שאילתות לאותו מסד נתונים.

אחרי שמזהים את הטרנזקציות עם זמן האחזור הגבוה, אפשר להשתמש בתגים המשויכים כדי לזהות את החלק הרלוונטי בקוד האפליקציה, ולהמשיך לפתור את הבעיה באמצעות סטטיסטיקות של טרנזקציות.

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 כדי למצוא את המקור של שאילתה בעייתית מטבלת query statistics ואת המקור של קריאה בעייתית מטבלת read statistics.

איך מוצאים את זמן האחזור ונתונים סטטיסטיים אחרים של עסקאות מאפליקציה או ממיקרו-שירות מסוימים

אם השתמשתם בשם האפליקציה או בשם המיקרו-שירות במחרוזת התג, תוכלו לסנן את טבלת נתוני העסקאות לפי תגים שמכילים את שם האפליקציה או שם המיקרו-שירות.

נניח שהוספתם עסקאות חדשות לאפליקציית התשלומים ואתם רוצים לראות את זמני האחזור ונתונים סטטיסטיים אחרים של העסקאות החדשות האלה. אם השתמשתם בשם של אפליקציית התשלום בתוך התג, תוכלו לסנן את טבלת הנתונים הסטטיסטיים של העסקאות כך שיוצגו רק התגים שמכילים את 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

באופן דומה, אפשר למצוא שאילתות או קריאות מאפליקציה ספציפית בטבלה query statistics או read statistics באמצעות תגי בקשה.

איתור העסקאות שמעורבות בקונפליקט נעילה

כדי לגלות אילו טרנזקציות ומפתחות שורה חוו זמני המתנה ארוכים לנעילה, אנחנו שולחים שאילתה לטבלה 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
זמרים(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 היא העמודה שבה התרחש העימות של הנעילה בין ReaderShared לבין WriterShared. אפשר גם לזהות את העסקאות התואמות (app=cart,service=order ו-app=cart,service=redis) שבהן מתרחש העימות.

אחרי שמזהים את העסקאות שגורמות לבעיות נעילה, אפשר להתמקד בהן באמצעות Transaction Statistics כדי להבין טוב יותר מה העסקאות עושות, ואם אפשר להימנע מבעיה או לקצר את משך הזמן שבו הנעילות מתבצעות. מידע נוסף זמין במאמר בנושא שיטות מומלצות לצמצום התחרות על נעילה.

המאמרים הבאים