シャーディング後にアップデートが失敗する理由スタンドアロンインスタンスからシャーディングされたクラスターに移行すると、データ操作のルールが変わります。最近、500GBのコレクションを3つのシャードで構成されるクラスターに移行するチームをサポートしました。長年完璧に動作していた彼らのアプリケーションコードが、突然次のエラーでクラッシュしました。
Illegal Operation: cannot perform a non-id update on a sharded collection without the shard key
これは、ルーティング層(mongos)が単一のデータバケットを参照しなくなるためです。_id または シャードキー が指定されていないと、mongos はどのシャードにドキュメントが保持されているか判断できません。それを見つけるために、MongoDBはクラスター内のすべてのシャードにクエリを投げる「スキャッター・ギャザー(scatter-gather)」と呼ばれる処理を実行し、その後に更新を試みる必要があります。単一ドキュメントの操作において、MongoDBはクラスターのパフォーマンスが急激に低下するのを防ぐために、この動作を明示的にブロックします。
「スキャッター・ギャザー」問題シャーディング環境では、データはシャードキーに基づいてチャンクに分割されます。updateOne()、replaceOne()、または findAndModify() を実行する場合、クラスターは明確なターゲットを必要とします。以下のいずれかを提供する必要があります。
_idフィールド:_idはグローバルに一意であるため、mongosは内部のメタデータを使用して正確なシャードを特定できます。- シャードキー: シャードキーを指定することで、ルーターは正しいパーティションに直接アクセスできます。フィルターでこの両方を省略し、かつマルチアップデート(複数一括更新)を行わない場合、操作は毎回失敗します。これはバグではなく、セーフガードです。
エラーの解決方法### 方法1:フィルターにシャードキーを含めるこれが最も一般的な修正方法です。たとえば、コレクションが tenant_id でシャーディングされているとします。一意の email フィールドでフィルタリングする場合でも、ルーターがどのシャードと通信すべきか判断できるように tenant_id を含める必要があります。
修正前のコード:
// シャードキーは { "region": 1 }
db.users.updateOne(
{ "email": "dev@example.com" },
{ $set: { "active": true } }
);
修正後のコード:
db.users.updateOne(
{
"email": "dev@example.com",
"region": "EMEA" // ここにシャードキーを追加
},
{ $set: { "active": true } }
);
方法2:_id でドキュメントを特定する_id による更新は、パフォーマンスにおける「黄金律」です。アプリケーションのロジックが許すのであれば、まずドキュメントの ID を取得してください。ターゲットを絞った _id による更新は通常 5ms 未満で完了しますが、ブロードキャスト検索はクラスターの規模によって 100ms 以上かかる場合があります。
db.users.updateOne(
{ "_id": ObjectId("654321098765432109876543") },
{ $set: { "active": true } }
);
方法3:updateMany に切り替える本当に複数のドキュメントを更新する必要がある場合、あるいはブロードキャストによるパフォーマンス低下を許容できる場合は、updateMany() を使用します。MongoDBは、操作がクラスター全体に及ぶことを意図していると見なすため、明示的に「マルチ」としてマークされた場合のブロードキャスト更新を許可しています。
db.users.updateMany(
{ "email": "dev@example.com" },
{ $set: { "status": "updated" } }
);
警告:この方法は慎重に使用してください。10個以上のシャードがあるクラスターで、シャードキーを指定せずに updateMany を頻繁に呼び出すと、ノード全体の CPU 使用率が大幅にスパイクする原因となります。
方法4:アップサート(Upsert)の処理アップサートは特に厳格です。upsert: true が設定されている場合、シャードキーを 必ず 含める必要があります。ドキュメントが存在しない場合、MongoDBは新しいレコードをどのシャードに配置するかを決定するためにそのキーを必要とするからです。
db.users.updateOne(
{ "username": "admin_user", "site_id": 42 }, // 'site_id' がシャードキー
{ $set: { "lastSeen": new Date() } },
{ upsert: true }
);
修正の確認ドライバから返される結果オブジェクトを確認してください。matchedCount が 1 になっているはずです。Mongo シェルでは、成功したターゲット更新は以下のようになります。
{
"acknowledged" : true,
"matchedCount" : 1,
"modifiedCount" : 1
}
もし matchedCount が 0 なのにドキュメントが存在することがわかっている場合は、フィルター内のシャードキーの値が正確かどうかを再確認してください。
シャーディング環境におけるプロのヒント本番環境の安定性を維持するために、以下の習慣を推奨します。
- キーの監査: 新しいクエリを作成する前に、
sh.status()またはdb.collection.getShardDistribution()を実行してください。シャードキーが何であるかを推測しないでください。- スキーマの強制: Mongoose や同様の ODM を使用している場合は、スキーマ内でシャードキーを定義してください。これにより、ライブラリが自動的にクエリにキーを含めるようになります。- ハッシュによるテスト: ハッシュ化されたシャードキーを使用するコレクションの場合、私はよく ToolCraft のハッシュジェネレーター を使用します。これは、開発段階でデータがどのようにチャンク間に分散されるかをテストするためのサンプル MD5 または SHA-256 文字列を生成するのに役立ちます。- 不足しているキーのログ出力: ログにこのエラーが表示される場合、フィルターに渡された変数がnullまたはundefinedであることがよくあります。データベースに到達する前に空のシャードキーをキャッチするためのバリデーション層を追加してください。更新が常にターゲットを絞ったものであることを確実にすることで、データベースの高速性を維持し、アプリケーションのレイテンシを低く抑えることができます。

