파티셔닝

복제와 달리, 파티셔닝은 데이터를 여러 파티션으로 쪼갤 때 사용하는 용어이다. 이 작업을 샤딩이라고 하기도 한다. 몽고 DB, 엘라스틱서치, 솔라클라우드에서는 샤드라고 부르고, Hbase 에서는 리전, 빅테이블에서는 태블릿, 카산드라와 리악에서는 브이로그라고 부른다. 하지만 파티션이 좀 더 일반적인 용어이므로 파티션을 사용하도록 하겠다.

데이터 파티셔닝을 통하여 다음과 같은 이점을 얻을 수 있다.

  1. 대용량 데이터셋을 여러 디스크에 분산시킬 수 있다. 이를 통하여 질의 부하를 여러 프로세스로 분산시킬 수 있다.
  2. 각 노드에서 쿼리를 처리할 수 있으므로, 질의 처리량을 늘리기 위해 노드를 추가할 수 있다.

파티셔닝과 복제

파티셔닝과 복제를 동시에 적용할 수 있다.

partitionAndReplication

이 경우에는 복제의 모든 특성이 적용된다. 즉, 복제와 파티셔닝은 서로 연관이 없다.

키-값 데이터 파티셔닝

파티셔닝의 핵심은 데이터와 질의 부하를 노드 사이에 고르게 분산시키는 것 이다. 파티셔닝이 고르게 이루어지지 않아 하나의 노드가 요청을 더 많이 받는 것을 쏠렸다(skewed) 고 한다. 데이터가 쏠리게 되면 파티셔닝의 효과가 매우 떨어진다. 이렇게 불균형하게 부하가 높은 파티션을 핫스팟 이라 한다.

핫스팟을 피하기 위한 가장 간단한 방법은 레코드를 할당할 노드를 무작위로 선택하는 것 이다. 그러나 커다란 단점이 있다. 어떤 파티션에서 데이터를 읽어야 하는지 알 수 없게 되는 것이다. 즉, 모든 노드에 병렬적으로 질의하여야 한다.

더 좋은 방법이 있다.

키 범위 기준 파티셔닝

이 방법은 종이 백과사전처럼 각 파티션에 연속된 범위의 키를 할당하는 것이다. 각 범위 사이의 경계를 알면, 키가 어떤 파티션에 속하는지 쉽게 확인할 수 있다.

예를 들면

[Ark - Bell] > 파티션 1 [Bike - Cake] > 파티션 2

처럼 파티션을 분할하는 것이다. 키 범위 기준 파티셔닝에는 다음과 같은 특징이 있다.

  1. 키의 범위가 항상 동일할 필요는 없다. 데이터가 고르게 분포하지 않았을 수 있기 때문이다.
  2. 파티션 경계는 관리자가 수동으로 선택하거나, 데이터베이스에서 자동으로 선택할 수 있다.
  3. 각 파티션 내에서는 키를 정렬된 순서로 저장할 수 있다(SS 테이블이나 LSM 트리처럼)
  • 그러나 키를 통하여 분할하는 방식은 특정한 접근 패턴이 핫스팟을 유발할 수 있다. 타임스탬프를 기준으로 하루 단위로 파티션을 분할한다고 가정해 보자. 키를 정렬된 순서로 저장한다면 가장 마지막 파티션이 모든 요청 부하를 받게 될 것이다.

키의 해시값 기준 파티셔닝

데이터가 skewed 되어 핫스팟이 발생할 수 있기 때문에, 많은 분산 데이터스토어는 해시함수를 사용하여 키의 파티션을 결정한다. 해시함수를 사용하여 키를 결정하면, 입력 문자열의 유사성과는 관련 없이 결과물은 파티션에 균등하게 배분된다.

파티셔닝용 해시 함수는 보안이 강력할 필요가 없다. 카산드라와 몽고DB 는 MD5 를 사용한다. 키에 적합한 해시 함수를 구했다면, 각 파티션에 키의 범위 대신에 해시값 범위를 가지고 데이터를 파티셔닝하면 된다.

partitionWithHash

이 기법은 키를 파티션 사이에 균일하게 분산시키기에 좋다. 그러나 단점이 있는데, 해시함수를 사용하여 키를 결정하면 정렬된 키를 가지고 데이터를 쉽게 질의할 수 있는 장점을 잃어버리게 된다. 몽고DB 에서는 해시 기반 샤딩 모드를 키면, range query 가 모든 파티션에 전송된다.

카산드라는 두 가지 파티셔닝 전략를 조합하여 사용한다. 카산드라에서 테이블을 생성할 때 여러 컬럼을 포함하는 복합 기본키를 지정할 수 있다. 키의 첫 부분에만 해싱을 적용해 파티션 결정에 사용하고, 남은 컬럼은 SS 테이블에서 데이터를 정렬하는 색인으로 사용한다. 따라서 복합키의 첫 번째 컬럼으로는 range query 로 질의할 수 없지만, 다른 키로는 범위 스캔을 효율적으로 활용할 수 있다.

쏠림 작업부하와 핫스팟 완화

극단적인 케이스를 가정해 보자 : 항상 동일한 키로 읽고 쓰는 경우. 이 경우에는 하나의 파티션으로 읽기/쓰기 요청이 몰리게 된다. 이런 케이스는 없을 거 같지만 실제로는 존재한다. 수백만의 팔로워를 지닌 유명인이 SNS 에서 글을 작성하는 경우를 가정해 보자. 유명인이 실행한 작업 때문에 동일한 키에 막대한 양의 데이터를 기록해야 할 수 있다. (키는 아마도 유명인의 userId 나 사람들이 댓글을 다는 actionId 가 될 것이다.)

현재 데이터 시스템에서는 이를 보정하기 어려우므로 애플리케이션단에서 이를 보정하여야 한다. 요청이 많이 쏠리는 키를 발견하면, 그 키의 시작이나 끝에 임의의 숫자를 붙이는 것이다. 임의의 10진수 두 개만 붙이더라도, 키의 쓰기 작업을 100개의 키로 분산시킬 수 있다.

다만 이렇게 하면 읽기 작업이 어려워진다. 100개의 키를 읽어서 데이터를 조립하여야 하기 때문이다. 이 방법은 요청이 몰리는 소수의 키에만 적용하는 것이 타당하다.

파티셔닝과 보조 색인

지금까지 설명한 파티셔닝은 키-값 데이터 모델에만 적용할 수 있다. 기본키를 통해 레코드에 접근한다면 키를 통해 파티션을 찾아서 그 파티션으로 읽기/쓰기 요청을 진행하는 것이다. 보조 색인은 레코드를 식별하는 목적이 아니라, 특정한 값이 발생한 항목을 검색하는 수단이다.

예를 들면 “car : red” 인 걸 모두 찾을 때 사용한다. 즉, 키값을 식별하는 것이 아니라 검색하는 용도로 사용하는 경우가 잦다. 이런 보조 색인은 파티션에 깔끔하게 대응되지 않는 경우가 많다. 이 경우 널리 사용하는 두 가지 방법이 있다. 문서 기준으로 색인하는 방법과 용어 기반으로 색인하는 것이다.

문서 기준 보조 색인 파티셔닝

문서 내의 값을 색인하면, 해당 문서를 저장하고 있는 파티션이 보조 색인을 보관하는 것이다. 예를 들면 두 개의 파티션이 있다고 가정해 보자. 두 파티션에 문서가 다음과 같이 분할된다고 가정하자.

파티션 1 : 0 ~ 499
파티션 2 : 500 ~ 1000

이제, 문서 내의 color 라는 프로퍼티를 색인한다고 하자. 이 경우 다음과 같이 색인이 된다.

secondaryIndexing

이 색인은 다음과 같은 특징이 있다.

  • 각 파티션은 자신의 보조 색인을 유지하며, 그 파티션에 속하는 문서만 담당한다.
    • 이런 까닭에 지역 색인(local index) 라고 부르기도 한다.
  • 모든 파티션에 걸쳐 데이터가 있기 때문에, 모든 파티션으로 질의를 하여 데이터를 가져와야 한다.
    • 이런 방식을 scatter / getter 라고 부르는데, 특히 여러 개의 보조 색인을 사용하는 경우 비용이 클 수 있다.

용어 기준 보조 색인 파티셔닝

모든 데이터를 저장하는 전역 색인(global index) 를 만들 수도 있다. 그러나 한 노드에만 데이터를 저장할 수는 없다. 그 노드가 병목이 되기 때문이다. 따라서 global index 또한 파티셔닝을 할 필요가 있다. 모든 파티션에 있는 자동차 정보는 color:red 에 저장되지만, 색깔 색인은 a ~ r 은 파티션 0에, s ~ z 는 파티션 1 에 저장한다.

찾고자 하는 용어에 따라 파티셔닝이 되므로, 이런 색인을 term-partitioned 되었다고 한다. 다음과 같은 장단점이 있다.

  • 모든 파티션에 scatter / getter 할 필요 없이 저장되어 있는 파티션에 바로 질의할 수 있다.
  • 쓰기에 단점이 있다
    • 데이터가 여러 파티션에 저장되기 때문에, 쓰기에 영향받는 모든 파티션에 분산 트렌젝션을 하여야 한다.
    • 현실에서는 보통 비동기로 term-partitioned 를 갱신하는데, 평소에는 번개같이 빠르지만 인프라에 결함이 생기면 느릴 수 있다.

파티션 재균형화

  • 장비에 장애가 발생하여 다른 장비가 이 장비의 역할을 넘겨받아야 한다.
  • 디스크나 램, cpu 를 추가하고 싶다.

이 경우에는 데이터가 하나의 장비에서 다른 장비로 이동하여야 한다. 이것을 재균형화(rebalancing) 이라고 부른다. rebalancing 은 다음과 같은 조건을 만족하여야 한다.

  • 리밸런싱 후에 부하가 노드 사이에 균등히 분배되어야 한다.
  • 리밸런싱을 하는 와중에도 클러스터는 요청을 처리하여야 한다.
  • 디스크 I/O 와 네트워크 쓰루풋을 유지하기 위해 노드들 사이에 데이터가 필요 이상으로 이동하면 안 된다.

재균형화 전략

쓰면 안 되는 방법: 해시값에 모드 N 연산을 실행

mod 연산을 사용하여 파티션을 분배할 수 있다. 예를 들면, hash(key) mod 파티션개수 를 하여 노드에 파티션을 분배하는 것이다. 잘 동작할 것 처럼 보이지만, 새로운 서버가 추가되는 경우 파티션 개수가 증가하게 되고, 대부분의 파티션을 다른 서버로 이동시켜야 한다. 이 경우 리밸런싱 비용이 지나치게 커진다.

파티션 개수 고정

  • 파티션을 노드 개수보다 많이 만든다.
  • 각각 노드에 여러 파티션을 할당한다.
  • 클러스터에 노드가 추가되면, 새 노드는 기존 노드에서 파티션을 몇 개 뺏어온다. 파티션은 수정되지 않고 다만 노드 사이를 이동할 뿐이다.

이 전략을 사용하는 경우, 보통 파티션 개수를 변경하지 않는다. 이론적으로는 파티션을 합치고 분할할 수 있지만, 개수를 변경하지 않는 편이 편리하기 때문이다.

동적 파티셔닝

키 범위 파티셔닝을 이용하는 경우에는, 파티션 경계와 개수가 고정되어 있는 것이 불편할 수 있다. 파티션들 사이에 키를 균등하게 분배하여야 하는데, 경계를 잘못 설정하면 노드 사이에 파티션 개수가 균등하게 분배되지 않는 경우가 있다. 이를 방지하기 위해, 리싱크나 HBase 에서는 파티션을 동적으로 만든다. 파티션이 일정 크기 이상이 되면(HBase 는 10GB), 파티션을 두 개로 쪼갠다. 반대로 데이터가 많이 삭제되어 파티션의 크기가 일정 수준 이하가 되면 파티션이 합쳐질 수 있다.

그러나 빈 데이터베이스는 시작할 때 파티션의 정보가 없으므로, 파티션이 한 개로 시작한다는 단점이 있다. 이렇게 되면 파티션이 하나기 때문에 모든 요청이 한 개의 노드에서 처리되는 문제가 발생한다. 이를 방자히가 위해, HBase 와 몽고DB 에서는 초기 파티션값을 설정할 수 있다.

노드 비례 파티셔닝

파티션 개수를 고정하게 되면, 개별 파티션의 크기가 데이터의 크기에 비례하게 된다. 반면에, 동적 파티셔닝을 사용하게 되면 파티션의 개수가 데이터의 크기에 비례하게 된다. 두 경우 다 노드의 개수와는 상관이 없다. 카산드라와 케타마에서 사용하는 방법은, 노드의 개수에 파티션의 개수를 비례하게 하는 것이다. 다시 말헤, 노드에 할당하는 파티션의 개수를 고정한다. 노드 개수가 동일한 경우에는 각 파티션의 용량이 커지고, 노드 개수를 늘리면 파티션이 증가하면서 각 파티션의 용량이 줄어든다.

새 노드를 추가하는 경우 고정된 개수의 파티션을 무작위로 선택하여 분할하고, 분할한 파티션의 절반을 새 노드로 옮긴다. 여러 파티션에 대해 평균적으로 보면, 균등한 부하가 새 노드에 갈 것을 기대할 수 있다.

운영: 자동 재균형화와 수동 재균형화

완전히 자동으로 재균형화하는 것은 유지보수에 손이 덜 가지만, 예측하기 어렵다. 재균형화는 요청 경로를 재설정하고 파티션을 이동해야 하기 때문에 부하가 큰 작업이다. 이것이 자동 장애 감지와 동시에 진행되면 위험해질 수 있다. 예를 들면 노드 한 대에 부하가 심하게 걸렸다고 하자. 다른 노드들은 과부하가 걸린 노드가 죽었다고 생각하여 재균형화를 할 수 있다. 이 경우에는 파이션이 이동하면서 네트워크에 부하를 주어 문제가 생길 수 있다.

이런 이유로 재균형화 작업에는 사람이 개입하는게 좋을 수도 있다. 완전 자동보다는 느리지만, 예상치 못한 상황을 방지할 수 있다.

요청 라우팅

이제 데이터셋을 여러 노드에 파티셔닝할 수 있다. 그러면, 다음과 같은 문제가 남는다: 클라이언트에서 요청을 보내려고 할 때, 어떤 노드로 접속해야 하는지 알 수 있을까? 파티션이 재균형화되면서 노드에 할당되는 파티션이 변경된다. 누군가에게 적확한 응답을 주려면, 파티션에 어떤 데이터가 있는지 명확하게 알고 있어야 한다.

이는 일반적인 서비스 찾기(service discovery) 문제로, 네트워크에 의해 접속되는, 고가용성을 지원하는 장비들은 모두 비슷한 문제가 있다. 상위 수준에서 보면 몇 가지 해결책이 있다.

  • 클라이언트가 아무 노드에나 접속하게 한다. 해당 노드에 데이터가 있다면 데이터를 반환하고, 그렇지 않다면 데이터가 있는 노드의 위치를 알려준다.
  • 클라이언트의 모든 요청을 라우팅 계층으로 보낸다. 라우팅 계층에서는, 데이터가 있는 위치를 클라이언트에게 알려준다.
  • 클라이언트는 데이터가 어디 있는지 모두 알고 있다.

이 문제의 핵심 요소는 라우팅 결정을 내리는 곳이 라우팅의 변경사항을 어떻게 아느냐 이다.

howtoKnowLocation

이를 위해 많은 분산 데이터 시스템은 파티션의 위치 변경을 추적하기 위해 주키퍼와 같은 별도의 코디네이터를 사용한다. 각 노드는 주키퍼에 자신을 등록하고, 주키퍼는 파티션과 노드 사이의 데이터 매핑을 관리한다. 파티션 소유자가 변경된다던지, 노드가 추가/삭제되면 주키퍼는 이를 라우팅 계층에 알려 라우팅 정보를 최신상태로 유지한다.

coordinator

정리

이 장에서는 대용량 데이터셋을 여러 파티션으로 분할하는 방법에 대해 알아보았다. 파티셔닝의 목적은 핫스팟(불균형적으로 높은 부하를 받는 노드)이 생기지 않게 하면서 데이터와 질의 부하를 여러 장비에 균일하게 분배하는 것이다. 그렇게 하려면 데이터에 적합한 파티셔닝 방식을 선택해야 하고 클러스터에 노드가 추가되거나 클러스터에서 노드가 제거될 때 파티션 재균형화를 실행해야 한다.

두 가지 주요 파티셔닝 방식이 있다.

  • 키 범위 파티셔닝 : 키가 정렬되어 있고, 개별 파티션은 키의 최소값 ~ 최대값 내의 데이터를 담당한다.
  • 해시 파티셔닝 : 각 키에 해시함수를 적용하고, 개별 파티션은 해시된 값의 최소값 ~ 최대값 내의 데이터를 담당한다.

두 가지 방법을 섞어 복합키를 활용할 수도 있다.

복합키를 사용하면 보조 색인을 어떻게 파티셔닝할지 결정하여야 한다. 문서 파티셔닝(지역 색인) 과, 용어 파티셔닝(전역 색인) 전략을 활용할 수 있다.

마지막으로 파티션을 이동시키는 리밸런싱과 리밸런싱 후에 어떻게 정확한 파티션으로 질의를 라우팅하는지에 대해 알아보았다.