데이터베이스 연구실.log

잠금과 대기 세션

세 창의 UPDATE 잠금 재현, pg_stat_activity와 pg_blocking_pids 원인 추적 및 누적 통계의 초당 수치 계산

pg_stat_activitypg_blocking_pidsidle in transactiontransactionidpg_stat_database

#핵심 요약

실무에서 가장 흔한 잠금 장애는 누군가 BEGIN 후 UPDATE하고 커밋하지 않아 다른 세션이 같은 행을 기다리는 경우임

psql은 기본 자동 커밋이라 잠금을 붙잡아 두려면 BEGIN으로 트랜잭션을 먼저 열어야 함(Oracle과 다른 점)

기다리는 세션은 active이고 wait_event_type = Lock으로 보이지만, 원인 세션은 idle in transaction이라 state = 'active' 조회에 나오지 않음

pg_blocking_pids(pid)가 그 세션을 막는 PID 배열을 돌려주므로 이것으로 홀더를 찾고, 홀더의 query에 남은 마지막 SQL을 봄

#개념과 동작 원리

창 역할 상태
A 홀더, lock_demo id 1을 UPDATE하고 미커밋 idle in transaction
B 웨이터, 같은 행 UPDATE가 멈춤(프롬프트가 돌아오지 않음) active, Lock: transactionid
C DBA, 세션, 블로킹 관계 조회
pg_stat_activity 열 의미
state active, idle, idle in transaction 등
wait_event_type, wait_event 무엇을 기다리는지(예: Lock, transactionid)
query 실행 중인 SQL, idle이면 마지막으로 실행한 SQL
query_start, state_change 쿼리 시작 시각, 상태가 바뀐 시각
backend_type client backend면 사용자 세션

#필수 명령어 또는 SQL

#잠금 재현 (창 A, B)

psql 창 A (postgres)객체 또는 설정을 변경함
create table lock_demo (id int primary key, cnt int);
insert into lock_demo values (1, 0);
begin;
update lock_demo set cnt = cnt + 1 where id = 1;
-- COMMIT 하지 않고 둠
psql 창 B (postgres)객체 또는 설정을 변경함
update lock_demo set cnt = cnt + 1 where id = 1;
-- 창 A의 잠금을 기다리며 멈춤

결과 해석: 개별 실행 출력은 기재 없음

#지금 실행 중인 세션과 대기 이벤트 (창 C)

psql 창 C (postgres)조회만 함
select pid, usename, state,
       wait_event_type as wtype, wait_event,
       extract(epoch from now() - query_start)::int as sec,
       left(query, 40) as query
  from pg_stat_activity
 where backend_type = 'client backend'
   and state = 'active'
   and pid <> pg_backend_pid()
 order by sec desc;

결과 해석:

뷰에는 시작 시각만 있어 몇 초째인지는 now() - query_start로 직접 계산함

left(query, 40)은 SQL을 잘라 한 줄에 맞추고, pg_backend_pid() 조건은 이 조회를 돌리는 자기 세션을 뺌

실습 기록: 창 B(PID 32587) 한 행이 active | Lock | transactionid | 8로 나오고 원인인 창 A는 보이지 않음

psql의 \watch 3으로 3초마다 다시 돌릴 수 있지만 화면이 계속 밀려 비교가 어려움

#누가 누구를 막는지, 홀더의 마지막 SQL (창 C)

psql 창 C (postgres)조회만 함
select w.pid as waiter,
       w.wait_event_type || ': ' || w.wait_event as event,
       extract(epoch from now() - w.query_start)::int as sec,
       pg_blocking_pids(w.pid) as blocked_by
  from pg_stat_activity w
 where cardinality(pg_blocking_pids(w.pid)) > 0;

select h.pid, h.state,
       extract(epoch from now() - h.state_change)::int as idle_sec,
       left(h.query, 45) as last_query
  from pg_stat_activity h
 where h.pid in (select unnest(pg_blocking_pids(pid)) from pg_stat_activity);

결과 해석:

실습 기록: 웨이터 32587 | Lock: transactionid | 8 | {32578}, 홀더 32578 | idle in transaction | 11 | update lock_demo set cnt = cnt + 1 where id =

홀더 PID는 배열 안에 있어 unnest()로 풀어야 찾을 수 있음

idle_sec는 상태가 idle in transaction으로 바뀐 뒤 흐른 시간이며 트랜잭션 전체 시간은 xact_start로 봄

웨이터가 여럿이면 홀더 ← 웨이터 1 ← 웨이터 2처럼 줄을 서므로 맨 앞 홀더를 찾으려면 배열을 따라 여러 번 올라감

세 쿼리를 거쳐서야 "창 A가 BEGIN 후 UPDATE하고 커밋하지 않아 창 B가 기다린다"는 그림이 나오며, 세션을 끊는 방법은 세션 종료와 영향에서 다룸

#누적 통계로 초당 수치 구하기 (창 C)

psql 창 C (postgres)조회만 함
select sum(xact_commit) as commits,
       sum(blks_read)   as disk_reads,
       sum(blks_hit)    as cache_hits,
       sum(tup_returned + tup_fetched) as rows_read
  from pg_stat_database;
select pg_sleep(10);
select sum(xact_commit) as commits,
       sum(blks_read)   as disk_reads,
       sum(blks_hit)    as cache_hits,
       sum(tup_returned + tup_fetched) as rows_read
  from pg_stat_database;

결과 해석:

pg_stat_database의 값은 통계가 초기화된 뒤 쌓인 누적값이라, 두 번 재서 빼고 간격(10초)으로 나눠야 초당 커밋, 디스크 읽기, 캐시 적중, 읽은 행 수가 됨

이 값은 DB 전체의 부하 흐름이며 앞의 잠금이 풀렸는지와는 관계없음

#사용 조건과 주의사항

마지막 SQL은 잘린 문자열이고 같은 트랜잭션 안의 앞선 SQL은 보이지 않으므로, 홀더가 무엇을 바꿨는지는 애플리케이션과 함께 확인함

idle in transaction이 오래 남지 않게 하려면 idle_in_transaction_session_timeout 같은 설정을 검토함

실습이 끝나면 창 A에서 rollback;(또는 commit;)으로 잠금을 풀고 lock_demo를 지움

SEO_HKabsumveritas@gmail.comBuilt with Claude Code & Codex

마지막 갱신 개인 학습 기록이며 운영 환경 적용 전 공식 문서 확인 필요

두 글자 이상 입력, 위아래 화살표로 이동하고 Enter로 열기