Column-level conflict detection#

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

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

テーブルtに2つの整数列aおよびbと、単一の行(1,1) がある単純な例を考えます。 1つのノードで次を実行します。

UPDATE t SET a = 100

別のノードでは、前述のUPDATE を受信する前に、次のことを同時に実行します。

UPDATE t SET b = 100

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

多くのユースケースでは、この動作は望ましいおよび予想される動作です。ただし、一部のユースケースでは、これが問題になる場合があります。たとえば、アプリケーションの各部分が別のノードに接続され、共有テーブルの列の専用サブセットを更新するマルチノードクラスターを考えます。その場合、さまざまなコンポーネントが競合し、変更を上書きする可能性があります。

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

前の例に適用すると、どのノードもそのような行を認識しないにもかかわらず、結果は両方のノードで(100,100) になります。

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

列レベルの競合解決には、テーブルにREPLICA IDENTITY FULL が必要です。 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,
db(# 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 を使用して無効にできます。

現在、コミットタイムスタンプには、

EDB Postgres Distributed Release notes で説明する制限があります。

カラムのタイムスタンプの検査#

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

この目的には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 ステートメントに含まれず、デフォルト値を受け取った列にも適用されます。 PGDは、デフォルト値を持つ属性を検出できますが、それが自動的に含まれたか明示的に指定されたかどうかを知ることはできません。

この状況は、 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);