it-swarm.com.de

MongoDB - Fehler: Befehl getMore fehlgeschlagen: Cursor nicht gefunden

Ich muss ein neues Feld sid für jedes Dokument in einer Sammlung von etwa 500.000 Dokumenten erstellen. Jede sid ist eindeutig und basiert auf den vorhandenen roundedDate- und stream-Feldern dieses Datensatzes.

Ich mache das mit dem folgenden Code:

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);

Es funktioniert zunächst gut, aber nach ein paar Stunden und etwa 100.000 Datensätzen tritt ein Fehler auf mit:

Error: getMore command failed: {
    "ok" : 0,
    "errmsg": "Cursor not found, cursor id: ###",
    "code": 43,
}: ...

 mongo error

14
Chava Sobreyra

EDIT - Abfrageleistung:

Wie @NeilLunn in seinen Kommentaren betonte, sollten Sie die Dokumente nicht manuell filtern, sondern stattdessen .find(...) verwenden:

db.snapshots.find({
    roundedDate: { $exists: true },
    stream: { $exists: true },
    sid: { $exists: false }
})

Außerdem ist die Verwendung von .bulkWrite() , das ab MongoDB 3.2 Verfügbar ist, weitaus performanter als die Durchführung einzelner Aktualisierungen.

Es ist möglich, dass Sie damit Ihre Abfrage innerhalb der 10-minütigen Lebensdauer des Cursors ausführen können. Wenn es immer noch länger dauert, verfällt der Cursor und Sie haben trotzdem das gleiche Problem, das unten erklärt wird:

Was geht hier vor sich:

Error: getMore command failed Kann auf ein Cursor-Timeout zurückzuführen sein, das mit zwei Cursor-Attributen zusammenhängt:

  • Zeitlimit, das standardmäßig 10 Minuten beträgt. Aus den Dokumenten :

    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: 101 Dokumente oder 16 MB für den ersten Stapel und 16 MB, unabhängig von der Anzahl der Dokumente, für nachfolgende Stapel (ab MongoDB 3.4). Aus den Dokumenten :

    find() und aggregate() Vorgänge haben standardmäßig eine anfängliche Stapelgröße von 101 Dokumenten. Nachfolgende getMore Vorgänge, die für den resultierenden Cursor ausgegeben werden, haben keine Standard-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 maximal 16 MB mit viel mehr Dokumenten. Da die Verarbeitung länger als 10 Minuten dauert, tritt eine Zeitüberschreitung des Cursors auf dem Server auf. Wenn Sie die Dokumente im zweiten Stapel bearbeitet haben, und fordern Sie ein neues an Cursor ist bereits geschlossen:

Wenn Sie den Cursor durchlaufen und das Ende des zurückgegebenen Stapels erreichen und weitere Ergebnisse vorliegen, führt cursor.next () eine getMore-Operation aus, um den nächsten Stapel abzurufen.


Mögliche Lösungen:

Ich sehe 5 Möglichkeiten, dies zu lösen, 3 gute mit ihren Vor- und Nachteilen und 2 schlechte:

  1. ???? Reduzieren Sie die Stapelgröße, um den Cursor am Leben zu halten.

  2. ???? Entfernen Sie das Timeout vom Cursor.

  3. ???? Versuchen Sie es erneut, wenn der Cursor abläuft.

  4. ???? Abfrage der Ergebnisse in Stapeln manuell.

  5. ???? Holen Sie sich alle Dokumente, bevor der Cursor abläuft.

Beachten Sie, dass sie nach bestimmten Kriterien nicht nummeriert sind. Lesen Sie sie durch und entscheiden Sie, welches für Ihren speziellen Fall am besten geeignet ist.


1. ???? Reduzieren Sie die Stapelgröße, um den Cursor am Leben zu halten

Eine Möglichkeit, dies zu lösen, besteht darin, mit cursor.bacthSize die Batch-Größe des Cursors, der von Ihrer find-Abfrage zurückgegeben wird, so festzulegen, dass sie mit den Werten übereinstimmt, die Sie in diesen 10 verarbeiten können Protokoll:

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 öfter auf den Server zugreifen müssen.

Wenn Sie dagegen einen Wert festlegen, der zu nahe an der Anzahl der Dokumente liegt, die Sie in 10 Minuten verarbeiten können, kann es sein, dass die Verarbeitung einiger Iterationen aus irgendeinem Grund etwas länger dauert (andere Prozesse verbrauchen möglicherweise mehr Ressourcen). verfällt der Cursor trotzdem und Sie erhalten den gleichen Fehler erneut.


2. ???? Entfernen Sie das Timeout vom Cursor

Eine andere Möglichkeit besteht darin, cursor.noCursorTimeout zu verwenden, um zu verhindern, dass der Cursor ein Zeitlimit überschreitet:

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:

Nachdem Sie die Option noCursorTimeout eingestellt haben, müssen Sie den Cursor entweder manuell mit cursor.close() schließen oder die Ergebnisse des Cursors erschöpfen.

Da Sie alle Dokumente im Cursor verarbeiten möchten, müssen Sie sie nicht manuell schließen. Es ist jedoch weiterhin möglich, dass in Ihrem Code ein anderer Fehler auftritt und ein Fehler ausgegeben wird, bevor Sie fertig sind, sodass der Cursor geöffnet bleibt .

Wenn Sie diesen Ansatz weiterhin verwenden möchten, verwenden Sie einen try-catch, Um sicherzustellen, dass Sie den Cursor schließen, wenn etwas schief geht, bevor Sie alle seine Dokumente verbrauchen.

Hinweis: Ich halte das nicht für eine schlechte Lösung (daher das ????), da ich sogar dachte, dass es eine schlechte Praxis ist ...:

  • Diese Funktion wird vom Treiber unterstützt. Wenn es so schlimm war, da es alternative Möglichkeiten gibt, um 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 ist nur eine Frage der besonderen Vorsicht.

  • Ich gehe davon aus, dass Sie diese Art von Abfragen nicht regelmäßig ausführen, sodass die Wahrscheinlichkeit gering ist, dass Sie überall offene Cursor lassen. Wenn dies nicht der Fall ist und Sie wirklich die ganze Zeit mit diesen Situationen fertig werden müssen, ist es sinnvoll, noCursorTimeout nicht zu verwenden.


3. ???? Versuchen Sie es erneut, wenn der Cursor abläuft

Grundsätzlich schreiben Sie Ihren Code in ein try-catch Und wenn Sie die Fehlermeldung erhalten, erhalten Sie einen neuen Cursor, der die Dokumente überspringt, die Sie bereits bearbeitet 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 Anforderungen an den Server, indem Sie die maximal mögliche Stapelgröße von 16 MB verwenden, ohne vorher erraten zu müssen, wie viele Dokumente Sie in 10 Minuten verarbeiten können. Daher ist es auch robuster als der bisherige Ansatz.


4. ???? Abfrage der Ergebnisse in Batches manuell

Grundsätzlich verwenden Sie skip () , limit () und sort () , um mehrere Abfragen mit einer Anzahl von auszuführen Dokumente, von denen Sie glauben, dass sie in 10 Minuten verarbeitet werden können.

Ich halte dies für eine schlechte Lösung, da der Fahrer bereits die Möglichkeit hat, die Stapelgröße festzulegen. Es gibt also keinen Grund, dies manuell zu tun. Verwenden Sie einfach Lösung 1 und erfinden Sie das Rad nicht neu.

Erwähnenswert ist auch, dass es die gleichen Nachteile wie Lösung 1 hat.


5. ???? Holen Sie sich 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 das tun, was Sie tun müssen.

Wenn Sie jedoch Zeitüberschreitungsprobleme haben, ist die Wahrscheinlichkeit groß, dass Ihre Ergebnismenge ziemlich groß ist. Daher ist es möglicherweise nicht ratsam, alles aus dem Speicher zu ziehen.


Hinweis zum Snapshot-Modus und zum Duplizieren von Dokumenten

Es ist möglich, dass einige Dokumente mehrfach zurückgegeben werden, wenn sie durch zwischenzeitliche Schreibvorgänge aufgrund einer Zunahme der Dokumentgröße verschoben werden. Um dies zu lösen, verwenden Sie cursor.snapshot(). Aus den Dokumenten :

Hängen Sie die snapshot () -Methode an einen Cursor an, um den "snapshot" -Modus umzuschalten. Auf diese Weise wird sichergestellt, dass die Abfrage ein Dokument nicht mehrmals zurückgibt, selbst wenn zwischenzeitliche Schreibvorgänge aufgrund der Zunahme der Dokumentgröße zu einer Verschiebung des Dokuments führen.

Beachten Sie jedoch die folgenden Einschränkungen:

  • Es funktioniert nicht mit Sharded Collections.

  • Es funktioniert nicht mit sort() oder hint() , daher funktioniert es nicht mit den Lösungen 3 und 4 .

  • Dies garantiert keine Isolation von Einfügungen oder Löschungen.

Beachten Sie bei Lösung 5, dass das Zeitfenster für das Verschieben von Dokumenten, bei dem möglicherweise doppelte Dokumente abgerufen werden, enger ist als bei den anderen Lösungen, sodass Sie möglicherweise snapshot() nicht benötigen.

In Ihrem speziellen Fall wird sich die Auflistung wahrscheinlich nicht ändern, da sie snapshot heißt. Daher benötigen Sie snapshot() wahrscheinlich nicht. Darüber hinaus führen Sie Aktualisierungen für Dokumente auf der Grundlage ihrer Daten durch. Sobald die Aktualisierung abgeschlossen ist, wird dasselbe Dokument nicht erneut aktualisiert, obwohl es mehrmals abgerufen wird, da die Bedingung if dies überspringt.


Hinweis zu offenen Cursorn

Um die Anzahl der offenen Cursor zu sehen, verwenden Sie db.serverStatus().metrics.cursor .

61
Danziger

Es ist ein Fehler in der Sitzungsverwaltung von Mongodb-Server. Korrektur, die gerade ausgeführt wird, sollte in 4.0+ behoben sein

SERVER-34810: Durch die Aktualisierung des Sitzungscaches können Cursor, die noch verwendet werden, irrtümlich gelöscht werden.

(reproduziert in MongoDB 3.6.5)

das Hinzufügen von collection.find().batchSize(20) half mir mit einer winzigen reduzierten Leistung.

2

Ich bin auch auf dieses Problem gestoßen, aber für mich wurde es durch einen Fehler im MongDB-Treiber verursacht.

Dies geschah in der Version 3.0.x des npm-Pakets mongodb, die z.B. verwendet in Meteor 1.7.0.x, wo ich auch dieses Problem aufgezeichnet habe. Es wird in diesem Kommentar weiter beschrieben und der Thread enthält ein Beispielprojekt, das den Fehler bestätigt: https://github.com/meteor/meteor/issues/9944#issuecomment-420542042

Die Aktualisierung des npm-Pakets auf 3.1.x hat es für mich behoben, da ich die guten Ratschläge, die hier von @Danziger gegeben wurden, bereits berücksichtigt hatte.

1
SimonSimCity

Bei Verwendung des Java-Treibers v3 sollte noCursorTimeout in den FindOptions festgelegt werden.

DBCollectionFindOptions options =
                    new DBCollectionFindOptions()
                        .maxTime(90, TimeUnit.MINUTES)
                        .noCursorTimeout(true)
                        .batchSize(batchSize)
                        .projection(projectionQuery);        
cursor = collection.find(filterQuery, options);
0
user1240792