{"id":"IL-0570","org":"CrowdWorks","date":"2026-09-07","docs":[],"title":{"en":"Dropping an unused MySQL table stalled queries on its busy parent table and crowdworks.jp was hard to reach for a minute","ko":"안 쓰는 테이블을 지우는 마이그레이션 직후 MySQL 쿼리가 막혀 crowdworks.jp에 1분쯤 접속하기 어려웠다"},"domain":["database"],"impact":{"scope":"crowdworks.jp was hard to reach for about a minute","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":null,"related":[],"sources":[{"url":"https://engineer.crowdworks.jp/entry/2026/09/07/100000","kind":"blog","archived":null}],"summary":{"en":"The cause was a MySQL metadata lock (MDL). The dropped table had a foreign key to a table with very heavy traffic, and to keep foreign keys consistent MySQL requests an exclusive MDL on the parent table even when the child table is dropped. The migration waited to get that exclusive lock, and queries to the parent table queued up behind it, which the team considers the direct cause. The service's lock_wait_timeout was set to 50 seconds by default; the migration eventually failed on the timeout and the service recovered by itself. As prevention, CrowdWorks decided that such migrations set lock_wait_timeout = 3 for their session, so that a migration that cannot get the lock fails after 3 seconds. The team also considered the strong_migrations gem, which detects dangerous migrations, and a monkey patch that forces a short timeout on every migration, but DDL on specific high-traffic tables is rare and the extra dependency and maintenance were not judged worth it, so the rule is spread through documentation, AI review instructions and automatic pull request comments, agreed as an ADR (Architecture Decision Record).","ko":"2026년 8월 CrowdWorks의 엔지니어가 더 이상 쓰지 않는 테이블을 지우는 drop_table 마이그레이션을 배포했다. 지울 테이블에는 트래픽이 매우 많은 테이블을 가리키는 외래 키가 걸려 있었다. MySQL은 외래 키 정합성을 지키려고 자식 테이블을 지울 때도 부모 테이블의 배타적 메타데이터 락, 곧 MDL을 요구한다. 마이그레이션이 이 배타적 MDL을 얻으려고 기다리는 동안 부모 테이블로 가는 쿼리가 그 뒤에 쌓인 것이 직접적인 원인으로 보인다. 서비스의 lock_wait_timeout 기본값은 50초였고, 마이그레이션이 타임아웃으로 실패하면서 1분쯤 뒤 서비스는 저절로 복구됐다. 팀은 재발 방지책으로 대상 마이그레이션에 세션 단위로 lock_wait_timeout = 3 을 설정해 3초 안에 락을 얻지 못하면 실패하게 하기로 했다. 위험한 마이그레이션을 찾는 strong_migrations gem이나 모든 마이그레이션에 타임아웃 단축을 강제하는 몽키 패치도 검토했지만, 이런 DDL이 드물어 의존성과 유지보수 부담을 늘릴 만하지 않다고 보고 미뤘다. 대신 규칙을 문서로 정하고 AI 코딩 에이전트 지시 파일과 PR 자동 코멘트로 알리는 방식을 택했다. 팀은 이 결정을 Architecture Decision Record, 곧 ADR로 정리해 합의했다."},"trigger":{"en":"An engineer's deploy ran a migration that used drop_table to delete a table no longer in use.","ko":"이미 쓰지 않는 테이블을 지우려고 drop_table 마이그레이션을 실행했다. 이 테이블에는 트래픽이 매우 많은 테이블을 가리키는 외래 키 제약이 걸려 있었다."},"severity":"unknown","symptoms":["lock-contention","intermittent-connection-failures"],"timeline":{"detected_after":null,"resolved_after":"1m"},"date_kind":"published","detection":null,"components":["database","schema-migration"],"root_cause":"deploy-rollout","remediation":[],"action_items":[{"text":{"en":"In migrations that may take an exclusive MDL on a high-traffic table, through DDL on the table, adding or removing a foreign key to it, or creating or dropping a table with such a foreign key, run SET SESSION lock_wait_timeout = 3 first","ko":"대상 마이그레이션은 execute 'SET SESSION lock_wait_timeout = 3' 으로 MDL 대기 상한을 줄여, 대기가 길어지기 전에 실패하게 한다. 3초는 운영하면서 조정할 임시 값이다."}},{"text":{"en":"Write these migrations with def up and def down instead of def change, setting the timeout before the DDL in both","ko":"execute는 change에서 되돌릴 수 없어 롤백 때 ActiveRecord::IrreversibleMigration 이 나므로, def change 대신 def up 과 def down 으로 쓴다. 롤백 쪽 DDL도 같은 MDL을 잡으므로 up과 down 모두 대상 DDL 앞에 설정을 둔다."}},{"text":{"en":"Put only one such DDL in each migration","ko":"중간에 실패해 일부만 적용되는 일을 피하려고 마이그레이션 하나에 대상 DDL은 하나만 둔다."}},{"text":{"en":"Release migrations that contain such DDL on their own","ko":"실패할 때 다른 마이그레이션까지 휩쓸리지 않도록 대상 DDL을 담은 마이그레이션은 따로 릴리스한다."}},{"text":{"en":"Run them during low-traffic hours","ko":"대상 마이그레이션은 트래픽이 적은 시간대에 실행한다."}},{"text":{"en":"Document the rules in db/migrate/README.md","ko":"대상 조건과 대응 방법의 자세한 내용은 db/migrate/README.md 에 둔다."}},{"text":{"en":"Point AGENTS.md at that README so Copilot and other AI agents review migrations against it","ko":"AGENTS.md 의 참조 표에 db/migrate/ → db/migrate/README.md 한 줄을 더해 Copilot과 AI 에이전트가 리뷰할 때 이 규칙을 보게 한다."}},{"text":{"en":"Add the check to the automatic comment posted on pull requests that change db/migrate","ko":"db/migrate/** 를 바꾼 PR에 자동으로 달리는 기존 코멘트의 체크리스트에 이번 확인 항목을 더한다."}}],"contributing":[],"recurrence_of":null,"symptoms_text":{"en":"Right after a deploy ran a migration with drop_table to remove a table that was no longer used, database queries started to pile up and crowdworks.jp was hard to reach for about a minute. The migration finally failed on a timeout and the service recovered on its own.","ko":"drop_table 마이그레이션을 실행한 직후부터 데이터베이스 쿼리가 막히기 시작했고, crowdworks.jp에는 1분쯤 접속이 잘 되지 않았다. 마이그레이션은 끝내 타임아웃으로 실패했고 서비스는 저절로 복구됐다."},"updated_at":"2026-11-14T12:34:21.780444Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0570","org_slug":"crowdworks"}