MongoDB大規模データ抽出時のカーソルタイムアウトエラーと制御手法

エラー現象とスタックトレースの解読

ETLツールやカスタムスクリプトを用いてMongoDBから大規模データを取得する際、以下のような例外がスローされることがあります。

com.mongodb.MongoCursorNotFoundException: Query failed with error code -5
Caused by: com.mongodb.MongoCursorNotFoundException: Query failed with error code -5 and error message 'Cursor 400554224227 not found on server 10.0.1.50:27017'
    at com.mongodb.operation.QueryBatchCursor.getMore(QueryBatchCursor.java:229)
    at com.mongodb.operation.QueryBatchCursor.hasNext(QueryBatchCursor.java:115)
    ...

このエラーは、クライアント側が保持しているカーソル識別子がMongoDBサーバー上で既に無効化された状態で、次のデータバッチ要求(getMore)を発行した際に発生します。

根本原因のメカニズム

find()メソッドを実行すると、MongoDBは結果セットをメモリにすべて展開せず、サーバー側でカーソルインスタンスを生成して返却します。デフォルトのフェッチ動作は以下の通り設計されています。

  • 初回リクエスト: 最大101ドキュメントまたは1MB(先に到達した条件で停止)
  • 以降のフェッチ: クライアントがカーソルを進めるたび、最大4MB分のデータを取得
  • アイドル破棄ルール: クライアントから何らかの通信がない状態が10分継続すると、サーバーがメモリ節約のためカーソルを自動的にクローズ

本番環境におけるネットワークジッター、業務負荷によるクエリスロットリング、あるいは複雑なデータ構造によるパース処理の遅延などが重なることで、バッチ処理に10分以上要してしまい、既存のカーソルIDが期限切れとなるのが主たる原因です。

実装レベルでの対応戦略

戦略1:サーバー側のタイムアウト閾値調整

MongoDBのシステムパラメータを変更してカーソルの生存期間を延長する方法です。本番クラスターでは設定反映後にサービスリブートが必要となる場合が多く、運用制約から即座に採用できないケースが大半です。

戦略2:メモリ内への即時評価

イテレータを即座に消費してリストへ格納し、その後で後処理を行うパターンです。

target_collection = client.db.get_collection('sales_records')
loaded_dataset = list(target_collection.find({}))

for record_payload in loaded_dataset:
    run_transformation(record_payload)

数万レコード程度であれば簡潔に実装可能ですが、億規模のデータセットではヒープメモリ不足(OOM)を誘発します。また、リスト生成と処理ループで2回走査するため、全体のスループットが低下します。

戦略3:バッチ単位の取得制御

1回のフェッチで返却されるドキュメント数を意図的に絞り、各バッチの消費時間を10分以内に収める手法です。

page_size = 120
stream_iterator = target_collection.find({}).batch_size(page_size)

for record_payload in stream_iterator:
    run_transformation(record_payload)

メモリフットプリントを抑えつつタイムアウトを回避できる稳妥な方法です。ただし、総データ量に対してネットワーク往復回数が急増するため、I/O待機時間がボトルネック化する可能性があります。接続プールの許容範囲とデータ量を加味して導入判断を行う必要があります。

戦略4:非同期タイムアウトカーソルと確定終了処理

カーソルの自動破棄機能を無効化し、処理完了後に手動でリソースを解放するアプローチです。

persistent_stream = target_collection.find(no_cursor_timeout=True)

try:
    for record_payload in persistent_stream:
        run_transformation(record_payload)
finally:
    persistent_stream.close()

このパラメータを有効にすると、処理時間にかかわらずカーソルは維持され続けます。その代償として、予期せぬ例外発生やプロセス強制終了時にサーバー側のメモリとファイルディスクリソースがリークするリスクがあります。そのため、try...finally構文を必須とし、いかなる実行パスでもclose()が保証される実装に固着する必要があります。このパターンは、長時間ストリーム処理や分散データパイプラインとの連携において、最も堅牢なリソース管理手法となります。

タグ: MongoDB pymongo データ抽出 カーソル制御 エラーハンドリング

6月13日 22:26 投稿