ApsaraDB for MongoDB インスタンスに多数のコレクションが含まれている場合、パフォーマンスが低下し、例外が発生します。WiredTiger ストレージエンジンは、各コレクションおよび各インデックスに対して個別のディスクファイルを作成し、各オープンリソースはメモリ内で対応するデータハンドル (dhandle) を使用します。数千のコレクションがある場合、オープンな dhandle の量によりロック競合が発生し、すべてのデータベース操作が遅くなります。
多数のコレクションが常に問題を引き起こすわけではありません。影響は、ビジネスモデルとワークロードによって異なります。たとえば、同じ仕様、10,000 のコレクション、100,000 のドキュメントを持つ 2 つのインスタンスは、非常に異なる動作をする可能性があります。
会計ソフトウェア:ほとんどのコレクションはコールドデータを保存し、アクセスされることはほとんどありません。パフォーマンスへの影響は最小限です。
マルチテナントシステム:コレクションはテナントごとに隔離されており、ほとんどがアクティブに使用されています。ロック競合は深刻です。
仕組み
WiredTiger ストレージエンジンは、各コレクションおよび各インデックスに対して個別のディスクファイルを作成します。すべてのオープンリソースは、チェックポイント情報、セッションリファレンス、インメモリ B ツリー構造へのポインター、およびデータ統計を追跡する dhandle と呼ばれる一意のデータ構造を使用します。
コレクションの数が増えるにつれて、より多くの OS ファイルがオープンされ、より多くの dhandle がメモリに蓄積されます。メモリ内の多数の dhandle はロック競合を引き起こし、インスタンスのパフォーマンスを低下させます。
潜在的な問題
ハンドルロックまたはスキーマロックによるスロークエリと高レイテンシー
ノード追加時の同期初期化中のメモリ不足 (OOM) エラー
インスタンスの再起動時間の延長
データ同期の遅延
バックアップおよびリストア操作の遅延
物理バックアップ失敗率の上昇
障害からのインスタンスリカバリ時間の延長
スロークエリの診断
多数のコレクションによるロック競合がスロークエリを引き起こす場合、スロークエリログには次のようなエントリが含まれます。
2024-03-07T15:59:16.856+0800 I COMMAND [conn4175155] command db.collections command: count { count: "xxxxxx", query: { A: 1, B: 1 },
$readPreference: { mode: "secondaryPreferred" }, $db: "db" } planSummary: COLLSCAN keysExamined:0 keysExaminedBySizeInBytes:0
docsExamined:1 docsExaminedBySizeInBytes:208 numYields:1 queryHash:916BD9E3 planCacheKey:916BD9E3 reslen:185
locks:{ ReplicationStateTransition: { acquireCount: { w: 2 } }, Global: { acquireCount: { r: 2 } }, Database: { acquireCount: { r: 2 } },
Collection: { acquireCount: { r: 2 } }, Mutex: { acquireCount: { r: 1 } } } storage:{ data: { bytesRead: 304, timeReadingMicros: 4 },
timeWaitingMicros: { handleLock: 40, schemaLock: 134101710 } } protocol:op_query 134268msこの例では、単一ドキュメントコレクションに対する count 操作に 134,268 ms かかりました。キーフィールドは timeWaitingMicros: { handleLock: 40, schemaLock: 134101710 } であり、読み取りリクエストが実際の作業を実行するのではなく、基盤となるストレージレイヤーでハンドルロックとスキーマロックを取得するのを待機するのにほとんどの時間を費やしたことを示しています。
次の表は、スロークエリログエントリの主要フィールドについて説明しています。
| フィールド | 説明 |
|---|---|
planSummary | 使用されたクエリプラン。COLLSCAN はインデックスなしのフルコレクションスキャンを示し、IXSCAN はインデックススキャンを示します。 |
keysExamined | スキャンされたインデックスキーの数。 |
docsExamined | スキャンされたドキュメントの数。返された結果の数に対して高い値は、インデックスの欠落または最適化されていないインデックスを示唆しています。 |
numYields | 他の操作の進行を許可するために、操作がロックを解放した回数。 |
timeWaitingMicros.handleLock | ハンドルロックの取得を待機した時間 (マイクロ秒)。 |
timeWaitingMicros.schemaLock | スキーマロックの取得を待機した時間 (マイクロ秒)。高い値は、オープンな dhandle が多すぎることを強く示しています。 |
protocol:op_query <N>ms | 操作の合計時間 (ミリ秒)。 |
アクションを実行する前に、次のコマンドを使用してコレクションとインデックスの数を診断します。
// Count collections in a database
db.getSiblingDB(<dbName>).getCollectionNames().length
// View database statistics (collection count, index count, document count, total size)
db.getSiblingDB(<dbName>).stats()
// View statistics for a specific collection
db.getSiblingDB(<dbName>).<collectionName>.stats()最適化手法
状況に最適な方法を選択してください。まず、最も影響の少ないオプションから始めます。
不要なコレクションの削除
期限切れまたは不要になったコレクションを特定し、dropCollection を使用して削除します。詳細については、「dropCollection()」をご参照ください。
コレクションを削除する前に、完全バックアップが利用可能であることを確認してください。
不要なインデックスの削除
各インデックスは WiredTiger で個別のディスクファイルを作成し、対応する dhandle を追加します。インデックス数を減らすことで、dhandle の負荷を直接軽減できます。
$indexStats 集約ステージを使用して、使用頻度の低いインデックスを特定します。変更を加える前に、次のコマンドを実行します (適切な権限が必要です)。
// View access statistics for all indexes in a collection
db.getSiblingDB(<dbName>).<collectionName>.aggregate({"$indexStats":{}})サンプル出力:
{
"name" : "item_1_quantity_1",
"key" : { "item" : 1, "quantity" : 1 },
"host" : "examplehost.local:27018",
"accesses" : {
"ops" : NumberLong(1),
"since" : ISODate("2020-02-10T21:11:23.059Z")
}
}次の表は、出力の主要フィールドについて説明しています。
| フィールド | 説明 |
|---|---|
name | インデックス名 |
key | インデックスキーの詳細 |
accesses.ops | インデックスを使用した操作の数 (ヒット数)。インスタンスの再起動またはインデックスの再構築時にリセットされます。 |
accesses.since | 統計収集が開始されたタイムスタンプ |
削除するインデックスを決定する際に、次のルールを適用します。
無効なインデックス:どのクエリもアクセスしないフィールドのインデックスを削除します。
インデックスプレフィックスの冗長性:
{a:1}と{a:1,b:1}の両方が存在する場合、{a:1}は冗長です。{a:1,b:1}は、それを使用するすべてのクエリをカバーします。等価クエリの順序:
{a:1,b:1}と{b:1,a:1}の両方が存在する場合、ヒット数の少ない方を削除します。等価マッチングの場合、フィールドの順序は結果に影響しません。範囲クエリの ESR ルール:Equality, Sort, Range 順序で結合インデックスを構築します。詳細については、「The ESR (Equality, Sort, Range) Rule」をご参照ください。
ヒット数の少ないインデックス:これらは、よりヒット数の多いインデックスによって重複していることがよくあります。削除する前に、すべてのクエリパターンに対して評価してください。
インスタンスが MongoDB 4.4 以降を実行している場合は、db.collection.hideIndex() を使用して、インデックスを削除する前に非表示にします。インスタンスを一定期間監視して、どのクエリもインデックスに依存していないことを確認してから、完全に削除してください。詳細については、「db.collection.hideIndex()」をご参照ください。
players コレクションのインデックス最適化の例
players コレクションには、次のビジネスルールを持つプレーヤーデータが保存されています。20 coins ごとに自動的に 1 star に変換されます。
// players collection - document structure
{
"_id": "ObjectId(123)",
"first_name": "John",
"last_name": "Doe",
"coins": 11,
"stars": 2
}コレクションには現在、次のインデックスがあります。
_id(デフォルト){ last_name: 1 }{ last_name: 1, first_name: 1 }{ coins: -1 }{ stars: -1 }
上記のルールを適用すると、次のようになります。
{ coins: -1 } を削除:どのクエリも
coinsフィールドに直接アクセスしません。{ last_name: 1 } を削除:
{ last_name: 1, first_name: 1 }のプレフィックスであるため、冗長です。{ stars: -1 } を保持:
$indexStatsがヒット数の少なさを示していても、ラウンド終了時のリーダーボードでは、プレーヤーをstarsの降順でソートする必要があります。これを削除すると、フルコレクションスキャンが強制されます。
最適化後、コレクションは _id、{ last_name: 1, first_name: 1 }、および { stars: -1 } の 3 つのインデックスを保持します。これにより、ストレージ使用量が削減され、書き込みパフォーマンスが向上します。
インデックス最適化に関するその他の質問については、「チケットを送信」してください。
複数のコレクションからのデータ統合
時間ベースのパーティショニングパターンによりコレクションが時間とともに増加する場合、それらを単一のコレクションにマージすることで、蓄積の問題が解消されます。
最適化前 — temperatures データベースは、日ごとの測定値を個別のコレクションに保存します。
// temperatures.march-09-2020
{ "_id": 1, "timestamp": "2020-03-09T010:00:00Z", "temperature": 29 }
{ "_id": 2, "timestamp": "2020-03-09T010:30:00Z", "temperature": 30 }
// ... 25 readings total (sensor runs 10:00-22:00, every 30 minutes)
{ "_id": 25, "timestamp": "2020-03-09T022:00:00Z", "temperature": 26 }
// temperatures.march-10-2020
{ "_id": 1, "timestamp": "2020-03-10T010:00:00Z", "temperature": 30 }
// ...新しい日ごとに、新しいコレクションと新しい _id インデックスが作成されます。日をまたぐクエリには $lookup が必要であり、これは単一コレクションに対するクエリよりもパフォーマンスが低下します。
最適化後 — 単一の temperatures.readings コレクションは、1 日あたり 1 つのドキュメントですべてのデータを保存します。デフォルトの _id インデックスは、各日の測定値に対して追加のインデックスなしで日付ベースのクエリをサポートします。
// temperatures.readings
{
"_id": ISODate("2020-03-09"),
"readings": [
{ "timestamp": "2020-03-09T010:00:00Z", "temperature": 29 },
{ "timestamp": "2020-03-09T010:30:00Z", "temperature": 30 },
// ...
{ "timestamp": "2020-03-09T022:00:00Z", "temperature": 26 }
]
}
{
"_id": ISODate("2020-03-10"),
"readings": [
{ "timestamp": "2020-03-10T010:00:00Z", "temperature": 30 },
{ "timestamp": "2020-03-10T010:30:00Z", "temperature": 32 },
// ...
{ "timestamp": "2020-03-10T022:00:00Z", "temperature": 28 }
]
}これにより、無制限のコレクション増加問題が解消され、各日のインデックスを作成する必要がなくなります。
時系列ワークロードの場合、時系列コレクション (MongoDB 5.0 以降) の使用を検討してください。詳細については、「Time Series」をご参照ください。
インスタンスの分割
ApsaraDB for MongoDB スタンドアロンインスタンスのコレクションの総数を削減できない場合は、複数のインスタンスにデータを分割します。
| シナリオ | 分割ソリューション | 主な手順 |
|---|---|---|
| コレクションが複数のデータベースに分散している | 複数のアプリケーションまたはサービスが同一インスタンスを共有しており、かつそのデータベース同士が密接に関連していない場合、Data Transmission Service (DTS) を使用して一部のデータベースを新しい ApsaraDB for MongoDB インスタンスに移行します。詳細については、「ApsaraDB for MongoDB レプリカセットインスタンスから ApsaraDB for MongoDB レプリカセットまたはシャードクラスターインスタンスへのデータ移行」をご参照ください。 | DTS タスク作成時に必要な ソースデータベース を選択します。必要に応じて、コレクション名を保持するか、または名前を変更します。移行完了後、ソースインスタンスで dropDatabase を実行します。 |
| すべてのコレクションが単一のデータベース内にある | コレクションをリージョン、都市、優先度などのディメンションで分割可能かどうかを判断します。DTS を使用して、コレクションのサブセットを個別の ApsaraDB for MongoDB インスタンスに移行します。 | DTS タスク作成時に必要な ソースデータベース を選択します。drop コマンドを実行して、移行済みのコレクションをソースから削除します。インスタンスをまたいだ集計クエリには、追加のアプリケーションロジックが必要です。 |
移行カットオーバーを完了する前に、アプリケーションのビジネスロジックと接続構成を更新してください。
マルチテナントプラットフォームの分割例
マルチテナント管理プラットフォームは、テナントごとに 1 つのコレクションを使用します。テナント数が 100,000 を超えると、データベースのサイズがテラバイトになり、クエリが遅くなりました。
アプリケーションは、テナントを華北、東北、華東、華中、華南、西南、西北のリージョンで分割しました。DTS は、テナントを対応するリージョンにデプロイされた個別の ApsaraDB for MongoDB インスタンスに移行しました。データは、クロスリージョン集計と分析のためにデータウェアハウスにも同期されました。
分割後:
各インスタンスは元のコレクション数の一部を保持するため、インスタンスの仕様を削減できます。
リクエストは最も近いインスタンスによって処理され、レイテンシーがミリ秒に短縮されます。
インスタンスごとの運用とメンテナンスの複雑さが大幅に削減されます。
シャードタグを使用したシャードクラスターへの移行
すべてのコレクションが単一の論理インスタンスの下に留まり、削減できない場合、それらをシャードクラスターインスタンスに移行し、シャードタグを使用して各コレクションを特定のシャードにピン留めします。これにより、アプリケーションの変更なしに dhandle の負荷がシャード全体に分散されます。変更されるのは接続文字列のみです。
たとえば、100,000 のアクティブなコレクションを 10 シャードクラスターに移行すると、シャードあたり約 10,000 のコレクションになります。
詳細については、「sh.addShardTag()」および「sh.addTagRange()」をご参照ください。
手順
シャードクラスターインスタンスを購入します。詳細については、「シャードクラスターインスタンスの作成」をご参照ください。
シャードクラスターインスタンスの mongos ノードに接続します。詳細については、「mongo shell を使用して ApsaraDB for MongoDB シャードクラスターインスタンスに接続する」をご参照ください。
各シャードにシャードタグを追加します。
これらのコマンドを実行するには、アカウントに「必要な権限」が必要です。Data Management (DMS) は
sh.addShardTagをサポートしていません。代わりに mongo shell または mongosh を使用してください。sh.addShardTag("d-xxxxxxxxx1", "shard_tag1") sh.addShardTag("d-xxxxxxxxx2", "shard_tag2")各コレクションをシャードにピン留めするために、タグ範囲を事前構成します。
[MinKey, MaxKey]を使用して、コレクション内のすべてのデータが単一のシャードに留まるようにします。use <dbName> sh.enableSharding("<dbName>") sh.addTagRange("<dbName>.test", {"_id": MinKey}, {"_id": MaxKey}, "shard_tag1") sh.addTagRange("<dbName>.test1", {"_id": MinKey}, {"_id": MaxKey}, "shard_tag2")_idを実際のシャードキーに置き換えます。すべてのクエリにはシャードキーフィールドを含める必要があります。各コレクションをシャード化します。
sh.shardCollection("<dbName>.test", {"_id": 1}) sh.shardCollection("<dbName>.test1", {"_id": 1})sh.status()を実行して、タグルールが有効であることを確認します。
シャードクラスターインスタンスにデータを移行します。詳細については、「Migrate data from an ApsaraDB for MongoDB replica set instance to an ApsaraDB for MongoDB replica set or sharded cluster instance」をご参照ください。
ステップ 5 でシャードコレクションを事前に設定したため、ターゲットにはすでにコレクションのメタデータが存在します。[競合テーブルの処理モード] パラメーターを DTS タスクで エラーを無視して続行 に設定します。
データ整合性が検証されたら、アプリケーションをシャードクラスターインスタンスに切り替えます。
後でシャードを追加するには、ステップ 3 を繰り返して新しいシャードにタグを割り当てます。
移行後に新しいコレクションが作成された場合は、新しいコレクションごとにステップ 4 と 5 を繰り返します。そうしないと、コレクションはプライマリシャードにのみ存在し、シャード内のコレクション数が多くなります。この場合、インスタンスは常にコマ落ち状態になるか、例外が発生します。
ゾーンを使用したシャードクラスターへの移行
ゾーンはシャードタグと同じように機能しますが、新しい sh.addShardToZone() および sh.updateZoneKeyRange() コマンドを使用します。詳細については、「シャードゾーンの管理」、「sh.addShardToZone()」、および「sh.updateZoneKeyRange()」をご参照ください。
手順
シャードクラスターインスタンスを購入します。詳細については、「シャードクラスターインスタンスの作成」をご参照ください。
シャードクラスターインスタンスの Mongos ノードに接続します。詳細については、「ApsaraDB for MongoDB シャードクラスターインスタンスに mongo シェルを使用して接続する」をご参照ください。
各シャードにゾーンを割り当てます。
アカウントに「必要な権限」が必要です。Data Management (DMS) は
sh.addShardToZoneをサポートしていません。代わりに mongo shell または mongosh を使用してください。sh.addShardToZone("d-xxxxxxxxx1", "ZoneA") sh.addShardToZone("d-xxxxxxxxx2", "ZoneB")各コレクションをゾーンにピン留めするために、ゾーンキー範囲を事前構成します。
use <dbName> sh.enableSharding("<dbName>") sh.updateZoneKeyRange("<dbName>.test", { "_id": MinKey }, { "_id": MaxKey }, "ZoneA") sh.updateZoneKeyRange("<dbName>.test1", { "_id": MinKey }, { "_id": MaxKey }, "ZoneB")_idを実際のシャードキーに置き換えます。各コレクションをシャード化します。
sh.shardCollection("<dbName>.test", { _id: "hashed" }) sh.shardCollection("<dbName>.test1", { _id: "hashed" })sh.status()を実行して、ゾーンルールが有効であることを確認します。
シャードクラスターインスタンスにデータを移行してください。詳細については、「ApsaraDB for MongoDB レプリカセットインスタンスから ApsaraDB for MongoDB レプリカセットインスタンスまたはシャードクラスターインスタンスへのデータ移行」をご参照ください。
DTS タスクで [競合テーブルの処理モード] パラメーターを [エラーを無視して続行] に設定します。
データ整合性が検証されたら、アプリケーションをシャードクラスターインスタンスに切り替えます。
後でシャードを追加するには、ステップ 3 を繰り返して新しいシャードにゾーンを割り当てます。
移行後に新しいコレクションが作成された場合は、新しいコレクションごとにステップ 4 と 5 を繰り返します。そうしないと、コレクションはプライマリシャードにのみ存在し、シャード内のコレクション数が多くなります。この場合、インスタンスは常にコマ落ち状態になるか、例外が発生します。
潜在的リスク
コレクション数の多いデータベースで dropDatabase を実行しないこと
dropDatabase を実行すると、WiredTiger が非同期で当該データベース内のすべてのコレクションのメタデータおよび物理ファイルを削除します。この処理により、プライマリからセカンダリへのレプリケーションに影響が及び、レプリケーションラグが継続的に増加する可能性があります。また、このプロセス中には MongoDB のフローコントロール機構が有効化され、{writeConcern: majority} を指定したすべての書き込み操作が影響を受ける場合があります。
この問題を回避するには、以下のいずれかの方法をご利用ください:
各コレクションを個別に削除し、削除間隔を設けたうえで、すべてのコレクションが削除された後に
dropDatabaseを実行します。DTS またはその他の移行ツールを用いて、保持したいデータベースおよびコレクションを新しいインスタンスへ移行し、切り替え完了後に元のインスタンスを廃止します。
ご利用のインスタンスでレプリケーションラグのアラートを設定してください。万一この問題が発生した場合は、チケットを送信してテクニカルサポートをご利用ください。チケットを送信チケットを送信
概要
レプリカセットインスタンスにおけるコレクションの合計数は、10,000 を超えてはなりません。また、単一のコレクションにおけるインデックスの合計数が 15 を超える場合は、その数を削減してください。
コレクション単位でのマルチテナント隔離など、多数のコレクションを必要とするビジネス要件がある場合は、負荷分散のためにシャードクラスターインスタンスをご利用ください。
データベースに多数のコレクションが存在し、サポートが必要な場合は、「チケットを送信」してください。