Incident Library

IL-0391 GoCardless · 2015-05-27 (게시일)database

외래 키 제약을 다시 만드는 Postgres 마이그레이션이 잠금 대기열에 걸려 GoCardless API 가 15초쯤 멈췄다

GoCardless 는 1월 말 Pro 제품의 인보이스 기능을 만들고 있었다. 앞서 배포한 테이블의 이름을 바꿔야 했는데, 테이블은 비어 있었고 운영에서 그 테이블을 쓰는 코드도 없었다. 이름을 바꾼 뒤 외래 키 제약의 이름이 맞지 않아 제약을 지우고 다시 만들기로 했다. 테이블이 비어 있으니 제약을 검증하느라 잠금을 오래 잡을 일도 없다고 봤다.

배포하고 마이그레이션이 시작되자 곧 API 요청 타임아웃 알림이 왔고, 15초쯤 뒤 마이그레이션이 끝나면서 API 가 돌아왔다. 그날 이른 시각의 백업에 마이그레이션을 다시 돌리자 수백 밀리초 만에 끝났다. Postgres 는 외래 키 제약을 더할 때 제약을 강제하는 트리거를 만드는 동안 제약이 걸린 테이블과 참조되는 테이블 양쪽에 AccessExclusive 잠금을 잡는다. 다른 트랜잭션이 잡은 잠금 때문에 잠금을 얻지 못하면 대기열에 들어가고, 그 잠금과 충돌하는 잠금은 모두 그 뒤에 줄을 선다. AccessExclusive 는 다른 모든 잠금과 충돌하므로, 대기열에 하나만 있어도 그 테이블의 다른 작업이 모두 막힌다. NOT VALID 를 붙여 제약을 더해도 마찬가지다.

제약을 더한 테이블은 앱 코드가 쓰지 않았지만, 참조되는 테이블은 가장 많이 쓰는 테이블 가운데 하나였다. 마침 그 부모 테이블에서 오래 도는 읽기 쿼리가 돌고 있었고, 마이그레이션은 그 쿼리가 잡은 AccessShare 잠금 뒤에서 기다렸다. API 의 읽기·쓰기 쿼리가 그 뒤로 쌓여 클라이언트 쪽에서 타임아웃이 났다. 당시 GoCardless 는 Rails 마이그레이션에 그런 메서드가 없던 탓도 있어 Postgres 가 ALTER TABLE 로 제약 이름을 바꿀 수 있다는 것을 몰랐다. 이 방법은 참조되는 테이블의 AccessExclusive 잠금을 피하지만, 제약이 걸린 테이블에는 여전히 그 잠금을 잡는다.

심각도
–
영향 범위
The GoCardless API stopped answering requests for about 15 seconds
영향 사용자
–
지역
–
감지까지
–
복구까지
–

증상

계획된 데이터베이스 스키마 마이그레이션이 시작된 직후 API 요청에서 타임아웃이 난다는 알림이 왔다. API 는 15초쯤 요청을 받지 못하다가 마이그레이션이 끝나자 다시 살아났다.

마이그레이션의 ALTER TABLE 문 자체는 금방 끝나는 작업이었다. 하지만 이 문이 참조되는 부모 테이블의 AccessExclusive 잠금을 기다리는 동안 API 호출이 보낸 읽기와 쓰기 쿼리가 그 뒤에 쌓였고, 클라이언트 쪽에서 타임아웃이 났다.

처음에는 그날 이른 시각의 데이터베이스 백업에 같은 마이그레이션을 다시 돌려 봤다. 마이그레이션은 수백 밀리초 만에 끝나 문제가 재현되지 않았다. 인터넷에서 찾은 글 대부분은 무중단으로 스키마를 바꾸는 일반적인 방법만 다뤘고, 결국 Postgres 메일링 리스트의 오래된 스레드에서 같은 상황을 찾았다.

증상 잠금 경합과 교착서비스 중단

원인

촉발
Pro 제품의 인보이스 기능을 만들면서 이름을 바꾼 빈 테이블에서 외래 키 제약을 지우고 다시 만드는 마이그레이션을 배포했다. 그때 참조되는 테이블에서 오래 도는 읽기 쿼리가 실행 중이었다.

감지

스키마 마이그레이션이 시작된 직후 API 요청에서 타임아웃이 난다는 알림이 왔다.

조치

–

후속 조치

–

교훈

앱에서 오래 도는 쿼리와 트랜잭션을 없애고, 분석 쿼리는 비동기로 갱신되는 복제본에서 돌린다. log_min_duration_statement 와 log_lock_waits 를 설정하면 이런 문제가 장애로 번지기 전에 찾을 수 있다. 마이그레이션 스크립트에는 앱이 견딜 수 있는 만큼의 lock_timeout 을 둔다. 앱을 멈추는 것보다 배포를 중단하는 편이 낫다. 스키마 변경을 잘게 나누면 문제를 진단하기 쉽고, DDL 을 감싼 트랜잭션이 짧아져 잠금을 오래 잡지 않는다. Postgres 는 릴리스마다 잠금 코드가 나아지므로 최신으로 유지한다. 운영 데이터베이스에 쿼리를 날릴 수 있는 콘솔을 쓰는 개발자는 아주 조심해야 한다. 트랜잭션을 열어 둔 채 자리를 비우면 누군가 스키마 변경을 배포할 때 장애가 난다.

출처

관련 사례

내 클러스터에도 있는지 확인하기

kubegap 은 Claude Code 나 Codex 에서 쿠버네티스 클러스터를 읽기만 하며 이 사례와 같은 상태를 점검합니다. 이 사례의 구성 요소: 데이터베이스, 스키마 마이그레이션. 점검 항목의 근거는 이 사례 같은 공개 보고서입니다.

AI 도구와 스크립트용

API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.