Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

PHPのマルチスレッドプログラミングを使ってシステム処理を爆速化するお話し

パーフェクトPHP (PERFECT SERIES 3)

パーフェクトPHP (PERFECT SERIES 3)

Intro

会社に入社して5年が経ち、4月から新しい部署で働いています。最近はプログラミング言語の学習としてC++/JAVA/Perl/R言語、理論の勉強として機械学習をやっています。平行して少しずつ勉強しているのでblogの記事内容も多種多様になってきています(笑)。新しい事をやる時は一つの事に集中して勉強したいのですが、直近は業務で成果を残さないと相手にされないので学習がforkします。ということで強引な繋ですが今日はforkの話をします。業務で必要になったPHPの処理爆即化に向けてマルチスレッドプログラミングを試してみました。pcntlにより親プロセスから子プロセスを作成してforkさせます。出来たところまでの成果を以下にまとめました。
PHP: PCNTL - Manual はてなブックマーク - PHP: PCNTL - Manual

Source Build

pcntlというPHPのマルチスレッドプログラミングはdefaultでは使えないようです。phpソースをbuildする時に--enable-pcntlを付ける必要があります。以前にmcrypt関数を使う時にもオプションをつけて--with-mcrypt=/usr/local/libを付けたりlibmcryptをインストールしないといけないことがあったので、標準で使えるようにして欲しいですね。

<?php

$pid = pcntl_fork();
if ($pid == -1) {
     die('fork できません');
} else if ($pid) {
     // 親プロセスの場合
    echo "parent process \n";
    pcntl_wait($status); // ゾンビプロセスから守る
} else {
    // 子プロセスの場合
    echo "child process \n";
}
$ php pcntl.php
Fatal error: Call to undefined function pcntl_fork()

次にphpソース取得とコンパイル、インストール手順を書きます。

$ wget http://jp.php.net/get/php-5.4.3.tar.gz/from/this/mirror
$ gunzip php-5.4.3.tar.gz
$ tar xf php-5.4.3.tar
$ cd php-5.4.3
$ ./configure --enable-pcntl
$ make && sudo make install

再度上のサンプルプログラムを実行します。うまくProcessがforkされたようです。

$ php pcntl.php 
child process 
parent process 

Practice

Non Multi-threadとMulti-threadの処理ロジックの比較を行います。下ではsleep関数を使っていますがsleepが重たい処理と見なした例です。

Non Multi-thread

直列で処理を実行します。当然ながら30secほど時間がかかります。

<?php

$t1 = microtime(true);

sleep( 10 );
echo "Complete No1 \n";

sleep( 10 );
echo "Complete No2 \n";

sleep( 10 );
echo "Complete No3 \n";

$t2 = microtime(true);
$process_time = $t2 - $t1;

echo "Process time = " . $process_time . "\n";
Complete No1 
Complete No2 
Complete No3 
Process time = 30.010495901108
Multi-thread

複数の子プロセスを発生させて処理を並列化させます。プロセスをforkしているのでsleepを3カ所に入れても1カ所に入れた場合と変わりないと思います。これにより重たい処理を並列化することが可能になりました。処理が爆速化されます。ただし気をつけないといけないのが親Processが子Process全て終わる事を確認してから先に処理を進めないと無限Loopする可能性があります。ここは要注意です。しっかりとしたテストをしてから導入するようにしましょう。

<?php

$t1 = microtime(true);
$pcount = 3;
$pstack = array();
for($i=1;$i<=$pcount;$i++){
    $pid = pcntl_fork();
    if( $pid == -1 ) {
        die( 'fork できません' );
    } else if ($pid) {
         // 親プロセスの場合
        $pstack[$pid] = true;
        if( count( $pstack ) >= $pcount ) {
            unset( $pstack[ pcntl_waitpid( -1, $status, WUNTRACED ) ] );
        }
    } else {
        sleep( 10 );
        echo "Complete No$i\n";
        exit(); //処理が終わったらexitする。
    }
}
//先に処理が進んでしまうので待つ
while( count( $pstack ) > 0 ) {
    unset( $pstack[ pcntl_waitpid( -1, $status, WUNTRACED ) ] );
}

$t2 = microtime(true);
$process_time = $t2 - $t1;
echo "Process time = " . $process_time . "\n";
Complete No1
Complete No2
Complete No3
Process time = 10.086293935776

以下は駄目な例です。親プロセス処理が先に進んでしまい、子プロセスの終了とともに再度親プロセスの処理が実行されてしまいます。上で示したようにexitを使って子プロセスが終わったらそのプロセスを終了するような処理を入れても良いと思います。

<?php

$t1 = microtime(true);
$pcount = 3;
$pstack = array();
for($i=1;$i<=$pcount;$i++){
    $pid = pcntl_fork();
    if( $pid == -1 ) {
        die( 'fork できません' );
    } else if ($pid) {
         // 親プロセスの場合
        $pstack[$pid] = true;
        if( count( $pstack ) >= $pcount ) {
            unset( $pstack[ pcntl_waitpid( -1, $status, WUNTRACED ) ] );
        }
    } else {
        sleep( 10 );
        echo "Complete No$i\n";
    }
}

$t2 = microtime(true);
$process_time = $t2 - $t1;
echo "Process time = " . $process_time . "\n";
Complete No1
Process time = 10.013136148453
Complete No2
Process time = 10.020040035248
Complete No3
Process time = 10.035511016846
Process time = 10.039574146271
Complete No2
Process time = 20.015119075775
Complete No3
Process time = 20.022233009338
Complete No3
Process time = 20.063854217529
Complete No3
Process time = 30.069846153259

High-load

マシンがどれぐらChildProcessを生成できるのかを試してみました。当然ながら通常の処理ではあり得ないようなProcessを生成しています。実行するプログラムは上のMulti-threadでpcoutを標準入力から取得するように修正して色々な値で試してみます。Memoryのスペックは次の通りです。

$ cat /proc/meminfo
MemTotal:       767556 kB
MemFree:         41368 kB
Buffers:         24760 kB
Cached:         303132 kB
SwapCached:          0 kB
Active:         396132 kB
Inactive:       268276 kB
HighTotal:           0 kB
HighFree:            0 kB
LowTotal:       767556 kB
LowFree:         41368 kB
SwapTotal:      786424 kB
SwapFree:       786424 kB
Dirty:             104 kB
Writeback:           0 kB
AnonPages:      336528 kB
Mapped:          35700 kB
Slab:            34288 kB
PageTables:       9584 kB
NFS_Unstable:        0 kB
Bounce:              0 kB
CommitLimit:   1170200 kB
Committed_AS:   720716 kB
VmallocTotal: 34359738367 kB
VmallocUsed:      1244 kB
VmallocChunk: 34359735835 kB
HugePages_Total:     0
HugePages_Free:      0
HugePages_Rsvd:      0
Hugepagesize:     2048 kB
pcout = 100

pcount=3に比べて2秒ほど遅くなりましたが、loadaverageも高くなる事なく処理がさばけています。100Prcessを12秒でさばいています。

$ php pcntl.php 100 > result.txt

$ top
top - 02:11:15 up 6 min,  2 users,  load average: 0.08, 0.56, 0.33
Tasks: 209 total,   2 running, 207 sleeping,   0 stopped,   0 zombie
Cpu(s):  1.0%us,  0.3%sy,  0.0%ni, 98.0%id,  0.0%wa,  0.0%hi,  0.7%si,  0.0%st
Mem:    767556k total,   759340k used,     8216k free,    24520k buffers
Swap:   786424k total,        0k used,   786424k free,   310164k cached

$ cat result.txt
(略)
Complete No100
Complete No97
Complete No37
Complete No42
Complete No69
Process time = 12.1791908741
pcount = 300

pcount=100に比べてloadaverageがだいぶ高くなりました。ですが許容範囲かと思っています。300Prcessを15秒でさばいています。

$ php pcntl.php 300 > result.txt

$ top
op - 02:35:37 up 4 min,  2 users,  load average: 18.11, 5.24, 1.88
Tasks: 108 total,   2 running, 106 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.7%us,  0.7%sy,  0.0%ni, 98.1%id,  0.0%wa,  0.0%hi,  0.4%si,  0.0%st
Mem:    767556k total,   675656k used,    91900k free,    23728k buffers
Swap:   786424k total,        0k used,   786424k free,   258656k cached

$ cat result.txt
(略)
Complete No202
Complete No212
Complete No218
Complete No224
Complete No235
Complete No239
Complete No257
Complete No268
Process time = 15.838124036789
pcount = 1000

あり得ないforkの仕方ではあるとおもいますがpcout=1000ではload averageが急激に高くなりました。しかも処理が終わりきる前にProcess timeの結果が出力されてしまいました。しかしforkに失敗したprocessは一つもなかったようです。これ以上は試しません。

$ php pcntl.php 1000 > result.txt

$top
top - 02:21:38 up 17 min,  2 users,  load average: 138.01, 33.80, 11.48
Tasks: 108 total,   2 running, 106 sleeping,   0 stopped,   0 zombie
Cpu(s): 14.5%us, 46.6%sy,  0.0%ni, 31.8%id,  0.0%wa,  1.7%hi,  5.4%si,  0.0%st
Mem:    767556k total,   488932k used,   278624k free,    10872k buffers
Swap:   786424k total,        0k used,   786424k free,    74836k cached

$ cat result.txt
Complete No385
Process time = 32.895464897156
Complete No610
Complete No626
Complete No632
Complete No641
Complete No646
Complete No657

Other Example

Non Pcntl

APIとの通信効率をよくする実装例(1) curl_multi (Yahoo! JAPAN Tech Blog) はてなブックマーク - APIとの通信効率をよくする実装例(1) curl_multi (Yahoo! JAPAN Tech Blog)
Pcntl以外にもPHPでmulti-threadプログラミングをよく利用します。例えば上の例がそれでWebAPIなど直列でたたくと時間がかかりそうな場合は並列で処理するとその分処理時間が短縮されます。APIの数が多いほど効果が発揮されると思います。

Conclusion

この記事での内容をまとめます。

  • 重たい処理を直列で実行するのではなく、出来る限り並列化させましょう。
  • 並列化する場合はメモリの使用量について気をつけましょう。
  • 単純な親子関係のプロセスならばさほど問題になりませんが、複数の子Processを生成する場合、親Processでのstatus判定と親Processで子のresponse待ち状態をきちんと管理しないと無限Loopが発生する可能性があります。十分に動作テストをしましょう。
  • PHP本体側では子Processの生成制御を掛けていない様子。数が膨大になればmemory容量次第で必ず落ちそうです。