PerlでRedisのSRANDMEMBERを実行して結果を取得

追記@2014-07-30

PerlのRedisライブラリですが、Redis::hiredisが一番使いやすいと思います。ほぼ、Redisプロトコルをしゃべるような感じです。なんとなくとっつきにくいかもですが、Redisプロトコルは簡単で読みやすいので1分で慣れると思います。


PerlのRedisライブラリはいろいろとありますが、RedisDBを使って表題のことを行いました。このライブラリがおそらくいちばん汎用的です。

Redisというそのものずばりのライブラリがあるんですが、なんとsrandmember命令の要素数を選べません。

というわけで、RedisDBというものを用いました。汎用的ですが例によって分かりにくいライブラリで、(ヽ´ω`)ハァ…って感じです。まあタダで使っているのに文句言うなよって感じですけれどね。。。ドキュメントに具体例がほとんどなくて、replyを取得するときに-1というindexを使ったりと、もうなんていうか。。。

下記にコードを載せます。内容としては、
– 0~100までの数字を入れた配列を用意
– Redisに接続して内容物を削除
– パイプラインで、先の配列を「myset」という名のSET型に挿入
– 3件ランダムに取得して、表示
です。私はPerl素人なので、変な記述があるかもしれません。

use RedisDB;

$max = 100;
$num = 3;

@pairs = ();
for($i = 0; $i < $max; $i++){
        push @pairs, $i;
}

$redis = RedisDB->new(host => 'localhost', port => 6379);

# 一旦削除
$redis->send_command('FLUSHALL');

# パイプラインのつもり
$redis->send_command('MULTI');
foreach $pair (@pairs) {
        $redis->send_command('SADD', 'myset', $pair);
}
$redis->send_command('EXEC');

# 3件ランダムに取得
$redis->send_command('SRANDMEMBER', 'myset', $num);
@results = $redis->get_all_replies;

for($i = 0; $i < $num; $i++){
        print $results[-1]->[$i] . "\n";
}

RedisのZSETのサイズの圧縮効率

RedisのZSETでは圧縮機能があります。デフォルトだと128個以内で1要素64Byte以下ならば圧縮されます(正確に言えば圧縮とは言えず、データ構造が異なる、と言うべきでしょうか)。
該当項目はredis.confのこの部分↓です。

redis.conf

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

* 検証方法
– zsetの要素数128個のkeyを1万個作る
– redis.confでzset-max-ziplist-entriesを128(圧縮される)と127(圧縮されない)を設定して、Redisを立ち上げる
– 検証用コード(後述)を使って、メモリ使用量をそれぞれ測定して比較

* 結果
KEYの数 要素数 zset-max-ziplist-entries サイズ(MB)
10000 128 128 20.268
10000 128 127 140.759

* 考察
128を閾値にすると20.268MB、127を閾値にすると140.759MBと、7倍もの違いがありました。もちろんkeyの長さや要素の長さによって変わってくるでしょうが、これは大きいですね。

* 環境
– Mac OSX 10.8.5
– Redis 2.9.11 (8f52173b/0) 64 bit
– gcc version 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)

* ふろく

ちなみに圧縮しているかどうかは、redis-cliで、
127.0.0.1:6379> object encoding u1
"ziplist"
として確かめることが出来ます。ziplist->圧縮されている、skiplist->無圧縮

ちなみに、検証用コード(後述)で、
>$element = gmp_strval(gmp_init($i + $j + ELEMENTS_OFFSET, 10), 62);
として、base62にしてみても、サイズは変わりありませんでした。Redisのコードを読んでいないのですが、INTが入ってくると型を認識してINTとして格納しているのかもしれません。

検証用コード
<?php

define('NUM_ELEMENTS', 128);
define('NUM_KEYS', 10000);
define('ELEMENTS_OFFSET', (int) (4294967295 / 100));

$redis = new Redis();
$redis->connect('localhost', 6379, 1.0);
$redis->flushAll();

$before = getRedisMemory($redis);

for ($i = 0; $i < NUM_KEYS; $i++) {
	$pipe = $redis->multi(Redis::PIPELINE);
	for ($j = 1; $j <= NUM_ELEMENTS; $j++) {
		$element = $i + $j + ELEMENTS_OFFSET;
		//$element = gmp_strval(gmp_init($i + $j + ELEMENTS_OFFSET, 10), 62);
		$pipe->zAdd('u' . $i, time() + $i + $j, $element);
	}
	$pipe->exec();
}
$after = getRedisMemory($redis);

printf("%d : # of elements\n", NUM_ELEMENTS);
printf("%d : # of keys\n", NUM_KEYS);
printf("%.3f [MB]: before\n", $before / 1024 / 1024);
printf("%.3f [MB]: after\n", $after / 1024 / 1024);
printf("%.3f [MB]: diff\n", $after / 1024 / 1024 - $before / 1024 / 1024);

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


mybenchでinsert

mybench: http://jeremy.zawodny.com/mysql/mybench/ というMySQLのPerl製ベンチマークスクリプトがあります。High Performance MySQLの作者によるスクリプトだそうです。Perlなので変数を使って自分で好きなクエリを作ってベンチマーク出来ていい感じです。

インストール方法や使い方は検索してみてください。

私が行ったのは、
– デフォルトの selectではなく insert で実行
– Redisを使って変数を共有
です。このスクリプトはPerlプロセスをフォークします。変数の受け渡しが困難になるので、Redisを用いました。

スクリプトは後述します。以下の様な結果になりました。Redisを立ち上げておいて下さい。

$ ./insert -n 100 -r 10000
forking: ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
sleeping for 11 seconds while kids get ready
waiting: ----------------------------------------------------------------------------------------------------
test: 1000000 6e-05 0.24007 0.001926992766 1926.992766 51894.330775085
  clients : 100
  queries : 1000000
  fastest : 6e-05
  slowest : 0.24007
  average : 0.001926992766
  serial  : 1926.992766
  q/sec   : 51894.330775085

100クライアントで、1クライアントにつき10000件のinsertです。つまり合計100万insertです。5.2万[q/sec]となりました。htopというコマンドで観察してますと、全コアがフルロードされていました。serialという項目が分かりにくいですが、おそらくクライアントを直列に実行した場合に掛かる時間だと思います。つまり、実際に実行して終わるまでに掛かった時間は、serial / clientsで、1926.992766 / 100 = 19.27 [sec] となります。

はまりどころとしては、
– -n 250などとすると、「failed: Too many connections」となる
– このスクリプトの実行前にtableのレコードを全削除しないとエラーになる
などです。

私のMySQLの環境については、書ききれないところが多いですが、

– Intel i7-3820
– 16GB DRAM (8GB * 4)
– Intel 520 Series 480GB
– Debian 3.2.57-3 x86_64 GNU/Linux
– mysql Ver 14.14 Distrib 5.5.37, for debian-linux-gnu (x86_64) using readline 6.2
– テーブル定義: create table t001 (edge varchar(255) not null primary key, ts int not null) engine=innodb row_format=dynamic;

です。my.confの主要なところは、

innodb_buffer_pool_size = 24G
innodb_flush_log_at_trx_commit = 2
sync_binlog = 0
innodb_flush_method = O_DIRECT
innodb_file_format = Barracuda
innodb_file_per_table = 1

です。実行する前に、tableの中身を消して下さい(delete from t001)。

insert
#!/usr/bin/perl -w

use strict;
use MyBench;
use Getopt::Std;
use Time::HiRes qw(gettimeofday tv_interval);
use DBI;
use Redis;

my %opt;
Getopt::Std::getopt('n:r:h:', \%opt);

my $num_kids  = $opt{n} || 10;
my $num_runs  = $opt{r} || 100;
my $db        = "bench01";
my $user      = "root";
my $pass      = "root";
my $port      = 3306;
my $host      = $opt{h} || "localhost";
my $dsn       = "DBI:mysql:$db:$host;port=$port";

our $min = 1;
our $max = 3 * $num_kids * $num_runs;
our $middle = ($min + $max) / 2;
our @uid = List::Util::shuffle ($min .. $middle);
our @fid = List::Util::shuffle (($middle+1) .. $max);

our $redis = Redis->new( server => 'localhost:6379');
$redis->set('pcounter', $num_kids);

my $callback = sub
{
    my $id  = shift;
    my $dbh = DBI->connect($dsn, $user, $pass, { RaiseError => 1 });
    #my $sth = $dbh->prepare("SELECT edge FROM t001 WHERE edge = ?");
    my $sth = $dbh->prepare("insert into t001 values (?, unix_timestamp(now()))");
    my $cnt = 0;
    my @times = ();

    ## wait for the parent to HUP me
    local $SIG{HUP} = sub { };
    sleep 600;

    ## get process number
    my $pcounter = $redis->decr('pcounter');
    #print "\npcounter: " . $pcounter . "\n";
 
    while ($cnt < $num_runs)
    {
        #my $v = int(rand(100_000));
        #my $v = '6-11';
        my $edge = $uid[$pcounter] . "-" . $fid[$cnt];
        #print "edge: " . $edge . "\n";
        ## time the query
        my $t0 = [gettimeofday];
        $sth->execute($edge);
        my $t1 = tv_interval($t0, [gettimeofday]);
        push @times, $t1;
        $sth->finish();
        $cnt++;
    }

    ## cleanup
    $dbh->disconnect();
    my @r = ($id, scalar(@times), min(@times), max(@times), avg(@times), tot(@times));
    return @r;
};

my @results = MyBench::fork_and_work($num_kids, $callback);
MyBench::compute_results('test', @results);
$redis->quit;

exit;

__END__

おまけ
mysql> show table status\G
*************************** 1. row ***************************
           Name: t001
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 1007866
 Avg_row_length: 66
    Data_length: 66732032
Max_data_length: 0
   Index_length: 0
      Data_free: 46137344
 Auto_increment: NULL
    Create_time: 2014-07-16 11:06:15
    Update_time: NULL
     Check_time: NULL
      Collation: latin1_swedish_ci
       Checksum: NULL
 Create_options: row_format=DYNAMIC
        Comment: 
1 row in set (0.00 sec)

100万レコードで、66732032[B] = 63.6[MB]ほど。ということは、
– 1000万レコードで636MB
– 1億レコードで6.36GB
– 10億レコードで63.6GB
となる。

MySQLに対してバルクインサートを行ってくれるPHPのライブラリ、bulkyを使ってみる

MySQLでのバルクインサートを簡単に出来るようにする、PHP向けのbulkyというライブラリがあります。
>http://blog.yuyat.jp/archives/2018
使ってみました。覚書です。

環境は、
– Debian 7
です。MySQLなどはインストールして走らせておいてください。

いちおうPHPのライブラリがらみはこんな感じですかね↓

$ aptitude install apache2 apache-dev php5 php5-dev php-pear php5-mysql php5-pgsql

以下、環境構築です。必要なファイルの具体的な中身は後ろの方に書いてありますのでそちらを参照して下さい。
# ディレクトリを作成
$ mkdir bulkyexample
$ cd bulkyexample

# ファイルを作って編集 (composer.jsonファイルは後述)
$ vi composer.json

# composerをダウンロードして、活用
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar install

# 確認 (ファイル3個、ディレクトリ1個出来ているはず)
$ ls
composer.json  composer.lock  composer.phar  vendor

# データベースとテーブルを作成 (users.sqlファイルは後述)
$ vi users.sql
$ sudo mysql -u root -p < users.sql

# PHPファイルを作成して実行 (example.phpファイルは後述)
$ vi example.php
$ php example.php

# mysqlにログインして確認
$ sudo mysql -uroot -p
mysql> use bulky_test;
mysql> select * from users limit 3;
+----+---------------------+------------+
| id | name                | birthday   |
+----+---------------------+------------+
|  1 | Scott Wino Weinrich | 1960-09-29 |
|  2 | Scott Wino Weinrich | 1960-09-29 |
|  3 | Scott Wino Weinrich | 1960-09-29 |
+----+---------------------+------------+

以上です。たいへん便利なライブラリですね。

composer.json
{
"require": {
"yuyat/bulky": "dev-master"
}
}

users.sql
CREATE DATABASE bulky_test;
USE bulky_test;

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) COLLATE utf8_unicode_ci NOT NULL,
  `birthday` date NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

example.php (ユーザー名とパスワードは書き換えて下さい。7行目の’root’, ‘root’が、ユーザー名とパスワードになっています)
<?php
require_once dirname(__FILE__) . '/vendor/autoload.php';

$queueFactory = new Yuyat_Bulky_QueueFactory(
    new Yuyat_Bulky_DbAdapter_PdoMysqlAdapter(new PDO (
        'mysql:dbname=bulky_test;host=localhost',
        'root', 'root'
    )),
    10
);
$queue = $queueFactory->createQueue('users', array('name', 'birthday'));

$queue->on('error', function ($records) {
    echo "Error!", PHP_EOL;
});

for ($i = 0; $i < 100; $i++) {
    $queue->insert(array('Scott Wino Weinrich', '1960-09-29'));
}

MacのTimeMachineで「バックアップを準備中」について

バックアップを準備中、といつまでも表示されている状態でした。外付けHDDのアクセスランプもついていないので、明らかに何もしていません。4時間ほど様子を見ても変わりません。

私の環境は、
– MacBook Late 2009
– Mac OSX 10.8.5
です。また、FileVaultの初期版を使っています。

spotlightを切れば良いという意見を見て、そうしてみました。ターミナルで、

$ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.metadata.mds.plist 

↑これをしてみたところ、急にバックアップが始まりました。spotlightなんてくずソフトなのでいらないですね、ターミナルでfindコマンドで一番です。

このような穴があるTimeMachineを平気な形で宣伝するAppleにはがっかりです。コンピュータをカジュアルに使う人でも、簡単に使えるソフトを作るべきなのに。