EC studio EC studio 技術ブログ

PHPで長時間かかる重い処理を行うとき、
マルチスレッドで動かせたらなぁ!と思うことがよくあります。

マルチスレッド処理を行うことで、時間のかかる処理を
複数同時に並列実行でき、高速化することができます。
(特に通信処理などで遅延がある場合に有効です)

PHPにはpcntl_forkというプロセスをフォーク(複製)することが
できる関数がありますが、これはPHPをCGIモードで
動かしている場合にのみ有効です。

通常はPHPをApacheモジュールとして動作させていることが
多いので、これが使えないケースが多々あります。

他には、system関数などでシステムコールを行い、

  1. system('php -f test.php > /dev/null &');

など & を使って実行するという方法もありますが、
これもphpがCGI版として実行できなければいけません。

システムコールで呼ぶプログラムをPerlなどで
書けばいいのですが、できればPHPで全部統一したいものです。

そこでいろいろ実験や研究をしていたのですが、
良さそうな方法を見つけました。

curl_multiを使って並列処理を行う

PHP5になってから、curl_multi_*という関数群が
使えるようになりました。
curlの関数はURLからソースを取得するなどの用途で
使っている方も多いと思いますが、
PHP5から並列にソースを取得するcurl_multi系の
機能が追加されています。

これを使ってAPIを複数同時にたたいたりできるのですが、
並列実行したいPHPをたたくことで並列化が可能です!

サンプルを書いてみました。

  1. <?php
  2.     header('Content-type:text/html; charset=UTF-8');
  3.  
  4.     //並列実行したいPHP
  5.     $url_list = array(
  6.         'http://localhost/sleep.php?pid=1',
  7.         'http://localhost/sleep.php?pid=2',
  8.         'http://localhost/sleep.php?pid=3',
  9.         'http://localhost/sleep.php?pid=4',
  10.         );
  11.     //開始時間取得
  12.     $time = time();
  13.    
  14.     //実行
  15.     $res = fetch_multi_url($url_list);
  16.    
  17.     //結果出力
  18.     echo '実行結果:<pre>';
  19.     print_r($res);
  20.     echo '</pre>';
  21.    
  22.     //実行時間
  23.     echo '--<br />time:'.(time() - $time).' sec';
  24.    
  25. /**
  26. * 複数URLを同時に取得する
  27. *
  28. * @param array $url_list URLの配列
  29. * @param int $timeout タイムアウト秒数 0だと無制限
  30. * @return array 取得したソースコードの配列
  31. */
  32. function fetch_multi_url($url_list,$timeout=0) {
  33.     $mh = curl_multi_init();
  34.  
  35.     foreach ($url_list as $i => $url) {
  36.         $conn[$i] = curl_init($url);
  37.         curl_setopt($conn[$i],CURLOPT_RETURNTRANSFER,1);
  38.         curl_setopt($conn[$i],CURLOPT_FAILONERROR,1);
  39.         curl_setopt($conn[$i],CURLOPT_FOLLOWLOCATION,1);
  40.         curl_setopt($conn[$i],CURLOPT_MAXREDIRS,3);
  41.        
  42.         //SSL証明書を無視
  43.         curl_setopt($conn[$i],CURLOPT_SSL_VERIFYPEER,false);
  44.         curl_setopt($conn[$i],CURLOPT_SSL_VERIFYHOST,false);
  45.         
  46.         //タイムアウト
  47.         if ($timeout){
  48.             curl_setopt($conn[$i],CURLOPT_TIMEOUT,$timeout);
  49.         }
  50.        
  51.         curl_multi_add_handle($mh,$conn[$i]);
  52.     }
  53.    
  54.     //URLを取得
  55.     //すべて取得するまでループ
  56.     $active = null;
  57.     do {
  58.         $mrc = curl_multi_exec($mh,$active);
  59.     } while ($mrc == CURLM_CALL_MULTI_PERFORM);
  60.    
  61.     while ($active and $mrc == CURLM_OK) {
  62.         if (curl_multi_select($mh) != -1) {
  63.             do {
  64.                 $mrc = curl_multi_exec($mh,$active);
  65.             } while ($mrc == CURLM_CALL_MULTI_PERFORM);
  66.         }
  67.     }
  68.    
  69.     if ($mrc != CURLM_OK) {
  70.         echo '読み込みエラーが発生しました:'.$mrc;
  71.     }
  72.    
  73.     //ソースコードを取得
  74.     $res = array();
  75.     foreach ($url_list as $i => $url) {
  76.         if (($err = curl_error($conn[$i])) == '') {
  77.             $res[$i] = curl_multi_getcontent($conn[$i]);
  78.         } else {
  79.             echo '取得に失敗しました:'.$url_list[$i].'<br />';
  80.         }
  81.         curl_multi_remove_handle($mh,$conn[$i]);
  82.         curl_close($conn[$i]);
  83.     }
  84.     curl_multi_close($mh);
  85.    
  86.     return $res;
  87. }

sleep.phpの中身は

  1. <?php
  2.     sleep(5);
  3.     echo 'pid='.$_GET['pid'];

こんな感じで、一つにつき5秒のsleepをかけてます。
(GET値がちゃんと渡っているか確認する為にechoしています)

このsleep.phpを4回実行します。
逐次実行すれば 5秒 x 4回 = 20秒 ほどかかるはずです。

結果は・・・

  1. 実行結果:
  2.  
  3. Array
  4. (
  5.     [0] => pid=1
  6.     [1] => pid=2
  7.     [2] => pid=3
  8.     [3] => pid=4
  9. )
  10.  
  11. --
  12. time:5 sec

バッチリ5秒で実行されてます! :) (感動)

DNS逆引き処理を並列実行してみました

実際にIPアドレスをホスト名にDNS逆引きする処理を
並列処理してみました。

アクセスログから抽出した重複しないIP1,000個に対して、
phpのgethostbyaddr関数を実行していきます。
(キャッシュの影響を除外する為に2回目の計測結果です)

実行結果
-----------------------------------
並列なし:124秒
10スレッド並列:33秒
-----------------------------------

すごい!
DNSが逆引きできない場合タイムアウト待ちをするので
さすがに十分の一とはいきませんでしたが、
それでもかなり高速化できています。

実運用に際して

実運用の際は、

  1. //並列実行したいPHP
  2. $url_list = array(
  3.     'http://localhost/something.php?from=0&to=100',
  4.     'http://localhost/something.php?from=101&to=200',
  5.     'http://localhost/something.php?from=201&to=300',
  6.     'http://localhost/something.php?from=301&to=400',
  7.     );

こんな感じでタスクを割り振るとか、
スレッドの合計並列数とスレッド番号を渡して
自動でタスクを分割するようなロジックをかけばいいですね。

もちろん長時間動かす時はset_time_limitの設定も忘れずに。
ignore_user_abortを使えばバックグラウンド処理も可能です。

あとは意図しないアクセスを防ぐために
IPアドレスなどでブロックしておくといいと思います。

ソースコードは各自の責任範囲で自由にお使いください。


関連した記事:

この記事へのコメント

PHPでマルチスレッド処理を行う方法を探してここにたどり着きました。
ここに記載されていることは、自分の得たい情報そのものであり、非常に感謝しています。

しかしながら、ひとつ疑問に思う点があり質問させていただきます。
それは、上の処理方法ですとsleep.phpからデータが返ってくるときは文字列データになってるのではないかということです。
数値データのままデータを返すことはできないのでしょうか?

よろしくお願いします。

投稿者: キレルーヤ | 2008/10/11 土曜日 4:55:25

curlで取得できるデータは全て文字列型になります。
もし数値や配列、オブジェクトなどで受け取りたい場合は、sleep.php側で、echo する値をserialize関数で文字列にし、curlを呼び出す側でunserializeしてやればそのまま受け取ることができますよ

投稿者: Masaki | 2008/10/16 木曜日 19:43:16
投稿者
人気のエントリー
カテゴリー
最近のエントリー
アーカイブ
Copyright© ChatWork, All Rights Reserved. secured by ESET.