【MongoDB】レプリカセットの基本:MongoDBの可用性を確保する仕組み

MongoDB

こんにちは、かつコーチです。

本番運用のMongoDBで避けて通れないのがレプリカセット(同じデータを複数のサーバーに複製し、1台が落ちても自動的に処理を継続する仕組み)です。

単一サーバー運用のまま本番投入してしまい、サーバー障害でサービス全体が止まった、という話は決して珍しくありません。

この記事は前提知識ありきの上級者向け記事として、レプリカセットの仕組みから実務での運用上の注意点までを扱います。

MongoDB 7.x系を前提に進めます。

レプリカセットの基本構造

プライマリとセカンダリの役割

レプリカセットは、書き込みを受け付けるプライマリ(1台のみ)と、プライマリのデータを複製するセカンダリ(複数台)で構成されます。

// レプリカセットの状態を確認する
rs.status();

rs.status()を実行すると、各メンバーのstateStr"PRIMARY""SECONDARY"かを確認できます。

書き込みは基本的にプライマリにしか行えず、セカンダリへの書き込みを試みると次のようなエラーになります。

MongoServerError: not primary

自動フェイルオーバーの仕組み

プライマリがダウンすると、残ったメンバー間で選挙(election、新しいプライマリを自動的に選出するプロセス)が行われ、セカンダリの1台が新しいプライマリに昇格します。

この仕組みにより、単一障害点を作らずにサービスを継続できます。

// レプリカセットの初期設定例(3台構成)
rs.initiate({
  _id: "myReplicaSet",
  members: [
    { _id: 0, host: "mongo1.example.com:27017" },
    { _id: 1, host: "mongo2.example.com:27017" },
    { _id: 2, host: "mongo3.example.com:27017" }
  ]
});

メンバー数は奇数にするのが基本原則で、これは選挙の際に票が割れて多数決が成立しなくなる事態を避けるためです。

書き込み・読み取りの整合性設定

write concernによる書き込み保証レベル

write concern(書き込みがどこまで反映されたら成功とみなすかの設定)を調整することで、性能と安全性のバランスを取れます。

// 過半数のメンバーへの複製が完了するまで書き込み成功としない
db.orders.insertOne(
  { customerId: "customer-001", amount: 5000 },
  { writeConcern: { w: "majority" } }
);

w: "majority"は、プライマリだけでなく過半数のセカンダリへの複製完了を待つため、フェイルオーバーが起きてもデータが失われにくくなりますが、その分レイテンシは増えます。

read preferenceによる読み取り先の制御

読み取りをセカンダリに分散させることで、プライマリの負荷を下げられます。

// セカンダリからの読み取りを優先する設定
db.orders.find({ status: "shipped" }).readPref("secondaryPreferred");

ただし、セカンダリへの複製にはレプリケーションラグ(プライマリの更新がセカンダリに反映されるまでの遅延)が発生するため、「今書き込んだばかりのデータをすぐ読みたい」処理には不向きです。

よくあるつまずきポイント・エラー対処

レプリケーションラグに気づかずデータ不整合が発生

筆者が実際に、注文確定直後に確認画面へリダイレクトする処理で、セカンダリから読み取る設定にしていたために「注文したのに確認画面に表示されない」という不具合を経験したことがあります。

原因は、書き込み直後のわずかなレプリケーションラグの間に、セカンダリへの読み取りクエリが実行されていたことでした。

❌ Before(書き込み直後の読み取りをセカンダリ優先にしている)

// 注文を作成した直後、確認画面用にセカンダリから読み取ってしまう
db.orders.insertOne({ customerId: "customer-001", amount: 5000 });

db.orders.find({ customerId: "customer-001" })
  .readPref("secondaryPreferred")
  .sort({ _id: -1 })
  .limit(1);

✅ After(書き込み直後の一貫性が必要な処理はプライマリから読み取る)

db.orders.insertOne({ customerId: "customer-001", amount: 5000 });

// 直後の読み取りはプライマリ(既定のread preference)にする
db.orders.find({ customerId: "customer-001" })
  .sort({ _id: -1 })
  .limit(1);

この修正後、確認画面が表示されない不具合は再現しなくなりました。

「セカンダリからの読み取りは負荷分散に有効だが、書き込み直後の強い整合性が必要な場面には使わない」という線引きを、この経験から学びました。

応用・一歩先の使い方

アービターによる構成の工夫

サーバーを3台用意するコストが厳しい場合、データを持たずに投票権だけを持つアービター(arbiter)をメンバーに加える方法もあります。

// アービターを追加して奇数構成を保つ
rs.addArb("mongo-arbiter.example.com:27017");

ただし、アービターはデータを複製しないため、可用性への貢献は「選挙の票数を確保する」ことに限られる点は理解しておく必要があります。

本番運用では、コストが許す限りデータを持つメンバーを奇数台用意する構成の方が、可用性の観点では望ましいというのが実務での判断です。

まとめ

この記事のポイント

  • レプリカセットはプライマリ1台とセカンダリ複数台で構成され、障害時は自動フェイルオーバーする
  • メンバー数は奇数が原則。選挙の多数決が割れないようにするため
  • write concernread preferenceを用途に応じて調整し、性能と整合性のバランスを取る
  • 書き込み直後の強い整合性が必要な処理は、セカンダリではなくプライマリから読み取る

次に読むべき記事

  • スキーマ設計の考え方:埋め込みvs参照(MongoDB編8本目)
  • インデックスの基本とパフォーマンス(MongoDB編7本目)
  • MongoDBとPostgreSQL(JSONB)比較検証(MongoDB編最終回)

タグ: MongoDB, 上級者向け, 運用

タイトルとURLをコピーしました