Column-level conflict detection

デフォルトでは、競合行レベルで解決されます。つまり、2つのノードからの変更が競合する場合、ローカルまたはリモートのタプルを選択し、もう一方を破棄します。例、2つの競合する変更のコミットタイムスタンプを比較し、新しい方を保持する場合があります。これにより、すべてのノードが同じ結果に収束し、クラスター全体でコミットオーダーのようなセマンティクスが確立されます。

ただし、行レベルではなく列レベルで競合を解決する方が適切な場合があります。

シンプル例を考えます。2つの整数列aとb、および単一の行(1,1) を持つテーブルtがあります。 1つのノードで実行と仮定します。

UPDATE t SET a = 100

別のノードで同時に(先行のUPDATE を受信する前に)実行ます。

UPDATE t SET b = 100

これにより、 UPDATE-UPDATE の競合が発生します。 update_if_newer の競合解決では、コミットのタイムスタンプを比較し、新しい行バージョンを保持します。 2番目のノードが最後にコミットされたと仮定すると、最終的に(1,100) になり、列aへの変更を効果的に破棄します。

多くのユースケースでは、これは望ましい動作ですが、問題になる場合があります。例、アプリケーションの各パートが異なるノードに接続され、共有テーブル内の列の専用サブセットを更新するマルチノードクラスターを考えます。その場合、さまざまなコンポーネントがお互いのつま先を踏んで、変更を上書きする可能性があります。

このようなユースケースでは、特定のテーブルの競合を列レベルで解決する方が適切な場合があります。これを実現するために、 BDRは各列の最終変更のタイムスタンプを個別に追跡し、それを使用して最新の値(基本的にupdate_if_newer )を選択します。

前の例に適用すると、どちらのノードもそのような行を見たことがないにもかかわらず、両方のノードで (100,100) になります。

列レベルの競合解決を考えるときは、テーブルを垂直にパーティション分割されていると見ると便利です。これにより、各更新は1つのスライスのデータにのみ影響します。このアプローチにより、列の異なるサブセットに対する変更間の競合が排除されます。実際、垂直パーティショニングは、列レベルの競合解決の実用的な代替手段になることさえあります。

列レベルの競合解決では、テーブルにREPLICA IDENTITY FULL が必要です。 bdr.alter_table_conflict_detection ファンクションはそれをチェックし、それ以外の場合はエラーで失敗します。

列レベルの競合解決の有効化と無効化

列レベルの競合解決は、 bdr.alter_table_conflict_detection() ファンクションによって管理されます。

例

bdr.alter_table_conflict_detection() の使用方法を確認するには、簡単なテーブルtest_table を作成し、それで列レベルの競合解決を有効にする次の例を考えます。

db=# CREATE TABLE my_app.test_table (id SERIAL PRIMARY KEY, val INT);
CREATE TABLE

db=# ALTER TABLE my_app.test_table REPLICA IDENTITY FULL;
ALTER TABLE

db=# SELECT bdr.alter_table_conflict_detection(
db(# my_app.test_table::regclass, column_modify_timestamp, cts);
 alter_table_conflict_detection
- -------------------------------
 t

db=# \d my_app.test_table

このファンクションは、(ファンクション呼び出しで指定されたように)新しいcts 列を追加しますが、各変更の前に新しい列のタイムスタンプを維持する責任がある2つのトリガー(BEFORE INSERT およびBEFORE UPDATE )も作成しました。

また、新しい列はデフォルト値でNOT NULL を指定しています。これは、 ALTER TABLE ... ADD COLUMN はテーブルの書き換えを実行しないことを意味します。

注釈

bdr.column_timestamps データタイプの列は、悪影響を与える可能性があるため、他の目的で使用しないことをお勧めします。例、テーブルを列レベルの競合解決に切り替えますが、これはトリガーがないと正しく機能しません。

列レベルの競合解決を備えたリストテーブル

次のクエリーで列レベルの競合解決が有効になっているテーブルをリストできます。このクエリーは、 bdr.column_timestamp 型の列の存在を検出します。

SELECT nc.nspname, c.relname
FROM pg_attribute a
JOIN (pg_class c JOIN pg_namespace nc ON c.relnamespace = nc.oid)
  ON a.attrelid = c.oid
JOIN (pg_type t  JOIN pg_namespace nt ON t.typnamespace = nt.oid)
  ON a.atttypid = t.oid
WHERE NOT pg_is_other_temp_schema(nc.oid)
  AND nt.nspname = bdr
  AND t.typname = column_timestamps
  AND NOT a.attisdropped
  AND c.relkind IN (r, v, f, p);

bdr.column_timestamps_create

このファンクションは、列レベルの競合解消を作成します。 column_timestamp_enable 内で呼び出されます。

概要

bdr.column_timestamps_create(p_source cstring, p_timestamp timestampstz)

パラメーター

  • p_source — 2つのオプションは、 current または commit です。

  • p_timestamp —タイムスタンプは、選択したソースによって異なります。 commit の場合、TIMESTAMP_SOURCE_COMMIT 。 current の場合、TIMESTAMP_SOURCE_CURRENT 。

DDLロック

テーブルの列のタイムスタンプを有効または無効にする場合、コードはDDLロックを使用して、スイッチ前からの保留中の変更がないことを保証します。このアプローチにより、両方のタプルまたはどちらでもタイムスタンプの競合のみが表示されます。そうしないと、コードはローカルタプルにタイムスタンプを、リモートのタプルにNULLを予期せずに表示する可能性があります。また、変更がすべてのノードで同じ方法(列レベルまたは行レベル)で解決されるようにします。

現在とコミットのタイムスタンプ

重要な決定は、変更された列に割り当てるタイムスタンプです。

デフォルトでは、変更された列に割り当てられるタイムタイムスタンプは、 clock_timestamp から取得したかのように現在のタイムスタンプです。これはシンプルで、多くの場合、完全に正しいです(例、競合する行が列の重複しないサブセットを変更する場合)。

ただし、さまざまな予期しない効果があります。

  • タイムスタンプはステートメントの実行中に変更されるため、 UPDATE がマルチプルの行に影響を与える場合、それぞれがわずかに異なるタイムスタンプを取得します。これは、同時変更の効果がさまざまな方法で「混在」する可能性があることを意味します(さまざまなノードで実行された変更がどのように正確にインターリーブするかに応じて)。

  • タイムスタンプはコミットタイムスタンプとは無関係であり、競合を解決するために使用すると、結果がコミットオーダーと同等になりません。つまり、シリアル化できない可能性があります。

注釈

将来的にステートメントとトランザクションのタイムスタンプを追加する可能性があります。これにより、同時実行のステートメントまたはトランザクションの影響が混在する問題に対処します。それでも、これらのオプションはいずれもコミット オーダーと同等の結果を生成できません。

実際のコミットタイムスタンプを使用することもできますが、この機能は現在実験的と見なされています。コミットタイムスタンプを使用するには、列レベルの競合解決を有効にするときに最後のパラメータを true に設定します。

SELECT bdr.column_timestamps_enable(test_table::regclass, cts, true);

bdr.column_timestamps_disable を使用して無効にできます。

コミットタイムスタンプには現在、 注意事項 で説明されている制限があります。

列のタイムスタンプの検査

変更された列のタイムスタンプを格納する列は、トリガーによって自動的に維持されます。直接変更しないでください。競合がどのように解決された例を調査しながら、現在のタイムスタンプ値を検査すると役立ちます。

このためには、次の3つの関数が役立ちます。

  • bdr.column_timestamps_to_text(bdr.column_timestamps)

このファンクションは、タイムスタンプマッピングの人間が読める表現形式を返し、値をtext にキャストするときに使用されます。

db=# select cts::text from test_table;
                                                 cts
- ----------------------------------------------------------------------------------------------------
 {source: current, default: 2018-09-23 19:24:52.118583+02, map: [2 : 2018-09-23 19:25:02.590677+02]}
(1 row)
  • bdr.column_timestamps_to_jsonb(bdr.column_timestamps)

このファンクションは、タイムスタンプマッピングのJSONB表現形式を変換し、値をjsonb にキャストするときに使用されます。

db=# select jsonb_pretty(cts::jsonb) from test_table;
                   jsonb_pretty
- --------------------------------------------------
 {                                                +
     "map": {                                     +
         "2": "2018-09-23T19:24:52.118583+02:00"  +
     },                                           +
     "source": "current",                         +
     "default": "2018-09-23T19:24:52.118583+02:00"+
 }
(1 row)
  • bdr.column_timestamps_resolve(bdr.column_timestamps, xid)

このファンクションは、最新のトランザクション(すでにコミットされている場合)によって変更された属性のコミットタイムスタンプでマッピングを更新します。これは、 コミット タイムスタンプを使用する場合にのみ重要です。例、この場合、最後のトランザクションで2番目の属性が更新されました(attnum = 2 を使用)。

test=# select cts::jsonb from test_table;
                                                                  cts
- ---------------------------------------------------------------------------------------------------------------------------------------
 {"map": {"2": "2018-09-23T19:29:55.581823+02:00"}, "source": "commit", "default": "2018-09-23T19:29:55.581823+02:00", "modified": [2]}
(1 row)

db=# select bdr.column_timestamps_resolve(cts, xmin)::jsonb from test_table;
                                               column_timestamps_resolve
- ----------------------------------------------------------------------------------------------------------------------
 {"map": {"2": "2018-09-23T19:29:55.581823+02:00"}, "source": "commit", "default": "2018-09-23T19:29:55.581823+02:00"}
(1 row)

CRDTデータ型を使用した列の競合の処理

デフォルトでは、列レベルの競合解決はより高いタイムスタンプを持つ値を選択し、もう一方を破棄します。ただし、別のより複雑な方法で競合を調整できます。例、情報を破棄せずに競合する値をマージできるCRDTタイプを使用できます。

注意事項

  • UPDATE によって変更される属性は、トリガーの古い行と新しい行を比較することによって決定されます。これは、属性が値を変更しない場合、明示的に設定されても変更されたものとして検出されないことを意味します。例、 UPDATE t SET a = a は、どの行でもa を変更済みとしてマークしません。同様に、 UPDATE t SET a = 1 は、既に1 に設定されている行に対してa を変更済みとしてマークしません。

  • INSERT ステートメントの場合、新しい行と比較する古い行がないため、すべての属性が変更されると見なし、新しいタイムスタンプを割り当てます。これは、 INSERT ステートメントに含まれず、デフォルト値を受け取った列にも適用されます。デフォルト値を持つ属性は検出できますが、自動的に含まれたか、明示的に指定されたかはわかりません。

これは、 INSERT ステートメントが列の異なるサブセットを指定している場合でも、列レベルの競合解決がINSERT-INSERT の競合では機能しないことを効果的に意味します。新しい行には、古い行よりもすべて新しいタイムスタンプがあります。

  • 列を独立して扱うことにより、すべての変更が同じノードで発生する場合に不可能な方法でコンストレインに違反するのは簡単です。例、次のようなテーブルを考えます。

CREATE TABLE t (id INT PRIMARY KEY, a INT, b INT, CHECK (a > b));
INSERT INTO t VALUES (1, 1000, 1);

1つのノードが次のことを行うと仮定します。

UPDATE t SET a = 100;

別のノードが同時に行います。

UPDATE t SET b = 500;

これらの各更新は、最初の行で実行された場合に有効であるため、各ノードに渡されます。ただし、他のノードに複製すると、結果の行はCHECK (A > b) 制約に違反し、問題が手動で解決されるまでレプリケーションは停止します。

・タイムスタンプマッピングを格納するカラムは自動管理されます。予測できない影響を与える可能性があるため、クエリで値を指定またはオーバーライドしないでください。 (とにかく、可能な場合は値を無視します。)

  • タイムスタンプのマッピングはトリガーによって維持されますが、トリガーが実行されるオーダーが重要です。したがって、タプルを変更し、 pgl_clcd_ トリガーの後に実行されるカスタムトリガーがある場合、変更された列は正しく検出されません。

  • 通常のタイムスタンプを使用して変更/コミットをオーダーする場合、競合する変更のタイムスタンプがまったく同じである可能性があります(2つ以上のノードがたまたま同じタイムスタンプを生成したため)。このリスクは、通常の行レベルの競合解決でも発生する可能性があるため、列レベルの競合解決に固有のものではありません。このシチュエーションでは、ノードIDをタイブレーカーとして使用します(高いノードIDが勝ちます)。これにより、すべてのノードに同じ変更が適用されます。

  • 異なるノード間にクロック スキューが存在する可能性があります。やや予期しない動作を引き起こす可能性がありますが(タイムスタンプが逆になっているため、一見新しい変更を破棄します)、パラメーター bdr.maximum_clock_skew および bdr.maximum_clock_skew_action を使用してノード間のクロックスキューを管理できます。

SELECT bdr.alter_node_group_config(group, ignore_redundant_updates := false);