MongoDBの一般的なエラーと解決策
目次- MongoDBの一般的なエラー - MongoDB例外:MongoCursorNotFoundException - 背景: - 問題分析: - 解決策 - 解決策1: - 解決策2: - 解決策3: - 解決策4:
MongoDB例外:MongoCursorNotFoundException
エラーメッセージ:
com.mongodb.MongoCursorNotFoundException: クエリがエラーコード -5 で失敗しました
原因: com.mongodb.MongoCursorNotFoundException: エラーコード -5 とエラーメッセージ 'カーソル 400554224227 がサーバー 11.11.16.123:17000 上で見つかりません' によりクエリが失敗しました。サーバー 11.11.17.251:9000 上で発生
at com.mongodb.operation.QueryHelper.translateCommandException(QueryHelper.java:27)
at com.mongodb.operation.QueryBatchCursor.getMore(QueryBatchCursor.java:229)
at com.mongodb.operation.QueryBatchCursor.hasNext(QueryBatchCursor.java:115)
at com.mongodb.MongoBatchCursorAdapter.hasNext(MongoBatchCursorAdapter.java:46)
at com.alibaba.datax.plugin.reader.mongodbreader.MongoDBReader$Task.startRead(MongoDBReader.java:301)
at com.alibaba.datax.core.taskgroup.runner.ReaderRunner.run(ReaderRunner.java:57)
背景:
この問題に遭遇したのは、DataXを使用してデータを抽出する際にカスタムしたMongoDBリーダーコンポーネントで、億規模のデータを持つテーブルを読み込む際にエラーが発生しました。データ量が多く、データ構造が複雑であり、読み込むデータベースデータも多かったため、オンライン業務データベースからのデータ読み取り速度が制限されていました。さらに、当時ネットワークに不安定さがあったため、以前は正常に動作していたタスクがカーソルタイムアウトのエラーを報告しました。
問題分析:
db.collection.find() を使用する場合、実際にすべてのデータを返すわけではなく、カーソルを返します。そのデフォルトの動作は:最初にデータベースにクエリを送信する際に101個のドキュメント、または1MBのドキュメントを返します(どちらの条件が先に満たされるかによる)。その後、カーソル内のドキュメントが使い果たされるたびに、4MBのドキュメントをクエリします。さらに、find() のデフォルトの動作は、10分間操作がないとタイムアウトするカーソルを返すことです。あるバッチのドキュメントを10分以内に処理しきれなかった場合、後で処理が完了しても、同じカーソルIDを使用してサーバーから次のバッチを取得しようとすると、カーソルIDが既に期限切れになっているため、カーソルIDが無効であるというエラーが発生します。
解決策
解決策1:
MongoDBの設定を変更してカーソルのタイムアウト時間を延長し、MongoDBを再起動します。ただし、本番環境のMongoDBは簡単に再起動できないため、運営チームに連絡しても拒否される可能性があります。この解決策は有効ですが、運営チームに拒否された場合は、以下の解決策を検討してください。
解決策2:
データを一度にすべて読み込んでから処理を行います。以下に説明例を示します:
全データ = [行 for 行 in ハンドラー.find()]
for 行 in 全データ:
解析(行)
この解決策の欠点は明らかで、データ量が非常に大きい場合、すべてをメモリに格納できない可能性があります。億規模のデータ量の背景では、ほぼ不可能です。すべてをメモリに格納できたとしても、リスト内包表記ですべてのデータを反復処理し、その後forループで再度反復処理するため、時間の無駄になります。そのため、データ量が小さい場合を除いてはお勧めできません。データ量が小さい場合は、すべてをメモリに格納して同様の試みが可能です。
解決策3:
カーソルが返すデータを100件未満にすることで、このバッチデータの消費時間を10分未満にします:
# データベースに接続するたびに、50行のデータのみを返す
for 行 in ハンドラー.find().batch_size(100):
解析データ(行)
この解決策は比較的確実ですが、データベースの接続回数が増え、I/Oの待機時間が増加します。1億件のデータがある場合、各接続クエリのデータ量を100件に制限すると、このデータを読み取るために少なくとも10万回の接続を作成する必要があります。このようなコードが発見された場合、その結果は自分で想像してみてください。ただし、接続の作成回数が許容範囲内であれば、全く問題ありません。
解決策4:
カーソルがタイムアウトしないように設定します。パラメータno_cursor_timeout=Trueを設定することで、カーソルがタイムアウトしないようにします:
カーソル = ハンドラー.find(no_cursor_timeout=True)
for 行 in カーソル:
解析データ(行)
カーソル.close() # カーソルを手動で必ず閉じる
しかし、この操作は非常に危険です。ループ中に例外が発生する場合、あるいは停電やネットワーク断が発生すると、MongoDBサーバーのリソースが永久に解放されなくなります。このカーソルはもう閉じることができなくなります!MongoDBを再起動しない限り、これらのカーソルはMongoDB上に残り続け、リソースを占有します。
では、このリスクを回避する方法はあるのでしょうか?スレッドを作成する場合やSparkを使用する際には、close()やstop()のメソッドがあり、それらを必ず実行される構造内に配置することがよくあります。そのため、try-catch-finallyを使用することを考えました。
/* try catch:自分で例外を処理する
* try {
* 例外が発生する可能性のあるコード
*} catch(例外クラス名A e){
* 例外クラスA型の例外が発生した場合、このコードを実行
*} ...(catchは複数持てる)
* finally {
* 最終的には必ず実行されるコード(例:リソースを解放するコード)
*}
この方法はコードの美観性を損なう可能性がありますが、問題を完全に解決したことは間違いありません。