nginx + PHP-

この投稿は4年半前の記事です。 情報が古くなっている可能性があるので、その点ご了承ください。
2012 年 6 月 28 日 1,763日前)
3,849文字 (読了時間9分)

SPONSORED LINK

さて、つい昨日Tips記事を収益化できないかなという邪な感情から「WordPressで管理画面以外から投稿させる機能を作る」というエントリーをぶち上げたのですが、コンテンツ公開後、コーディング画面を録画したビデオがダウンロードできないという報告を受けてしまいました。

端的にいうと、270MBという巨大なファイルだからダウンロードできなかったわけです。

この動画がダウンロードできなかった
この動画がダウンロードできなかった

一応有料で販売しているものなので、こりゃまずいと直しにかかったのですが、異常に時間がかかりました。これはその闘争の記録です。

Apache + mod_phpで巨大ファイルをPHPでダウンロードさせる

さて、今回のように、サーバにぽこっとファイルを置いておくだけではなく、PHPでユーザーにアクセス権があるかどうかを確認してからファイルをダウンロードさせる場合、PHPでファイルを出力させる必要があります。

ファイルの場合は普通ヘッダーを出力します。

//ファイルへのパス
$path;
//IEの場合はキャッシュさせないようにしないとSSLでエラー
global $is_IE;if($is_IE){
	header("Cache-Control: public");
	header("Pragma:");
}
header('Content-Type: video/quicktime');
header('Content-Disposition: attachment; filename="'.basename($path).'"');
header("Content-Length: ".filesize($path));

で、あとは本体を出力するだけなのですが、ごく小さなファイルの場合はこんな風にやればオッケーです。

echo file_get_contents($path);

ところが、今回のように動画などのサイズが大きいファイルの場合は”Allowed memory size of xxxxxx bytes exhausted”などと表示され、メモリ使い過ぎでPHPが止まってしまいます。これを防ぐためには、ファイルをちょっとずつ読み込んで出力というのを繰り返すのが定石です。

$handle = fopen($path, "r");
while(!feof($handle)){
	//指定したバイト数だけ出力
	$bytes = fread($handle, 1000 * 1024);
	echo $bytes;
	//出力
	flush();
	//1秒休む
	sleep(1);
}
//ファイルを閉じる
fclose($handle);
//終了
exit;

PHPは基本的に出力すべき内容をいったんバッファと呼ばれる領域に保存してから一気に出力します。これをレストランに喩えると、寿司ではなく定食ですね。寿司のように注文があるたび出すのではなく、定食のように全部揃ってから出すというわけです。動画のように巨大なファイルの場合、読み込みが終わるまでけっこうな時間がかかるので、それを全部待っていると時間がかかりすぎてしまいます。そんな寿司屋行きたくないですよね。したがって、flush()を使ってPHPを寿司屋モードに変える必要があります。

ローカルもApache + mod_phpだったのでこれで動いたのですが、本番環境では動きませんでした。なぜか”Allowed memory size〜”のエラーが出てるんですね。

NginxもPHPも出力バッファがあるんだって

まず環境の違いで動かなかったので、Nginxを疑って色々ぐぐったのですが、そこで”PHP, Nginx, and Output Flushing“というエントリーを見つけました。このエントリーによると、NginxはバックエンドのPHP(FastCGIでもPHP-FPMでも同じだと思いますが)のすべての挙動を一度バッファリングするらしいです。

PHPもバッファリングを行うのですが、そんなに大きなサイズではないので設定をいじる必要はなさそうです。今回の場合、ダウンロードリンクはSSL環境だったので、そこでだけバッファリングを極端に小さくしました。

location ~ \.php$ {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass  127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param  SCRIPT_FILENAME  /path/to/public_html/$fastcgi_script_name;
    fastcgi_read_timeout 600;
    fastcgi_buffer_size   1k;
    fastcgi_buffers       128 1k;  # up to 4k + 128 * 4k
    fastcgi_max_temp_file_size 0;
    gzip off;
}

これで直るかなーと思いましたが、Allowed memory size〜のエラーが出てダウンロードできず。

PHPはflushだけじゃ出力バッファまでは出さないんだって

ここから数時間をググって過ごしたのですが、おなじみDo You PHP はてなで「出力バッファとflush()・ob_flush()」というエントリーを発見しました。これによると、「flushが動作する条件は出力バッファがないこと」とあるので、そもそもflushだけじゃダメだったんじゃないかと思うようになりました。そこでこんな風に直します。

//出力バッファを一度出力
ob_end_flush();
ob_start('mb_output_handler');
//ファイルを読み込む
$handle = fopen($path, "r");
while(!feof($handle)){
	//指定したバイト数だけ出力
	$bytes = fread($handle, $per_size);
	echo $bytes;
	//出力
	ob_flush();
	flush();
	//1秒休む
	sleep(1);
}
//ファイルを閉じる
fclose($handle);

これで無事ダウンロードできるようになりました。flushとob_flushはセットということを覚えておこうと思います。

ログアウトできなくなる

さて、ダウンロードできるようになって嬉しーなと思った矢先、なんとログアウトできなくなるという事態に見舞われました。”upstream sent too big header while reading response header from upstream”というエラーが出ていたので、おそらくヘッダーがデカ過ぎだぞということだと思います。

デカイと言っても、そんなにでかくないんじゃないかと思うのですが、たしかに先ほどバッファーの値を1kとか小さい値にしてしまっていました。なので、nginx.confの設定を見直し、これだけあればいいだろうという設定にしたのがこちら。バッファサイズの指定をなくしています。

location ~ \.php$ {
	include       fastcgi_params;
        fastcgi_pass  127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /path/to/server$fastcgi_script_name;
	fastcgi_read_timeout 600;
        fastcgi_max_temp_file_size 0;
	gzip off;
}

これでログアウトもダウンロードもできるようになり、一安心です。しかし、nginxはまだ情報少なくて大変ですね。

追記@20120724

コメント欄に書いてありますが、NginxにはX-Accel-Redirectという非公開領域にバイパスを通すような仕組みがあるようです。PHPで認証かましてからダウンロードさせるならこの方法がいいみたいですね。Apacheにも同様のmod_xsendfileといのがあるようです。詳しくはググっておくんなまし。

なお、この記事で書いたのはWordPressプラグインだったので、サーバ環境が決定できない以上使えませんが、CakePHP + Nginxなどでサイトを構成している場合はX-Accel-Redirectの方がシャレオツです。

 

フォローしてください

ここで会ったのもなにかの縁。
高橋文樹.comの最新情報を見逃さないためにもフォローをお願いします。
めったに送らないメルマガもあります。

SPONSORED LINK

この記事について

この記事はが2012 年 6 月 28 日にプログラミングの記事として公開しました。

高橋先生の電子書籍

高橋先生の電子書籍

Amazonで電子書籍も買えます。

好きな言葉

なんだって? 僻地の村で十年間の先生稼業か? 食うや食わずの売れない三文作家か? お前はルソーじゃないんだ、坊や。地に足をつけるんだな。

— ジョナサン・リテル

高橋先生の処女作

『途中下車』高橋文樹

2001年幻冬舎NET学生文学大賞受賞作です。

Web制作やります

Web制作やります

Web制作のご依頼は株式会社破滅派へ

不定期メルマガ

高橋文樹.comでは、不定期でニュースレターを配信しています。滅多に送らないので是非購読してください。

高橋文樹.comではプライバシーポリシーに準じて登録情報を取り扱います。