BEARBEITEN - Abfrageleistung:
Wie @NeilLunn in seinen Kommentaren betonte, sollten Sie die Dokumente nicht manuell filtern, sondern .find(...)
verwenden dafür stattdessen:
db.snapshots.find({
roundedDate: { $exists: true },
stream: { $exists: true },
sid: { $exists: false }
})
Auch mit .bulkWrite()
, verfügbar ab MongoDB 3.2
, ist weitaus leistungsfähiger als einzelne Aktualisierungen.
Damit ist es möglich, dass Sie Ihre Abfrage innerhalb der 10-Minuten-Lebensdauer des Cursors ausführen können. Wenn es immer noch länger dauert, läuft Ihr Cursor ab und Sie haben sowieso das gleiche Problem, das unten erklärt wird:
Was ist hier los:
Error: getMore command failed
kann an einem Cursor-Timeout liegen, das mit zwei Cursorattributen zusammenhängt:
-
Timeout-Limit, das standardmäßig 10 Minuten beträgt. Aus der Dokumentation:
Standardmäßig schließt der Server den Cursor automatisch nach 10 Minuten Inaktivität oder wenn der Client den Cursor erschöpft hat.
-
Stapelgröße, die 101 Dokumente oder 16 MB für den ersten Stapel und 16 MB, unabhängig von der Anzahl der Dokumente, für nachfolgende Stapel beträgt (ab MongoDB
3.4
). Aus der Dokumentation:find()
undaggregate()
Operationen haben standardmäßig eine Anfangsstapelgröße von 101 Dokumenten. Nachfolgende getMore-Operationen, die für den resultierenden Cursor ausgeführt werden, haben keine standardmäßige Stapelgröße, daher sind sie nur durch die Nachrichtengröße von 16 Megabyte begrenzt.
Wahrscheinlich verbrauchen Sie diese ersten 101-Dokumente und erhalten dann einen Stapel von 16 MB, was das Maximum ist, mit viel mehr Dokumenten. Da die Verarbeitung mehr als 10 Minuten dauert, läuft der Cursor auf dem Server ab, und wenn Sie mit der Verarbeitung der Dokumente im zweiten Stapel fertig sind und einen neuen anfordern, ist der Cursor bereits geschlossen:
Wenn Sie den Cursor durchlaufen und das Ende des zurückgegebenen Stapels erreichen, führt cursor.next() eine getMore-Operation aus, um den nächsten Stapel abzurufen, wenn weitere Ergebnisse vorhanden sind.
Mögliche Lösungen:
Ich sehe 5 Möglichkeiten, dies zu lösen, 3 gute mit ihren Vor- und Nachteilen und 2 schlechte:
-
👍 Reduzieren der Stapelgröße, um den Cursor am Leben zu erhalten.
-
👍 Entfernen Sie die Zeitüberschreitung vom Cursor.
-
👍 Wiederholen, wenn der Cursor abgelaufen ist.
-
👎 Fragen Sie die Ergebnisse stapelweise manuell ab.
-
👎 Erhalten Sie alle Dokumente, bevor der Cursor abläuft.
Beachten Sie, dass sie nicht nach bestimmten Kriterien nummeriert sind. Lesen Sie sie durch und entscheiden Sie, welche für Ihren speziellen Fall am besten geeignet ist.
1. 👍 Reduzieren der Stapelgröße, um den Cursor am Leben zu erhalten
Eine Möglichkeit, dies zu lösen, ist die Verwendung von cursor.bacthSize
um die Stapelgröße auf dem Cursor festzulegen, der von Ihrem find
zurückgegeben wird Abfrage, die mit denen übereinstimmt, die Sie innerhalb dieser 10 Minuten verarbeiten können:
const cursor = db.collection.find()
.batchSize(NUMBER_OF_DOCUMENTS_IN_BATCH);
Beachten Sie jedoch, dass das Festlegen einer sehr konservativen (kleinen) Stapelgröße wahrscheinlich funktioniert, aber auch langsamer ist, da Sie jetzt häufiger auf den Server zugreifen müssen.
Wenn Sie ihn andererseits auf einen Wert setzen, der zu nahe an der Anzahl der Dokumente liegt, die Sie in 10 Minuten verarbeiten können, bedeutet dies, dass es möglich ist, dass einige Iterationen aus irgendeinem Grund etwas länger dauern (andere Prozesse verbrauchen möglicherweise mehr Ressourcen). , läuft der Cursor sowieso ab und Sie erhalten denselben Fehler erneut.
2. 👍 Entfernen Sie die Zeitüberschreitung vom Cursor
Eine weitere Option ist die Verwendung von cursor.noCursorTimeout, um zu verhindern, dass der Cursor abläuft:
const cursor = db.collection.find().noCursorTimeout();
Dies wird als schlechte Vorgehensweise angesehen, da Sie den Cursor manuell schließen oder alle Ergebnisse ausschöpfen müssten, damit er automatisch geschlossen wird:
Nach dem Setzen von noCursorTimeout
müssen Sie den Cursor entweder manuell mit cursor.close()
schließen oder durch Erschöpfen der Ergebnisse des Cursors.
Da Sie alle Dokumente im Cursor verarbeiten möchten, müssten Sie ihn nicht manuell schließen, aber es ist immer noch möglich, dass etwas anderes in Ihrem Code schief geht und ein Fehler ausgegeben wird, bevor Sie fertig sind, wodurch der Cursor geöffnet bleibt .
Wenn Sie diesen Ansatz dennoch verwenden möchten, verwenden Sie ein try-catch
um sicherzustellen, dass Sie den Cursor schließen, wenn etwas schief geht, bevor Sie alle seine Dokumente verbrauchen.
Hinweis:Ich halte dies nicht für eine schlechte Lösung (daher das 👍), da ich sogar dachte, dass es als schlechte Praxis angesehen wird ...:
-
Es ist eine Funktion, die vom Treiber unterstützt wird. Wenn es so schlimm war, da es alternative Möglichkeiten gibt, Zeitüberschreitungsprobleme zu umgehen, wie in den anderen Lösungen erläutert, wird dies nicht unterstützt.
-
Es gibt Möglichkeiten, es sicher zu verwenden, es geht nur darum, besonders vorsichtig damit umzugehen.
-
Ich nehme an, Sie führen diese Art von Abfragen nicht regelmäßig aus, daher ist die Wahrscheinlichkeit gering, dass Sie anfangen, überall offene Cursor zu hinterlassen. Wenn dies nicht der Fall ist und Sie sich wirklich ständig mit diesen Situationen auseinandersetzen müssen, ist es sinnvoll,
noCursorTimeout
nicht zu verwenden .
3. 👍 Wiederholen, wenn der Cursor abgelaufen ist
Grundsätzlich fügen Sie Ihren Code in einen try-catch
ein und wenn Sie den Fehler erhalten, erhalten Sie einen neuen Cursor, der die Dokumente überspringt, die Sie bereits verarbeitet haben:
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;
}
}
}
Beachten Sie, dass Sie die Ergebnisse sortieren müssen, damit diese Lösung funktioniert.
Mit diesem Ansatz minimieren Sie die Anzahl der Anfragen an den Server, indem Sie die maximal mögliche Stapelgröße von 16 MB verwenden, ohne vorher raten zu müssen, wie viele Dokumente Sie in 10 Minuten verarbeiten können. Daher ist es auch robuster als der vorherige Ansatz.
4. 👎 Fragen Sie die Ergebnisse stapelweise manuell ab
Grundsätzlich verwenden Sie skip(), limit() und sort(), um mehrere Abfragen mit einer Anzahl von Dokumenten durchzuführen, von denen Sie glauben, dass Sie sie in 10 Minuten verarbeiten können.
Ich halte dies für eine schlechte Lösung, da der Treiber bereits die Möglichkeit hat, die Stapelgröße festzulegen, sodass es keinen Grund gibt, dies manuell zu tun. Verwenden Sie einfach Lösung 1 und erfinden Sie das Rad nicht neu.
Es ist auch erwähnenswert, dass es die gleichen Nachteile wie Lösung 1 hat,
5. 👎 Erhalte alle Dokumente, bevor der Cursor abläuft
Wahrscheinlich dauert die Ausführung Ihres Codes aufgrund der Ergebnisverarbeitung einige Zeit, sodass Sie zuerst alle Dokumente abrufen und dann verarbeiten können:
const results = new Array(db.snapshots.find());
Dadurch werden alle Stapel nacheinander abgerufen und der Cursor geschlossen. Dann können Sie alle Dokumente in results
durchlaufen und tun, was Sie tun müssen.
Wenn Sie jedoch Zeitüberschreitungsprobleme haben, besteht die Möglichkeit, dass Ihre Ergebnismenge ziemlich groß ist, sodass es möglicherweise nicht ratsam ist, alles aus dem Speicher zu ziehen.
Hinweis zum Snapshot-Modus und doppelten Dokumenten
Es ist möglich, dass einige Dokumente mehrmals zurückgegeben werden, wenn dazwischenliegende Schreibvorgänge sie aufgrund einer Zunahme der Dokumentgröße verschieben. Um dies zu lösen, verwenden Sie cursor.snapshot()
. Aus der Dokumentation:
Hängen Sie die Methode snapshot() an einen Cursor an, um den „Snapshot“-Modus umzuschalten. Dadurch wird sichergestellt, dass die Abfrage ein Dokument nicht mehrfach zurückgibt, selbst wenn zwischenzeitliche Schreibvorgänge aufgrund der Zunahme der Dokumentgröße zu einer Verschiebung des Dokuments führen.
Beachten Sie jedoch die Einschränkungen:
-
Es funktioniert nicht mit fragmentierten Sammlungen.
-
Es funktioniert nicht mit
sort()
oderhint()
, daher funktioniert es nicht mit den Lösungen 3 und 4. -
Es garantiert keine Isolation von Einfügungen oder Löschungen.
Beachten Sie, dass bei Lösung 5 das Zeitfenster für eine Verschiebung von Dokumenten, die zum Abruf doppelter Dokumente führen kann, enger ist als bei den anderen Lösungen, sodass Sie snapshot()
möglicherweise nicht benötigen .
In Ihrem speziellen Fall heißt die Sammlung snapshot
, wahrscheinlich wird es sich nicht ändern, also brauchen Sie wahrscheinlich snapshot()
nicht . Darüber hinaus führen Sie Aktualisierungen an Dokumenten basierend auf ihren Daten durch, und sobald die Aktualisierung abgeschlossen ist, wird dasselbe Dokument nicht erneut aktualisiert, obwohl es mehrere Male abgerufen wird, wie der if
Bedingung wird es übersprungen.
Hinweis zu offenen Cursorn
Um die Anzahl der geöffneten Cursor anzuzeigen, verwenden Sie db.serverStatus().metrics.cursor
.