14.4 트랜잭션 함정
PostgreSQL의 긴 트랜잭션과 잊혀진 트랜잭션은 운영 사고의 가장 큰 원인 중 하나. dead tuple 정리·vacuum·BLOAT가 모두 xmin horizon(3.2)에 잡혀 멈춥니다. 트랜잭션 운영의 흔한 함정과 방어 패턴을 정리합니다.
1. 긴 트랜잭션
-- 안티패턴
BEGIN;
SELECT ... ; -- 5 ms
-- 사용자 입력 대기 5분
INSERT ... ;
COMMIT;5분 동안 이 backend의 xmin이 잡혀 클러스터 전체 vacuum이 못 정리합니다.
대안:
- 트랜잭션은 짧고 좁게
- 사용자 입력 대기 같은 외부 작업을 트랜잭션 안에 두지 말 것
idle_in_transaction_session_timeout = '5min'설정
ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
SELECT pg_reload_conf();이 timeout이 trigger되면 PostgreSQL이 자동으로 트랜잭션 끊습니다.
2. idle in transaction 누적
pg_stat_activity.state = 'idle in transaction'이 분 단위로 떠 있습니다. 거의 항상 application 버그입니다.
-- 진단
SELECT pid, usename, datname, state, now() - xact_start AS age, query
FROM pg_stat_activity
WHERE state LIKE 'idle in transaction%'
AND xact_start < now() - interval '1 minute'
ORDER BY xact_start;원인 후보:
BEGIN후 application에서 외부 API 호출- ORM이 connection을 트랜잭션 안에 보관
- 디버거가 일시 정지
- exception 처리에 ROLLBACK 누락
대응: idle_in_transaction_session_timeout + application 로직 점검합니다.
3. SAVEPOINT 폭주
-- 안티패턴
BEGIN;
SAVEPOINT s1;
INSERT ... ;
SAVEPOINT s2;
INSERT ... ;
...
SAVEPOINT s100;각 SAVEPOINT = subtransaction. 64개 초과 시 SubtransSLRU lock 경합 폭증 — 클러스터 전체 느려집니다.
ORM 일부가 모든 작업을 SAVEPOINT로 감싸는 경우가 있습니다. 코드 점검합니다.
대응:
- 트랜잭션을 더 작게 쪼개기
- 명시적 SAVEPOINT 남발 금지
- 결국 SAVEPOINT 필요하다면 전체 SQL을 한 번에 묶어 작성
4. 잊혀진 prepared transaction
PREPARE TRANSACTION 'order-42';
-- COMMIT PREPARED 또는 ROLLBACK PREPARED 안 함
-- 세션이 끊겨도 prepared transaction은 살아 있음xmin·락·WAL이 모두 잡힘. 잊혀진 prepared transaction은 vacuum 영원히 차단합니다.
-- 진단
SELECT gid, prepared, owner, database FROM pg_prepared_xacts;
-- 정리
ROLLBACK PREPARED 'order-42';운영 권장:
max_prepared_transactions = 0 -- 사용 안 함이 기본XA 같은 분산 트랜잭션 매니저 없으면 굳이 사용 안 합니다.
5. autocommit OFF (어떤 클라이언트)
-- psql에서
\set AUTOCOMMIT off
SELECT 1; -- 자동으로 BEGIN
-- 그 후 COMMIT 안 하면 트랜잭션 살아 있음ORM에서도 autocommit off가 기본인 경우 있음 (예: Java/JDBC Connection.setAutoCommit(false)).
대응: autocommit ON이 안전 기본. autocommit OFF 쓸 경우 명시적 COMMIT 호출 점검합니다.
6. statement_timeout 미설정
-- 안티패턴 — 무한 실행
SELECT ... ; -- 1시간 걸리는 쿼리운영자가 알아채기 전에 다른 작업까지 느려집니다.
대응: 역할·세션 단위로 timeout 설정합니다.
ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE analytics_user SET statement_timeout = '10min';
-- 또는 세션
SET statement_timeout = '30s';lock_timeout도 함께:
ALTER ROLE app_user SET lock_timeout = '5s';7. DDL을 큰 트랜잭션 안에
-- 안티패턴
BEGIN;
ALTER TABLE big_table ADD COLUMN ...; -- ACCESS EXCLUSIVE 잡음
-- 다음 작업 5분 걸림
-- 그 5분 동안 SELECT도 막힘
COMMIT;ACCESS EXCLUSIVE를 트랜잭션 끝까지 잡고 있어 다른 모든 쿼리 차단합니다.
대응:
- DDL은 별도 짧은 트랜잭션
- 가능하면 CONCURRENTLY 옵션 사용 (
CREATE INDEX CONCURRENTLY) - 큰 ALTER는 점검 시간
8. ROLLBACK 누락
# 안티패턴
try:
cur.execute("BEGIN")
cur.execute("UPDATE ...")
cur.execute("INSERT ...") # raises
cur.execute("COMMIT")
except Exception:
pass # ROLLBACK 안 함 — connection이 'idle in transaction (aborted)' 상태대응: try/finally 또는 context manager.
with conn.transaction():
cur.execute("UPDATE ...")
cur.execute("INSERT ...")
# exception 시 자동 ROLLBACK9. 트랜잭션 안에서 외부 시스템 호출
-- 안티패턴
BEGIN;
UPDATE orders ... ;
-- application: HTTP API 호출 (10초)
INSERT INTO audit_log ... ;
COMMIT;API 시간 동안 row lock·xmin 잡고 있습니다. 사고 상시.
대응:
- 외부 호출은 트랜잭션 밖
- queue 패턴 — DB에 작업만 기록, 외부 호출은 worker가 별도
-- 트랜잭션 안에는 DB 작업만
BEGIN;
UPDATE orders ... ;
INSERT INTO pending_tasks(type, payload) VALUES ('send_email', ...);
COMMIT;
-- worker가 별도로 pending_tasks를 처리10. lock 순서가 일관되지 않음
-- session A
BEGIN;
UPDATE accounts WHERE id = 1;
UPDATE accounts WHERE id = 2;
-- session B (동시)
BEGIN;
UPDATE accounts WHERE id = 2;
UPDATE accounts WHERE id = 1;
-- → deadlock대응: 모든 트랜잭션이 같은 순서로 row를 잡기.
-- 항상 id 작은 순서로
UPDATE accounts WHERE id IN (1, 2)
ORDER BY id -- 또는 명시 sort 후 UPDATE11. 큰 UPDATE/DELETE 한 트랜잭션에
-- 안티패턴
BEGIN;
DELETE FROM events WHERE created_at < '2025-01-01'; -- 1억 row
COMMIT;- 락 보유 시간 길어짐
- xmin 오래 잡힘
- WAL 폭증 (commit 시 fsync 폭증)
- ROLLBACK 시 긴 undo
대안: 배치 처리합니다.
-- 한 번에 10000 row씩
DELETE FROM events
WHERE id IN (
SELECT id FROM events
WHERE created_at < '2025-01-01'
LIMIT 10000
);
-- 반복또는 파티션 단위 DETACH + DROP(7.5)가 더 효율적.
12. SERIALIZABLE을 디폴트로
ALTER SYSTEM SET default_transaction_isolation = 'serializable';모든 트랜잭션이 SSI 비용 + serialization failure 처리 필요합니다. 대부분 application은 re-try 로직 없음 → 에러 노출합니다.
대응: Read Committed 디폴트. 필요한 트랜잭션만 SERIALIZABLE.
13. CHECK 없는 무한 재시도
# 안티패턴
while True:
try:
do_transaction()
break
except SerializationFailure:
continue # 무한 재시도영원히 fail할 수 있는 트랜잭션은 재시도 한도 + 백오프.
for attempt in range(3):
try:
do_transaction()
break
except SerializationFailure:
time.sleep(0.1 * (2 ** attempt))
else:
raise정리
- 긴 트랜잭션은 모든 사고의 시작
idle_in_transaction_session_timeout·statement_timeout·lock_timeout설정 표준- SAVEPOINT 64+ 폭주 회피
- 잊혀진 prepared transaction 정기 점검
- DDL은 짧은 트랜잭션, 큰 변경은 배치
- 외부 호출은 트랜잭션 밖
- lock 순서 일관성으로 deadlock 회피
- SERIALIZABLE은 명시적 트랜잭션만
다음 절(14.5)에서는 운영 명령의 흔한 실수 — 운영 실수를 봅니다.