{"id":"IL-0391","org":"GoCardless","date":"2015-05-27","docs":[],"title":{"en":"GoCardless API requests timed out for about 15 seconds while a Postgres migration waited for an AccessExclusive lock","ko":"외래 키 제약을 다시 만드는 Postgres 마이그레이션이 잠금 대기열에 걸려 GoCardless API 가 15초쯤 멈췄다"},"domain":["database"],"impact":{"scope":"The GoCardless API stopped answering requests for about 15 seconds","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Avoiding renames of tables in use, table rewrites under an exclusive lock and expensive work while holding one was not enough for GoCardless: a short DDL statement can still cause downtime by waiting in the lock queue behind a long-running query. GoCardless recommends removing long-running queries and transactions from the application and running analytics on an asynchronously updated replica, setting log_min_duration_statement and log_lock_waits to find such queries, setting lock_timeout in migration scripts so a deploy aborts rather than taking the application down, splitting schema changes so locks are held for shorter times, and keeping Postgres up to date. Developers with a console on the production database can cause the same downtime by leaving a transaction open.","ko":"앱에서 오래 도는 쿼리와 트랜잭션을 없애고, 분석 쿼리는 비동기로 갱신되는 복제본에서 돌린다. log_min_duration_statement 와 log_lock_waits 를 설정하면 이런 문제가 장애로 번지기 전에 찾을 수 있다. 마이그레이션 스크립트에는 앱이 견딜 수 있는 만큼의 lock_timeout 을 둔다. 앱을 멈추는 것보다 배포를 중단하는 편이 낫다. 스키마 변경을 잘게 나누면 문제를 진단하기 쉽고, DDL 을 감싼 트랜잭션이 짧아져 잠금을 오래 잡지 않는다. Postgres 는 릴리스마다 잠금 코드가 나아지므로 최신으로 유지한다. 운영 데이터베이스에 쿼리를 날릴 수 있는 콘솔을 쓰는 개발자는 아주 조심해야 한다. 트랜잭션을 열어 둔 채 자리를 비우면 누군가 스키마 변경을 배포할 때 장애가 난다."},"related":[],"sources":[{"url":"https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/","kind":"blog","archived":"https://web.archive.org/web/20261007021156/https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts"}],"summary":{"en":"In late January, GoCardless was building invoicing for its Pro product and had to rename tables that were already deployed. The tables were empty and no production code used them, and after the rename the team dropped and recreated their foreign key constraints so the names were up to date, expecting no long-held lock because there was no data to validate.\n\nTo add a foreign key constraint, Postgres takes AccessExclusive locks on both the table with the constraint and the table it references while it adds the triggers that enforce the constraint, and this applies even with NOT VALID. A lock that cannot be acquired waits in a queue, and every lock that conflicts with it queues behind it; because AccessExclusive conflicts with every other lock type, a waiting AccessExclusive lock blocks all other operations on that table. A long-running read query that happened to be running on one of the heavily used referenced tables held an AccessShare lock, the ALTER TABLE waited behind it, and the API's read and write queries on that table piled up behind the ALTER TABLE until clients timed out. The ALTER TABLE itself was fast, and the API recovered once it went through after about 15 seconds.","ko":"GoCardless 는 1월 말 Pro 제품의 인보이스 기능을 만들고 있었다. 앞서 배포한 테이블의 이름을 바꿔야 했는데, 테이블은 비어 있었고 운영에서 그 테이블을 쓰는 코드도 없었다. 이름을 바꾼 뒤 외래 키 제약의 이름이 맞지 않아 제약을 지우고 다시 만들기로 했다. 테이블이 비어 있으니 제약을 검증하느라 잠금을 오래 잡을 일도 없다고 봤다.\n\n배포하고 마이그레이션이 시작되자 곧 API 요청 타임아웃 알림이 왔고, 15초쯤 뒤 마이그레이션이 끝나면서 API 가 돌아왔다. 그날 이른 시각의 백업에 마이그레이션을 다시 돌리자 수백 밀리초 만에 끝났다. Postgres 는 외래 키 제약을 더할 때 제약을 강제하는 트리거를 만드는 동안 제약이 걸린 테이블과 참조되는 테이블 양쪽에 AccessExclusive 잠금을 잡는다. 다른 트랜잭션이 잡은 잠금 때문에 잠금을 얻지 못하면 대기열에 들어가고, 그 잠금과 충돌하는 잠금은 모두 그 뒤에 줄을 선다. AccessExclusive 는 다른 모든 잠금과 충돌하므로, 대기열에 하나만 있어도 그 테이블의 다른 작업이 모두 막힌다. NOT VALID 를 붙여 제약을 더해도 마찬가지다.\n\n제약을 더한 테이블은 앱 코드가 쓰지 않았지만, 참조되는 테이블은 가장 많이 쓰는 테이블 가운데 하나였다. 마침 그 부모 테이블에서 오래 도는 읽기 쿼리가 돌고 있었고, 마이그레이션은 그 쿼리가 잡은 AccessShare 잠금 뒤에서 기다렸다. API 의 읽기·쓰기 쿼리가 그 뒤로 쌓여 클라이언트 쪽에서 타임아웃이 났다. 당시 GoCardless 는 Rails 마이그레이션에 그런 메서드가 없던 탓도 있어 Postgres 가 ALTER TABLE 로 제약 이름을 바꿀 수 있다는 것을 몰랐다. 이 방법은 참조되는 테이블의 AccessExclusive 잠금을 피하지만, 제약이 걸린 테이블에는 여전히 그 잠금을 잡는다."},"trigger":{"en":"A deploy with a schema migration that renamed the empty invoicing tables and dropped and recreated their foreign key constraints, which referenced some of the most heavily used tables.","ko":"Pro 제품의 인보이스 기능을 만들면서 이름을 바꾼 빈 테이블에서 외래 키 제약을 지우고 다시 만드는 마이그레이션을 배포했다. 그때 참조되는 테이블에서 오래 도는 읽기 쿼리가 실행 중이었다."},"severity":"unknown","symptoms":["lock-contention","service-unavailable"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"Alerts about API requests timing out began just after the schema migration started.","ko":"스키마 마이그레이션이 시작된 직후 API 요청에서 타임아웃이 난다는 알림이 왔다."},"components":["database","schema-migration"],"root_cause":"deploy-rollout","remediation":[],"action_items":[],"contributing":["assumption-unverified"],"recurrence_of":null,"symptoms_text":{"en":"Just after a planned Postgres schema migration started, GoCardless received alerts that API requests were timing out. The API was down for about 15 seconds, until the migration went through and the API came back.\n\nThe same migrations, run afterwards against a backup of the database from earlier that day, finished in a few hundred milliseconds. Common advice on zero-downtime schema changes did not explain the outage; the explanation came from an old thread on the Postgres mailing list and a blog post that went into more depth.","ko":"계획된 데이터베이스 스키마 마이그레이션이 시작된 직후 API 요청에서 타임아웃이 난다는 알림이 왔다. API 는 15초쯤 요청을 받지 못하다가 마이그레이션이 끝나자 다시 살아났다.\n\n마이그레이션의 ALTER TABLE 문 자체는 금방 끝나는 작업이었다. 하지만 이 문이 참조되는 부모 테이블의 AccessExclusive 잠금을 기다리는 동안 API 호출이 보낸 읽기와 쓰기 쿼리가 그 뒤에 쌓였고, 클라이언트 쪽에서 타임아웃이 났다.\n\n처음에는 그날 이른 시각의 데이터베이스 백업에 같은 마이그레이션을 다시 돌려 봤다. 마이그레이션은 수백 밀리초 만에 끝나 문제가 재현되지 않았다. 인터넷에서 찾은 글 대부분은 무중단으로 스키마를 바꾸는 일반적인 방법만 다뤘고, 결국 Postgres 메일링 리스트의 오래된 스레드에서 같은 상황을 찾았다."},"updated_at":"2026-11-14T12:34:04.208702Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0391","org_slug":"gocardless"}