風柳メモ

ソフトウェア・プログラミング関連の覚書が中心。

RHEL6.3で、HTTP GET時に5分以上受信データがないとだんまりになる

現象

とあるレンタルサーバ(telnetやsshは未サポート)上のデータをローカル(RHEL6.3 サーバ)上に定期的にバックアップを取る必要があり、ファイル数が多くFTPだと時間がかかって仕方がないので、

  1. レンタルサーバ上のPHPスクリプトを呼び出し、tar コマンドにより全ファイルをアーカイブ。
  2. アーカイブした 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)をチェックして、例えばサイズが変わらなくなったら終了、とか。