現象
とあるレンタルサーバ(telnetやsshは未サポート)上のデータをローカル(RHEL6.3 サーバ)上に定期的にバックアップを取る必要があり、ファイル数が多くFTPだと時間がかかって仕方がないので、
- レンタルサーバ上のPHPスクリプトを呼び出し、tar コマンドにより全ファイルをアーカイブ。
- アーカイブした tar ファイルを FTP でダウンロード。
という方法を取っていたのだが、ある時点から、正常にバックアップが取れなくなってしまった。
調べてみたところ、
- 1. で、HTTP GET Request 後に、一定時間(5分)以上受信データが無い状態が続くと、HTTP クライアントがその後のデータを受信しないままフリーズしてしまう。
状態であることがわかった。
ちなみに、HTTP クライアントには wget を使用していたが、
- 同一バージョンの wget を使用しても、個人持ちの CentOS 6.5 上ではフリーズせずに問題なく完了。
- 当該 RHEL6.3 サーバ上では、wget 以外の方法であっても、同様の現象が発生してしまう。
RHEL6.3 サーバ上で使用している socket 関連の共有ライブラリ(あるいはその設定)に問題があるのであろうところまでしかわかっていない。
どなたか、このような場合の対策をご存じの方がおられたら、教えてほしい。
もっとも、5分以上もデータが無い状態が続くと、そもそも他のレンタルサーバとかだと Apache とかのタイムアウトの方でひっかかってしまう気がしなくもないので、どちらにしてもレンタルサーバ側の処理も見直す必要があるのだろうけれども。
再現方法
次のようなPHPスクリプトをレンタルサーバ上に設置し、
/test/wait.php
<?php $WAIT_MIN = 5; // <=4:OK, >5:NG if (isset($_GET['wait']) && is_numeric($_GET['wait'])) $WAIT_MIN = intval($_GET['wait']); $WAIT_SEC = $WAIT_MIN * 60; $WAIT_UNIT_SEC = 10; $WAIT_COUNT = (int) ($WAIT_SEC / $WAIT_UNIT_SEC); set_time_limit($WAIT_SEC * 2); function echo_flush($str) { echo($str); ob_flush(); flush(); } // end of flush_output() ob_start(); header("Content-Type: text/plain; charset=utf-8"); ob_end_flush(); // バッファフラッシュ&バッファリングをOFFに echo_flush("wait {$WAIT_MIN} minutes ({$WAIT_SEC} seconds) ...\n"); for ($ci=0; $ci < $WAIT_COUNT; $ci++) { sleep($WAIT_UNIT_SEC); //↓の行を有効にして、10秒毎にデータを送信するようにすればクライアント側でフリーズしない //echo_flush(sprintf("%5d sec.\n", $WAIT_UNIT_SEC*(1+$ci))); } echo_flush("done.\n"); exit(0); // ■ end of file
RHEL6.3 サーバ上で wget を実行すると、
$ wget "http://example.com/test/wait.php?wait=4" -q -O - wait 4 minutes (240 seconds) ... done.
のように、4分までは問題なく完了するのに、
$ wget "http://example.com/test/wait.php?wait=5" -q -O - wait 5 minutes (300 seconds) ...
5分からは(wget側でタイムアウトするまで)だんまりになる。
また、telnet で試しても、
$ telnet example.com 80 Trying xxx.xxx.xxx.xxx... Connected to example.com. Escape character is '^]'. GET /test/wait.php?wait=4 HTTP/1.0 User-Agent: telnet Host: example.com HTTP/1.1 200 OK Date: Sun, 15 Jun 2014 00:00:00 GMT Server: Apache Connection: close Content-Type: text/plain; charset=utf-8 wait 4 minutes (240 seconds) ... done. Connection closed by foreign host.
のように、4分までは問題なく完了するのに、
$ telnet example.com 80 # : (中略) GET /test/wait.php?wait=5 HTTP/1.0 # : (中略) wait 5 minutes (300 seconds) ...
5分からは、この状態でだんまりになる。
暫定対策
レンタルサーバ側のPHPスクリプトで、次のような関数を使って tar コマンドをバックグランドで呼び出した後、処理が終わるまで一定時間毎にポーリングし、何らかのデータを出力するようにしている。
<?php function exec_nowait($cmdline, &$outfile, &$errfile) { $outfile = tempnam('./', 'OUT_'); $errfile = tempnam('./', 'ERR_'); $cmdline = "{$cmdline} >{$outfile} 2>{$errfile} & echo \$!"; $pid = null; exec($cmdline, $output, $rcode); foreach ($output as $line) if (($pid = trim($line)) !== '') break; return $pid; } // end of exec_nowait() $pid = exec_nowait("tar cvf backup.tar /path/to/target", $outfile, $errfile); while ($pid) { sleep(10); if (/*バックグラウンド処理のチェックを行い、終了していたら*/) break; echo(date("Y-m-d H:i:s") . "\n"); // データ出力 ob_flush(); flush(); } // この辺で後処理を入れる unlink($outfile); unlink($errfile);
バックグラウンド処理の終了チェックは、psコマンドが使える場合は、
<?php function get_proc_dict() { $proc_dict = array(); $cmd = "/bin/ps -A -o pid= -o comm="; $fp = popen($cmd, "r"); while (($line=fgets($fp))!==false) { $line = trim($line); $parts = explode(' ', $line, 2); $proc_dict[$parts[0]] = $parts[1]; } pclose($fp); return $proc_dict; } // end of get_proc_dict() function get_proc_name($pid) { $proc_dict = get_proc_dict(); return isset($proc_dict[$pid]) ? $proc_dict[$pid] : null; } // end of get_proc_name() // while ループ内のチェック部分で、if (!get_proc_name($pid)) break;
のような関数を用意して使うのがよさそう。
ps コマンドが存在しない場合は…tar の標準出力($outfile)をチェックして、例えばサイズが変わらなくなったら終了、とか。