約50万のドキュメントのコレクション内の各ドキュメントに新しいフィールドsid
を作成する必要があります。各sid
は一意であり、そのレコードの既存のroundedDate
およびstream
フィールドに基づいています。
私は次のコードでそうしています:
var cursor = db.getCollection('snapshots').find();
var iterated = 0;
var updated = 0;
while (cursor.hasNext()) {
var doc = cursor.next();
if (doc.stream && doc.roundedDate && !doc.sid) {
db.getCollection('snapshots').update({ "_id": doc['_id'] }, {
$set: {
sid: doc.stream.valueOf() + '-' + doc.roundedDate,
}
});
updated++;
}
iterated++;
};
print('total ' + cursor.count() + ' iterated through ' + iterated + ' updated ' + updated);
最初は問題なく動作しますが、数時間後に約10万件のレコードが記録されると、次のエラーが発生します。
Error: getMore command failed: {
"ok" : 0,
"errmsg": "Cursor not found, cursor id: ###",
"code": 43,
}: ...
@NeilLunnがコメントで指摘したように、ドキュメントを手動でフィルタリングするのではなく、代わりに.find(...)
を使用する必要があります。
db.snapshots.find({
roundedDate: { $exists: true },
stream: { $exists: true },
sid: { $exists: false }
})
また、MongoDB 3.2
から利用可能な .bulkWrite()
を使用すると、個々の更新を行うよりもはるかにパフォーマンスが向上します。
それにより、カーソルの10分間の有効期間内にクエリを実行できる可能性があります。それでもそれ以上かかる場合は、カーソルが期限切れになり、とにかく同じ問題が発生します。これについては以下で説明します。
Error: getMore command failed
は、2つのカーソル属性に関連するカーソルタイムアウトが原因である可能性があります。
タイムアウト制限。デフォルトでは10分です。 ドキュメントから :
デフォルトでは、非アクティブ状態が10分間続くか、クライアントがカーソルを使い果たした場合、サーバーは自動的にカーソルを閉じます。
バッチサイズ。101個のドキュメントまたは最初のバッチでは16 MB、ドキュメントの数に関係なく、後続のバッチでは16 MB(MongoDB 3.4
時点)。 ドキュメントから :
find()
およびaggregate()
操作の初期バッチサイズは、デフォルトで101ドキュメントです。後続の getMore 結果のカーソルに対して発行される操作にはデフォルトのバッチサイズがないため、16メガバイトのメッセージサイズによってのみ制限されます。
おそらく、最初の101個のドキュメントを使用してから、最大16 MBのバッチを取得し、さらに多くのドキュメントを追加することになります。それらを処理するのに10分以上かかるため、サーバー上のカーソルがタイムアウトし、2番目のバッチでドキュメントの処理が完了するまでに および新しいドキュメントを要求します 、カーソルは既に閉じています:
カーソルを繰り返し処理し、返されたバッチの最後に到達すると、さらに結果がある場合、cursor.next()はgetMore操作を実行して次のバッチを取得します。
私はこれを解決するための5つの可能な方法、3つの良い方法、それらの長所と短所、2つの悪い方法を見ています:
????バッチサイズを小さくして、カーソルを維持します。
????カーソルからタイムアウトを削除します。
????カーソルの有効期限が切れたら再試行してください。
????結果をバッチで手動でクエリします。
????カーソルが期限切れになる前にすべてのドキュメントを取得します。
特定の基準に従って番号が付けられていないことに注意してください。それらを読み、特定のケースに最適なものを決定します。
これを解決する1つの方法は、 cursor.bacthSize
を使用して、find
クエリによって返されるカーソルのバッチサイズを、10分以内に処理できるものと一致するように設定することです。
const cursor = db.collection.find()
.batchSize(NUMBER_OF_DOCUMENTS_IN_BATCH);
ただし、非常に保守的な(小さな)バッチサイズを設定することはおそらく機能しますが、サーバーへのアクセス回数を増やす必要があるため、速度も遅くなることに注意してください。
一方、10分で処理できるドキュメントの数に近すぎる値に設定すると、何らかの理由で一部の反復の処理に少し時間がかかる場合があります(他のプロセスがより多くのリソースを消費している可能性があります) 、とにかくカーソルは期限切れになり、同じエラーが再び表示されます。
別のオプションは、 cursor.noCursorTimeout を使用して、カーソルがタイムアウトしないようにすることです。
const cursor = db.collection.find().noCursorTimeout();
これは、カーソルを手動で閉じるか、すべての結果を使い果たして自動的に閉じる必要があるため、悪い習慣と見なされます:
noCursorTimeout
オプションを設定したら、カーソルをcursor.close()
で手動で閉じるか、カーソルの結果を使い果たす必要があります。
カーソル内のすべてのドキュメントを処理するため、手動で閉じる必要はありませんが、コード内で何か他の問題が発生し、完了する前にエラーがスローされ、カーソルが開いたままになる可能性があります。
それでもこの方法を使用する場合は、try-catch
を使用して、すべてのドキュメントを使用する前に何か問題が発生した場合にカーソルを確実に閉じてください。
注:これは悪い方法だと考えられていたので、これは悪い解決策ではありません(したがって????)。
これは、ドライバーがサポートする機能です。他のソリューションで説明されているように、タイムアウトの問題を回避する代替方法があるため、それが非常に悪かった場合、これはサポートされません。
それを安全に使用する方法があります、それはそれで余分な注意を払うだけの問題です。
この種のクエリを定期的に実行しているわけではないため、開いているカーソルをどこにでも残してしまう可能性は低くなります。これが当てはまらず、常にこれらの状況に対処する必要がある場合は、noCursorTimeout
を使用しないのが理にかなっています。
基本的に、コードをtry-catch
に配置し、エラーが発生すると、すでに処理したドキュメントをスキップする新しいカーソルを取得します。
let processed = 0;
let updated = 0;
while(true) {
const cursor = db.snapshots.find().sort({ _id: 1 }).skip(processed);
try {
while (cursor.hasNext()) {
const doc = cursor.next();
++processed;
if (doc.stream && doc.roundedDate && !doc.sid) {
db.snapshots.update({
_id: doc._id
}, { $set: {
sid: `${ doc.stream.valueOf() }-${ doc.roundedDate }`
}});
++updated;
}
}
break; // Done processing all, exit outer loop
} catch (err) {
if (err.code !== 43) {
// Something else than a timeout went wrong. Abort loop.
throw err;
}
}
}
このソリューションが機能するには、結果をソートする必要があることに注意してください。
このアプローチでは、事前に10分以内に処理できるドキュメントの数を推測することなく、最大16 MBのバッチサイズを使用して、サーバーへの要求の数を最小限に抑えています。したがって、以前のアプローチよりも堅牢です。
基本的に、 skip() 、 limit() および sort() を使用して、処理できると思われる多数のドキュメントで複数のクエリを実行します10分で。
ドライバーには既にバッチサイズを設定するオプションがあるため、これは悪いソリューションであると考えます。したがって、手動でこれを行う理由はなく、ソリューション1を使用し、車輪を再発明しないでください。
また、ソリューション1と同じ欠点があることに言及する価値があります。
おそらく、結果処理のためにコードの実行に時間がかかるため、最初にすべてのドキュメントを取得してから処理することができます。
const results = new Array(db.snapshots.find());
これにより、すべてのバッチが次々に取得され、カーソルが閉じられます。次に、results
内のすべてのドキュメントをループして、必要な処理を実行できます。
ただし、タイムアウトの問題がある場合は、結果セットが非常に大きい可能性があります。したがって、メモリ内のすべてをプルすることは、最も賢明なことではありません。
文書サイズの増加により、書き込み操作を介在させると、一部の文書が複数回返される可能性があります。これを解決するには、cursor.snapshot()
を使用します。 ドキュメントから :
Snapshot()メソッドをカーソルに追加して、「スナップショット」モードを切り替えます。これにより、文書サイズの増加により、書き込み操作の途中で文書が移動しても、クエリが文書を複数回返さないことが保証されます。
ただし、その制限に留意してください。
ソリューション5では、重複ドキュメントの取得を引き起こす可能性のあるドキュメントの移動の時間ウィンドウが他のソリューションよりも狭いため、snapshot()
は必要ない場合があります。
特定のケースでは、コレクションはsnapshot
と呼ばれるため、おそらく変更される可能性は低いため、おそらくsnapshot()
は必要ありません。さらに、データに基づいてドキュメントを更新します。更新が完了すると、同じドキュメントはif
条件によってスキップされるため、複数回取得されても再び更新されることはありません。
開いているカーソルの数を確認するには、 db.serverStatus().metrics.cursor
を使用します。
これは、mongodbサーバーのセッション管理のバグです。現在進行中の修正、4.0 +で修正される必要があります
SERVER-34810:セッションキャッシュのリフレッシュにより、まだ使用中のカーソルが誤って強制終了される場合があります
(MongoDB 3.6.5で再現)
collection.find().batchSize(20)
を追加すると、パフォーマンスがわずかに低下しました。
私もこの問題に遭遇しましたが、私にとってはMongDBドライバーのバグが原因でした。
3.0.x
npmパッケージmongodb
のバージョンで発生しました。この問題を記録したMeteor 1.7.0.x
で使用されています。このコメントでさらに詳しく説明し、スレッドにはバグを確認するサンプルプロジェクトが含まれています。 https://github.com/meteor/meteor/issues/9944#issuecomment-420542042
Npmパッケージを3.1.x
に更新すると、ここで@Danzigerから与えられた良いアドバイスを考慮に入れていたので、それを修正しました。
Java v3ドライバーを使用する場合、FindOptionsでnoCursorTimeoutを設定する必要があります。
DBCollectionFindOptions options =
new DBCollectionFindOptions()
.maxTime(90, TimeUnit.MINUTES)
.noCursorTimeout(true)
.batchSize(batchSize)
.projection(projectionQuery);
cursor = collection.find(filterQuery, options);