1. TOP
  2. コムセント 技術情報
  3. cron を PHP によって秒単位で実行する

cron を PHP によって秒単位で実行する

サーバで定期処理をする際、cron を用いるのは定番中の定番でしょう。

そんな cron ですが、実は設定できる最小単位は分単位で、秒単位での設定はできません。

でも、システムの要件によっては、秒単位での実行が必要な場合もあるかもしれません。

私は cron を使うほとんどの場合 PHP を起動するのに使用するので、 PHP 側で秒単位の処理を行えるようにしてみました。

そんなことしなくても cron の設定だけで同じことはできる

秒単位の処理を行う PHP のコードを書き始める前に、 cron の設定だけで同じことができそうだなと思い、調べてみました。

結果、簡単に解決策が見つかりました。優秀な先人がいるのと同時に、世の中は無常ですね。

10秒間隔のcron実行を1行で表すにはこのように書くときれいでよい。
cronを秒単位で指定する方法
* * * * * for i in `seq 0 10 59`;do (sleep ${i}; コマンド) & done;

 

上記コマンド部分に PHP の実行コマンドを入れれば、秒単位での処理が可能です。

めでたしめでたし。

とはいえ重複実行を避けたかったので、やっぱり PHP で書いてみる

上記の方法で秒単位の処理が可能であることはわかりました。

しかし、ここまで細かい頻度で定期実行が行われると、重複実行のリスクが高まります。

前の処理が終わる前に次の処理が始まってしまうと、処理内容によってはデータの整合性が取れなくなる可能性があります。

加えて、処理しきれないタスクが溜まっていくと、サーバの負荷が高まり、サービス全体に影響が出る可能性もあります。

そこで、前回の処理が終わっていない場合は次の処理を行わないように、PHP で秒単位の処理を行うコードを書いてみました。

<?php
// このファイルを cron にて 1 分に一回実行することで、指定した関数を指定した秒数ごとに実行できる
// 設定例: * * * * * php /path/to/seconds.php 秒数 関数名 引数1 引数2 ...
// ここに実行したい関数を直接書いたり、他のファイルを読み込んだりする
function test_log($message = '')
{
  file_put_contents(__DIR__.DIRECTORY_SEPARATOR.'test_log.txt', date('Y-m-d H:i:s').' '.$message."\n", FILE_APPEND);
}

// CLI 以外からのアクセスを禁止する
if (PHP_SAPI !== 'cli')
{
  die();
}

// 引数がない場合、もしくは一番目の引数が数字でない、または 0 の場合は終了する
if (count($argv) < 3 || ! ctype_digit(str_replace('-', '', $argv[1])) || (int)$argv[1] === 0)
{
  die('引数がありません。');
}

// 一番目の引数が正の数であった場合、最初の引数は実行したい関数名、一番目の引数は繰り返し実行する秒数として扱う
if ((int)$argv[1] > 0)
{
  // 1 分に一回、 cron から叩かれるはずなので、 1 分間に何回実行するかを計算する
  for ($i = 0; $i < 60 / floor((int)$argv[1]); $i++)
  {
    // 指定した関数をバックグラウンド実行する
    exec('nohup php '.__FILE__.' -'.implode(' ', array_slice($argv, 1)).' > /dev/null &');

    // 指定した秒数待つ
    sleep((int)$argv[1]);
  }

  // 終了する
  die();
}

// 実行したい関数が本当にあるか確認する
if ( ! function_exists($argv[2]))
{
  die('関数が存在しません。');
}

// 重複実行防止用のファイルパス
$lock_file = __DIR__.DIRECTORY_SEPARATOR.'lock_'.md5($argv[2]).'.lock';

// ロックファイルが存在するかつ、実行間隔の秒数 * 10 よりも前回の実行時間が前であれば終了する
if (file_exists($lock_file) && (time() - filemtime($lock_file)) < abs((int)$argv[1]) * 10)
{
  die();
}

// ロックファイルを作成する
file_put_contents($lock_file, '');

// 三番目以降の引数を関数に渡して実行する
call_user_func_array($argv[2], array_slice($argv, 3));

// ロックファイルを削除する
unlink($lock_file);

上記のコードを seconds.php として保存し、 cron で 1 分に一回実行することで、指定した関数を指定した秒数ごとに実行できます。

例えば、以下のように設定することで、 10 秒ごとに test_log 関数を実行できます。

* * * * * php /path/to/seconds.php 10 test_log 'Hello, World!'

今回は一つのファイルにまとめましたが、関数を別ファイルに書いて読み込むようにすることで、複数の関数を秒単位で実行することも可能です。

フレームワークを使用しているなら、クラスとして機能を分解して実装すれば、より柔軟に秒単位の処理を行えるかもしれません。

……まあ、PHP でなくとも、 sh などのスクリプト言語を使えば同じことができるかもしれませんが。後々条件分岐などが過激になっていくにつれ、 PHP の方が将来的に扱いやすいかもしれません。

とはいえ、 Laravel を使用すれば秒単位の処理も簡単に行える

ほとんどのオペレーティングシステムでは、cronジョブの実行は最短で1分に1回に制限されています。しかし、Laravelのスケジューラーを使えば、1秒に1回など、より細かい間隔でタスクを実行できます。
https://readouble.com/laravel/11.x/ja/scheduling.html#sub-minute-scheduled-tasks
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->everySecond();

 

デフォルトでは以前の同じジョブが起動中であっても、スケジュールされたジョブは実行されます。これを防ぐには、withoutOverlappingメソッドを使用してください。
https://readouble.com/laravel/11.x/ja/scheduling.html#preventing-task-overlaps
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->withoutOverlapping();

 

フレームワークってすごいですね。嫉妬しちゃいますね。

まとめ

cron で秒単位の処理を行う方法について検討してみました。

このようなことができる、ということに気が付く前は PHP を Linux デーモンにしたりしていましたが、おそらくこちらの方が簡単かつ安全に実装できるのではないかと思います。
業務上ではフレームワークあたりに任せたい処理ではあるので、使用しているフレームワークに該当機能があったら迷わず使い倒しましょう。

ただし、 cron から叩いた処理はその数だけプロセスが立ち上がるので、処理内容によって負荷が高くなることもあります。
その点、デーモンを使用すると一つのプロセスを使い回してくれるので効率は良くなります。
あと、意外なところでは cron を 1 秒ごとに実行するとログが大量に溜まってしまう、というのもデメリットかもしれません。

そもそもの話、これら秒単位の定期実行は特殊な例と言えます。
実際に行う処理・頻度と相談して慎重に実装方法を選択した方がよいでしょう。

弊社でも CodeIgniter や Laravel など、 PHP フレームワークを使用した開発を得意としております。
複雑な定期実行を必要とするシステムの開発が必要であればぜひお問い合わせください。

プログラマー / N.Go

CodeIgniterやLaravel、Vue.jsといったフレームワークを用い、ECシステム、リアルタイム課金制生配信、掲示板ライクなSNSシステムなどのWebシステム制作に携わる。 プロジェクトによってはフロントエンドも一貫して請け負う。

このメンバーの記事一覧へ