연결 복원력은 Microsoft ODBC 드라이버 for SQL Server가 서버와 세션 상태가 복구를 지원할 때 끊긴 유휴 연결을 복원할 수 있게 합니다. PHP 확장 기능은 ConnectRetryCount 및 ConnectRetryInterval를 ODBC에 전달하며, 재연결 프로토콜을 구현하지 않습니다. ODBC 문서에는 SQL Server 2014 및 이후 버전, Azure SQL Database, Microsoft Fabric의 SQL 데이터베이스 지원이 나와 있습니다. Fabric에서 SQL 데이터베이스 지원으로 웨어하우스 지원을 추론하지 마세요.
| 키워드 | 값 | 기본 | 설명 |
|---|---|---|---|
ConnectRetryCount |
0과 255(포함) 사이의 정수 | 1 | 포기하기 전에 끊어진 연결을 다시 설정하는 최대 시도 횟수입니다.
0 값이면 재연결이 비활성화됩니다. |
ConnectRetryInterval |
1과 60(포함) 사이의 정수 | 10 | 시도 사이에 기다려야 하는 초 수.
ConnectRetryCount이 0이면 무시됩니다. |
SQL 문의 쿼리 타임아웃은 실행 중 ODBC가 유휴 연결을 복구하는 데 소요할 수 있는 시간을 제한합니다.
QueryTimeout를 SQLSRV 문 옵션으로 설정하거나 PDO_SQLSRV의 경우 PDO::SQLSRV_ATTR_QUERY_TIMEOUT를 설정하세요.
LoginTimeout 연결 설정을 제어합니다; 이를 늘리는 것은 문의 복구 예산을 늘리지 않습니다.
동일한 재시도 키워드를 SQLSRV 연결 옵션 배열(예: ["ConnectRetryCount" => 2, "ConnectRetryInterval" => 5]) 또는 PDO_SQLSRV 데이터 소스 이름(DSN)(예: ConnectRetryCount=2;ConnectRetryInterval=5)에 설정하세요. 이 설정들은 실패한 트랜잭션을 재처리하거나 애플리케이션의 초기 연결 호출을 다시 시도하지 않습니다.
쿼리가 실행 중에 발생하는 일시적 오류에 대한 애플리케이션 수준의 재시도(유휴 연결 복원력은 이를 포함하지 않음)에 대해서는 Microsoft Drivers for PHP for SQL Server의 재시도 패턴을 참조하세요.
회복 정책을 선택하세요
유휴 연결 복구는 복구 가능한 세션을 보존하여 애플리케이션이 PHP 연결 핸들을 계속 사용할 수 있도록 합니다. 명령을 다시 실행하라는 일반적인 명령이 아닙니다.
| Failure | 유휴 연결 탄력성이 이를 처리하나요? | 애플리케이션 동작 |
|---|---|---|
| 초기 연결을 설정할 수 없습니다 | No. 복원할 기존 세션이 없습니다. | 연결 오류를 점검하세요. 제한된 정책으로 일시적 실패에만 재시도하세요. |
| 유휴 상태에서 복구 가능한 연결이 끊깁니다 | ODBC는 연결이 다시 사용될 때 복구를 시도할 수 있습니다. | 의도된 복구 시도를 위해 충분한 statement timeout을 허용하고, 최종 실패도 처리합니다. |
| 실행 중에 문이 실패합니다 | 회복이나 리플레이를 가정하지 마세요. | 오류를 점검하고 작업이 완료되었는지 확인하세요. 읽기를 다시 수행해도 애플리케이션에 허용되는 경우에만 재시도하세요. |
| 거래 중에 연결이 끊어집니다 | 거래가 계속될 거라고 가정하지 마세요. | 깨진 세션을 포기하고 트랜잭션이 커밋되었는지 확인한 후 다시 시도할지 결정하세요. |
| 클라이언트는 명시적으로 PHP 연결을 종료합니다 | No. 핸들을 닫는 것은 애플리케이션 정리이지, 유휴 연결 해제가 아닙니다. | 추가 작업이 필요하면 새 연결을 열어보세요. |
복구 간격과 쿼리 마감 시간을 일정하게 유지하세요. 예를 들어, 프로그램들은 5초 간격과 30초 타임아웃으로 두 번의 재연결 시도를 사용합니다. 연결 시도에도 시간이 걸리며; 카운트에 간격을 곱하는 것이 반드시 전체 복구 시간을 보장하는 것은 아닙니다. 최종 타임아웃도 오류 처리가 필요합니다.
설명
연결 복원력은 연결이 유휴 상태일 때만 적용됩니다. 거래 중 실패가 발생하면 재연결이 일어나지 않습니다. 다음 세션 상태 역시 재연결을 방지합니다:
- 임시 테이블.
- 글로벌 및 로컬 커서.
- 트랜잭션 컨텍스트와 세션 수준의 트랜잭션 락.
- 애플리케이션이 멈춥니다.
-
EXECUTE AS/REVERT보안 맥락. - 객체 연결 및 임베딩(OLE) 자동화 핸들.
- 미리 생성된 XML 핸들.
- 추적 플래그.
예시
각 프로그램은 테스트 SQL Server 인스턴스에 두 개의 연결을 엽니다. 두 번째 연결은 KILL로 첫 번째 세션을 종료합니다. 첫 번째 연결에 대한 후속 쿼리는 ODBC의 유휴 연결 복구 기능을 시험합니다. 쿼리는 인라인 행을 사용하기 때문에 테이블 설정이 필요하지 않습니다. 프로그램은 연결을 끊기 전에 각 문장을 해제하고, 복구된 쿼리의 값과 변경된 서버 로그인 시간을 모두 확인합니다. 세션 ID만으로는 복구 증거가 되지 않습니다. SQL Server가 이를 재사용할 수 있기 때문입니다.
Caution
이 예제는 테스트 인스턴스에 대해서만 실행하세요. 로그인은 KILL로 세션을 종료하려면 허가가 필요합니다. 코드는 방금 열린 세션만 종료합니다. 다른 애플리케이션의 세션 ID를 대체하지 마세요.
SQL_SERVER, SQL_DATABASE, SQL_USER, 그리고 SQL_PASSWORD를 프로세스 환경에 설정하세요.
SQL_SERVER와 이름이 일치하는 신뢰할 수 있는 서버 인증서를 구성하세요. 이 예시는 테스트 SQL Server 인스턴스에 SQL 인증을 사용합니다. 이것은 Fabric이나 관리 신원 인증 예시가 아닙니다.
실제 데이터베이스 이름, 사용자 이름, 비밀번호를 환경 변수에 저장하세요. 각 프로그램은 PHP 드라이버의 ODBC 스타일 연결 처리를 위해 닫는 중괄호를 두 배로 늘리고 각 값을 중괄호로 감싸서 이 값들을 한 번 이스케이프합니다. 두 연결 모두 이스케이프된 값을 재사용합니다.
SQLSRV
프로그램을 recover-sqlsrv.php로 저장하고 php recover-sqlsrv.php를 실행하십시오. 헬퍼는 SQLSRV 실패 시 예외를 발생시키며, 애플리케이션의 SQL 문은 다시 시도하지 않습니다.
<?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 또는 Fabric의 Warehouse용 드롭인 테스트로 간주하거나, 일반 연결이 성공했다고 해서 해당 프로그램의 복구 지원 여부를 추론하지 마세요. 선택한 서비스에 의존하기 전에 해당 서비스의 복구 지원을 확인하세요. Fabric 내 Warehouse와 SQL 데이터베이스는 여전히 별개의 타깃으로 남아 있습니다.