昨天在測試數據導出的時候發現,若連續導出多次,則會報如下異常:
com.mongodb.MongoCursorNotFoundException: Query failed with error code -5
異常信息為Mongo查詢的游標找不到導致查詢失敗;
網上的解決辦法大多包含如下幾種:
- noCursorTimeout 設置cursor無超時時間
此種操作查詢完成后需要手動清理cursor,若因為異常或網絡則會導致游標一直存在,所以不推薦此方法
- 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的形式傳給驅動,由驅動自動完成負載。