PostgreSQL 16~18 락 대기 진단: pg_locks·pg_stat_activity로 차단 세션 찾는 순서

PostgreSQL에서 쿼리나 DDL이 오래 멈추면 먼저 대기 세션의 pid를 찾고 pg_blocking_pids(pid)로 차단 PID를 확인한다. 그다음 pg_stat_activity에서 차단 세션의 트랜잭션 시작 시각·상태·실행문을 확인한 뒤, 업무 영향이 확인된 경우에만 취소 또는 종료를 검토한다.

이 순서는 PostgreSQL 16~18의 일반적인 운영 진단에 적용한다. pg_locks만 직접 조인해 차단 관계를 추측하는 방법보다, 대기열까지 고려하는 pg_blocking_pids()를 출발점으로 삼는 편이 안전하다. 준비 트랜잭션은 차단 PID가 0으로 보일 수 있고, 병렬 쿼리는 같은 PID가 반복될 수 있다는 예외도 함께 봐야 한다.

핵심 답변

락 대기 자체가 곧 장애 원인은 아니다. 먼저 기다리는 세션과 실제 차단자를 연결하고, 차단자의 업무 소유자·트랜잭션 시간·현재 상태를 확인한다. 급하더라도 처음부터 pg_terminate_backend()를 실행하지 말고, 실행 중인 한 문장만 멈추는 pg_cancel_backend()가 충분한지 판단한다.

적용 범위와 첫 판단

항목 적용/확인 내용 운영 영향 또는 예외
대상 버전 PostgreSQL 16~18, 기본 카탈로그 뷰와 내장 함수 관리형 서비스는 뷰 열·권한을 제한할 수 있다.
첫 관찰 대상 wait_event_type = 'Lock'인 클라이언트 백엔드 I/O·CPU·네트워크 대기는 다른 진단 경로가 필요하다.
차단자 판정 pg_blocking_pids(waiter_pid)로 관계를 얻는다. 준비 트랜잭션은 PID 0, 병렬 쿼리는 중복 PID가 나올 수 있다.
조치 원칙 취소, 종료, 기다림 중 가장 작은 조치를 고른다. 세션 종료는 열린 트랜잭션의 롤백과 업무 중단을 유발한다.

락 대기를 읽는 방식

PostgreSQL은 MVCC를 사용하므로 단순 조회가 언제나 쓰기를 막는 것은 아니다. 다만 행을 수정하거나 SELECT ... FOR UPDATE로 잠그는 세션, 그리고 일부 ALTER TABLE·VACUUM FULL 같은 DDL은 충돌하는 잠금을 요구한다. 잠금은 보통 트랜잭션이 끝날 때까지 유지되므로, 짧은 UPDATE라도 커밋하지 않은 채 연결을 유지하면 뒤의 작업이 길게 기다릴 수 있다.

pg_locks는 현재 잠금 관리자에 있는 요청을 보여 주지만, 차단 관계에는 이미 대기열 앞에 서 있는 세션도 영향을 준다. pg_blocking_pids()는 충돌 잠금을 보유한 세션뿐 아니라 대기열상 앞선 소프트 차단자도 반환한다. 그래서 운영 화면에서 ‘막힌 세션’을 찾을 때는 이 함수를 먼저 쓰고, 객체·잠금 모드를 설명해야 할 때 pg_locks를 보조로 붙인다.

1. 대기 세션을 좁힌다

SELECT
    a.pid,
    a.usename,
    a.application_name,
    a.client_addr,
    a.backend_type,
    a.state,
    a.wait_event_type,
    a.wait_event,
    a.xact_start,
    a.query_start,
    now() - a.query_start AS query_age,
    a.query
FROM pg_stat_activity AS a
WHERE a.backend_type = 'client backend'
  AND a.wait_event_type = 'Lock'
ORDER BY a.query_start ASC;

실행 목적

현재 Lock 이벤트를 기다리는 클라이언트 세션만 시간순으로 찾는다. 권한 다른 사용자의 쿼리 텍스트와 상태를 충분히 보려면 일반적으로 모니터링 역할 또는 운영 계정 권한이 필요하다. 예상 결과 결과의 pid가 다음 단계의 대기자 PID가 된다. 이 SELECT는 데이터를 바꾸지 않는다.

state = 'active'wait_event_type = 'Lock'이 함께 보인다고 해서 실행문 전체가 실패한 것은 아니다. 단지 현재 잠금 획득을 기다린다는 뜻이다. 여러 행이 있으면 먼저 업무 영향이 큰 요청과 가장 오래 기다린 요청을 분리해 기록한다. 모든 대기를 한꺼번에 종료하면 차단 사슬의 원인을 잃는다.

2. 대기자와 차단자를 연결한다

SELECT
    waiter.pid AS waiting_pid,
    waiter.usename AS waiting_user,
    waiter.application_name AS waiting_app,
    waiter.query_start AS waiting_since,
    blocker.pid AS blocking_pid,
    blocker.usename AS blocking_user,
    blocker.application_name AS blocking_app,
    blocker.state AS blocking_state,
    blocker.xact_start AS blocking_xact_start,
    blocker.query_start AS blocking_query_start,
    blocker.query AS blocking_query
FROM pg_stat_activity AS waiter
CROSS JOIN LATERAL unnest(pg_blocking_pids(waiter.pid)) AS b(blocking_pid)
LEFT JOIN pg_stat_activity AS blocker
  ON blocker.pid = b.blocking_pid
WHERE waiter.backend_type = 'client backend'
  AND waiter.wait_event_type = 'Lock'
ORDER BY waiter.query_start ASC, blocker.xact_start ASC NULLS LAST;

실행 목적

pg_blocking_pids()의 배열을 행으로 풀어 대기자와 차단 세션 정보를 한 화면에서 확인한다. 적용 버전 PostgreSQL 16~18. 예상 결과 대기 PID마다 하나 이상의 차단 PID가 보이며, 차단자가 준비 트랜잭션이면 PID 0은 pg_stat_activity와 연결되지 않을 수 있다.

차단 세션이 idle in transaction이면 마지막 문장이 끝났더라도 트랜잭션은 끝나지 않았을 가능성이 있다. 반대로 active라면 큰 UPDATE나 DDL이 정상 진행 중일 수 있다. 두 상태를 같은 위험도로 취급하지 말고, xact_start와 애플리케이션 이름, 연결 원격지, 배치 창을 함께 확인한다.

3. 어떤 객체와 잠금 모드가 얽혔는지 확인한다

SELECT
    l.pid,
    n.nspname AS schema_name,
    c.relname AS relation_name,
    l.locktype,
    l.mode,
    l.granted,
    a.usename,
    a.application_name,
    a.state,
    a.xact_start
FROM pg_locks AS l
LEFT JOIN pg_class AS c
  ON c.oid = l.relation
LEFT JOIN pg_namespace AS n
  ON n.oid = c.relnamespace
LEFT JOIN pg_stat_activity AS a
  ON a.pid = l.pid
WHERE l.pid IN (12345, 12346)
ORDER BY l.pid, l.granted, n.nspname, c.relname, l.mode;

실행 목적

앞 단계에서 확인한 실제 PID만 넣어 relation 잠금의 객체·모드·허가 여부를 살핀다. 권한 시스템 카탈로그와 활동 뷰를 읽을 권한이 필요하다. 예상 결과 granted = false인 요청과 같은 객체에 잡힌 충돌 잠금을 비교할 수 있다. PID 숫자는 예시이므로 그대로 실행하지 않는다.

relation이 NULL인 행도 오류가 아니다. transactionid, virtualxid, advisory 같은 잠금은 테이블 이름으로 바로 풀리지 않는다. 특히 행 수준 충돌은 pg_locks에서 특정 행 번호로 보이지 않고 트랜잭션 ID 대기로 나타날 수 있다. 이럴 때는 객체명 하나만 보고 원인을 단정하지 말고 차단 세션의 트랜잭션과 SQL을 다시 확인한다.

작업 전 확인과 조치 기준

확인 순서 확인할 값 다음 행동
1 대기 시간, 대기 SQL, 차단 PID 일시적 경합이면 관찰 시간을 정하고 기다린다.
2 차단 세션의 사용자·앱·클라이언트·트랜잭션 시작 업무 소유자와 배치·배포 여부를 확인한다.
3 객체·잠금 모드·대기 사슬 한 세션만 원인인지, 여러 단계 사슬인지 구분한다.
4 취소 후 재조회 결과 대기가 풀렸는지 같은 진단 SQL로 확인한다.

예를 들어 배포 직전에 ALTER TABLE이 기다린다면, 차단자를 종료하기 전에 배포를 늦추는 편이 더 안전할 수 있다. 반대로 앱 연결이 idle in transaction으로 수십 분째 남아 있고 업무 소유자가 확인됐다면, 해당 연결의 현재 작업을 취소하거나 종료하는 판단이 가능하다. Oracle에서 차단 세션을 확인하는 순서와 마찬가지로, DB 제품마다 뷰는 달라도 ‘증거 확인 뒤 최소 조치’라는 운영 원칙은 같다.

4. 종료보다 취소를 먼저 검토한다

운영 환경 주의

pg_cancel_backend()는 해당 세션의 현재 쿼리를 취소한다. pg_terminate_backend()는 세션을 끊으므로 열린 트랜잭션이 롤백되고, 애플리케이션 오류·재시도 폭증·커넥션 풀 재연결이 이어질 수 있다. 소유자와 영향 범위를 확인하지 못했다면 조치 SQL을 실행하지 말고 대기 시간과 증거를 남긴다.

SELECT pg_cancel_backend(12345);

SELECT pg_terminate_backend(12345, 5000);

실행 목적

첫 문장은 특정 PID의 현재 쿼리 취소를 요청하고, 둘째 문장은 최대 5초 동안 종료를 기다린다. 권한 대상 역할의 멤버이거나 pg_signal_backend 권한이 필요하며, 슈퍼유저 백엔드는 슈퍼유저만 종료할 수 있다. 예상 결과 반환값 true는 신호 전송 또는 지정 시간 안의 종료 성공을 뜻하므로, 반드시 다음 조회로 실제 상태를 확인한다.

롤백 방법

세션 취소와 종료는 되돌리는 SQL이 없다. 종료가 필요하면 먼저 대상 PID·소유자·트랜잭션 시작 시각을 기록하고, 종료 뒤 애플리케이션 오류율과 대기 목록을 다시 확인한다. 데이터 변경 트랜잭션이었으면 PostgreSQL이 롤백을 수행할 시간을 주며, 같은 작업을 즉시 재실행하지 않는다.

조치 뒤에는 같은 기준으로 다시 본다

SELECT
    a.pid,
    a.state,
    a.wait_event_type,
    a.wait_event,
    now() - a.query_start AS query_age,
    pg_blocking_pids(a.pid) AS blocking_pids
FROM pg_stat_activity AS a
WHERE a.backend_type = 'client backend'
  AND a.wait_event_type = 'Lock'
ORDER BY a.query_start ASC;

실행 목적

취소·종료·자연 커밋 뒤에도 Lock 대기가 남았는지 확인한다. 예상 결과 대상 대기 행이 사라지거나 차단 PID 배열이 바뀐다. 대기가 남으면 방금 조치한 PID만 보지 말고 새로운 차단자를 다시 따라간다.

차단자가 사라졌는데도 요청이 느리다면 락 문제는 해소됐고 다른 병목을 찾아야 한다. 오래 열린 트랜잭션이 반복된다면 커넥션 풀의 트랜잭션 경계, 예외 처리 뒤 ROLLBACK 누락, 배치 단위와 타임아웃을 점검한다. MySQL의 metadata lock처럼 DDL이 멈추는 경우도 별도 진단이 필요하며, MySQL 8.4 metadata lock 대기 진단에서 제품별 차이를 확인할 수 있다.

흔한 실수

  • 대기 세션부터 종료하기: 기다리는 쪽을 끊어도 원래 차단자가 남아 다음 요청을 다시 막는다.
  • PID만 보고 조치하기: PID는 재사용될 수 있다. 실행 직전에 pg_stat_activity에서 사용자·앱·시작 시각을 다시 확인한다.
  • pg_locks의 relation NULL을 무시하기: transactionid·virtualxid·advisory 잠금도 경합 원인이 된다.
  • true를 해결 완료로 해석하기: 취소·종료 함수의 성공 반환 뒤에도 대기 목록과 애플리케이션 상태를 재조회해야 한다.
  • 원인 없는 재시도: 잠금 순서와 트랜잭션 경계를 고치지 않으면 같은 대기가 다시 발생한다.

마무리

PostgreSQL 락 장애에서는 ‘누가 기다리는가’보다 ‘누가 왜 막고 있는가’를 먼저 확정해야 한다. pg_blocking_pids()로 사슬을 만들고, pg_stat_activitypg_locks로 업무·트랜잭션·객체를 확인한 뒤 최소 조치를 택하면 불필요한 세션 종료와 데이터 작업 중단을 줄일 수 있다.

공식 출처