Displaying posts written in

12月 2009

CentOS 5にcicindelaをインストール

ライブドア製のレコメンデーションエンジン cicindela を CentOS 5.4 にインストールした記録です。極力 yum/rpm によるインストールを行うようにしています。参考にさせていただいたサイトははてブのcicindelaタグにまとめておきます。

cicindela ソースコードのチェックアウト

Google Code でホストされている cicindela 本体のソースコードを /home/cicindela にチェックアウト。ここは公式の手順と全く同じなので、そちらを参照のこと。

Perlモジュールのインストール

DBI/DBD-mysql は mysql-server を入れたら付いてくるので、その他のモジュールをrpmforgeから導入。公式サイトには書いていないが、私の環境では Class::Data::Inheritable と DBIx::ContextualFetch も必要だった。

sudo yum --enablerepo=rpmforge install \
  perl-Ima-DBI \
  perl-Time-Piece \
  perl-Log-Log4perl \
  perl-Module-Pluggable \
  perl-Class-Singleton \
  perl-Class-Data-Inheritable \
  perl-DBIx-ContextualFetch

ここで次のようなエラーが出た。

/usr/share/man/man3/Sys::Syslog.3pm.gz from install of perl-Sys-Syslog-0.27-1.el5.rf.i386 conflicts with file from package perl-5.8.8-27.el5.i386

Log-Log4perl の依存パッケージとしてインストールされる perl-Sys-Syslog パッケージが、CentOS 5(RHEL5)標準の perl パッケージに含まれる Sys::Syslog のファイルと衝突するらしい。参考URL。 仕方がないので Log::Log4perl だけは cpan コマンドでインストールした。

sudo cpan -i Log::Log4perl

MySQL のインストールと設定

MySQL をインストール。

yum install mysql-server

公式のインストールガイドには /home/cicindela/etc/mysql/my.cnf を /etc/my.cnf にコピーせよとあるが、この中にはかなり癖の強い設定が含まれているので、既存のサーバを流用する場合は要注意。というか、実質的には cicindela 専用サーバが必要になると考えた方が良いだろう。

特に注目すべき設置項目は以下の4つ。

max_heap_table_size = 1024M
MEMORY テーブルの最大サイズ。cicindela は MEMORY テーブルに入力データを保存するため、それなりに大きなサイズが必要になる。実際のところ、どのくらいあれば十分と言えるのだろうか…。
innodb_log_file_size = 256M
この設定値を後から変更すると起動不能に陥る場合があるので注意。変更する場合はこちらのサイトを参考に。
innodb_lock_wait_timeout = 1800
デッドロックと判断するためのタイムアウト。デフォルトは50秒なので、極端に長い。おそらくはバッチ処理の都合でこうなっているのだろう。
innodb_flush_log_at_trx_commit = 2
この設定値がどの程度意味を持つものなのか、正直言って分からない。MySQLリファレンスによれば、値を2にすることで、『より良い性能』と引き換えに『OS のクラッシュか停電によって、最後の秒のトランザクションが消されてしまいます。 』とのこと。

cicindela 用データベースの作成

misc/create_init_sql.pl を実行して、DB初期化用SQLを生成する。このスクリプトで生成できるのは CREATE DATABASE文、CREATE TABLE文 さらに –grant_* オプションを指定することで GRANT文も生成できる。

cd misc
perl create_init_sql.pl --db_name=cicindela \
  --grant_user=cicindela \
  --grant_host=localhost \
  --grant_pass=cicindela_pass | mysql -uroot

cicindela の設定

cicindela の設定ファイル lib/cicindela/lib/Cicindela/Config/_common.pm の中で最小限の設定を含む集計セットを定義する。ここでは pick という名前にした。datasource に指定するDB名やhost名は適宜変更のこと。

$C{SETTINGS} = {
    'pick' => {
        datasource =>  [ 'dbi:mysql:cicindela;host=localhost', 'cicindela', 'cicindela_pass' ],
        filters => [
            'PicksExtractor',
            'InverseUserFrequency',
            'ItemSimilarities',
        ],
        recommender => 'ItemSimilarities',
        refresh_interval => 1,
        },
};

設定を変更したらApacheを再起動。error_log や logs/log.txt にエラーが出ていないことを確認しておく。

動作確認

以下のようなURLにアクセスして、HTTPステータスコード204が返ることを確認。user_id や item_id を変えつつ何度かアクセスしておく。

http:///cicindela/record?set=pick&op=insert_pick&user_id=3&item_id=23

入力バッファテーブルにデータが入っていることを確認。

mysql> select * from picks_buffer;
+----+---------+--------------+---------+--------------+-----------+---------------------+
| id | user_id | user_char_id | item_id | item_char_id | is_delete | timestamp           |
+----+---------+--------------+---------+--------------+-----------+---------------------+
|  1 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:03 |
|  2 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:11 |
|  3 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:29 |
|  4 |       3 | NULL         |      23 | NULL         |         0 | 2009-12-25 17:28:52 |
|  5 |       5 | NULL         |      23 | NULL         |         0 | 2009-12-28 13:24:00 |
|  6 |       5 | NULL         |      29 | NULL         |         0 | 2009-12-28 13:24:20 |
+----+---------+--------------+---------+--------------+-----------+---------------------+
6 rows in set (0.01 sec)

手動で bin/flush_buffer.pl を実行。

./bin/flush_buffer.pl

入力バッファがクリアされ、一時データテーブルに入力されることを確認。

mysql> select * from picks_buffer;
Empty set (0.00 sec)

mysql> select * from picks;
+---------+---------+---------------------+
| user_id | item_id | timestamp           |
+---------+---------+---------------------+
|       3 |      23 | 2009-12-25 17:28:03 |
|       5 |      23 | 2009-12-28 13:24:00 |
|       5 |      29 | 2009-12-28 13:24:20 |
+---------+---------+---------------------+
3 rows in set (0.00 sec)

次に bin/batch.pl を実行、集計結果を算出する。初回実行時には2回実行する必要がある。ある程度以上のデータが溜まっていないと算出されないので注意。

./bin/batch.pl
mysql> select * from item_similarities_online;
+----------+----------+------------------+
| item_id1 | item_id2 | score            |
+----------+----------+------------------+
|       23 |       25 | 1.58496250072116 |
|       25 |       23 |                0 |
+----------+----------+------------------+
2 rows in set (0.00 sec)

最後に以下のようなURLにアクセスして、Web API経由でリコメンデーション・データを取得できることを確認。


http:///cicindela/recommend?set=pick&op=for_item&item_id=23

以上で cicindela の最低限の動作は確認できた。

daemontools のインストール

次に flush_buffer.p と batch.pl の実行を自動化するため、daemontools をセットアップする。まず daemontools-toaster のSRPMを http://www.qmailtoaster.org/ から取得して、ビルド&インストール。

rpmbuild --rebuild daemontools-toaster-0.76-1.3.6.src.rpm
rpm -Uvh daemontools-toaster-0.76-1.3.6.$ARCH.rpm

cicindela のバッチスクリプトに対するシンボリック・リンク作成。

sudo ln -s /home/cicindela/etc/service/cicindela_batch /service/
sudo ln -s /home/cicindela/etc/service/cicindela_flush_buffers /service/

/etc/inittab の末尾に svscan の起動コードを追加。

SV:123456:respawn:/command/svscanboot

init に HUP シグナルを送ってinittabを読み込み直し、svscanを起動する。

sudo kill -HUP 1

ps コマンド等で svscan, supervise プロセスが起動していることを確認しておく。

この状態でもう一度データの入力テストを行い、自動的に一次データ・計算結果が更新されることを確認。

このまま放っておくとどんどんログが肥大化していくので、ログローテーション用設定ファイル /etc/logrotate.d/cicindela を以下の内容で作成する。

/home/cicindela/var/logs/log.txt {
  daily
  create 0666 root root
  rotate 2
}

次はより実践的な構成でテストを行う予定。

WARNING: mismatch_cnt is not 0 on /dev/md0

Software RAID1を組んでいるサーバで、CentOS 5.3 -> 5.4にアップグレードしたところ、週末ごとに次のような警告メールが届くようになった。

/etc/cron.weekly/99-raid-check:

WARNING: mismatch_cnt is not 0 on /dev/md0

/proc/mdstat 等には特に異常などは見当たらないし、構成の異なる複数のサーバで同じ現象が起こったので、ハード的な問題ではないだろうと判断し、忙しかったこともあってそのままにしていた。しかしやはり気持ち悪いので、きちんと調べてみた。

メール中にある通り、この警告は /etc/cron.weekly/99-raid-check というスクリプトから送信されている。このスクリプトを提供しているのは mdadm パッケージで、5.4で更新されている。更新内容はレッドハットのサイトで RHBA-2009:1382-1 として提供されており、99-raid-check について説明しているのは、次の2箇所である。

* LinuxのソフトウェアRAIDスタックでは、データスクラビングがサポートされています(RAIDアレイのディスクを読み取って不良セクタを検索し、不良セクタが見つかると、他のディスクやパリティの情報を使用して、不良セクタを正常なデータで書き直します)。しかし、mdadmパッケージはこの機能を利用していませんでした。今回のパッケージは、cronジョブが/etc/cron.weeklyに追加されており、ディスクをチェックして不良セクタがないかどうかを確認し、見つかった場合は修復します。(BZ#233972)

* 今回のアップデートでは、mdadmに冗長アレイでの「データスクラビング」を可能にする新しい機能が追加されています。データスクラビングでは、冗長アレイ内のドライブ上で不良セクタが検索され、見つかった場合は他のドライブのデータを使用して修正されます。それによって、読み取りエラーを返すセクタは再構築されます。注:この新しいパッケージでは、データスクラビングはデフォルトでオンになっています。この機能は週1回実行され、実行中にある程度のパフォーマンス低下を引き起こす場合があります。パフォーマンス上の理由でこの機能を無効にしたい場合、アレイに対して実行されるチェックのタイプを管理したい場合、あるいは、どのアレイをチェックするかを制御したい場合は、/etc/sysconfig/raid-checkファイルを編集します。オプションを設定する方法の詳細は、そのファイルにコメントとして含まれています。(BZ#513200)

「データスクライピング(Data Scrubbing)」とは何か、なぜ重要なのかについては、ここで示されているBugzilla #233972およびその先のリンクに説明がある(一箇所リンクが切れているのはRAID/Software – GEntoo Linux Wikiだと思われる)。

データスクライピングを実行するには、root権限で次のコマンドを実行する。これによって不良セクタが検出され、自動的に修復される。99-raid-check が実行するのも、まさにこのコマンドである(少なくともデフォルトの設定においては)。

echo check > /sys/block/mdX/md/sync_action

ではこのコマンドと、問題の mismatch_cnt とはどう関係するのか。/etc/sysconfig/raid-check のコメントを読むと次のように書いてある。

If it finds good sectors that contain bad data (meaning that the data in a sector does not agree with what the data from another disk indicates the data should be, for example the parity block + the other data blocks would cause us to think that this data block is incorrect), then it does nothing but increments the counter in the file /sys/block/$dev/md/mismatch_count.

つまりセクタ自体は正常だが、中に入ってるデータがディスク間で異なる場合に mismatch_cnt が増加するということらしい。

ならば mismatch_cnt が 0 でないということは、ただちにRAIDが正常に機能していないことを意味するのだろうか?いろいろなMLの投稿などを読む限り、少なくとも swap パーティションを含むRAIDデバイスにおいては、全く正常であっても各ディスク間の内容が異なり得る…すなわち mismatch_cnt が0でないことがあり得るようだ。では swap を含まないパーティションの場合は?私の(また他のCentOS利用者の)ように、/boot のみを含む md0 の場合は?……これがよく分からない。この投稿を読む限り、ある種の状況においてはそうしたことも起こり得る、とのことである。

もしそうした状況に該当するなら。mismatch_cnt が 0 でなくとも、異なっているのは実際にはファイルシステムからアクセス不能な部分であって、システムの運用には影響しないのであれば。修復する方法はある。check の代わりに repair を使うことである。

#このコマンドを実行する前に以下の説明をよく読んでください!!
echo repair > /sys/block/mdX/md/sync_action

ただし /etc/sysconfig/raid-check には、次のような恐ろしいコメントが書いてある(強調は私による)。

The repair option does the same thing, but when it encounters a mismatch in the data, it automatically updates the data to be consistent. However, since we really don’t know whether it’s the parity or the data block that’s correct (or which data block in the case of raid1), it’s luck of the draw whether or not the user gets the right data instead of the bad data.

というわけで、以下は「それでも自己責任でこの方法に賭けるぜ!」という方のみ読んでください。

まず各デバイスの mismatch_cnt の値をチェックしておく。

cat /sys/block/md0/md/mismatch_cnt

そして repair を実行する。

sudo sh -c 'echo repair > /sys/block/md0/md/sync_action'

進行状況は /proc/mdstat で確認できる。

cat /proc/mdstat

repair を実行しただけでは mismatch_cnt は更新されないので、もう一度 check を実行する。進行状況はやはり /proc/mdstat で確認できる。

sudo sh -c 'echo check > /sys/block/md0/md/sync_action'

終了したら mismatch_cnt をチェックして、0 になっているか確認する。

cat /sys/block/md0/md/mismatch_cnt

……

参考にしたサイトははてブの mismatch_cnt タグにまとめてあるので、この記事に書いてあることを鵜呑みにせず、自分で調べ自分で判断するようお願いします。

key-valueストア用DataSource(CakePHP1.2)

key-valueストア用の DataSource サンプルをgithubで公開しておきます。

cakephp-key-value-source – ver.0.1

以下の説明はバージョン0.1を対象としたものです

以下のクラスが含まれています:

KeyValueSource
抽象ベースクラス。サブクラスでは最低限 get/set/del/count 4つのメソッドを実装する
JsonFileSource
jsonエンコードしてローカルファイルに保存する実装例
MemcacheSource
pecl-memcache を用いる実装例

CakePHPバージョン1.2の DataSource の仕組みはまだまだ未成熟で、あまり突っ込んだ説明は書きにくいので、詳しく知りたい方はコードを読んでください。スキーマ・レスな(スキーマの設定を DataSource側ではなく Model 側で行う)Sourceの例としては最小限の構成になっているはずです。

使用例

database.php

class DATABASE_CONFIG
{
  var $json = array('datasource' => 'json_file',
                    'root' => TMP,
                    'path' => '/json-encoded');
 
  var $memcache = array('datasource' => 'memcache',
                        'host' => 'localhost',
                        'port' => 11211,
                        'default_expire' => 3600,
                        'default_flag' => 0);
}

モデル

class Foo extends AppModel
{
  //どちらを使ったとしても、基本的な機能を使う限り、全く同じ動作になる
  var $useDbConfig = 'json';
  //var $useDbConfig = 'memcache';
 
  /*
    スキーマの設定
    id には length 必須(特に意味がなくても)
  */
  var $_schema = array('id' => array('type' => 'integer', 'length' => 4),
                       'bar_count' => array('type' => 'integer'),
                       'baz_count' => array('type' => 'integer')
                       /* ... */);
}

コントローラ

//保存するデータの構造などはDBを使う場合と同じ
$data = array('Foo' => array('id' => 1,
                             'bar_count' => 1000,
                             /* ... */));
 
//保存
$this->Foo->save($data);
 
//読み出し
$data = $this->Foo->read(null, 1);
 
//戻り値の構造もDBを使う場合と同じ
$barCount = $data['Foo']['bar_count'];

この程度の使い方だと cache と大して変わらないので、バックエンド固有の機能も少しだけ実装してみました。JsonFileSource は decode オプションを指定することでJSONからのデコード方法を指定できます。false を指定すればJSON(文字列)のまま取り出すこともできます。

//JSONのオブジェクトをPHPのオブジェクトとしてデコード(デフォルトでは連想配列)
$obj = $this->Foo->find('first',
                        array('conditions' => array('Foo.id' => 1),
                              'decode' => 'object'));
$bar = $obj['Foo']->bar_count;
 
//JSON文字列のまま取り出す
$data = $this->Foo->find('first',
                         array('conditions' => array('Foo.id' => 1),
                               'decode' => false));
$json = $data['Foo'];

MemcacheSource はモデルの expires プロパティでデータの有効期限を設定できます。

//期限無制限に
$this->Foo->expire = 0;
$this->Foo->save($data);

参考:

簡単DataSourceの作り方
countまわりの実装など参考にしました。こちらではbakeで動かすことが主眼となっていますが、私のサンプルはbakeでは動かないはずです(bake自体使わないので)

動的にSELECTを構築するためのjQueryプラグイン

全てのブラウザで安定してSELECT要素を操作するためのプラグイン……というか、実質IE対策プラグインです。

gits: 256077動作テスト

ごく小さなプラグインで、余計な機能は一切ついていません。使用できるメソッドは clearOptions と addOption の2つです。当たり前ですが、SELECT要素以外に使っても意味はありません。

clearOptions()
全てのOPTION要素を削除します
addOption(text, value, selected)
新しいOPTION要素を追加します。引数はそれぞれ表示されるテキスト、値、選択状態です

こんな感じで使います。

//start月からend月までの選択肢を作る
function updateMonth(start, end) {
  var select  = $('#month');
  var current = select.val();
 
  select.clearOptions();
  for(var i=start; i<=end; ++i) {
      select.addOption(i + "月", i, i == current)
  }
}

全てのブラウザで(というかIEで)安定した結果を得るために、次の点に注意すると良いでしょう:

  • 構築直後に val メソッドによる選択は行わない。代わりに addOption メソッドの第3引数でデフォルトの選択状態を設定する
  • 構築処理全体を setTimeout で遅延実行する

検索で来る方のために、このプラグインを作るまでにIEで遭遇した問題も書いておきます:

  • 構築後、SELECTの横幅がおかしくなる。構築を繰り返すたびにだんだんと短くなり、最後には消滅する
  • 構築に時間がかかる。そのために処理の途中でSELECTの表示がチラつく
  • 構築直後に val メソッドで選択状態を変更すると「selected プロパティを設定できませんでした。未定義のエラーです。」となる(val メソッドを使わず、addOption メソッドの第3引数を使うことで回避可能)
  • ページ読み込み直後にSELECTを構築すると、選択がレンダリング結果に反映されない(構築処理全体を setTimeout で遅延実行することで回避可能)

コード全体:

SONY NW-S645購入

16GBモデルで15000円弱。同容量のiPod nanoより2000円ほど安い。その安さに惹かれて購入したのだが、良い意味で驚かされた。それまで使っていた二世代前のiPodに比べて、圧倒的に音が良い。同じイヤホン(MDR-EX90SL)、同じ音源(mp3やAAC)なのに、全く違って聞こえる。さすがに最近のiPodならもう少し差は縮まるのかも知れないが、個人的にはもうiPodに戻ろうとは思わない。

私は初代からのiPodユーザなので、iPodシリーズとはかれこれ8年近い付き合いになる。その間にも、実はiPodの音を疑ったことは何度かあった。曲によってはっきりと不快に感じることすらあった。その度にイヤホンが悪いのか音源が悪いのかと疑い、iPod本体に目を向けることはなかった。iPodに代わる選択肢は事実上存在しない状況だったので、iPodを疑っても仕方がなかったのだ。でもついにはっきりと現実を認識する時が来てしまった。高音の歪みが不快で仕方なかったあの曲を、NW-S645は見事に奏でてくれた。iPodは、音が悪かったのだ……。

回顧はこのくらいにして、少し真面目なレビューを。

曲の転送

NW-S645は「ドラッグ&ドロップ転送」に対応しているので、サポート対象外だがWindows以外でも使える。私の母艦であるMacBook Proに接続すると普通のUSBストレージとして認識された。ボリューム名は「WALKMAN」。中に MUSIC というディレクトリあるので、そこにコピーすればあとは勝手にアーティスト名やアルバム名のデータベースを構築してくれる(コピーしたディレクトリ単位で再生することもできる)。またiTunes Storeから購入した曲(PlusなどDRMがかかっていないもの)を転送すると、アートワークも正常に反映された。しかしiTunes上で後から割り当てたものについては反映されないようだ。

これは私の環境だけかも知れないが、取り外す際にちょっとしたトラブルがあった。Finderの取り外しアイコンをクリックすると一旦は接続解除されるのだが、すぐにまた接続が開始されてしまうのだ。結局「解除した瞬間にUSBケーブルを引っこ抜く」という、なんともいい加減な方法で対処することにした。

操作性

操作性はすこぶる良い。ほとんど何の不満も無い。iPodより使いやすいと思う。というか、これも比較して初めてはっきり認識できたことだが、iPodのタッチホイールによるUIは、別に使いやすくはなかった。例えば曲の再生方法(リピートやシャッフル)を変更したいとき、iPodだといちいちトップメニューまで戻って「設定」から変更する必要があったが、NW-S645は「OPTION」ボタンから即座に変更できる。曲を選ぶ時のカーソルの移動速度はホイールにはかなわないが、左右ボタンでA-Zあ-わ移動ができるので問題ない。全体として、よく考えられた作りになっていると思う。

その他

本体デザインは金属やガラスの質感が美しく、iPod nanoほどエレガントではないものの、とてもカッコいい。液晶も綺麗で見やすい。ACアダプタが別売りなことには面食らったが、そもそもバッテリの持ちが非常に良いのであまり問題にならない(実はiPodのものが流用できそう?)。

追記:音質について

ピアノやヴァイオリンなどの比較的静かな曲だとそのままでも素晴らしい音が出るのだが、JPOPなどの賑やかな曲だとボーカルが埋没してしまって、ひどくアンバランスに聞こえることがある。そういうときはイコライザを「ポップス」あたりに変更してやれば、見違えるような(聞き違えるような?)音になる。イコライザもOPTIONボタンから簡単に切り替えられるので、いろいろ試してみると面白い。

CakePHP 1.2 prefix付きURL生成時の注意点

※CakePHP バージョン 1.2限定の話題です。

Router::url によるURLの生成時に、prefix パラメータは考慮されない。ありがちな例として携帯用URLを作るために次のような route を追加した場合、

Router::connect('/m/:controller/:action/*',
                array('prefix' => 'mobile'));

URL生成時には prefix パラメータの有無によらず、この route がマッチする。各種ヘルパー($html->link | $paginator | $form->create)でPC用のURLに意図しないプレフィクスが付いてしまう、という問題の原因はこれである。

// どちらも /m/foo/bar になってしまう!
$url->link('link', array('controller' => 'foo', 'action' => 'bar', 'prefix' => 'mobile'));
$url->link('link', array('controller' => 'foo', 'action' => 'bar'));

この問題を回避するためのイディオムとして、prefix パラメータの他に [prefix名] => true というパラメータを設定する。

Router::connect('/m/:controller/:action/*',
                array('prefix' => 'mobile',
                      'mobile' => true));

URL生成時にはこちらのパラメータを使う。

// こちらは /m/foo/bar
$url->link('link', array('controller' => 'foo', 'action' => 'bar', 'mobile' => true));
 
// こちらは /foo/bar
$url->link('link', array('controller' => 'foo', 'action' => 'bar'));

参考:

3.4.5.5 プリフィックスルーティング(Prefix Routing) :: The Cookbook
現在は正しいコード例が示されている。
“Prefix利用時のPaginatorHelperが吐くURLが正しく表示されない” フォーラム – CakePHP Users in Japan:
cakephp.jpのフォーラムにて。検索パラメータが引き継げないのはおそらく connectNamed が設定されていないから?

なお Routing.admin によるルーティングは純粋なプレフィクスルーティングではなく、Router の中で特別扱いされているのでこの問題は生じない。また CakePHP 1.3 ではプレフィクスルーティングの機能は大きく拡張されるため、最終的にどのようになるのかは分からない(コードを斜め読みした限りでは、個々の route に個別に設定する必要は無くなり、Configure::write(‘Routing.prefixes’, array(…)); で一括指定できるようになるらしい。ただ少なくとも 1.3.0-alpha においては、1.2同様の prefix も使えるようだ)。

追記:

[prefix名] => true を設定してもうまくプレフィクスが付かない場合は、route 側にパラメータが足りない可能性が高い。最もありがちなのは名前付きパラメータを使用する場合。

// こんなURLを作りたいが => /m/foo/bar/type:99 
// こうなってしまう => /foo/bar/mobile:1/type:99
$html->link('link', array('controller' => 'foo', 'action' => 'bar', 'mobile' => true, 'type' => 99));

こういうときは Router::connectNamed を使って routes.php の中で宣言しておく必要がある。

Router::connectNamed(array('type'));

Router::connectNamed についての説明はAPIドキュメントが最も詳しい。ちなみに page や sort といった paginator 関連のパラメータはデフォルトで設定されるので改めて設定する必要はない(だからこそ気が付きにくいとも言えるが)。