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 |
|---|---|---|
| 272 | 2015. 04. 29. | [ oracle ] 널처리 NVL , NVL2 함수 |
| 271 | 2015. 04. 29. | [ Oracle ] 오라클 계증구조 쿼리 답글 댓글리스트 |
| 266 | 2015. 03. 26. | [ oracle ] 오라클 drop table ( 테이블 삭제) 시 복구방법 |
| 166 | 2015. 03. 02. | [ oracle ] ORA-28001: the password has expired 처리방법 |
| 143 | 2015. 01. 24. | [ Oracle ] 오라클 dmp 파일로 쉬운 복구 |
| 124 | 2015. 01. 05. | [ Oracle ] 오라클에서 날짜의 월 간격구하기 |
| 46 | 2014. 09. 30. | [ Database ] 다중 db 의 transaction 관리. |
| 35 | 2014. 09. 25. | [ oracle ] 오라클 XaDataSource 사용중 oracle.jdbc.xa.OracleXAResource.recover 대처법 |