כתיבה שעברה אופטימיזציה של קצב העברת הנתונים

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

סקירה כללית

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

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

התנהגות ברירת מחדל

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

תרחישים נפוצים לדוגמה

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

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

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

הגדרת זמני השהיה שונים לביצוע שינויים

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

מגבלות

אפשר להגדיר זמן השהיה של אישור בין 0 ל-500 אלפיות השנייה. הגדרת השהיות של אישור מעל 500 אלפיות השנייה גורמת לשגיאה.

הגדרת השהיה מקסימלית של שליחת בקשות לאישור

הפרמטר של עיכוב השליחה המקסימלי הוא חלק מהשיטה CommitRequest. אפשר לגשת לשיטה הזו באמצעות RPC API,‏ API בארכיטקטורת REST או באמצעות ספריית הלקוח של Cloud Spanner.

C#‎


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

public class CommitDelayAsyncSample
{
    public async Task<int> CommitDelayAsync(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 =>
        {
            transaction.TransactionOptions.MaxCommitDelay = TimeSpan.FromMilliseconds(100);

            using var insertSingerCmd = connection.CreateInsertCommand("Singers",
                new SpannerParameterCollection
                {
                    { "SingerId", SpannerDbType.Int64, 1 },
                    { "FirstName", SpannerDbType.String, "Marc" },
                    { "LastName", SpannerDbType.String, "Richards" }
                });
            insertSingerCmd.Transaction = transaction;
            int rowsInserted = await insertSingerCmd.ExecuteNonQueryAsync();

            using var insertAlbumCmd = connection.CreateInsertCommand("Albums",
                new SpannerParameterCollection
                {
                    { "SingerId", SpannerDbType.Int64, 1 },
                    { "AlbumId", SpannerDbType.Int64, 2 },
                    { "AlbumTitle", SpannerDbType.String, "Go, Go, Go" }
                });
            insertAlbumCmd.Transaction = transaction;
            rowsInserted += await insertAlbumCmd.ExecuteNonQueryAsync();

            return rowsInserted;
        });
    }
}

המשך


import (
	"context"
	"fmt"
	"io"
	"time"

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

func setMaxCommitDelay(w io.Writer, db string) error {
	// db is the fully-qualified database name of the form `projects/<project>/instances/<instance-id>/database/<database-id>`
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return fmt.Errorf("setMaxCommitDelay.NewClient: %w", err)
	}
	defer client.Close()

	commitDelay := 100 * time.Millisecond
	resp, err := client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		stmt := spanner.Statement{
			SQL: `INSERT Singers (SingerId, FirstName, LastName)
					VALUES (111, 'Virginia', 'Watson')`,
		}
		rowCount, err := txn.Update(ctx, stmt)
		if err != nil {
			return err
		}
		fmt.Fprintf(w, "%d record(s) inserted.\n", rowCount)
		return nil
	}, spanner.TransactionOptions{CommitOptions: spanner.CommitOptions{MaxCommitDelay: &commitDelay, ReturnCommitStats: true}})
	if err != nil {
		return fmt.Errorf("setMaxCommitDelay.ReadWriteTransactionWithOptions: %w", err)
	}
	fmt.Fprintf(w, "%d mutations in transaction\n", resp.CommitStats.MutationCount)
	return nil
}

Java


import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.Options;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerOptions;
import java.time.Duration;
import java.util.Arrays;

public class SetMaxCommitDelaySample {

  static void setMaxCommitDelay() {
    // TODO(developer): Replace these variables before running the sample.
    final String projectId = "my-project";
    final String instanceId = "my-instance";
    final String databaseId = "my-database";

    try (Spanner spanner =
        SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) {
      final DatabaseClient databaseClient = spanner
          .getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
      setMaxCommitDelay(databaseClient);
    }
  }

  static void setMaxCommitDelay(DatabaseClient databaseClient) {
    final CommitResponse commitResponse = databaseClient.writeWithOptions(Arrays.asList(
        Mutation.newInsertOrUpdateBuilder("Albums")
            .set("SingerId")
            .to("1")
            .set("AlbumId")
            .to("1")
            .set("MarketingBudget")
            .to("200000")
            .build(),
        Mutation.newInsertOrUpdateBuilder("Albums")
            .set("SingerId")
            .to("2")
            .set("AlbumId")
            .to("2")
            .set("MarketingBudget")
            .to("400000")
            .build()
    ), Options.maxCommitDelay(Duration.ofMillis(100)));

    System.out.println(
        "Updated data with timestamp + " + commitResponse.getCommitTimestamp() + ".");
  }
}

Node.js

const {Spanner, protos} = require('@google-cloud/spanner');

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

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

async function setMaxCommitDelay() {
  const instance = spanner.instance(instanceId);
  const database = instance.database(databaseId);

  database.runTransaction(async (err, transaction) => {
    if (err) {
      console.error(err);
      return;
    }
    try {
      const [rowCount] = await transaction.runUpdate({
        sql: 'INSERT Singers (SingerId, FirstName, LastName) VALUES (111, @firstName, @lastName)',
        params: {
          firstName: 'Virginia',
          lastName: 'Watson',
        },
      });

      console.log(
        `Successfully inserted ${rowCount} record into the Singers table.`,
      );

      await transaction.commit({
        maxCommitDelay: protos.google.protobuf.Duration({
          seconds: 0, // 0 seconds
          nanos: 100000000, // 100 milliseconds
        }),
      });
    } catch (err) {
      console.error('ERROR:', err);
    } finally {
      // Close the database when finished.
      database.close();
    }
  });
}
setMaxCommitDelay();

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 insert_singers(transaction):
    row_ct = transaction.execute_update(
        "INSERT Singers (SingerId, FirstName, LastName) "
        " VALUES (111, 'Grace', 'Bennis')"
    )

    print("{} record(s) inserted.".format(row_ct))

database.run_in_transaction(
    insert_singers, max_commit_delay=datetime.timedelta(milliseconds=100)
)

Ruby

require "google/cloud/spanner"

##
# This is a snippet for showcasing how to pass max_commit_delay in  commit_options.
#
# @param project_id  [String] The ID of the Google Cloud project.
# @param instance_id [String] The ID of the spanner instance.
# @param database_id [String] The ID of the database.
#
def spanner_set_max_commit_delay project_id:, instance_id:, database_id:
  # Instantiates a client
  spanner = Google::Cloud::Spanner.new project: project_id
  client  = spanner.client instance_id, database_id

  records = [
    { SingerId: 1, AlbumId: 1, MarketingBudget: 200_000 },
    { SingerId: 2, AlbumId: 2, MarketingBudget: 400_000 }
  ]
  # max_commit_delay is the amount of latency in millisecond, this request
  # is willing to incur in order to improve throughput.
  # The commit delay must be at least 0ms and at most 500ms.
  # Default value is nil.
  commit_options = {
    return_commit_stats: true,
    max_commit_delay: 100
  }
  resp = client.upsert "Albums", records, commit_options: commit_options
  puts "Updated data with #{resp.stats.mutation_count} mutations."
end

מעקב אחרי זמן האחזור של בקשות כתיבה

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