PHP 8.3 の FPM で prefork 後に mysqli の接続が切れる現象を追った

这篇内容暂时只有 日本語 版本,以下是原文。

PHP 8.3.4、Amazon Linux 2023、FPM(pm=dynamic、pm.max_children=32)、MySQL 8.0.36 の構成で、夜間バッチ後の初回リクエストだけ「MySQL server has gone away」が出る件を追いました。アクセスは日中 1 時間あたり 1.2 万リクエスト程度、バッチは 3 万行を 500 件ずつ更新します。

まず FPM のログと MySQL の SHOW PROCESSLIST を突き合わせ、切断が起きるのは決まってワーカーが pm.max_requests に達して再生成された直後だと分かりました。再現手順は単純で、pm.max_requests=1 にして 2 回叩くだけです。

; www.conf
pm = dynamic
pm.max_children = 32
pm.max_requests = 500

次に、再接続をアプリ側で握るのではなく、接続を使い回さない方向に寄せました。具体的には mysqli の持続接続をやめ、リクエスト終了時に明示的に閉じます。

// 変更前: ホスト指定で持続接続
$mysqli = new mysqli('p:db.internal', $user, $pass, $db);

// 変更後: 通常接続 + finally で close
$mysqli = new mysqli('db.internal', $user, $pass, $db);
try {
    $mysqli->query('SET SESSION wait_timeout = 60');
    // ...
} finally {
    $mysqli->close();
}

なぜこれで収まるかというと、持続接続はワーカーのプロセスに紐づくため、pm.max_requests による再生成や MySQL 側の wait_timeout を跨いだ瞬間に「生きているつもりの死んだ接続」が残るからです。ここでは性能より再現性を優先し、接続コストは 1 リクエストあたり数 ms 増える代わりに、gone away をゼロにしました。

ただしこの方法は、接続数が跳ね上がる高トラフィックでは効きません。max_connections に近づくと今度は「Too many connections」に化けます。実際、検証環境で 200 並列をかけたときに別のエラーへ置き換わったのを見ています。持続接続を残すなら、mysqli.reconnect に頼らず、クエリ前に ping() して死んでいたら張り直す方が安全でした。

もう一つの穴は、トランザクション中にワーカーが落ちた場合です。close を finally に置いても、プロセスが kill されればロールバックは MySQL 任せになります。バッチ側の分離レベルとロック待ち時間もセットで見ないと、この対策だけでは足りません。

同じように FPM の再生成と DB 接続の寿命がずれて困った経験のある方、どの層で吸収していますか。アプリ側の ping か、proxy か、それとも接続プールを別に立てていますか。

0
赞同度 100% · 0 条评论 · 5 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。