MongoDB Replica Setの構築と設定:本番環境仕様の自動フェイルオーバー

Database tutorial - IT technology blog
Database tutorial - IT technology blog

1. 苦い教訓:深夜に単一のMongoDBがダウンしたとき

午前3時、MongoDBサーバーのクラッシュによりアラートの通知音が鳴り響く。すべてのAPIが停止し、決済が不能になる。このような障害を一度でも経験したことがあれば、本番環境(Production)を単一のスタンドアロンノードに委ねることの恐ろしさを痛感しているはずです。

メモリの不良やディスクの容量枯渇(100%消費)だけでも、システム全体が停止する単一障害点(SPOF)になり得ます。唯一の解決策は、複数の独立したノードにデータを分散・複製することです。

本番環境における標準的な最小構成は3ノードです:

  • 1台のPrimaryノード:すべての書き込み(Write)処理を実行し、デフォルトでは読み取り(Read)処理も担います。データの変更履歴はすべて、oplog(Operations Log)と呼ばれる内部ログに順次記録されます。
  • 2台のSecondaryノード:Primaryからoplogを継続的に取得(Pull)し、自身のデータに適用します。Primaryが突然ダウンした場合でも、クラスタはわずか2〜12秒で最新データを持つSecondaryを選出してPrimaryに昇格させます。

なぜノード数は常に3、5、7といった奇数でなければならないのでしょうか?データセンター間のネットワーク分断(Network Partition)が発生した際、クラスタがPrimaryを選出するには過半数(n/2 + 1)の投票が必要です。もし2ノード構成の場合、接続が切断されると双方とも50%の票しか確保できません。その結果、どちらのノードもPrimaryへの昇格条件を満たせず、システム全体が停止してしまいます。

2. インフラの準備とインストール

今回のハンズオンでは、同一プライベートネットワークに属する3台のUbuntu 22.04 LTS VPS(推奨スペック:2 vCPU / 4GB RAM以上)を使用します:

  • node1.db.internal – 10.0.0.11(プライマリ候補ノード)
  • node2.db.internal – 10.0.0.12(セカンダリノード1)
  • node3.db.internal – 10.0.0.13(セカンダリノード2)

ステップ2.1:ホスト名解決の設定

データベースの設定にIPアドレスを直接ハードコードするのは避けましょう。ネットワークインフラの変更時にメンテナンスが容易になるよう、3台すべてのサーバーの/etc/hostsでホスト名をマッピングします:

sudo bash -c 'cat <<EOF >> /etc/hosts
10.0.0.11 node1.db.internal node1
10.0.0.12 node2.db.internal node2
10.0.0.13 node3.db.internal node3
EOF'

ステップ2.2:MongoDB 7.0のインストール

MongoDB公式リポジトリから最新パッケージを取得するため、3台のサーバーすべてで以下のコマンドを順に実行します:

# 公式GPGキーのインポート
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
   sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor

# Ubuntu 22.04 (Jammy) 用リポジトリの追加
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
   sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list

# アップデートとインストール
sudo apt update && sudo apt install -y mongodb-org

# サービスの自動起動を有効化
sudo systemctl enable mongod

3. Keyfileによる認証セキュリティの設定とクラスタの初期化

Replica Setをセキュリティ設定なしで稼働させることは絶対に避けてください。ノード間でデータを安全に同期するには、相互認証を行うための共通鍵(Keyfile)が不可欠です。

ステップ3.1:内部認証用Keyfileの生成

node1上で756バイトのランダム文字列を生成し、厳格なアクセス権限を設定します:

sudo mkdir -p /var/lib/mongodb-key
openssl rand -base64 756 | sudo tee /var/lib/mongodb-key/mongo-keyfile
sudo chown -R mongodb:mongodb /var/lib/mongodb-key
sudo chmod 400 /var/lib/mongodb-key/mongo-keyfile

権限400(mongodbユーザーのみ読み取り可能)の設定は必須です。777のように過剰な権限を与えている場合、mongodプロセスは起動を拒否します。

scpを使用して、このファイルをnode2とnode3へ転送します:

# node2およびnode3へコピー
scp /var/lib/mongodb-key/mongo-keyfile [email protected]:/tmp/
scp /var/lib/mongodb-key/mongo-keyfile [email protected]:/tmp/

転送後、node2とnode3の両方でファイルを所定のディレクトリへ移動し、適切な権限を再設定します:

sudo mkdir -p /var/lib/mongodb-key
sudo mv /tmp/mongo-keyfile /var/lib/mongodb-key/
sudo chown -R mongodb:mongodb /var/lib/mongodb-key
sudo chmod 400 /var/lib/mongodb-key/mongo-keyfile

ステップ3.2:mongod.confファイルの編集

各サーバーで/etc/mongod.confを開き、以下の3つの設定ブロックを修正します:

# /etc/mongod.conf

net:
  port: 27017
  bindIp: 127.0.0.1,node1.db.internal # 各ノードのホスト名に合わせて変更

security:
  keyFile: /var/lib/mongodb-key/mongo-keyfile
  authorization: enabled

replication:
  replSetName: "rs0"

ローカルのCLIツールが接続できるよう、bindIpには127.0.0.1を必ず残してください。修正完了後、3台すべてのノードでサービスを再起動します:

sudo systemctl restart mongod

ステップ3.3:クラスタの初期化とRootアカウントの作成

MongoDBのLocalhost Exception機能により、初期状態では認証なしでnode1のローカルから接続し、最初のユーザーを作成できます:

mongosh --port 27017

rs.initiate()コマンドを実行してクラスタを初期化します:

rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "node1.db.internal:27017", priority: 2 },
    { _id: 1, host: "node2.db.internal:27017", priority: 1 },
    { _id: 2, host: "node3.db.internal:27017", priority: 1 }
  ]
});

priority: 2の設定により、稼働中であればnode1が優先的にPrimaryとして選出されます。Enterキーを押して2〜3秒待つと、プロンプトがrs0 [direct: other] test>からrs0 [direct: primary] test>へと切り替わります。

このタイミングで、速やかに管理者アカウントを作成します:

use admin;
db.createUser({
  user: "siteRootAdmin",
  pwd: "MatKhauSieuKho123!@#",
  roles: [ { role: "root", db: "admin" } ]
});

これ以降、シェルを起動する際は常に認証ログインが必須となります:

mongosh -u siteRootAdmin -p --authenticationDatabase admin

ステップ3.4:テストデータの投入と同期の確認

ステージング環境の構築やパフォーマンステストの際、開発チームは顧客のExcel/CSVファイルから数万行規模のデータをインポートすることがよくあります。CSVからJSONへ手軽に変換したい場合は、toolcraft.app/ja/tools/data/csv-to-json などを活用できます。ブラウザ上のJavaScriptで直接パース処理が行われるため、機密データが外部ネットワークに漏洩する心配がありません。

JSONファイルが準備できたら、mongoimportを使用してanalyticsデータベースへ一括投入します:

mongoimport --uri="mongodb://siteRootAdmin:MatKhauSieuKho123!%40%[email protected]:27017/analytics?authSource=admin" \
  --collection=users --file=data.json --jsonArray

4. ヘルスチェックとフェイルオーバーの検証

ステップ4.1:レプリケーション遅延(Replication Lag)の確認

クラスタの状態を診断する最も基本的なコマンドはrs.status()ですが、各ノード間の遅延状況を手早く確認するには次のコマンドを実行します:

rs.printSecondaryReplicationInfo();

実行すると、SecondaryとPrimary間の遅延秒数が表示されます。この値が5〜10秒を超えている場合は、ネットワーク帯域の逼迫やストレージのI/Oボトルネック(IOPSの枯渇)を調査する必要があります。

また、rs.status()の出力に含まれるstateStrフィールドにも注目してください。ノードがSTARTUP2やRECOVERINGの状態で止まっている場合、初期データの同期中であり、まだ選出への参加やクエリの受付はできません。

ステップ4.2:Primary停止時のフェイルオーバー訓練

障害発生時にシステムが自動復旧するかどうかを確かめる唯一の方法は、実際にプライマリノードを停止させてみることです。

node1でMongoDBサービスを停止します:

# node1で実行
sudo systemctl stop mongod

直ちにnode2のターミナルを開き、シェルにログインします:

mongosh -u siteRootAdmin -p --authenticationDatabase admin

わずか2〜4秒ほどで、node2のプロンプトがrs0 [direct: primary] test>に切り替わります。リーダー選出は完全に自動で行われ、エンジニアの手動介入なしにアプリケーションからの書き込み処理が継続されます。

次に、node1を再起動します:

# node1で実行
sudo systemctl start mongod

起動直後、node1は自動的にSECONDARYとして復帰し、停止中に書き込まれた差分データを同期します。さらにpriority: 2が設定されているため、データの同期が完了するとnode1はnode2からの正常なステップダウン(Graceful Stepdown)を促し、再びPrimaryの役割を引き継ぎます。

ステップ4.3:アプリケーション向け接続文字列(Connection String)の適正化

開発者によくあるミスは、接続先として単一ノードのIPアドレスのみを指定してしまうことです。アプリケーションコード(Node.js、Go、Python、Spring Bootなど)で定義する接続文字列には、クラスタの全ノードと適切なパラメータを記述する必要があります:

mongodb://siteRootAdmin:[email protected]:27017,node2.db.internal:27017,node3.db.internal:27017/analytics?replicaSet=rs0&authSource=admin&w=majority&readPreference=primaryPreferred

必ず押さえておくべき2つの重要パラメータ:

  • w=majority:ドライバが書き込み成功のレスポンスを返す前に、過半数(3ノード中2ノード以上)がディスクへの書き込み完了を確認することを必須とします。これにより、Primaryが予期せず停止した場合のデータ損失を防ぎます。
  • readPreference=primaryPreferred:データの整合性を保つため原則としてPrimaryから読み取りますが、Primaryが一時的にダウンしている間は自動的にSecondaryへ読み取りクエリをルーティングし、ユーザーへのHTTP 500エラー返却を防ぎます。
Share: