アイドル接続の回復性

PHP ドライバーのダウンロード

接続レジリエンシーにより、Microsoft ODBCドライバー for SQL Serverは、サーバーおよびセッションの状態が回復をサポートしている場合、切断されたアイドル状態の接続を復元できます。 PHP拡張はODBCに ConnectRetryCount と ConnectRetryInterval を渡しますが、再接続プロトコルは実装していません。 ODBCのドキュメントには、SQL Server 2014以降のバージョン、Azure SQL Database、Microsoft FabricのSQLデータベースのサポートが記載されています。 FabricのSQLデータベースサポートからウェアハウスサポートを推測しないでください。

キーワード 値 既定 説明
ConnectRetryCount 0 から 255 までの整数 1 中止するまでに、切断された接続の再確立を試みる最大回数。 値を0にすると、再接続は無効になります。
ConnectRetryInterval 1 から 60 までの整数 10 試行間の待機秒数。 ConnectRetryCountが0場合は無視されます。

ステートメントのクエリタイムアウトは、実行中にODBCがアイドル接続の回復に費やせる時間を制限します。 PDO::SQLSRV_ATTR_QUERY_TIMEOUT は SQLSRV 文のオプションで、QueryTimeout は PDO_SQLSRV で設定します。 LoginTimeout は接続の確立を制御します。LoginTimeout を増やしても、ステートメントの回復予算は延長されません。

同じリトライキーワードを、たとえば ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5] のような SQLSRV の接続オプション配列や、たとえば ConnectRetryCount=2;ConnectRetryInterval=5 のような PDO_SQLSRV データ ソース名 (DSN) に設定します。 これらの設定は、失敗したトランザクションを再実行したり、アプリケーションの最初の接続呼び出しを再試行したりしません。

クエリ実行中に発生する一時的なエラーのアプリケーションレベルの再試行(アイドル接続レジリエンシーはカバーしません)については、Microsoft Drivers for PHP for SQL Serverの再試行パターンを参照してください。

回復方針を選択してください

アイドル接続回復は回復可能なセッションを保持し、アプリケーションはPHP接続ハンドルを継続使用できるようにします。 これはコマンドを再度実行する一般的な命令ではありません。

Failure アイドル接続のレジリエンシー(回復力)で対応できますか? アプリケーション アクション
初回接続を確立できません No. 復元するための既存のセッションはありません。 接続エラーを確認してください。 制限付きポリシーで一時的な失敗のみを再試行してください。
アイドル状態で復旧可能な接続が切断される ODBCは接続が再び使用された際に復旧を試みることができます。 意図した復旧試行のために十分なステートメントタイムアウトを確保し、最終的な失敗も処理します。
ステートメントの実行に失敗する 回復やリプレイを前提にしないでください。 エラーを確認し、操作が完了したかどうかを判断してください。 読み取りを再試行するのは、それを繰り返してもアプリケーションで許容される場合にのみしてください。
取引中に接続が切断される 取引を続行できると思い込まないでください。 壊れたセッションを中断し、トランザクションがコミットされたかどうかを確認してから、次の試みを決めましょう。
クライアントは明示的にPHP接続を閉じます No. ハンドルを閉じることはアプリケーションのクリーンアップであり、アイドル切断ではありません。 さらに作業が必要なら新しい接続を開いてください。

回復間隔やクエリの期限を一定に保ちましょう。 例えば、プログラムは5秒間隔で2回の再接続試行と30秒のステートメントタイムアウトを使用します。 接続試行にも時間がかかります。カウントに間隔を掛けるだけでは、必ずしも回復にかかる合計時間が保証されるわけではありません。 最後のタイムアウトでもエラー処理が必要です。

解説

接続のレジリエンシーは接続がアイドル状態の場合にのみ適用されます。 トランザクション中の失敗は再接続をトリガーしません。 以下のセッション状態も再接続を妨げます:

  • 一時テーブル。
  • グローバルカーソルとローカルカーソル。
  • トランザクションコンテキストとセッションレベルのトランザクションロック。
  • アプリケーションがフリーズします。
  • EXECUTE AS / REVERT セキュリティ コンテキスト。
  • オブジェクトのリンクと埋め込み(OLE)自動化ハンドル。
  • 準備済みの拡張マークアップ言語(XML)ハンドル。
  • トレース フラグ。

例

各プログラムはテスト用SQL Serverインスタンスへの2つの接続を開きます。 2番目の接続は最初のセッションを KILLで終了させます。 最初の接続に対する後続のクエリは、ODBCのアイドル接続回復を検証します。 クエリはインライン行を使用しているため、テーブルの設定は不要です。 プログラムは接続を切断する前に各文を解放し、回復したクエリの値と変更されたサーバーログイン時間の両方を確認します。 セッションIDだけでは回復の証拠にはなりません。SQL Server ではセッション ID を再利用できるためです。

Caution

この例はテストインスタンスに対してのみ実行してください。 ログインには、KILLでセッションを終了する権限が必要です。 コードは、自身がたった今開いたセッションのみを終了します。 他のアプリケーションのセッションIDを置き換えないでください。

プロセス環境に SQL_SERVER、 SQL_DATABASE、 SQL_USER、 SQL_PASSWORD を設定しましょう。 SQL_SERVERと一致する名前の信頼できるサーバー証明書を設定しましょう。 この例では、テスト用SQL Serverインスタンスに対してSQL認証を用いています。 これはFabricやマネージドID認証の例ではありません。

実際のデータベース名、ユーザー名、パスワードを環境変数に保存します。 各プログラムは、PHPドライバのODBCスタイルの接続処理のために、閉じ波かっこを倍増させ、各値を波かっこで囲むことで、これらの値を一度だけエスケープします。 両方の接続はエスケープされた値を再利用します。

SQLSRV

プログラムを recover-sqlsrv.phpとして保存し、 php recover-sqlsrv.phpを実行してください。 ヘルパーは SQLSRV の失敗時に例外をスローします。アプリケーション文の再試行はしません。

<?php
function requireSqlsrv($result)
{
    if ($result === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    return $result;
}

$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$connectionInfo = [
    'Database' => $config['SQL_DATABASE'],
    'UID' => $config['SQL_USER'],
    'PWD' => $config['SQL_PASSWORD'],
    'Encrypt' => true,
    'TrustServerCertificate' => false,
    'LoginTimeout' => 30,
    'ConnectRetryCount' => 2,
    'ConnectRetryInterval' => 5,
];
$conn = requireSqlsrv(sqlsrv_connect($config['SQL_SERVER'], $connectionInfo));
$breaker = null;
try {
    $breaker = requireSqlsrv(sqlsrv_connect($config['SQL_SERVER'], $connectionInfo));
    $sessionQuery = "SELECT @@SPID AS SessionId, "
        . "CONVERT(varchar(33), login_time, 126) AS LoginTime "
        . "FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
    $idStatement = requireSqlsrv(sqlsrv_query($conn, $sessionQuery));
    $before = requireSqlsrv(sqlsrv_fetch_array($idStatement, SQLSRV_FETCH_ASSOC));
    sqlsrv_free_stmt($idStatement);
    if (!is_array($before) || !is_int($before['SessionId']) || $before['SessionId'] <= 0) {
        throw new RuntimeException('Expected the current session identity.');
    }
    $spid = $before['SessionId'];

    $query = 'SELECT id FROM (VALUES (1), (2), (3)) AS Demo(id) ORDER BY id';
    $options = ['Scrollable' => SQLSRV_CURSOR_CLIENT_BUFFERED, 'QueryTimeout' => 30];
    $stmt = requireSqlsrv(sqlsrv_query($conn, $query, [], $options));
    if (requireSqlsrv(sqlsrv_num_rows($stmt)) !== 3) {
        throw new RuntimeException('Expected three rows before the disconnect.');
    }
    sqlsrv_free_stmt($stmt);

    // KILL doesn't accept a parameter marker; $spid is a checked server-returned integer.
    $kill = requireSqlsrv(sqlsrv_query($breaker, 'KILL ' . $spid));
    sqlsrv_free_stmt($kill);
    // Allow the simulated disconnect to reach the client before the next query.
    sleep(1);

    $stmt = requireSqlsrv(sqlsrv_query($conn, $query, [], $options));
    $ids = [];
    while (($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) !== null) {
        $row = requireSqlsrv($row);
        $ids[] = $row['id'];
    }
    if ($ids !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected recovered rows or PHP types.');
    }
    sqlsrv_free_stmt($stmt);

    $idStatement = requireSqlsrv(sqlsrv_query($conn, $sessionQuery));
    $after = requireSqlsrv(sqlsrv_fetch_array($idStatement, SQLSRV_FETCH_ASSOC));
    sqlsrv_free_stmt($idStatement);
    if (!is_array($after) || $after['LoginTime'] === $before['LoginTime']) {
        throw new RuntimeException('Expected a new physical session.');
    }
    echo "Recovered connection: 3 matching rows; login time changed.\n";
} finally {
    if (is_resource($breaker)) {
        sqlsrv_close($breaker);
    }
    sqlsrv_close($conn);
}

PDO_SQLSRV

この別プログラムを recover-pdo.phpとして保存し、同じ環境と権限で php recover-pdo.php 実行してください。 数値フェッチを明示的に設定し、整数アサーションがSQLSRVプログラムと一致するようにします。 PDOは sqlsrv_errors() コールを持ちません。例外モードでは、操作が失敗すると PDOException がスローされます。

<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$server = $config['SQL_SERVER'];
$database = $config['SQL_DATABASE'];
$dsn = "sqlsrv:Server=$server;Database=$database;Encrypt=true;"
    . "TrustServerCertificate=false;LoginTimeout=30;"
    . "ConnectRetryCount=2;ConnectRetryInterval=5";
$attributes = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
    PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 30,
];
$pdo = new PDO($dsn, $config['SQL_USER'], $config['SQL_PASSWORD'], $attributes);
$breaker = null;
$stmt = null;
try {
    $breaker = new PDO($dsn, $config['SQL_USER'], $config['SQL_PASSWORD'], $attributes);
    $sessionQuery = "SELECT @@SPID AS SessionId, "
        . "CONVERT(varchar(33), login_time, 126) AS LoginTime "
        . "FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
    $stmt = $pdo->query($sessionQuery);
    $before = $stmt->fetch(PDO::FETCH_ASSOC);
    $stmt = null;
    if (!is_array($before) || !is_int($before['SessionId']) || $before['SessionId'] <= 0) {
        throw new RuntimeException('Expected the current session identity.');
    }
    $spid = $before['SessionId'];
    $query = 'SELECT id FROM (VALUES (1), (2), (3)) AS Demo(id) ORDER BY id';
    $stmt = $pdo->query($query);
    if ($stmt->fetchAll(PDO::FETCH_COLUMN) !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected initial rows or PHP types.');
    }
    $stmt = null;

    // KILL requires a checked integer, not a parameter marker.
    $stmt = $breaker->query('KILL ' . $spid);
    $stmt = null;
    sleep(1);

    $stmt = $pdo->query($query);
    if ($stmt->fetchAll(PDO::FETCH_COLUMN) !== [1, 2, 3]) {
        throw new RuntimeException('Unexpected recovered rows or PHP types.');
    }
    $stmt = null;
    $stmt = $pdo->query($sessionQuery);
    $after = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!is_array($after) || $after['LoginTime'] === $before['LoginTime']) {
        throw new RuntimeException('Expected a new physical session.');
    }
    echo "Recovered connection: 3 matching rows; login time changed.\n";
} finally {
    $stmt = null;
    $breaker = null;
    $pdo = null;
}

ODBCが接続を復元した際のいずれのプログラムからの期待出力も以下の通りです:

Recovered connection: 3 matching rows; login time changed.

テストは意図的にアイドル状態のセッションを終了させます。 ネットワークパーティション、サービスフェイルオーバー、アクティブトランザクションリカバリー、書き込みリプレイ、Azureのスロットリングはテストしません。 成功を、すべての中断されたアプリケーション操作が安全に繰り返せる証拠と解釈しないでください。

失敗した復旧の診断

症状: チェック
KILL は拒否される 許可されたテストIDを使用するか、管理者に制御されたテストの実施を依頼してください。 別のセッションIDに置き換えないでください。
最初のクエリはすでに失敗しています 復旧テストの前に接続、証明書、クエリ、権限を修正してください。
KILL の後のクエリは失敗します ConnectRetryCount、クエリのタイムアウト、ODBC の診断、セッション状態が回復可能かどうかを確認しましょう。 無条件の再挑戦で誤りを隠さないでください。
新しい接続を開くと一時的なテーブルが消えます アプリケーションが作成した代替接続は古いセッションではありません。 自動状態の再現を前提とするのではなく、セッション依存の作業を再設計すること。
書き込みで接続エラーが返されます 再挑戦する前に結果を明確にしましょう。 クライアントエラーだけではサーバーがロールバックしたとは限りません。

診断は保護ログに保管してください。 SQLSRVは複数のレコードを含む可能性のあるエラーコレクションを公開します。PDOは例外を通じてSQLSTATEおよびドライバーの詳細を提供します。 パスワードやトークンを記録せずに、操作とそのタイムアウト設定を記録しましょう。 詳細については 、「ログ活動」をご覧ください。

適用対象

ODBCとSQLターゲットは自動再接続を実行します。 PHPは設定を渡し、成功や失敗を報告します。 一時オブジェクトの再作成も、アプリケーションの変数の復元も、トランザクションの再生もしません。

制御プログラムはSQL Serverを使用し、2つ目の接続を通じて自分のテストセッションを終了するには許可が必要です。 Azure SQL DatabaseとFabricのSQLデータベースにはそれぞれ独自のサービス権限と認証要件があります。 これらのプログラムをAzure SQL Managed Instance、Azure Synapse Analytics、Warehouse in Fabricのドロップインテストとして扱わないでください。また、通常の接続が成功したからといってその復旧サポートを推測しないでください。 選択したサービスのリカバリーサポートに頼る前に確認してください。 Fabric 内の Warehouse と SQL データベースは、依然として別個のターゲットのままです。