PHPのset_time_limitでsleepやDB待ちが数えられない理由
set_time_limit() とは、いま実行中のスクリプトの最大実行時間を秒単位で設定し直す関数のことです。Linuxの標準的なビルドでは、スクリプト自身の処理時間が対象で、sleep() やDB待ちの時間は数えられません。
「max_execution_time を30秒にしているのに、なぜか1分以上動き続けている」という相談は、何度か見てきました。逆に、Windowsで動いていたバッチをLinuxへ移したら、今度はタイムアウトしなくなって驚く人もいます。どちらも、何の時間を数えているのかを知らないと説明がつかない話です。
set_time_limit() は何を数えているのか
公式ドキュメントでは、set_time_limit() と max_execution_time が制限するのは「スクリプト自身の実行時間」で、system() の呼び出し、ストリーム操作、データベースクエリなど、スクリプトの外で起きている活動の時間は、Windows以外では含まれないと説明されています。Windowsでは実時間(壁時計の時間)で測られるので、同じコードでも挙動が変わります。
Linuxの標準的な構成では、内部的にCPU時間で数えている、と理解しておくのが実務ではわかりやすいと思います。sleep() で寝ている間やI/O待ちの間はCPUを使わないので、時計が進まない、というイメージですね。
<?php
set_time_limit(2);
// Linuxの標準的なビルドでは、5秒寝てもタイムアウトしない
sleep(5);
echo "sleepは通過しました\n";
// 一方、CPUを使い続けるループは約2秒で止まる
while (true) {
// 何もしない空ループ
}
// Fatal error: Maximum execution time of 2 seconds exceeded
この例の前半が通るのは、あくまで「CPU時間で数える」標準的なLinuxビルドの話です。次の節で触れるとおり、ビルドの設定によっては前半でも止まります。手元の環境で一度だけ試しておくと、思い込みを防げます。
ZTSビルドだと数え方が変わるのか
変わる場合があります。ドキュメントによると、PHPを –enable-zend-max-execution-timers 付きでビルドすると、POSIXのスレッド単位タイマーで経過時間を測るようになり、sleep や I/O 待ちも制限に含まれます。このオプションは、PHP 8.3.0 以降のZTSビルドでは既定で有効、とされています。
FPMのような通常のNTSビルドを使っているなら、気にしなくて大丈夫です。ただ、FrankenPHPなどZTSで動かす環境へ移るときは、「今まで数えられなかった待ち時間が数えられる」可能性があるので、重い外部API呼び出しを含む処理はテストしておいたほうが安全です。
呼び出すたびにカウンタが0に戻るのはどう使うのか
set_time_limit() は呼び出した時点からカウントを数え直します。公式の例では、25秒経過した時点で set_time_limit(20) を呼ぶと、合計45秒まで動ける、とされています。つまり「全体で何秒」ではなく「ここから何秒」の指定になります。
これを逆手に取ると、長いバッチでも1回分の処理が重くない限りタイムアウトしない書き方ができます。全体を無制限にするより、「1チャンクで詰まったら止まる」ほうが、暴走時の被害が小さくなります。
<?php
$ids = range(1, 10000);
foreach (array_chunk($ids, 100) as $chunk) {
// 1チャンクあたり最大30秒。毎回カウンタを戻す
set_time_limit(30);
foreach ($chunk as $id) {
processOne($id);
}
}
set_time_limit(0) で無制限にするのは簡単ですが、無限ループに入ったときに誰も止めてくれなくなります。チャンクごとに区切る形にしておくと、「どこで詰まったか」もログから追いやすいです。
タイムアウトしないのに504が出るのはなぜか
PHP側の制限と、Webサーバやプロキシ側のタイムアウトは別物です。ドキュメントにも、Webサーバが独自にPHPを中断することがあり、ApacheのTimeoutやIISのCGIタイムアウトの既定値は300秒と書かれています。
PHP-FPMを使っているなら、プールの設定にある request_terminate_timeout も別の上限になります。これは実時間で効く設定なので、sleep が多い処理はこちらに引っかかることがあります。「PHPでは数えられていないのに止められた」ときは、Nginxの fastcgi_read_timeout なども含めて、経路上の上限を順に確認するのが近道だと思います。
CLIでは set_time_limit() を呼ぶ必要があるのか
CLIでは max_execution_time の既定値が0(無制限)なので、普通は呼ばなくて足ります。同じコードをWebからもCLIからも呼ぶ共通処理に set_time_limit(30) を書いておくと、CLIでは意図せず30秒の上限がつく、というのは地味なハマりどころです。共通ライブラリの中に埋め込むより、入口(コントローラやコマンド)側で決めるほうが、挙動が読みやすくなります。
まとめ
set_time_limit() が数えるのは「スクリプトが実際に動いた時間」で、Linuxの標準的な構成ではsleepやDB待ちは含まれず、Windowsやtimer方式のZTSビルドでは含まれる、というのが今回の要点でした。長いバッチは、チャンクごとにカウンタを戻す形にしておくと、無制限にせずに済みます。Webサーバ側の上限は別に存在するので、タイムアウトの原因は経路全体で見たほうがよさそうだ、という気がしています。
よくある質問
Q. set_time_limit(0) と max_execution_time=0 は同じ意味ですか。
A. どちらも実行時間の制限なしを意味します。set_time_limit() はそのスクリプトの実行中だけ上書きする関数で、php.ini の値自体は変わりません。
Q. sleep(60) を入れているのに max_execution_time=30 で止まりません。
A. Linuxの標準的なビルドでは、sleep中はスクリプト自身の実行時間として数えられないためです。Windowsや、zend max execution timers が有効なZTSビルドでは数えられるので、環境によって結果が変わります。
Q. タイムアウトしたときに後始末の処理を走らせられますか。
A. 致命的エラーとして終了するので、register_shutdown_function() で後始末を登録しておけば、終了時に呼ばれます。error_get_last() でタイムアウトによる終了かどうかも判別できます。