生のPHP Curlのデフォルトタイムアウトは0(無制限)の罠
- 掲載日
障害発生
あるシンプルなAPI連携処理がありました。
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とCURLOPT_CONNECTTIMEOUTを設定する。
以下の処理があるだけで防げた事故です。タイムアウトをちゃんと設定しようという事ですね…。
<?php
$curl = curl_init('https://example.com');
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); // 接続タイムアウト
curl_setopt($curl, CURLOPT_TIMEOUT, 30); // リクエスト自体のタイムアウトなお、普通のサイトにはcURLを実行しても正常なレスポンスが返ってきてしまうので、再現したい場合は以下のような無限ループの処理を作成します。(ファイル名をloop_response_test.phpとします。)
<?php
while (ob_get_level()) {
ob_end_flush();
}
$index = 0;
while (true) {
echo $index . "\n";
flush();
sleep(1);
$index++;
}
(※無限ループなのでうっかり公開状態で放置しないよう注意してください。)
続いて以下のような実行処理を作成し、先に作ったファイルloop_response_test.phpをApache等でアクセスできるようにした上で実行します。
<?php
$curl = curl_init("http://XXXX/loop_response_test.php");
$option = array(
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
);
curl_setopt_array($curl, $option);
$response = curl_exec($curl);
$curl_errno = curl_errno($curl);
$curl_error = curl_error($curl);
echo var_dump($curl_errno);
echo var_dump($curl_error);以下のような実行結果が表示され、10秒後にタイムアウトしたことが分かります。
> php timeout_test.php
0
1
2
(中略)
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
cat $lockfile
echo "上記時間よりロック中"
# アラートメール(※処理の間隔によっては、時間の比較を入れて30分以上ロックされていたらメール等、適切に変更する。)
sendmail -f root@example.com -t example@example.com -s "[warning]【処理名】がロックされています。"
exit
fi
date > $lockfile
# ここに定期的に実行する処理
rm -f $lockfile
ロックされていたらアラートメールを送って対応も出来ますし、夜間で爆睡していて気が付かなかったとしてもAPI連携が止まるだけで他のシステムには影響を及ぼさずに済みます。
※複雑な依存関係がある場合等ファイルロックだけだとどうにもならないケース等あるのであくまで解決策の一つとして。
参考
- 【PHP】 プロセスが永遠に起動している問題への対策と知見
- curl で相手が突然死すると無限に待ってシグナルでも死なない件
- curl_init
- cURL - 定義済み定数
ちゃんと以下の通り書いてありました。
- cURL 関数の実行を許容する最大秒数を指定します。 デフォルトは 0 で、転送中に関数がタイムアウトしないことを意味します。 cURL 7.1.0 以降で利用可能です。

この記事を書いた人
- W.A
- 茨城県在住Webエンジニアです。 PHP、PostgreSQL、Linuxなどを業務で使用しています。 趣味ではGoやNuxt、Flutterをやってます。
