PHP 8.2/8.3でPDOのprepared statementが効かない条件を潰す

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

PHP 8.2.13 + MySQL 8.0.35(InnoDB)、PHP-FPM + nginx の構成で、1リクエストあたり30〜60件のSELECTを叩く画面がある。負荷試験でQPSを上げるとMySQL側のCom_stmt_prepareとCom_stmt_executeの比率が1:1に近づき、prepared statementがほぼ再利用されていないことが分かった。PDOのATTR_EMULATE_PREPARESはfalse、ATTR_STRINGIFY_FETCHESもfalseにしているのに、だ。

まず疑うべきはPDO::ATTR_EMULATE_PREPARESの設定が接続後に効いているか。getAttributeで確認する。

$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
var_dump($pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES)); // false であること

次に、prepare()を呼んだ後にexecute()を複数回呼んでいるかをコードで確認する。PDOのネイティブprepared statementはPDOStatementオブジェクト単位でサーバー側に残る。ループの内側で毎回prepare()していないか、だ。

// 悪い例: ループ内で毎回 prepare
foreach ($ids as $id) {
    $stmt = $pdo->prepare('SELECT * FROM items WHERE id = ?');
    $stmt->execute([$id]);
}

// 良い例: prepare は1回、execute を繰り返す
$stmt = $pdo->prepare('SELECT * FROM items WHERE id = ?');
foreach ($ids as $id) {
    $stmt->execute([$id]);
}

ここで注意が必要なのは、ネイティブprepared statementはexecute()後にcloseCursor()を呼ばないと、次のexecute()が「Commands out of sync」になることがある点だ。MySQLのunbuffered queryを使っている場合は特にそうなる。

では、なぜprepare()を1回にしてもCom_stmt_prepareが減らないのか。原因は接続プールではなく、PHP-FPMのワーカープロセスごとにPDO接続が張られ、リクエスト終了時に接続が閉じられるからだ。Persistent connection(PDO::ATTR_PERSISTENT => true)を使わない限り、prepared statementはリクエストをまたいで再利用されない。

$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_PERSISTENT => true,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

ただし、Persistent connectionはトランザクションの状態やSETしたセッション変数がワーカーに残る。前のリクエストでSET SESSION sql_mode = ...を変えていた場合、次のリクエストに影響する。これは自分も踏んだ。対策として、接続直後に$pdo->exec("SET SESSION sql_mode = 'STRICT_TRANS_TABLES'")のように明示的にリセットするか、Persistentを使わずにprepared statementの再利用を諦めるかの二択になる。

この方法が効かない条件もある。MySQLのmax_prepared_stmt_count(デフォルト16382)に達すると、新しいprepareが失敗する。Persistent connectionを多数のワーカーで使うと、ワーカー数×prepared statement数がこの上限に近づく。また、PDO::ATTR_EMULATE_PREPARESをtrueにしていると、そもそもサーバー側にprepared statementが作られないので、Com_stmt_prepareは増えないが、これは「効いていない」のではなく「使っていない」。

計測はSHOW GLOBAL STATUS LIKE 'Com_stmt_%'を前後で差分を取るのが手軽だ。ただし、グローバルステータスは他の接続の影響も受けるので、単一接続で試すならSHOW SESSION STATUSを使う。

あなたの環境では、Persistent connectionを使うことでprepared statementの再利用をどこまで許容しているか。トランザクション分離レベルやセッション変数のリセットをどう管理しているか、具体的な運用を聞きたい。

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

0 条评论

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

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