1つのシステムで2つのMongoDBを使ったらコネクションエラー、その対応策を考察

先日、1つのシステムで2つMongoDBを使ったら、PHPで使うと片方からrstパケットが50%ぐらいの確率で飛んでくるようになりました。

– MacOSX 10.8
– MongoDB 2.4.7
– PHPのMongoDBドライバ-> mongo 1.4.4 stable
です。現時点でMongoDBもドライバも最新、firewallも切っている、にも関わらずコネクションエラーが50%ぐらいの確率で起きるわけです。

RSTパケットが飛んでくる方の立ち上げ方は以下の様にしていました。

$ cat run
#!/bin/sh

./bin/mongod

$ cat run37017 
#!/bin/sh

./bin/mongod --port 37017 --dbpath ./data/37017/

いちおうdbpathは分離していたわけです。しかしながら後者、port 37017の方からRSTが飛んできたりしていました。

友人に、適当にググったりstackoverflowで対処療法を見つけるのではなく公式ドキュメントを読めと言われたので調べてみました。公式にはこのように書いてありました↓

Run Multiple Database Instances on the Same System

In many cases running multiple instances of mongod on a single system is not recommended. On some types of deployments [2] and for testing purposes you may need to run more than one mongod on a single system.

In these cases, use a base configuration for each instance, but consider the following configuration values:

dbpath = /srv/mongodb/db0/
pidfilepath = /srv/mongodb/db0.pid

The dbpath value controls the location of the mongod instance’s data directory. Ensure that each database has a distinct and well labeled data directory. The pidfilepath controls where mongod process places it’s process id file. As this tracks the specific mongod file, it is crucial that file be unique and well labeled to make it easy to start and stop these processes.

Create additional control scripts and/or adjust your existing MongoDB configuration and control script as needed to control these processes.


そもそもおすすめしない、と書いてありますので、よくなかったのかもしれません。。。
これを見るに、プロセスIDとログを指定しなかったのが悪かったのかなと感じました。

まだ検証していませんが、次回からはpidを使おうと思います。
-> 検証してみました↓

CPU/ネットワーク/DISK、どこがボトルネックになっているかに寄ると思いますが、たとえば1つのシステムにSSDを4つ挿して、1つのMongoDBの書き込み領域をそれぞれ1つのSSDに割り当てて、1つのシステムで4つのMongoDBにしたい場合があると思います。もちろんCPUとネットワークに余裕があるときにやらないとだめですけれども。

というわけで、データ、プロセスID、ログを明示的に指定して実行してみたところ、エラーが出なくなりました。私のやり方がまずかったわけです。

うまく行くconfファイルは以下のようになります。

$ cat 27017.conf 
fork = true
bind_ip = 127.0.0.1
port = 27017
quiet = false
dbpath = ./data/27017/
pidfilepath = ./piddir/27017.pid
logpath = ./log/27017.log
logappend = true
journal = true

$ cat 27018.conf 
fork = true
bind_ip = 127.0.0.1
port = 27018
quiet = false
dbpath = ./data/27018/
pidfilepath = ./piddir/27018.pid
logpath = ./log/27018.log
logappend = true
journal = true

そして、立ち上げ用スクリプトです。
$ cat run27017 
#!/bin/sh

./bin/mongod --config ./27017.conf

$ cat run27018
#!/bin/sh

./bin/mongod --config ./27018.conf

もちろん事前に、./data/27017/、./piddir/27017.pid、、./log/27017.logを作っておく必要があります(mkdirとtouchを使ってください)。

という訳で無事解決したので良かったです^^
MongoDBのせいにしてすみませんでしたorz馬鹿は私でした。

シェルスクリプトでのMacアドレスの認証

rsyncやrdiff-backupをリモート先に行いたいことがよくあると思います。一応認証しておきたいところだと思います。偶然パスワードなしで入れるところに繋がった場合(考えてみるとなさそうですが)。。。そこでMacアドレスを確認するスクリプトを書いてみました。

[Linux]ホームディレクトリをrsyncでバックアップする


↑こちらを参考にさせていただきました。

↓は、MACAddrがxx:xx:xx:xx:xx:xxとダミーですので書き換えてください。

#!/bin/sh

MACADDR=xx:xx:xx:xx:xx:xx

TO=`ssh root@192.168.0.144 ifconfig | grep HWaddr | awk '{print $NF}' 2>&1`

if [ "${TO}" = "${MACADDR}" ] ; then
  echo "\n=== AUTH OK. ===\n"
else
  echo "\n=== AUTH FAILED. EXIT. ===\n"
  exit 1
fi

AUTH OKのあとにしたい処理を書けば良いと思います。そしてcronに登録、と。

CentOSでのマウントのあれこれ

いつも忘れるので自分用に。。。

# partedでディスクを確認する
$ parted
(parted) print all

# ↓こういうのを見つける。。。どうやら/dev/mapper/vg-vol01が2TBのパーティションらしい、と。
-----
モデル: Linux device-mapper (linear) (dm)
ディスク /dev/mapper/vg-vol01: 2000GB
セクタサイズ (論理/物理): 512B/4096B
パーティションテーブル: loop

番号  開始   終了    サイズ  ファイルシステム  フラグ
 1    0.00B  2000GB  2000GB  ext4
-----

# マウントするためにディレクトリを作成する
$ mkdir /extdisk1

# マウントする
$ mount /dev/mapper/vg-vol01 /extdisk1/

こんな感じかな。。。でも/etc/fstabに追加したらおかしくなった。。。なんかCentOSの初期画面が出るようになった。。。どういうことですか。。。

reveal.jsでページ番号とfooterを加える

reveal.jsというイケてるHTMLスライドソフト(ソースデモ)があります。諸事情により、ページ番号とfooterを入れる必要がありました。

ページ番号は、

added page number feature


↑のmohikanerさんのソースを利用させてもらい、footerは自力でcssで頑張りました。

こんな感じになります↓

reveal.js.slide-number_footer

以下、ソースのHTMLです。実際に実行する際は、公式からreveal.jsを落としてきて、index.htmlと同じ階層のディレクトリにindex2.htmlとでもして、↓をコピペするといいと思います。ちなみに私はHTMLもCSSもJavaScriptもjQueryもぜんぜん分からないので、いろいろと間違った記法をしている可能性があります。

<!doctype html>
<html lang="en">
    <head>
	<meta charset="utf-8">
	<title>Page Number and Footer in reveal.js</title>
	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
	<link rel="stylesheet" href="css/reveal.min.css">
	<link rel="stylesheet" href="css/theme/default.css" id="theme">

	<!-- css for footer and page number -->
	<style type="text/css">
	    footer{
		clear:both;
		font-size:20px;
		color: red;
		height:75px;
		display:block;
		position:absolute;
		bottom:0;
		width: 100%;
	    }

	    footer div{
		margin:0 auto;
		text-align: center;
	    }

	    aside.slide-number{
		position:absolute;
		bottom: 25px;
		left: 25px;
		width: 100%;
	    }
	</style>
    </head>
    <body>
	<div class="reveal">
	    <div class="slides">
		<section>
		    <h2>Page Number and Footer in reveal.js</h2>
		    <p>Press right arrow key</p>
		</section>
	    </div>

	    <div class="slides">
		<section>
		    <h2>You can see the page number and footer. Yey!</h2>
		</section>
	    </div>

	    <div class="slides">
		<section>
		    <h2>This must be #2</h2>
		</section>
	    </div>
	</div>

	<footer>
	    <div>I am a footer!</div>
	</footer>

	<script src="lib/js/head.min.js"></script>
	<script src="js/reveal.js"></script>
	<script>
	    Reveal.initialize({
		controls: true,
		progress: true,
		history: true,
		center: true,
		theme: 'default',
		transition: Reveal.getQueryHash().transition || 'default',
		transitionSpeed: 'default'
	    });
	</script>

	<!-- Page number -->
	<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
	<script>
	    function currentPageFormatter(event) {
		var formattedStr;
		if (event.indexh === 0) {
		    return "";
		}

		formattedStr = event.indexh;

		if (event.indexv) {
		    formattedStr += "/" + event.indexv;
		}

		return formattedStr;
	    }

	    Reveal.addEventListener('ready', function(event) {
		$('<aside class="slide-number"></aside>')
			.text(currentPageFormatter(event))
			.appendTo('.reveal.center');
	    });

	    Reveal.addEventListener('slidechanged', function(event) {
		$(".slide-number").text(currentPageFormatter(event));
	    });
	</script>
    </body>
</html>

Redisのメモリ使用量(setについて、valueがstringとint、そしてttlを付加した場合)について

Redisに対してストアするとどのぐらいメモリを使うのか調べました。表題のとおりです。

環境
– Mac OSX 10.8
– Redis 2.6.16

その1 – setでvalueがstring

方法
ある長さのkeyとある長さのvalueを、ある回数だけsetして、そのときのメモリ使用量を調査しました。

結果

1000 keys (key-byte: 24, value-byte: 24)
Expectation : 46.9 [KB]
Redis Memory: 148.6 [KB]
(Expectationとは、素直に (keyとvalueの長さ) * 回数、を計算したものです)

結論
単純なsetですと、おおよそrawのbyte数の5倍程度使うようです。

String版
<?php

define("MAX", 1000);
define("REDIS_KEY_BYTE", 24);
define("REDIS_VALUE_BYTE", 24);

$redis = new Redis();
$redis->connect("localhost", 6379);

$redis->flushAll();

$before = getRedisMemory($redis);
for ($i = 0; $i < MAX; $i++) {
	$token = openssl_random_pseudo_bytes(REDIS_KEY_BYTE);
	$redis->set($token, openssl_random_pseudo_bytes(REDIS_VALUE_BYTE));
}
$after = getRedisMemory($redis);

printf("%d keys (key-byte: %d, value-byte: %d)\n", MAX, REDIS_KEY_BYTE, REDIS_VALUE_BYTE);
printf("Expectation : %.1f [KB]\n", MAX * (REDIS_KEY_BYTE + REDIS_VALUE_BYTE) / 1024.0);
printf("Redis Memory: %.1f [KB]\n", ($after - $before) / 1024.0);

function getRedisMemory(Redis $redis) {
	$res = $redis->info('MEMORY');
	return $res['used_memory'];
}


その2 – setでvalueがint

先程はvalueに24-byteのランダムな文字列を使いましたが、今度は21億ちょっとまでの整数値をvalueとしました。

結果
1000 keys (key-byte: 24, value-byte(int): 4)
Expectation : 27.3 [KB]
Redis Memory: 101.8 [KB]

結論
32bit Integer (signed)のつもりです。おおよそ2/3ぐらいに収まるかなと予想しましたがその通りの結果となりました。

考察
cookieのsessionを保存することを考えてみます。sessionの24byteとして、sessionにはサーバー側でuserid(32bit)が紐付けられているとします(ちょうどこの実測例です)。

さて、1GBのDRAMで何個のtokenを保存できるのでしょうか?

↑の結果より、1000個で約100KBですので、1万個で1MBです。その1000倍ですので、1GBで1000万個、と言えそうです。

今時であれば、サーバーに32GBや64GBのDRAMを詰むのは当たり前のことだと思います。ですのでRedisで16GB使うと考えると、1.6億個のtokenを管理できるのでかなり余裕がありますね。32GBマシン1個でtokenの処理は十分にこなせそうです。もちろん冗長化(redis-sentinelやslaveなど)は必要ですが。もちろん、メモリだけでなくネットワーク帯域などの問題もありますが。

scale-outではなくscale-upで対応できそうですね。

ちなみに、Redisのバックアッププロセスでのメモリ使用量を考えると、搭載しているDRAMの半分程度の運用が安全でしょう。その他のプロセスもメモリを使うので、もっと言えば1/3程度ですかね。

Integer版
<?php

define("MAX", 1000);
define("REDIS_KEY_BYTE", 24);
define("REDIS_VALUE_BYTE", 4); // 32bit Integer = 4-byte

$redis = new Redis();
$redis->connect("localhost", 6379);

$redis->flushAll();

$before = getRedisMemory($redis);
for ($i = 0; $i < MAX; $i++) {
	$token = openssl_random_pseudo_bytes(REDIS_KEY_BYTE);
	$redis->set($token, mt_rand(1, 2147483647)); // max signed_int = 2147483647
}
$after = getRedisMemory($redis);

printf("%d keys (key-byte: %d, value-byte(int): %d)\n", MAX, REDIS_KEY_BYTE, REDIS_VALUE_BYTE);
printf("Expectation : %.1f [KB]\n", MAX * (REDIS_KEY_BYTE + REDIS_VALUE_BYTE) / 1024.0);
printf("Redis Memory: %.1f [KB]\n", ($after - $before) / 1024.0);

function getRedisMemory(Redis $redis) {
	$res = $redis->info('MEMORY');
	return $res['used_memory'];
}


その3 – setでvalueがstringでttl付

さらにttlを付けて計測してみました。tokenの期限を半年と設定すると仮定してみます。半年、という時間は秒に直すと、60 [sec/min] * 60 [min/hour] * 24 [hour/day] * 365 / 2 = 15768000 [sec] ですね。

結果
1000 keys (key-byte: 24, value-byte(int): 4)
Expectation : 27.3 [KB]
Redis Memory: 141.0 [KB]

結論
先ほどの1.5倍程度となりました。とは言え1台のマシンで1つのシステムのtokenは何とかなりそうな感じですが。

TTL版
<?php

define("MAX", 1000);
define("REDIS_KEY_BYTE", 24);
define("REDIS_VALUE_BYTE", 4); // 32bit Integer = 4-byte
define("TTL", 15768000);

$redis = new Redis();
$redis->connect("localhost", 6379);

$redis->flushAll();

$before = getRedisMemory($redis);
for ($i = 0; $i < MAX; $i++) {
	$token = openssl_random_pseudo_bytes(REDIS_KEY_BYTE);
	$redis->setex($token, TTL, mt_rand(1, 2147483647)); // max signed_int = 2147483647
}
$after = getRedisMemory($redis);

printf("%d keys (key-byte: %d, value-byte(int): %d)\n", MAX, REDIS_KEY_BYTE, REDIS_VALUE_BYTE);
printf("Expectation : %.1f [KB]\n", MAX * (REDIS_KEY_BYTE + REDIS_VALUE_BYTE) / 1024.0);
printf("Redis Memory: %.1f [KB]\n", ($after - $before) / 1024.0);

function getRedisMemory(Redis $redis) {
	$res = $redis->info('MEMORY');
	return $res['used_memory'];
}