ApsaraDB for MongoDB でのトランザクションと Read/Write Concern 設定のベストプラクティス。
背景情報
MongoDB 4.0 では、単一のレプリカセット内の 1 つ以上のコレクションに対する操作向けに、スタンドアロン トランザクション (レプリカセット トランザクション) が導入されました。MongoDB 4.2 では、複数のコレクションおよびシャードにまたがる操作向けに、分散トランザクション (シャーディングされたトランザクション) が追加されました。
MongoDB では、単一ドキュメントの操作は常に原子性を満たします。正規化されたコレクションや結合を必要とするリレーショナルデータベースとは異なり、アプリケーションは埋め込みドキュメントや配列を使用して、1 つのドキュメント内で関係をモデル化できます。適切なデータモデリングにより、単一ドキュメントの原子性によって多くの場合は分散トランザクションが不要になります。
ただし、金融や会計のアプリケーションでは、分散トランザクションが必要になる場合があります。MongoDB 4.2 以降では、この機能が完全にサポートされています。
トランザクション
基本情報
MongoDB のトランザクション API はリレーショナルデータベースのものと似ているため、学習コストは最小限です。
次の例は、startTransaction/abortTransaction/commitTransaction API を使用した完全なトランザクションに加えて、session および readConcern/writeConcern の設定を示しています。
// コレクションを作成します。
db.getSiblingDB("mydb1").foo.insertOne(
{abc: 0},
{ writeConcern: { w: "majority", wtimeout: 2000 } }
)
db.getSiblingDB("mydb2").bar.insertOne(
{xyz: 0},
{ writeConcern: { w: "majority", wtimeout: 2000 } }
)
// セッションを開始します。
session = db.getMongo().startSession( { readPreference: { mode: "primary" } } );
coll1 = session.getDatabase("mydb1").foo;
coll2 = session.getDatabase("mydb2").bar;
// トランザクションを開始します。
session.startTransaction( { readConcern: { level: "local" }, writeConcern: { w: "majority" } } );
// トランザクション内で操作を実行します。
try {
coll1.insertOne( { abc: 1 } );
coll2.insertOne( { xyz: 999 } );
} catch (error) {
// エラーが発生した場合はトランザクションをアボートします。
session.abortTransaction();
throw error;
}
// トランザクションをコミットします。
session.commitTransaction();
session.endSession();
トランザクションの主な動作:
-
各トランザクションはセッションに関連付ける必要があります。1 つのセッションで同時にアクティブにできるトランザクションは 1 つだけです。セッションを終了すると、アクティブなトランザクションはロールバックされます。
-
分散トランザクションは、複数のドキュメント、コレクション、データベースにまたがることができます。
-
トランザクションは自身の未コミットの書き込みを読み取れますが、これらの書き込みはトランザクション外の操作からは参照できません。
-
書き込みは、トランザクションがコミットされるまでセカンダリノードにレプリケートされません。コミット後、書き込みはすべてのセカンダリノードに自動的に適用されます。
-
トランザクションは変更するドキュメントをロックし、完了するまで他の操作をブロックします。5 ミリ秒 (デフォルト) 以内にロックを取得できない場合、トランザクションは書き込みコンフリクトによりアボートします。このタイムアウトは、maxTransactionLockRequestTimeoutMillis パラメータで制御されます。
-
トランザクションは、ネットワークの一時的な中断などの一時的なエラーが発生した場合に自動的に再試行されます。この再試行はクライアントからは透過的です。
-
60 秒を超えて実行されるトランザクションは強制的にアボートされます。このタイムアウトは、transactionLifetimeLimitSeconds パラメータで制御されます。
制限
-
分散トランザクション内で新しいコレクションまたはインデックスを作成することはできません。
-
トランザクションではキャップ付きコレクションに書き込めません。
-
トランザクションでは、
snapshotの読み取り懸念を使用してキャップ付きコレクションから読み取ることはできません。この制限は MongoDB 5.0 以降に適用されます。 -
トランザクションでは、
config、admin、localの各データベース内のコレクションに対して読み取りまたは書き込みを実行できません。 -
トランザクションでは、
system.*形式のコレクションなど、システムコレクションに書き込めません。 -
トランザクションは
explainをサポートしていません。 -
トランザクション内で
getMoreを使用して、トランザクション外で作成されたカーソルを読み取ることはできません。同様に、トランザクション外でgetMoreを使用して、トランザクション内で作成されたカーソルを読み取ることもできません。 -
トランザクションの最初の操作を、killCursors や
helloなどのコマンドにすることはできません。 -
トランザクション内では非 CRUD コマンドを実行できません。例として、listCollections、listIndexes、createUser、getParameter、
countなどがあります。 -
分散トランザクションでは、シャード上の writeConcernMajorityJournalDefault パラメータを false に設定できません。
-
分散トランザクションは、アービター を含むシャードをサポートしていません。
ベストプラクティス
分散トランザクションよりスタンドアロン トランザクションを優先
分散トランザクションはロジックがより複雑なため、スタンドアロン トランザクションよりもパフォーマンスが低下します。MongoDB では、非正規化データモデル (埋め込みドキュメントと配列) が引き続き最適な選択肢です。適切にデータモデリングを行えば、スタンドアロン トランザクションでほとんどのトランザクション要件を満たせます。
長時間実行トランザクションの回避
MongoDB は 60 秒を超えて実行される分散トランザクションを自動的にアボートします。大きなトランザクションは小さな単位に分割し、クエリが適切なインデックスを使用して高速に実行されるようにしてください。
1 つのトランザクションでの過剰なドキュメント変更の回避
トランザクションで読み取れるドキュメント数に厳密な上限はありませんが、多数のドキュメントを変更するとプライマリ/セカンダリ間の同期負荷が増加し、レプリケーションラグが発生する可能性があります。1 トランザクションあたりの変更は 1,000 ドキュメントに制限してください。より大きなバッチの場合は、複数のトランザクションに分割します。
16 MB を超える大規模トランザクションの回避
MongoDB 4.0 では、トランザクションは 1 つの oplog エントリを使用し、そのサイズ上限は 16 MB です。更新操作では差分が格納され、挿入ではドキュメント全体が格納されます。oplog の合計が 16 MB を超えると、トランザクションはアボートされてロールバックされます。
MongoDB 4.2 以降では、トランザクションの書き込みを複数の oplog エントリに分割できるため、単一エントリの 16 MB 制限は解消されます。ただし、他の問題を避けるため、トランザクションサイズは 16 MB 以内に抑えてください。
クライアント側でのトランザクション ロールバック処理
トランザクションがアボートされると、例外が返されてロールバックされます。アプリケーションはこれらの例外をキャッチし、一時的なエラー (プライマリ/セカンダリ スイッチオーバー、ノード障害) の場合は再試行する必要があります。MongoDB ドライバーは 再試行可能な書き込み によりコミットを自動的に再試行しますが、TransactionTooLarge、TransactionTooOld、TransactionExceededLifetimeLimitSeconds などの再試行不可エラーは、アプリケーション側で対処する必要があります。
トランザクション内での DDL 操作の回避
createIndex や dropDatabase などの DDL 操作は、同じデータベースまたはコレクション上でアクティブなトランザクションによってブロックされます。ブロックされた DDL 操作により、新しいトランザクションがロックを取得できなくなり、アボートの原因になります。
MongoDB 4.4 では、shouldMultiDocTxnCreateCollectionAndIndexes パラメータにより、これらの制限が緩和されました。以下の制限付きで、分散トランザクション内で createCollection または createIndex を実行できます:
-
コレクションは暗黙的にのみ作成できます。
-
対象コレクションが既に存在していてはなりません。
-
対象コレクションは空である必要があります。
そのため、トランザクション内での DDL 操作は避けてください。
未コミットまたはエラーのトランザクションの迅速なロールバック
未コミットのトランザクションによる変更は WiredTiger キャッシュに保持されます。未コミットまたはエラーのトランザクションが複数同時に存在すると、キャッシュに負荷がかかり、さらなる問題を引き起こす可能性があります。不要なトランザクションはできるだけ早くロールバックし、リソースを解放してください。
トランザクションのロールバックが頻発する場合のロック タイムアウトの延長
デフォルトでは、トランザクション操作は 5 ミリ秒 以内にロックを取得できない場合にロールバックされます。ロックはコミットまたはロールバック時に解放されます。ロック タイムアウトが原因でロールバックが頻発する場合は、maxTransactionLockRequestTimeoutMillis パラメータを増やしてください。
タイムアウトを増やしても改善しない場合は、DDL 操作や最適化されていないクエリなど、ロックを長時間保持する操作がないか確認し、最適化してください。
トランザクション内外での同一ドキュメントへの同時変更の回避
外部の書き込みが、進行中のトランザクションでも変更しているドキュメントを変更すると、トランザクションは書き込みコンフリクトによりロールバックされます。逆に、トランザクションが既にドキュメントのロックを保持している場合、そのドキュメントへの外部書き込みはトランザクションが完了するまで待機します。
書き込みコンフリクトが発生しても、外部書き込みは失敗しません。MongoDB が内部で再試行し、成功するまで writeConflicts カウンターをインクリメントします。クライアントからは、成功するものの遅い操作として認識されます。
少数の書き込みコンフリクトであれば影響は最小限です。書き込みコンフリクトが頻発するとパフォーマンスが低下します。監査ログやスロークエリログを使用して、書き込みコンフリクトの過多を検出してください。
カーネル バグのリスク
長時間実行または過大なトランザクションは WiredTiger キャッシュに大きな負荷をかけます。最も古い未コミット トランザクションの開始時点から、WiredTiger はその後のすべての書き込みに対するデータと状態を維持する必要があります。アクティブなトランザクションは同一のスナップショットを共有するため、トランザクションの存続期間中は新しい書き込みがキャッシュに蓄積され続け、トランザクションがコミットまたはアボートされるまでエビクトできません。キャッシュが過負荷 (使用量およびダーティ使用量がしきい値を超過) になると、データベースの断続的な停止、レイテンシの増加、CPU 使用率 100%、およびデッドロックが発生する可能性があります。関連する問題:SERVER-50365 および SERVER-51281。
ApsaraDB for MongoDB は、トランザクションを多用するアプリケーションには、MongoDB 5.0 以降へのアップグレードを推奨します。
読み取り懸念
基本情報
読み取り懸念 (Read Concern) は、次のレベルで整合性と分離を制御します:
-
"local":プライマリまたはセカンダリノードから読み取る際のデフォルトです。ローカルノードから読み取り、後でロールバックされる可能性があるデータを返す場合があります。 -
"available":シャードクラスターのセカンダリから読み取る際のデフォルトです。後でロールバックされる可能性があるデータを返す場合があります。シャードバージョンをチェックしないため、孤立ドキュメントを返す可能性があります。アクセス レイテンシが最小です。 -
"majority":レプリカセット メンバーの過半数によって確認されたデータを読み取ります。このデータはロールバックされません。 -
"linearizable":最も厳格なレベルです。先行するすべての書き込みがノードの過半数によって確認されるまで待機します。パフォーマンスは最も低く、プライマリでのみ使用できます。 -
"snapshot":ノードの過半数によって確認されたデータのスナップショットから読み取ります。atClusterTime を使用して特定の時点に関連付けることができます。
使用上の注意:
-
単一の mongod ノード上の最新データが、読み取り懸念レベルにかかわらず、必ずしもレプリカセットにおける最新データを表すとは限りません。
-
読み取り懸念レベルは操作ごとに指定できます。MongoDB 4.4 以降ではサーバー側のデフォルトもサポートされ、操作レベルの設定がそれを上書きします。
-
localデータベースから読み取る場合、読み取り懸念は無視されます。localデータベース内のデータは、レベルにかかわらず読み取り可能です。 -
分散トランザクションでサポートされる読み取り懸念レベルは、
"local"、"majority"、"snapshot"の 3 つのみです。 -
因果整合性のあるセッションは、読み取り懸念レベルとして
"majority"を使用する必要があります。
ベストプラクティス
分散トランザクションでは、個々の操作ではなくトランザクションに読み取り懸念を設定
読み取り懸念はトランザクション レベルで指定してください。トランザクション レベルの設定は、他の読み取り懸念設定やデフォルトを上書きします。
読み取り懸念は、単一ドキュメントの読み取り、複数ドキュメントの読み取り、またはトランザクションの一部であるかどうかにかかわらず、あらゆるデータベース クエリに適用されます。
一般的なユースケースでは "majority" の読み取り懸念レベルを使用
分離と整合性を確保するため、読み取り懸念は "majority" に設定してください。これにより、アプリケーションはノードの過半数にレプリケートされたデータのみを読み取り、プライマリ選出時のロールバックを防止できます。
「Read Your Own Writes」のシナリオでは、プライマリから読み取り、"local" または "linearizable" の読み取り懸念レベルを使用
プライマリノードから、読み取り懸念 "local" または "linearizable" で読み取ります。書き込み懸念が "majority" の場合は、読み取り懸念として "majority" も使用できます。
MongoDB 3.6 以降では、このシナリオ向けに因果整合性のあるセッションもサポートされています。
最強の整合性が必要なシナリオでは、"linearizable" の読み取り懸念と maxTimeMS のタイムアウトを併用
"linearizable" レベルは、ノードが引き続きプライマリであること、および返されるデータがロールバックされないことを確認します。ただし、レイテンシに大きな影響があります。ノードの過半数が利用できなくなった場合に無期限にブロックされるのを防ぐため、maxTimeMS のタイムアウトと併用してください。
書き込み懸念
基本情報
書き込み懸念は次の形式を使用します。完全なリファレンスについては、書き込み懸念 (Write Concern) をご参照ください。
{ w: <value>, j: <boolean>, wtimeout: <number> }
-
書き込み懸念は、次のレベルでデータ永続性の保証を制御します:
-
{w: 0}:確認応答なし。完了を確認しないため、データ損失が発生する可能性があります。
-
{w: 1}:メモリ上の書き込みを確認します。MongoDB 5.0 より前のデフォルトです。データはまだディスクに永続化されていないため、データ損失が発生する可能性があります。
-
{j: true}:ジャーナルの確認応答。永続ストレージ上の先行書き込みログ (WAL) に書き込みがフラッシュされたことを確認します。書き込みは失われません。
-
{ w: "majority" }:マジョリティの確認応答。MongoDB 5.0 以降のデフォルトです。書き込みがノードの過半数にレプリケートされるまで待機します。データはロールバックされません。
-
レプリカの確認応答:指定した数のノードへのレプリケーションを待ってから確認応答します。
-
カスタムの確認応答:settings.getLastErrorModes パラメータを使用して、タグを用いた他のカスタム確認応答方法を指定できます。
-
使用上の注意:
-
書き込み懸念は、任意の書き込み操作またはトランザクションに指定できます。省略した場合はデフォルトが使用されます。
説明MongoDB 5.0 以降では、標準の 3 メンバー構成のレプリカセットにおけるグローバルなデフォルト書き込み懸念が
{w:1}から{w:"majority"}に変更されました。アップグレード後にパフォーマンスが低下する可能性があります。 -
レプリカセット内の隠しメンバー、遅延メンバー、および priority が 0 のその他の投票ノードは、いずれも
"majority"の確認応答にカウントされます。 -
書き込み懸念レベルは操作ごとに指定できます。MongoDB 4.4 以降ではサーバー側のデフォルトもサポートされ、操作レベルの設定がそれを上書きします。
-
localデータベースに書き込む場合、指定した書き込み懸念は無視されます。 -
因果整合性のあるセッションは、書き込み懸念として
"majority"を使用する必要があります。
ベストプラクティス
分散トランザクションでは、個々の操作ではなくトランザクションに書き込み懸念を設定
トランザクション内の個々の書き込み操作に書き込み懸念を設定すると、エラーが返されます。
一般的なユースケースでは "majority" の書き込み懸念を使用
"majority" の書き込み懸念により、レプリカセット ノードの過半数が書き込みを確認応答するため、ノード障害や予期しないスイッチオーバーが発生しても、データ損失やロールバックを防止できます。
書き込み負荷が高いシナリオでは {w:1} の使用を検討し、セカンダリノードのレプリケーションラグを監視
{w:1} を使用すると、書き込み負荷が高いシナリオで書き込みパフォーマンスが向上します。ただし、セカンダリノードのレプリケーションラグを監視してください。ラグが過大になると、プライマリが ROLLBACK する可能性があります。ラグが oplog の保持期間を超えると、セカンダリは RECOVERING 状態になり、手動介入が必要になります。
ApsaraDB for MongoDB の 5.0 より前のバージョンのインスタンスで、データの一括ロードまたは DTS 移行を実行する際に大きな遅延が発生した場合は、"majority" Write Concern を使用してください。
操作に応じた最適な書き込み懸念の設定
書き込み懸念は操作ごとに設定できます。たとえば、金融データでは原子性を確保するために特定の書き込み懸念を指定したトランザクションを使用し、主要なプレイヤーデータではロールバックを防ぐために "majority" を使用し、ログデータではデフォルトまたは {w:1} を使用します。
MongoDB はこの柔軟性を提供しており、アプリケーションは要件に応じて適切な設定を選択できます。