Commit At Most Once

Commit At Most Once (CAMO)機能の目的は、アプリケーションが複数回コミットするのを防ぐことです。

CAMOがないと、 COMMIT が送信された後にクライアントが接続を失うと、アプリケーションはサーバーから応答を受信しない可能性があるため、トランザクションがコミットされたかどうかがわかりません。

アプリケーションは、次の2つのオプションのいずれかを簡単に決定できません。

  • 場合によってはデータが2回入力される可能性があるため、同じデータでトランザクションを再試行します

  • トランザクションを再試行せず、データがまったく処理されないリスク

これらはいずれも、価値の高いデータに関する重大なエラーです。

この状況を回避する1つの方法は、トランザクションが一意のインデックスを持つテーブルに少なくとも1つのINSERT を含めるようにすることです。ただし、それはアプリケーションの設計によって異なり、アプリケーション固有のエラー処理ロジックが必要になるため、すべての場合に効果的であるとは限りません。

PGDのCAMO機能は、より一般的なソリューションを提供し、 INSERT を必要としません。 bdr.commit_scope によってアクティブ化されると、アプリケーションは、トランザクション識別子が既に割り当てられている場合、トランザクション識別子を含むメッセージを受け取ります。それ以外の場合、トランザクションの最初のwriteステートメントはその情報をクライアントに送信します。

アプリケーションが明示的なCOMMIT を送信する場合、プロトコルは、アプリケーションがCOMMIT が送信される前にトランザクション識別子の通知を受信することを保証します。サーバーがCOMMIT に応答しない場合、アプリケーションはトランザクション識別子を使用して別のPGDノードからトランザクションの最終ステータスを要求することにより、このエラーを処理できます。以前のトランザクションのステータスがわかっている場合、アプリケーションはトランザクションを再試行するかどうかを安全に決定できます。

CAMOは、同じPGDグループの2つのPGDノードであるパートナーノードのペアを作成することによって機能します。この動作モードでは、ペアの各ノードは他のピアで実行された最近のトランザクションの結果を知っており、特に(必要に応じて) COMMIT 中に切断されたトランザクションの結果を知っています。アプリケーションからトランザクションを受信するノードを「オリジン」、これらのトランザクションを確認するノードを「パートナー」と呼ぶ場合があります。ただし、 CAMOペア内のノードのCAMO構成に違いはありません。ペアは対称です。

警告

CAMOでは、高度なエラー処理を利用するには、ユーザーのアプリケーションを変更する必要があります。パラメーターを有効にするだけでは、保護を得るには十分ではありません。リファレンスクライアント実装は、要求に応じて顧客に提供されます。

要件

CAMOを使用するには、アプリケーションはマルチステートメントリクエストの一部としてではなく、別のリクエストとして明示的なCOMMIT メッセージを発行する必要があります。 CAMOは、プロシージャから発行されたトランザクション、または暗黙的なコミットを使用する単一ステートメントトランザクションのステータスを提供できません。

構成

CAMOの構成は commitスコープへの移行 を介して行われます。

失敗シナリオ

異なる構成では、さまざまな障害シナリオが発生します。

受信側でのデータの永続化

デフォルトでは、PGLライタは、リモートノードからトランザクションを適用するときにbdr.synchronous_commit = off モードで動作します。これはCAMOにも当てはまります。つまり、 CAMOパートナーのディスクに到達する前に、オリジンノードへのトランザクションが確認されることを意味します。クラッシュまたはハードウェア障害が発生した場合、確認されたトランザクションは、 CAMOパートナーで単独では回復できない場合があります。 CAMOパートナーノードが回復するとトランザクションを再分散するため、 CAMOオリジンノードが動作し続けている限り、これは問題ではありません。

これは、 CAMOが単一ノード障害から保護できることを意味します。これは、ローカルモードだけでなく、リモート書き込みとの組み合わせにも適しています。

CAMOペアの両方のノードの停止をカバーするには、 bdr.synchronous_commit = local を使用して、コミット前の確認の前にフラッシュを強制します。これはリモート書き込みモードでもローカルモードでも機能せず、レイテンシーの影響を受けやすいコミットパスにあるCAMOパートナーのI / O要件により、パフォーマンスに影響します。

非同期モード

DEGRADE ON ... TO ASYNC 句がコミットスコープで使用される場合、ノードはそのCAMOパートナーの準備ができているかどうかを検出します。そうでない場合、一時的に非同期(ローカル)モードに切り替わります。このモードの場合、ノードはCAMOモードに戻るまでトランザクションをローカルでコミットします。

これにより、 COMMIT ステータスを取得することはできませんが、一貫性よりも可用性を選択できます。このモードは、単一ノードの障害に耐えることができます。 CAMOペアの両方のノードに障害が発生した場合、可用性を維持するために一致しないコミット決定を選択し、データの不整合につながる可能性があります。

CAMOパートナーが準備完了に切り替えるには、接続する必要があり、推定キャッチアップ間隔がTO ASYNC のtimeout 値を下回る必要があります。 bdr.is_camo_partner_ready でCAMOパートナーの現在の準備状況を確認できます。bdr.node_replication_rates は、キャッチアップ時間の現在の見積もりを提供します。

CAMO保護から非同期モードへの切り替えは、コミットがTO ASYNC のtimeout 値を超えるため、またはCAMOパートナーがすでにわかっている場合はコミット時に切断されるため、実際のCAMOトランザクションによってのみトリガーされます。このスイッチは、推定キャッチアップ間隔とは無関係です。 enable_proxy_routing ノードグループオプションで構成されたように、現在のノードがグループの書き込みリードであることを要求するようにCAMOペアが構成されている場合。構文については、

commitスコープへの移行 を参照してください。これにより、分離されたノードが非同期モードに切り替わることによるスプリットブレインの状況を防ぐことができます。

CAMOグループにenable_proxy_routing が設定されていない場合、起点ノードはすぐに非同期モードに切り替わります。

非同期モードからCAMOモードへの切り替えは、接続を開始するCAMOパートナーノードによって異なります。 CAMOパートナーは、少なくとも30秒ごとに再接続を試行します。したがって、接続が再確立された後、 CAMOパートナーが元のノードに接続するまでに最大30秒かかる場合があります。 CAMOパートナーに蓄積されたラグは、 CAMO保護モードへの切り替えをさらに遅らせます。

通常のCAMO操作中とは異なり、非同期モードでは、追加のコミットオーバーヘッドはありません。 CAMOペアが通常処理できるよりも多くのトランザクションをノードが継続的に処理できるようになるため、これは問題になる可能性があります。 CAMOパートナーが最終的に再接続してトランザクションを適用した場合でも、そのような状況ではラグが増加するだけであり、 CAMO保護を再確立できません。トランザクションスループットを人為的に調整するために、PGDはbdr.camo_local_mode_delay 設定を提供します。これにより、ローカルモードでCOMMIT を任意の時間遅延させることができます。予想されるワークロード中に通常のCAMOモードでコミット時間を測定し、それに応じてこの遅延を構成することをお勧めします。デフォルトは5ミリ秒で、これは非同期ネットワークと比較的迅速なCAMOパートナーの応答を反映しています。

アーキテクチャと可用性の要件を考慮して、非同期モードを許可するかどうかの選択を検討します。次の例は、いくつかの詳細を提供します。

例

この例では、互いのCAMOパートナーである2つのPGDノードを使用したセットアップを検討します。

edb_notranlate_0このCAMOコミットスコープが正当であるためには、グループ内のノードの数がちょうど2に等しい必要があります。非定量化されたグループ式は明確なペアに解決されないため、複数のノードで構成されるグループでALLまたはANY 2を使用するとエラーになりますノードの。

非同期モードの場合

非同期モードが許可されている場合、単一障害点はありません。 1つのノードに障害が発生した場合:

  • 他のノードは、障害が発生したノードのCOMMIT 中に切断されたすべてのトランザクションのステータスを特定できます。

  • 新しい書き込みトランザクションが許可されます。 2番目のノードにも障害が発生した場合、その時点でコミットされていたトランザクションの結果は不明です。

非同期モードなし

非同期モードが許可されていない場合、各ノードはトランザクションをコミットするために他のノードを必要とします。つまり、各ノードは単一障害点です。 1つのノードに障害が発生した場合:

  • 他のノードは、障害が発生したノードのCOMMIT 中に切断されたすべてのトランザクションのステータスを特定できます。

  • ノードが回復するまで、新しい書き込みトランザクションは防止されます。

アプリケーション使用

概要と要件

CAMOは、クライアント側での再試行ループと特定のエラー処理に依存しています。それには3つの側面があります。

  • トランザクションのCOMMIT の結果を確認する必要があり、一時的なエラーの場合、クライアントはトランザクションをリトライする必要があります。

  • COMMIT より前は、クライアントはノードIDとトランザクションID(両方とも32ビット整数)で構成されるトランザクションのグローバル識別子を取得する必要があります。

  • トランザクションのCOMMIT を試行中に現在のサーバーに障害が発生した場合、アプリケーションはCAMOパートナーに接続し、そのトランザクションのステータスを取得し、応答に応じて再試行する必要があります。

アプリケーションは、 COMMIT 中に切断された場合のトランザクションステータスを確認する目的でのみ、グローバルトランザクション識別子を保存する必要があります。特に、アプリケーションは別の永続化レイヤーを必要としません。アプリケーションが失敗した場合、再起動するのに必要なのはデータベース内の情報のみです。

CAMOパートナー接続ステータス

ファンクションbdr.is_camo_partner_connected を使用すると、ペアモードで構成されたCAMOパートナーノードの接続ステータスを確認できます。現在、 Eager Replicationで使用されるCAMOに相当するものはありません。

概要

bdr.is_camo_partner_connected()

戻り値

CAMOパートナーが現在ローカルノード上のWAL送信者プロセスに接続しているため、トランザクションデータを受信して確認を送信できるかどうかを示すブール値。

CAMOパートナーの準備

ファンクションbdr.is_camo_partner_ready を使用すると、ペアモードで構成されたCAMOパートナーノードの準備ステータスを確認できます。下では、これによりローカルモードとの間の切り替えがトリガーされます。

概要

bdr.is_camo_partner_ready()

戻り値

CAMOパートナーが、ローカルノードから発信されるトランザクションをタイムリーに、つまりTO ASYNC のtimeout が期限切れになる前に確認することが合理的に期待できるかどうかを示すブール値。

注釈

この関数は、過去または現在の状態を照会します。正の戻り値は、 CAMOパートナーが将来の取引を確認できるかどうかを示すものではありません。

CAMOパートナーを取得する

この機能は、ローカルノードのCAMOパートナー(ペアモードで構成)を表示します。

bdr.get_configured_camo_partner()

CAMOパートナーからの適用キューが消費されるのを待ちます

関数bdr.wait_for_camo_partner_queue は、デフォルトでCAMOパートナーノードを照会するbdr.wait_for_apply_queue のラッパーです。ローカルノードがCAMOペアの一部でない場合、エラーを返します。

概要

bdr.wait_for_camo_partner_queue()

CAMOノード間のトランザクションステータス

この機能により、 CAMOトランザクションが完全に解決されるのを待つことができます。

bdr.camo_transactions_resolved()

取引状況照会機能

ノードに障害が発生したときにコミットされていたトランザクションのステータスを確認するには、アプリケーションで次の関数を使用する必要があります。

bdr.logical_transaction_status(node_id OID, xid OID,
                               require_camo_partner boolean)

ペアモードで使用するCAMOでは、 CAMOペアの一部であるノードでのみこの機能を使用します。 Eager Replicationとともに 、すべてのノードで使用できます。

どちらの場合も、コミットが発行されてから15分以内に関数を呼び出す必要があります。 CAMOパートナーは、このようなメタ情報を定期的に消去する必要があるため、古いトランザクションに正しい答えを提供できません。

トランザクションのステータスを照会する前に、この関数は受信キューが消費されて完全に適用されるのを待ちます。このメカニズムにより、受信されたがまだ適用されていないトランザクションに対する早期の否定的な回答が防止されます。

その名前にもかかわらず、それは常に読み取り専用の操作ではありません。ステータスが不明な場合、 CAMOパートナーはトランザクションをコミットするかアボートするかを決定し、その決定をローカルに保存して、今後の一貫性を保証します。

クライアントは、オリジンでコミットする前にこの関数を呼び出さないでください。そうしないと、トランザクションが強制的にロールバックされる可能性があります。

概要

bdr.logical_transaction_status(node_id OID, xid OID,
                               require_camo_partner boolean DEFAULT true)

パラメーター

  • node_id —トランザクションの発信元であるPGDノードのノードID。通常、PQパラメーターbdr.local_node_id からCOMMIT の前にクライアントによって取得されます。

  • xid —オリジンノードのトランザクションID。通常、PQパラメーターtransaction_id からCOMMIT の前にクライアントによって取得されます。

  • require_camo_partner —デフォルトはtrueで、構成チェックを有効にします。これらのチェックを無効にし、 CAMOトランザクションではないトランザクションのステータスを照会するには、falseに設定します。

戻り値

この関数は、次のいずれかの結果を返します。

  • 'committed'::TEXT —トランザクションはコミットされ、 CAMOペアの両方のノードで可視であり、最終的に他のすべてのPGDノードにレプリケートされます。クライアントが再試行する必要はありません。

  • 'aborted'::TEXT —トランザクションは中止され、他のPGDノードにレプリケートされません。クライアントは、トランザクションをコミットするために再試行するか、失敗をエスカレートする必要があります。

  • 'in progress'::TEXT —トランザクションはこのローカルノードでまだ進行中であり、まだコミットまたは中止されていません。トランザクションはCOMMITフェーズにあり、 CAMOパートナーがコミットを確認または拒否するのを待っている可能性があります。推奨されるクライアントの反応は、オリジンノードから切断し、 CAMOパートナーに再接続して、代わりに照会することです。ロードバランサーまたはプロキシが間にあり、クライアントが照会するノードを制御できない場合、クライアントは、ステータスが'committed' または'aborted' に切り替わるまでのみ繰り返しポーリングできます。

Eager All-Node Replicationの場合、ピアノードは、まだコミットまたは中止されていないトランザクションに対してこの結果を生成します。この場合、まだ複製されていない(またはオリジンノードで開始されていない)トランザクションでも、ピアPGDノードでin progress 結果が得られる場合があります。ただし、クライアントは、オリジンでコミットする前にトランザクションステータスを照会してはなりません。

  • 'unknown'::TEXT —指定されたトランザクションは不明です。それは未来のものであるか、その特定のノードにまだレプリケートされていないか、過去すぎます。そのようなトランザクションのステータスはまだないか、わかっていません。この戻り値は、クライアントによる不適切な使用の兆候です。

クライアントは、エラー時に関数呼び出しを再試行する準備ができている必要があります。

接続プールとプロキシ

CAMOクラスターを設計するときは、接続プールとプロキシの影響を考慮してください。プロキシは、コミットグループ内のすべてのノード、つまりCAMOペアの両方のノード、またはEager All-Node Replicationの場合はすべてのPGDノードにトランザクションを自由に分散する場合があります )。

アプリケーションが適切なノードIDをフェッチするように注意してください。セッションプーリングを使用する場合、クライアントは同じノードに接続されたままであるため、ノードIDはクライアントセッションの存続期間中一定のままです。ただし、より粒度の高いトランザクションプーリングを使用すると、クライアントはすべてのトランザクションのノードIDを取得する必要があります(次の例のように。

PGDノードに直接接続されていないクライアントは、フェールオーバーまたはスイッチオーバーに気付かない場合があります。ただし、 bdr.local_node_id パラメーターを常に使用して、現在接続されているノードを判断できます。 COMMIT中に切断するという重大な状況では、プロキシはその切断をエラーとしてCAMOプロトコルを適用するクライアントに適切に転送する必要があります。

received モードのCAMOの場合、 CAMOペアを切り替える可能性のあるプロキシは、 bdr.wait_for_camo_partner_queue ファンクションを使用して古い読み取りを防ぐ必要があります。

例

この例は、 CAMO対応クライアントアプリケーションの再試行ループがCのような擬似コードでどのように見えるかを示しています。接続情報を提供する2つのDSN origin_dsn およびpartner_dsn が必要です。これらは通常、bdr.create_node への最初の呼び出しに使用されたものと同じDSNであり、bdr.node_summary 、列interface_connstr で検索できます。

PGconn *conn = PQconnectdb(origin_dsn);

loop {
    // start a transaction
    PQexec(conn, "BEGIN");

    // apply transactional changes
    PQexec(conn, "INSERT INTO ...");
    ...

    // store a globally unique transaction identifier
    node_id = PQparameterStatus(conn, "bdr.local_node_id");
    xid = PQparameterStatus(conn, "transaction_id");

    // attempt to commit
    PQexec(conn, "COMMIT");
    if (PQresultStatus(res) == PGRES_COMMAND_OK)
        return SUCCESS;
    else if (PQstatus(res) == CONNECTION_BAD)
    {
        // Re-connect to the partner
        conn = PQconnectdb(partner_dsn);
        // Check if successfully reconnected
        if (!connectionEstablished())
            panic();

        // Check the attempted transactions status
        sql = "SELECT bdr.logical_transaction_status($node_id, $xid)";
        txn_status = PQexec(conn, sql);
        if (txn_status == "committed")
            return SUCCESS;
        else
            continue;   // to retry the transaction on the partner
    }
    else
    {
        // The connection is intact, but the transaction failed for some
        // other reason.  Differentiate between permanent and temporary
        // errors.
        if (isPermanentError())
            return FAILURE;
        else
        {
            // Determine an appropriate delay to back-off to account for
            // temporary failures due to congestion, so as to decrease
            // the overall load put on the servers.
            sleep(increasing_retry_delay);

            continue;
        }
    }
}

この例は、再試行とエラー処理を含む、接続のための適切なロジックで拡張する必要があります。 PgBouncerなどのロードバランサーを使用している場合、 PQreset を使用して再接続を実装できます。ロードバランサーがクライアントのみをCAMOパートナーにリダイレクトし、他のPGDノードにはリダイレクトしないことを確認します。

実際には、再試行の上限をお勧めします。トランザクションで実行されるアクションによっては、他の一時的なエラーが発生する可能性があります。 SERIALIZABLE 分離モードでのデッドロックまたはシリアル化の失敗に関するベストプラクティスと同様に、エラーコードに応じてトランザクションを再試行することによって処理する必要があります。

DDLとグローバルロックとの相互作用

CAMOによって保護されたトランザクションには、 DDL操作を含めることができます。ただし、 DDLはグローバルロックを使用します。これにより、ノード間の同期がすでに提供されます。詳細については、 DDLロックの詳細 を参照してください。

CAMOとDDLを組み合わせると、待ち時間が長くなり、グローバルデッドロックの可能性も高くなります。したがって、比較的低いDDLを使用することをお勧めします。DDLを中止し、妥当な時間内にデッドロックを解決します。

非トランザクションDDL

次のDDL操作はトランザクションブロックで許可されていないため、 CAMO保護の恩恵を受けることはできません。これらの場合、 CAMOは内部で自動的に無効になります。

*すべての同時インデックス操作(CREATE 、DROP 、およびREINDEX )

  • REINDEX DATABASE 、REINDEX SCHEMA 、およびREINDEX SYSTEM

  • VACUUM

*パラメーターなしのCLUSTER

  • ALTER TABLE DETACH PARTITION CONCURRENTLY

  • ALTER TYPE [enum] ADD VALUE

  • ALTER SYSTEM

  • CREATE およびDROP DATABASE

  • CREATE およびDROP TABLESPACE

  • ALTER DATABASE [db] TABLESPACE

CAMOの制限

CAMOの制限については、 PGD limitations ページで説明されています。

パフォーマンスへの影響

CAMOは、コミット時にメッセージラウンドトリップを追加することにより、Postgresレプリケーションプロトコルを拡張します。アプリケーションは、非同期レプリケーションよりもコミットレイテンシーが高く、主に関連するノード間のラウンドトリップ時間によって決まります。同時セッションの数を増やすと、並列処理が増えて、適切なトランザクションスループットが得られます。

トランザクションを確認するCAMOパートナーは、トランザクション状態を保存する必要があります。非CAMOオペレーションと比較して、これには、オリジンから適用されるトランザクションごとに追加のシークが必要になる場合があります。

クライアントアプリケーションのテスト

クライアント側でCAMOを適切に使用することは簡単ではありません。ノードのクラッシュやネットワークの停止などの障害シナリオに対して、PGDクラスターを使用してアプリケーションの動作をテストすることを強くお勧めします。