{"id":"IL-0589","org":"Kickstarter","date":"2013-03-07","docs":[],"title":{"en":"Every Kickstarter MySQL replica on Amazon RDS stopped replicating on a duplicate-key error in the backings table","ko":"Kickstarter 의 모든 MySQL 복제본이 중복 키 오류로 복제를 멈췄다"},"domain":["database","aws"],"impact":{"scope":"All MySQL replicas stopped updating; most of Kickstarter's traffic is served from the replicas","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"Databases are intricate. In the Rails community they are sometimes treated as simple data stores hidden behind ActiveRecord and ActiveRelation, and Kickstarter's engineers concluded that it pays to learn how the database carries out what it is asked to do.","ko":"데이터베이스는 복잡하다. Rails 커뮤니티에서는 데이터베이스를 ActiveRecord 와 ActiveRelation 뒤에 숨은 단순한 데이터 저장소로 여기기도 하지만, 데이터베이스가 요청받은 일을 어떻게 처리하는지 더 깊이 알아야 한다."},"related":[],"sources":[{"url":"https://kickstarter.engineering/the-day-the-replication-died-e543ba45f262","kind":"blog","archived":"https://web.archive.org/web/20230103002408/https://kickstarter.engineering/the-day-the-replication-died-e543ba45f262"}],"summary":{"en":"Kickstarter runs managed MySQL on Amazon RDS, whose default replication mode is MIXED: MySQL replicates the statements themselves and switches to row-based replication when it recognises an unsafe query. To give each backer of a project a unique, incremental sequence number, a background job runs an UPDATE on the backings table that uses a user-defined counter variable and is ordered by pledged_at and id.\n\nIn the period when the data became inconsistent, a set of transactions that had written to the backings table hung waiting for locks and then committed out of order. MySQL writes a transaction's queries to the binlog at COMMIT, so the replicas replayed the writes in a different order than the primary had written the records to disk. With an explicit ORDER BY this should not have mattered, but a MySQL bug sometimes ignores the ORDER BY clause, so the primary and the replicas each used their own implicit row order for the sequencing UPDATE and gave backings different sequence numbers. Under statement-based replication the difference stayed silent until a later unsafe query was replicated as a full row, which set a sequence value on a replica that already held it in another row and stopped replication with error 1062 on every replica.\n\nOver the next hours the team kept the site stable, limited the effects of the stale replicas, told users about the problem and recovered, and then the whole cycle repeated.","ko":"Kickstarter 는 관리형 MySQL 로 Amazon RDS 를 쓰고, RDS 의 기본 복제 모드는 MIXED 다. MIXED 모드는 평소에는 쿼리 자체를 넘기는 statement 기반 복제를 쓰다가, 복제본마다 결과가 달라질 수 있는 unsafe 쿼리를 만나면 잠시 행의 새 값 전체를 넘기는 row 기반 복제로 바꾼다.\n\nKickstarter 는 후원자가 후원을 마치면 프로젝트 안에서 겹치지 않고 차례로 늘어나는 순번을 매긴다. 경합 상태와, 중복 키 때문에 생기는 불필요한 롤백과 재시도를 피하려고, 백그라운드 작업이 사용자 정의 카운터 변수로 최근 후원의 순번을 한 번에 매기는 UPDATE 를 실행했다. 순서는 ORDER BY pledged_at ASC, id ASC 로 정했다. 데이터가 어긋난 시기에는 backings 테이블에 쓴 뒤 락을 기다리며 멈춘 트랜잭션들이 있었고, 이 트랜잭션들은 순서가 뒤바뀌어 끝났다. MySQL 은 트랜잭션의 쿼리를 COMMIT 때 binlog 에 쓰므로, primary 가 디스크에 쓴 행의 순서와 복제본이 binlog 를 다시 실행하며 쓴 순서가 달라졌다. 여기에 MySQL 이 가끔 ORDER BY 절을 무시하는 버그가 겹쳐, primary 와 복제본이 각자의 암묵적 순서로 순번을 매겼고 값이 어긋났다.\n\n어긋난 데이터는 며칠에서 한 달 넘게 복제를 깨지 않았다. 나중에 unsafe 쿼리가 row 기반 복제를 일으키자 Update_rows 이벤트가 복제본의 고유 키와 충돌했고, 3월 7일 모든 복제본이 Error_code: 1062 로 멈췄다. 엔지니어들은 몇 시간에 걸쳐 사이트를 안정시키고 복구했지만, 그 뒤 같은 일이 다시 일어났다."},"trigger":{"en":"A later unsafe query on the backings table made MySQL switch to row-based replication for that query, and replaying the full row on the replicas collided with sequence values that already differed from the primary.","ko":"데이터가 이미 어긋나 있는 상태에서, 나중에 실행된 unsafe 쿼리가 MIXED 모드 복제를 row 기반 복제로 바꿨다. 복제본이 그 행의 새 값을 그대로 쓰려다 고유 키가 겹쳐 복제가 멈췄다."},"severity":"unknown","symptoms":["replication-lag"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"incident","detection":{"en":"The team saw that the replicas had stopped updating, and SHOW SLAVE STATUS showed the duplicate-key replication error.","ko":"트래픽 대부분을 받는 복제본들이 모두 갱신을 멈춘 것을 보고 알았고, SHOW SLAVE STATUS\\G 에서 중복 키 복제 오류를 확인했다."},"components":["database","database-replication"],"root_cause":"software-bug","remediation":[],"action_items":[],"contributing":[],"recurrence_of":null,"symptoms_text":{"en":"On Thursday, March 7th, all of Kickstarter's MySQL replicas on Amazon RDS stopped applying updates, not just one replica that could be replaced. Most of Kickstarter's traffic is served from these replicas, so the site ran on stale copies of the database.\n\nSHOW SLAVE STATUS on the replicas showed an error the team had not seen before: \"Last_Error: Could not execute Update_rows event on table kickstarter.backings; Duplicate entry '123456-789' for key 'index_backings_on_project_id_and_sequence', Error_code: 1062; handler error HA_ERR_FOUND_DUPP_KEY; the event's master log mysql-bin-changelog.169933, end_log_pos 12969124\".\n\nThe error led to a range of rows in the backings table where the replicas differed from the primary: backer sequence numbers had been assigned in a different order on the replicas. That data had been inconsistent for days, and a wider search found inconsistent rows more than a month old. Replication broke only when a later unsafe query was replicated as full rows and hit the unique index. After the team recovered and gave the all clear, the same cycle happened again.","ko":"3월 7일 목요일, Kickstarter 트래픽 대부분을 받는 MySQL 복제본들이 갱신을 멈췄다. 복제본 하나만 멈췄다면 그 복제본을 갈아 끼우면 됐지만, 모든 복제본이 같은 상태였다.\n\nSHOW SLAVE STATUS\\G 의 출력에는 처음 보는 오류가 있었다. Last_Error: Could not execute Update_rows event on table kickstarter.backings; Duplicate entry '123456-789' for key 'index_backings_on_project_id_and_sequence', Error_code: 1062; handler error HA_ERR_FOUND_DUPP_KEY; the event's master log mysql-bin-changelog.169933, end_log_pos 12969124\n\n오류가 가리킨 곳을 보니 primary 와 복제본의 값이 서로 다른 행 범위가 있었다. 이 데이터는 며칠 전부터 어긋나 있었고, 범위를 넓혀 찾자 한 달이 넘은 불일치도 나왔다. 어긋난 값은 몇몇 후원자 순번이었다.\n\n엔지니어들은 몇 시간 동안 사이트를 안정시키고, 오래된 복제본 데이터의 영향을 줄이고, 사용자에게 알리고, 복구했다. 정상이라고 알린 뒤 같은 일이 처음부터 다시 일어났다."},"updated_at":"2026-11-14T12:34:23.532786Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0589","org_slug":"kickstarter"}