{"id":"IL-0567","org":"Devsisters","date":"2023-09-05","docs":[],"title":{"en":"Aurora MySQL behind Cookie Run: OvenBreak hit 100% CPU after a sales-ranking feature launch, and players could not play","ko":"쿠키런: 오븐브레이크의 패키지 매출 랭킹 기능을 배포한 뒤 Aurora MySQL CPU가 100%에 이르러 유저가 게임에 접속하지 못했다"},"domain":["database","aws"],"impact":{"scope":"Cookie Run: OvenBreak players could not log in to or play the game while database connections failed","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A heavy query combined with per-server caches in a distributed server setup caused a problem nobody expected, so the points likely to cause trouble need to be found in advance. During an outage, restoring the service comes before finding the cause, and a switch that turns a feature off, built in ahead of time, is a safety device that helps run a stable service. The online ALTER itself went well with Percona Toolkit and was used again for later schema changes.","ko":"무거운 쿼리와 분산 서버의 캐시 구조가 겹치면 예상하지 못한 문제가 생기므로, 문제가 될 만한 지점을 미리 파악해야 한다. 장애가 나면 원인 파악보다 서비스 정상화가 먼저다. 문제가 되는 기능을 끄고 켤 수 있는 안전장치를 미리 만들어 두면 서비스를 더 안정적으로 운영하는 데 도움이 된다. Production 환경의 online alter에는 Percona Toolkit 같은 유용한 툴을 잘 활용하는 것이 중요하다."},"related":[],"sources":[{"url":"https://tech.devsisters.com/posts/cro-mysql-online-alter/","kind":"blog","archived":"https://web.archive.org/web/20260514132003/https://tech.devsisters.com/posts/cro-mysql-online-alter/"}],"summary":{"en":"The package sales ranking feature of Cookie Run: OvenBreak ranks the packages on sale by their sales over the last N hours, which needed the purchase time in the AWS Aurora MySQL table that holds every purchase since the game started. A plain ALTER TABLE to add a purchase_dt column with an index would have taken a table lock on a table tied directly to purchases, and the table was too large for the usual 2-hour maintenance. The in-place algorithm was tested on a copy of a test database: the first attempt failed with ERROR 1062 (23000): Duplicate entry '73817' for key 'PRIMARY' and the second succeeded, and since MySQL documentation describes this as an intermittent limit of the in-place algorithm, that plan was dropped. pt-online-schema-change from Percona Toolkit was then tested on a staging copy, on a production copy with chunk-size 250 (steady at about 45% CPU) and 500 (up to about 80% at first), and on staging under mocked user traffic, and was run in production with chunk-size 250 and a separate backup ready. The online ALTER finished without problems.\n\nThe outage came on the day the feature was released. The ranking query counted purchases with one OR branch per product, each with its own sale start time and a condition on the current time, and there was no index on (product_id, purchase_dt). Each game server is horizontally scaled in an AWS autoscaling group and keeps its own cache, and because planners upload new game data at any time, several game data versions coexist and each gets its own cache. The sale start time depends on the game data, so the query ran (number of servers) × (number of game data versions) times, and every server update left more servers with empty caches running it again. In the development environment there was little payment data and few servers, so the problem did not show.\n\nThe developer put the servers into maintenance, quickly added a switch to turn the ranking feature off and restored the service with it disabled. A composite index on (product_id, purchase_dt) improved the query by only about 10% in tests and was not added. The query was rewritten to group products that share a sale start time and to drop the meaningless current-time condition, which helped little but was applied. The main fix moved the query to a separate scheduler server: game servers read the result from a remote store and queue a request, and the scheduler server periodically runs the query once for the latest request and caches the result. With both changes the feature was turned back on and CPU stayed normal apart from small peaks when the scheduler ran the query.","ko":"2022년 8월에 추가된 쿠키런: 오븐브레이크의 패키지 매출 랭킹은 판매 중인 패키지의 지난 N시간 매출을 집계해 순위를 보여주는 기능이다. 구매 기록을 담은 AWS Aurora MySQL 테이블에 구매 시점 정보가 없어 purchase_dt 칼럼과 인덱스를 추가해야 했다. 일반 ALTER TABLE은 table lock을 잡는다. 게다가 이 테이블은 서비스 시작 이후의 모든 구매 기록을 담은 큰 테이블이라 2시간 점검 안에 끝내기도 어려웠다. 그래서 팀은 online alter를 택했다. lock을 잡지 않는 inplace algorithm은 테스트 서버 Database 복제본에서 첫 시도에 ERROR 1062 (23000): Duplicate entry '73817' for key 'PRIMARY' 오류로 실패했다. MySQL 공식 문서가 이를 간헐적으로 생기는 한계라고 설명하고 있어, 팀은 이 방식을 쓰지 않기로 했다. 대신 Percona Toolkit의 pt-online-schema-change를 Staging 복제본, Production 복제본, Staging, Production 순서로 시험하고 적용했다. Production 복제본에서는 CPU를 45% 정도로 유지하는 chunk-size 250을 골랐고, Production 적용은 문제 없이 끝났다.\n\n기능 배포 당일 CPU가 조금 오른 데 이어, 버그 픽스를 더 배포하고 유저를 재접속시키자 CPU가 100%에 이르러 Database 접속이 실패했다. 팀은 서버 점검을 걸고, 랭킹 기능을 끄고 켤 수 있는 설정을 급히 넣어 꺼 둔 뒤 서비스를 정상화했다. 원인은 두 가지였다. 첫째, 쿼리가 product_id와 purchase_dt를 함께 조건으로 쓰는데도 둘을 묶은 인덱스가 없었고, 매번 바뀌는 현재 시각이 조건에 있어 Database가 최적화하기 어려웠다. composite index는 개선 폭이 10% 정도에 그쳐 넣지 않았다. 대신 판매 시작 시간이 같은 상품의 조건을 묶고 현재 시각 조건을 지운 쿼리로 바꿨다. 둘째, AWS autoscaling group으로 수평 분산된 서버마다 게임데이터 버전별로 캐시를 따로 만들어, 쿼리가 분산 서버 수 곱하기 게임데이터 가짓수만큼 실행됐다. 서버를 업데이트하면 캐시가 빈 서버가 늘어 부하가 더 커졌다.\n\n팀은 스케줄러 서버가 모든 서버의 요청을 모아 주기적으로 쿼리하고 결과를 원격 저장소에 캐싱하는 공용 캐시를 만들었다. 각 서버는 Database 대신 원격 저장소의 결과를 읽는다. 이후 랭킹 기능을 다시 켰을 때 CPU는 스케줄러 서버의 작은 피크 말고는 안정적으로 유지됐다."},"trigger":{"en":"The package sales ranking feature was deployed, and further bug-fix deployments followed that reconnected users; each server update left more servers with empty caches that ran the ranking query against the database.","ko":"새 패키지 매출 랭킹 기능의 집계 쿼리가 들어간 배포 뒤 버그 픽스를 추가로 배포하고 유저를 재접속시키면서, 캐시가 빈 서버들이 이 쿼리를 한꺼번에 실행했다."},"severity":"unknown","symptoms":["database-overloaded","service-unavailable"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"published","detection":{"en":"A database alarm fired, and the CPU graph of the Aurora MySQL database showed 100%.","ko":"버그 픽스를 추가로 배포하고 유저를 재접속시킨 뒤 Database 알람이 울려 CPU 그래프를 보고 알았다."},"components":["database"],"root_cause":"deploy-rollout","remediation":[{"text":{"en":"Put the game servers into maintenance","ko":"서버 점검을 걸었다."}},{"text":{"en":"Added a switch for the package sales ranking feature and turned the feature off, which restored the service","ko":"문제가 된 패키지 매출 랭킹 기능을 켜고 끌 수 있는 설정을 빠르게 추가해 꺼 둔 뒤 서비스를 정상화했다."}},{"text":{"en":"Rewrote the ranking query to group products that share a sale start time and to drop the current-time condition","ko":"판매 시작 시간을 공유하는 상품들의 product_id 조건을 하나로 묶고, 의미가 없는 현재 시각 조건을 지운 쿼리로 바꿔 바로 적용했다."}},{"text":{"en":"Moved the ranking query to a scheduler server that runs it periodically and stores one shared cached result","ko":"모든 서버가 함께 쓰는 캐시를 스케줄러 서버에 따로 두었다. 각 서버는 Database 대신 원격 저장소의 결과를 읽고, 요청은 큐에 넣는다. 스케줄러 서버는 주기적으로 마지막 요청으로 쿼리해 결과를 원격 저장소에 캐싱한다. 이렇게 해서 쿼리 실행 횟수를 줄였다."}},{"text":{"en":"Turned the package sales ranking feature back on after both fixes","ko":"쿼리 최적화와 캐시 사용 방식 개선을 적용한 뒤 패키지 매출 랭킹 기능을 다시 켰고, CPU가 문제 없이 유지되는 것을 확인했다."}}],"action_items":[],"contributing":["inefficient-query","warm-cache-dependency","staging-not-representative"],"recurrence_of":null,"symptoms_text":{"en":"After further bug-fix deployments and reconnecting users on the launch day of the package sales ranking feature, a database alarm fired and the CPU of the Cookie Run: OvenBreak AWS Aurora MySQL database was at 100%. Database connections then failed, and players could not log in to or play the game.\n\nEarlier that day, while the feature was being deployed, the CPU graph had already risen from its usual level of about 20% by about 10 to 20 points. The developer connected this to the query added for the new feature, but since it did not look serious yet, other problems found in features released at the same time were handled first.","ko":"패키지 매출 랭킹 기능을 배포한 뒤 버그 픽스를 몇 차례 더 배포하고 유저를 재접속시키자 Database 알람이 울렸고, Database CPU 그래프가 100%에서 내려오지 않았다. Database 접속에 문제가 생기면서 유저들이 게임에 정상적으로 접속하거나 플레이할 수 없었다.\n\n배포 직후에는 평소 20% 정도이던 CPU가 10~20% 정도 올랐다. 개발자는 새 기능에 넣은 쿼리의 영향이라는 것을 알아챘지만, 아직 큰 문제는 아니라고 보고 함께 배포한 다른 기능의 문제부터 고쳤다.\n\n각 서버는 게임데이터 버전마다 캐시를 따로 만들기 때문에, 서버를 업데이트하면 캐시가 빈 서버가 늘어 같은 집계 쿼리가 한꺼번에 많이 실행됐다. 개발 환경에서는 결제 데이터가 적어 쿼리가 빨랐고 분산 서버 수도 적어 이 문제가 드러나지 않았다."},"updated_at":"2026-11-14T12:34:21.512014Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0567","org_slug":"devsisters"}