이 글은 Martin Kleppmann의 데이터 중심 애플리케이션 설계를 읽고 기억하고자 적는 게시글입니다.
6. 파티셔닝
파티셔닝에 대해서 공부해보았습니다. 이전 챕터들에서 복제에 대한 내용을 공부하기도 했지만, 대규모의 데이터를 다루게 되면 복제만으로는 안정성을 얻기 쉽지 않습니다.
100TB의 데이터를 저장하기 위해 필요한 컴퓨터 자원이 100TB 디스크를 탑재한 컴퓨터 2대라면, 이후 더 많은 데이터가 수집되었을 때마다 얼마나 많은 디스크를 추가해야 할 지 감이 잡히지 않습니다.
그래서 우리는 상대적으로 적은 용량의 컴퓨터 여러 대에 분산으로 저장하는 방식을 사용합니다. 마치 HDFS의 시스템을 생각하면 편할 것 같습니다. 그 또한 대표적인 파티셔닝이니까요.
파티셔닝을 한다는 이야기는 곧 데이터를 분산 저장하고 분산 처리한다는 의미를 가지고 있습니다. 여기서 분산 처리는 각 노드에서 동일한 작업에 대해 병렬 처리를 한다는 이야기로 해석 할 수 있고, Hadoop의 HDFS와 MapReduce가 정확히 그런 방식을 적용한 느낌입니다.
파티셔닝의 주요 목적은 확장성입니다.
파티셔닝과 복제
일반적으로 파티셔닝과 복제는 동시에 수행됩니다. 각 파티션의 복사본을 여러 노드에 분산하여 저장합니다. 그러면 장애가 발생해도 동일한 데이터를 다른 노드에서 받을 수 있기 때문에 내결함성을 갖게 됩니다.
각 노드는 어떤 파티션의 리더 파티션이 될 수 있으며, 다른 파티션의 팔로워 파티션이 될 수 있습니다. 하지만 각 파티션의 리더 파티션은 특정 노드 1개만 가지고 있을 수 있습니다.
이 부분은 이전에 공부한 Kafka의 파티션 개념과 유사하다는 생각을 합니다.
키-값 데이터 파티셔닝
파티셔닝을 사용 할 때 주의깊게 봐야 하는 부분이 “어떤 레코드(문서)를 어느 노드에 저장해야 하는가?”에 대한 규칙입니다.
파티셔닝이 고르지 않게 되면 특정 파티션에 데이터가 이상하게 많이 쌓이거나 많은 질의를 받아서 부하가 크게 몰릴 수 있습니다. 이런 경우 파티셔닝의 효과가 매우 떨어집니다. 이렇듯 불균형하게 부하가 몰린 파티션을 핫스팟이라고 합니다.
만약 데이터가 키-값으로 묶여있는 데이터라면 어떻게 이러한 문제를 최적화 할 수 있는지 방법을 알아보겠습니다.
키 범위 기준 파티셔닝
첫 번째 방법은 키의 범위로 파티셔닝하는 방법입니다. 예를들면 A로 시작하는 단어와 B로 시작하는 단어와 같이 키 자체가 가지고 있는 범위를 가지고 파티셔닝하는 방법입니다.
이 경우 문제가 발생 할 수 있는데, A로 시작하는 단어가 Z로 시작하는 단어보다 월등하게 많다는 점 입니다. 그렇기 때문에 이 범위를 잘 잡아야 각 파티션에 골고루 데이터가 저장 될 수 있습니다.
관리자가 수동으로 관리 할 수 있지만, 빅테이블 기반의 데이터베이스, 리싱크DB 에서는 자동으로 설정해준다고 합니다.
이렇게 키 범위로 파티셔닝하면 파티션내의 키가 범위를 갖기 때문에 LSM트리와 SS 테이블과 같이 범위 스캔이 쉬워집니다
즉 파티셔닝을 설계 할 때 범위 스캔의 효율이 중요하다면, 이런 부분을 설계에 반영해야 합니다.
예를 들면 날짜로 범위 스캔을 하고 싶을 때 년-월-일을 기반으로 파티셔닝을 한다고 가정하겠습니다. 이 때 문제는 파티셔닝 자체의 기준을 년-월-일로 해버리면 특정 파티션만 부하가 몰리는 경향이 생길 수 있습니다.
왜냐하면 현재 입력되는 데이터가 2022-10-31일의 타임스탬프를 가지고 있다면, 모든 데이터가 2022-10-31일의 파티션에 입력되기 때문입니다. 이럴때는 앞에 다른 센서 이름을 붙여서 파티셔닝 할 때 사용하면 더욱 효율적일 수 있습니다.
키의 해시값 기준 파티셔닝
쏠림현상과 핫스팟을 방지하기 위해 많은 분산 데이터베이스 환경에서는 키의 파티션을 정하는 데 해시 함수를 사용합니다. 이번에 사용하게 될 예정인 Mongo DB는 MD5를 사용하여 간단한 해싱을 사용합니다.
프로그래밍 언어에서 제공하는 해시함수와 같이 동일한 값에 대해서는 다른 값이 나오는 해시함수는 사용 할 수 없습니다.
이러한 해시 파티셔닝은 범위 파티셔닝보다 상대적으로 더욱 키를 균일하게 분산할 수 있습니다. 물론 분산에 대해서는 해시 알고리즘의 영향을 받지만 대체적으로 균일하게 분산됩니다.
하지만 문제점이 있습니다. 바로 범위 파티셔닝과는 다르게 범위 질의의 효율을 잃어버리기 때문에 범위 질의에서 어려움이 있습니다.
2.4버전 이후의 MongoDB는 해시 파티셔닝을 사용하고 있는데, 만약 MongoDB에서 범위질의를 사용하면 모든 파티션을 읽게 됩니다. 그 뿐 아니라 리악, 카우치베이스, 볼드모트는 범위질의를 지원하지 않습니다.
카산드라의 경우에는 두 파티셔닝 방법을 어느정도 조합해서 사용하고, 키의 첫 부분만을 해싱해서 파티션을 설정하고 남은 컬럼을 가지고 SS테이블에서 데이터를 정렬하는 색인으로 사용하여 범위스캔을 지원합니다.
파티셔닝과 보조 색인
지금까지 알아 본 파티셔닝은 키를 기반으로 했지만, 키를 제외하고도 보조 색인이라는 기능이 있습니다. RDB에서는 인덱스라고 부르는 검색을 위한 식별자정도로 생각 할 수 있습니다.
보조 색인은 파티셔닝과는 별개의 작업이기 때문에 깔끔하게 대응되지 않는 문제가 있습니다. 그래서 보조 색인이 있는 데이터베이스는 문서 기반 파티셔닝 혹은 용어 기반 파티셔닝을 사용하고 있습니다.
문서 기준 보조 색인 파티셔닝
문서 기준 보조 색인의 예시로는 각 문서에 ID값이 주어질때 해당 ID 값의 범위로 색인하는 방법이 있습니다. 예를들면 1~500번까지의 문서와 501번부터 1000번째 문서와 같이 나눌 수 있습니다.
이 방식은 각 파티션이 완전히 독립적으로 동작하고, 그렇기 때문에 어떤 데이터를 다룰 때 해당 데이터가 있는 파티션에만 접근하면 됩니다. 이러한 특징으로 지역 색인이라고 불리고 있습니다.
하지만 문제점은 어떤 데이터가 어느 파티션에 있는지를 알 수 없다는 점입니다. 1파티션에 A 데이터가 있는지를 알기 위해서는 일단 1파티션을 읽어야 알 수 있습니다. 그렇기 때문에 문서 기준 보조 색인은 특정 데이터를 찾을 때 모든 파티션에 요청을 날려야 합니다. 이런 이유로 전역 색인이라고 불리며, 모든 파티션에 요청을 날리는 것은 스캐터/개더라고 부릅니다.
용어 기준 보조 색인 파티셔닝
용어 기준 보조 색인은 특정 단어나 데이터를 기준으로 색인하는 방식입니다. 예를들면 에어컨이 옵션으로 있는 방을 찾고 있다면 option:”airconditioner”라는 값이 있는 문서를 찾는다고 해보겠습니다. 이 경우 시작 단어인 “a”를 기준으로 색인을 하게 되면 이 데이터가 어느 파티션에 있는지를 쉽게 알 수 있습니다.
하지만 시작단어와 같은 기준을 사용하면 이전에 고려했던 문제가 발생 할 수 있습니다. 바로 데이터가 고르게 파티셔닝 되지 않을 수 있다는 점입니다. 물론 정렬되어 있는 기준이기 때문에 범위 탐색에는 좋겠지만요. 이 경우에는 해시 값을 이용해서 색인을 가질 수 있습니다.
파티션 재균형화
데이터베이스를 운영하다보면 다음과 같은 이유로 변화가 발생 할 수 있습니다.
- 질의 처리량이 늘어서 CPU를 늘리고싶다.
- 데이터 셋 크기가 커져서 디스크나 램을 추가하고 싶다.
- 장비에 장애가 발생해서 다른 장비가 넘겨받아야한다.
이러한 변화에서는 재균형화가 정상적으로 동작해야하고, 그 때 최소 요구사항은 다음과 같습니다.
- 재균형화 이후 부하는 고르게 분포되어야 한다.
- 재균형화 도중에도 데이터베이스는 읽기 쓰기가 동작해야한다.
- 재균형화는 빨리 실행되고, 네트워크 IO와 디스크 IO가 최소화 되어야 한다.
Worst Case: 해시값에 N mod 연산을 수행
해시값을 이용해서 파티셔닝을 할 때 N mod연산을 수행합니다. 하지만 만약 서버가 추가되고 N+M mod 연산을 모든 키 값들에 다시 실행을 시키면, 그 부하는 엄청 날 수 있습니다.
적어도 모든 값들에 한번씩 연산이 들어가기 때문에 불필요한 네트워크 IO가 발생하는 건 당연합니다.
파티션 개수 고정
애초에 개발단계에서 파티션을 많이 만들어놓고 동작시키는 방법입니다. 이 경우에는 굳이 모든 값들에 대해서 다시 연산을 할 필요 없이 파티셔 자체를 옮기면 모든게 해결됩니다. 그러면 정확히 이동하는 파티션만 네트워크 IO가 발생하기 때문에 효과적입니다.
엘라스틱서치도 이러한 재균형화를 수행한다고 합니다. 하지만, 이 경우에는 설계단계에서 이미 파티션의 갯수가 어느정도 설정이 되어야 하고, 각 파티션 조차도 하나의 관리 포인트가 되기 때문에 너무 많은 파티션을 만들면 재균형화, 노드 장애시 비용이 커집니다. 하지만 너무 작게 만들면 오버헤드가 커집니다.
적당한 파티션 수를 설정하는 것도 매우 중요한 설계 포인트입니다.
동적 파티셔닝
HBase, MongoDB, 리싱크DB 와 같은 데이터베이스에서는 파티셔닝을 동적으로 분할 및 결합해준다고 합니다.
특정 파티션에 많은 데이터가 쌓이게 되면 자동으로 파티션을 하나 생성하고 절반에 가까운 데이터를 옮겨주면서 데이터 크기에 맞는 파티션을 자동으로 조절해주는 방식입니다. 최적의 파티션을 직관이 아닌 데이터에 적합하게 설정 할 수 있다는 점에서 매우 좋은 파티셔닝 방법입니다.
단점으로는 초기에는 1개의 파티션에서 시작해서 초기 부하가 한 파티션으로 몰린다는 문제는 있지만, 초기 설계에서 N개로 초기 파티션을 잡아주는 방법으로 어느정도 해결 할 수 있다고 합니다.
노드 비례 파티셔닝
동적 파티셔닝이 데이터 크기에 맞는 파티션의 갯수로 조절했다면 노드 비례 파티셔닝은 노드의 수와 파티션의 갯수를 비례하게 관리하는 방법입니다. 노드가 추가되면 해당 노드가 가지는 파티션을 할당하고 랜덤한 기존 노드의 파티션을 절반으로 나누어 주는 방식으로 전체 파티션 크기를 조절합니다.
요청 라우팅
이제 파티셔닝의 방법과 재균형화에 대해서 공부했기 때문에 어떤 데이터베이스의 파티셔닝을 할 때 고려 할 사항을 알 수 있었습니다. 아직 문제는 남아있습니다.
바로 어떤 키를 읽을 때 어떤 주소, 포트번호로 접속해야 할까?에 대한 의문이 남아 있습니다. 키의 범위 혹은 해시값을 이용하여 데이터를 분할하는 것 까지는 이해했지만, 실제 물리적으로 데이터가 저장 된 저장소에 접근은 필수적입니다. 그러면 쿼리를 날렸을 때 어떻게 찾아 갈 수 있을까요?
다음과 같은 접근 방식이 있습니다.
- 클라이언트가 아무 노드에 접근해서 있으면 읽고, 없으면 데이터가 있는 노드로 전달
- 모든 요청을 라우팅 계층에 보내고 해당 노드를 알아낸 뒤 전송
- 파티셔닝 방법과 파티션이 어떤 노드에 할당되어 있는지 클라이언트가 알도록 하는 방법
많은 분산 저장소는 주키퍼를 이용해서 이런 부분을 관리하고 있습니다.
몽고DB는 유사한 관리 방법을 사용하지만 자체 설정 서버를 구현하여 사용한다고 합니다.