일부 주문 건 UPDATE가 응답 없이 계속 멈춰 있어서 pg_stat_activity를 봤더니 이런 상태였어요.
SELECT pid, state, wait_event_type, xact_start, query
FROM pg_stat_activity WHERE state <> 'idle';
pid | state | wait_event_type | xact_start
-------+---------------------+-----------------+---------------------
20144 | idle in transaction | | 00:01:12
20330 | active | Lock | 00:07:4020330번이 Lock을 기다리며 멈춰 있고, 20144번은 idle in transaction 상태로 6분 넘게 트랜잭션을 열어둔 채 아무것도 안 하고 있어요. 20144가 뭔가 락을 쥐고 안 놓는 것 같은데, 이 세션은 왜 트랜잭션을 계속 열고 있는 거고 어떻게 막아야 하나요?
댓글 3
왜 트랜잭션을 열어둔 채 노는지가 핵심인데, 거의 항상 앱 코드 문제예요. 트랜잭션을 시작한 뒤 그 안에서 느린 작업(외부 API 호출, 파일 처리, await 걸린 무거운 로직)을 하고 있거나, 커밋을 깜빡했거나, ORM이 커넥션을 트랜잭션 중간에 놓쳤거나입니다. DB 문제가 아니라 애플리케이션이 트랜잭션을 너무 오래 붙잡고 있는 거죠.
급한 불은 SELECT pg_terminate_backend(20144); 로 그 세션을 끊으면 락이 풀리면서 20330이 진행됩니다. 다만 이건 응급처치고, 재발을 막으려면 두 가지를 하세요. 첫째, 트랜잭션 안에서는 절대 외부 I/O(HTTP 호출, 슬립 등)를 하지 마세요. 트랜잭션은 짧게 열고 짧게 닫아야 합니다. 둘째, idle_in_transaction_session_timeout을 걸어두면(예: 30s) 이런 좀비 트랜잭션을 DB가 알아서 끊어줘서 서비스 전체가 물리는 걸 막아줍니다.
혹시 트랜잭션 안에서 뭘 하고 계신지 코드 흐름 알려주시면 더 정확히 짚어드릴게요.
외부 호출은 트랜잭션 밖으로 빼야 해요. 흐름을 바꿔서, 결제 API를 먼저 호출해 결과를 받고 그 다음에 짧은 트랜잭션으로 DB만 갱신하거나, 정합성이 중요하면 outbox 패턴 + 멱등키로 나눠 처리하는 식이 안전합니다. 트랜잭션 안에는 순수 DB 작업만 두세요.
timeout은 두 층으로 겁니다. DB 쪽은 idle_in_transaction_session_timeout(예: 30s)으로 좀비를 잘라내고, 앱 쪽은 prisma $transaction 옵션의 timeout(콜백 최대 실행시간)과 maxWait(커넥션 대기시간)를 현실적인 값으로 잡아두면 외부 호출이 늘어져도 트랜잭션이 무한정 안 살아 있습니다. 다만 근본은 어디까지나 외부 호출을 트랜잭션 밖으로 빼는 거예요. timeout은 사고를 줄여줄 뿐 구조를 고쳐주진 않습니다.