接続レジリエンシーにより、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 は接続の確立を制御します。この値を大きくしても、ステートメントの回復予算は拡張されません。
SQLSRV 接続オプションの配列 (例: ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5]) や、PDO_SQLSRV データソース名 (DSN) (例: ConnectRetryCount=2;ConnectRetryInterval=5) に、同じ再試行キーワードを設定します。 これらの設定では、失敗したトランザクションを再実行したり、アプリケーションの最初の接続呼び出しを再試行したりすることはありません。
クエリ実行中に発生する一時的なエラーのアプリケーションレベルの再試行(アイドル接続レジリエンシーはカバーしません)については、Microsoft Drivers for PHP for SQL Serverの再試行パターンを参照してください。
回復方針を選択してください
アイドル接続回復は回復可能なセッションを保持し、アプリケーションはPHP接続ハンドルを継続使用できるようにします。 これは、コマンドを再度実行するよう指示する一般的な指示ではありません。
| Failure | アイドル接続のレジリエンシー(回復力)で対応できますか? | アプリケーションのアクション |
|---|---|---|
| 最初の接続を確立できません | No. 復元するための既存のセッションはありません。 | 接続エラーを確認してください。 制限付きポリシーで一時的な失敗のみを再試行してください。 |
| アイドル状態で復旧可能な接続が切断される | ODBCは接続が再び使用された際に復旧を試みることができます。 | 意図した復旧試行に十分なステートメント タイムアウトを設定し、最終的な失敗にも適切に対処できるようにします。 |
| ステートメントの実行中にエラーが発生しました | 回復やリプレイを前提にしないでください。 | エラーを確認し、操作が完了したかどうかを判断してください。 アプリケーションの仕様上、読み取り操作を繰り返し実行しても問題ない場合に限り、その操作を再試行してください。 |
| 取引中に接続が切断される | トランザクションが継続できることを決めつけないでください。 | 壊れたセッションを中断し、トランザクションがコミットされたかどうかを確認してから、次の試みを決めましょう。 |
| クライアントは明示的にPHP接続を閉じます | No. ハンドルを閉じることはアプリケーションのクリーンアップであり、アイドル切断ではありません。 | さらに作業が必要なら新しい接続を開いてください。 |
リカバリ間隔とクエリの期限の整合性を保ってください。 例えば、プログラムは5秒間隔で2回の再接続試行と30秒のステートメントタイムアウトを使用します。 接続試行にも時間がかかります。カウントに間隔を掛けるだけでは、必ずしも回復にかかる合計時間が保証されるわけではありません。 最後のタイムアウトでもエラー処理が必要です。
解説
接続のレジリエンシーは接続がアイドル状態の場合にのみ適用されます。 トランザクション中の失敗は再接続をトリガーしません。 以下のセッション状態も再接続を妨げます:
- 一時テーブル。
- グローバルカーソルとローカルカーソル。
- トランザクションコンテキストとセッションレベルのトランザクションロック。
- アプリケーションのロック。
-
EXECUTE AS/REVERTセキュリティのコンテキスト。 - Object Linking and Embedding (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 形式の接続処理に対応するため、閉じ中括弧を 2 つ重ね、各値を中括弧で囲むことで、これらの値を 1 回エスケープします。 両方の接続はエスケープされた値を再利用します。
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 を使用しており、別の接続を通じて自身のテスト セッションを終了させるアクセス許可を必要とします。 Azure SQL DatabaseとFabricのSQLデータベースにはそれぞれ独自のサービス権限と認証要件があります。 これらのプログラムをAzure SQL Managed Instance、Azure Synapse Analytics、Warehouse in Fabricのドロップインテストとして扱わないでください。また、通常の接続が成功したからといってその復旧サポートを推測しないでください。 選択したサービスのリカバリーサポートに頼る前に確認してください。 Fabric 内の Warehouse と SQL データベースは、依然として別個のターゲットのままです。