2011-04-14

PHPでpcntl_forkでふぉーくの薀蓄

えー、勢いあまってPHPでマルチプロセスを実装してみました。
ある意味血迷ってるといまさらながら思いますが、最初は・・・言語かえるのやだし、ちょこっとググッたらできそうな感じだったので。。。

もちろん、やっとちゃんと動きましたよ。
ええかなりちゃんとしてます。
でも、、、Cとかお流行のJAVAに書き換える日はもう見えてるんで。ええ。

ググれば出てくるさんぷるクリプト


大体、「PHP pcntl_fork」でぐぐればいっぱい使い方が見つかります。みつかるけど・・・これだけではなんだかようわからない。
PHP: pcntl_fork - Manual
//子プロセス生成
$pid = pcntl_fork();
if ($pid == -1)
{
  // fork失敗
  echo 'Failed forc process.';
  exit(1);
}
else if ($pid) 
{
  //親プロセス
  $mypid = getmypid();
  printf("mypid:%d pid:%d\n", $mypid, $pid);
  pcntl_waitpid($status);
}else{
  // 子プロセス
  $mypid = getmypid();
  printf("mypid:%d pid:%d\n", $mypid, $pid);
  exit(0);
}


実装したい場合って割と複雑なんですね。で、特にApacheと一緒に使うわけじゃないので、更にいろいろとあるわけです。(Apacheと一緒に使うならこれは使わないほうがいい。)

batch処理をするのにPHPを選んでる時点でちょっと負けた気がする。。。
ちなみに、batchで使うときのPHPはクライアントモードとかCGIモードとか言うみたいなのだけれど、本当かどうかは不明。

で、このクライアントモードでしか使わないので、いろいろな制限を無制限にしちゃったりしている。
その上でマルチプロセスで一気に平行処理をして歩いいというのが今回の目的。

実装する仕事


・リモートファイルの取得
・中身の集計
・各ジャンルごとにデータベース(MySQL)に集計結果を格納

この流れで平行で10個くらい走って間もらいます。ちなみに、直列でも可。
ただ1日は24時間しかなくて1個2時間かかってたらずーっと仕事してることになって効率が悪い。
マルチプロセスでそれぞれ仕事してくれたらまぁ3時間で全部終わるくらいになることを期待しているのん。

さて、さて。。結局はちら様のソースを元に作らせていただいた↓
PHPでの並列処理について - With-No-Parachute D-side

ただ、すんなりこのとおりでは動かなかった。
こちら様の前提は、PHP5.3.xで、うちはなんと、PHP5.2.x
そんなに大事ではないのだけれど、ちょっとかっこいいなぁって思っていた「無名関数」っていうのが使えないので
// 子プロセスで行う仕事
if (!is_array($this->work)) //外部クロージャが設定されている場合
{
  $function = $this->work;
  $function($arg);
}

これが使えない。もう一個の方は問題なく実行できたのでそちらのみ利用。しかし、両方書いておいて下さるなんて素敵だ。

あとは、ファイル分割をしたときのスコープがいろいろと変わってくるので、何度と無くテストをして動いてくれるまで持っていく。
このときに注意が必要なのは、シグナルハンドラのセット位置と、declare(ticks=1) {}の記述場所。

位置を間違えると期待通りの振る舞いにはならない。
あと忘れてはならないのが、MySQLのこと。こちら様の例にはMySQLの例は記載されていないので、やはりまたぐぐる。
次に参考にさせていただいたのがこちら様↓
小泉守義のPHPソースコードウォッチ - pcntl_fork() - fork() で複製される fd に注意セヨ....

こちらにも書かれているとおり、接続する場所がとても重要。親と子の間でデータが渡せるとはいえ、コネクションはNGなのだ。
なぜか共有すると、誰かの処理が終わった時点でみんなのコネクション(オブジェクト?)が破棄されているような振る舞いになる。
あの大嫌いな。。。。。あれが出る。
Lost connection to MySQL server
MySQL server has gone away
とかとか。毎回同じのが出るわけでもないし、ある種ハマルこと間違いない(--

ここはひとつ素直におまじないだと思って、子プロセスでコネクションを張ろうと心に誓うのがよい。
が、しかーし。。更に大きな落とし穴がある。
こちら様や、ほかの方々の例をみるとみなさま全てmysql_connect()を使っていらっしゃる。実はこれが大きなヒント!

自分、PDOっス(--

と思っては見たものの、中身はmysql_connect()なわけだから、何も考えずに実装。
意外と問題なくて、mysql_connect()のところを $dbh = new PDO(); 煮してあげればO.K.

が、しかーし、地獄はここから始まった。。。苦節2weekぐらいかも。なれないstraceとかして原因を探るとか(意味わかんないから眺めて勘だより)ぐぐりまくりましたよ。
もちろんBugも疑い、「バグジャン」っていってる外人さんに「バグじゃないよ。ちゃんと使い方みな」なんて書いてるPHPの中の人らしきコメントを読んだり。。。
マジ疲れました。
で、結局。PHPの中の人が正しい (*'▽')db('▽'*) ねー。

さんぷるクリプト


なんか切れるほう
try{
  $pdo = new PDO( $dns, $user, $pass, array(
    PDO::ATTR_PERSISTENT => true,
    PDO::MYSQL_ATTR_LOCAL_INFILE => true,
    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET CHARACTER SET ujis;'
  ));
  $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
}catch( PDOException $e ){
  printf("PDOException:%s\n",$e->getMessage());
  exit(1);
}


切れないほう
try{
  $pdo = new PDO( $dns, $user, $pass, array(
    PDO::MYSQL_ATTR_LOCAL_INFILE => true,
    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET CHARACTER SET ujis;'
  ));
  $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
}catch( PDOException $e ){
  printf("PDOException:%s\n",$e->getMessage());
  exit(1);
}

どっちも繋がるのだけど、切れるほうは割と始めのほうで切れる。それいご切れないのに、最初の何かがきれうる。
ずーっと悩んでたんですけどね、あれです、見ていただいたとおり。。。
子プロセスに仕事させたいなら持続的な接続ってやつはご法度なんです(-- mysql_pconnect() は、つかっちゃーいかんのです!

PHP: mysql_pconnect - Manual

1 番目の違いとして、この関数は接続時にまず 同じホスト、ユーザ名、パスワードを有する(持続的)リンクが すでにオープンされていないかどうかを調べます。
 それがみつかった場合、新規の接続をオープンする代わりに そのリンクの ID が返されます。

ほかの子プロセスのを掴んじゃったりするんじゃないかい・・・・

2 番目の違いは、スクリプトの実行が終了しても SQL サーバとの接続が 閉じられないということです。
そのかわりに、将来再利用されるために リンクがオープンされたままとなります(mysql_close() は、mysql_pconnect()によって確立されたリンクを 閉じません)。

けっこういっぱいプロセスがいるから、また変なのをつかんじゃうよね?

と、思うしだいでありました。
本当のところはわからないのだけれど、変えたら切れなくなったし。。。
まだまだ謎がおおいんですね。

お世話になったリンクのまとめ


PHP: pcntl_fork - Manual
PHP: mysql_pconnect - Manual
PHPでの並列処理について - With-No-Parachute D-side
小泉守義のPHPソースコードウォッチ - pcntl_fork() - fork() で複製される fd に注意セヨ....

system callのtraceについて - YutaKikuchiのTechBlog

mysql.php at PHPPDO - Free PHP Code