アクティブ/アクティブ競合について
pgactive をアクティブ/アクティブモードで使用する場合、複数のノードから同じテーブルに書き込むと、データの競合が発生する可能性があります。一部のクラスタリングシステムでは、同時アクセスを防ぐために分散ロックを使用していますが、pgactive は地理的に分散されたアプリケーションに適した楽観的なアプローチを採用しています。
一部のデータベースクラスタリングシステムは、分散ロックを使用して同時データアクセスを防止します。このアプローチはサーバーが近接している場合に機能しますが、優れたパフォーマンスのためには非常に低いレイテンシーが必要となるため、地理的に分散したアプリケーションはサポートしていません。pgactive 拡張機能は、分散ロック (悲観的アプローチ) を使用する代わりに、楽観的アプローチを使用します。つまり、次のことを意味します。
-
可能な場合には競合を回避できます。
-
特定のタイプの競合の発生を許可します。
-
競合が発生した場合に競合を解決します。
このアプローチにより、分散アプリケーションを構築する際の柔軟性が向上します。
競合の発生方法
ノード間の競合は、関連するすべてのトランザクションが同じノードで同時に発生した場合には発生しない一連のイベントから発生します。ノード間の変更はトランザクションのコミット後にのみ行われるため、各トランザクションはコミットされたノードで個別に有効ですが、その間に他の作業を行った別のノードで実行された場合は有効になりません。pgactive 適用は基本的に他のノードでトランザクションを再実行するため、適用されているトランザクションと受信側ノードでコミットされたトランザクションの間に競合がある場合、再実行オペレーションが失敗する可能性があります。
すべてのトランザクションが単一のノードで実行されている場合にほとんどの競合が発生しないのは、PostgreSQL にそれを防ぐためのトランザクション間通信メカニズムがあるからです。これには、以下が含まれます。
-
UNIQUE インデックス
-
SEQUENCE
-
行とリレーションのロック
-
SERIALIZABLE 依存関係の追跡
これらのメカニズムはすべて、望ましくない同時実行の問題を防ぐためにトランザクション間で通信する方法です。
pgactive は、分散トランザクションマネージャーやロックマネージャーを使用しないため、低レイテンシーを実現し、ネットワークパーティションを適切に処理します。ただし、これは、異なるノード上のトランザクションが相互に完全に分離して実行されることを意味します。通常、分離はデータベースの一貫性を向上させますが、この場合は、競合を防ぐために分離を減らす必要があります。
競合のタイプ
発生する可能性のある競合には、以下が含まれます。
トピック
PRIMARY KEY または UNIQUE の競合
行の競合は、複数のオペレーションが単一のノードで同じ行キーを変更しようとしたときに発生します。これらの競合は、最も一般的なタイプのデータ競合です。
pgactive は検出された競合を、最終更新を優先する処理またはカスタム競合ハンドラーで解決します。
行の競合には以下が含まれます。
-
INSERT と INSERT
-
INSERT と UPDATE
-
UPDATE と DELETE
-
INSERT と DELETE
-
DELETE と DELETE
-
INSERT と DELETE
INSERT/INSERT の競合
この最も一般的な競合は、2 つの異なるノードの INSERT が同じ PRIMARY KEY 値 (または PRIMARY KEY が存在しない場合は同じ UNIQUE 制約値) を持つタプルを作成するときに発生します。
pgactivelink は、発信元ホストのタイムスタンプを使用して最新のタプルを保持することで、INSERT の競合を解決します。このデフォルトの動作は、カスタム競合ハンドラーで上書きできます。このプロセスでは特別な管理者アクションは必要ありませんが、pgactivelink はノード全体で 1 つの INSERT オペレーションを破棄することに注意してください。カスタムハンドラーが実装しない限り、自動データマージは発生しません。
pgactivelink は、単一の制約違反に関連する競合のみを解決できます。INSERT が複数の UNIQUE 制約に違反している場合は、追加の競合解決戦略を実装する必要があります。
複数の UNIQUE 制約に違反する INSERT
INSERT/INSERT 競合は、PRIMARY KEY を含む複数の UNIQUE 制約に違反する可能性があります。pgactivelink は、単一の UNIQUE 制約を含む競合のみを処理できます。競合が複数の UNIQUE 制約に違反すると、適用ワーカーは失敗し、次のエラーを返します。
multiple unique constraints violated by remotely INSERTed tuple.
古いバージョンでは、この状況では代わりに「一意性の相違による競合」エラーが発生しました。
これらの競合を解決するには、手動でアクションを実行する必要があります。競合するローカルタプルを DELETE するか、UPDATE して新しいリモートタプルとの競合を削除します。複数の競合するタプルに対処する必要がある場合があることに注意してください。現在、pgactivelink には、複数の一意の制約に違反するタプルを無視、破棄、またはマージする組み込み機能はありません。
注記
詳細については、「複数の UNIQUE 制約に違反する UPDATE」を参照してください。
UPDATE/UPDATE の競合
この競合は、2 つのノードが PRIMARY KEY を変更せずに同じタプルを同時に変更した場合に発生します。pgactivelink は、定義されている場合には、最終更新を優先するロジックまたはカスタム競合ハンドラーを使用してこれらの競合を解決します。PRIMARY KEY は、タプルマッチングと競合の解決に不可欠です。PRIMARY KEY のないテーブルの場合、pgactivelink は次のエラーで UPDATE オペレーションを拒否します。
Cannot run UPDATE or DELETE on table (tablename) because it does not have a
primary key.
PRIMARY KEY の UPDATE 競合
pgactive は、PRIMARY KEY 更新を処理する際に制限があります。PRIMARY KEY で UPDATE オペレーションを実行することはできますが、pgactive はこれらのオペレーションに最終更新を優先するロジックを使用して競合を自動的に解決することはできません。PRIMARY KEY の更新が既存の値と競合しないようにする必要があります。PRIMARY KEY の更新中に競合が発生した場合、相違による競合が発生し、手動による介入が必要になります。これらの状況の処理の詳細については、「相違による競合」を参照してください。
複数の UNIQUE 制約に違反するUPDATE
pgactivelink は、受信する UPDATE が複数の UNIQUE 制約または PRIMARY KEY 値に違反している場合、最終更新を優先する競合解決を適用できません。この動作は、複数の制約違反がある INSERT オペレーションに似ています。このような状況では、手動による介入を必要とする相違による競合が発生します。詳細については、「相違による競合」を参照してください。
UPDATE/DELETE の競合
この競合は、あるノードが行を UPDATE し、別のノードが同時にその行を DELETE した場合に発生します。この場合、再実行時に UPDATE/DELETE 競合が発生します。解決策は、カスタム競合ハンドラーが特に指定しない限り、DELETE の後に送信される UPDATE をすべて破棄することです。
pgactivelink は、タプルを照合して競合を解決するために PRIMARY KEY を必要とします。PRIMARY KEY のないテーブルの場合、次のエラーで DELETE オペレーションが拒否されます。
Cannot run UPDATE or DELETE on table (tablename) because it does not have a
primary key.
注記
pgactivelink は、UPDATE/DELETE と INSERT/UPDATE の競合を区別できません。どちらの場合も、UPDATE は存在しない行に影響します。非同期レプリケーションおよびノード間に再実行順序がないことにより、pgactivelink は UPDATE が新しい行 (INSERT をまだ受信していない) に対するものか、削除された行に対するものかを判断できません。どちらのシナリオでも、pgactivelink は UPDATE を破棄します。
INSERT/UPDATE の競合
この競合は、マルチノード環境で発生する可能性があります。これは、あるノードが行を INSERT し、2 番目のノードが行を UPDATE し、3 番目のノードが元の INSERT の前に UPDATE を受信したときに発生します。デフォルトでは、カスタム競合トリガーで特に指定されていない限り、pgactivelink は UPDATE を破棄することで、これらの競合を解決します。この解決方法により、ノード間でデータの不整合が発生する可能性があることに注意してください。同様のシナリオとその処理の詳細については、「UPDATE/DELETE の競合」を参照してください。
DELETE/DELETE の競合
この競合は、2 つの異なるノードが同じタプルを同時に削除した場合に発生します。pgactivelink は、両方の DELETE オペレーションの結果が同じであるため、これらの競合を無害と見なします。このシナリオでは、pgactivelink はデータ整合性に影響を与えることなく、一方の DELETE オペレーションを安全に無視します。
外部キー制約の競合
FOREIGN KEY の制約により、既存のローカルデータにリモートトランザクションを適用するときに競合が発生する可能性があります。これらの競合は通常、トランザクションが発信元ノードの論理順序とは異なる順序で適用された場合に発生します。
デフォルトでは、pgactive は session_replication_role を replica
として変更を適用し、レプリケーション中に外部キーチェックをバイパスします。アクティブ/アクティブ設定では、これが外部キー違反につながる可能性があります。ほとんどの違反は一時的なものであり、レプリケーションが追いつくと解決されます。ただし、pgactive はクロスノードの行ロックをサポートしていないため、外部キーのダングリングが発生する可能性があります。
この動作は、分断耐性のある非同期アクティブ/アクティブシステムに固有のものです。例えば、ノード A が新しい子行を挿入すると同時に、ノード B がその親行を削除する場合があります。システムは、ノード間でのこのタイプの同時変更を防ぐことはできません。
外部キーの競合を最小限に抑えるには、以下のことをお勧めします。
-
外部キー関係を密接に関連するエンティティに制限します。
-
可能な場合は、単一のノードから関連エンティティを変更します。
-
ほとんど変更を必要としないエンティティを選択します。
-
変更に対してアプリケーションレベルの同時実行制御を実装します。
排他制約の競合
pgactivelink は排他制約をサポートしておらず、作成を制限します。
注記
既存のスタンドアロンデータベースを pgactivelink データベースに変換する場合は、すべての排他制約を手動で削除します。
分散非同期システムでは、制約に違反する行のセットが存在しないことは保証できません。これは、異なるノード上のすべてのトランザクションが完全に分離されているためです。排他制約は、排他制約違反のためにどのノードからも別のノードに再実行できなくなる、再実行デッドロックにつながる可能性があります。
pgactivelink に排他制約の作成を強制した場合、またはスタンドアロンデータベースを pgactivelink に変換するときに既存の排他制約を削除しない場合、レプリケーションが中断する可能性があります。レプリケーションの進行状況を回復するには、受信リモートタプルと競合するローカルタプルを削除または変更し、リモートトランザクションを適用できるようにします。
グローバルデータの競合
pgactivelink を使用する場合、ノード間でロールなどの PostgreSQL システム全体のグローバルデータが異なると、競合が発生する可能性があります。これらの競合により、オペレーション (主に DDL) が 1 つのノードで成功してコミットされるものの、他のノードには適用されない可能性があります。
ユーザーが 1 つのノードに存在するものの、別のノードには存在しない場合、レプリケーションの問題が発生する可能性があります。
-
Node1 には
fred
という名前のユーザーがいますが、このユーザーは Node2 には存在しません。 -
fred
が Node1 にテーブルを作成すると、テーブルはfred
を所有者としてレプリケートされます。 -
この DDL コマンドを Node2 に適用すると、ユーザー
fred
が存在しないため、失敗します。 -
この失敗により、Node2 の PostgreSQL ログに ERROR が生成され、
pgactive.pgactive_stats.nr_rollbacks
カウンターが増分されます。
解決策: Node2 でユーザー fred
を作成します。ユーザーが同じアクセス許可を持つ必要はありませんが、両方のノードに存在する必要があります。
1 つのノードにテーブルが存在するものの、別のノードには存在しない場合、データ変更オペレーションは失敗します。
-
Node1 には、Node2 に存在しない
foo
という名前のテーブルがあります。 -
Node1 の
foo
テーブルに対する DML オペレーションは、Node2 にレプリケートされると失敗します。
解決策: 同じ構造で Node2 にテーブル foo
を作成します。
注記
pgactivelink は現在、CREATE USER コマンドまたは DDL オペレーションをレプリケートしません。DDL レプリケーションは将来のリリースが予定されています。
ロックの競合とデッドロックの中止
pgactive 適用プロセスは通常のユーザーセッションのように動作するため、標準的な行とテーブルのロックルールに従います。そのため、pgactivelink 適用プロセスは、ユーザートランザクションまたは他の適用プロセスによって保持されているロックを待機する可能性があります。
以下のタイプのロックは適用プロセスに影響を与える可能性があります。
-
ユーザーセッションによる明示的なテーブルレベルのロック (LOCK TABLE...)
-
ユーザーセッションによる明示的な行レベルのロック (SELECT ... FOR UPDATE/FOR SHARE)
-
外部キーからのロック
-
ローカルアクティビティまたは他のサーバーからの行の UPDATE、INSERT、または DELETE による暗黙的なロック
デッドロックは、次の間で発生する可能性があります。
-
pgactivelink 適用プロセスとユーザートランザクション
-
2 つの適用プロセス
デッドロックが発生すると、PostgreSQL のデッドロック検出機能は問題のトランザクションのうち 1 つを終了します。pgactivelink 適用ワーカーのプロセスが終了すると、自動的に再試行され、通常は成功します。
注記
-
これらの問題は一時的なものであり、通常は管理者の介入は必要ありません。適用プロセスがアイドル状態のユーザーセッションのロックによって長期間ブロックされている場合は、ユーザーセッションを終了してレプリケーションを再開できます。この状況は、ユーザーが別のユーザーセッションに影響を与える長期間にわたるロックを保持する場合に似ています。
-
ロック関連の再生遅延を特定するには、PostgreSQL で
log_lock_waits
機能を有効にします。
相違による競合
ノード間で同一であるはずのデータが予期せず異なる場合、相違による競合が発生します。これらの競合は発生すべきではないものの、現在の実装ではすべてを確実に防止できるわけではありません。
注記
行の PRIMARY KEY を変更すると、すべてのノードが変更を処理する前に別のノードが同じ行のキーを変更した場合、相違による競合が発生する可能性があります。プライマリキーの変更を避けるか、変更を指定された 1 つのノードに制限してください。詳細については、「PRIMARY KEY の UPDATE 競合 」を参照してください。
行データが関係した相違による競合には通常、管理者の介入が必要です。これらの競合を解決するには、pgactive.pgactive_do_not_replicate
を使用してレプリケーションを一時的に無効にし、一方のノードのデータをもう一方のノードに合わせて手動で調整する必要があります。これらの競合は、pgactive を記載通りに使用し、安全でないとマークされた設定や関数を回避すれば、発生しません。
管理者は、これらの競合を手動で解決する必要があります。競合タイプによっては、pgactive.pgactive_do_not_replicate
などの高度なオプションを使用する必要があります。不適切に使用すると状況が悪化する可能性があるため、これらのオプションは慎重に使用してください。競合にはさまざまな可能性が考えられるため、普遍的な解決の手順を提供することはできません。
異なるノード間で同一であるはずのデータが予期せず異なる場合、相違による競合が発生します。これらの競合は発生すべきではないものの、現在の実装ではこのような競合をすべて確実に防止できるわけではありません。
競合の回避または許容
ほとんどの場合、適切なアプリケーション設計を使用することで、競合を回避したり、アプリケーションが競合を許容するようにしたりできます。
競合は、複数のノードで同時操作が行われた場合にのみ発生します。競合を回避するには、以下を行います。
-
1 つのノードにのみ書き込む
-
各ノードの独立したデータベースサブセットに書き込む (各ノードに個別のスキーマを割り当てるなど)
INSERT と INSERT の競合の場合は、グローバルシーケンスを使用して競合を完全に防止します。
競合が許容できないユースケースの場合は、アプリケーションレベルで分散ロックを実装することを検討してください。多くの場合、最善の方法は、すべての競合を防ぐのではなく、pgactive の競合解決メカニズムと連携するようにアプリケーションを設計することです。詳細については、「競合のタイプ」を参照してください。
競合のログ記録
pgactivelink は、競合インシデントを pgactive.pgactive_conflict_history
テーブルに記録し、アクティブ/アクティブ競合の診断と処理に役立つようにします。このテーブルへの競合ログは、pgactive.log_conflicts_to_table
を true に設定した場合にのみ記録されます。pgactive 拡張機能は、log_min_messages が LOG
または lower
に設定されている場合に、pgactive.log_conflicts_to_table
設定に関係なく、競合を PostgreSQL ログファイルにも記録します。
競合履歴テーブルは、以下の目的で使用します。
-
アプリケーションに競合が起きる頻度を測定する
-
競合が発生する場所を特定する
-
アプリケーションを改善し、競合率を低減する
-
競合の解決策により望ましい結果が得られないケースを検出する
-
ユーザー定義の競合トリガーまたはアプリケーション設計の変更が必要な場所を特定する
行が競合した場合、オプションで行の値をログに記録できます。これは pgactive.log_conflicts_to_table
設定によって制御されます。以下の点に注意してください。
-
これはデータベース全体のグローバルオプションです。
-
行値のログ記録をテーブル別に制御することはできません。
-
フィールド番号、配列要素、またはフィールド長に制限はありません。
-
競合を引き起こす可能性のある数メガバイトの行を使用する場合は、この機能を有効にすることはお勧めできません。
競合履歴テーブルには、データベース内のすべてのテーブルのデータ (それぞれ異なるスキーマを持つ可能性がある) が含まれているため、ログに記録された行値は JSON フィールドとして保存されます。JSON は、SQL から直接呼び出す場合と同様に、row_to_json
を使用して作成されます。PostgreSQL では json_to_row
関数が提供されていないため、ログに記録された JSON から複合型のタプルを再構築するには、テーブル固有のコード (PL/pgSQL、PL/Python、PL/Perl など) が必要です。
注記
ユーザー定義の競合のサポートは、今後の拡張機能として計画されています。