ファイルベースの自作KVS

追記@2013年9月30日

この自作KVSを叩くようなAPIを作って、apache benchやwrkベンチをしてみたところ、ファイルの実体がおかしくなりエラーが延々と出るようになりました。。。同時並列で行うとまず間違いなくだめになります。いちおうロックしているはずなんですがね、、、どうしてでしょう。いやー難しいですね、DBは。まあこれはあくまで例として用いるKVSですのでショボくていいのです。


思うところあって、SQLiteのようなローカルのファイルを使うようなKey-Value Storeを作る必要性が出てきて、実際に作ってみました。

とは言えPHPのクラスとして動くものであり、シェルなどもなく、素朴なものです。そのうちCで書きなおしてシェルも作って各種クライアントを用意してGithubにでも公開して、Local KVSだ(ドヤッ とやりたいところですが他にやることが山ほどあるので一生できそうもありません。。。

基本的な方針は以下の通りです。
– ファイルベース。デーモンではなくファイルでやりとり。SQLiteみたいな感じ。
– 単純なkey-value。stringのみでintなどはない。arrayは受け入れない。どうしてもarrayなど使いたいときにはシリアライズしてinsert。
– keyは自動生成(uuid)。insert時はvalueだけで完結するような感じ。
– CRUD (Create/Read/Update/Delete)を備える。
– パフォーマンスは考慮しない。遅くてよい。
– どのファイルに保存するか選択できるようにする。
– JSONでシリアライズしてファイルに保存。編集するときはアンシリアライズして操作する。こうすることでdeleteが簡単。もちろんトロいけれど、パフォーマンスを求めているものではないので。JSONではなくMessagePackも良いが、パッケージを入れるのが面倒なのでひとまずデフォルトで入っているJSONに。

実際のコードと使い方と実行例は以下です。main.phpを見ればどうやって使うのか明らかだと思います。

注意:
PHP 5.3では動きません。もし使うのであればPHP 5.4以降を使ってください。

LocalKVS.php

<?php

/*
 * Local KVS
 *
 * [Available]
 * - Create (means Insert)
 * - Read
 * - Update
 * - Delete
 * - ReadAll
 * - DeleteAll
 *
 * [TODO]
 * - CreateMulti
 * - DeleteMulti
 * - ReadMulti
 */

class LocalKVS {

	private $path = './';
	private $filepath;

	public function __construct($filename = 'default.lkvs') {
		$this->filepath = $this->path . $filename;
		if (!file_exists($this->filepath)) {
			touch($this->filepath);
			chmod($this->filepath, 0666);
		}
	}

	/**
	 * Writing $value to the database. Return ID if success, FALSE if failed.
	 *
	 * e.x.
	 * <pre>
	 * $kvs = new LocalKVS();
	 * var_dump($kvs->create('This is example.'));
	 * </pre>
	 * @param type $value String. e.x. 'This is a value'
	 * @return string The function returns id or FALSE on failure.
	 */
	public function create($value) {
		$id = uniqid();
		$db = @file_get_contents($this->filepath);
		$doc = null;
		if (!$db) {
			$doc = array($id => $value);
		} else {
			$doc = array_merge(self::s2a($db), array($id => $value));
		}

		// return FALSE if writing failed
		if (!file_put_contents($this->filepath, self::a2s($doc))) {
			return FALSE;
		}

		return $id;
	}

	/**
	 *
	 * @param type $id
	 * @return boolean
	 */
	public function read($id) {
		$db = @file_get_contents($this->filepath);
		if (!$db) {
			return FALSE;
		}
		$doc = self::s2a($db);
		if (!array_key_exists($id, $doc)) {
			// return false if $id does not exist
			return FALSE;
		}
		return $doc[$id];
	}

	public function update($id, $value) {
		$db = @file_get_contents($this->filepath);
		if (!$db) {
			return FALSE;
		}
		$doc = self::s2a($db);
		if (!array_key_exists($id, $doc)) {
			// return false if $id does not exist
			return FALSE;
		}
		// update
		$doc[$id] = $value;

		// return FALSE if writing failed
		if (!file_put_contents($this->filepath, self::a2s($doc))) {
			return FALSE;
		}

		return $id;
	}

	public function delete($id) {
		$db = @file_get_contents($this->filepath);
		if (!$db) {
			return FALSE;
		}
		$doc = self::s2a($db);
		if (!array_key_exists($id, $doc)) {
			// return false if $id does not exist
			return FALSE;
		}
		// delete
		unset($doc[$id]);

		// if empty
		if (count($doc) === 0) {
			unlink($this->filepath);
		} else {
			// return FALSE if writing failed
			if (!file_put_contents($this->filepath, self::a2s($doc))) {
				return FALSE;
			}
		}

		return TRUE;
	}

	public function readAll() {
		$db = @file_get_contents($this->filepath);
		if (!$db) {
			return FALSE;
		}
		return self::s2a($db);
	}

	public function deleteAll() {
		$db = @file_get_contents($this->filepath);
		if (!$db) {
			return FALSE;
		}
		// delete a database file
		unlink($this->filepath);
		return TRUE;
	}

	/**
	 * Array to String
	 *
	 * @param array $arry
	 * @return type
	 */
	private static function a2s(Array $arry) {
		return json_encode($arry, JSON_UNESCAPED_UNICODE);
	}

	/**
	 * String to Array
	 *
	 * @param type $string
	 * @return type
	 */
	private static function s2a($string) {
		return json_decode($string, TRUE);
	}
}


main.php
<?php

require './LocalKVS.php';

// Create a instance
$kvs = new LocalKVS('./db.lkvs');

echo "\nWrite something:\n";
$id = $kvs->create('This is a test insert1.');
var_dump($id);
$id2 = $kvs->create('This is a test insert2.');
var_dump($id2);
$id3 = $kvs->create('This is a test insert3.');
var_dump($id3);

echo "\nRead:\n";
var_dump($kvs->read($id));
var_dump($kvs->read($id2));
var_dump($kvs->read($id3));

echo "\nUpdate:\n";
$result2 = $kvs->update($id2, 'test insert2 was updated!');
var_dump($result2);

echo "\nReadAll:\n";
$db = $kvs->readAll();
var_dump(json_encode($db));

echo "\nDelete:\n";
$id_to_delete = array_rand($db);
var_dump($kvs->delete($id_to_delete));

echo "\nReadAll:\n";
$db2 = $kvs->readAll();
var_dump(json_encode($db2));

echo "\nDeleteAll:\n";
var_dump($kvs->deleteAll());


実行例
Write something:
string(13) "5248ce753e566"
string(13) "5248ce753e85b"
string(13) "5248ce753eb3f"

Read:
string(23) "This is a test insert1."
string(23) "This is a test insert2."
string(23) "This is a test insert3."

Update:
string(13) "5248ce753e85b"

ReadAll:
string(129) "{"5248ce753e566":"This is a test insert1.","5248ce753e85b":"test insert2 was updated!","5248ce753eb3f":"This is a test insert3."}"

Delete:
bool(true)

ReadAll:
string(87) "{"5248ce753e566":"This is a test insert1.","5248ce753e85b":"test insert2 was updated!"}"

DeleteAll:
bool(true)


苦労した点:
– createでなぜかidが保存されないでvalueだけ保存されることが極稀にあった。現在ではなんとか起きないように出来た、はず。。。
– いちいちファイルを全部読み込むなんてあほじゃ!と思ってfopenやfgetsを使って書いてみたら、一行書き換えが出来そうもなくて詰む(ファイルシステムメソッドで行う必要があるらしい)
– エラー処理めんどう。1個だけ要素があるときにdeleteする場合とか。。。

PHPのdefineのコスト

追記@2013-09-24
defineではなくvariableで試してみましたが、defineとほぼ同じ結果が出ました。
要は行数が増えれば当然遅くなる、ということのようです。


PHPのdefineで定数をよく作りますがそのコストはどれほど、と思って計測してみました。
やり方としては、defineを1, 10, 100, 1k, 10k含むPHPコードに対して、ベンチマークをとって比較、です。

サーバー
– Opteron 3280
– DDR3-1333
– Ubuntu 12.04.3
– Apache 2

クライアント
– G530
– DDR3-1333
– Ubuntu 12.04.3
– wrk (ベンチマークツール。apache benchの代わり)

そしてサーバー、クライアント間はGbEスイッチングハブで繋いでいます。

結果 (apache2, mod_php) (左側の数字はdefineの数、右側はreq/sec)

# defineの数 req/sec
1   28679.09
10  25657.40
100 11788.40
1k  1530.04
10k 132.53

apache2, mod_php, APC有効
1   34124.95
10  31946.15
100 20356.26
1k  5025.39
10k 425.58

となりました。どんどん遅くなりますね。defineはO(N)とみて良さそうです。APCが有効な状態でもけっこう重いですね。ちなみに、APC有効のときにdstatで見てみるとCPUを使いきっていました。素の状態でも同じだと思います。100個を超えるdefineは危険かもしれません。

Nginx+PHP-FPMやその他キャッシュも試してみたいですね。

defineを含むPHPコードを生成するコードはこちら ($maxをいろいろと変更します)↓
<?php

printf("<?php\n\n");
$max = 10;
for ($i = 0; $i < $max; $i++) {
	printf("define(\"%s\", \"%s\");\n", 'key' . $i, 'value' . $i);
}

printf("echo \"define * %d\";\n", $max);

↑のスクリプトで出来上がるコード
<?php

define("key0", "value0");
define("key1", "value1");
define("key2", "value2");
define("key3", "value3");
define("key4", "value4");
define("key5", "value5");
define("key6", "value6");
define("key7", "value7");
define("key8", "value8");
define("key9", "value9");
echo "define * 10";

MongoIdのバイトサイズについて

Why is not the MongoId 12-byte long but the 24-byte?
>But actually it’s a 24-byte value like 4d7f4787ac6d604009000000

>That’s a hexidecimal value. One hex digit = 4 bits. 24 hex digits = 96 bits = 12 bytes.


↑いまいちこれが理解できませんでした。MongoIdは、12-byteとのことですが、何も考えずにDBに保存しようとするとStringにキャストして24-byteになってしまいます。
よく分からないので実験してみました。文字列形式にすると16進数表現とのことなので、hex2binしてみたものとの比較です。

<?php

$hex = (string) (new MongoId());
printf("MongoId (hex): %s, Length: %d\n", $hex, strlen($hex));
printf("MongoId (bin): %s, Length: %d\n", hex2bin($hex), strlen(hex2bin($hex)));

結果 (文字化けしています)
MongoId (hex): 523ccdfcba4ed42c1ff78592, Length: 24
MongoId (bin): R<��N�,���, Length: 12

という訳で、どうやら12-byteで表すことが出来ました。文字列だと思っていたら16進数表現だったでござるの巻、ということですね。これでDBに保存するときも12-byteで保存できて効率的ですね。

PHPにおけるプロセス生成コストの評価

本当に怖いパフォーマンスが悪い実装 #phpcon2013


↑これのスライド、p27~33 に、ホスト名取得はexecを使わないほうがよい、とあって、
execを使ったとき -> 2000 req/sec
execを使わないとき -> 14000+ req/sec
と書いてありました。

プロセス生成がそこまでコストなのか、本当かなと思って、手持ち環境で追試してみました。

結果としては、
– execを使った場合 -> 2022.65 [req/sec]
– execを使わない場合 -> 28297.03 [req/sec]
となりました。↑のスライドで示される結論とほぼ一緒です。結論として、プロセス生成はかなりのコストのようです。

サーバー:
– Opteron 3280
– Linux ratta 3.5.0-39-generic #60~precise1-Ubuntu SMP Wed Aug 14 15:38:41 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
– DDR3-1333 * 2
– Apache/2.2.22 (Ubuntu)

クライアント:
– Celeron G540
– Linux pikachu 3.2.0-52-generic #78-Ubuntu SMP Fri Jul 26 16:21:44 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
– DDR3-1333 * 2

ネットワークトポロジ:
(Opteron) <= (GbE UTP Cable) => (GbE Switch) <= (GbE UTP Cable) => (Celeron)

結果 (wrkによる測定)
– execを使った場合

# ./wrk -c 300 -t 4 -d 5 http://192.168.1.2/get_host_from_exec.php
Running 5s test @ http://192.168.1.2/get_host_from_exec.php
  4 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.78s     2.40s    5.05s    64.99%
    Req/Sec     1.42k     1.09k    2.73k    58.88%
  11162 requests in 5.52s, 2.11MB read
  Socket errors: connect 0, read 0, write 0, timeout 276
Requests/sec:   2022.65
Transfer/sec:    391.43KB

– execを使わずに、gethostnameを使った場合
root@pikachu:~/my_repos/wrk# ./wrk -c 300 -t 4 -d 5 http://192.168.1.2/get_host_from_php_func.php
Running 5s test @ http://192.168.1.2/get_host_from_php_func.php
  4 threads and 300 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   382.43ms    1.08s    3.57s    89.75%
    Req/Sec     7.49k     5.44k   25.33k    61.82%
  141496 requests in 5.00s, 26.74MB read
  Socket errors: connect 0, read 0, write 0, timeout 90
Requests/sec:  28297.03
Transfer/sec:      5.35MB

ToDo:
Nginx + PHP-FPM でも試してみる。

CentOS 6.4 で ufw を導入

Debian/Ubuntuのufwにすっかり慣れて、CentOS系のiptables直書きは辛くなってきたので、CentOSにufwを導入してみました。

CentOS 5.8 – ufwでファイアーウォールの設定


↑だいたいこの通りやったら出来ました。ありがたし。

作業ログ(rootで作業を想定)↓

# cd /usr/local/src/
# wget https://launchpad.net/ufw/0.33/0.33/+download/ufw-0.33.tar.gz
# tar zxvf ufw-0.33.tar.gz 
# cd ufw-0.33
# /usr/bin/python ./setup.py install
# chmod -R g-w /etc/ufw /lib/ufw /etc/default/ufw /usr/sbin/ufw
# ufw status
# ufw reset
# ufw default deny
# ufw allow 22
# ufw allow 443
# ufw enable
# ufw status