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

MongoDBを本番運用で利用する機会があったので、
レプリケーション及びフェイルオーバーの仕組みを導入しておきたく、そのときに実施した対応のメモ。

前回の記事では、MongoDBのレプリケーションとフェイルオーバーの設定について行ったため、
今回は主にプログラム側の処理や挙動の確認を実施する。


# 概要

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

1. MongoDBのレプリケーション及びフェイルオーバーの設定
2. MongoDBのフェイルオーバーの動作確認
3. フェイルオーバー時のプログラム側の処理や設定
4. コネクション数の管理

1と2にあたるMongoDB周りの設定や動きについては、[前回の記事](http://qiita.com/megadreams14/items/c6a54ba5eca08cd052cb)を参考
今回は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)で記載しています）

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

[前回の記事](http://qiita.com/megadreams14/items/c6a54ba5eca08cd052cb)で構築したサーバを元に接続の切り替えがうまくいくのかを検証する。


| 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](https://github.com/megadreams14/mongoDB_failover_test)を参照


- 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);
  },
```

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

```javascript
// 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](https://github.com/megadreams14/mongoDB_failover_test)



# まとめ
今回はNode.js/Express構成にてMongoDBのフェイルオーバーについて簡単に検証してみました。
設定やプログラム自体はやっているときはいくつかハマリポイントもありましたが、簡単に接続することは可能でしたが、
実際にプログラム側がPRIMARY/SECONDARYの判定をどのようにやっているのかや
参照系のDBと書き込み系のDBを分けて扱う際にコネクションをどう管理するのか、その時のフェイルオーバーをどのように対応するのかなど
まだまだわかっていないことは多いので引き続き勉強できればと...