nginxとFastCGI環境でPHPのob_flushを使う方法
PHPでリアルタイムに進捗状況を把握したい時など、文字出力の際に、ob_flush関数を使い、バッファを出力すると思いますが、これが動作する環境としない環境があるんですよね。
単純に言うとFastCGIを使っているか否かなのですが、ググって調べてみると昔ながらのApache+PHP Module環境下での情報なども山ほどヒットするため、混乱しがちです。実際混乱しました。なので備忘録として記事にしておきます。
nginx+FastCGIでPHPのバッファフラッシュを使う方法
あれこれ語る前にとっとと実例いきましょう。
header('X-Accel-Buffering: no'); //nginxでのバッファリングをさせない
ob_end_clean(); //バッファクリア
ob_implicit_flush(1); //毎度ob_flush呼ぶのが面倒なので自動フラッシュをON
for ($i = 1; $i <= 5; $i++) {
echo $i.' ';
sleep(1);
}
echo 'Done';
これで1秒ごとに1 2 3 4 5とカウントされていき、最後にDoneと表示されます。
実際の動作を見たい方はこちらをどうぞ。
https://blog.ver001.com/script/flush.php
解説
大事なのは header('X-Accel-Buffering: no'); の部分ですね。
nginxに対して proxy_buffering と fastcgi_buffering の両方をオフにせよ、という指示のようです。
…のようです、と自信がないのは、下記ページの受け売りだから…。
https://stackoverflow.com/questions/4870697/php-flush-that-works-even-in-nginx
proxy_bufferingとfastcgi_bufferingの謎
きちんと検証できれば堂々と言えるのですが、うちの環境でテストした限りでは、nginx.confで、proxy_bufferingとfastcgi_bufferingを両方オフにしても、バッファリングされ続けてしまい、即時表示が出来なかったんですよ。
でも、魔法の言葉「 header('X-Accel-Buffering: no'); 」さえ付ければちゃんと出力できちゃう。
あ、それだけじゃないや。ob_end_clean();も必要でした。
これも謎なのですが、ob_end_cleanを実行しておかないとやっぱりまとめて表示されてしまうんです。なんでだろうなぁ。confの書き方が間違っていたのかなぁ。
ちなみに、うちのテスト環境はWindowsなので、Win版のnginxを使っていますし、PHPは7.4です。そのあたりも何か影響しているのだろうか。
でもたぶん、こういうバッファフラッシュが必要なのってテスト環境とか管理画面じゃないですか?
例えばWeb上の管理画面での日次処理。バックアップボタンをポチっと押したら、「バックアップ開始」「ファイルをコピー中...」「コピー完了」「ファイルを圧縮中...」「圧縮完了」みたいに進捗表示をしたいケースとか。
ひとつひとつのタスクを一本のPHPにして毎回呼び出すとか、進捗状況をテキストファイルに書き出しつつ、別のプログラムやJavaScriptの非同期通信で進捗表示をする方法もあると思うし、そちらのほうが王道でしょう。しかし、自分しか使わない管理画面にそこまで手間かけたくない。そんなケースありませんかね。ありますよね? あるって言え。
うちはまさにそういうケースでob_flush系を使いたかったので、やや気持ち悪い部分は残りますが、 header('X-Accel-Buffering: no'); と ob_end_clean();を実行すればオーケー!とわかったのでヨシとします。
ほかのおまじない
なんだか理由は曖昧だけれど、これを実行しておけばOK的なコードを「おまじない」と呼ぶケースがあり、そういうの気持ち悪くてあまり好きではないのですが、ob_flushが動作しない状況を調べていて出てきた情報があったので、こちらも一応書いておきます。
ini_set("output_buffering", 0); //PHPの出力バッファをオフ
ini_set("zlib.output_compression", 0); //圧縮しない
ini_set("implicit_flush", 1); //自動フラッシュをオン
header('Content-Encoding: none'); //gzip圧縮を無効
ソースコードにコメントしたとおり、ちゃんとそれぞれに意味はあるのですが、うちの環境(FastCGI)では特に効果はありませんでした。
Apache+PHPモジュールの環境なら、PHPのバッファリングがデフォルトで4KB (4096バイト) なので、output_bufferingの設定は有効だとは思います。
それから、Content-Encoding: noneをヘッダ出力するとgzip圧縮が無効になるってちょっと面白いですね。知りませんでした。
そもそもnginxの設定でgzip offを付けているので特に意味はないのですが、レンタルサーバー等で設定変更できない場合には有効なのかも知れません。
泥臭いけど場合によっては選択肢に入る力技
これも海外の掲示板で見かけた方法を少しアレンジしたのですが、クスッと笑いつつも、ちゃんと解決策のひとつにはなりえると感心もした方法です。
for ($i = 1; $i <= 5; $i++) {
echo $i.' ';
echo str_pad('', 4096);
@ob_flush();
@flush();
sleep(1);
}
echo 'Done';
バッファリングが止められないならバッファをあふれさせれば良いじゃない!
という脳みそ筋肉な発想、嫌いじゃありません。
出力するたびに4096バイト分の空白が入るので、非常に、ひっじょ~~~に通信帯域の無駄遣いですが、見た目ではわかりませんし、社内ネットワークでしか使わないとか、ほんの数行しか出力する予定はないとか、いくらかの条件付きながら、十分選択肢のひとつになりえると感じました。
まとめ
- nginx+FastCGI環境下でPHPのob_flushを動作させるには header('X-Accel-Buffering: no'); を使ってバッファリングをオフにしよう
- あと ob_end_clean も忘れずにね
以上です。
だいぶニッチな記事で需要があるのか… おそらくほとんどないと思いますけど、最近記憶力に自信がなくて、わからないことをググったら自分のブログ記事がヒットする、という現象がとてもと~っても増えてきたので、きっと未来の自分の役には立つはず。
そう信じて投稿することに致しました。