PHP cURLのデフォルトタイムアウトは0(無制限)

掲載日
更新日

障害発生

あるシンプルなAPI連携処理があり、PHPのcURLを使用していました。

APIを実行するとJSONが返ってくるので、パースして削除されたIDはDELETE、重複しているIDはUPDATE、重複していないIDはINSERTするようなよくある処理です。

さらにこの更新対象テーブルは一つだけ、実行頻度も30分間隔で利用者からの更新もないテーブル(連携のためだけのテーブル)だったため、Lockなんて早々起きないだろうと思っていました。(敗因1)

取得できなかったIDのデータは削除するという仕様のため、

APIのデータが上手く取得できなかった場合はデータ更新したくないため、念のためtry catchとtransactionだけ実装(敗因2)していました。

 

ここからが本題で、生のPHP cURL処理はなんとデフォルトタイムアウトが0(無制限)です。

ネットワークの状態等によっては一生プロセスが残ることになってしまいます。

ゾンビプロセスが残るのも問題ですが、cURLを実行している位置が不味く、transactionの中でcURLを実行してしまっていたので一生解放待ちのtransactionが生まれてしまいました。

よって、アクセス中のテーブルはロックされ、後続の30分おきの処理もロックされているので解放待ちになり…
ということで、一個のAPI実行への考慮不足で、シンプルなつもりの処理で接続数が爆増する不具合を引き起こしてしまいました。

 

CURLOPT_TIMEOUTを設定する。

以下の処理があるだけで防げた事故です。タイムアウトをちゃんと設定しようという事ですね…。

curl_setopt($curl, CURLOPT_TIMEOUT, 30);
  • CURLOPT_TIMEOUT
    リクエスト自体のタイムアウト設定で、こちらがデフォルト0(無制限)になっています。
  • CURLOPT_CONNECTTIMEOUT(オマケ)
    接続のタイムアウト設定。こちらはデフォルト300秒なので設定しなくても大丈夫です。(5分とちょっと長いので設定するとなお良いとは思います。)
    最初こっちも設定が必要かと思ってましたが、デフォルト設定があるのでなくても大惨事にはなりにくいと思われます。
     

なお、普通のサイトにはcURLを実行しても正常なレスポンスが返ってきてしまうので、再現したい場合は以下のような無限ループの処理を作成します。(ファイル名をloop_response_test.phpとします。)

<?php
while (ob_get_level()) {
    ob_end_flush();
}

$index = 0;
while (true) {
    echo $index . "\n";

    flush();

    sleep(1);

    $index++;
}

(※無限ループなのでうっかり公開状態で放置しないよう注意してください。)

まず、PHP側でレスポンスをリアルタイムに配信し続ける必要があるため、ob_get_level() を用いて一度すべての出力バッファを強制終了させています。
その上で、while(true) による無限ループを回し、1秒ごとにカウントをインクリメントしながら flush() 関数で出力し続けて何秒でタイムアウトしたか分かるようにしてあります。

 

続いて以下のような実行処理を作成し、先に作ったファイルloop_response_test.phpをApache等でアクセスできるようにした上で実行します。

<?php
$curl = curl_init("http://XXXX/loop_response_test.php");

$option = array(
    CURLOPT_TIMEOUT => 10,
); 
curl_setopt_array($curl, $option);

$response = curl_exec($curl);

echo var_dump(curl_errno($curl));
echo var_dump(curl_error($curl));

最初に作ったファイル(loop_response_test.php)のURLを設定しつつcurlの初期化を行います。
その後CURLOPT_TIMEOUTを適切な数字に設定します。(今回は仮に10秒としています。)

curl_execで処理を実行し、エラーを取得して出力しています。

(末尾が似ていて紛らわしいですが、curl_errnoとcurl_errorでそれぞれエラーの番号と内容を取得しています。)

 

このスクリプトを実行すると以下のような実行結果が表示され、10秒後にタイムアウトしたことが分かります。

> php timeout_test.php
0
1
(中略)
8
9
int(28)
string(67) "Operation timed out after 10010 milliseconds with 20 bytes received"

場合によってはファイルロックもした方が良い

こういった定期的に実行する処理には以下のようなファイルロック処理も有効です。
(自分の場合、処理自体はphpで作って実行はshellscriptで管理しているので以下はshellscriptの処理になります。一貫してphpでやっても問題ないと思います。)

#!/bin/bash

lockfile=/tmp/.LockProcessName
if [ -f $lockfile ]
then
    sendmail -f root@example.com  -t example@example.com -s "[warning]【処理名】がロックされています。"

    exit
fi

date > $lockfile

# ここに定期的に実行する処理

rm -f $lockfile

lockfile変数でファイルパスを設定し、if [ -f XX]でロックファイルの有無を確認、ファイルがあれば(ロック中であれば)アラートメールを送信してexitで処理を抜け、無ければロックファイルを作成して後続の処理を実行するシンプルな処理です。

 

アラートメールを送ることで気が付けば対応も出来ますし、夜間で爆睡していて気が付かなかったとしてもAPI連携が止まるだけで他のシステムには影響を及ぼさずに済みます。
(今回の場合、解放待ちのtransactionが一個だけ作成された状態で止まり、無限にtransactionが生成されて他のDB処理に影響を及ぼすことが防げます。)

※複雑な依存関係がある場合等ファイルロックだけだとどうにもならないケース等あるのであくまで解決策の一つとして。

参考

  • cURL 関数の実行を許容する最大秒数を指定します。 デフォルトは 0 で、転送中に関数がタイムアウトしないことを意味します。 cURL 7.1.0 以降で利用可能です。
    • BlueSkyで共有する
    • Hatenaで共有する
記事の作成者のW.Aのアイコン

この記事を書いた人

W.A
茨城県在住Webエンジニアです。 PHP、PostgreSQL、Linuxなどを業務で使用しています。 趣味ではGoやNuxt、Flutterをやってます。
  • GitHubのリンク

Comment

関連記事

  • CentOS7でPHP8.2.35をコンパイルして使うのサムネイル画像

    CentOS7でPHP8.2.35をコンパイルして使う

    掲載

    CentOS7環境でCVE-2026-109618、 CVE-2026-109619対応のPHPのバージョンをコンパイルして使えるようにする方法の備忘録です。(※ご利用は自己責任で、何かあっても責任は負いかねます。必ず仮想マシンなどで本番と同じ環境を作って試してから本番作業を行ってください。)

  • 絵文字のアクセシビリティのサムネイル画像

    絵文字のアクセシビリティ

    掲載

    生成AIの登場で絵文字がふんだんにあしらわれた記事を見かけることが増えました。絵文字はスクリーンリーダーではどのように読み上げられ、どのようなアクセシビリティ対策が出来るのか、また推奨されているのか調査し、PHPを使ってaria-hiddenでマークアップする方法をまとめました。

  • PHPで見た目が同じ違う漢字に立ち向かうのサムネイル画像

    PHPで見た目が同じ違う漢字に立ち向かう

    掲載

    見た目は同じなのにDBやCtrl+Fでの文字列検索にヒットしない文字がある原因は「CJK部首補助」等の特殊な文字コードの混入かもしれません。PDFコピペ等で発生するこういったトラブルを、PHPでの自前マッピングで解決した備忘録です。

check コピーしました!