전체 데이터베이스에 모든 데이터를 한 테이블 혹은 데이터베이스에서 관리하기가 어려워진다.데이터베이스 볼륨이 커지면 커질수록 데이터베이스 읽기/쓰기 성능은 감소할 것이고, 데이터베이스가병목 지점이 될 것이다. 따라서 이를 적절히 분할할 필요가 있다. 데이터베이스를 분할하는 방법은 크게샤딩(sharding)과파티셔닝(partitioning)이 있다. 이 두 가지 기술은 모두거대한 데이터셋을 서브셋으로 분리하여 관리하는 방법이다. 이번 포스팅에서는 이 둘의 개념과 차이점에 대해 알아본다.
파티셔닝이란?
MySQL 기준으로 기술되었다.
파티셔닝은 매우 큰 테이블을 여러개의 테이블로 분할하는 작업이다. 큰 데이터를 여러 테이블로 나눠 저장하기 때문에 쿼리 성능이 개선될 수 있다. 이때, 데이터는물리적으로 여러 테이블로 분산하여 저장되지만, 사용자는 마치하나의 테이블에 접근하는 것과 같이 사용할 수 있다는 점이 특징이다. 파티셔닝은 MySQL 단에서 자체적으로 지원하는 기능이다.
MySQL 에서 파티셔닝을 지원하는 스토리지 엔진은 InnoDB와 NDB이며, MyISAM은 파티셔닝을 지원하지 않는다.
파티셔닝 종류는 Oracle과 MySQL 공식문서에서 소개한 4가지 방식을 간단하게 설명하겠다.
List Partitioning
데이터 값이 특정 목록에 포함된 경우 데이터를 분리한다. 위 그림 처럼 특정 지역별로 데이터를 분할할 때 사용할 수 있겠다.
Range Partitioning
데이터를 특정 범위 기준으로 분할할 때 사용한다. 위 처럼 1~2월, 3~4월, 5~6월 … 으로 데이터를 분리할 때 사용할 수 있다.
Hash Partitioning
해시 함수를 사용하여 데이터를 분할할 때 사용한다. 특정 컬럼의 값을 해싱하여 저장할 파티션을 선택한다. MySQL 공식 문서에 따르면, 여러 컬럼으로 해싱하는 것은 크게 권장하지 않는다고 한다 (참고).
Composite Partitioning
위 파티셔닝 종류 중 두개 이상을 사용하는 방식이다.
샤딩이란?
샤딩은동일한 스키마를 가지고 있는 여러대의 데이터베이스 서버들에 데이터를작은 단위로 나누어 분산 저장하는 기법이다. 이때, 작은 단위를샤드(shard)라고 부른다.
어떻게 보면 샤딩은 수평 파티셔닝의 일종이다. 차이점은 파티셔닝은 모든 데이터를 동일한 컴퓨터에 저장하지만, 샤딩은 데이터를 서로 다른 컴퓨터에 분산한다는 점이다. 물리적으로 서로 다른 컴퓨터에 데이터를 저장하므로, 쿼리 성능 향상과 더불어부하가 분산되는 효과까지 얻을 수 있다. 즉, 샤딩은 데이터베이스 차원의 수평 확장(scale-out)인 셈이다.
샤딩은 위와 같이 물리적으로 분산된 환경에서 사용되는 기법으로 데이터베이스 차원이 아닌애플리케이션 레벨에서 구현하는 것이 일반적이다. 다만 샤딩을플랫폼 차원에서 제공하는 시도가 많다고 한다.Naver d2 포스팅에 따르면, Hibernate Shards와 같이 애플리케이션 서버에서 동작하는 형태, CUBRID SHARD, Spock Proxy, Gizzard 와 같이 미들 티어(middle tier)에서 동작하는 형태, nStore나 MongoDB와 같이 데이터베이스 자체에서 샤딩을 제공하는 형태로 나뉜다고 한다.
(고등학교 때 MERN stack 공부하면서, mongodb 샤딩이 대체 뭐지?… 하고 그냥 넘어갔는데 드디어 공부했다!)
주의점
데이터를 물리적으로 독립된 데이터베이스에 각각 분할하여 저장하므로, 여러 샤드에 걸친 데이터를 조인하는 것이 어렵다. 또한, 한 데이터베이스에 집중적으로 데이터가 몰리면 Hotspot이 되어 성능이 느려진다. 따라서 데이터를 여러 샤드로 고르게 분배하는 것이 중요하다. 또 Celebrity Problem 등 다양한 문제가 존재한다. 자세한 내용은 이전에 작성한[가상면접 사례로 배우는 대규모 시스템 설계 기초] Chap01. 사용자 수에 따른 규모 확장성포스팅을 참고하자.
샤딩 종류
샤딩의 종류는 다양하지만, 크게 Hash Sharding, Range Sharding 두 가지를 알아보겠다.
Hash Sharding 중 나머지 연산을 사용한Modular Sharding을 알아본다. Modular Sharding은 PK값의 모듈러 연산 결과를 통해 샤드를 결정하는 방식이다.총 데이터베이스 수가 정해져있을 때 유용하다. 데이터베이스 개수가 줄어들거나 늘어나면 해시 함수도 변경해야하고, 따라서데이터의 재 정렬이 필요하다.
PK 값을 범위로 지정하여 샤드를 지정하는 방식이다. 예를 들어 PK가 1~1,000 까지는 1번 샤드에, 1,001~2,000 까지는 2번 샤드에, 2,001~ 부터는 3번 샤드에 저장할 수 있다. Hash Sharding 대비 데이터베이스증설 작업에 큰 리소스가 소요되지 않는다. 따라서급격히 증가할 수 있는 성격의 데이터는 Range Sharding 을 사용함이 좋아보인다. 이런 특징으로 Range Sharding은 Dynamic Sharding 으로도 불린다.
다만, 이렇게 기껏 분산을 시켜놨는데특정한 데이터베이스에만 부하가 몰릴 수 있다. 예를 들어 페이스북 게시물을 Range Sharding 했다고 가정해보자. 대부분의 트래픽은 최근에 작성한 게시물에서 발생할 것이다. 위 그림에서는 2, 3번 샤드에만 부하가 몰리는 것이다. 부하 분산을 위해 데이터가 몰리는 DB는 다시 재 샤딩(re-sharding)하고, 트래픽이 저조한 데이터베이스는 다시 통합하는 작업이 필요할 것이다.
하나의 RDBMS로 구성되어 있는 상품 DB를 Write DB와 Read DB를 분리하여 CQRS(Command Query Responsibility Segregation) Pattern을 적용해 본다면, 각각의 독립적인 인스턴스를 유연하게 운영할 수 있고, 장애 전파를 미연에 방지할 수 있지 않을까?라는생각이 들었습니다.
현재 지마켓은 하나의 상품 DB에서 수많은 Write 작업과 Read 작업이 이루어지고 있습니다.
특히 상품은 오픈마켓의 근간이 되는 데이터로 여러 도메인에서 동시다발적으로 DB를 사용하면서 가용 리소스의 한계를 가져오고, DB에 이슈가 생기면 지마켓 전체 장애로 확산될 수도 있습니다.
지마켓은 코어 데이터베이스로 Microsoft SQL Server(MSSQL)를 사용하고 있기 때문에 여러 방식을 생각해 볼 수 있습니다.
별도의 조회 DB를 구성
Microsoft SQL Server Replacation 기능을 이용해 복제 DB에 데이터 동기화
Trigger를 이용한 Sync 어플리케이션을 구현하여 데이터 동기화
실제로 지마켓에서는 두 가지 방법 모두 사용 중입니다.
그렇다면, 왜 기존 방식을 사용하여 익숙한 Microsoft SQL Server에 Read DB를 구성하는 대신 MySQL Cluster를 사용하였을까요?
첫 번째, 상대적으로 저렴한 비용을 들 수 있습니다.
상용 버전을 사용해도license 비용이 저렴합니다.
두 번째, 수평적 확장(scale-out)이 가능합니다.
수직적 확장(scale-up)을 통한 성능 확장은 한계가 있고, 이미 지마켓 코어 데이터베이스 서버의 하드웨어 스펙은 업계 최고임에도 불구하고 가용 리소스의 한계를 가지고 있습니다. 성능 확장이 필요한 경우,노드를 증설하거나 DB 인스턴스 클러스터 추가 등의 scale-out을 통해 유연한 대처가 가능합니다.
세 번째, 급격히 증가하는 상품 데이터의 샤딩(Sharding)을 통해 데이터베이스 처리 성능을 향상할 수 있습니다.
하나의 DB에 존재하는 수억 건의 대용량 상품 데이터를 여러 개의 DB에 나누어 저장하는 샤딩(Sharding)을 통해,데이터베이스 처리 작업을 여러 대의 서버에 분산 처리할 수 있어 처리 능력을 향상할 수 있습니다.
MySQL Cluster 도입을 결정했다면, Source DB로부터 Target DB로 실시간 데이터 동기화는 어떻게 처리하였는지 살펴봅시다.
실시간 데이터 복제
DB to DB 작업(특히 지금과 같이 Source DB는 Microsoft SQL Server이고 Target DB는 MySQL인 이기종 간 데이터 복제)에는 CDC(Change Data Capture)를 활용하여 실시간 데이터 동기화를 할 수 있습니다.
CDC(Change Data Capture)는 데이터베이스 내 데이터 변경을 식별하여 외부 저장소에 전달하는 기술을 의미하는데, 데이터 실시간 동기화나 데이터 변경 이벤트를 외부 시스템에 보내는 등 폭넓게 활용하고 있습니다.
대표적으로 오픈 소스 Debezium을 들 수 있는데, Debezium은 카프카 커넥터(Kafka Connect)의 Source 커넥터의 집합으로 다양한 Source 커넥터를 제공합니다. Kafka Connect는 Kafka로 레코드를 보내는 Debezium과 같은 소스 커넥터(Source connector)와 Kafka 토픽에서 다른 시스템으로 레코드를 전파하는 싱크 커넥터(Sink connector)로 구성되어 있습니다.
Debezium Architecture
그러나, CDC를 이용하여 실시간 데이터 동기화를 할 수가 없었습니다. 이미 지마켓 코어 데이터베이스는 Replication과 Trigger를 사용 중이고, 가용 리소스가 많지 않아 현실적으로 CDC를 활성화하기 힘들기 때문입니다. CDC 사용이 어렵다면 Debezium Architecture에서 착안하여소스 커넥터와 싱크 커넥터를 Application으로 구현하여 이를 활용해보는 것으로 계획을 수정하였습니다.
Architecture
소스 커넥터 역할을 하는 Application(이하 DBStreaming)
Spark Streaming을 이용하여 SQLServer에서 변경되는 데이터를 주기적으로 읽어와 Kafka 토픽으로 전송합니다.
싱크 커넥터 역할을 하는 Application(이하 Rummikub-Consumer)
Spring Kafka의 Listener를 이용하여 해당 토픽에서 레코드를 가져와 MySQL에 반영합니다.
데이터 분할
지마켓 상품은 수억 건의 대용량 데이터로 매우 거대하고 많은 정보를 포함하고 있어, 수직적이든 수평적이든 분할(파티셔닝, Partitioning)이 필요합니다.
이에 동일한 테이블 스키마로 여러 개의 DB에 나누어 저장하는 Sharding(수평적 파티셔닝)을 적용, Sharding을 사용함으로써 데이터를 분산시켜 저장하는 Application, 분산된 데이터베이스에서 조회하는 Application에서는 Sharding 처리가 가능해야 합니다.
물론 직접 Sharding 처리를 구현해볼 수 있지만 Application의 복잡도가 증가하는 것에 비해, Sharding Middleware를 사용한다면 개발 편의성과 안정성이 강화됩니다. Sharding Middleware 선택에도 고려해야 할 사항이 많지만,신속한 개발과 시스템의 안정성을 위해 Open Source Sharding Middleware인 ShardingSphere를 사용하게 되었습니다.
샤딩이란 무엇인가?
일단 ShardingSphere(샤딩스피어) 설명을 진행하기에 앞서 샤딩에 대해서 간단하게 이해합시다!
한 개의 DB에 많은 데이터가 쌓이다 보면 용량도 커지지만, 데이터 처리 성능이 큰 영향을 받게 됩니다. 이러한 부분을 해결하기 위해서 샤딩을 사용합니다.샤딩을 사용하면 DB 트래픽이 분산되어 큰 효과를 볼 수 있습니다. 여기서,샤딩의 핵심은 데이터를 효율적으로 분산시키는 것입니다.
분산이 잘 되지 않는다면 한쪽으로 Data 요청이 몰리게 되고, 결국 성능이 느려지게 됩니다. 샤드 키를 통해 데이터를 균일하게 분산시키는 것이 샤딩의 필수 조건입니다.
샤딩의 종류
위 사진을 보다시피 크게 두 가지로 나뉩니다.
수직 분할 (좌측)
수평 분할 (우측)
보편적으로대부분수평 분할을 사용합니다.
1. 수직 분할
한 스키마에 저장되어 있는 데이터를 특정 칼럼 단위로 잘라내어 분할 저장합니다.수직 파티셔닝은 각 스키마를 나누고 데이터가 따라 옮겨갑니다.
결국 논리적 엔티티들을 다른 물리 엔티티들로 나누는 것을 의미합니다.
2. 수평 분할
한 스키마에 저장되어 있는 데이터를 특정 알고리즘을 통해 행 단위로 잘라내어 분할 저장합니다.수평 분할은 하나로 구성된 스키마를 동일한 구성의 여러 개의 스키마로 분리한 후, 각 스키마에 어떤 데이터가 저장될지를 샤드키를 기준으로 분리합니다.
제일 보편적으로 많이 사용하고, 애플리케이션 서버 레벨에서 진행할 수도 있습니다. 또한 분할을 시키기 위한 샤딩 알고리즘을 필요로 합니다.
샤딩 알고리즘
수평 분할을 통한 샤딩은 샤딩 알고리즘이 필요합니다.기준이 되는 샤드키를 통해 어느 스키마에 접근하여 데이터를 핸들링할 것인지 정해야 하기 때문입니다. 한마디로 라우팅을 위한 알고리즘입니다.
크게 두 가지 샤딩 방법이 있습니다.
Modular Sharding
Range Sharding
1. Modular Sharding
Modular 샤딩은 샤드키를 모듈러 연산한 결과로 DB를 선택하는 방식입니다.
장점:Range 샤딩에 비해 데이터가 균일하게 분산됩니다.
단점:DB를 추가 증설하게 된다면 이미 적재된 데이터들의 재정렬이 필요합니다.
(ex) (DB는 2개, 상품번호가 0~10까지 있을 경우) 상품번호 % 2 모듈러 연산 시 DB1에는 0,2,4,6,8,10 상품, DB2에는 1,3,5,7,9 상품이 들어가게 됩니다.
Modular 샤딩은 데이터량이 일정 수준에서 유지될 것으로 예상되는 데이터 성격을 가진 곳에 적용할 때 어울리는 방식입니다.
실제적으로 상품 데이터를 적재하고 있으나, 해당 데이터는 장기적으로 미사용 할 경우 다른 디비에 옮겨 보관하기 때문에 적합합니다. 데이터가 꾸준히 늘어나더라도 적재속도가 그리 빠르지 않다면 문제없다고 합니다.
일단 데이터가 균일하게 분산된다는 점 자체가 트래픽을 안전하게 소화하면서도 DB 리소스를 최대한 활용할 수 있기 때문입니다.
2. Range Sharding
Range 샤딩은 샤드키의 범위를 기준으로 DB를 선택하는 방식입니다.
장점:Modular 샤딩에 비해 증설에 재정렬 비용이 들지 않습니다.
단점:일부 DB에 데이터가 몰릴 수 있습니다.
Range 샤딩은 증설작업에 드는 비용이 크지 않습니다. Modular의 경우 증설작업이 진행될 경우 기존 데이터들도 모두 재정렬을 해야 합니다. 이런 부분에서 편합니다.
하지만 많이 접근하는 데이터가 있는 DB 쪽으로 트래픽이나 데이터량이 몰릴 수 있습니다. 결국 샤딩을 했더라도 동일한 현상이 나타난다면 또 부하 분산을 통해 DB를 쪼개 재정렬하는 작업이 필요하고, 반대로 트래픽이 저조한 DB는 통합 작업을 통해 유지비용을 아끼도록 관리해야 합니다.
그래서
샤딩은 데이터가 많을 경우 효율적으로 데이터를 나누어 부하를 줄일 수 있습니다.
하지만운영 복잡도가 높아지기 때문에 최대한 샤딩을 피하여 개선할 수 있는 방법을 찾고 시도해 본 후 진행하는 것이 좋습니다만..저희 팀은 이미 많은 고난을 헤쳐 왔기 때문에 더없이 문제없었습니다.
샤딩 잘못쓰면 위 짤처럼 되어버립니다
저희는 리소스 사용 balance가 좋은 'Modular Sharding'을 이용하여 분산 처리의 효과를 극대화하여 사용하기로 했습니다. 확장이 필요할 경우 그 배수로 확장을 하기로 하고 MySQL Cluster를 구성하였습니다.
ShardingSphere
ShardingSphere의 가장 큰 장점은 위 그림처럼 분산된 테이블을 하나의 테이블처럼 사용할 수 있다는 점입니다. ShardingSphere는 Sharding-JDBC, Sharding-Proxy, Sharding-Sidecar(TODO) 독립된 제품으로 구성되어 있고, 각 제품군의 특징을 정리하면 다음과 같습니다.
Sharding-JDBC
shardingsphere-jdbc
ShardingSphere-JDBC는 Java의 JDBC 계층에서 추가 서비스를 제공하는 경량 Java 프레임워크입니다. 클라이언트가 데이터베이스에 직접 연결하면 jar 형식으로 서비스를 제공하며 추가 배포 및 종속성이 필요하지 않습니다. JDBC 및 모든 종류의 ORM 프레임워크와 완벽하게 호환되는 JDBC 드라이버의 향상된 버전으로 생각할 수 있습니다.
JDBC 기반 ORM 프레임워크(JPA, Hibernate, Mybatis, Spring JDBC 등)에 적용 가능
DBCP, C3P0, BoneCp, HikariCP 등의 서드파티 데이터베이스 연결 풀 지원
모든 종류의 JDBC 표준 데이터베이스 지원(MySQL, PostgreSQL, Oracle, SQLServer 등)
Sharding-Proxy
shardingsphere-proxy
ShardingSphere-Proxy는 이기종 언어를 지원하기 위해 데이터베이스 프로토콜을 캡슐화하여 데이터베이스 서버를 제공하는 프록시입니다. MySQL과 PostgreSQL 프로토콜이 제공되며, MySQL/PostgreSQL 프로토콜과 호환되는 모든 종류의 터미널을 사용하여 운영할 수 있어 DBA에게 더 친숙합니다.
MySQL/PostgreSQL 서버같이 직접 사용 가능
MariaDB와 같은 MySQL 기반 데이터베이스 및 openGauss와 같은 PostgreSQL 기반 데이터베이스와 호환
MySQL Command Client, MySQL Workbench 등 MySQL/PostgreSQL 프로토콜과 호환되는 모든 종류의 클라이언트에 적용 가능
ShardingSphere-JDBC
ShardingSphere-Proxy
데이터베이스
Any
MySQL/PostgreSQL
연결 수 비용
많음
적음
지원되는 언어
Java만
Any
성능
손실이 적음
비교적 손실이 높음
분산
가능
불가
정적 항목
불가
가능
Spring Boot를 사용하여 개발 중이고, 손실이 적고 자바 어플리케이션 위에서 JDBC 형태로 동작하는 ShardingSphere-JDBC 방식을 사용해 봅니다.
Architecture
ShardingSphere 적용기 (부제: 첫 번째 시련)
ShardingSphere-JDBC는 총 3가지 버전이 존재합니다.
Java API 버전
YML Configuration 버전
SpringBoot Starter 버전
YML Configuration과 SpringBoot Starter를 이용한다면 손쉽게 ShardingSphere DataSource를 설정할 수 있습니다.
하지만 사내 보안정책에 따라 DB 주소 및 계정정보를 노출시키지 않기 위해 만들어진 사내 라이브러리인 DCM(Database Connection String Management)을 사용하기 위해서는 Spring Boot Starter의 JNDI 혹은 Java API를 사용해 직접 DataSource를 주입해야 했습니다.
그리하여 ShardingSphere DataSource 생성 시에 DCM DataSource를 직접 주입하기 위해서 Java API버전을 선택하게 되었고,지마켓용으로 직접 YML Configuration 버전을 구현하였습니다.
기존의 Java API 버전을 사용한 설정 적용
일반적인 샤딩스피어에서 제공하는 'Java API' 버전을 이용하여 샤딩 스피어 기본 세팅을 한 코드입니다. 아래처럼 기나긴 코드와 알아보기 힘든 로직들로 인하여 가독성이 매우 떨어졌습니다.
//보기 힘든 영어들의 향연
private Collection<RuleConfiguration> getRuleConfigurations(){
ShardingRuleConfiguration ruleConfiguration = new ShardingRuleConfiguration();
// Sharding tables rules
Collection<ShardingTableRuleConfiguration> tableRulesConfList = new ArrayList<>();
ShardingStrategyConfiguration strategyConfiguration = new StandardShargindStrategyConfiguration("샤드키", "generalModShardingAlgorithm");
ShardingTableRuleConfiguration tableRuleConfiguration = new ShardingTableRuleConfiguration("테이블", "ds${0...1}.goods");
goodsTableRuleConfiguration.setDatabaseShardingStrategy(strategyConfiguration);
tableRuleConfList.add(goodsTableRuleConfiguration);
ruleConfiguration.setTables(tableRulesConfList);
// Algorithm rules
Properties generalModShardingAlgorithmProperties = new Properties();
generalModShardingAlgorithmProperties.setProperty("sharding-count", "4");
ShardingSphereAlgorithmConfiguration shardingSphereAlgorithmConfiguration = new ShardingSphereAlgorithmConfiguration("MOD", generalModShardingAlgorithmProperties);
Map<String, ShardingSphereAlgorithmConfiguration> shardingSphereAlgorithmConfigurationMap = new HashMap<>();
shardingSphereAlgorithmConfigurationMap.put("generalModShardingAlgorithm", shardingSphereAlgorithmConfiguration);
ruleConfiguration.setShardingAlgorithm(shardingSphereAlgorithmConfigurationMap);
Collection<RuleConfiguration> ruleConfigs = new ArrayList<>();
ruleConfigs.add(ruleConfiguration);
return ruleConfigs;
}
지마켓용 YML Configuration 버전을 사용한 설정 적용
기본 Java API 버전을 사용할 경우 유지보수를 어렵게 만들기 때문에 Java API 기반으로 지마켓용 세팅을 개발하여 YML로 편하게 세팅이 가능하도록 했습니다. 샤딩스피어 공식 문서를 보고 쉽게 세팅할 수 있도록 샤딩스피어에서 제공하는 SpringBoot Starter 버전과 거의 유사한 로직으로 설정하도록 했습니다.
...
gmarket:
shardingsphere:
datasources:
ds0: ...
ds1: ...
rules:
tables:
테이블명:
actual-data-nodes: ds$->{0..1}.테이블명
database-strategy:
sharding-column: 샤드키
sharding-algorithm-name: 샤드 모드명
sharding-algorithms:
모드명:
type: ...
props:
sharding-count: 샤드 mod 값
sharding:
broadcast-tables: #샤딩하지 않고, 모든 데이터베이스에 저장할 경우
- 브로드캐스트 테이블명
...
이러한 과정들을 거쳐 드디어 ShardingSphere를 사용할 수 있게 되었습니다.
Data Migration
메인 로직들은 모두 SQLServer를 통해 처리되어 있으니 해당 DB에 저장되어 있던 데이터를 MySQL로 옮기는 데이터 마이그레이션 과정이 필요합니다. 마이그레이션 또한 Sharding을 사용하여 데이터를 저장해야 하므로 Application으로 구현해야 합니다.
애초에는 대용량 데이터를 빠르게 마이그레이션 하는 것을 목표로 로직을 최소화하여 insert 하도록 구현하였습니다. Export를 수행한 시점 이후에 SQLServer에서 발생하는 데이터 변경 건은 MySQL에 반영되지 않으므로 마이그레이션 작업 시간을 최소한으로 하고, Export를 수행한 시점 이후 데이터 변경 건을 빠르게 복제해 가는 것을 계획하였습니다.
그러나, 실제 운영에서 마이그레이션을 진행해보니 MySQL Primary 인스턴스에서 Secondary 인스턴스로 복제 지연이 발생하였습니다.
마이그레이션 작업 시간 최소화가 힘들어진 이상 당초 계획을 수정하여, Rummikub-Consumer(싱크 커넥터 역할을 하는 Application)와 마이그레이션 Application이 같이 실행되어도 데이터 정합성에 문제가 없도록 Upsert 처리로 변경되었습니다.
Architecture
운영 반영기 (부제: 두 번째 시련)
드디어 운영 서버에 반영을 하는 날이 되었습니다. 해당 프로젝트는 새로운 프로젝트라 기존 트래픽이 없었고, Real 운영 배포를 진행하기 위해 서버에 반영하고 테스트를 하던 와중..
요청을 보내면 무려 3초 뒤에 응답이 오는 획기적인 서비스가 되었습니다.
해당 현상은 개발서버에서는 전혀 발견되지 않았고 운영 서버에서만 발견되었던 현상이라 매우 당황스러웠습니다.
여러 가지 방법 시도
프로그램을 첫 실행한 후, 첫 번째 요청건이 굉장히 느려지는 현상이었는데요. 현재 서비스를 쿠버네티스로 관리하고 있는 현재 상황상, 각 Pod마다 첫 요청건이 느려지는 문제였습니다. 많은 트래픽이 들어오는 상황의 3초 동안의 latency는 굉장히 큰 부분이며, 차후 서비스를 새로 배포할 때마다 해당 현상을 겪어야 한다는 것이 문제였습니다.
개발서버에서 발견하지 못했던 이유가 Pod 1개로 테스트를 하고 있었기 때문에 눈에 크게 띄지 못했던 것이었습니다.프로파일링을 통해 디테일하게 확인해보니 샤딩스피어쪽 + DB 연결 쪽 문제가 확인되었는데요. 저희가 시도해본 방법은 아래와 같습니다.
Hikari Connection Config 세팅 변경
ShardingSphere 사용하지 않는 API 구현하여 확인
사내 DCM 세팅 확인
jvm 속성 변경
ShardingSphere 버전 변경
그 외 등등..
위 방식들은 모두 실패하였습니다.
도대체 무엇이 문제일까 싶어 ShardingSphere 공식 문서를 정독하고 github repository의 issue 탭과 Release history를 확인했습니다.
Warm Up 코드를 통한 해결
그러던 중 closed issue에서 동일한 현상을 호소하는 글을 발견합니다. (Issue Reporter 외에도 꽤나 다수 고통받고 있었습니다.)
(질문자): 샤딩스피어 버전 x.xx 버전 사용하고 있는데 너무 느립니다.
프로그램 시작하고 첫번째 실행구문이 너무 느린데 이게 5~6초 걸려요.
공식 홈페이지에서 이 문제를 해결할 마땅한 세팅방법을 찾지 못했어요.
(다른사람-1,2,3...): 저두요..
(메인터넌스 답변): SQL은 처음 수신되었을때 AST로 구문 분석이 되며 캐시에 저장됩니다.
SQL 웜업 기능을 사용하면 애플리케이션을 시작하기 전에 사용자가 SQL을 신속하게 사용할 수 있을겁니다.
저희는 해당 기능에 대해 원하는 사람이 있다면 논의해서 설계할 수 있습니다.
실제적으로Spring에서의 WarmUp 로직을 실행시키도록 하면 해결되는 문제였습니다. 실제 ShardingSphere + DataDog의 환장의 콜라보로 지연시간이 발생한 것으로 추가적으로 확인이 되었습니다.
일단 이러한 웜업 코드를 작성하기 전 JVM의 기본 아키텍처를 한번 짚고 넘어갑시다.
JVM 기본 아키텍처
새로운 JVM 프로세스가 시작될 때마다 ClassLoader 인스턴스에 인해 필요한 모든 클래스가 메모리에 로드됩니다. 이 프로세스는 3단계로 이루어집니다.
Bootstrap Class Loading
Java 코드와 java.lang과 같은 필수 Java 클래스를 메모리에 로드합니다.
Extension Class Loading
java.ext.dirs 경로에 있는 모든 JAR 파일을 로드합니다. 개발자가 수동으로 Jar을 추가하거나 maven과 gradle 기반이 아닌 응용프로그램에선 이런 모든 클래스가 이 단계에서 로드됩니다.
Application Class Loading
애플리케이션 클래스 경로에 있는 모든 클래스를 로드합니다.
이 초기화 프로세스는 Lazy Loading으로 동작됩니다.
결국 이러한 로딩 방식에 따라 중요 클래스는 프로세스 시작과 동시에 JVM 캐시로 로드되어 빠르게 접근이 가능하지만, 다른 클래스들은 프로세스가 실행 중일 때 실제 요청을 받아야만 로드되는 방식입니다.
Spring에서의 Warm Up
이러한 부분을 바탕으로 저희는 Spring에서의 Warm Up 코드를 작성하였습니다. 필요한 클래스들을 미리 Warm Up 처리하려면 스프링 부트 애플리케이션이 정상적으로 실행된 이후에 Warm Up을 수행해야 합니다. 이때 스프링 부트에서는 간단하게ApplicationListener의구현체를 통해 프로세스를 만들 수 있습니다.
사실내부적으로 모든 클래스를 분석하여 캐싱하는 것은 굉장히 어렵기 때문에 내부적으로 하나하나 분석해서 처리하기보다는 외부에서 처리하는 방식을 사용하였습니다.
클라이언트가 서버로 요청하는 상황을 가정하여 진행되는 프로세스들을 캐싱하는 부분이니 웹 애플리케이션의 Warm Up에 알맞다고 판단되었습니다. 외부에서 처리하는 방식으로는 서비스 내부에서 Http 요청을 보내는 방식이 제일 간편하나,문제 되는 부분이 샤딩스피어 + DataDog인 부분으로 파악되어 자체적으로 Service를 실행하도록 하는 부분으로 처리하였습니다. (혹시나 다른 좋은 방법이 있다면 꼭 알려주세요!)
해당 프로젝트는 각 샤딩키마다 접근할 수 있는 DB가 정해져 있고 전체 접근 DB는 4곳이기 때문에 각 DB를 예열할 수 있는 샤딩키를 전달함으로써 Warm Up 처리를 완료했습니다!
Warm Up 코드를 통해 첫 요청건이라도 아래와 같이 매우 준수한 응답 속도를 낼 수 있었습니다.
추가적으로해당 프로젝트를 진행하며 DataDog을 통해 한눈에 볼 수 있는 모니터링 시스템도 구축하게 되었습니다.
마치며
할 수 이따. 우리는 어른이니까...
이런저런 시행착오를 겪으며 결국 Sharded MySQL Cluster를 도입하여 대량의 상품 데이터 대상으로 대규모 트래픽을 빠르게 처리하는 상품 조회 플랫폼을 성공적으로 구축하게 되었습니다.
CDC를 활성화할 수 없어 Kafka Connect Echo System을 이용할 수 없었다는 점, Target DB가 Sharding 된 MySQL Cluster이라는 점 때문에 개발자의 역할과 책임이 커지게 되었고 그 과정에서 다소 생산성이 떨어지게 되었습니다. 또한, 오픈 소스를 이용하여 개발하면서 개발 편의성의 이점과 동시에 경험의 부족으로 이슈 트래킹에 어려움도 겪었습니다.
시스템 고도화와 자동화 숙제는 남아있지만, 경험을 바탕으로 Sharded MySQL Cluster 사용이 활발해지는 날을 기대해 봅니다.