Amazon Web Services ブログ
Amazon Aurora DSQL の同時実行制御
このブログは “Concurrency control in Amazon Aurora DSQL” を翻訳したものです。
データベース管理システム (DBMS) では、同時実行制御が重要な役割を果たし、複数のトランザクションを同時に実行できるようにします。これにより、データの矛盾や破損を防ぐことができます。同時実行制御とは、特に共有リソースにアクセスする際のトランザクションの相互作用を管理するメカニズムを指します。分散データベース管理システム (D-DBMS) では、データが複数のノードに分散しているため、データ複製、ネットワークの遅延、一貫性の維持などの課題が生じ、同時実行制御の重要性がさらに高まります。
効率的にスケールする分散システムを設計する際、絶え間ない調整を避けることが重要になります。そのため、コンポーネントは他のコンポーネントの状態を継続的に確認できないため、前提を立てる必要があります。これらの前提は、「楽観的」または「悲観的」に分類できます。楽観的な前提は、楽観的同時実行制御 (OCC) の中心的な考え方で、競合は稀であると想定し、ロックを使わずにトランザクションを進行させ、競合が発生した際にのみ解決します。この手法は、分散環境でのスケーラビリティ向上に特に有効ですが、ACID 特性 (アトミック性、一貫性、分離性、耐久性) を維持するために、堅牢な競合検出と解決が必要です。一方、悲観的な前提は、悲観的同時性実行制御 (PCC) の基礎をなすもので、競合は一般的に発生すると想定し、トランザクション中にリソースにロックをかけて干渉を防ぎます。これにより一貫性は確保できますが、スケーラビリティとパフォーマンスが制限される可能性があります。OCC と PCC のどちらを選択するかは、早期の調整を避けて後で競合に対処するか、事前に全てのトランザクション競合を防ぐかによって決まります。
この記事では、効率的なトランザクションパターンを設計するための貴重な洞察を提供し、一般的な同時実行性の課題に対する効果的な解決策の例を示しながら、同時実行制御について深く掘り下げます。また、サンプルコードを含め、Amazon Aurora DSQL (DSQL) での同時実行性性制御の例外を滞りなく管理する方法を紹介します。
PCC または OCC を使用する場合
OCC と PCC のどちらを選択するかは、ワークロードとデータベース環境に合わせて慎重に検討する必要があります。
PCC は、ロック管理を最適化できる単一インスタンスのデータベースに適しています。同じ株価指標を継続的に更新するような、小さなキー範囲での頻繁な更新が行われる高競合シナリオで優れた性能を発揮します。この手法は単一インスタンスの RDBMS では効果的ですが、分散システムでは効果が低くなります。
一方、OCC は、リソースのロックを行わずにトランザクションを進行させ、コミット段階でのみ競合をチェックすることで、低競合環境で最も優れたパフォーマンスを発揮します。これにより、実行オーバーヘッドとブロッキングの遅延が削減され、競合が少ない、またはノンブロッキングの実行が重要なワークロードに適しています。OCC は高競合シナリオの管理がより複雑になる可能性がありますが、水平スケーリングの恩恵を受けるスケーラブルなキー範囲や追加のみの操作などの分散システムを効果的にサポートします。
Aurora DSQL のための OCC の利点
Aurora DSQL は楽観的同時実行制御を採用しています。これは、リレーショナルデータベースよりも非リレーショナルデータベースでよく使われる手法です。楽観的同時実行制御では、同じ行を更新しようとする他のトランザクションについてあまり考慮せずにトランザクションロジックを実行します。トランザクション完了時に、データベースへの変更をコミットしようとします。その際、Aurora DSQL は他の同時実行トランザクションからの書き込みが干渉していないかを確認します。干渉がなければトランザクションは正常にコミットされますが、干渉があった場合はデータベースがエラーを返します。このような場合、ユーザーは悲観的同時実行制御を使うデータベースと同様に、どのように処理を進めるかを決める必要があります。ほとんどのユースケースでは、トランザクションをリトライするのが最善のアプローチです。
Aurora DSQL では、クライアントの遅延や長時間実行されるクエリが他のトランザクションに影響を与えたり遅延させたりすることはありません。これは、SQL の実行中ではなく、コミット時にサーバー側で競合が処理されるためです。一方、ロッキングベースの設計では、クライアントがトランザクション開始時に行や全テーブルに排他ロックを取得します。クライアントが予期せず停止した場合、これらのロックが無期限に保持される可能性があり、他の操作がブロックされ、不定長の待ち行列が発生する可能性があります。対して、OCC では、即座に競合を通知し、確定的な結果を提供するため、即座にリトライやアボートができます。ロックベースのシステムでは通常、タイムアウト期間に達した後にのみリトライやキャンセルのロジックが実行されます。このため、Aurora DSQL は、特に AWS リージョン間にまたがる大規模な分散アプリケーションの構築時に発生する現実的な障害や故障に対してより堅牢です。
マルチリージョンクラスターでは、ユーザーがトランザクションを送信すると、SQL 操作がトランザクションが送信されたリージョン (リージョン 1) のローカルストレージで実行されます。トランザクションが完了すると、そのトランザクションに関与したキーとともに事後イメージが、別のリージョン (リージョン 2) に送信されます。リージョン 2 には、そのリージョンの現在の変更をすべて認識しているトランザクション処理リーダーがいます。リーダーはリージョン 1 から事後イメージとトランザクションに関与したキーのリストを受け取ると、それがリージョン内で現在アクティブに変更されているすべてのキーと競合していないかを確認します。競合がなければ、コミットの確認を送り返します。
競合がある場合、最初にコミットしたトランザクションが成功し、残りのトランザクションはリトライする必要があります。その結果、同時実行制御の競合が発生します。
Aurora DSQL は、マルチリージョンのアクティブ・アクティブ可用性を持つ組織のビジネス継続性を実現します。OCC は、同期レプリケーションを使用したマルチリージョンのトランザクションの効率を向上させます。Aurora DSQL は、リージョン間の通信なしにローカルでの読み取りおよび書き込みトランザクションを処理し、トランザクションのコミット要求時にのみリージョン間の整合性を確認します。OCC により、ロックの交渉が不要になるため、Aurora DSQL は完全な事後イメージを事前に処理し、マルチ AZ およびマルチリージョンの定足数に対してチェックできます。このアプローチにより、ロックのオーバーヘッドなしにトランザクションを処理できるため、アベイラビリティゾーンおよびリージョン間の同期トランザクションのレイテンシーが低減されます。
次の図は、マルチリージョンクラスター (アクティブ-アクティブ) を示しています。
楽観的同時実行制御とアイソレーションレベル
他の分散 SQL データベースがSerializable分離レベルをサポートするのに対し、Aurora DSQL は PostgreSQL の repeatable read 分離レベルに相当する強力なスナップショット分離をサポートしています。トランザクションは、開始時のデータベースのスナップショットからデータを読み取ります。これは読み取り専用のトランザクションに特に役立ちます。読み取り専用のトランザクションは待機する必要がなく、OCC の影響も受けにくいためです。
トランザクションパターンのガイダンス
同時に実行されるデータベーストランザクションが同じレコードを更新すると、コンフリクトのリスクがあります。適切なデータモデリングでこのリスクを低減できますが、コンフリクトは避けられません。開発者は、データベースの提供する同時実行管理機能を理解し、アプリケーションに効果的に実装する必要があります。
Aurora DSQL は楽観的同時実行制御を使用しているため、同一キーに対する高頻度の並行更新が発生するユースケースでは、プログラミングアプローチが異なる必要があります。作業が完了したら、トランザクションのコミットを試みます。Aurora DSQL は、他の更新トランザクションがあなたのトランザクションに干渉していないかを確認します。干渉がなければ、トランザクションは成功します。そうでなければ、データベースがエラーを報告します。その場合、すぐにトランザクションをリトライするか、エクスポネンシャルバックオフとジッターを導入して、後続の競合の可能性を減らすかを決める必要があります。
OCC は常にデータベースでのトランザクションの進行を支援しますが、高い競合状態では性能が低下する可能性があります。そのため、以下のようなトランザクションパターンのガイドラインに従うことをお勧めします:
- トランザクションが失敗する可能性があることを前提に、常にリトライできるようにトランザクションを設計してください
- 行レベルの競合やデータ更新の競合を最小限に抑えるため、各トランザクションにタイムアウトロジックを実装してください。設定するタイムアウト値は、不要なトランザクションのキャンセルを避けるため、最大クエリ時間を考慮する必要があります
- 競合が激しく、リトライ率が高い場合は、失敗したトランザクションが異なるランダムな間隔でリトライされるよう、エクスポネンシャルバックオフとジッターを実装してください
- 既存のキーに対する更新が多い (更新とアップサート) システムでは、競合の可能性を最小限に抑えるため、トランザクションの範囲を小さく保つことが重要です
Aurora DSQL で OCC の例外が発生する可能性のあるいくつかのユースケースと、コード例を見ていきましょう。
例 1: リージョン間トランザクションでのデータ競合
Aurora DSQL などの分散 SQL システムでは、OCC 例外が発生する一般的なシナリオは、複数のリージョンが同じデータを同時に更新しようとする場合です。この例では、同じアカウントデータを操作する 2 つのリンクされたリージョン、us-east-1
と us-east-2
を持つクラスターを想定しましょう。
- 「us-east-1」のトランザクション A では、アカウントの残高とバージョンを読み取り、更新の準備をしています
- 同時に、「us-east-2」のトランザクション B も同じアカウントの残高とバージョンを読み取り、自身の更新を行う準備をしています
- トランザクション B は、アカウントの残高を正常に更新し、バージョンをインクリメントしました
- その後、トランザクション A が古いバージョンを使って更新をコミットしようとすると、バージョンがトランザクション B によって更新されたため、OCC 例外が発生しました
このシナリオは、異なるリージョンの同じデータに対する同時トランザクションが、OCC 例外につながる可能性を示しています。
それでは、コード例を使って詳しく見ていきましょう。
- テーブルを作成し、レコードを挿入します:
- トランザクション A がアカウントの残高とバージョンを読み取ります:
- トランザクション B が同じデータを読み取り、更新し、バージョンをインクリメントします:
- トランザクション A が古いバージョンのアカウントを使って更新しようとすると、OCC 競合が発生します:
この場合、トランザクション A は、バージョン番号がトランザクション B によって変更されたことをシミュレートした OCC 例外のため、失敗します。
実際に何が起きたのかの詳細を見ていきましょう。
トランザクション A は読み取り/書き込みトランザクションなので、読み取り段階では、クエリプロセッサーが SELECT を解析し、シャードマップを参照してこのデータを保持するストレージノードを特定し、それらのノードからデータを取得します。トランザクション B も読み取り/書き込みトランザクションで、クエリプロセッサーは UPDATE ステートメントの一部として、読み取りセットと書き込みセットを作成します。トランザクション B がコミットを発行すると、クエリプロセッサーは読み取りセットと書き込みセットをパッケージ化し、トランザクションプロセッサーに送信します。各トランザクションプロセッサーは、他のトランザクションによる読み取りセットの変更がないかを確認します。変更がなければ、事前イメージと事後イメージをコミットレイヤーに読み書きし、コミットを永続化し、バージョンは増分されません。トランザクション A が UPDATE ステートメントを発行します。クエリプロセッサーはすでに更新に必要なすべてのデータを持っているので、書き込みセットを作成し、読み取りセットと書き込みセットをトランザクションプロセッサーに渡します。読み取りセットのデータが変更されたため、トランザクションプロセッサーは変更を拒否し、OCC 例外を返します。この時点で、クライアントはトランザクション B による更新を含む同じトランザクションをリトライできます。
例 2: SELECT FOR UPDATE を使用したライトスキューの管理
前述のとおり、Aurora DSQL は通常、読み取ったレコードに対して同時実行性チェックを行いません。SELECT FOR UPDATE
はこの動作を変更し、読み取った行に同時実行性チェックのフラグを立てます。
これが Aurora DSQL でライトスキューを管理する方法です。
ライトスキューでは、2 つの同時トランザクションが共通のデータセットを読み取り、それぞれが共通のデータセットを変更する更新を行いますが、お互いに重複しません。重複がないため (同じデータを変更しないため)、同時実行性の保護は発動しません。
Aurora DSQL では、FOR UPDATE
句により、フラグ付きの行に対する追加のチェックを行うことで、Optimistic Concurrency Control (OCC) の標準的な動作が変更されます。この調整により、トランザクション
が同じデータセットに同時にアクセスする際に発生する可能性のある異常を防ぐことができます。ロック機構を使ってコンフリクトを管理する従来の Pessimistic Concurrency Control (PCC) とは異なり、OCC は潜在的な書き込みコンフリクトを別の方法で処理します。以下の例は、FOR UPDATE 句がこのコンテキストでどのように同時実行性チェックを強制するかを示しています。
この状況が実際に起こりうる例を見てみましょう。
- 最初に、
Orders
テーブルを作成し、いくつかの行を挿入します:
2. トランザクション A は、FOR UPDATE
句を使用して読み取り/書き込みトランザクションを開始しますが、まだ更新を実行したり、コミットしていません:
トランザクション B は独自の読み取り/書き込みトランザクションを開始し、コミットします:
3. トランザクション A は、同じトランザクション内で accounts
テーブルの更新を試みます:
4. トランザクション A がコミットを発行すると、次の OCC 例外が発生します:
OCC 例外が発生しましたが、変更された読み取りセットを持っていた orders
テーブルではなく、別のテーブルが更新されていました。
実際に何が起きたのかを詳しく見ていきましょう
このシナリオでは、トランザクション A が order_id=1
の詳細を含む読み取りセットを作成します。一方、読み取り/書き込みトランザクションであるトランザクション B も同じ order_id
を読み取りと更新します。その後、トランザクション A が accounts
テーブルの残高を更新しようとすると、OCC エラー (OC000) が発生します。トランザクション A が開始されると、クエリプロセッサーは読み取りセットを作成し、コミットする前に書き込みセットを生成するのを待ちます。しかし、トランザクション A が進行中の間に、トランザクション B がトランザクション A の読み取りセットと同じ order_id
を更新してコミットします。トランザクション A が accounts
テーブルを更新しようとすると、クエリプロセッサーは書き込みセットを作成し、トランザクションプロセッサーによる検証のために読み取りセットと書き込みセットの両方を渡します。この段階で、トランザクションプロセッサーは読み取りセットのデータに新しいバージョンがあることを検知し、トランザクションを拒否し、トランザクション A にリトライを強制します。
この例では、Aurora DSQL でライトスキューを管理したい場合、for update を使うのが効率的な方法であることを示しました。
注意: FOR UPDATE
は読み書きトランザクション内でのみ機能します。読み取り専用トランザクションで FOR UPDATE
を使用しようとすると、警告が表示され、読み取られた行の更新は報告されません。
また、SELECT FOR UPDATE フィルターには、選択するテーブルのプライマリーキーを含める必要があります。今回の場合、orders テーブルのプライマリーキーは order_id なので、この SELECT FOR UPDATE は失敗します。
しかし、このクエリは主キーを含むフィルターが設定されているため、成功します。
例 3: カタログの同期が取れていない例外
データ競合は OCC 例外の主な原因ですが、カタログの同期が取れていない問題も、これらのエラーをトリガーする可能性があります。これらのエラーは、アクティブなセッション中に、テーブルの作成や変更などのデータベーススキーマの変更が行われた際に発生します。例えば、1 つのトランザクションがテーブルを作成している間に、別のトランザクションがそのテーブルの読み取りや書き込みを試みると、セッションのカタログ情報が古くなっているため、OCC エラー (OC001 など) が発生する可能性があります。
操作をリトライすると、初期の失敗後にデータベースカタログが更新されるため、問題が解決されることが多いです。本番環境でこのようなエラーのリスクを最小限に抑えるには、マルチスレッド方式で DDL 操作を行うのは避けることをお勧めします。同時並行のスキーマ変更は、競合状態、トランザクションの失敗、ロールバックの原因となる可能性があります。制御された単一スレッドの環境で DDL 変更を管理することで、競合の問題を軽減し、データベース操作をより滑らかに行うことができます。
これが実際にどのように起こるかの具体例を見てみましょう。
- トランザクション A によってテーブルが作成されます:
- 既にセッションを開いているトランザクション B が、テーブルにレコードを書き込もうとすると:
この例では、トランザクション A がテーブルを作成し、トランザクション B が同じテーブルにレコードを書き込もうとしますが、OCC エラーが発生します。しかし、その後のリトライは成功します。
以下は、OCC 例外 (OC001) に遭遇する可能性のある他のいくつかのシナリオで、通常はリトライで解決できます:
- トランザクション A が既存のテーブルにカラムを追加している間、トランザクション B がそのテーブルから読み取りまたは書き込みを試みると、OCC 例外が発生します。
- トランザクション A がテーブルをドロップし、トランザクション B がその後同じテーブルにアクセスしようとします。
- トランザクション A がテーブルにカラムを追加し、トランザクション B が同時に別の名前のカラムを追加しようとします。
- 進行中のトランザクションと競合するカタログの変更。
要約すると、OCC 例外 (OC001) は、データベーススキーマやカタログの変更が同時に行われることが原因で発生することが多いですが、適切なリトライメカニズムを実装することで、一般的に解決できます。
OCC 例外のリトライメカニズムによる処理
OCC では、トランザクションの競合時にリトライを管理するためのベストプラクティスとして、バックオフとジッターの実装が重要です。これにより、同期的なリトライによる更なる競合や、システムの過負荷を回避できます。バックオフにより、競合後のリトライが即座ではなく、徐々に長くなる遅延を設けることで、システムの負荷を軽減できます。ジッターは、これらの遅延にランダム性を導入します。バックオフとジッターを組み合わせることで、OCC を採用する分散システムにおけるコンテンション問題を軽減し、リトライロジックの効率を高めることができます。詳細については、Exponential Backoff And Jitterを参照してください。以下のコードサンプルは、このリポジトリから入手できます。
高トランザクション環境で OCC の例外をシミュレーションし、バックオフとジッター戦略を使ってリトライを管理するシナリオを見ていきましょう。
- まず、
create.py
スクリプトを使用して、注文スキーマとaccounts
およびorders
の 2 つのテーブルを作成します load_generator.py
スクリプトを実行
して、データベースにロードを生成し、orders
テーブルにデータを挿入します- OCC 条件を導入するために、別の PostgreSQL セッションで
accounts
テーブルを変更し、新しい列を追加しますスキーマが更新された後、
load_generator.py
スクリプトは次のエラーで失敗します次に、
retry_backoff_jitter.py
スクリプトを実行して、組み込みのリトライメカニズムを使用したload_generator.py
スクリプトの拡張版を実行し、バックオフとジッターを統合します。 accounts
テーブルに別のスキーマ変更を加えます
リトライロジックが発動すると、スクリプトが OCC 例外をリトライで処理しているのが確認できます
リトライ戦略に応じて、この値を微調整することができます。この場合、遅延時間の単位は秒になります。
分散システムでの OCC 例外の効果的な管理には、バックオフとジッターを組み込むことができる包括的なリトライ戦略が必要です。アプリケーションのキースペースの高コンテンション領域 (ホットキー) を避けられない場合でも、予測可能性を確保し、スループットを最大化し、高コンテンション率 (ホットキー) を示す負荷領域の安定性を高めることができます。リトライロジックに加えて、更新インプレースではなく追加のみのパターンを好むことで、最初からコンテンションを最小限に抑えることが重要です。ほとんどの最新のアプリケーション設計と Aurora DSQL などの分散データベースは、既存のキーを更新するのではなく新しいキーを導入することで、最適なシステムのスケーラビリティを実現できます。さらに、冪等性を実装することで、リトライによる重複操作や データ不整合を防ぐことができ、永続的な障害に対するデッドレターキューを設けることで、エスカレーションと手動による介入を可能にし、システムの信頼性をさらに高めることができます。
結論
同時実行制御は、あらゆるデータベース管理システムにとって重要な側面であり、分散データベースを扱う際にはさらに重要になります。Aurora DSQL で OCC を使用することは、分散アーキテクチャのため適切です。トランザクション実行中にリソースロックを必要としないため、スループットとシステム効率が向上します。
Aurora DSQL における OCC の主な利点は次のとおりです:
- 遅いクライアントや長時間実行クエリに対するレジリエンス – OCC では、コンテンション処理がクエリ実行中ではなくコミット段階で行われるため、1 つの遅いトランザクションが他のトランザクションに影響を与えたり遅延させたりすることはありません。
- スケーラブルなクエリ処理 – Aurora DSQL の逆方向検証アプローチは、現在のトランザクションをコミット済みのトランザクションと比較するため、コーディネーションなしにクエリプロセッサーレイヤーをスケールアウトできます。
- 現実的な障害に対するロバスト性 – OCC により、Aurora DSQL はデッドロックやパフォーマンスボトルネックを引き起こす可能性のあるロック機構に依存しないため、大規模な分散アプリケーションを構築する際の障害やエラーに対してより強靭になります。
OCC には大きな利点がありますが、最適なパフォーマンスを得るには、トランザクションパターンを慎重に検討する必要があります。トランザクションが失敗する可能性があることを前提とし、タイムアウトと ジッター を伴うエクスポネンシャルバックオフを実装し、SELECT FOR UPDATE
を使ってライトスキューを管理するといった原則は、Aurora DSQL を使う開発者にとって重要なガイドラインです。
Aurora DSQL の同時実行制御メカニズムとベストプラクティスを理解することで、複雑で高トランザクションの負荷がかかる環境でも、データの整合性と可用性を維持しながら、システムの分散機能を活用できます。Aurora DSQL の詳細については、ドキュメントをご覧ください。
著者について
Rajesh Kantamani は、シニア データベース スペシャリスト SA です。彼は、Amazon Web Services 上でデータベース ソリューションの設計、移行、最適化を支援し、スケーラビリティ、セキュリティ、パフォーマンスを確保することに特化しています。余暇には、家族や友人と屋外で過ごすことが好きです。
Prasad Matkar は、EMEA 地域を担当する AWS のデータベース専門ソリューションアーキテクトです。関係データベースエンジンに焦点を当て、AWS へのデータベースワークロードの移行とモダナイゼーションについて、お客様に技術的なサポートを提供しています。