MongoDB異常MongoCursorNotFoundException


昨天在測試數據導出的時候發現,若連續導出多次,則會報如下異常:

com.mongodb.MongoCursorNotFoundException: Query failed with error code -5

異常信息為Mongo查詢的游標找不到導致查詢失敗;
網上的解決辦法大多包含如下幾種:

  1. noCursorTimeout 設置cursor無超時時間

    此種操作查詢完成后需要手動清理cursor,若因為異常或網絡則會導致游標一直存在,所以不推薦此方法

  2. batchSize 指定在 MongoDB 實例的每批響應中要返回的文檔數

    https://www.docs4dev.com/docs/zh/mongodb/v3.6/reference/reference-method-cursor.batchSize.html

官方文檔:

  • 關於cursor的說明

看完上面的解決方法,應該為虎軀一震,原來是這樣。但是不要忽略開篇第一句的問題所在,若連續導出多次,則會報如下異常
由此可知,我們的異常並不是因為cursor的過期而導致的,那為什么會出現cursor not found呢?

我們先看下find的部分查詢源碼:

	/**
	 * Internal method using callback to do queries against the datastore that requires reading a collection of objects.
	 * It will take the following steps
	 * <ol>
	 * <li>Execute the given {@link ConnectionCallback} for a {@link DBCursor}.</li>
	 * <li>Prepare that {@link DBCursor} with the given {@link CursorPreparer} (will be skipped if {@link CursorPreparer}
	 * is {@literal null}</li>
	 * <li>Iterate over the {@link DBCursor} and applies the given {@link DocumentCallback} to each of the
	 * {@link Document}s collecting the actual result {@link List}.</li>
	 * <ol>
	 *
	 * @param <T>
	 * @param collectionCallback the callback to retrieve the {@link DBCursor} with
	 * @param preparer the {@link CursorPreparer} to potentially modify the {@link DBCursor} before iterating over it
	 * @param objectCallback the {@link DocumentCallback} to transform {@link Document}s into the actual domain type
	 * @param collectionName the collection to be queried
	 * @return
	 */
	private <T> List<T> executeFindMultiInternal(CollectionCallback<FindIterable<Document>> collectionCallback,
			@Nullable CursorPreparer preparer, DocumentCallback<T> objectCallback, String collectionName) {

		try {

			MongoCursor<Document> cursor = null;

			try {

				FindIterable<Document> iterable = collectionCallback
						.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName));

				if (preparer != null) {
					iterable = preparer.prepare(iterable);
				}

				cursor = iterable.iterator();

				List<T> result = new ArrayList<>();

				while (cursor.hasNext()) {
					Document object = cursor.next();
					result.add(objectCallback.doWith(object));
				}

				return result;
			} finally {

				if (cursor != null) {
					cursor.close();
				}
			}
		} catch (RuntimeException e) {
			throw potentiallyConvertRuntimeException(e, exceptionTranslator);
		}
	}

摘自網絡博客:

當我們在使用db.collection.find()命令查詢mongodb數據時,直接返回給你的並不是數據本身,而是一個游標,每個游標都有對應的一個游標ID,服務器會記錄這個游標ID,真正獲取數據時,是通過對游標進行遍歷拿到數據,對應的遍歷方法主要是hashNext()和next(),跟iterator迭代器一樣使用(命令行客戶端之所以通過find()命令就得到數據,是因為它自動幫你遍歷了游標,且默認展示了20條數據),客戶端通過游標從服務端獲取數據時並不是一條一條的,而是一批一批的,這樣可以提升IO性能,每批數據都緩存在客戶端內存中,通過next()遍歷完后,繼續通過getMore()方法去服務器獲取下一批數據,而此時需要攜帶cursorid的,服務器通過cursorid辨別是取什么數據,當服務器端沒有這個cursorid時,就會發生這個游標找不到的錯誤。

以此我們知道了find命令是依賴batchsize配置來進行迭代多次查詢的,那么如果說cursor並沒有過期,只是多次獲取時找不到了呢?

我們繼續查閱源碼,在Mongo驅動的代碼中找到了獲取連接的代碼:

    // 摘自com.mongodb.operation.QueryBatchCursor類中
    private void getMore() {
        Connection connection = connectionSource.getConnection();
        try {
            if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                try {
                    initFromCommandResult(connection.command(namespace.getDatabaseName(),
                                                             asGetMoreCommandDocument(),
                                                             NO_OP_FIELD_NAME_VALIDATOR,
                                                             ReadPreference.primary(),
                                                             CommandResultDocumentCodec.create(decoder, "nextBatch"),
                                                             connectionSource.getSessionContext()));
                } catch (MongoCommandException e) {
                    throw translateCommandException(e, serverCursor);
                }
            } else {
                QueryResult<T> getMore = connection.getMore(namespace, serverCursor.getId(),
                        getNumberToReturn(limit, batchSize, count), decoder);
                initFromQueryResult(getMore);
            }
            if (limitReached()) {
                killCursor(connection);
            }
            if (serverCursor == null) {
                this.connectionSource.release();
                this.connectionSource = null;
            }
        } finally {
            connection.release();
        }
    }
  • 以此可以看見finally中執行了connection.release() 即每次連接后都會斷開連接;

  • 那么會不會存在mongo集群下,連接到另一台機器的情況呢?
    查閱資料:

    正常情況下,當我們使用mongodb集群時,將所有mongodb服務器以 IP1:PORT1,IP2:PORT2,IP3:PORT3的形式傳給驅動,驅動能夠自動完成負載均衡和保持會話轉發到同一個服務器,這時候不會出現問題;

  • 一旦我們自己實現負載均衡,即用了統一域名或者ip分發了Ip.就會存在每次連接到不同機器,導致找不到cursor,也因此會拋出MongoCursorNotFoundException的錯誤;

  • 當然,如果自己實現的負載根據Ip來進行了機器分發,確保相同ip每次分發請求到同一台機器,那么也不會存在此類問題;

后來問了我們這邊的dba,發現mongo集群的確是自己實現了負載,且存在此類問題;

結論:

知道這個問題的原因后,可以知道之前的修改batchSize也是行不通的,之所以修改后避免了問題,只是因為batchsize修改的足夠大,避免了多次獲取游標;

那么我們可以得到解決方案,將Mongo的配置改為真實的mongo機器IP,以 IP1:PORT1,IP2:PORT2,IP3:PORT3的形式傳給驅動,由驅動自動完成負載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM