ロックの統計情報

Cloud Spanner は、ある特定期間内にデータベースで発生したトランザクションのロック競合について、主な要因となった行キーとテーブル列を特定できるロックの統計情報を提供します。この統計情報は、SQL ステートメントを使用して SPANNER_SYS.LOCK_STATS* システム テーブルから取得できます。

対象

SPANNER_SYS データは SQL インターフェース(executeQuerygcloud spanner databases execute-sql など)を介してのみ利用できます。Cloud Spanner が提供する他の単一読み取りメソッドは SPANNER_SYS をサポートしていません。

行キーによるロックの統計情報

次の表では、最長待機時間を使用して行キーを追跡します。

  • SPANNER_SYS.LOCK_STATS_TOP_MINUTE: 1 分間隔におけるロック待機時間が最も長い行キー。

  • SPANNER_SYS.LOCK_STATS_TOP_10MINUTE: 10 分間隔におけるロック待機時間が最も長い行キー。

  • SPANNER_SYS.LOCK_STATS_TOP_HOUR: 1 時間間隔におけるロック待機時間が最も長い行キー。

これらのテーブルには以下のプロパティがあります。

  • 各テーブルには、テーブル名で指定された長さの、時間間隔が重複しないデータが含まれています。

  • 時間間隔は時刻に基づきます。1 分の間隔は現在の分が終わる際に終了し、10 分の間隔は正時に始まって 10 分ごとに終了し、1 時間の間隔は正時になるたびに終了します。各間隔が経過すると、Cloud Spanner はすべてのサーバーからデータを収集し、その後すぐに SPANNER_SYS テーブルでデータを使用できるようにします。

    たとえば、午前 11 時 59 分 30 秒に SQL クエリで使用可能な最新の時間間隔は次のとおりです。

    • 1 分: 午前 11:58:00~11:58:59
    • 10 分: 午前 11:40:00~11:49:59
    • 1 時間: 午前 10:00:00~10:59:59
  • Cloud Spanner は、統計情報を開始行キーの範囲ごとにグループ化します。

  • 各行には、指定された期間中に Cloud Spanner が統計情報をキャプチャした特定の開始行キーの範囲のロック待機時間の合計に関する統計が含まれています。

  • 期間中のロック待機に対するすべての行キーの範囲の情報を Cloud Spanner が保存できない場合、指定された期間中にロック待機時間が最も長い行キー範囲が優先されます。

テーブル スキーマ

列名 タイプ 説明
INTERVAL_END TIMESTAMP 含まれているロックの競合が発生した時間間隔の終了。
ROW_RANGE_START_KEY BYTES(MAX) ロックの競合が発生した行キー。行の範囲に競合がある場合、この値はその範囲の開始キーを表します。プラス記号 + は範囲を示します。詳細については、行範囲の開始キーとはをご覧ください。
LOCK_WAIT_SECONDS FLOAT64 行キー範囲のすべての列で記録されたロックの競合の累積ロック待機時間(秒)。
SAMPLE_LOCK_REQUESTS ARRAY<STRUCT<
  lock_mode STRING,
  column STRING>>
この配列の各エントリは、特定の行キーまたは行キー範囲のロック競合を引き起こすサンプルのロック リクエストに対応しています。この配列のサンプルの最大数は 20 です。各サンプルには、次の 2 つのフィールドがあります。

  • lock_mode: リクエストされたロックモード。詳細については、ロックモードをご覧ください。
  • column: ロックの競合が発生した列。この値の形式は tablename.columnname です。

ロックモード

Cloud Spanner オペレーションは、オペレーションが読み取り / 書き込みトランザクションの一部である場合にロックを取得します。読み取り専用トランザクションはロックを取得しません。Cloud Spanner は、異なるロックモードを使用して、所定の時間に特定のデータセルにアクセスできるトランザクションの数を最大化します。ロックの種類によって特徴は異なります。たとえば、一部のロックは複数のトランザクション間で共有できますが、他のロックは共有できません。

トランザクションで次のいずれかのロックモードを取得しようとすると、ロックの競合が発生する場合があります。

  • ReaderShared ロック - トランザクションが commit する準備ができるまで、他の読み取りでデータにアクセスできるようにするロック。この共有ロックは、読み取り / 書き込みトランザクションがデータを読み取るときに取得されます。

  • WriterShared ロック - このロックは、読み取り / 書き込みトランザクションが書き込みを commit しようとするときに取得されます。

  • Exclusive ロック - 排他ロックは、すでに ReaderShared ロックを取得した読み取り / 書き込みトランザクションが読み取りを完了した後に取得されます。排他ロックは ReaderShared ロックからのアップグレードです。排他ロックは、ReaderShared ロックと WriterShared ロックの両方を保持するトランザクションの特殊なケースです。他のトランザクションが同じセルのロックを取得することはできません。

  • WriterSharedTimestamp ロック - 主キーの一部として commit タイムスタンプがあるテーブルに新しい行を挿入するときに取得された特殊なタイプの WriterShared ロック。このタイプのロックは、トランザクションの参加者がまったく同じ行を作成して、互いに競合することを防ぎます。Cloud Spanner は、挿入された行のキーを更新し、その挿入で実行されたトランザクションの commit タイムスタンプと一致させます。

トランザクションのタイプと使用可能なロックの種類について詳しくは、トランザクションをご覧ください。

ロックモードの競合

次の表は、異なるロックモード間で発生する可能性のある競合を示したものです。

ロックモード ReaderShared WriterShared 優先的 WriterSharedTimestamp
ReaderShared ×
WriterShared × 不適用
優先的 不適用
WriterSharedTimestamp 該当なし 不適用

WriterSharedTimestamp ロックは、タイムスタンプを持つ新しい行を主キーの一部として挿入する場合にのみ使用されます。WriterShared ロックと Exclusive ロックは、既存のセルに書き込むか、タイムスタンプなしで新しい行を挿入する際に使用されます。その結果、WriterSharedTimestamp は他のタイプのロックと競合できず、これらのシナリオは上のテーブルで「該当なし」と表示されます。

唯一の例外は ReaderShared です。これは既存の行には適用できないため、WriterSharedTimestamp と競合する可能性があります。たとえば、テーブル全体のスキャンでは、作成されていない行も含めテーブル全体がロックされるため、ReaderSharedWriterSharedTimestamp と競合する可能性があります。

行範囲の開始キーとは

ROW_RANGE_START_KEY 列は、ロックの競合が存在する複合主キーまたは行範囲の開始主キーを識別します。次のスキーマを使用して、例を示します。

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY (SingerId);

CREATE TABLE Albums (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  AlbumTitle   STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE TABLE Songs (
  SingerId     INT64 NOT NULL,
  AlbumId      INT64 NOT NULL,
  TrackId      INT64 NOT NULL,
  SongName     STRING(MAX),
) PRIMARY KEY (SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
  ...
) PRIMARY KEY (UserId, LastAccess);

次の行キーと行キー範囲の表では、範囲はプラス記号「+」で表され、キーのログインを示します。このような場合のキーは、ロックの競合が発生したキー範囲の開始キーを表します。

ROW_RANGE_START_KEY 説明
singers(2) キー SingerId=2 にある Singers テーブル
albums(2,1) キー SingerId=2,AlbumId=1 にある Albums テーブル
songs(2,1,5) キー SingerId=2,AlbumId=1,TrackId=5 にある Songs テーブル
songs(2,1,5+) SongsId=2,AlbumId=1,TrackId=5 で始まる Songs テーブルのキー範囲
albums(2,1+) SingerId=2,AlbumId=1 で始まる Albums テーブルのキー範囲
users(3, 2020-11-01 12:34:56.426426+00:00) キー UserId=3, LastAccess=commit_timestamp にある Users テーブル

統計情報の集計

SPANNER_SYS には、Cloud Spanner によって特定の期間にキャプチャされたロックの統計情報の集計データを保存するテーブルも含まれています。

  • SPANNER_SYS.LOCK_STATS_TOTAL_MINUTE: 1 分間のすべてのロック待機の統計情報を集計します。

  • SPANNER_SYS.LOCK_STATS_TOTAL_10MINUTE: 10 分間のすべてのロック待機の統計情報を集計します。

  • SPANNER_SYS.LOCK_STATS_TOTAL_HOUR: 1 時間のすべてのロック待機の統計情報を集計します。

統計情報の集計テーブルには次のプロパティがあります。

  • 各テーブルには、テーブル名で指定された長さの、時間間隔が重複しないデータが含まれています。

  • 時間間隔は時刻に基づきます。1 分の間隔は現在の分が終わるときに終了し、10 分の間隔は正時に始まって 10 分ごとに終了し、1 時間の間隔は正時になるたびに終了します。

    たとえば、午前 11 時 59 分 30 秒の時点で、ロックの統計情報の集計に対する SQL クエリで使用可能な最新の間隔は次のとおりです。

    • 1 分: 午前 11:58:00~11:58:59
    • 10 分: 午前 11:40:00~11:49:59
    • 1 時間: 午前 10:00:00~10:59:59
  • 各行には、指定した時間間隔で、データベースに対するすべてのロック待機の統計がまとめて含まれています。時間間隔ごとに 1 行のみとなります。

  • SPANNER_SYS.LOCK_STATS_TOTAL_* テーブルにキャプチャされた統計には、Cloud Spanner が SPANNER_SYS.LOCK_STATS_TOP_* テーブルでキャプチャしなかったロック待機が含まれます。

テーブル スキーマ

列名 説明
INTERVAL_END TIMESTAMP ロックの競合が発生した期間の終了時点。
TOTAL_LOCK_WAIT_SECONDS FLOAT64 データベース全体で記録されたロック競合のロック待機時間の合計(秒)。

クエリの例

次に、ロックの統計情報を取得するために使用できる SQL ステートメントの例を示します。これらの SQL ステートメントは、クライアント ライブラリgcloud コマンドライン ツール、Cloud Console を使用して実行できます。

過去 1 分間のロックの統計情報のリスト表示

次のクエリは、最新の 1 分間の間隔におけるロックの競合の合計の一部を含む、ロック競合のある各行キーのロック待機情報を返します。

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

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_minute t, spanner_sys.lock_stats_top_minute s
WHERE t.interval_end =
  (SELECT MAX(interval_end)
   FROM spanner_sys.lock_stats_total_minute)
AND s.interval_end = t.interval_end
ORDER BY s.lock_wait_seconds DESC;
クエリ出力
row_range_start_key total_lock_wait_seconds lock_wait_seconds frac_of_total sample_lock_requests
Songs(2,1,1) 0.184476 0.184476 1 LOCK_MODE: ReaderShared

COLUMN: Singers.SingerInfo

LOCK_MODE: WriterShared

COLUMN: Singers.SingerInfo
Users(3, 2020-11-01 12:34:56.426426+00:00) 5.21 2.37 0.454 LOCK_MODE: ReaderShared

COLUMN: users._exists1

LOCK_MODE: WriterShared

COLUMN: users._exists1

1 _exists は、特定の行が存在するかどうかを確認するために使用する内部フィールドです。

データの保持

Cloud Spanner は最低でも、次の期間中に各テーブルのデータを保持します。

  • SPANNER_SYS.LOCK_STATS_TOP_MINUTE および SPANNER_SYS.LOCK_STATS_TOTAL_MINUTE: 過去 6 時間を対象とする間隔。

  • SPANNER_SYS.LOCK_STATS_TOP_10MINUTE および SPANNER_SYS.LOCK_STATS_TOTAL_10MINUTE: 過去 4 日間を対象とするインターフェース。

  • SPANNER_SYS.LOCK_STATS_TOP_HOUR および SPANNER_SYS.LOCK_STATS_TOTAL_HOUR: 過去 30 日間を対象とする間隔。

ロック統計情報を使用したデータベースでのロックの競合のトラブルシューティング

以降の手順では、ロックの統計情報を使用してデータベース内のロックの競合を調査する方法を説明します。

会社のアプリの速度が遅いという苦情があるとします。次の手順を実行して、問題を調査します。

調査する期間を選択する

Cloud Spanner データベースのレイテンシの指標を調べて、アプリのレイテンシと CPU 使用率が高い期間を確認します。たとえば、2020 年 11 月 12 日午後 10 時 50 分頃に問題が発生したとします。

選択した期間のロックの待機時間とともにトランザクションの commit レイテンシが増加しているかどうか判断する

ロックはトランザクションによって取得されるため、ロックの競合が長い待機時間の原因となっている場合、ロックの待機時間の増加とともにトランザクションの commit レイテンシの増加を認識できるはずです。

調査を開始する期間を選択したら、その時刻前後のロックの統計情報 LOCK_STATS_TOTAL_10MINUTE を含むトランザクションの統計情報 TXN_STATS_TOTAL_10MINUTE を結合し、ロックの待機時間の増加が平均 commit レイテンシの増加が寄与したかどうかを把握します。

SELECT t.interval_end, t.avg_commit_latency_seconds, l.total_lock_wait_seconds
FROM spanner_sys.txn_stats_total_10minute t
LEFT JOIN spanner_sys.lock_stats_total_10minute l
ON t.interval_end = l.interval_end
WHERE
  t.interval_end >= "2020-11-12T21:50:00Z"
  AND t.interval_end <= "2020-11-12T23:50:00Z"
ORDER BY interval_end;

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

interval_end avg_commit_latency_seconds total_lock_wait_seconds
2020-11-12 21:40:00-07:00 0.002 0.090
2020-11-12 21:50:00-07:00 0.003 0.110
2020-11-12 22:00:00-07:00 0.002 0.100
2020-11-12 22:10:00-07:00 0.002 0.080
2020-11-12 22:20:00-07:00 0.030 0.240
2020-11-12 22:30:00-07:00 0.034 0.220
2020-11-12 22:40:00-07:00 0.034 0.218
2020-11-12 22:50:00-07:00 3.741 780.193
2020-11-12 23:00:00-07:00 0.042 0.240
2020-11-12 23:10:00-07:00 0.038 0.129
2020-11-12 23:20:00-07:00 0.021 0.128
2020-11-12 23:30:00-07:00 0.038 0.231

この結果は、2020 年 11 月 12 日 午後 10 時 40 分 00 秒から2020 年 11 月 12 日 午後 10 時 50 分 00 秒までの同じ期間に avg_commit_latency_secondstotal_lock_wait_seconds が急激に増加し、その後減少したことを示します。留意する点は、avg_commit_latency_seconds が、commit ステップにのみ費やされた平均時間であることです。一方、total_lock_wait_seconds はその期間のロック時間の合計であるため、トランザクションの commit 時間よりもかなり長いように見えます。

ロックの待機時間の遅延と書き込みのレイテンシに密接な関係があることが確認できたので、次のステップでは、どの行と列が長い待機時間の原因となっているかを調べます。

選択した期間でのロックの待機時間が長かった行キーと列を見つける

調査している期間中のロックの待機時間が長かった行キーと列を特定するには、LOCK_STAT_TOP_10MINUTE テーブルに対してクエリを実行します。このテーブルには、ロックの待機に最も貢献した行キーと列がリスト表示されます。

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

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-11-12T22:50:00Z" 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) 780.193 780.193 1 LOCK_MODE: WriterShared

COLUMN: Singers.SingerInfo

LOCK_MODE: ReaderShared

COLUMN: Singers.SingerInfo

このテーブルの結果から、Singers テーブルのキー SingerId=32 で競合が発生していることがわかります。Singers.SingerInfo は、ReaderShared と WriterShared の間でロックの競合が発生した列です。

これは、あるトランザクションが特定のセルを読み取ろうとするときに、もう 1 つのトランザクションが同じセルへの書き込みを試みている場合に発生する一般的なタイプの競合です。これで、トランザクションがロックを引き起している正確なデータセルが判明しました。次のステップでは、ロックを引き起こしているトランザクションを特定します。

ロックの競合に関係する列にアクセスしているトランザクションを見つける

どのトランザクションがこのロックの競合に関連する列の読み取りまたは書き込みを試行したのかを調べるには、次のクエリのロックとトランザクションの統計情報を結合します。

TXN_STATS_TOTAL_10MINUTE から fprintread_columnswrite_constructive_columnsavg_commit_latency_seconds を取得することにより、トランザクションを特定できます。

SELECT
  fprint,
  read_columns,
  write_constructive_columns,
  avg_commit_latency_seconds
FROM spanner_sys.txn_stats_top_10minute t2
WHERE (
  EXISTS (
    SELECT * FROM t2.read_columns columns WHERE columns IN (
      SELECT DISTINCT(req.COLUMN)
      FROM spanner_sys.lock_stats_top_10minute t, t.SAMPLE_LOCK_REQUESTS req
      WHERE req.LOCK_MODE = "ReaderShared" AND t.interval_end ="2020-11-12T23:50:00Z"))
OR
  EXISTS (
    SELECT * FROM t2.write_constructive_columns columns WHERE columns IN (
      SELECT DISTINCT(req.COLUMN)
      FROM spanner_sys.lock_stats_top_10minute t, t.SAMPLE_LOCK_REQUESTS req
      WHERE req.LOCK_MODE = "WriterShared" AND t.interval_end ="2020-11-12T23:50:00Z"))
)
AND t2.interval_end ="2020-11-12T23:50:00Z"
ORDER BY avg_commit_latency_seconds DESC;

このクエリは、調査対象の期間中に次のトランザクションを識別します。

  • ReaderShared ロックの取得を試行中に、ロックの競合が発生した列から読み取りを行うトランザクション。

  • WriterShared ロックの取得を試行中に、ロックの競合が発生した列への書き込みを行うトランザクション。

クエリ結果は avg_commit_latency_seconds 列で並べ替えられるため、ロック待機の影響を最も受けやすいトランザクションが最初に返されます。

fprint read_columns write_constructive_columns avg_commit_latency_seconds
1866043996151916800


['Singers.SingerInfo',
'Singers.FirstName',
'Singers.LastName',
'Singers._exists']
['Singers.SingerInfo'] 4.89
4168578515815911936 [] ['Singers.SingerInfo'] 3.65

クエリ結果に示されているように、2 つのトランザクションが Singers.SingerInfo にアクセスしようとしました。これは、この期間中にロックが競合した列です。ロックの競合を引き起こすトランザクションを特定したら、そのフィンガープリント(fprint)を使用してトランザクションを分析し、その問題の原因と思われる問題を特定できます。

fprint=1866043996151916800 を使用してトランザクションを確認した結果、このトランザクションの DML が主キー SingerId でフィルタリングされていないことが判明しました。そのため、トランザクションが commit するまで、テーブル全体がスキャンされ、そのテーブルはロックされていました。SingerId がわかっている場合は、ステートメント内の WHERE 句を SingerId に対する条件に更新できます。更新する行の ID がわからない場合は、まず別の読み取り専用トランザクションを使用して ID をフェッチしてから、別の読み取り / 書き込みトランザクションを送信して、その更新 ID に基づいて行を検索します。

ロックの競合を減らすためのベスト プラクティスを適用する

今回のシナリオの例では、更新時にテーブルの主キーを使用しなかったトランザクションに、ロックとトランザクションの統計情報を使用して問題を絞り込むことができました。事前に更新の必要な行のキーがあることを認識しているかどうかに基づいてトランザクションを改善するアイデアにたどり着きました。

ソリューションの潜在的な問題を調べる場合や、ソリューションを設計する際にも、データベース内のロックの競合の数を減らすためにこれらのベスト プラクティスを検討してください。

  • 可能な限り、ロックを取得しないため、読み取り専用トランザクションを使用します。

  • 読み取り / 書き込みトランザクションでのテーブルの完全なスキャンを避けます。これには、主キーでの条件付き DML の書き込みや、Read API を使用する場合の特定のキー範囲の割り当てが含まれます。

  • データが読み取り / 書き込みトランザクションで読み取り可能になった直後に変更を commit することで、ロック期間を短縮します。読み取り / 書き込みトランザクションでは、変更が正常に commit されてからデータを読み取ると、データは変更されません。これを実現するには、読み取り時と commit 時にデータセルをロックする必要があります。そのため、ロックの期間を短めにしておくと、トランザクションでロックの競合が発生する可能性が低くなります。

  • 大規模なトランザクションよりも小規模なトランザクションを優先するか、長時間実行の DML トランザクションのパーティション化 DML を検討してください。長時間実行するトランザクションは、長いロックを取得します。そのため、可能な限り、数千の行から成るトランザクションを数百行を更新する複数の小さなトランザクションに分解してください。

  • 読み取り / 書き込みトランザクションが提供している保証が不要な場合は、読み取り / 書き込みトランザクションでデータを読み取ってから変更を commit するという操作を避けてください。たとえば、別の読み取り専用トランザクションでデータを読み取ってください。ロックの競合の多くは、読み取りと commit の間でデータが変更されないことを厳格に保証することによって発生します。したがって、読み取り / 書き込みトランザクションでデータを読み取られない場合は、長期間にわたってセルをロックする必要はありません。

  • スキーマ設計のベスト プラクティスに従います。

次のステップ