Posted at

MongoDBのフェイルオーバー時のNode.jsのプログラム制御と動作確認

More than 1 year has passed since last update.

MongoDBを本番運用で利用する機会があったので、

レプリケーション及びフェイルオーバーの仕組みを導入しておきたく、そのときに実施した対応のメモ。

前回の記事では、MongoDBのレプリケーションとフェイルオーバーの設定について行ったため、

今回は主にプログラム側の処理や挙動の確認を実施する。


概要

前回同様に改めて検証する内容を確認。


  1. MongoDBのレプリケーション及びフェイルオーバーの設定

  2. MongoDBのフェイルオーバーの動作確認

  3. フェイルオーバー時のプログラム側の処理や設定

  4. コネクション数の管理

1と2にあたるMongoDB周りの設定や動きについては、前回の記事を参考

今回は3と4のプログラム側の処理や挙動の確認を実施する。


環境

利用ツール
バージョン

Node.js
v6.11.3

Express
v4.13.4

mongoose
v4.11.7


設定

まず必要なパッケージをpackage.jsonに記載。

今回MongoDBへの接続はmongooseを利用します。


package.json

  "dependencies": {

"body-parser": "^1.15.2",
"config": "^1.21.0",
"js-yaml": "^3.6.1",
"cors": "^2.7.1",
"express": "^4.13.4",
"mongodb": "^2.2.31",
"mongoose": "^4.11.7"
}

次にNode.jsのプログラムからDBへ接続するプログラムを記載

var mongoose = require('mongoose');

mongoose.Promise = global.Promise;

// 複数ある場合はhost:portを,区切りで記載する
var url = 'mongodb://localhost.localdomain:50000,localhost.localdomain:50001/sampledb?replicaSet=LocalRep';

// 任意で自由に設定
var options = {
server: {
  poolSize: 10, autoReconnect: true, monitoring: true, reconnectTries: 10, reconnectInterval: 10
},
replset:{
poolSize: 10, autoReconnect: true, monitoring: true, reconnectTries: 10, reconnectInterval: 10
}
};

// 接続
var db = mongoose.createConnection(url, options);

上記のようにurlにて、host:portを,区切りで記載するだけでOK。

(検証時にmongooseのバージョンによっては複数接続できなかったが、どのバージョンだったか忘れたのでうまくいったバージョン(v4.11.7)で記載しています)


フェイルオーバー時のプログラム側の挙動の確認

前回の記事で構築したサーバを元に接続の切り替えがうまくいくのかを検証する。

DB名
ポート
役割
備考

DB01
50000
Primary
レプリケーションの親 

DB02
50001
Secondary
レプリケーションの子 

DB03
50002
Arbiter
データは保持せず、Primaryへの昇格投票のみを行う

3台のDBをローカル環境で起動させ、DB01とDB02に対して接続を実施。

ユーザの検索とログインログを残すというシンプルなAPIを1本用意し、

参照と書き込みが正常に動作するのかを検証。


DBの起動と設定

# DB01の起動

mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork

# DB02の起動
mongod --port=50001 --dbpath=/var/lib/mongodb/db02 --logpath=/var/log/mongodb/db02.log --replSet=LocalRep --fork

# DB03の起動
mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork

もし起動コマンドを実行した際に下記のようなエラーが出た場合...

$ mongod --port=50000 --dbpath=/root/mongod/db01 --logpath=/root/mongod/logs/db01.log --replSet=LocalRep --fork

about to fork child process, waiting until server is ready for connections.
forked process: 11203
ERROR: child process failed, exited with error number 1

「mongod.lock」というファイルが存在しているため起動できない可能性が高い。

そのため、dbpathで指定しているフォルダ以下に「mongod.lock」ファイルが存在していれば

削除してから再度起動コマンドを実行すると起動することが出来る。

DB01にログインし、プログラムで利用する「sampledb」を作成し、

「users」と「login.log」コレクションを作成する。

$  mongo --port 50000

LocalRep:PRIMARY> use sampledb;
LocalRep:PRIMARY> db.createCollection('users')
{ "ok" : 1 }
LocalRep:PRIMARY> db.createCollection('login.log')
{ "ok" : 1 }

テストデータの作成

LocalRep:PRIMARY> db.users.insert({loginCode:'12345', userName:'sample'})

WriteResult({ "nInserted" : 1 })


プログラム側の処理


  1. ユーザを検索(loginCode:12345のユーザを検索)

  2. 存在していればログインログを残す

 ※ 検証時の全体のプログラムはGitHubを参照


  • MongoDBのコレクションスキーマの定義


src/models/schema/users.js

'use strict';

var userSchema = {
loginCode:{type:String},
userName:{type:String}
};
module.exports = userSchema;


src/models/schema/loginLog.js

'use strict';

var mongoose = require('mongoose');
var LoginLogSchema = {
userSchemaId: mongoose.Schema.Types.ObjectId,
loginDate:{type:Date},
};
module.exports = LoginLogSchema;

上記Schemaを「src/models/index.js」のベースとなるModelsの中で読み込むことで

DBへの接続管理や各Actionから呼び出しやすいように設定


src/models/index.js

  // 一部抜粋) Schemaの定義を呼び出す処理

setModels: function() {
var self = this;
Config.models.forEach(function(config){
if (!self.models[config.model]) {
var Schema = require('./schema/' + config.fileName);
self.models[config.model] = self.createModel(config.collection, new self.mongoose.Schema(Schema, {collection: config.collection}));
}
});
},
createModel: function(collectionName, Scheme) {
return this.db.model(collectionName, Scheme);
},


  • ロジック部分の処理抜粋

// Modelを定義(詳しくはGitHubのコードを参考)

const BaseModel = require(path + '/src/models/');
const UserModel = BaseModel.models.UserModel;
const LoginLogModel = BaseModel.models.LoginLogModel;

return new Promise((resolve, reject) => {
// ユーザを検索
UserModel.findOne({loginCode: '12345'}, function(error, res){
if (error) {
return reject(error);
}
return resolve(res);
});
}).then(function(user) {
    // ユーザが存在していればログインログを残す
if (user) {
var loginLog = new LoginLogModel({userSchemaId: user['_id'], loginDate: new Date()});
return new Promise((resolve, reject) => {
loginLog.save(function(error, res){
if (error) {
return reject(error);
}
return resolve(res);
})
});
}
});

上記処理は「/api/v1/login」というAPIに設定したため、

http://localhost:8080/api/v1/login 」 とローカル環境でアクセス

$ curl http://localhost:8080/api/v1/login

{"statusCode":0,"message":"Success."}

データベースにログが入っていることも確認!

$ mongo --port 50000

LocalRep:PRIMARY> use sampledb;
LocalRep:PRIMARY> db.users.find()
{ "_id" : ObjectId("59bb33b17748fa4368bf8874"), "loginCode" : "12345", "userName" : "sample" }
LocalRep:PRIMARY> db.login.log.find()
{ "_id" : ObjectId("59bb34d1f969de2d2a4ae19c"), "userSchemaId" : ObjectId("59bb33b17748fa4368bf8874"), "loginDate" : ISODate("2017-09-15T02:02:57.803Z"), "__v" : 0 }


DB01を落としてみる


  • 落とすコマンド

sudo ps aux | grep db01 | grep -v 'grep db01' | awk '{print $2}' | xargs kill -9


  • プロセス確認

$ ps aux | grep mongo

root 11292 1.8 14.8 1558696 93952 ? Sl 02:42 0:39 mongod --port=50001 --dbpath=/var/lib/mongodb/db02 --logpath=/var/log/mongodb/db02.log --replSet=LocalRep --fork
root 11370 1.5 7.9 1078072 49952 ? Sl 02:42 0:34 mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork
root 11605 0.0 0.1 103304 884 pts/1 R+ 03:19 0:00 grep mongo


  • curlコマンド実行

$ curl http://localhost:8080/api/v1/login

{"statusCode":0,"message":"Success."}

コネクションエラーも起きずに実行可能!


  • DBの中身確認

# DB01は落ちているのでつながらないことを確認

$ mongo --port 50000
exception: connect failed

# DB02にアクセス
$ mongo --port 50001
LocalRep:PRIMARY> use sampledb;

# 最新のログインログを1件を取得する
LocalRep:PRIMARY> db.login.log.find().sort({'loginDate':-1}).limit(1)
{ "_id" : ObjectId("59bb34d1f969de2d2a4ae19c"), "userSchemaId" : ObjectId("59bb33b17748fa4368bf8874"), "loginDate" : ISODate("2017-09-15T02:02:57.803Z"), "__v" : 0 }

うん、正常にDB02に書き込みができている模様!


DB01を復活


  • DB01のプロセスを起動

$ mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork


  • プロセス確認

$ ps aux | grep mongo

root 11292 1.8 14.8 1594552 93660 ? Sl 02:42 0:45 mongod --port=50001 --dbpath=/var/lib/mongodb/db02 --logpath=/var/log/mongodb/db02.log --replSet=LocalRep --fork
root 11370 1.6 8.2 1078072 52004 ? Sl 02:42 0:39 mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork
root 11616 3.0 7.6 1590748 48012 ? Sl 03:23 0:00 mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork
root 11706 0.0 0.1 103304 888 pts/1 R+ 03:23 0:00 grep mongo


  • curlコマンド実行

$ curl http://localhost:8080/api/v1/login

{"statusCode":0,"message":"Success."}

こちらもエラーなく実行可能


  • DBの中身確認

# とりあえずDB02に接続

$ mongo --port 50001
LocalRep:SECONDARY>
## DB01が起動したのでPRIMARYからSECONDARYに変わっている

# DB01に接続
$ mongo --port 50000
LocalRep:PRIMARY> use sampledb;

# 最新のログインログを1件を取得する
LocalRep:PRIMARY> db.login.log.find().sort({'loginDate':-1}).limit(1);
{ "_id" : ObjectId("59bb39c0f969de2d2a4ae19f"), "userSchemaId" : ObjectId("59bb33b17748fa4368bf8874"), "loginDate" : ISODate("2017-09-15T02:24:00.067Z"), "__v" : 0 }

うん、正しくデータが入っている!


DB02を落としてみる

ついでにDB02を落としたときの影響もチェック


  • DB02落とすコマンド

sudo ps aux | grep db02 | grep -v 'grep db02' | awk '{print $2}' | xargs kill -9


  • プロセス確認

$ ps aux | grep mongo

root 11616 1.9 8.6 1591776 54304 ? Sl 03:23 0:04 mongod --port=50000 --dbpath=/var/lib/mongodb/db01 --logpath=/var/log/mongodb/db01.log --replSet=LocalRep --fork
root 11370 1.6 8.2 1078072 52004 ? Sl 02:42 0:42 mongod --port=50002 --dbpath=/var/lib/mongodb/db03 --logpath=/var/log/mongodb/db03.log --replSet=LocalRep --fork
root 11726 0.0 0.1 103304 876 pts/1 R+ 03:26 0:00 grep mongo


  • curlコマンド実行

$ curl http://localhost:8080/api/v1/login

{"statusCode":0,"message":"Success."}

こちらもエラーなく実行可能


  • DBの中身確認

# とりあえずDB02に接続し、落ちていることを確認

$ mongo --port 50001
exception: connect failed

# DB01に接続
$ mongo --port 50000
LocalRep:PRIMARY> use sampledb;

# 最新のログインログを1件を取得する
LocalRep:PRIMARY> db.login.log.find().sort({'loginDate':-1}).limit(1);
{ "_id" : ObjectId("59bb3aeaf969de2d2a4ae1a0"), "userSchemaId" : ObjectId("59bb33b17748fa4368bf8874"), "loginDate" : ISODate("2017-09-15T02:28:58.602Z"), "__v" : 0 }

うん、うまくいっていそう!

どちらのDBが落ちていてもプログラムとしては正常に動作していることを確認。


コネクション周りについて

あまり詳しくない状況で利用しているのですが、実際に運用時にコネクションが無限に増え続けて

プログラムの処理としては終わっているのにコネクションが切断されず無限に増えていく問題が起きていた。

(結果的にDBが落ちる問題まで発生...)

原因としてはModelクラスのconstructorの中でDBへの接続を行っており、

シングルトンでの管理をしていなかったので、

Modelがインスタンス化されるたびにコネクションが張られ、

しかも切断する処理もないので最悪の状況でした...

// 以下実装イメージ

class BaseModel {
constructor() {
// ここでDBへのコネクションを作成
this.db = xxxxxx;
}
}

class UserModel extends BaseModel {
get() {
// ここでDBからデータを取得
return this.db.model.find(xxxxxx);
}
}

みたいなイメージです。ん、ダメダメですね...

今回アクションの中で、シングルトンでDBのコネクションを管理することで、

Nodeのプロセスが立ち上がっているときはコネクションを張り続けるような挙動で実装しているが、果たしてこれが良いのかどうか...

コネクション周りの管理などまだまだよくわかっておらず、とりあえず動いているという状況...

んーなんとも

とりあえず、検証に利用したサンプルアプリケーションは下記にアップしました。

GitHub: megadreams14/mongoDB_failover_test


まとめ

今回はNode.js/Express構成にてMongoDBのフェイルオーバーについて簡単に検証してみました。

設定やプログラム自体はやっているときはいくつかハマリポイントもありましたが、簡単に接続することは可能でしたが、

実際にプログラム側がPRIMARY/SECONDARYの判定をどのようにやっているのかや

参照系のDBと書き込み系のDBを分けて扱う際にコネクションをどう管理するのか、その時のフェイルオーバーをどのように対応するのかなど

まだまだわかっていないことは多いので引き続き勉強できればと...