エラーの概要
MongoDB クラスターに対して insert、update、または delete を実行すると、次のエラーが発生します:
MongoServerError: not primary
書き込みが突然拒否されます。ほとんどの場合、アプリがプライマリではなくセカンダリノードと通信しています。もう一つのケースは、プライマリが降格したばかりで選出がまだ完了していない場合です。
原因
レプリカセットでは、プライマリのみが書き込みを受け付けます。セカンダリは読み取り専用です。not primary エラーはいくつかの特定の状況で発生します:
- 接続文字列がレプリカセット URI ではなく、セカンダリの IP/ポートを直接指定している。
- フェイルオーバーが発生し、古いプライマリが降格したが、新しいプライマリがまだ選出されていない。
- 読み取り設定が
secondaryまたはsecondaryPreferredに設定されているが、書き込み操作が同じ接続を使用している。 - メンテナンス中に隠しセカンダリまたは遅延セカンダリが一時的に昇格した。
- DNS またはロードバランサーがトラフィックを誤ったノードに転送している。
ステップ 1 — レプリカセットのステータスを確認する
任意のノードに接続して実行します:
rs.status()
members 配列の各ノードの stateStr フィールドを確認します:
{
"members" : [
{ "name" : "mongo1:27017", "stateStr" : "PRIMARY" },
{ "name" : "mongo2:27017", "stateStr" : "SECONDARY" },
{ "name" : "mongo3:27017", "stateStr" : "SECONDARY" }
]
}
すべてのノードが SECONDARY になっていますか?または RECOVERING でスタックしているものがありますか?レプリカセットには現在プライマリがありません。選出中は正常な状態です — 10〜30 秒待ってから再度確認してください。
実際に接続しているノードを確認するには:
db.isMaster()
// または新しい同等のコマンド:
db.hello()
ismaster(または isWritablePrimary)が false を返す場合、セカンダリに接続しています。これが問題の原因です。
修正 1 — レプリカセット接続文字列を使用する
最も一般的な原因です。単一ノードへの接続では、ドライバーはレプリカセットのトポロジーを認識できません:
# 誤り — 単一ノード、レプリカセット未認識
mongodb://mongo1:27017/mydb
replicaSet パラメーターを含む全メンバーのフル URI に切り替えます:
# 正しい — ドライバーが自動的にプライマリを検出
mongodb://mongo1:27017,mongo2:27017,mongo3:27017/mydb?replicaSet=rs0
これでドライバーがフェイルオーバーを自動的に処理します。プライマリが変わっても、新しいプライマリを自動的に再検出します。何も変更する必要はありません。
Atlas の場合は SRV 形式を使用します — レプリカセット情報がすでに含まれています:
mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/mydb
修正 2 — Node.js / Mongoose 接続の更新
// 修正前(問題あり)
const uri = 'mongodb://192.168.1.10:27017/mydb';
// 修正後(レプリカセット対応)
const uri = 'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/mydb?replicaSet=rs0';
await mongoose.connect(uri, {
serverSelectionTimeoutMS: 5000,
heartbeatFrequencyMS: 10000,
});
修正 3 — Python / PyMongo 接続の更新
from pymongo import MongoClient
# 修正前(問題あり)
client = MongoClient('mongodb://192.168.1.10:27017/')
# 修正後(レプリカセット対応)
client = MongoClient(
'mongodb://192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/',
replicaSet='rs0',
serverSelectionTimeoutMS=5000
)
# プライマリに接続していることを確認
print(client.primary) # ('192.168.1.10', 27017) と表示されるはず
修正 4 — フェイルオーバー中(一時的な状態)
クラスターがプライマリを失った場合(クラッシュ、ネットワーク分断、計画メンテナンスなど)、選出には時間がかかります。リアルタイムで監視する方法:
# mongosh からループで実行する
while true; do
mongosh --quiet --eval "rs.status().members.forEach(m => print(m.name, m.stateStr))"
sleep 3
done
通常の状態では、選出は 10 秒以内に完了します。長引く場合は、以下の 3 点を確認してください:
- ノードが少なくとも 3 台ありますか?プライマリを選出するには過半数の投票が必要です。
- すべてのノード間のネットワーク接続は正常ですか?
RECOVERING状態でスタックしているノードはありますか?
手動で新しい選出を開始する必要がある場合は、現在のプライマリに接続して実行します:
rs.stepDown() // 現在のプライマリを降格させ、新しい選出をトリガーする
修正 5 — writeConcern 設定を確認する
見落としやすいポイント:書き込み操作に secondary の readPreference を設定しても意味がなく、このエラーの原因になり得ます。書き込み呼び出しを再確認してください:
// 誤り — 書き込み操作にセカンダリの読み取り設定を使用
db.collection('orders').insertOne(
{ item: 'widget' },
{ readPreference: 'secondary' } // 書き込みには適用されない
);
// 正しい — 読み取り設定とは別に書き込み懸念を設定
db.collection('orders').insertOne(
{ item: 'widget' },
{ writeConcern: { w: 'majority', wtimeout: 5000 } }
);
修正の確認
接続文字列を更新した後、mongosh から簡単なスモークテストを実行します:
use mydb
db.test.insertOne({ ping: new Date() })
// 返答例: { acknowledged: true, insertedId: ... }
MongoServerError: not primary エラーが表示されなくなるはずです。ドライバーがプライマリに接続していることを確認するには:
# Node.js — トポロジーをログに記録
mongoose.connection.on('connected', () => {
console.log('接続先:', mongoose.connection.host);
});
予防策
- **本番環境では必ずレプリカセット接続文字列を使用する。**単一ノードの IP をハードコーディングするのは時限爆弾です — フェイルオーバーはいつか必ず発生し、たいてい深夜3時に起きます。
- タイムアウトを厳しく設定する。
serverSelectionTimeoutMSとconnectTimeoutMSを設定して、選出中にハングする代わりに素早く失敗してリトライするようにします。 - **再試行可能な書き込みを有効にする。**接続文字列に
retryWrites=trueを追加します(最新のドライバーではデフォルト)。一時的なプライマリの変更がアプリケーションに見えなくなります。 - **レプリカセットの健全性を積極的に監視する。**メンバーが
RECOVERINGまたはUNKNOWN状態になったらアラートを出す — ユーザーが書き込みエラーを報告するまで待たないでください。 - **読み取りクライアントと書き込みクライアントを分離する。**負荷分散のためにセカンダリから読み取る場合は、
readPreference: 'secondaryPreferred'を持つ専用クライアントを使用します。書き込みクライアントはプライマリのみをターゲットにします。
# 本番環境対応の完全な接続文字列
mongodb://mongo1:27017,mongo2:27017,mongo3:27017/mydb
?replicaSet=rs0
&retryWrites=true
&w=majority
&readPreference=primaryPreferred
&serverSelectionTimeoutMS=5000

