DBのタイムアウトはサーバ側をクライアント側より長くする

多くのデータベースにはコネクションのタイムアウトという設定が存在します。指定した期間使われていないコネクションを解放するというもので、クライアント、サーバ双方で設定可能になっています。

タイムアウトはサーバ側の寿命がクライアント側よりも長くなるように設定すべきです。逆にしてしまうとサーバ側で切断されたコネクションに対してクライアントが読み書きを行おうとしてエラーになってしまうことがあります。

MySQLとnode.jsで再現すると以下のようになります。

$ node index.js
[ RowDataPacket { Variable_name: 'wait_timeout', Value: '28800' } ]
wait 11 seconds
[ RowDataPacket { '1': 1 } ]
end
$
$ mysql -u root -e "set global wait_timeout=10;"
$
$ node index.js
[ RowDataPacket { Variable_name: 'wait_timeout', Value: '10' } ]
wait 11 seconds
{ Error: Cannot enqueue Query after fatal error.
    at Protocol._validateEnqueue (/Users/nullpon/mysql-node/node_modules/mysql/lib/protocol/Protocol.js:201:16)
    at Protocol._enqueue (/Users/nullpon/mysql-node/node_modules/mysql/lib/protocol/Protocol.js:139:13)
    at PoolConnection.query (/Users/nullpon/mysql-node/node_modules/mysql/lib/Connection.js:208:25)
    at PoolConnection.query (internal/util.js:230:26)
    at __dirname (/Users/nullpon/mysql-node/index.js:21:59)
    at <anonymous> code: 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR', fatal: false }

index.jsのコードは以下になります

'use strict';
const {promisify} = require('util')
const mysql = require('mysql');

(async () => {
    var pool  = mysql.createPool({
        host     : 'localhost',
        user     : 'root',
        password : '',
        database : 'test',
        acquireTimeout: 60000 // timeout 60秒
    });
    const conn = await promisify(pool.getConnection).bind(pool)();

    const result1 = await promisify(conn.query).bind(conn)('show global variables like \'wait_timeout\'');
    console.log(result1);

    console.log('wait 11 seconds');
    await new Promise((r) => setTimeout(r, 11000))

    const result2 = await promisify(conn.query).bind(conn)('SELECT 1');
    console.log(result2);

    conn.release();

    await promisify(pool.end).bind(pool)();

})().then(() => {
    console.log('end');
}).catch((e) => {
    console.log(e);
});

Redisのようにデフォルトでサーバ側のタイムアウトが無限大となっているDBもあります。特にAWSのElastiCacheでありがちなのですが、アプリがOOMなどで終了したり、正常終了時でもコネクションを解放しない雑な実装だったりすると、サーバ側のコネクションは消えずに残ってしまいます。タイムアウトを無限大にしておくと、このようなゴミが永遠に解放されず少しずつ溜まっていき、いずれサーバのコネクション数の上限に達して障害の原因となります。

こういう事態を防ぐためにもサーバ側でもタイムアウトを設定した方が無難なのですが、クライアントよりも短い値を設定しないように注意してください。

mongoDBクラスタにrestoreする時の高速化テクニック

developers.cyberagent.co.jp

真新しいmongoDBクラスタにデータをrestoreする時に高速化テクニックとして、メンテナンス開始前にバックアップデータ等を一度restoreしてmongoDBクラスタを暖気しておく方法があります。

mongoDBの大規模なクラスタに巨大なデータをrestoreすると非常に時間がかかります。例えば800GBのdumpデータをrestoreすると丸一日以上かかってしまいますが、流石にメンテナンスで丸一日サービスを止めたくはないでしょう。

mongoDBはinsertしたデータを一旦プライマリシャードに保存し、データが増えると自動的に他のシャードにデータが分散されていくのですが、このチャンク移動の負荷がかなり大きいのです。

そこで事前にバックアップの本番データをdump、新クラスタにrestoreしてチャンク移動の完了を待ち、ドキュメントの保存先のシャードが決定された状態にします。この状態でデータをremove(dropするとシャーディングの情報も消えてしまうのでremove)してrestoreすると、プライマリシャードを経由せず、最初から適切なシャードに分散してinsertされます。これでチャンク移動の負荷がなくなるので restore の所要時間は1/3 〜 1/4程度に短縮されます。

本番稼働中のDBに対して新規コレクションを追加する時も、あらかじめダミーデータを少しずつ入れて暖気しておくと、新機能のリリース直後のDB負荷が下がるのでおすすめです。