Contents
see List잠금 대기가 느린 서비스로 번지는 이유
PostgreSQL에서 API 응답 시간이 갑자기 길어졌다고 해서 CPU나 인덱스만 원인은 아니다. 하나의 트랜잭션이 행 또는 테이블 잠금을 오래 보유하면 같은 데이터를 수정하려는 다른 요청이 순서대로 대기한다. 대기 요청은 커넥션 풀을 점유하고, 풀의 여유가 사라지면 잠금과 직접 관련 없는 요청까지 지연된다. 주문 상태 변경, 재고 차감, 정산 배치처럼 같은 키를 자주 갱신하는 업무에서 특히 주의해야 한다.
잠금 자체는 오류가 아니다. 트랜잭션의 일관성을 지키려면 동시 변경을 조정해야 한다. 운영에서 해결할 문제는 어떤 세션이 얼마나 오래 잠금을 잡고 있는지, 누가 그 세션을 기다리는지, 애플리케이션이 불필요하게 트랜잭션 범위를 넓히고 있지는 않은지를 빠르게 확인하는 일이다. 장애 시간에는 무작정 세션을 종료하지 말고 관측 결과를 확보한 뒤 대응한다.
대기와 블로킹 세션을 함께 조회하기
아래 조회는 잠금을 기다리는 세션과 이를 막는 세션의 PID, 실행 시간, 쿼리를 함께 보여 준다. 관리 권한이 있는 운영 계정에서만 실행하고, 쿼리 문자열에는 개인정보나 비밀값이 포함될 수 있으므로 결과 공유 범위를 제한한다.
SELECT blocked.pid AS blocked_pid, now() - blocked.query_start AS blocked_for, blocked.query AS blocked_query, blocker.pid AS blocker_pid, now() - blocker.query_start AS blocker_for, blocker.state AS blocker_state, blocker.query AS blocker_query FROM pg_stat_activity AS blocked CROSS JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS bpid JOIN pg_stat_activity AS blocker ON blocker.pid = bpid WHERE blocked.wait_event_type = 'Lock' ORDER BY blocked_for DESC;결과에서 대기 시간이 길고 같은 blocker_pid가 반복되면 해당 세션이 우선 조사 대상이다. blocker_state가 idle in transaction이면 애플리케이션이 SELECT 또는 UPDATE 후 커밋이나 롤백을 하지 않은 채 연결을 잡고 있을 가능성이 크다. blocker_query가 짧아 보여도 트랜잭션 시작 시점부터 잠금을 보유했을 수 있다. 장애 분석에서는 query_start뿐 아니라 xact_start, application_name, client_addr도 함께 확인해 호출 주체를 좁힌다.
원인별로 판단하는 순서
- 대량 UPDATE나 DELETE 배치가 수십 분 실행되면 짧은 온라인 요청과 충돌한다. 한 번에 처리하는 행 수를 줄이고 커밋 단위를 분할한다.
- 외부 API 호출, 파일 업로드, 사용자 입력 대기를 BEGIN과 COMMIT 사이에 넣으면 잠금 시간이 예측하기 어려워진다. DB 변경에 필요한 최소 코드만 트랜잭션으로 감싼다.
- 주문과 재고를 갱신하는 기능이 서로 다른 순서로 행을 잠그면 교착 상태가 생길 수 있다. 여러 행을 수정할 때는 업무 전체에서 동일한 정렬 기준과 잠금 순서를 정한다.
- 인덱스 생성이나 컬럼 변경 같은 DDL은 예상보다 강한 잠금을 요구할 수 있다. 트래픽이 적은 시간에 수행하고 사전 리허설로 소요 시간을 확인한다.
잠금 시간을 짧게 만드는 업데이트 패턴
재고 차감처럼 동시에 같은 행을 갱신해야 하는 경우에는 먼저 읽고 애플리케이션에서 계산한 뒤 업데이트하는 방식보다 조건부 UPDATE가 안전하다. 아래 방식은 재고가 남아 있을 때만 한 문장으로 차감하므로 읽기와 쓰기 사이의 경쟁 구간을 줄인다. 반환 행 수가 0이면 재고 부족으로 처리한다. 중복 요청을 막아야 하는 결제나 주문 처리에는 별도의 멱등성 키와 고유 제약도 함께 설계한다.
BEGIN; UPDATE inventory SET quantity = quantity - 1, updated_at = now() WHERE product_id = $1 AND quantity > 0 RETURNING product_id, quantity; COMMIT;트랜잭션 안에서는 네트워크 호출, 로그 전송 대기, 긴 반복문을 피한다. 모든 요청에 무제한 대기를 허용하지 않는 것도 중요하다. 잠금 대기 제한은 장애를 즉시 없애지는 않지만 커넥션 풀이 장시간 묶이는 것을 막고, 실패를 애플리케이션의 재시도 또는 사용자 안내로 전환할 수 있게 한다. 값은 업무 특성에 맞춰 검증해야 하며 너무 짧으면 정상적인 일시 경합도 실패할 수 있다.
BEGIN; SET LOCAL lock_timeout = '3s'; SET LOCAL statement_timeout = '15s'; UPDATE orders SET status = 'PAID', paid_at = now() WHERE order_id = $1 AND status = 'PENDING'; COMMIT;세션 종료는 마지막 대응으로 제한하기
블로킹 세션이 비정상이고 업무 영향이 더 크다고 판단되면 쿼리 취소를 먼저 고려한다. pg_cancel_backend(pid)는 현재 실행 중인 쿼리 취소를 요청하며, 세션을 강제로 끊는 pg_terminate_backend(pid)보다 영향이 작다. 두 함수 모두 롤백 비용과 사용자 오류를 유발할 수 있으므로 PID, 실행 SQL, 트랜잭션 시작 시간, 업무 담당자를 확인한 뒤 제한적으로 사용한다. 오래 실행된 모든 세션을 자동으로 일괄 종료하는 방식은 피해야 한다.
운영 체크리스트
- 대시보드에 lock wait 수, 가장 오래 열린 트랜잭션, idle in transaction 세션 수를 기록한다.
- 배치 작업은 작은 단위로 커밋하고 온라인 트래픽과 겹치는 시간대를 피한다.
- 트랜잭션 내부에서 외부 통신과 사용자 대기 작업을 제거한다.
- 동시에 갱신하는 데이터는 잠금 순서와 조건부 UPDATE 규칙을 문서화한다.
- 장애 대응 시에는 블로킹 관계를 저장한 뒤 취소 또는 종료 여부를 판단한다.
PostgreSQL 잠금 문제는 단일 설정값으로 해결되지 않는다. 대기 관계를 관측하고, 트랜잭션 범위를 줄이며, 업무별 실패 처리 기준을 정해 두면 성능 저하가 전체 서비스 장애로 번지는 상황을 크게 줄일 수 있다.
database
| No | 작성일 | Title |
|---|---|---|
| 3254 | 2026. 07. 18. | PostgreSQL 잠금 대기 진단 가이드: pg_stat_activity로 블로킹 쿼리 찾고 장애 줄이기 |
| 3202 | 2026. 07. 09. | PostgreSQL 백업 복구 검증 가이드: pg_dump와 psql로 실제 복원까지 확인하기 |
| 3170 | 2026. 07. 01. | MySQL 느린 쿼리 원인 찾기: EXPLAIN과 복합 인덱스 점검 가이드 |
| 3116 | 2026. 06. 23. | PostgreSQL 트랜잭션 충돌 줄이기: 잠금 순서와 짧은 재시도로 데드락 대응하기 |
| 3058 | 2026. 06. 15. | PostgreSQL JSONB 조회 튜닝: GIN 인덱스와 생성 컬럼으로 느린 검색 줄이기 |
| 2971 | 2026. 06. 07. | MySQL/MariaDB 복합 인덱스 설계: 느린 목록 조회를 줄이는 WHERE·ORDER BY 점검법 |
| 2914 | 2026. 05. 30. | PostgreSQL Autovacuum 운영 점검: 테이블 팽창과 느린 쿼리를 막는 VACUUM·ANALYZE 실전 가이드 |
| 2859 | 2026. 05. 22. | PostgreSQL 운영 백업과 PITR 복구 점검: pg_dump, pg_basebackup, WAL 보관 실전 가이드 |
| 2777 | 2026. 05. 14. | MySQL 8.4 LTS 운영 쿼리 튜닝: 실행 계획, 히스토그램, 복합 인덱스 실전 점검법 |
| 2619 | 2026. 04. 26. | PostgreSQL 18 완전 정복: Async I/O, UUIDv7, Temporal 제약 조건 실전 가이드 |