PostgreSQL 교착 상태(Deadlock) 현상 및 대응 방법
교착 상태의 개념과 발생 원인
교착 상태는 두 개 이상의 트랜잭션이 서로의 자원 해방을 무한정 기다리면서 모든 트랜잭션이 진행되지 않는 상황을 말합니다. PostgreSQL에서 교착 상태는 주로 여러 트랜잭션이 동일한 자원을 다른 순서로 요청할 때 발생합니다. 예를 들어:
- 트랜잭션 T1이 자원 A의 잠금을 보유하고 자원 B의 잠금을 요청합니다.
- 트랜잭션 T2가 자원 B의 잠금을 보유하고 자원 A의 잠금을 요청합니다.
이 경우 T1과 T2는 서로를 기다리는 순환 상태에 빠져 교착 상태가 형성됩니다.
PostgreSQL의 교착 상태 자동 감지 및 해제
PostgreSQL은 내장된 교착 상태 감지 메커니즘을 통해 그래프 기반 알고리즘으로 트랜잭션 간의 잠금 의존 관계를 주기적으로 스캔합니다. 교착 상태 루프가 감지되면 시스템은 자동으로 한 트랜잭션을 희생자로 선택(보통 실행 시간이 가장 짧거나 최근 시작된 트랜잭션)하여 롤백하고 해당 트랜잭션이 보유한 잠금을 해제함으로써 교착 상태를 해결합니다. 이 과정은 사용자에게 투명하지만, 트랜잭션 롤백으로 인해 비즈니스 로직이 중단될 수 있습니다.
교착 상수 수동 식별 및 해제 방법
자동 감지가 제때 발생하지 않거나 능동적인 개입이 필요한 경우 다음 단계로 수동으로 처리할 수 있습니다:
1. 활성 트랜잭션 및 잠금 상태 조회
SELECT spid, 상태, 실행쿼리, 시작시간
FROM pg_활동세션
WHERE 상태 = '활성' AND 대기이벤트유형 = 'Lock';
이 쿼리는 잠금을 기다리는 프로세스 ID(`spid`), 상태, 실행 중인 SQL 문장 및 시작 시간을 반환합니다.
2. 차단 프로세스 종료
- 트랜잭션 취소(정상 종료 시도):
SELECT pg_취소_백엔드(spid);
트랜잭션이 응답하지 않을 경우 강제 종료:
SELECT pg_강제종료_백엔드(spid);
- 종료 후: 종료된 트랜잭션의 잠금이 즉시 해제되며 다른 트랜잭션이 계속 실행될 수 있습니다.
3. 교착 상태 근본 원인 분석
SELECT l.잠금종류, l.데이터베이스, l.spid, l.모드, t.테이블이름
FROM pg_잠금 l
JOIN pg_테이블 t ON l.관계 = t.oid
WHERE t.테이블이름 = '대상테이블명';
이 쿼리는 구체적으로 잠금이 걸린 테이블 및 잠금 유형(예: 행 잠금, 테이블 잠금)을 식별하여 트랜잭션 설계를 최적화하는 데 도움을 줍니다.
교착 상태 예방을 위한 최적의 방법
1. 대형 트랜잭션 분할
긴 실행 시간을 가진 트랜잭션을 여러 개의 소규모 트랜잭션으로 분할하여 잠금 보유 시간을 줄입니다. 예를 들어:
-- 원본 대형 트랜잭션
BEGIN;
테이블1 UPDATE SET 컬럼1 = 값1 WHERE 조건;
테이블2 UPDATE SET 컬럼2 = 값2 WHERE 조건;
COMMIT;
-- 최적화된 소규모 트랜잭션
BEGIN; 테이블1 UPDATE SET 컬럼1 = 값1 WHERE 조건; COMMIT;
BEGIN; 테이블2 UPDATE SET 컬럼2 = 값2 WHERE 조건; COMMIT;
2. 잠금 획득 순일화
모든 트랜잭션이 동일한 순서로 자원에 접근하도록 보장합니다(예: 먼저 테이블 A를 조작한 후 테이블 B를 조작).
3. 인덱스 설계 최적화
고빈도 쿼리 조건에 대해 합리적인 인덱스를 생성하여 전체 테이블 스캔으로 인한 테이블 레벨 잠금 증가를 방지합니다. 예를 들어:
CREATE INDEX idx_계정번호 ON 계정테이블(계정번호);
4. 격리 수준 조정
비즈니스 허용 시 격리 수준을 `REPEATABLE READ`(RR)에서 `READ COMMITTED`(RC)로 낮춰 간격 잠금(Gap Lock)으로 인한 교착 상태를 줄입니다.
5. 트랜잭션 보유 시간 최소화
트랜잭션 내에서 시간이 많이 소요되는 작업(네트워크 요청, 파일 I/O 등)을 수행하지 않고, 가능한 한 빨리 트랜잭션을 커밋하거나 롤백합니다.
PostgreSQL 테이블 잠금 관리
PostgreSQL에서 테이블 잠금은 일반적으로 트랜잭션이 자동으로 관리하지만(트랜잭션 종료 시 해제), 필요에 따라 수동으로 잠금을 해제해야 할 수도 있습니다(예: 차단 트랜잭션 종료). 다음은 구체적인 조작 방법입니다:
1. 테이블 잠금 보유자 확인
먼저 현재 어떤 세션(트랜잭션)이 대상 테이블의 잠금을 보유하고 있는지 쿼리합니다:
SELECT
잠금.잠금종류,
잠금.관계::regclass AS 테이블이름,
잠금.모드 AS 잠금모드,
활동세션.spid AS 프로세스ID,
활동세션.쿼리 AS 실행중인쿼리,
활동세션.상태 AS 트랜잭션상태
FROM
pg_잠금 잠금
JOIN
pg_활동세션 활동세션 ON 잠금.spid = 활동세션.spid
WHERE
잠금.관계::regclass = '대상테이블명'::regclass;
주요 필드 설명:
- 잠금모드: 잠금 유형(예: AccessExclusiveLock, RowExclusiveLock 등)
- 프로세스ID: 잠금을 보유한 세션의 PID
- 실행중인쿼리: 현재 세션에서 실행 중인 SQL(비어 있을 수 있음, 유휴 트랜잭션을 의미)
2. 잠금 보유 트랜잭션 종료
쿼리에서 얻은 프로세스ID를 기반으로 해당 세션을 종료하여 잠금을 해제합니다:
방법 1: 정상 취소 권장
SELECT pg_취소_백엔드(실제_PID); -- 실제 프로세스ID로 교체
기능: 세션을 종료하려고 시도하며 트랜잭션이 롤백되도록 허용(실행 중인 쿼리를 즉시 중단하지 않음).
적용 시나리오: 세션이 시간이 많이 걸리는 작업을 실행 중이지만 완전히 차단되지 않은 경우.
방법 2: 강제 종료(즉시 적용)
SELECT pg_강제종료_백엔드(실제_PID); -- 실제 프로세스ID로 교체
기능: 세션을 강제로 종료하며 트랜잭션이 즉시 롤백됩니다.
위험: 일부 작업이 완료되지 않을 수 있음(예: 커밋되지 않은 데이터 손실).
3. 잠금 해제 여부 확인
다시 1단계의 쿼리를 실행하여 대상 테이블에 활성 잠금이 없는지 확인합니다:
SELECT * FROM pg_잠금 WHERE 관계::regclass = '대상테이블명'::regclass;
결과가 비어 있으면 잠금이 해제된 것입니다.
4>테이블 잠금 장기 보유 방지
향후 테이블 잠금 차단이 다시 발생하는 것을 방지하기 위해 다음 조치를 취할 수 있습니다:
1. 트랜잭션 시간 단축: 트랜잭션 내에서 시간이 많이 소요되는 작업(외부 API 호출 등)을 수행하지 않습니다.
2. 잠금 수준 조정: 낮은 격리 수준(예: READ COMMITTED) 사용 또는 명시적으로 잠금 유형 지정:
BEGIN;
LOCK TABLE 대상테이블명 IN ROW EXCLUSIVE MODE; -- 동시 읽기/쓰기 허용
-- 작업 수행...
COMMIT;
3. 모니터링 및 경고: 도구(예: pg_활동세션 또는 외부 모니터링)를 통해 장기 트랜잭션을 실시간으로 감지합니다.
주의사항
- 권한 요구사항: pg_취소_백엔드 또는 pg_강제종료_백엔드 실행에는 슈퍼 사용자 권한 또는 동일한 역할 권한이 필요합니다.
- 데이터 일관성: 강제 종료로 인해 트랜잭션이 롤백될 수 있으므로 비즈니스가 이러한 예외를 처리할 수 있도록 해야 합니다.
- 시스템 테이블 잠금: 잠금이 PostgreSQL 내부 작업(예: VACUUM)에 의해 보유된 경우 작업이 완료될 때까지 기다려야 하며 강제 종료를 피해야 합니다.
위 단계를 통해 PostgreSQL의 테이블 잠금을 안전하게 해제하고 잠금 관리 전략을 최적화할 수 있습니다.