Redmineを2.0.0にアップデートした

Ruby全然分かんないんですけど割とすんなりいきました。

Redmineをアップデート

まあ普通にファイル類を置き換えるだけなんでここはとっても簡単です。

  • 一応DBとかfilesディレクトリはバックアップしておけとのことなんで適当にバックアップする
mysqldump -proot --default-character-set=binary redmine > redmine.dump
cp -ai files/ /tmp/
  • Redmineの最新版を落としてきて適当に配置
wget http://rubyforge.org/frs/download.php/76134/redmine-2.0.0.tar.gz
mv /home/redmine/ /home/redmine_old/
mv redmine-2.0.0 /home/redmine/
  • 設定ファイルを配置
cp /home/redmine_old/config/database.yml config/
cp /home/redmine_old/config/configuration.yml config/
cp /home/redmine_old/config/unicorn_config.rb config/

ついでにrvmからrbenvにしてRubyを1.9.3にしちゃう

  • とりあえずrvmを外す

.zshrcなり/etc/profileに入っているrvm関連のPATH設定とかを外しちゃいましょう。

  • rbenv入れる

ついでにruby-buildというのを入れると「rvm install」相当のことができるらしいです。

git clone git://github.com/sstephenson/rbenv.git
mv rbenv/ /usr/local/rbenv/

; usr/local/rbenv/binをPATH設定に入れる
; eval "$(rbenv init -)"を.zshrcに入れる
; soruceも忘れずに!

git clone git://github.com/sstephenson/ruby-build.git
./ruby-build/install.sh
  • Rubyをインストール
rbenv install 1.9.3-p194
rbenv global 1.9.3-p194

ruby -v
; バージョン表示が正しく出ていれば成功!
  • bundleする

Redmineに必要なgem一式を一気に入れてくれるそうです。
APサーバーをUnicornでやりたいのでRedmineディレクトリに「Gemfile.local」というファイルを作り、「gem "unicorn"」を追加する。
尚、コマンドはRedmineディレクトリで。

bundle install --without development test
  • rakeコマンド

この辺まではRedmineのインストールガイドに書いてあるので普通に出来ると思います。

rake generate_secret_token
rake db:migrate RAILS_ENV="production"
rake tmp:cache:clear
rake tmp:sessions:clear

ログを見るとdeprecatedっぽいのがバンバン出てる気がしますが特に気にしない。

bundle exec unicorn_rails -E production -c /home/redmine/config/unicorn_config.rb -p 3000 --path /redmine

余談

一昔前はRubyRoR)製のWebアプリって入れるの超めんどくせーイメージあったんですが(というか実際そうだったと思いますが)、最近は割と簡単にできて素晴らしいと思います。

「net.ipv4.tcp_tw_recycle」を有効にするのは(場合によっては)やめた方がいい

そもそも「tcp_tw_recycle」ってなに?

TIME_WAIT状態のソケット*1を高速に再利用するためのLinuxカーネル特有の仕組みらしい。

「/etc/sysctl.conf」でこいつ(net.ipv4.tcp_tw_recycle)を1にしてやって「sysctl -p」するだけで有効になります。

「TIME_WAITのソケットを少なくしてくれるんでしょ?いいじゃん!」という感じで設定してしまいそうになりますが・・・

なんでダメなの?

結論から言うと、こいつを有効にしたとき、同じグローバルIPのクライアントからの接続でかつTCPパケットにタイムスタンプ情報が入っている場合で、ほぼ同時にパケットを送ると、古いタイムスタンプの方のパケットを勝手にドロップしちゃいます。

どういうことなの・・・

tcp_tw_recycle」は、「同一IPからのパケットが到着したとき、使っていたソケットをすぐに開放する」というロジックの中でタイムスタンプ情報を使っているのだと思われます。

TCPパケットにタイムスタンプ情報が入っている本来の目的は、再送タイムアウトの間隔を動的に調整するためであったり、同一のTCPシーケンス番号でほぼ同時に来たパケットのうち、どっちが最新なのか(処理すべき物か)っていうのを判断するためだそうです。
そして、このタイムスタンプは端末内で付加され、Unixtimeではなく「端末の起動時間(の秒数?)」*2を送っているようです。

本来の目的であれば、前者に関してはパケット一個で整合性が取れていれば良く*3、後者に関してはTCPのシーケンス番号*4が被ったタイミングでタイムスタンプを比較すれば良いので、正常なパケットがサーバーに来た時点で破棄されるケースはほとんどないでしょう。

しかし、「tcp_tw_recycle」の仕組みでは、同一IPからくるパケットのうち、いずれかの(起動時間の短い)端末のパケットを(過去のものであるとみなして?)常に破棄するということになってしまうので大問題です。

あと、このタイムスタンプ情報は最近のOSから送られるパケットには(デフォルト設定で)大体入ってます*5

どういうケースでまずいの?

例えば、同じWAN内のWiFiで接続しているiPhoneAndroidとPCからほぼ同時に同じサーバーに接続したとすると、どれかひとつ*6は普通に接続出来ますが、それ以外の端末は(サーバー側で設定してある)タイムアウトまでずっと読み込み画面のままで固まってしまいます(最終的にはブラウザとかアプリ側が「接続できませんでした云々」のステータスを返します)。

特に、「同じ回線から複数の端末で接続する」ようなケースとして、テスト・デバッグ環境用のサーバーでは必ず無効にしておいた方がいいと思われます*7
上記のようなケースが多発するのでかなりひどいです。

というか・・・

これを有効にして恩恵を受けられるケースって果たしてあるんでしょうか・・・。

昔であれば、一般的な家庭であればグローバルな回線とPCがルーターを介さずに直でつながっていたり、3G回線とかも一応それぞれにIPが割り振られるので別にいいんですが、今だったら公衆WiFiとかで違う人々が(この設定を有効にしてあるサーバーの)同じサービスとかWebページ見ようとしたら一人しかまともに接続できなくなっちゃうんですよね。
それってデメリットでかすぎじゃないですかね・・・?

そもそも、TIME_WAITを早く再利用しないといけない環境ってことは、すげーパケットのやりとりが行われていて回線がパンクしそうってことなので、素直にサーバーを増やして通信を分散させるか、どうしてもこの仕組みを利用するんであれば「net.ipv4.tcp_fin_timeout」*8をかなり短くしてやったほうが良いです。

*1:通信が終わったけど同じポートを同時に使わないように一定時間待ってる状態のソケット

*2:いわゆるuptime

*3:つまりuptimeでも全然問題ない

*4:ちなみに32bit

*5:少なくともiOSAndroidからのパケットには入ってます

*6:前述した通りであれば「一番起動時間が長い端末」ということになる

*7:ただしデフォルトでは無効らしいのでカーネル設定を変更していなければ特に気にすることはありません

*8:TIME_WAIT状態の猶予時間

さくらVPSで一般的な?LAMP環境を構築する

プランが一新され、今借りてるメモリ1.5Gのプランと同じ値段でCPU3コア、メモリ2Gが借りられるということで移行することにした。
その際に、イチから構築したのでその備忘録。

注意

自己流。

とりあえずログイン

VPSの作成が終わるとさくらからメールが来るので、所定のIPおよびアカウント情報を用いてssh接続する。

アカウント周り&SSH周りを変更

rootで弄り続けるのは危ない(と言われている)ので、普段使い用のアカウントを作り、sudo経由で弄れるようにする。
併せて、ssh経由でのrootログインを禁止し、パスフレーズでのsshログインも禁止する。

ユーザー追加

# useradd -g users pull
# passwd pull

# visudo
>> 以下の部分を変更。

## Allow root to run any commands anywhere
root    ALL=(ALL)       ALL
pull    ALL=(ALL)       ALL <- 追加

sshキー作成

# su - pull
$ ssh-keygen -t rsa
$ cd .ssh/
$ chmod 600 *

$ mv id_rsa.pub authorized_keys
>> 本当はこうやった方が意味合い的には良さげ?

$ touch authorized_keys
$ cat id_rsa.pub >> authorized_keys

id_rsaをローカルに移動に移動してわかりやすい名前に変えておく(内容をコピペでも可)。
MacおよびLinuxの場合は「chmod 600 (移動したid_rsa)」をしないと怒られるかも(WinSCPとかPuttyとかFilezilla使う場合は「puttygen」を使ってppk形式に変える必要有り)。

sshd設定

# vi /etc/ssh/sshd.conf
>> 以下の部分を変更

PermitRootLogin no
RSAAuthentication yes
PubkeyAuthentication yes
PermitEmptyPasswords no
PasswordAuthentication no
UsePAM no

作ったユーザーでログイン&sudo確認

ssh -i (ローカルのid_rsa) pull@(ip)
sudo -s

以上が確認できたらrootでログインしているsshを切断して、以降はこのアカウントで構築作業を進める。

remi,rpmforgeリポジトリ入れる

epelはすでに入ってた・・・

# rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
# rpm -Uvh http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.2-2.el6.rf.x86_64.rpm

enable(リポジトリのデフォルト有効化)はまあ、どっちでもいいんじゃないすかね・・・。

dagリポジトリ入れる

# vi /etc/yum.repos.d/dag.repo
[dag]
name=Dag
baseurl=http://ftp.riken.jp/Linux/dag/redhat/el$releasever/en/$basearch/dag/
enabled=0
gpgcheck=1
gpgkey=http://dag.wieers.com/packages/RPM-GPG-KEY.dag.txt

アップデート

# yum update

途中でGPGキーの警告が出るのでy入力して進める。
ちょっと時間かかる。

Zsh入れる

この辺でいい加減Zshといつもの.zshrcじゃねーからストレス溜まるわ!!!」ってなってくると思います。そういう方はさっさとインストールしてログインシェル変えましょう。

ちなみに、CentOS5.x系においてZshyum経由で入れると日本語入力に難ありなのでいちいちソースから入れてたのですが、6.x系はそのバグが解消された4.3.10がインストールされるようなので*1yumで入れて問題無しです。
ついでに他の環境で使ってるオレオレ仕様の.zshrcを入れておくと良いでしょう(当たり前ですけど元の環境でしか必要ない設定は事前にコメントアウトしてからsourceした方がいいですよ)。

# yum install zsh

$ chsh
>> /bin/zshを指定

「sudo -s」対策(非推奨)

CentOSの5.x系と6.x系で(個人的な観点で)大きく変わっているのは「sudo -s」の挙動だと思われます。

  • ホームディレクトリが変更される(ログインしたユーザーのホームではなく「/root」になる)
  • ログインシェルは変わらないが、.zshrcはやはり「/root」にある.zshrcを読む

多分、これが本来の挙動なんだと思います。前者は別に良いとして、後者はなんとなく落ち着かないので「/root」ディレクトリに先ほど移動させた.zshrcのエイリアスを作成しておきます。

というよりは、「sudo -s」をやめろっていう簡単な話なんでしょうね。。。

# ln -s /home/pull/.zshrc /root/.zshrc

byobu入れる

別にscreenでもいいんですけど。

# yum --enablerepo=epel-testing install byobu

Apache入れる

一時期ソースから入れてたりしましたが、設定ファイルの構成とかが面倒なのでyumから入れるようにします。

yum install httpd httpd-devel

MySQL入れる

remiリポジトリから最新stable(5.5.23)を入れられたりしますが、
ソースらしきものが見当たらなかったりする*2のでrpmから入れます。
MySQLの公式サイト(多分登録が必要・・・)から以下のrpmを落として普通にインストールします。

どうでもいいんですが、MySQL公式のダウンロードページのリスト、ディストリビューションがプルダウンで分かれてるのはいいんですが、その代わりアーキテクチャi686とかx64)とかrpmの種類がごっちゃになってるのでその辺もっと見やすくして欲しいです。。。

インストールする順番はserver入れてその先は適当でいいんじゃないでしょうか。
ちなみに私はserver->client->devel->shared->shared-compat->srcと入れてます。

尚、細かいですが5.x系の時とソースのインストール場所が変わってます。
(5.x系は「/usr/src/redhat/SOURCES/」に入ってたはず・・・)
とりあえず、(MySQL本体を弄らないのであれば)適当なディレクトリに移して解凍しておくといいんじゃないでしょうか。

# mv /root/rpmbuild/SOURCES/mysql-5.5.23.tar.gz /usr/local/src/
# cd /usr/local/src/
# tar xvf mysql-5.5.23.tar.gz

標準的な設定(要はmy.cnfのサンプル)が「/usr/share/mysql」(の中の頭が「my」で始まって「.cnf」でおわるもの)にあるので、適当に「my-large.cnf」あたりをコピーしておけばいいと思います。どうせ後で設定変えますよ!

cp /usr/share/mysql/my-large.cnf /etc/my.cnf

PHP入れる

remiで入れると5.3.10入るんだぜぇ〜!
でもソースからビルドして入れたいぜぇ〜!
yumで入れられるなのにだぜぇ〜!
ワイルドだろぉ〜?

とりあえず、その場合は事前に必要なライブラリ類を入れておかないとconfigureでガンガン引っかかりまくりますのでご注意下さい。

# wget http://jp2.php.net/get/php-5.3.10.tar.bz2/from/jp.php.net/mirror
# tar xvf php-5.3.10.tar.bz2
# cd php-5.3.10/
# yum install zlib-devel libxml2-devel libjpeg-devel libmcrypt-devel libpng-devel openssl-devel expect curl curl-devel bzip2-devel
# ./configure \
--prefix=/usr/local/php \ ; 適宜変える(この場合/usr/local/phpの中にPHPに関するファイルが全部入る)
--with-mysql=/usr/bin/ \ ; この辺環境に応じて
--with-mysqli=mysqlnd \ ; ネイティブドライバ
--with-pdo-mysql=mysqlnd \ ; ネイティブドライバ
--enable-mbstring \
--enable-zend-multibyte \
--with-libdir=lib64 \
--with-gd \
--with-jpeg-dir=/usr/lib \
--with-png-dir=/usr/lib \
--with-apxs2 \
--with-mcrypt \
--with-openssl \
--with-zlib \
--with-bz2 \
--enable-zip \
--with-curl \
--enable-pcntl
# make

makeに割と時間が掛かります。
前にさくらVPSでビルドしたときはするするっと行った気がするんですが、今回はmakeの途中で怒られました。

/usr/bin/ld: cannot find -lltdl
collect2: ld returned 1 exit status
make: *** [libphp5.la] エラー 1

なんかlibmcrypt関連のエラーらしいです。
ここのエラー14を参考にして必要なのをインストールしてからmakeすると今度はちゃんと成功しました。

ちなみに(上記設定だと)設定ファイルの場所は「/usr/local/php/lib/php.ini」になります。
とりあえず標準設定をコピーしておきましょう。

# cp /usr/local/src/php-5.3.10/php.ini-production /usr/local/php/lib/php.ini

設定ファイルの場所はcofigure時のオプション「--with-config-file-path=」で変えられるので、たとえば「/etc/php.ini」にしたい場合は「--with-config-file-path=/etc」とすると良いと思います。

PHPのbinディレクトリをPATHに入れる

上記のように、ソースから入れてprefixを変えた場合、php関係のバイナリが全部prefixで指定したディレクトリ以下のbinディレクトリに入ると思うので、.zshrcなり/etc/profileなりにPATHの設定を追加しておかないとcli環境が使えません(たぶんmod_phpは使える)。

$ vi ~/.zshrc
>> 以下を追加

export PATH=${PATH}:/usr/local/php/bin

pecl installで入れた)エクステンションがインストールされる場所

試しにapcなんかをpeclコマンドで入れると、後ろのほうにインストールしたディレクトリが出てくるはずです。恐らくCentOSであれば以下のような場所にインストールされるはず。。。

# pecl install apc
# ll /usr/local/php/lib/php/extensions/no-debug-non-zts-20090626/
合計 728
-rw-r--r-- 1 root root 741397  4月 20 17:32 2012 apc.so

php.ini変更

# vi /usr/local/php/lib/php.ini
>> 以下の部分を弄っておけばとりあえずなんとか動くはずです

> 変更
short_open_tag = Off(or On)
extension_dir = "/usr/local/php/lib/php/extensions/no-debug-non-zts-20090626"
max_execution_time = (適宜)
memory_limit = (適宜)
date.timezone = Asia/Tokyo
session.save_path = "/var/lib/php/session"
mysql.default_socket= /var/lib/mysql/mysql.sock
mysqli.default_socket= /var/lib/mysql/mysql.sock
pdo_mysql.default_socket= /var/lib/mysql/mysql.sock

> 追加
[apc]
extension=apc.so

上記にある通り、session.save_pathを変更した場合、当然そのディレクトリを作成してApacheで読み書きできるようにしておかないとセッション管理が出来ませんのでご注意を。

PHPがちゃんとインストールされてるか確認

# php -v
PHP 5.3.10 (cli) (built: Apr 20 2012 17:24:33) 
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2012 Zend Technologies

# php -m
[PHP Modules]
apc <- あるか確認
bz2
Core
ctype
curl
date
dom
ereg
fileinfo
filter
gd
hash
iconv
json
libxml
mbstring
mcrypt
mysql
mysqli
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
pdo_sqlite
Phar
posix
Reflection
session
SimpleXML
SPL
SQLite
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
zip
zlib

[Zend Modules]

Apacheの設定にPHPの設定を入れる

mod_phpのロードは(以上のようにソースから入れても)自動的にhttpd.confに入るようなのですが、AddTypeの設定がないとPHPが動かないので入れておきます。

AddType application/x-httpd-php .php

以上のような設定でLAMP環境が構築できました。めでたしめでたし。

*1:もしかしたらリポジトリの違いによる物かもしれない。その辺はよく知りません

*2:ソースがないとインストールできないプラグインがあったりするのでバージョンがちゃんとあったソースが欲しい。。。

続・間違いだらけの負荷対策

事の始まり

3月に入ってから「斉藤さん」のWebサーバーのピークタイムでのアクセス数が秒間180アクセスを境に動かなくなり、しまいには一部アクセスが固まって動かなくなるという現象が発生した。

Apacheのエラーログを見ると、データベースサーバーに接続できていない旨が出ていたため、その辺を念頭に置いていろいろと探ってみたのだが・・・。

ソケット数の限界?

netstat -ant」と打つと、その時点でのTCPコネクションの一覧が出る。
ピーク時にWebサーバー側は30000前後、DBサーバー側は8000前後。
HandlerSocketのポート(標準では9998と9999)に絞ってみてみると、Webサーバー側は8000前後なのに対し、DBサーバー側は500前後。
明らかにWebサーバーのコネクション数がおかしい。
MySQLのコネクション数は問題なさそうなので、HandlerSocketのソケットが開けなくてパケットがドロップしてる?と想像して、/etc/sysctl.confにいろいろ設定を書いてみるが駄目。

iptables

そういえばカーネル周りの設定をしておきながらカーネル周りのログを見ていなかった、と思い/var/log/messageを見てみると以下のようなログが出ていた。

Mar  8 02:57:26 api kernel: ip_conntrack: table full, dropping packet.
Mar  8 02:57:38 api kernel: printk: 346 messages suppressed.

これを元にググってみると、どうやらiptablesに関係するエラーらしいということが分かった。
ちなみに、ニフティクラウドのサーバーは建てた時点で外部ネットワークに晒されるようになっているため、iptablesによるポートフィルタリングがデフォルトで設定された状態*1である。
この、iptablesはパケットを解析する際に/proc/net/ip_conntrackというファイルにその情報を記録するのだが、テーブルの上限数が設定されており、それを超えてしまうとパケットを落としてしまうらしい。
現在のiptablesのテーブルの項目数(?)とその限界は以下のように確認できる。

# 最大
$ cat /proc/sys/net/ipv4/ip_conntrack_max
65536
# 使ってる項目数
$ cat /proc/sys/net/ipv4/netfilter/ip_conntrack_count
63642

うおおおおテラギリギリスwwwwwwww
というわけで、そいつを上げればいいんだなと思い、両方のサーバーの/etc/sysctl.confに

net.ipv4.ip_conntrack_max = 131072

を設定してApacheBenchを動かしてみると、今まで途中で止まっていたベンチがスムーズに動くようになった。

まとめ

  • それなりのアクセスがあるWebサーバー・DBサーバーではiptablesがネックとなる場合がある。
    • サーバー建てた時点であるいはアクセス数が増大してスケールアップした時点で、「net.ipv4.ip_conntrack_max」の設定をチェックすべし。
    • 目安は搭載メモリ数(MB)*64くらい

もしかして・・・

(アクセス裁けなかったの)Nginxのせいじゃない・・・!?

*1:デフォルトでは22(つまりSSH)だけを許可する設定だった気がする

Nginxで2週間ほど『斉藤さん』のサーバーサイドを運用してみて

考察

  • 同時アクセス数が大きくなると(感覚では秒間150〜160前後)後ろが詰まってアクセスを取りこぼすことがある。下のグラフの0時前後(23時ちょうどと0時過ぎくらい)に注目。グラフが途切れている部分はアクセスが集中し、munin-nodeがステータスページへアクセスしても結果がうまくとってこれなくなっている(という個人的な解釈ですが・・・)。

  • Apacheでも同様なことがあったものの、(同スペックのサーバー環境下でも)もうちょっと耐えていたはず*1
  • あと、Apacheの時に比べてアクセスが集中し始めると途端にloadtimeが長くなりがちになることもちょっと気になる。

  • メモリ消費はキャッシュが一定量づつたまっていく。実用上問題はなさそうだが、なんとなく気持ち悪い増え方。
  • PHP-FPMをバカみたいに起動しているせい*2か、メモリのコミット量が半端じゃない。

  • 今後もアクセスが増加していったときに、Apacheであれば原理上はメモリをだんだん増やしていって、コア数も適宜増やしていく・・・みたいなスケールアップ策がとれそうだけど、Nginxは正直どっちを先に増やせばいいのか分からないし、増やしたところで本当に性能が上がるのか未知数*3
  • Webサーバーの設定とAPサーバーの設定*4が分かれていてメンテナンスが結構めんどい。

結論

結論としては「動的な処理にはこの組み合わせはあんまり向いてないんじゃないか???」ということになりました。
というわけで、結局WebサーバーをApacheに戻しました。

ただ、静的なもの(HTMLとか画像ファイル)は適切なキャッシュの設定をすれば爆速ですし、動的なものでも秒間100アクセスレベルくらいならWebサーバー部分のメモリ消費を抑えて、その代わりに(同じサーバーに入れている)MySQL等のメモリ消費量を増やす、という策が取れそうでいいかもしれません。
何だかんだ言って、適材適所ですね。

おまけ

PHP-FPMはこんな感じで推移してました。

『斉藤さん』のウィークリーでのアクセス数のグラフ。
他のサービス同様、平日・祝日共に「18時前後」「0時前後」に大きな山ができています。学生さんの昼休みの時間帯と思われる「(平日)12時〜13時」のあたりにも小さな山ができているのが特徴的です。

*1:その代わりLoad-averageがすごいことになりますが・・・

*2:php-fpm.confの中の「pm.max_children」を450に設定していた。ぶっちゃけどれくらいが適切な値なんでしょう???

*3:調べ方が悪いだけなのかもしれませんが、Apacheに比べるとそういう情報があんまり転がってないっすよね・・・

*4:ここではNginxとPHP-FPMの設定

お手軽メッセージキュー「Kestrel」を使ってみる

いきさつ

ついに『斉藤さん』が北米・UKなどで公開されたらしいです。
海外ではウケるのかどうか未知数ではありますが、さらにアクセス数が増えると思うとgkbrな日々です。

今のところWebサーバーのプロセスが比較的スムーズに処理されているようなので問題ないのですが、「ある程度時間のかかる処理で、その処理の結果を返す必要がないもの*1」の割合が多くなってきた場合、処理の待ち時間が長くなってしまい、Webサーバーでのプロセスが溜まってしまう、というようなことが起きてしまう懸念点があります。

そういう処理ってWebサーバー側ではやることやったらレスポンスを返して、時間のかかる処理自体はバックエンドでやりたいですよね?
まあ、いま流行りの「非同期処理」というやつですね。

非同期処理をする場合はいわゆる「この処理をこういう条件でやってね!」→「はーい!わかりましたー!」というメッセージングのやり取りが必要になるので、Memcachedプロトコルを話せるお手軽なメッセージキューサーバー「Kestrel」を導入することにしました。

インストール

KestrelはScalaで作られているそうなので、Javaが動く環境なら公式サイトからビルド済みのプログラムファイル一式を持ってきて、適当な場所に置いてしまえば、あとは普通にjarファイルを実行するだけで使えます。

そして、常駐させておきたいのでデーモン化するためにシェルスクリプトをごりごり書く・・・のでもいいのですが、最近は常駐プログラムを管理するときに「Supervisor」というプロセス管理ツールを使うのが流行りのようなので、そっちでKestrelを常駐させるように設定します*2
ついでに、タスクワーカー(タスクを処理するプログラム)もSupervisorで常駐させてしまうといい感じです。

詳しくはここを見ればだいたいわかります。

ハマった点

PHPMemcached拡張(pecl-memcached)は、セットしたいデータが100バイトを超えると自動的に圧縮をかけるらしい。
普通にMemcache使う場合は自動的に解凍してくれるので問題ないが、Kestrelでは解凍してくれないので圧縮を無効化する必要がある。

その他

配列とかを入れても自動的にシリアライズ・デシリアライズしてくれますが、Msgpackとかが使える環境であれば、それをつかって手動でシリアライズ・デシリアライズしたほうが良さげ(多分そっちのほうが速い)。

サンプル(タスクキュー送信)

<?php
$mem = new Memcached;
$mem->addServer(KESTREL_HOST, 22133);
// セットする際に自動的に圧縮しないようにする
$mem->setOption(Memcached::OPT_COMPRESSION, false);

$data = array('key'=>'value');
$data = msgpack_pack($data);
// 「message」キューのタスクをセット($dataの中には処理に必要なデータを入れる)
$mem->set('message', $data);

サンプル(ワーカープロセス)

<?php
$mem = new Memcached;
$mem->addServer(KESTREL_HOST, 22133);

for (;;) {
	// 「message」キューのタスクがあるか2秒待つ
	$result = $mem->get('message/t=2000/open');
	if ($result !== false) {
		$result = msgpack_unpack($result);
		// ここで$resultに応じて処理をする
		// 下記はAndroidのPush通知を送る処理の例
		$registrationId = $result['registration_id'];
		$collapseKey = $result['collapse_key'];
		$data = $result['data'];

		$header = array();
		$header[] = "Content-Type: application/x-www-form-urlencoded";
		$header[] = "Authorization: GoogleLogin auth=" . GOOGLE_C2DM_TOKEN;

		$post = array();
		$post['registration_id'] = $registrationId;
		$post['collapse_key'] = urlencode($collapseKey);
		foreach ($data as $name => $d) {
			$post['data.' . $name] = urlencode($d);
		}
		$postf = http_build_query($post, '&');

		$ch = curl_init(GOOGLE_C2DM_URL);
		$options = array(
			CURLOPT_RETURNTRANSFER => TRUE,
			CURLOPT_FAILONERROR    => TRUE,
			CURLOPT_FOLLOWLOCATION => TRUE,
			CURLOPT_POST           => TRUE,
			CURLOPT_HTTPHEADER     => $header,
			CURLOPT_POSTFIELDS     => $postf,
			CURLOPT_HEADER         => TRUE,
			CURLOPT_TIMEOUT        => 15,
			CURLOPT_CONNECTTIMEOUT => 15,
		);

		curl_setopt_array($ch, $options);
		curl_exec($ch);
		curl_close($ch);

		$result = $mem->get('message/close');
	}
}

*1:具体的にはAppleGoogleのサーバーにクエリを投げる必要があるPush通知などの処理

*2:『斉藤さん』『POMPA』のマッチングプログラムもC++で書かれた自作プログラムなのでSupervisorで起動監視しています

ディスクの中にあるディレクトリをサイズ順に並べるPHPスクリプト

セーフモードが有効だと多分動きません。

<?php

exec('du -Sh /', $return);

foreach ($return as $data) {
	$data2 = explode("\t", $data);
	
	$size = $data2[0];
	$unit = substr($size, -1);
	$size = substr($size, 0, -1);
	
	switch ($unit) {
		case 'G':
			$size2 = $size * 1000 * 1000;
			break;
		case 'M':
			$size2 = $size * 1000;
			break;
		default:
			$size2 = $size;
			break;
	}
	
	$sizeList[] = array($size2, $data2[0], $data2[1]);
}


usort($sizeList, 'sort_for_number');

var_dump($sizeList);

function sort_for_number($a, $b) {
	return ($a[0] < $b[0]) ? TRUE : FALSE;
}

こんな感じで表示される。膨大な量が出てくるはずなのでmoreとかlessとかを繋げといたほうが良さげ。

array(25543) {
  [0]=>
  array(3) {
    [0]=>
    float(5300000)
    [1]=>
    string(4) "5.3G"
    [2]=>
    string(14) "/var/lib/mysql"
  }
  [1]=>
  array(3) {
    [0]=>
    float(1700000)
    [1]=>
    string(4) "1.7G"
    [2]=>
    string(9) "/home/xxx"
  }
  [2]=>
  array(3) {
    [0]=>
    float(1100000)
    [1]=>
    string(4) "1.1G"
    [2]=>
    string(41) "/home/www/xxx.xxx.com/logs/20120214"
  }
  [3]=>
  array(3) {
    [0]=>
    int(678000)
    [1]=>
    string(4) "678M"
    [2]=>
    string(32) "/home/www/xxxx.xxx.com/logs"
  }
  [4]=>
  array(3) {
    [0]=>
    int(417000)
    [1]=>
    string(4) "417M"
    [2]=>
    string(34) "/home/www/xxxxx.xxx.com/logs"
  }
...(以下略)