쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기

이창원

이 글과 관련된 다른 글 보기


안녕하세요, 저는 데브시스터즈 진저랩 인프라셀에서 데브옵스 엔지니어로 일하고 있는 이창원입니다. 지난 글인 쿠키런: 킹덤 런칭 회고에 이어 이번에는 런칭 후 4일 만에 기술적인 문제로 인해 발생했던 약 36시간 동안의 서비스 장애에 대해 전해드리려고 합니다. 여러 편의 글을 통해 다각도에서 이야기를 풀어드리고자 하는데, 이번 글에서는 서비스 장애의 원인과 해결 과정, 그리고 회고를 다루려고 합니다.

Disclaimer

  • 이 이야기는 박새미님과 NDC22에서 공동 발표한 쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 - 그때 그 명검은 왜 뽑아야 했는가와 CockroachDB 고객사 컨퍼런스 행사인 Roachfest 세션에서 발표되기도 하였습니다. 보다 생동감 있게 혹은 짧은(?) 버전으로 일련의 이야기를 접하고자 하는 분들은 발표 영상을 보시는 것을 권해드립니다.
  • 데브시스터즈에서 어째서 CockroachDB라는 다소 생소한 데이터베이스를 사용하기로 결정했는지 이유나, 이 글을 읽으면서 CockroachDB의 내부 동작이나 설계가 궁금하신 분들은 CockroachDB in Production을 읽으시는 것을 추천합니다.

지난 글에서…

지난 글에서 여러 장치를 두어 <쿠키런: 킹덤>을 런칭했고, 첫 주말을 안정적으로 넘겼지만 모니터링 중에 사용자 수가 단조증가하여 스토리지 사용량이 크게 늘고 있었다는 이야기로 마무리되었는데요. <쿠키런: 킹덤>에서 사용한 CockroachDB가 배포되어 있는 리눅스 운영체제의 특성 상 디스크 공간이 가득 차게 되면 더 이상의 쓰기 작업이 불가능하게 됩니다. 이렇게 되면 당연하게도 사용자 상태의 변경사항을 기록하는 데이터베이스 본연의 기능을 할 수 없을 뿐더러, 분산 데이터베이스로써 변경사항을 여러 노드 간에 항시 복제, 전파하는 안전장치가 동작할 수 없게 됩니다. CockroachDB는 CP 시스템이므로 이런 경우 가용성, 즉 문제가 발생한 노드를 희생하여 데이터 일관성을 보존하도록 설계되어 있으며, <쿠키런: 킹덤>의 경우 7개의 복제본을 저장하도록 설정되어 가용성이 보장되지 않는 상태에서도 데이터 영향도가 가장 적도록 의도하였습니다.

런칭 후 첫 주말을 보내고 월요일 오전 출근하여 클러스터 상태를 확인하니 사용자 인입이 현재 상태를 유지할 경우 위에서 묘사한 상황이 닥치기까지 약 36시간이 남은 것으로 계산할 수 있었습니다. 오픈 이후 유저 수는 단조증가하고 있었지만 마케팅은 서버 안정성이 확보된 이후에 집행하기로 결정되어 있던 상황이었습니다. 따라서 마케팅 집행이 시작되면 지금보다 더 가파른 규모로 유저가 증가할 수 있다는 사실을 인지하고 있었기 때문에 팀 내 위기감이 더욱 고조되었습니다. 당장 발등에 불이 떨어진 상황이기 때문에 저희는 여러 지표들을 따져보며 클러스터 규모를 얼마나 더 크게 키워야할지를 계산을 하는 동시에 공식 가이드를 따라 모든 노드에 Ballast 파일을 생성해두기로 결정했습니다.

01 crdb ballast file documentation
당시 사용 중이던 19.1.11 버전 공식 가이드에 적힌 내용.

사실 파일 시스템이 가득 찰 경우 데이터베이스 쓰기 작업이 문제가 아니라, 리눅스 시스템에서는 대부분의 작업을 할 수 없게 됩니다. 이러한 상황에 대처할 수 있도록 CockroachDB의 공식 가이드에서는 Ballast 파일을 미리 생성해둘 것을 공식적으로 가이드하고 있습니다. 여기서 Ballast 파일이란 정확히 이런 경우에 대비한 보험으로, 미리 공간을 점유해두는 더미 파일을 의미합니다. 유사시 Ballast 파일을 삭제하면 스토리지 증설에 필요한 명령 등을 실행할 수 있게 되고, 이후 다시 클러스터에 Join할 수도 있어 재해 복구에 도움이 됩니다.

데이터베이스 작업과 이슈 발생

그러나 Ballast 파일을 생성하기 위해 미리 작성해둔 스크립트를 실행하였는데 Ballast 파일의 경로가 잘못 설정되어 Block Device를 가리키고 있었고, 마침 Ballast 파일을 만들어주는 커맨드에는 대상 경로에 파일이 이미 있는지 확인하는 안전장치가 없었기 때문에 스크립트의 실행 결과 파티션을 덮어 쓰게 되었습니다. 이 영향으로 데이터베이스의 일부 노드에서 파일 시스템 불안정이 발생하게 되어 쓰기 작업이 실패하게 되고, 클러스터에서 자동적으로 격리되었습니다.

저희가 사용했던 AWS EC2 인스턴스 타입은 m5d.8xlarge로, 2개의 로컬 스토리지 디바이스를 제공하는 유형입니다. 여러 개의 로컬 스토리지 디바이스가 존재할 경우 EC2에서는 랜덤한 것을 Primary Mount로 삼는데, 이 때 설정 이슈로 인해 지정된 경로가 Primary Mount에 해당할 경우 문제가 발생하였습니다.1 총 24대의 노드로 클러스터를 구성하였으므로 확률적으로는 12대의 노드가 영향을 받아야 했겠지만, 저희의 경우는 과반 이상인 16대의 노드가 영향을 받게 되었습니다. 설계상 CockroachDB는 Raft라는 컨센서스 알고리즘을 차용하므로 동작하려면 적어도 과반 이상의 노드가 필요한데, 하필 그 이상의 노드가 영향을 받게 되었으므로 노드 단위 장애에서 클러스터 전역적인 장애로 번지게 됩니다.

지금에서야 정리된 형태로 사건을 서술할 수 있게 되었지만, 당시에는 설정 이슈가 있다는 사실을 인지하지 못하던 상태였으므로 작업 수행 직후 미리 설정해두었던 메트릭을 통해 상황을 인지하게 되었습니다. 삽시간에 에러율, 데이터베이스 연결, 그리고 Health Check 알람까지 수백 건의 알람이 발생했는데, 알람의 메세지를 읽기도 전에 여러 알람이 동시다발적으로 발생하는 상황은 일반적으로 생각해도 아주 굉장히 큰일 난 상황일 확률이 높습니다. 정상적인 게임 이용이 어려운 상황임을 인지하자마자 운영팀에 긴급 점검을 요청하여 16시 52분부터 긴급 점검이 시작되었습니다. 이후 데브옵스 팀 전체가 달려들어 약 30분 정도 셸 히스토리와 CRDB 로그, 커널 로그, 그리고 CRDB 소스코드를 분석한 이후 정확히 어떤 일이 일어나고 있는지를 파악할 수 있었습니다.

02 terminal screen
빨간 글씨가 많이 있어 딱 봐도 뭔가 잘못되었다는 것은 알 수 있는 당시 커널 작업 화면.

상황을 인지한 이후부터 팀의 최우선순위는 데이터베이스 클러스터의 상태를 복원하는 것으로 설정되었습니다. 문제에 대한 대응 방법을 탐색하는 과정에서 AWS와 CockroachDB 기술지원을 요청해보자는 의견이 있었고, 지푸라기 잡는 심정으로 양 측 기술지원팀과 동시에 화상 회의를 진행하였습니다. 설정 오류로 인해 커널 시스템 장애로 이어졌던만큼, 먼저 AWS EC2 팀에 기대어 보았습니다. 저희보다 리눅스 시스템에 대한 이해도가 조금이라도 더 깊을 것이라는 막연한 기대와 더불어 혹시라도 우리가 알지 못하는 사이에 모종의 가상머신 스냅샷을 찍어 보관하고 있지는 않을까 하는 희망을 가져보았지만, 역시나 세상에 그런건 없었고, 별다른 소득 없이 EC2 팀과의 서포트는 종료되었습니다. 이제 심각한 상황을 뭔가 매지컬한 방법으로 빠르게 없던 일로 되돌릴 수 있는 방법이 없다는 사실을 받아들이고 실질적으로 해결할 수 있는 전략을 세워야 했습니다.

CockroachDB 서포트 엔지니어와도 문의를 이어가고 있었는데, 저희는 먼저 영향 받지 않은 노드들을 바탕으로 과반 이상의 구성원을 잃어 클러스터 형성이 깨져버린 이 상황을 복구할 수 있는지 알아보고 있었습니다. 서포트 엔지니어도 이러한 상황이 흔하지는 않았던 모양인지 내부 엔지니어들과 지속적으로 소통하며 도움을 주려고 노력하였지만, 내부 논의가 썩 긍정적이지는 않았는지, 가장 빠르게 서비스를 복구하는 방법은 이전에 만들어둔 백업 데이터를 바탕으로 복원하는 것이라는 의견을 제시해 주었습니다. 이 경우 백업 데이터가 생성된 시점부터 장애 발생 시점까지의 모든 데이터는 유실되어, 흔히 백섭이라고 말하는 상황이 발생합니다. 냉정하게 생각하면, 주어진 상황을 고려했을 때 일반적으로 떠올릴 수 있는 방법인 것은 맞습니다. 그리고 직접 머신에 접근할 수 없는 CockroachDB 서포트팀 입장에서 원론적인 답변을 하는 것 또한 이해할 수 있습니다.

그러나 저희 팀은 이에 대해 쿠키런: 킹덤 개발팀과 깊게 논의한 끝에, 시간이 오래 걸리더라도 다른 방법이 존재하는 한 백섭이라는 선택지는 배제하기로 가장 먼저 결정하였습니다. 게임 운영 관점에서도 여러 이유가 있었겠지만, 엔지니어이기 이전에 한 명의 유저로서 적어도 제가 이해한 배경은 ‘유저들이 쿠키들을 막 만나가며 자신만의 왕국을 꾸며가는 과정에서 쌓은 추억을 그렇게 쉽게 없던 것으로 만들 수는 없다’는 것이었습니다. 또 모든 단서를 종합해봤을 때, 저희 팀은 그렇게 높은 확률은 아닐지언정 방법이 아예 존재하지 않는 것은 아니라고 판단했습니다. 따라서 저희는 서비스 중단이 발생하기 바로 이전까지의 모든 데이터를 복원하는 것을 장애 복구의 최종 목표로 설정하고 세 가지 방법을 동시에 진행하게 됩니다.

Plan A, 운영체제 레벨에서 노드 복원하기, 그러나 영 좋지 않은 노드가…

먼저 Plan A는 앞서 시도해보고 있었던 데이터베이스 클러스터의 상태를 복원하는 것이었습니다. 클러스터의 상태를 복원할 수만 있다면 기존에 보유하고 있던 데이터도 모두 복원할 수 있을 것이기 때문에 따르는 사이드 이펙트가 가장 적을 것으로 예상되었습니다. 이를 위해 일단 영향을 받은 모든 머신에 ebs를 붙이고 dd 커맨드를 사용해 파티션을 복제하였습니다. 이후 파티션을 복원하기 위해 복제된 EBS 파티션에 fsck 명령어를 돌려보았는데, 오류 항목이 너무 많아 실질적으로 복원이 잘 되고 있다고 생각하기 어려웠습니다. 어쨌든 fsck 이후의 결과물을 바탕으로 클러스터를 재구성하려고 시도해보았지만, 여전히 특정 SST 파일을 찾을 수 없다는 등, 정황상 파일시스템 오류가 해결되지 않았습니다.

여러 가지 시도를 이어가던 중, 23시 02분 경 영향을 받지 않아 정상적인 파일 시스템을 소유하고 있던 노드 하나가 Host Status Failure를 맞고 그대로 내려가 버리고 말았습니다. 물론 이런 경우를 상정하고 Replication Factor를 높게 설정하기도 했지만, 하필이면 장애 복구 중에, 그것도 문제가 생긴 노드도 아닌 멀쩡한 노드에 발생하다니요. 안타깝게도 이때까지 대부분의 작업은 망가진 노드를 복구하는 것에 초점이 맞추어져 있었기 때문에 정상 작동하던 노드는 EBS 복원도 해두지 않았고, 따라서 이제 남아있는 노드는 8대가 아닌 7대가 되어버렸습니다.

03 ec2 host failure datadog alert
아니 물론 저희가 잘못을 하긴 했지만 이렇게까지 시련을 겪을 필요는 없잖아요… ㅜㅜ

Plan B, 데이터가 여기에 있는데 이걸 못 쓸 리가 없잖아!

일련의 과정에서 저희는 CockroachDB가 널리 알려진 RocksDB와 유사한 스토리지 엔진을 차용하고 있다는 점과 데이터가 7개의 복제본을 가지도록 설정해두었던 사실에서 착안하여 스토리지 이슈가 발생하지 않은 노드에 남아있는 데이터를 활용하여 복원할 수 있지 않을까 하는 아이디어를 떠올리고, 두번째 복구 전략인 Plan B를 수립하였습니다. 남아있는 노드가 7대이므로 우선 데이터의 완전 손실율이 C(16, 7) / C(24, 7) ~= 3.3%에 불과한데다, Event Sourcing 아키텍쳐를 사용한 킹덤 서버 구조, 그리고 MVCC 모델을 사용하여 데이터를 증분 저장하는 CockroachDB의 스토리지 레이어 구조를 종합적으로 고려할 때 이 정도 손실율이면 충분히 투자할 가치가 있다고 판단했습니다.

CockroachDB 서포트 팀에도 이 아이디어를 공유했지만 몹시 회의적인 반응이었습니다. 먼저 수동으로 Low-level Storage Layer에서 데이터를 추출할 수 있는 방법이 있는지를 물었지만 수 주 이상 소요될 것이고 성공하리라는 보장도 없다는 답변을 들었습니다. 이런 상황에 활용할 수 있는 내부툴이 있는지 역시 물었는데, 별로 쓸만하진 않을 것이라고 덧붙이긴 했지만 다행히 디버그 용도로 사용하는 명령어가 있다는 사실을 알 수 있었습니다. 저희는 아예 CockroachDB의 소스코드를 탐색하였고, 이윽고 CockroachDB 스토리지 레이어에서 사용하는 파일 확장자인 SST 파일 내부를 볼 수 있는 명령어를 찾아냈습니다. 저희는 떠올렸던 접근방식이 유효한지 검증하기 위해 PoC를 진행하기로 했지만, 서포트 팀에서는 여전히 귀중한 시간을 불확실한 확률에 낭비하기보다 백업 데이터에서 복원하는 것을 추천하는 등, 분위기가 썩 고무적이지는 않았습니다.

Plan C, 헨젤과 그레텔도 집을 찾아갈 수 있었다면 우리도?

실제로 이 방법이 성공하더라도 이 방법을 통해 데이터를 복구해내기에는 걸려 넘어질 수 있는 구석이 많아 보였기 때문에, Plan C도 세워둘 필요가 있었습니다. 데이터 완전 복구를 위한 세번째 방법인 Plan C는 가장 최신의 백업 데이터를 사용해 데이터베이스를 어느정도 복원해둔 상태에서, 백업 시점부터 장애시점까지 발생한 분석용 로그 데이터를 바탕으로 사용자의 최종 상태를 복원하는 것이었습니다. 이 방법은 마치 LoL 중계에서 크로노 브레이크가 발생할 때 동작하는 원리와도 비슷한데요. 사용자의 행동을 다시감기하는 방법이므로 모든 로그를 완벽히 잘 남기고 있어야만 가능한 방법입니다. 언뜻 그럴듯 해 보이기는 하지만, 이 방법은 수백 종류가 넘는 로그의 정합성이 모두 보장되어야 한다는 전제 조건이 따르며, 사실상 모든 서버 API에 대해서 gRPC로 요청을 받는 방식에서 로그를 바탕으로 처리하는 방식으로 다시 구현해야 한다는 의미였습니다. 또 여러 가지 일반적이지 않은 엣지 케이스들도 처리해야 했고, 테스트를 통해 결과의 동일성 또한 보장할 수 있어야 합니다. 수백가지나 되는 모든 서버 API에 대해서 이러한 작업을 하기에는 시간과 인력, 정신력 모두 부족했기 때문에 우선순위를 두어 가장 핵심적이라고 생각되는 API에 대해서부터 대응을 시작하였습니다.

Plan A, B, C 모두 어느 하나 쉬운 방법이 없었습니다. 작업자들 모두 자신이 가장 잘 기여할 수 있는 방법에 최선을 다해 기여하면서도, 각자 진행하는 작업이 성공하리라는 확신은 없는 채로 그저 다른 쪽에서 진행하는 방법이 꼭 성공해줄 것이라 믿고 있는 상태였습니다.

Plan A 쪽에서는 몇 가지 시도를 더 이어간 끝에, 안타깝게도 결국 디스크 복구를 통해서도 리눅스 파일 시스템을 복구하는 것은 영영 불가능하다고 결론내렸습니다. 남아있는 7대의 노드 만으로 클러스터를 재구성할 수 있는지 또한 시도를 해보았는데, CockroachDB 클러스터가 시작할 때 Raft Quorum을 검증하는 로직이 있었고, 영향을 받은 노드들이 존재했던 기록이 남아있어서 Healthy State로 진행하지 못하고 있었습니다. 문서화되지 않은 CockroachDB 내부 툴까지도 총동원하며 이 로직을 우회할 방법이 없는지를 연구해보기도 하였지만, 다음날 오전 5시까지 시도해본 끝에 이 단계를 우회할 수 있는 방법은 찾지 못했고, 따라서 기존 클러스터를 재사용할 수 있을 확률은 매우 희박하다는 결론에 다다랐습니다.

다행히 Plan B 쪽에서 시도하던 방법이 어느 정도 성공을 거둬 SST 파일이 어떤 구조로 작성되어 있는지 해독하는 데에 성공했습니다. 또 해독한 바이너리 코드를 인코딩 규약에 따라 손으로 파싱을 해보니 그 결과가 그럴듯 했습니다. 이 경험을 바탕으로 장애 발생 다음날 오전 4시 경 Spark 기반으로 SST 파일의 내용물을 CSV 포맷의 텍스트 파일로 추출하는 샘플 코드를 작성할 수 있었습니다. 이 작업의 실행속도를 미리 계산해보니 32-core 머신 150대 규모의 Spark 클러스터를 사용해서 24개 데이터베이스 노드에 존재하는 모든 SST 파일을 변환한다고 할 때 약 24시간이 걸리는 것으로 예상되었습니다. 이는 너무도 긴 시간이었기 때문에 우리가 필요한 작업만을 고려하여 CockroachDB CLI 소스코드를 최적화 할 여지가 없는지 연구를 했고, 개조에 성공하여 결과적으로 약 10배 이상 빨라지도록 개선하기도 하였습니다. 이 과정에 대해 자세하고 기술적인 내용을 담은 글도 작성될 예정이니 참고를 해보셔도 좋겠습니다.

다음 날, 일단 데이터는 읽었다. 그런데 너무 많다

이 방법이 성공할 경우 SST 파일에 존재하는 행(Row) 단위 내용물을 CSV로 추출할 수 있었고, CockroachDB는 CSV의 데이터로부터 테이블로 들여오는 기능이 있었기 때문에 데이터를 다시 복구해낼 수 있다는 희망이 보였습니다. 마침 저희가 사용하던 버전에 새로 추가된 기능이기도 했습니다. 오전 10시 30분 경부터 최후 생존한 7개 노드에 남아 있던 약 7.5TB 분량의 SST 파일을 CSV로 변환하며 발생하는 에러를 수정하는 사이클을 진행하였습니다.

이 때 실제로 테이블 스키마를 작성하고 복원하는 작업을 진행해보니 몇 가지 제약사항을 발견했습니다. 먼저 IMPORT INTO 명령어는 데이터를 특정 테이블에 대입하는 방식으로 동작하는데, 명령이 수행되는 동안은 테이블이 마치 존재하지 않는 것처럼 오프라인 상태가 되었고, 단일 Transaction으로 처리되어 Primary Key나 Unique Key Constraint 등에서 충돌이 발생할 경우 모두 Revert 되었습니다. 이러한 동작의 이유는 아마도 동시성 환경에서 테이블을 원자적으로 생성하기 위한 의도였을 것으로 추측되는데, 이 때문에 하나의 SST 파일, 즉 Range 단위로 병렬적으로 데이터를 들여오려던 원래의 계획대로 진행할 수 없게 되었습니다.

저희가 복원하고자 하는 것은 백만 단위의 사용자에 대한 이벤트소싱 아키텍쳐 하의 유저 데이터였으므로 아무리 성능 좋은 분산 데이터베이스 시스템이더라도 처리하는데 많은 시간이 소요될 수 밖에 없었는데요. 충돌이 발생하여 Transaction Rollback 되게 된다면 그만큼 복구시점이 늦어진다는 의미였으므로, 7.5TB 분량의 데이터에 대해서 신중하고 완벽한 중복처리 작업이 필요하게 되었습니다. 각 Range 파일은 최대 512메가, 혹은 Range Split 알고리즘에 따라 그보다 더 작은 크기로 관리되는데요. 이튿날 오후 12시부터는 발견한 제약사항에 따라 중복을 제거하여 실제 DB에 넣을 수 있는 형태로 된 하나의 거대한 CSV 파일로 병합하는 Spark 코드를 작성하였습니다.

04 zombie cookie
길고 고된 복구 작업 중 잠시 누워 휴식을 취하는 동료의 모습

장애 발생 후 약 22시간이 지나 둘째날 오후 2시 30분 경이 되었을 때 위 작업이 모두 완료되었고, 보다 넉넉한 스토리지를 차용한 새로운 데이터베이스 클러스터에 데이터를 들이붓는 작업을 시작하였는데요. 그럼에도 Row Count와 유저 데이터의 크기가 물리적으로 너무 방대해 지나치게 긴 시간이 소요될 것으로 예상되었기에 각 유저당 킹덤 이벤트소싱 구조 처리에 필요한 최소 단위인 가장 최근 스냅샷 1개와 가장 최근 저널 300개만 복원하기로 다시금 조건을 좁혔습니다.2 데이터 전처리 작업을 위해 Spark Worker Node를 최대한 끌어쓰다보니, 저희가 사용하는 AWS 도쿄 리전의 모든 AZ에서 가용한 R계열 인스턴스를 모두 소진하는 일도 있었습니다. 당시 회사의 최우선 과제는 데이터베이스 복원 작업이었으므로 일상의 다른 작업을 위해 사용 중이던 인스턴스들까지 모두 종료하고 끌어모아 전처리 작업에 활용하기도 하였습니다.

마침내 오후 4시 20분, 저널 테이블의 복원이 성공적으로 완료되었고 오후 4시 57분에는 스냅샷 테이블의, 오후 5시 22분에는 유저 테이블의 복원에 성공하였습니다. 그러나 기쁨도 잠시, 복원 방법 자체가 사실 정상적인 데이터베이스 작업은 아니고 저희가 설정한 몇가지 가설들을 토대로 하고 있었기 때문에 완벽한 검증을 필요로 했습니다.

일단 가장 최근의 “정상적인” 데이터베이스 백업으로부터 또다른 데이터베이스를 구성해 정합성 검증에 나섰습니다. 모든 테이블에 대해 고유키를 대상으로 DISTINCT COUNT를 계산해 보았을 때, 자체적으로 복원해낸 데이터베이스(이하 ”리커버 본”)의 Row Count가 데이터베이스 백업으로부터 복원해낸 데이터베이스(이하 “백업 본”)의 Row Count보다 모두 더 많았습니다. 데이터베이스 백업은 실시간이 아니므로 장애 시점의 데이터를 토대로 복원해낸 데이터는 데이터베이스 백업보다 무조건 더 미래시점의 데이터입니다. 런칭 후 얼마 지나지 않아 사용자 탈퇴로 인한 데이터 보관 기한이 도래하지도 않았으므로, Row Count가 더 많다는 것은 일단 긍정적인 신호로 볼 수 있습니다. 또, 백업 본의 사용자 고유 식별자의 집합에서 리커버 본의 사용자 고유 식별자의 집합의 차집합을 구하도록 계산을 해보니 공집합을 얻었습니다. 이것은 백업 본이 리커버 본의 부분 집합이라는 의미이므로, 일단 백업 본에 존재하던 데이터들은 모두 복원에 성공한 것으로 해석할 수 있습니다! 이 날 들은 소식 중에 단연 가장 기쁜 소식이었습니다!

그러나 기쁜 소식만 있었던 것은 아니었던 것이, 간단한 검증 과정을 통해 유저 테이블에 오염이 심각한 것을 발견할 수 있었습니다. 왕국 이름에 중복되는 항목이 많다거나, 변화가 생겨야 하는 필드들이 그대로 남아있는다거나 하는 이상한 항목들도 많았고, 탈퇴한 유저들의 경우 또한 제대로 복구되지 않았습니다. 이 유저 테이블은 필수적인 테이블은 아니지만 유저 정보를 보여주기 위한 일종의 Projection Table인데, 이 테이블의 정보가 제대로 되어 있지 않으면 유저의 실제 상태와 프로필 등에서 보여지는 상태가 달라지는 이슈가 발생할 수 있습니다. 다행히 Plan C 작업에 몰두하시던 서버 엔지니어 분들께서 진실의 근원인 스냅샷과 저널 테이블을 기반으로 유저 테이블을 처음부터 재구성할 수 있도록 로직을 추가해주셔서 복원 대상에서 제외하기로 결정하였습니다. 나중에 알고 보니 INSERT/DELETE 쿼리 위주로 사용하는 저널과 스냅샷 테이블과 다르게 유저 테이블은 UPDATE가 자주 발생하는 테이블이었고, MVCC 데이터 처리 과정에서 중점적으로 고려한 시나리오는 킹덤 서버 구동에 필수적인 저널과 스냅샷 테이블의 복원 방식에 초점이 맞추어져 있어 전처리 과정에서 문제가 있었던 것으로 밝혀지기는 하였습니다.

마침내! 서비스 복구 가시화와 만일을 위한 정합성 검증

오후 7시부터는 데이터 복원이 모두 성공되었을 때를 가정하고 서비스를 재개할 때 사용할 깨끗한 데이터베이스를 준비하기 시작했습니다. 지금까지 사용하던 데이터베이스는 여러 번의 실패도 겪었던 클러스터인지라 아무래도 정리되지 않은 쓰레기값을 내부적으로 들고 있을 수 있어 성능에 영향을 줄 수도 있기 때문에 프로덕션에 그대로 사용하기는 찝찝합니다. 복원에 성공한 2개의 필수 테이블 외에도 언급되지 않은 다양한 메타데이터성 테이블이나 데이터의 최신성이 크게 중요하지 않은 보조 테이블들도 있었는데, 이런 것들은 단순히 백업 시점의 데이터를 활용하는 것으로 타협하였습니다. 최종적으로 저널, 스냅샷, 유저 테이블 이외의 테이블은 백업에서 복원하였고, 저널과 스냅샷 테이블은 SST 파일에서 추출하여 전처리를 거친 CSV 파일에서 Import 하였으며, 유저 테이블은 서버 로직으로 재구성하는 방식으로 진행하여 마침내 오후 8시에는 모든 작업이 완료되었습니다.

그리하여 최종적으로 서비스를 받을 수 있는 준비가 되었지만, 서비스 오픈 이후 문제가 발생한다면 더 큰 문제로 이어질 수도 있기 때문에 조금 더 신중을 기하였습니다. 일부 유저를 샘플링하여 주요 상태에 대해서 분석 로그에 기록된 상태와 최종값이 같은지를 검증하였습니다. 모든 유저에 대해서 진행할 수 있었다면 좋았겠지만, 이 작업은 한 건당 매우 오래 걸리기 때문에 부득이 샘플링을 할 수 밖에 없었습니다. 또, 누락된 저널이 있다면 스냅샷 리플레이가 제대로 되지 않고 게임 플레이 진행에 문제가 생길 수 있다고 판단하여, 모든 유저에 대해서 스냅샷 리플레이가 정상적으로 작동하는지를 검증까지 한 결과 모든 유저의 리플레이가 정상적으로 가능한 것을 확인했습니다. 이로써 모든 사용자 데이터의 정합성이 일치한다는 확신을 얻을 수 있었습니다. 마지막으로 간단한 기능 테스트까지 진행하여 서비스 오픈까지도 할 수 있는 상황임을 확인하였습니다.

QA 상황보다 테스트 모수를 더 많이 확보하기 위하여 오후 10시에는 사내망 테스트까지 진행하여 사내 구성원들이 마지막으로 기억하고 있는 계정의 상태와 일치하는지 또 한번 검증하기도 하였습니다. 이론적으로도 일치해야 했고, 실제로 코드를 통해 일치하는 것도 확인한 것은 맞지만, 이 과정이 모두 사람이 진행한 일인지라 직접 눈으로 확인하고 이상이 없다는 말을 듣고 나서야 복구에 참여한 엔지니어들도 비로소 안심할 수 있었습니다. 이 때 늦은 시간임에도 굉장히 많은 사내 구성원들이 사내망 테스트에 참여해주셔서 오랜 복구 작업이었음에도 모든 구성원들이 관심을 가지고 응원해주고 계셨다는 것을 몸소 느낄 수 있어 감동적이었습니다.

긴급 점검 해제, 해치웠나?

최종적으로 이튿날 23시 37분, 30시간 50분 만에 서비스를 다시 오픈할 수 있게 되었습니다. 감사하게도 인내심을 가지고 기다려주신 유저분들께서 커뮤니티에 많은 글을 남겨주셨는데요. 작업 중간중간 제보받은 센스 넘치는 게시글들을 보며 많은 힘을 얻을 수 있었습니다.

05 community reactions
당시 서버 오픈을 기다려주시던 유저 분들의 소중한 반응들

이렇게 서비스를 다시 오픈하여 쿠키런: 킹덤은 안정적으로 운영을 해오고 있습니다. 라고 마무리를 할 수 있다면 좋겠지만 그렇지 못했죠. 바로 이렇게 긴 시간 점검을 했던 것도 처음이었고, 그만큼 많은 유저분들께서 서비스 오픈을 기다리고 계셨다는 것을 간과하였다는 것인데요. 쿠키런 킹덤에 로그인을 하게 되면 데브시스터즈 게임 플랫폼인 DevPlay로 먼저 로그인, 인증 관련 요청이 들어오는 구조로 되어있습니다.

중요한 컴포넌트이지만 정상적인 상황에서는 요청량도 많지 않고 오토스케일링도 적용되어 있기 때문에 문제가 되는 일이 많지 않았지만, 서버가 열려있지 않은 상태에서 많은 유저 분들께서 언제 서버가 열리는지 지속적으로 확인하시던 것이 게임 진입 플로우상 DevPlay 플랫폼에 대한 부하로 이어지게 되었고, 플랫폼 데이터베이스 장애로 이어지는 Cascading Failure가 발생했습니다. 당시 플랫폼 서버에 발생했던 순간 API 수는 글을 작성하는 지금까지도 역대 최고 기록으로 남아 있습니다. DevPlay 플랫폼의 데이터베이스는 AWS RDS로 구성되어 있어 일시적 순단을 각오하고 Failover를 수행한 뒤에 정상화되었습니다.

06 devplay api call count
1차 오픈 이후 DevPlay 플랫폼 서버에 발생했던 API Call Count.

한편 얼마 지나지 않아 킹덤의 데이터베이스에도 부하가 누적되기 시작했습니다. 쿠키런: 킹덤은 게임 특성상 자정에 많은 요청이 발생하는데, 이 때를 기점으로 DB Timeout으로 인한 에러율이 관측되기 시작했습니다. 당시 클러스터 일부 노드의 CPU는 100%를 사용하는 등 매우 불안정한 상황이 이어졌고, DB가 감당할 수 있는 수준의 부하를 넘어섰다고 판단하고 있었습니다. 원래라면 적절한 수준의 부하가 유지될 때 분산 데이터베이스의 특성을 활용해 Scale out을 할 수 있어야 하겠지만, 이번 경우에는 워낙 빠르게 악화되었기 때문에 뒤늦게 Scale out을 진행하는 것은 적절하지 않았습니다. 새벽 시간대임을 감안하여 점차 요청이 줄어들 것으로 예상하고 모니터링을 하고 있었지만 새벽 2시 42분, 다시 데이터베이스에서 모든 쿼리와 요청이 실패하는 것을 관측하자마자 2차 긴급 점검에 들어갈 수 밖에 없었습니다.

07 crdb dashboard
2차 긴급 점검 당시 CockroachDB 클러스터 대시보드

새로운 데이터베이스 클러스터를 구성할 때는 i3계열 인스턴스를 사용하는 것으로 변경했었는데요, i3계열 인스턴스는 스토리지 특화 인스턴스임에도 지표를 보았을 때 Disk Write Ops로 인한 부하가 CPU 부하로까지 이어진 것으로 추측할 수 있었습니다. 이로 인한 컨텐션이 충분히 빠르게 해소되지 않고 있다고 판단했기 때문에 다시 한 번 새 데이터베이스 클러스터를 구성하기로 결정했고, 이번에는 공식 지원하는 데이터베이스 백업 및 복원 기능을 사용하여 데이터를 안전하게 이전할 수 있었습니다. 워크로드에 대해 충분히 테스트를 사전에 진행하지 않은 채로 다른 인스턴스 타입을 선택한 것은 지나친 모험이었다는 의견이 있었기 때문에, 기존에 사용하던 m5d 인스턴스를 사용하되 더 많은 인스턴스 대수를 사용하여 3번째 데이터베이스 클러스터를 구성하기로 결정하였습니다. 데이터 규모가 큰 만큼 이전에 많은 시간이 걸렸는데, CockroachDB 서포트 엔지니어들과 논의하여 아예 TPC-C 벤치마크에 사용되는 것과 비슷한 설정값을 적용하여 많은 시간을 단축하기도 하였습니다.

이와 같은 과정을 거쳐 오전 8시 30분, 총 점검 시간 36시간 29분 만에 임시점검을 종료하게 되었습니다.

요약

점검 경과를 요약하면 다음과 같은 내용이 되겠습니다.

  1. 쿠키런: 킹덤 런칭 이후 지속적인 이용자 증가로 인해 데이터베이스 저장장치 공간 부족 현상 가속화
  2. 안정적인 데이터베이스 운영을 위해 필요한 작업을 수행하려 했으나, 이 과정에 발생한 인적 오류로 인해 전체 데이터베이스 클러스터의 작동 중지
  3. 해결을 위해 데이터베이스 소스 코드와 기술문서 등 가용 자원을 총동원하여 노드에 퍼져있는 바이너리 형식의 데이터 파일을 모아 해독 가능한 형태로 변환할 수 있음을 확인
  4. 데이터베이스 소스 코드를 개량하고 데이터 분석용 분산 인프라를 활용해 이진 데이터 파일을 데이터베이스 복원에 사용할 수 있는 형태로 재가공.
    • 이 과정에서 일부 데이터 유실에 대한 위험이 있기는 하였으나, 약간의 운과 게임 아키텍처의 특성을 활용해 모든 데이터 복원 및 재구축에 성공
  5. 복원해낸 데이터를 바탕으로 새로운 데이터베이스 클러스터를 구축하여 정합성 검증
  6. 서비스 오픈을 결정했으나, 미증유의 트래픽을 받아 다시금 서비스 품질 불안정 발생, 두번째 데이터베이스 클러스터도 서비스 수용 불가 상태
  7. 위 상황을 고려해 여러 설정을 추가 적용한 세 번째 데이터베이스 클러스터를 구축하여 게임 서비스를 최종 복구

회고

회사 역사상 유래가 없었던 당시의 점검을 회고해보자면, 가장 먼저 사람의 실수로 시스템의 오류가 발생한 만큼 인프라 작업 시의 프로세스를 전면적으로 개선하였습니다. 프로덕션 인프라에 어떤 사소한 작업을 하더라도 반드시 2명 이상의 작업자가 참여해야 한다고 규칙을 개선했고, 작업 중 사용할 스크립트나 진행할 작업에 대한 시나리오를 엄격하게 작성하여 사전에 팀 내에 공유하고 리뷰를 받도록 하였습니다. 아무리 숙련되고 경험이 많은 작업자라고 하더라도 사람인 이상 실수를 할 수도 있으며, 이를 예방하기 위해서는 시스템을 갖추어야 한다고 생각했기 때문입니다.

아무리 회고해보아도 아슬아슬한 지점이 많았지만, 그럼에도 복구를 가능하게 했던 몇 가지 요인들이 있다고 생각합니다. 먼저 데브시스터즈에는 장애 대응에 대해 확고한 원칙이 이미 사전이 정립되어 있었고, 발생한 장애에 대해 분석하고 회고하는 문화가 있었습니다. 이 장애 대응 원칙에는 작업자 개인을 탓하는 것보다는 상황 해결을 위해 모두가 적극적으로 기여하고, 대응 과정을 상세히 기록으로 남겨서 동일한 유형의 장애를 예방할 수 있는 액션아이템을 설정하여야 한다고 되어 있습니다. 이번 경우에도 발생한 장애에 대해 숨김 없이 공유가 된 덕분에 해결 방안을 고안하는데 모두가 머리를 맞댈 수 있었습니다. 또, 다년간의 게임 운영 경험에서 이미 축적되어 있는 크고 작은 장애 경험이 풍부했고, 그 경험들이 기록으로 남아있었기 때문에 장애를 직접 경험하지 않은 구성원들도 대형 장애 상황에서 어떤 일을 수행해야 하는지 간접적으로나마 알고 있었습니다. 이런 문화적인 배경 덕분에 여러 가지 복구 전략을 시도해 볼 수 있었다고 생각합니다.

또 다른 요소로는 장애 해결에 필요한 인프라와 전문성이 모두 미리 갖추어져 있었고, 인프라 운영과 데이터 분석을 담당하는 팀들이 한 조직에 속해 있어 서로의 역량이 시너지를 내며 복구 시간을 획기적으로 단축할 수 있었습니다. 그러나 무엇보다 중요한 것으로 구성원들의 꺾이지 않는 마음을 꼽지 않을 수 없습니다. 오랜 시간동안 체력적, 정신적으로 한계에 부딪힌 상황 속에서도 모두가 최선을 다해 더 긴 장애로 이어지거나 데이터 유실이 발생하는 최악의 상황을 피할 수 있었습니다.

한 명의 엔지니어로서 솔직히 다시 기억을 떠올리기 싫은 사건이었습니다만, 일련의 과정을 거치면서 우리가 이 정도로 심각한 문제도 극복해낼 수 있는 팀이라는 것을 느끼는 순간이었습니다. 이 같은 문제를 숨기는 것이 아니라 널리 공유하는 문화가 전파되어야 업계 전체의 역량이 성장할 수 있다고 생각하여 꽤 긴 시간이 지났지만 글의 형태로 공유하게 되었습니다. 긴 글 읽어주셔서 감사드립니다. 하지만 저희 발표를 들으셨거나, 이 당시 킹덤을 플레이하고 계시던 분들은 다른 사건도 있었다는 것을 기억하고 계시겠죠? 이 다음에 있었던 이야기에도 많은 관심 가져주시면 감사하겠습니다.

덧1) CockroachDB 측에서도 안전장치의 필요성에 대해 공감하여 빠르게 개선해 주었습니다. (관련 PR)

덧2) 21.2 버전 이상의 CRDB에서는 Ballast 파일이 자동으로 생성되어, 저희가 진행하려던 작업을 진행할 필요가 더 이상 없습니다.


1: 이 대목을 읽으면 그러면 RAID는…? 이라는 질문을 하실 수도 있으실텐데, 런칭을 준비하며 막바지에 인스턴스 타입이 결정되는 바람에 RAID 설정에 대한 우선순위를 낮게 설정하고 작업하지 않았습니다. 돌이켜 생각해보면 아무런 설정도 하지 않았기 때문에 확률적으로 살아있는 스토리지가 있었다고도 할 수 있고, 반대로 RAID 설정을 했다면 Storage Pressure가 발생하지 않았다고도 할 수 있겠습니다. 제 의견으로는 RAID0 설정을 진행해두어 장시간 점검이 없었던 평행우주에서도, 당시 Replication Factor 등 용량 측면에서 비효율적인 설정들이 많았기 때문에 스토리지 압박은 시간 문제였을 것 같기는 합니다 😅

2: 킹덤 아키텍쳐와 관련된 자세한 기술적 배경은 권태국님의 NDC 발표를 참고하시면 좋겠습니다.

데브시스터즈는 최고의 인재를 찾고 있습니다.

데브시스터즈에서는 능력있는 SRE/DevOps Engineer를 찾고 있습니다.
자세한 내용은 채용 사이트를 확인해주세요!
DevOpsInfra

© 2024 Devsisters Corp. All Rights Reserved.