読者です 読者をやめる 読者になる 読者になる

mongoDBのcount正しくない問題

mongoDBでは、シャーディングされたコレクションのcountが正しい値を返さないことがあります。これには2つの原因があります。

  1. チャンク移動中である
  2. 孤児ドキュメントが存在する

1は、チャンク移動中は正しい値が帰らないということです。シャード間でデータが移動している最中なので正しい値が取れないのは直感的に理解できると思います。

2は、孤児ドキュメント(Orphaned Documents)がカウントされてしまう問題です。孤児ドキュメントとはチャンク移動したドキュメントが元のシャードから削除されず残ってしまったものです。シャードキーの範囲外なのでmongosからは到達不能になっていますが、countには計上されてしまいます。

孤児ドキュメントを解消するには、以下のコマンドを各シャードのreplica set primaryのmongod(mongosではありません)で実行します。これは重い処理なのでサービス稼働中に実行してはいけません。

use admin;
db.runCommand({ creanupOrphaned: "<database>.<collection>" });

集計関数を使うと、孤児ドキュメントを無視して正しい件数を得ることができます。ただし、これはかなり時間がかかるので注意してください。

db.collection.aggregate(
   [
      { $group: { _id: null, count: { $sum: 1 } } }
   ]
)

db.collection.count() — MongoDB Manual 3.4

我々の環境では、2系のmongoDBでdumpし、3系の新しいクラスタへrestoreした時に孤児ドキュメントが大量に発生しました。この孤児の発生は、dumpしたデータをrestoreし、データのチャンク移動が完了したのちにremove、再度restoreすることで回避できました。初回restore時には大量のチャンク移動が発生するため、孤児ドキュメントが発生しやすいものと思われます。

Node.jsの循環参照問題

モジュールの循環参照というNode.jsユーザの間ではよく知られている問題がある。

requireしたモジュールのメソッドやプロパティがundefinedになってしまうという問題だ。これは以下の条件で発生する

  1. モジュールaとモジュールbが互いにrequireしている
    • 間に他のモジュールを挟んでいても発生する、a → b → c → d → e→ aのようなrequireでもダメ
  2. module.exports を置き換えている
// a.js
const b = require('./b');

module.exports = {
    print() {
        console.log('a');
    },
    exec() {
        b.print();
    }
};

//  b.js
const a = require('./a');

module.exports = {
    print() {
        console.log('b');
    },
    exec() {
        a.print();
    }
};

// index.js
var a = require('./a');
a.exec();

var b = require('./b');
b.exec();

index.jsを実行すると・・・

$ node index.js
b
/Users/nullpon/b.js:10
        a.print();
          ^

TypeError: a.print is not a function
    at Object.exec (/Users/nullpon/b.js:10:11)
    at Object.<anonymous> (/Users/nullpon/index.js:5:3)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:990:3

1はモジュール設計の問題。モジュールが互いに依存してしまうのは設計が間違っている証拠である。

どうしても直せない場合はsetImmediateで逃げるという技がある。イベントループの同一のフェーズで相互参照するモジュールをrequireしなければ回避できる。

// a.js
const b = require('./b');

module.exports = {
    print() {
        console.log('a');
    },
    exec() {
        b.print();
    }
};

// b.js
let b;
setImmediate(() => {
  b = require('./b');
});

module.exports.print = function() {
    console.log('a');
};

module.exports.exec = function() {
    b.print();
};

// index.js
var a = require('./a');
a.exec();

var b = require('./b');
setImmediate(() => {  // 次tickに行かないとrequireが完了しない
  b.exec();
});

2に関してはexportsを置き換えるのではなく、exportsを拡張する形でモジュールを作成すると良い。サンプルのコードで言えば以下のようにすると良い。

// b.js
const b = require('./b');

module.exports.print = function() {
    console.log('a');
};

module.exports.exec = function() {
    b.print();
};

ただしclassを作る場合は大抵exportsを置き換える事になるので、これで回避できないことも多い。基本的には相互依存を回避するように設計すべき。