Last Updated on 12월 29, 2023 by Jade(정현호)
안녕하세요
이번 글은 얼마전에 GitHub 블로그에 업로드 된 GitHub 시스템의 MySQL 버전 업그레이드 과정에 대해서 소개하는 포스팅 글을 번역한 글입니다.
GitHub에서 진행한 MySQL 업그레이드 과정에서 배우고 참고할 내용이 많아서 읽어 보던 중에 한글로 번역해서 공유해 보아도 좋을 것 같아서 번역 포스팅을 진행하였습니다.
• 원문 : Upgrading GitHub.com to MySQL 8.0
Contents
Intro
15년 전, GitHub는 단일 MySQL 데이터베이스를 사용하는 Ruby on Rails 애플리케이션으로 시작했습니다. 그 이후로 GitHub는 고가용성을 위한 빌드, 테스트 자동화 구현, 데이터 분할을 포함하여 플랫폼의 확장 및 복원력 요구 사항을 충족하기 위해 MySQL 아키텍처를 발전시켰습니다. 오늘날 MySQL은 GitHub의 인프라와 GitHub가 선택한 관계형 데이터베이스의 핵심 부분으로 남아 있습니다.
이번 글은 우리가 1200개가 넘는 MySQL 호스트를 8.0으로 업그레이드한 방법에 대한 이야기입니다. 서비스 수준 목표(SLO)에 영향을 주지 않고 제품군을 업그레이드하는 것은 결코 작은 일이 아니었습니다. 계획, 테스트 및 업그레이드 자체에만 1년 이상이 걸렸으며 GitHub 내 여러 팀 간의 협업이 필요했습니다.
Motivation for upgrading
MySQL 8.0으로 업그레이드해야 하는 이유?
MySQL 5.7의 수명이 거의 끝나가면서 우리는 다음 주요 버전인 MySQL 8.0으로 제품을 업그레이드했습니다. 또한 우리는 최신 보안 패치, 버그 수정 및 성능 향상 기능을 갖춘 MySQL 버전을 사용하고 싶었습니다. 그리고 8.0에는 테스트하고 활용하고 싶은 새로운 기능 Instant DDL, invisible indexes, compressed bin logs 등이 포함되어 있습니다.
GitHub’s MySQL infrastructure
업그레이드 방법을 자세히 알아보기 전에 MySQL 인프라를 10,000피트 위에서 살펴보겠습니다.
- 우리의 플렛폼은 1200대 이상의 호스트로 구성되어 있습니다. 이는 MS Azure 가상 머신과 자체 데이터 센터의 베어 메탈 호스트 조합입니다.
- 우리는 300TB 이상의 데이터를 저장하고 50개 이상의 데이터베이스 클러스터에 걸쳐 초당 550만 건의 쿼리를 처리합니다.
- 각 클러스터는 고가용성을 위해 기본 클러스터와 복제본 클러스터로 구성됩니다.
- 우리는 데이터를 분할하여 관리하고 있습니다. 수평적 샤딩과 수직적 샤딩을 모두 활용하여 MySQL 클러스터를 확장하고 있습니다. 특정 제품 도메인 영역에 대한 데이터를 저장하는 MySQL 클러스터를 사용하고 있으며 또한 단일 기본 MySQL 클러스터의 용량을 능가하는 대규모 도메인 영역을 위해 수평적으로 샤딩된 Vitess 클러스터를 활용합니다.
- 우리는 Percona Toolkit, gh-ost, Orchestrator, freno 등으로 이루어진 방대한 도구들의 생태계를 사용하고 있으며, 그리고 플렛폼을 운영하기 위해 사용되는 내부의 자동화도 있습니다.
이 모든 것은 SLO를 유지하면서 업그레이드해야 하는 다양하고 복잡한 배포로 요약됩니다.
Preparing the journey
GitHub의 주요 데이터 저장소로서, 우리는 가용성에 대한 높은 기준을 가지고 있습니다. 우리는 시스템의 규모와 MySQL 인프라의 중요성 때문에 업그레이드 프로세스에 몇 가지 요구 사항이 있었습니다.
- 우리는 SLO(서비스 수준 목표) 및 SLA(서비스 수준 계약)를 준수하면서 각 MySQL 데이터베이스를 업그레이드할 수 있어야 합니다.
- 우리는 테스트 및 검증 단계에서 모든 실패 모드를 설명할 수 없습니다. 따라서 SLO를 유지하려면 서비스 중단 없이 이전 버전의 MySQL 5.7로 롤백 할 수 있어야 했습니다.
- 우리는 MySQL 제품군 전반에 걸쳐 매우 다양한 워크로드를 보유하고 있습니다. 위험을 줄이려면 각 데이터베이스 클러스터를 원자적으로 업그레이드하고 다른 주요 변경 사항에 맞춰 일정을 잡아야 했습니다. 이는 업그레이드 프로세스가 오랜 시간이 걸린다는 것을 의미했습니다. 따라서 우리는 다양한 버전 환경을 지속적으로 운영할 수 있어야 한다는 것을 처음부터 알고 있었습니다.
업그레이드 준비는 2022년 7월에 시작되었으며 단일 프로덕션(single production) 데이터베이스를 업그레이드하기 전에도 몇 가지 도달해야 할 몇 가지 이정표(milestones)가 있었습니다.
Prepare infrastructure for upgrade
우리는 MySQL 8.0에 대한 적절한 기본값을 결정하고 몇 가지 기본적인 성능 벤치마킹을 수행해야 했습니다. 우리는 두 가지 버전의 MySQL을 운영해야 했기 때문에 우리의 도구와 자동화는 혼합 버전(mixed versions)을 처리하고 5.7과 8.0 사이에서 새롭거나 다르거나 더 이상 사용되지 않는 구문을 인식할 수 있어야 했습니다.
Ensure application compatibility
MySQL을 사용하는 모든 애플리케이션에 대해 CI(Continuous Integration)에 MySQL 8.0을 추가했습니다. 우리는 장기간의 업그레이드 프로세스 중에 회귀(퇴보,regressions)가 발생하지 않도록 CI에서 MySQL 5.7과 8.0을 나란히 실행했습니다. CI에서 다양한 버그와 비호환성을 발견하여 지원되지 않는 구성이나 기능을 제거하고 새로운 예약 키워드를 이스케이프하는 데 도움이 되었습니다.
애플리케이션 개발자가 MySQL 8.0으로 전환하는 데 도움을 주기 위해 GitHub Codespaces에서 디버깅을 위해 사전 구축된 MySQL 8.0 컨테이너를 선택하는 옵션을 활성화하고 추가적인 사전 프로덕션(pre-prod) 테스트를 위해 MySQL 8.0 개발 클러스터를 제공했습니다.
Communication and transparency
우리는 GitHub 프로젝트를 사용하여 롤링 캘린더를 만들어 내부적으로 업그레이드 일정을 전달하고 추적했습니다. 우리는 업그레이드를 조정하기 위해 애플리케이션 팀과 데이터베이스 팀 모두의 체크리스트를 추적하는 이슈 템플릿을 만들었습니다.
[Project Board for tracking the MySQL 8.0 upgrade schedule]
Upgrade plan
가용성 표준을 충족하기 위해 프로세스 전반에 걸쳐 체크포인트와 롤백을 허용하는 점진적인 업그레이드 전략을 사용했습니다.
Step 1: Rolling replica upgrades
우리는 기본 기능이 안정적인지 확인하기 위해 단일 복제본을 업그레이드하고 오프라인 상태에서 모니터링하는 것으로 시작했습니다. 그런 다음 프로덕션 트래픽을 활성화하고 쿼리 대기 시간, 시스템 지표 및 애플리케이션 지표를 계속 모니터링했습니다.
우리는 데이터 센터 전체를 업그레이드할 때까지 8.0 복제본을 점차 온라인으로 가져왔고, 그런 다음 다른 데이터 센터를 반복했습니다.
롤백을 위해 충분한 수의 5.7 버전의 복제본을 온라인에 남겨두었지만 8.0 서버를 통해 모든 읽기 트래픽을 제공하기 위해 프로덕션 트래픽을 비활성화했습니다.
[The replica upgrade strategy involved gradual rollouts in each data center (DC).]
Step 2: Update replication topology
모든 읽기 전용 트래픽이 8.0 복제본을 통해 제공되면 복제 토폴로지를 다음과 같이 조정했습니다.
- 8.0 primary 후보는 현재 5.7 primary 후보에서 직접 복제되도록 구성되었습니다.
- 해당 8.0 버전의 복제본의 하위에서 두 개의 복제 체인(Replication chains)이 생성되었습니다.
- 5.7 복제본 집합(트래픽을 제공하지 않지만 롤백을 준비됨)
- 8.0 복제본 세트(트래픽 제공)
- 토폴로지는 다음 단계로 이동하기 전까지는 짧은 시간(최대 몇 시간) 동안만 유지되었습니다.
[To facilitate the upgrade, the topology was updated to have two replication chains.]
Step 3: Promote MySQL 8.0 host to primary
우리는 primary 데이터베이스 호스트에서 직접 업그레이드를 수행하지 않기로 결정했습니다. 대신 Orchestrator를 사용하여 수행되는 정상적인 장애 조치(graceful failover)를 통해 MySQL 8.0 복제본을 primary 로 승격합니다. 그 시점에서 복제 토폴로지는 두 개의 복제 체인(Replication chains)이 연결된 8.0 primary 인스턴스가 있으며 롤백을 위한 오프라인 5.7 복제본 세트와 8.0 복제본 제공 세트로 구성되었습니다.
또한 Orchestrator는 계획되지 않은 장애 조치가(failover) 발생할 경우 우발적인 롤백을 방지하기 위해 5.7 호스트를 잠재적인 장애 조치 후보에서 블랙리스트로 추가하도록 구성되었습니다.
[Primary failover and additional steps to finalize MySQL 8.0 upgrade for a database]
Step 4: Internal facing instance types upgraded
백업 또는 비프로덕션 워크로드를 위한 보조 서버도 있습니다. 그것들도 일관성을 위해 이후에 업그레이드하였습니다.
Step 5: Cleanup
클러스터가 롤백할 필요가 없고 8.0으로 성공적으로 업그레이드되었음을 확인한 후 5.7 서버를 제거했습니다. 유효성 검증은 최대 트래픽 중에 문제가 없는지 확인하기 위해 최소 24시간 트래픽 주기로 수행되었습니다.
Ability to Rollback
업그레이드 전략을 안전하게 유지하는 핵심 부분은 이전 버전의 MySQL 5.7로 롤백하는 기능을 유지하는 것이었습니다. 읽기 전용 복제본의 경우 프로덕션 트래픽 로드를 처리하기에 충분한 5.7개의 복제본이 온라인 상태로 유지되도록 하고, 성능이 좋지 않은 경우 8.0 복제본을 비활성화하여 롤백을 시작했습니다. primary의 경우 데이터 손실이나 서비스 중단 없이 롤백하려면 8.0과 5.7 사이에서 역방향 데이터 복제를 유지할 수 있어야 했습니다.
MySQL은 한 릴리스에서 다음 상위 릴리스로의 복제를 지원하지만 그 반대의 경우(MySQL 복제 호환성)를 명시적으로 지원하지는 않습니다. 스테이징 클러스터에서 8.0 호스트를 기본 호스트로 승격시키는 것을 테스트했을 때 모든 5.7 복제본에서 복제가 중단되는 것을 확인했습니다. 우리가 극복해야 할 몇 가지 문제가 있었습니다.
- MySQL 8.0에서는 utf8mb4가 기본 문자 세트이며 보다 현대적인 utf8mb4_0900_ai_ci 데이터 정렬을 기본값으로 사용합니다. 이전 버전의 MySQL 5.7은 utf8mb4_unicode_520_ci 데이터 정렬을 지원했지만 최신 버전의 Unicode utf8mb4_0900_ai_ci는 지원하지 않았습니다.
- MySQL 8.0에는 권한 관리 역할(roles)이 도입되었지만 이 기능은 MySQL 5.7에는 없었습니다. 8.0 인스턴스가 클러스터의 primary 인스턴스로 승격되었을 때 문제가 발생했습니다.
우리의 구성 관리는 Role 구문을 포함하도록 특정 권한 집합을 확장하고 실행했으며, 이로 인해 5.7 복제본에서 다운스트림 복제가 중단되었습니다. 우리는 업그레이드 기간 동안 영향을 받는 사용자에 대해 정의된 권한을 일시적으로 조정하여 이 문제를 해결했습니다.
캐릭터(character) collation 비호환성을 해결하기 위해 기본 문자 인코딩을 utf8로, 데이터 정렬을 utf8_unicode_ci로 설정해야 했습니다.
GitHub.com 모놀리스의 경우 Rails 구성을 통해 문자 정렬의 일관성이 보장되었으며 클라이언트 구성을 데이터베이스에 더 쉽게 표준화할 수 있었습니다. 그 결과 우리는 가장 중요한 애플리케이션에 대해 역방향 복제(backward replication)를 유지할 수 있다는 높은 확신을 갖게 되었습니다.
Challenges
우리는 테스트, 준비 및 업그레이드 과정에서 몇 가지 기술적인 문제에 직면했습니다.
What about Vitess?
우리는 관계형 데이터를 수평적으로 샤딩하기 위해 Vitess를 사용합니다. 대부분의 경우 Vitess 클러스터 업그레이드는 MySQL 클러스터 업그레이드와 크게 다르지 않았습니다. 우리는 이미 CI에서 Vitess를 실행하고 있었기 때문에 쿼리 호환성을 확인할 수 있었습니다.
샤딩된 클러스터에 대한 업그레이드 전략에서는 한 번에 하나의 샤드를 업그레이드했습니다. Vitess 프록시 계층인 VTgate는 MySQL 버전을 알리고 일부 클라이언트 동작은 이 버전 정보에 따라 다릅니다
예를 들어, 한 애플리케이션은 5.7 서버에 대한 쿼리 캐시를 비활성화하는 Java 클라이언트를 사용했는데, 8.0에서 쿼리 캐시가 제거되었기 때문에 해당 서버에 대한 차단 오류가 발생했습니다. 따라서 특정 키스페이스에 대해 단일 MySQL 호스트가 업그레이드되면 8.0을 알리기 위해 VTgate 설정도 업데이트했는지 확인해야 했습니다.
Replication delay
우리는 읽기 전용 복제본을 사용하여 읽기 가용성을 확장합니다. GitHub.com 최신 데이터를 제공하기 위해 낮은 복제 지연이 필요합니다.
테스트 초기에 MySQL 8.0.28에서 패치된 복제 버그가 발생했습니다.
Replication: If a replica server with the system variable replica_preserve_commit_order = 1 set was used under intensive load for a long period, the instance could run out of commit order sequence tickets. Incorrect behavior after the maximum value was exceeded caused the applier to hang and the applier worker threads to wait indefinitely on the commit order queue. The commit order sequence ticket generator now wraps around correctly. Thanks to Zhai Weixiang for the contribution. (Bug #32891221, Bug #103636)
우리는 우연히 이 버그를 발생시키는 모든 기준을 충족하게 되었습니다.
- GTID 기반 복제를 사용하기 때문에 replica_preserve_commit_order를 사용합니다.
- 우리는 많은 클러스터와 가장 중요한 모든 클러스터에 대해 오랜 시간 동안 집중적인 로드를 가하고 있습니다. 대부분의 클러스터는 쓰기 작업이 매우 많습니다.
이 버그는 이미 업스트림에 패치되었으므로 8.0.28보다 높은 버전의 MySQL을 배포하고 있는지 확인하면 됩니다.
또한 복제 지연을 유발하는 과도한 쓰기가 MySQL 8.0에서 더욱 악화되는 것을 관찰했습니다. 이로 인해 쓰기가 과도하게 급증하는 것을 방지하는 것이 더욱 중요해졌습니다. GitHub에서는 freno를 사용하여 복제 지연에 따라 쓰기 워크로드를 조절합니다.
Queries would pass CI but fail on production
우리는 프로덕션 환경에서 처음으로 문제가 불가피하게 발생한다는 것을 알았습니다. 따라서 복제본 업그레이드를 통한 점진적인 롤아웃 전략을 세웠습니다. CI를 통과했지만 실제 워크로드가 발생할 때 프로덕션에서는 실패하는 쿼리가 발생했습니다. 특히 큰 WHERE IN 절이 포함된 쿼리로 인해 MySQL을 크래시시키는 문제가 있었습니다.
수만 개가 넘는 값을 포함하는 대규모 WHERE IN 쿼리가 있었습니다. 이러한 경우에는 업그레이드 프로세스를 계속하기 전에 쿼리를 다시 작성해야 했습니다. 쿼리 샘플링은 이러한 문제를 추적하고 감지하는 데 도움이 되었습니다. GitHub에서는 쿼리 관찰성을 위해 SaaS 데이터베이스 성능 모니터인 Solarwinds DPM(VividCortex)을 사용합니다.
Learnings and takeaways
테스트, 성능 조정, 확인된 문제 해결 사이에서 전체 업그레이드 프로세스는 1년 이상 걸렸으며 GitHub의 여러 팀의 엔지니어가 참여했습니다. 스테이징 클러스터, GitHub.com을 지원하는 프로덕션 클러스터, 내부 도구를 지원하는 인스턴스를 포함하여 전체 제품군을 MySQL 8.0으로 업그레이드했습니다. 이번 업그레이드는 관찰 가능성(observability) 플랫폼, 테스트 계획 및 롤백 기능의 중요성을 강조했습니다. 테스트 및 점진적 롤아웃 전략을 통해 문제를 조기에 식별하고 기본 업그레이드에서 새로운 실패 모드가 발생할 가능성을 줄일 수 있었습니다.
점진적인 롤아웃 전략이 있었지만 여전히 모든 단계에서 롤백할 수 있는 기능이 필요했고 롤백이 필요한 시기를 나타내는 신호를 식별할 수 있는 관찰 가능성이 필요했습니다. 롤백 활성화 시 가장 어려운 점은 새로운 8.0 primary에서 5.7 복제본으로의 역방향 복제를 유지하는 것이었습니다. 우리는 Trilogy 클라이언트 라이브러리의 일관성이 연결 동작에 대한 더 많은 예측 가능성을 제공하고 기본 Rails 모놀리스의 연결이 역방향 복제를 중단하지 않는다는 확신을 가질 수 있게 해준다는 것을 알게 되었습니다.
그러나 서로 다른 프레임워크/언어를 사용하는 여러 클라이언트의 연결이 있는 일부 MySQL 클러스터의 경우 역방향 복제가 몇 시간 만에 중단되어 롤백 기회가 단축되었습니다. 다행히도 이러한 경우는 거의 없었고 롤백이 필요하기 전에 복제가 중단되는 경우가 없었습니다. 그러나 우리에게는 이것이 잘 알려져 있고 잘 이해된 클라이언트 측 연결 구성을 갖는 것이 이점이 있다는 교훈이었습니다. 이러한 구성의 일관성을 보장하기 위해 지침과 프레임워크를 개발하는 것의 가치를 강조했습니다.
데이터를 분할하려는 이전의 노력은 성과를 거두었습니다. 이를 통해 다양한 데이터 도메인에 대해 더 많은 대상 업그레이드를 수행할 수 있었습니다. 하나의 실패하는 쿼리가 전체 클러스터의 업그레이드를 차단하는 중요한 상황이었으며, 서로 다른 워크로드를 분할함으로써 우리는 점진적으로 업그레이드할 수 있었고 프로세스 중에 발생한 알려지지 않은 리스크의 범위를 줄일 수 있었습니다. 여기서의 절충점은 MySQL 제품군이 성장했다는 것을 의미한다는 것입니다.
GitHub가 마지막으로 MySQL 버전을 업그레이드했을 때 5개의 데이터베이스 클러스터가 있었고 이제는 50개 이상의 클러스터가 있습니다. 성공적으로 업그레이드하려면 관측 가능성(옵저버빌리티), 도구, 관리를 위한 프로세스에 투자해야 했습니다.
Conclusion
MySQL 업그레이드는 우리가 수행해야 하는 일상적인 유지 관리의 한 유형일 뿐이며, 우리 회사에서 실행하는 모든 소프트웨어에 대한 업그레이드 경로를 확보하는 것이 중요합니다. 업그레이드 프로젝트의 일환으로 우리는 MySQL 버전 업그레이드를 성공적으로 완료하기 위한 새로운 프로세스와 운영 기능을 개발했습니다. 그러나 업그레이드 프로세스에는 여전히 수동 개입이 필요한 단계가 너무 많았으며 향후에는 MySQL 업그레이드를 완료하는 데 소요되는 노력과 시간을 줄이려고 합니다.
우리는 GitHub.com 이 증가함에 따라 우리의 플랫폼이 계속 증가할 것으로 예상되며, 시간이 지남에 따라 MySQL 클러스터 수를 늘리기 위해 데이터를 추가로 분할하는 것을 목표로 하고 있습니다. 운영 작업 및 자가 복구 기능을 위한 자동화를 구축하면 향후 MySQL 운영을 확장하는 데 도움이 될 수 있습니다. 우리는 신뢰할 수 있는 관리 및 자동화에 투자하면 github를 확장하고 필요한 유지 관리를 유지하여 보다 예측 가능하고 탄력적인 시스템을 제공할 수 있다고 믿습니다.
이 프로젝트의 교훈은 MySQL 자동화의 기초를 제공했으며 향후 업그레이드를 보다 효율적으로 수행하면서도 동일한 수준의 주의와 안전을 유지할 수 있는 기반을 마련할 것입니다.
• 원문 : Upgrading GitHub.com to MySQL 8.0
Principal DBA(MySQL, AWS Aurora, Oracle)
핀테크 서비스인 핀다에서 데이터베이스를 운영하고 있어요(at finda.co.kr)
Previous - 당근마켓, 위메프, Oracle Korea ACS / Fedora Kor UserGroup 운영중
Database 외에도 NoSQL , Linux , Python, Cloud, Http/PHP CGI 등에도 관심이 있습니다
purityboy83@gmail.com / admin@hoing.io