0. 시작
파티셔닝, 샤딩, 레플리케이션은 모두 대규모 데이터베이스 관리, 성능 최적화, 그리고 시스템의 안정성 및 가용성을 향상시키기 위한 전략이라고 할 수 있습니다. CS공부도 조금 해야하고, 마침 새로 시작하는 프로젝트를 MSA로 진행할 것 같아서, 한번 이 개념들에 대해서 알아볼게요
1. 파티셔닝(Partitioning)
파티셔닝이란 기본적으로 Database Table을 더 작은 Table로 나누는 것을 의미합니다. 파티셔닝은 크게 두 가지 종류로 나눌 수 있습니다.
Vertical Partitioning
Column을 기준으로 table을 나누는 방식입니다.
Horizontal Partitioning
Row를 기준으로 table을 나누는 방식입니다.
먼저, Vertical Partitioning부터 알아보겠습니다.
2. Vertical Partitioning
게시판을 의미하는 Article이라는 테이블이 이렇게 정의되어 있고, 사용되고 있다고 가정해 보겠습니다.
위의 테이블을 기준으로, 어떠한 게시판이 만들어져 있다고 생각해볼게요. 게시판의 목록의 형태를 생각해보면, 보통 내용을 제외한 목록을 보여주고 있을 거에요.
SELECT id, title , ... , comment_cnt
FROM article
WHERE ...
아마 이러한 형식의 쿼리문으로 목록을 불러올 것 같습니다. 이 쿼리문은, article이라는 테이블에서 특정 조건을 만족하는 게시글들을 불러오겠네요. 여기서 어떤 조건은, 시간이 될 수도 있고 작성자 기준이 될 수도 있겠죠?
어떠한 조건이던간에, 목록을 불러올 때는 content라는 항목은 필요 없으니 이를 제외한 쿼리를 작성할 것입니다.
여기서 중요한 점은, 테이블에 있는 이런 데이터들은 HDD, SDD와 같은 저장공간에 저장이 되어있을거에요. 이 쿼리문이 실제로 어떻게 동작하는지 간략하게 설명하면, 저장공간에서 저 쿼리문을 불러올 때 관심있는 항목들인 id, title... 만 불러오는 것이 아니라 일반적으로는 행 전체를 불러온 뒤, 쿼리문을 기준으로 필터링을 해서 원하는 정보만을 가져옵니다.
여기서 중요한 점은, content라는 항목은 다른 항목들에 비해 차지하는 크기가 매우 클 가능성이 높지만, 보여주려고 하는 데이터가 아니라는 점이에요. 즉, 실제 필요한 항목이 아니어서 쿼리문을 저렇게 작성했는데, 사실은 모든 행을 저장공간에서 불러와서 메모리에 올린 다음에, 원하는 항목만 필터링 해서 가져온다는 것입니다.
설명이 복잡했는데요, 요약하면 content라는 관심 없는 항목에 대해서 I/O 작업에 대한 부담이 생긴다는 것이에요. 이 화면에서 사용되지 않는 content까지도 일단 메모리에 올려야 하는데, 하필이면 사이즈가 큰 항목인거죠.
Where절에 Index가 잘 걸려있다면 체감하지 못할 수도 있지만, Full-Scan이 일어날때에는 체감이 될 정도로 성능에 영향을 끼칠 수 있는 부분입니다.
이런 경우에 칼럼을 기준으로 테이블을 분리하는 방법인 Vertical Partitioning을 활용할 수 있어요.
이렇게 분리하면, 무거운 content는 다른 테이블로 이동했으므로 게시글 목록을 불러올 때 빠른 Select가 가능하게 되었습니다. 만약에 전체 게시글 정보가 필요하면, join을 적절히 활용하면 되겠죠?
Vertical Partitioning은 정규화가 되어있는 테이블에도 사용 가능하고, 또 보안에 민감한 정보들을 따로 모아두는 용도로도 활용할 수 있습니다. 또한, 자주 사용되는 혹은 자주 사용되지 않는 항목들로 구성하여 활용할수도 있겠네요.
3. Horizontal Partitioning
이번에는 row 기준으로 table을 나누는 방식인 Horizontal Partitioning이에요. Vertical과 달리, 이 방식은 테이블의 스키마에는 변화가 없어요. row 기준으로 나누니 당연하겠죠? 예제를 통해서 자세히 알아볼게요.
Subscription이라는 테이블의 예시입니다. 유튜브에서 사용자의 구독 정보를 포함하는 형태의 간단한 예시에요. 이 테이블은 어느 정도의 데이터를 최대치로 가질 수 있을까요?
사용자 수를 N으로, 채널의 수를 M으로 가정한다면 이 테이블의 최대 행의 수(모든 사용자가 모든 채널을 구독)는 N*M이 될거에요. 따라서, 사용자가 백만명이고 채널 수가 천개라면 최대 행의 수는 10억개가 되겠네요.
여기서 주의깊게 생각해봐야 할 부분이 있어요. 우선, 테이블의 크기가 커질수록 인덱스의 크기도 커질거에요. 따라서, 테이블에 읽기/쓰기 작업이 있을 때 마다 인덱스에서 처리되는 시간도 조금씩 늘어날겁니다. 인덱스를 타고 효율적으로 검색한다고 해도, 인덱스의 갯수 자체가 많아지면 시간이 조금 더 걸릴거에요.
이런 상황에서 사용될 수 있는 방법이 Horizontal Partitioning 입니다. 이것을 수행하는 여러가지 방법론이 있는데, 가장 많이 사용되는 hash-based의 방식을 설명해 보겠습니다.
이런 큰 테이블을 만들 때, Hash Function도 하나 만듭니다. 이 함수는 user_id를 입력했을 때 0, 1, 2, 3중 한 값을 반환한다고 가정해보겠습니다. 이제, Subscription과 같은 스키마 구조를 가진 Subscription_0, Subscription_1, Subscription_2, Subscription_3 라는 테이블을 만들고, 이 해시 함수가 반환하는 숫자에 해당하는 테이블에 값을 배정해주는거에요. 테이블의 갯수는 프로젝트의 요구사항, 여러 상황에 따라서 조정해서 구성하면 되겠습니다.
위에서 user_id를 입력으로 넣는다고 했는데요, 이러한 값을 partition key라고 합니다. 이 때, a라는 유저가 구독한 모든 채널을 조회하려면 어떻게 해야할까요? 단순하게, 해시 함수에 a값을 넣고, 반환하는 번호에 해당하는 테이블로 가서 조회하면 됩니다.
그렇다면, user_id가 아닌 특정한 채널을 구독한 모든 사용자를 불러오려면 어떻게 해야할까요? channel_id는 partition key가 아니기 때문에, 모든 테이블(0, 1, 2, 3)을 순회하면서 조회를 해봐야해요. 따라서, partition key는 가장 많이 사용될 패턴에 따라서 정해주는게 정말 중요해요. 그래야, 이렇게 테이블을 나눈 이점을 최대한 활용할 수 있습니다. 또한, 데이터가 균등하게 분배될 수 있도록 해시 함수를 정의하는 것도 매우 중요해요. 이렇게 테이블을 나눠놓았는데, 특정 테이블에 레코드들이 몰리면 테이블을 나눈 이점을 많이 활용할 수 없겠죠?
위에서 설명한 해시 기반의 Horizontal Partitioning은 한번 partition이 나눠지면, 이후에 partition을 추가하기가 매우 까다로워서 신중하게 설계해야합니다. 이미 저장되어 있는 레코드들도 새로운 partition으로 옮겨줘야 할 가능성이 생기기 때문이에요. 실제 서비스를 운영하면서 이러한 작업을 하는건 매우 부담스러운 작업일 것이니깐요.
해시 기반 외에도, 순차적인 값(날짜, 시간, 숫자 범위...)의 범위를 기준으로 나누는 Range-based, 특정 카테고리(지역, 국가, 부서...) 기반으로 나누는 List-based 등의 방법들도 존재합니다.
4. Sharding
샤딩은 앞에서 보았던 Horizontal Partitioning과 굉장히 유사해요. 실제로 동작 자체도 동일하게 이루어집니다. 즉, row를 기준으로 테이블을 나누게 됩니다. Horizontal Partitioning과의 큰 차이점은 각각의 Partition들이 독립된 DB 서버에 저장된다는 점입니다.
위에서 보았던, Subscription_0, Subscription_1, Subscription_2, Subscription_3들은 Horizontal Partitioning에서는 같은 DB 서버에 저장됩니다. 이러한 방식은, 하나의 컴퓨터(서버)에 저장이 되기 때문에 백엔드로부터 많은 요청이 발생하면 어떤 Partition에 대한 요청이던 한 서버의 cpu와 memory를 사용해서 요청을 처리하게 됩니다. 즉, 하드웨어 자원이 한정되어 있습니다.
이를 샤딩에서는 각각 다른 서버에 저장하는 거에요. 이 경우에는, 백엔드로부터 많은 요청이 들어와도, 각각 Partition에 해당하는 DB 서버가 존재하기 때문에 부하를 분산시키는 효과를 가질 수 있습니다.
요약하면, 샤딩은 각 Partition을 서로 다른 DB 서버에 저장해서 부하를 분산시키고자 하는 방식입니다.
규모가 큰 서비스, 데이터가 많이 쌓이는 테이블, 트래픽이 많이 몰리는 테이블은 이런 식으로 샤딩을 활용하여 각 Partition 마다 독립된 DB 서버를 할당하고, 이를 통해 트래픽을 분산시켜 DB 서버의 부하를 낮출 수 있습니다. 이러한 샤딩의 경우에는, 위에서 Partition key라고 부르던 것을 Shard key라고 부르고, 각 Partition을 Shard라고 부릅니다.
서버에 여유만 있다면, 샤딩은 매우 좋은 방식 같아보이고 실제로도 그렇습니다. 하지만 고려해야 할 부분은 트랜잭션에 관한 부분이에요. 다른 DB 서버에 데이터들이 존재하기 때문에, 트랜잭션 관리가 실제로 많이 복잡해집니다. 이를 위해 2PC, SAGA 패턴 등 다양한 방법들이 존재합니다. 이 부분에 대해서는 MSA를 적용해보면서 조금 더 학습하고 작성해 보겠습니다.
5. Replication
어떠한 테이블이 DB 서버에 저장되어 있고, 백엔드 서버에서 DB 서버로 read/write 요청을 보내는 경우를 가정해 보겠습니다. 그런데 이 때, DB 서버에 어떠한 문제가 생긴다면 정상적으로 동작할 수 없기 때문에, 사용자들은 피해를 볼거에요. 이런 상황이 실제 서비스에서는 발생하면 안되기 때문에, 어떻게든 해결을 해줘야 하는데 이럴 때 사용하는 방식 중 하나가 레플리케이션입니다.
레플리케이션이란, 메인 DB 서버와 동기화된 보조 DB 서버를 운영하는 것을 의미합니다. 즉, 메인 DB 서버에서 write 작업이 발생하면, 보조 DB 서버에도 같은 작업이 발생해요. 즉, 위의 상황에서 메인 DB 서버에 문제가 발생했을 시, 보조 DB 서버를 통해 문제를 해결하는 방식이 레플리케이션입니다.
이러한 방식을 failover(장애 극복 기능) 이라고도 부르고, 이런 식의 장애 상황이 발생했음에도 서비스를 운영할 수 있게 하는 이러한 구조, 특성을 High Availability(HA, 고 가용성) 이라고 합니다.
레플리케이션은 이렇게 사용할 수 있을 뿐만 아니라, 동기화된 보조 DB를 활용하여 서버의 트래픽이 몰릴 시 read 작업을 보조 DB로 분산 시켜 부하를 적절히 완화하는데 이용할수도 있습니다.
레플리케이션에서는, 메인 DB 서버와 보조 DB 서버를 (master - slave, primary - secondary, leader - replica) 이러한 형태로 호칭합니다. 사실, 저도 master-slave 형태의 말들을 많이 들어왔고 사용해왔는데요 이러한 호칭 방식이 구시대적인 방식이니 더이상 이렇게 쓰지 말자 라는 의견들도 있다고 하네요.
'DB' 카테고리의 다른 글
트랜잭션(Transaction) (0) | 2023.08.28 |
---|