ロックの統計情報

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

提供状況

SPANNER_SYS データは SQL インターフェースを介してのみ使用できます。例えば:

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 時間の間隔は正時になるたびに終了します。各間隔が経過すると、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
  • Spanner は、統計情報を開始行キーの範囲ごとにグループ化します。

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

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

  • テーブル内のすべての列は null 値を許容できます。

テーブル スキーマ

列名 タイプ 説明
INTERVAL_END TIMESTAMP 含まれているロックの競合が発生した時間間隔の終了。
ROW_RANGE_START_KEY BYTES(MAX) ロックの競合が発生した行キー。行の範囲が競合する場合、この値はその範囲の開始キーを表します。プラス記号 + は範囲を示します。詳しくは、行範囲の開始キーとはをご覧ください。
LOCK_WAIT_SECONDS FLOAT64 行キー範囲のすべての列で記録されたロックの競合の累積ロック待機時間(秒)。
SAMPLE_LOCK_REQUESTS ARRAY<STRUCT<
  column STRING,
  lock_mode STRING,
   transaction_tag STRING>>
この配列の各エントリは、特定の行キー(範囲)でロックを待機するか、他のトランザクションがロックを取得できないようにすることで、ロック競合の原因となったサンプルロック リクエストに対応しています。この配列のサンプルの最大数は 20 です。
各サンプルには次の 3 つのフィールドがあります。ロックの競合に寄与したすべてのロック リクエストは、無作為にサンプリングされるため、競合の半分(ホルダーまたはウェイター)のみがこの配列に記録される可能性があります。

ロックモード

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

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

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

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

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

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

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

ロックモードの競合

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

ロックモード ReaderShared WriterShared Exclusive WriterSharedTimestamp
ReaderShared ×
WriterShared × 該当なし
Exclusive 該当なし
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 には、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_* テーブルにキャプチャされた統計には、Spanner が SPANNER_SYS.LOCK_STATS_TOP_* テーブルでキャプチャしなかったロック待機が含まれます。

  • これらのテーブルの一部の列は、Cloud Monitoring で指標として公開されます。公開される指標は次のとおりです。

    • ロック待機時間

    詳細については、Spanner の指標をご覧ください。

テーブル スキーマ

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

クエリの例

次に、ロックの統計情報を取得するために使用できる SQL ステートメントの例を示します。これらの SQL ステートメントは、クライアント ライブラリgcloud spanner、または Google 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) 2.37 1.76 0.7426 LOCK_MODE: ReaderShared

COLUMN: Singers.SingerInfo

LOCK_MODE: WriterShared

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

COLUMN: users._exists1

LOCK_MODE: WriterShared

COLUMN: users._exists1

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

データの保持

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 日間を対象とする間隔。

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

SQL または Lock Insights ダッシュボードを使用すると、データベースにおけるロックの競合を表示できます。

以下のトピックでは、SQL コードを使用してそのようなロックの競合を調査する方法を説明します。

調査する期間を選択する

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 は、ReaderSharedWriterShared の間でロックの競合が発生した列です。

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

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

ロックの競合が原因で、特定の期間内に commit レイテンシが大幅に発生しているトランザクションを特定するには、SPANNER_SYS.TXN_STATS_TOTAL_10MINUTE テーブルから次の列をクエリする必要があります。

  • fprint
  • read_columns
  • write_constructive_columns
  • avg_commit_latency_seconds

SPANNER_SYS.LOCK_STATS_TOP_10MINUTE テーブルで特定されたロックされた列をフィルタリングする必要があります。

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

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

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;

クエリ結果は avg_commit_latency_seconds 列で並べ替えられるため、commit レイテンシが最も高いトランザクションが最初に表示されます。

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 を使用してトランザクションを確認した後、read_columns 列と write_constructive_columns 列を使用して、アプリケーション コードのどの部分がトランザクションをトリガーしたかを特定できます。次に、主キー SingerId でフィルタリングされていない基盤となる DML を表示できます。これにより、テーブル全体がスキャンされ、トランザクションが commit されるまでテーブルがロックされました。

ロック競合を解決するには、次の操作を行います。

  1. 読み取り専用トランザクションを使用して、必要な SingerId 値を特定します。
  2. 別の読み取り / 書き込みトランザクションを使用して、必要な SingerId 値の行を更新します。

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

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

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

  • 読み書きトランザクションで大量の読み取りを回避する

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

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

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

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

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

  • 読み取り / 書き込みトランザクションに必要な最小限の列セットのみを指定します。Spanner ロックはデータセルごとに行われるため、読み取り / 書き込みトランザクションで列を読み取る量が多すぎると、これらのセルに対する ReaderShared ロックが取得されます。これにより、他のトランザクションが過剰な列への書き込みで WriterShared ロックを取得するときに、ロック競合が発生する可能性があります。たとえば、読み取り時に * ではなく一連の列を指定することを検討してください。

  • 読み取り / 書き込みトランザクションでの API 呼び出しを最小限に抑えます。API 呼び出しはネットワークの遅延やサービス側の遅延の影響を受けるため、API 呼び出しのレイテンシが Spanner のロック競合を引き起こす可能性があります。可能な限り、読み取り / 書き込みトランザクションの外部で API 呼び出しを行うことをおすすめします。読み取り / 書き込みトランザクション内で API 呼び出しを実行する必要がある場合は、ロック取得期間への影響を最小限に抑えるために、API 呼び出しのレイテンシをモニタリングしてください。

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

次のステップ