中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

MongoDBの統一トポロジー(useUnifiedTopology)について

最近のMongoDBドライバーはエラーが出る

(node:8746) [MONGODB DRIVER] Warning: Current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.

で、その新トポロジーの説明はこれ。

mongodb.github.io

すごくザックリ内容を紹介すると

  • 接続中(connected)という状態とはなんぞや?
  • 実際のネットワークが接続状態という事に意味はあるのか?
  • 別にになっていなくても処理可能な状態を保っていれば良いよね?
  • だからisConnectedも将来廃止するよ。
  • 直ちに処理出来ない状態でもドライバー内でなんとか辻褄取るよ
  • でも失敗したら30秒位でエラー返すよ

なので、useUnifiedTopologyautoReconnect を両方指定すると怒られる。

(node:8962) [MONGODB DRIVER] DeprecationWarning: The option `autoReconnect` is incompatible with the unified topology, please read more by visiting http://bit.ly/2D8WfT6

新しいトポロジーでは、利用者側は接続状態を気にしなくて良い事になっているからね。

ザックリ図解

現行ReplicaSet

f:id:hiroppon:20211124122931p:plain readPreferenceに合致するコネクションを割り当てる事しかしない。

一度コネクションを取得した後は素通し。

コネクションプール内のコネクションが全部エラーで破棄されるまでエラーが続く。

新統一トポロジー

f:id:hiroppon:20211124122936p:plain 1層咬んでいるのでエラーにならない所を勝手に探してくれる。

全部ダメならタイムアウトserverSelectionTimeoutMS までリトライする。

実際の挙動

createConnection

現行ReplicaSet

require('mongoose').createConnection('mongodb://localhost:27017/foo', {
  autoReconnect: true, 
  readPreference: 'secondaryPreferred'
})

直ちにエラーが帰る

MongoNetworkError: failed to connect to server [localhost:27017] on first connect [Error: connect ECONNREFUSED 127.0.0.1:27017
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1161:16)
    at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
  name: 'MongoNetworkError'
}]

新統一トポロジー

 require('mongoose').createConnection('mongodb://localhost:27017/foo', {
  useUnifiedTopology: true, 
  readPreference: 'secondaryPreferred',
  serverSelectionTimeoutMS: 30000,
})

約30秒後にこうなる

> Uncaught MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017
    at NativeConnection.Connection.openUri (/****/node_modules/mongoose/lib/connection.js:847:32)
    at Mongoose.createConnection (/****/node_modules/mongoose/lib/index.js:291:17) {
  reason: TopologyDescription {
    type: 'Unknown',
    setName: null,
    maxSetVersion: null,
    maxElectionId: null,
    servers: Map(1) { 'localhost:27017' => [ServerDescription] },
    stale: false,
    compatible: true,
    compatibilityError: null,
    logicalSessionTimeoutMinutes: null,
    heartbeatFrequencyMS: 10000,
    localThresholdMS: 15,
    commonWireVersion: null
  }
}

fetch

図解の通りに動くので、動作確認のコード例だけ。

このコードを動かしてレプリカセットのノードを落としたり上げたりすればいい。

新統一トポロジーの方は全ノードを落として、再起動せずにタイムアウトに抵触するまではエラーが出ない。

現行ReplicaSet

let conn = null;
require('mongoose').createConnection('mongodb://localhost:27018/foo', {
  replicaSet: 'RS',
  autoReconnect: true,
  readPreference: 'secondaryPreferred'}).then(r => conn=r).catch(r => console.log('---', r));
(async () => {
  for(let i = 0; i < 100000;i++) {
    try {
      let r = await conn.db.collection('tests').findOne({i: 1});
      console.log(i, r);
    } catch(e) {
      console.log(e);
    }
  }
})();

新統一トポロジー

let conn = null;
require('mongoose').createConnection('mongodb://localhost:27018/foo', {
  replicaSet: 'RS',
  useUnifiedTopology: true,
  readPreference: 'secondaryPreferred'}).then(r => conn=r).catch(r => console.log('---', r));
(async () => {
  for(let i = 0; i < 100000;i++) {
    try {
      let r = await conn.db.collection('tests').findOne({i: 1});
      console.log(i, r);
    } catch(e) {
      console.log(e);
    }
  }
})();

注意

良い改善だとは思うが、ハイロードな環境下では、30秒ものオペレーションを貯めたり再送して重複処理するのは危険極まりない

しっかり設計せずに移行するのは危険だ。