본문으로 건너뛰기
15.3 락 대기·세션 문제

15.3 락 대기·세션 문제

PostgreSQL 사고의 절반은 락 대기·세션 폭주. 진단 SQL과 즉시 대응 절차를 정리합니다.

대기 세션 진단

SELECT pid, usename, application_name, client_addr,
       state, wait_event_type, wait_event,
       xact_start, now() - xact_start AS xact_age,
       query_start, now() - query_start AS query_age,
       LEFT(query, 100) AS query
  FROM pg_stat_activity
 WHERE state != 'idle'
 ORDER BY xact_start NULLS LAST;
컬럼의미
wait_event_typeLock/LWLock/IO/Client/Activity
wait_event구체 — transactionid, relation, BufferContent
xact_age트랜잭션 길이 — 5분 이상이면 알람

블로킹 그래프

SELECT bl.pid AS blocked, bl.usename AS u1, bl.query AS blocked_query,
       kl.pid AS blocker, kl.usename AS u2, kl.query AS blocker_query
  FROM pg_stat_activity bl
  JOIN LATERAL (
    SELECT * FROM pg_stat_activity
     WHERE pid = ANY(pg_blocking_pids(bl.pid))
  ) kl ON true
 WHERE cardinality(pg_blocking_pids(bl.pid)) > 0;

체인 추적:

WITH RECURSIVE chain AS (
  SELECT pid, pg_blocking_pids(pid) AS blockers, 0 AS depth
    FROM pg_stat_activity
   WHERE cardinality(pg_blocking_pids(pid)) > 0
  UNION ALL
  SELECT a.pid, pg_blocking_pids(a.pid), c.depth + 1
    FROM chain c
    JOIN pg_stat_activity a ON a.pid = ANY(c.blockers)
   WHERE c.depth < 5
)
SELECT * FROM chain;

대기 트리의 root = 사고의 원인입니다.

즉시 대응 — pg_cancel / pg_terminate

SELECT pg_cancel_backend(<pid>);     -- 현재 쿼리 취소
SELECT pg_terminate_backend(<pid>);  -- 세션 강제 종료
명령효과
pg_cancel_backend현재 SQL만 cancel. 트랜잭션은 살아 있음
pg_terminate_backend트랜잭션 전체 종료. connection 끊김

우선순위: cancel 먼저, 안 풀리면 terminate.

흔한 락 시나리오

A. ALTER TABLE이 SELECT 차단

ALTER TABLE orders ADD COLUMN ...

ACCESS EXCLUSIVE — 모든 SELECT 차단합니다. 운영 중 큰 ALTER가 수 분~수 시간 동안 클러스터 정지처럼 보이는 사례.

대응:

  • 짧은 lock_timeout으로 ALTER 자체 취소
SET lock_timeout = '5s';
ALTER TABLE orders ADD COLUMN ...;   -- 5초 안 잡히면 ERROR
  • 작업 분리 — ADD COLUMN NULLABLE (메타데이터만, PG 11+ 빠름) + 별도 UPDATE

B. row-level lock 폭주

UPDATE accounts SET balance = ... WHERE id = popular_id
-- 같은 popular row를 100 트랜잭션이 동시에

대응:

  • application 측 row-level 동시성 줄임 (advisory lock으로 직렬화)
  • 데이터 모델 검토 — 핫 row를 여러 partition으로 분할
  • SKIP LOCKED로 queue 패턴

C. autovacuum이 락 잡고 있음

SELECT pid, query, wait_event FROM pg_stat_activity
 WHERE backend_type = 'autovacuum worker';

대부분 SHARE UPDATE EXCLUSIVE만 — DDL이 아니면 비차단. 그러나 VACUUM FULL이면 ACCESS EXCLUSIVE — 사고입니다.

대응: autovacuum worker를 cancel하면 빠르게 해제.

D. 잊혀진 prepared transaction

SELECT * FROM pg_prepared_xacts;

비어 있어야 정상. row가 있고 prepared 시간이 옛날이면 ROLLBACK PREPARED.

세션 폭주

max_connections 한계

FATAL: too many connections
-- 사용자별 연결 수
SELECT usename, count(*)
  FROM pg_stat_activity
 GROUP BY usename
 ORDER BY count(*) DESC;

-- application별
SELECT application_name, count(*)
  FROM pg_stat_activity
 GROUP BY application_name;

-- state별
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;

idle in transaction이 많으면 application connection leak. idle이 많으면 풀 크기 너무 큽니다.

대응:

  • pgBouncer 도입(13.4)
  • application connection lifecycle 점검
  • idle_in_transaction_session_timeout

LWLock 경합

SELECT wait_event, count(*) FROM pg_stat_activity
 WHERE wait_event_type = 'LWLock'
 GROUP BY wait_event;
LWLock원인
WALInsert매우 높은 commit TPS
BufferContenthot row 동시 수정
ProcArray매우 많은 연결 (스냅샷 계산)
SubtransSLRUSAVEPOINT 폭주 (14.4)
WALWriteWAL fsync 한계
WALWriteLock같음

대응:

  • WALInsert·WALWrite: WAL 디스크 분리, sync 모드 조정
  • ProcArray: 연결 수 줄임 (pgBouncer)
  • SubtransSLRU: SAVEPOINT 정리

advisory lock 진단

SELECT pid, locktype, classid, objid, mode, granted, query
  FROM pg_locks l
  JOIN pg_stat_activity a USING (pid)
 WHERE locktype = 'advisory';

pg_advisory_unlock_all() 호출이 빠진 application이면 세션 단위 lock이 누적.

운영 알람

지표WARNCRIT
pg_blocking_pids 응답 있는 PID 수520
트랜잭션 5분 초과1개
트랜잭션 30분 초과1개
idle in transaction 1분5개20개
total active connectionsmax_connections × 70%× 90%
LWLock wait 비중일시지속

사고 절차

  1. 알람 → 대기 세션 목록 캡처
  2. 블로킹 트리 식별
  3. root blocker 결정
  4. cancel·terminate (필요 시)
  5. 트랜잭션이 다시 진행 확인
  6. 사후 분석 — 왜 발생했나, 예방

운영 체크리스트

-- 한 번에 모든 진단
SELECT 'long tx' AS issue, count(*) FROM pg_stat_activity WHERE xact_start < now() - interval '5 min' AND state != 'idle'
UNION ALL
SELECT 'idle in tx',     count(*) FROM pg_stat_activity WHERE state = 'idle in transaction' AND state_change < now() - interval '5 min'
UNION ALL
SELECT 'blocked',        count(*) FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0
UNION ALL
SELECT 'prepared',       count(*) FROM pg_prepared_xacts
UNION ALL
SELECT 'replication slot inactive', count(*) FROM pg_replication_slots WHERE NOT active;

이 한 번의 쿼리로 주요 위험 신호를 잡습니다.

pg_terminate_backend는 마지막 수단. application·DBA에게 미리 알린 후. 자동 모니터링이 self-healing으로 자동 terminate한다면 알람·로깅 포함합니다.

정리

  • pg_blocking_pids() + pg_stat_activity가 락 진단의 핵심
  • ALTER TABLE의 ACCESS EXCLUSIVE가 가장 흔한 차단
  • 잊혀진 prepared transaction은 영구 차단
  • LWLock 경합은 공유 자원 한계 — 별도 분리·풀러 대응
  • 사고 시 cancel → terminate 순서, 사후 분석 필수

다음 절(15.4)에서는 디스크·메모리 자원 사고 — 자원 부족을 봅니다.