{"id":"IL-0524","org":"Open Collective","date":"2023-04-04","docs":["https://github.com/opencollective/opencollective-api/pull/8774","https://github.com/tsenart/vegeta"],"title":{"en":"An index migration locked Open Collective's whole Transactions table and degraded the service for nearly an hour","ko":"인덱스를 다시 만드는 마이그레이션이 운영에서 Transactions 테이블을 잠가 Open Collective 서비스가 느려짐"},"domain":["database"],"impact":{"scope":"Open Collective's service was degraded while the Transactions table was locked, and the frontend was in maintenance mode for part of that time, from 09:20 to 10:08 UTC","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Dropping the old index before creating the new one was too optimistic under constant production queries, even with CONCURRENTLY, and an unreviewed pull request let it through. Tests locally and on staging did not show the problem because there were no tools to run migrations in stressed environments. Putting the frontend in maintenance mode limited the impact while showing users a clear message.","ko":"옛 인덱스를 지우고 새 인덱스를 만드는 방식은 쿼리가 끊임없이 들어오는 테이블에서 너무 낙관적이었다. 로컬과 staging 에서 시험해도 부하가 걸린 환경에서 마이그레이션을 돌려 볼 도구가 없으면 이런 문제를 미리 잡지 못한다. 문제를 빨리 알아채고 엔지니어 두 명이 함께 조사한 것과, 사용자에게 분명한 안내를 보이면서 영향을 줄인 프런트엔드 점검 모드는 도움이 됐다."},"related":[],"sources":[{"url":"https://github.com/opencollective/opencollective/blob/main/postmortem/2023-04-04-transactions-null-indexes.md","kind":"github","archived":null}],"summary":{"en":"On 4 April 2023 Open Collective deployed a migration that re-created indexes on the Transactions table with soft-deleted rows excluded, to improve performance. It had been tested locally and ran successfully on staging, and the pull request had been shared with the team but not reviewed. In production the index creation ran with CONCURRENTLY, but the numerous queries constantly hitting the table seem to have conflicted with it and the whole table locked. Because the old index was dropped before the new one was created, queries arriving in between used no index, which degraded performance further.\n\nThe deployment was triggered at 09:13 UTC and the degradation was noticed at 09:20. The team killed the migration, but the table remained locked. It put the frontend into maintenance mode to limit queries while showing users a clear message, closed the other connections to the database, cancelled the new index creation and restored the old index between 10:00 and 10:05 UTC. At 10:08 UTC the frontend was restored and the issue was confirmed resolved.","ko":"Open Collective 는 2023년 4월 4일 성능을 높이려고, 소프트 삭제된 행을 빼고 Transactions 테이블의 인덱스를 다시 만드는 마이그레이션을 배포했다. 마이그레이션은 로컬에서 시험했고 staging 에서도 성공했다. 운영에서는 이 마이그레이션이 Transactions 테이블 전체를 잠가 서비스가 느려졌다. 인덱스 생성은 `CONCURRENTLY` 로 돌렸지만, Transactions 테이블에 끊임없이 들어오는 많은 쿼리가 마이그레이션과 부딪쳐 테이블이 잠긴 것으로 보였다. 옛 인덱스를 먼저 지우고 새 인덱스를 만드는 방식이었기 때문에, 그 사이에 들어온 쿼리는 인덱스를 전혀 쓰지 못했고 이것이 성능 저하를 크게 키웠다.\n\n엔지니어들은 마이그레이션을 죽였지만 테이블은 잠긴 채로 남았다. 쿼리를 줄이려고 프런트엔드를 점검 모드로 돌리고 데이터베이스의 다른 연결을 닫은 뒤, 새 인덱스 생성을 취소하고 10:00 UTC 부터 옛 인덱스를 되살렸다. 복원은 10:05 UTC 에 끝났고, 10:08 UTC 에 프런트엔드를 되살리고 문제가 해결됐다고 공식으로 확인했다."},"trigger":{"en":"A deployment ran a migration that dropped indexes on the Transactions table and re-created them with CONCURRENTLY, excluding soft-deleted rows.","ko":"2023년 4월 4일 09:13 UTC 에 성능을 높이려고, 소프트 삭제된 행을 빼고 Transactions 테이블의 인덱스를 다시 만드는 마이그레이션을 배포했다."},"severity":"unknown","symptoms":["lock-contention","database-overloaded"],"timeline":{"detected_after":"7m","resolved_after":"55m"},"date_kind":"incident","detection":{"en":"The degradation of service was noticed at 09:20 UTC, seven minutes after the deployment was triggered.","ko":"배포를 시작하고 7분 뒤인 09:20 UTC 에 서비스 성능 저하를 알아챘다."},"components":["database","schema-migration"],"root_cause":"deploy-rollout","remediation":[{"text":{"en":"Killed the migration, though the table remained locked","ko":"마이그레이션을 죽였지만 Transactions 테이블은 잠긴 채로 남았다."}},{"text":{"en":"Put the frontend into maintenance mode to limit queries","ko":"쿼리를 줄이려고 프런트엔드를 점검 모드로 돌렸다. 사용자에게는 분명한 안내 문구가 보였다."}},{"text":{"en":"Closed the other connections to the database","ko":"데이터베이스에 붙어 있던 다른 연결을 닫았다."}},{"text":{"en":"Cancelled the new index creation and restored the old index","ko":"새 인덱스 생성을 취소하고 10:00 UTC 부터 옛 인덱스를 되살렸으며, 10:05 UTC 에 복원이 끝나 성능이 정상으로 돌아왔다."}},{"text":{"en":"Restored the frontend and confirmed the issue was resolved","ko":"10:08 UTC 에 프런트엔드를 되살리고 문제가 해결됐다고 공식으로 확인했다."}}],"action_items":[{"text":{"en":"Add a script that simulates production load locally, using a tool like vegeta","ko":"vegeta 같은 도구로 로컬에서 운영 부하를 흉내 내는 스크립트를 더한다."}},{"text":{"en":"Run this kind of migration during specific maintenance windows","ko":"이런 마이그레이션은 정해진 점검 시간대에 돌린다."}},{"text":{"en":"Create the new index first and drop the old one afterwards in future migrations","ko":"앞으로 마이그레이션은 새 인덱스를 먼저 만들고 옛 인덱스를 나중에 지운다."}},{"text":{"en":"Insist on a review from another engineer before merging such a pull request","ko":"PR 을 머지하기 전에 다른 엔지니어의 리뷰를 꼭 받는다. 이번 PR 은 팀에 공유됐지만 리뷰되지 않았다."}}],"contributing":["review-missed","staging-not-representative"],"recurrence_of":null,"symptoms_text":{"en":"At 09:20 UTC on 4 April 2023, seven minutes after a deployment was triggered, Open Collective's service degraded: a migration in the deployment had locked the entire Transactions table in the production database.\n\nThe migration re-created indexes on the Transactions table so that soft-deleted rows were excluded, to improve performance. It dropped the old index and then created the new one, with CONCURRENTLY. The numerous queries constantly hitting the Transactions table seem to have conflicted with the migration and the table locked, and every query that arrived between the drop and the creation ran without an index, which added heavily to the slowdown.\n\nKilling the migration did not end the problem, because the table remained locked. The migration had been tested locally and had run successfully on staging.","ko":"2023년 4월 4일 09:13 UTC 에 배포를 시작하고 7분 뒤인 09:20 UTC 에 서비스 성능 저하가 보였다. 운영에서 실행한 마이그레이션이 Transactions 테이블 전체를 잠근 상태였다.\n\n엔지니어들이 마이그레이션을 죽였지만 테이블은 계속 잠겨 있었다. 프런트엔드를 점검 모드로 돌리고 데이터베이스의 다른 연결을 닫은 뒤 옛 인덱스를 되살렸다."},"updated_at":"2026-11-14T12:34:16.737553Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0524","org_slug":"open-collective"}