Edited at

バッチ処理の時に使う、ロックファイルを使ったPHPの排他処理について

More than 1 year has passed since last update.

今後の自分に向けて、備忘録代わりに。

使用しているバージョンはPHP7ですが、5.6以下でも問題はないと思います。たぶん。


STEP1:ロックファイルを使った排他制御の方法

PHPにおいて、バッチを使ってメンテしたり、データを更新したり、といった日次や月次の処理を行う業務は多いけど、その時についてまわるのが、他のバッチや自身のバッチの多重起動や人為的アクセスなどをどうやって排他するかだと思います。もっともオーソドックスなやり方は、ロックファイルを使った以下のやり方でしょうか。


  • バッチ開始時、ロックファイルがあるかどうか確認

  • なければロックファイルを作成してバッチ処理に入る

  • バッチ実行終了時にロックファイルを削除

コードにすると、以下の様な感じになると思います。単純です。作成・削除・確認しかありません。


class exclusiveControl {

/**
* ロックファイルを作成する
* @param unknown $bid
*/

public function make_lock_file($bid) {
$dir = __DIR__;
$fpath = $dir.'/tmp/'.$bid.'.lock';
touch($fpath);
}

/**
* ロックファイルを削除する。
* @param unknown $bid
*/

public function delete_lock_file($bid) {
$dir = __DIR__;
unlink($dir.'/tmp/'.$bid.'.lock');
}

/**
* ロックファイルの存在確認
* @param unknown $bid
* @return true = 処理続行、false = エラーを出して中断
*/

public function check_lock_file($bid) {
$dir = __DIR__;
//ファイルパス指定
$fpath = $dir.'/tmp/'.$bid.'.lock';

//ファイルチェック
if (file_exists($fpath)) {
return false;
}
return true;
}
}

バッチが1本しかない場合はロックファイルも適当な名前でもいいかもしれません。ただ、複数本のバッチが同時に動くケースがあったりすると、何のバッチによって作成されたロックファイルなのかを区別しなければなりません。

一括で「1本でもバッチが動いてたら他は全部ダメー!」というのなら簡単ですが、各バッチごとに排他をかけるケースに対応するためにはロックファイルが重複しないようにする必要があります。

そのため、各バッチ固有のID(今回はこちらを$bidと呼称)や名称などをロックファイルの名前につけるようにします。こうすることで、どのバッチが動作中なのかもエクスプローラーやコンソールから確認するだけでわかります。


STEP2:システムやプロセスの不慮の終了に対応する

ロックファイルを使った排他制御において、問題になりうる点がこれです。バッチが何らかの理由(ロックファイル削除メソッドを経由しない文法上のエラーや、マシンのシステムそのものの終了など・・・)により強制終了した場合、ロックファイルだけ残ってしまい、次回起動時に、”正しく起動しようと思ったのに排他がかかってしまった!”ということが想定されます。いちいちフォルダを参照してファイル削除して・・・を使用者にさせるのは流石に無責任がすぎるでしょう。

やり方はいろいろある・・・と思いますが、今回採用したのは

バッチ動作時のプロセスIDが一致するかどうかまでチェックする

という方法です。

強制終了した場合に残るロックファイルはただのゴミファイルなので削除しなければなりません。ゴミかどうかを見分けるためには、バッチを起動した際のPHP.exeのプロセスIDが、ロックファイルを作った時のプロセスIDと一致するかどうか見るようにするのです。

都合の良いことに、プロセスIDは、一つの処理が開始して終了するまでが不変なので、強制終了に対してかなり高い精度で対応することができます。

手順としては、先述したロックファイルの一連の処理に以下を追加します。


  • バッチ開始時、ロックファイルがあるかどうか確認


    • ロックファイルがあったら、ロックファイルのプロセスIDを読み取り、PHP.exeのプロセスIDと一致するか確認。なかったらバッチ実行処理へ


      • 一致したら、現在動いているバッチなので実行しないでエラーを返す

      • 不一致なら、そのロックファイルは異常終了の残骸なので、削除してバッチ実行処理へ





  • ロックファイルを作成してバッチ処理に入る


    • その際、ロックファイルにPHP.exeのプロセスIDを書き込み



  • バッチ実行終了時にロックファイルを削除

フローチャートにすると、以下の図のような感じです。

wiita.jpg

本案件では諸処の事情により、プロセスIDの確認処理が他でも必要になるところがあったので、その処理だけ別クラスに切り出しています。


class exclusiveControl {

/**
* ロックファイルの作成とプロセスIDの書き込み。バッチ実行開始時
* @param unknown $bid
*/

public function make_lock_file($bid) {
$dir = __DIR__;
$fpath = $dir.'/../tmp/'.$bid.'.lock';
touch($fpath);

//プロセスIDを取得し、ファイルに書き込む
$pid = getmypid();
file_put_contents($fpath, $pid);
}

/**
* ロックファイルの削除。バッチ終了時
* @param unknown $bid
*/

public function delete_lock_file($bid) {
$dir = __DIR__;
unlink($dir.'/../tmp/'.$bid.'.lock');
}

/**
* ロックファイルの存在確認、およびプロセスIDに基づく確認
* @param unknown $bid
* @return true = 処理続行、false = エラーを出して中断
*/

public function check_lock_file($bid) {
$dir = __DIR__;
//ファイルパス指定
$fpath = $dir.'/../tmp/'.$bid.'.lock';

//ファイルチェック→プロセスIDチェック
if (file_exists($fpath)) {
$pid = file_get_contents($fpath);
//引数のプロセスIDがphpプロセスのIDと一致するか確認
if (!Process::check_pid($pid)){
//不一致だったら削除して処理続行
self::delete_lock_file($bid);
return true;
} else {
//一致してたら排他する
return false;
}
}
return true;
}
}

class Process{

/**
* 引数のプロセスIDがphp.exeによるものかどうかチェックする。
* @param unknown $pid
* @return 引数と現実行分のプロセスIDが一致 = false
*/

public function check_pid ($pid) {
if ($pid === '') {
return true;
}

$imagename = "php.exe";
$tasklist = `tasklist /nh`;

if (preg_match_all('/(.*?)\s+(\d+).*[^\n]?/', $tasklist, $match)) {
//preg_matchの結果からphpのプロセスを検索し、
//見つかったらそのプロセスIDを引数のプロセスIDと比較
foreach ($match[1] as $key => $value) {
if($value == $imagename){
$active_pid = $match[2][$key];
if($active_pid == $pid){
return false;
}
} else {
continue;
}
}
return true;
}
return true;
}
}

getmypid()は、phpの現在のプロセスIDを取得する関数です。これを作成したロックファイルに書き込むことで、起動時のPHPのプロセスIDがバッチ終了までデータとして保持されます。

「phpのプロセスIDを見るって簡単に言うけど、じゃあドコで見るの?」という話ですが、その際にはtasklistを使います。

(参考→ http://wa3.i-3-i.info/word12514.html )

これを使うことで、現在使っているタスク一覧を取得することができます。

上記の条件でtasklistを検索すると、格納される$match配列には、以下の様な感じで入ります。本当は実行中のアプリケーションがもっと表示されますが、ここでは割愛しています。


$match[0] //タスク一覧的な
Array
(
[0] => System Idle Process 0 Services 0 24 K
[1] => System 4 Services 0 15,680 K
[2] => smss.exe 356 Services 0 1,416 K
[3] => csrss.exe 504 Services 0 7,016 K
)

$match[1] //プロセスの名前
Array
(
[0] => System Idle Process
[1] => System
[2] => smss.exe
[3] => csrss.exe 
  ・
  ・
  ・
  [10] => php.exe
)

$match[2] //プロセスID(数値はあくまでも例です)
Array
(
[0] => 0
[1] => 4
[2] => 356
[3] => 504
  ・
  ・
  ・
  [10] => 1840 //ほしいやつ
)

$match[1]$match[2]の番号は紐付いていますので、

例えば上のように、php.exe$match[1][10]に入っていた場合、phpのプロセスIDは、$match[2][10]にいる値、ということになります。

それこそが、今回チェックすべきプロセスID。

あとはそれをロックファイルに書かれたプロセスIDと比較してあげれば、そのバッチが動いているなう、なのかどうかがわかります。その結果に応じて排他するか否かを決めてあげればいいわけですね。


おわりに

PHPでの排他制御の手法は、今回のロックファイルだけでなく、公式に用意されているflock関数であるとか、セマフォであるとかいろいろありますが、それぞれが一長一短みたいで、どれが主流、というのはないようです(自分が知らないだけかもしれませんが)。

ロックファイルによる排他の手法は、上記のような、強制終了などに脆いという部分が敬遠されがちな点なのかなと思いますが、今回のようにプロセスを参照する処理を追加すると排他処理の精度は格段に向上しますし、処理速度がほとんど変わらない点は強みと言っていいのではないかなと思います。理屈さえある程度抑えてしまえば簡単に書けますので、PHP触ったばかりでよくわかんないんだけど、という方でもとっつきやすいかなと考えたりします。

ただ、これはどの手法を使った場合にも言えることですが、

多くの場合、バッチ処理は裏で実行することになるので、バッチ実行中は画面上でもバッチ実行のsubmitボタン押させないようにするとか、起動中のバッチをチェックできないようにするとか、UI上で分かるような工夫が必要不可欠です。いちいちログ見てね、は不親切なのかなと。(特にwin環境だとね・・・)

また、もちろんこれが十全というわけでは勿論ないので、他の手法についてもきちんと勉強したいと思います。また、自分もPHPを触り始めてまだ1年と経っておりませんので、「ここがまずいのでもっとこうしたほうがいい」などがあれば是非ご教授頂きたいです。おわり。