본문 바로가기
Q&A2026년 8월 1일1분 읽기

운영 중에 마이그레이션 한 줄 돌렸다가 서비스 전체가 멈췄어요

m
migrate_ssong
조회 688댓글 3

어제 낮에 컬럼 하나 추가하는 마이그레이션을 운영 DB에 돌렸는데, 그 직후 몇 분간 서비스 전체가 먹통이 됐습니다. 단순히 컬럼 추가하는 ALTER TABLE 한 줄이었어요.

ALTER TABLE events ADD COLUMN source varchar(32);

이상한 건, 이 events 테이블이랑 아무 상관 없는 다른 화면들(유저 조회, 상품 목록 등)까지 전부 응답이 안 왔다는 거예요. 나중에 보니 마이그레이션 돌리던 시점에 분석팀이 돌리는 긴 SELECT 하나가 events를 읽고 있었더라고요. 컬럼 추가가 왜 이렇게 온 서비스를 멈추게 하는지, 그리고 다음엔 어떻게 안전하게 돌릴 수 있을지 궁금합니다.

댓글 3

김김영삼3개월 전
락 큐(lock queue) 때문이에요. 이게 은근히 사람 잡는 함정입니다.

ALTER TABLE은 아무리 가벼운 변경이라도 AccessExclusiveLock(가장 센 락)을 잡아야 합니다. 그런데 분석팀의 긴 SELECT가 events에 AccessShareLock을 이미 쥐고 있으니, ALTER는 그 SELECT가 끝날 때까지 락을 못 얻고 대기 상태로 들어가요. 여기까지는 ALTER 하나만 멈추는 거라 괜찮은데, 문제는 Postgres 락 대기가 '공정 큐'라는 점입니다. ALTER가 AccessExclusiveLock을 달라고 줄을 서면, 그 뒤에 오는 events에 대한 모든 요청(가벼운 SELECT 하나까지)이 ALTER 뒤에 줄줄이 막힙니다. ALTER가 앞에서 센 락을 기다리는 중이라, 뒤에 온 애들은 호환되는 락이어도 새치기를 못 하거든요.

그렇게 events 커넥션이 하나씩 물리다가 커넥션 풀이 바닥나면, events랑 상관없던 다른 화면들도 커넥션을 못 얻어서 같이 멈춘 겁니다. 그래서 '온 서비스가 죽은' 것처럼 보인 거예요.

예방은 이렇게 갑니다. 첫째, DDL 돌리기 전에 SET lock_timeout = '3s' 같이 짧게 걸어두세요. 그러면 ALTER가 락을 바로 못 얻을 때 온 서비스를 막는 대신 자기 혼자 실패하고 빠집니다. 실패하면 다시 돌리면 되고요. 둘째, 무거운 DDL과 대형 인덱스 생성은 트래픽 낮은 시간에. 셋째, 인덱스는 CREATE INDEX CONCURRENTLY로. Prisma는 기본적으로 lock_timeout을 안 걸어주니 이 부분을 직접 챙겨야 합니다.
mmigrate_ssong(guest)3개월 전
락 큐라는 개념을 처음 알았네요. 그럼 Prisma 마이그레이션에서 lock_timeout은 어떻게 걸어야 하나요? 그리고 CONCURRENTLY는 트랜잭션 안에서 못 쓴다고 들었는데, Prisma가 마이그레이션을 트랜잭션으로 감싼다면 인덱스는 어떻게 만들죠?
김김영삼3개월 전
둘 다 Prisma의 아픈 지점을 정확히 찌르셨네요.

lock_timeout은 마이그레이션 SQL 파일 맨 위에 SET lock_timeout = '3s'; 를 직접 한 줄 넣으면 그 세션에 적용됩니다. 아예 마이그레이션 돌리는 전용 롤에 ALTER ROLE ... SET lock_timeout 으로 박아두는 방법도 있고요.

CONCURRENTLY는 말씀대로 트랜잭션 블록 안에서 못 돕니다. 그런데 Prisma migrate는 각 마이그레이션을 트랜잭션으로 감싸서 그냥 넣으면 에러가 나요. 실무에서 흔한 우회는, CONCURRENTLY 인덱스는 Prisma 마이그레이션에 안 넣고 손으로(psql로) 운영 DB에 직접 만든 다음, 스키마만 맞춰놓고 prisma migrate resolve 로 '적용된 걸로' 표시해서 히스토리를 맞추는 겁니다. 이 흐름이 번거롭긴 한데 CONCURRENTLY를 안전하게 쓰려면 어쩔 수 없어요. 최근 버전들에서 관련 처리가 조금씩 나아지곤 하니 쓰시는 Prisma 버전의 마이그레이션 문서도 한번 확인해 보세요.
Ctrl+Enter로 등록