DynamoDB(2/2)

  1. Key-value store의 일반 특성

NoSQL에는 크게 Key-value, Key-document, Key-column Family, graph 로 나뉜다. 그 중 DynamoDB는 key-value 저장소이며 단순히 get(key)과 put(key,value) 동작만을 지원한다.

value에 해당하는 값은 바이너리 오브젝트이며 데이터베이스에서 봤을 때는 값을 알 수 없는 하나의 덩어리로 보여지게 된다 (불투명하다고 일컬음) 따라서 value에 대한 범위 검색이라던지, 한번의 조회로 value 내용을 이용한 다른 key 검색 둥의 쿼리는 불가능하다. 대신 hash 알고리즘을 이용해 key를 hash값으로 변환하여 값을 찾아가는데 검색 속도가 매우 빠르다.

    2. 파티셔닝 알고리즘

DynamoDB에서 get이나 put 요청을 처리하는 노드를 coordinator 노드라고 하는데 요청하는 key에 따라 coordinator 노드가 달라지게 된다. 즉 모든 노드는 특정 key 범위를 처리하도록 역할이 지정되어 있으며 이 범위를 정하는 과정이 파티셔닝 알고리즘이다.

노드 자신이 담당할 key 범위를 정하기 위해선 먼저 노드들의 위치가 정해져야 하는데 이를 위해 노드들은 임의의 일련번호를 받게 된다. (여기서 위치라고 하는 것은 데이터 센터내에 서버의 물리적 위치를 의미하는 것이 아니라 논리적인 위치를 말한다) 일련번호라는 것은 hash 알고리즘을 통해 구해지는데 MD5 hash 알고리즘을 이용해 128bit로 반환되는 함수 값을 의미한다(이후부터는 포지션이라 언급함) Hash 알고리즘의 특징 상 포지션은 임의의 값을 가지게 된다. 포지션대로 일렬로 죽 늘어선 노드를 떠올려 보자. 이제 첫 시작노드와 끝 노드를 이어 붙이면 링 구조가 나오는데 이 링 구조가 노드들의 포지션을 완성한 형태가 된다.

노드들의 위치가 정해졌으면 이제 담당하는 key범위는 어떻게 할당받을까?
put()요청에서 주어진 key 역시 Md5의 Hash 알고리즘 변환을 하게 되며 반환 값을 가지고 링 구조 어딘가에 위치하게 된다. 이 링의 시계방향을 돌며 첫 번째로 마주치는 노드가 바로 coordinator 노드가 된다. coordinator 노드는 오브젝트를 write 하고 옆 노드로 복제본을 전송하는 역할 및 클라이언트에게 OK응답을 보내는 역할을 하게 된다. 아래 그림에서는 key K에 대해 B가 coordinator가 되며 C와 D 노드로 값을 복제하는 역할을 하게 된다.

sosp-figure2-small

이상 본 것과 같이 노드건 get, put 요청에서 쓰이는 key건 모두 동일한 hashing 알고리즘에 의해 링을 찾아가는 구조임을 알 수 있다. 이러한 이유로 Dynamo에서 파티셔닝 알고리즘은 consistent hashing 기법을 사용한다고 한다.

여기서 한 가지 이슈가 생길 수 있다. 노드의 위치가 랜덤으로 정해지는 과정에서 노드의 분포가 균일하지 않을 수 있다는 것이다. 노드간 간격이 균일해야 노드가 처리하는 부하가 비슷할텐데 어떤 노드 간격은 매우 크고, 어떤 노드 간격은 매우 좁아서 처리량이 다른 결과가 나타날 수도 있다는 것이다. 이를 해결하기 위해 Dynamo는 physical한 노드를 여러개의 논리적인 노드로 나타낸다. 위 그림에서 A부터 G까지 7개의 노드가 있는 것으로 보이지만, 실제로는 3개 혹은 4개의 물리적인 노드가 있을 수도 있는 것이다. 이렇게 물리적인 노드 하나를 여러개의 가상노드로 만듦으로써 평균적으로 노드가 처리해야 할 부하의 양이 비슷해지도록 만든다.

   3. 정족수(Quorum)

put 요청을 했을 때 coordinator 노드는 자신에게 write 하고 복제본을 보내 응답을 받아야 하는데, 몇 개의 노드에 복제본을 보내야 할까?

Dynamo에서는 정족수 개념을 사용하는데 N 값은 put 요청시 저장되어야 할 노드의 개수를 의미한다. 즉 coordinator 노드는 자신의 제외한 N-1개의 노드에게 복제본을 전송하여 key를 복사하는 것이다. 위 그림에서 보면 N=3이라고 봤을 때, key K에 대해 노드 B가 노드 C와 D에 복사를 하는 것이다. 이 때 서비스의 응답속도를 위해서 N-1개의 노드로부터의 응답을 모두 기다리지 않는다. N-1보다 작은 W개의 노드로부터만 응답이 오면 정상이라고 판단하고 클라이언트에게 OK 응답을 보내게 된다. 가령 N이 3이고 W가 2라면 coordinator 노드는 자신을 제외한 2개의 노드에 복사를 하지만 일단 1개의 노드로부터 OK응답이 오면 해당 put 요청은 정상 처리가 되었다고 판단하는 것이다. N, W 값 외에 R 값이 존재하는데 R은 읽기 요청에 정상으로 응답하는 최소 노드의 개수를 의미한다. R 값이 2라면 최소 2개 노드로부터 응답이 돌아오면 정상처리 되었다고 판단하고 클라이언트에게 응답하게 된다.  R값과 W값의 합이 N값만 넘으면 비둘기집 원리에 의해 read시 적어도 1개의 노드는 최근 값을 리턴하게 된다.

만약 쓰기가 거의 없고 읽기가 대부분이며, 읽기 성능이 매우 중요한 시스템이라면 R을 1, W를 2나 3으로 할 수 있다. 반면 R이 2 또는 3을 가지고 W이 1을 가진다면 어떤 시스템이 될까?
쓰기시 N개의 노드들 중 하나만 응답해도 성공으로 간주하기 때문에 쓰기 성능은 향상되겠지만 읽기시 항상 최소 2개 또는 3개 노드로부터 값을 받아와야 하기 때문에 읽기 성능은 조금 느릴 것이다.
AWS에서 DynamoDB 서비스를 사용해보면 R값과 W값을 입력하도록 되어있는데 각각 9999까지 입력이 가능하다. 그러나 그만큼 비용이 기하급수적으로 늘어나기 때문에 대부분 R과 W를 모두 2로 한다.

   4. 버저닝(Versioning)

오브젝트가 Write 될 때 coordinator는 버전을 새겨서 저장하고 이웃한 노드에 복제본을 보낸다. 버전은 Timestamp가 될 수도 있고 카운터가 될 수도 있는데 논문에서는 카운터를 가지고 설명하였으므로 카운터가 어떻게 동작하는지 알아보자.
버전은 [노드, 카운터] 쌍으로 저장된다. 어떤 노드가 update나 insert를 처리하면 자신의 카운터를 하나씩 증가시키는데, 예를 들어 노드 A가 오브젝트X를 저장하였다면 X[A,1] 을 버전으로 저장한다. 이 오브젝트는 노드 B와 C로 전파되어 B와 C 모두 X[A,1] 버전을 가진 오브젝트를 저장하게 된다. 이때 다른 클라이언트가 노드 A에서 이 오브젝트를 업데이트 하였다면 버전은 X[A,2]이 된다. 해당 내용이 다시 B 노드와 C노드로 전파되었다. B와 C는 동일한 key를 가진 기존 오브젝트와 수정된 오브젝트 중 버전이 더 높은 X[A,2]을 확인하고 오브젝트를 덮어쓴다. 이제 새로운 key를 가진 오브젝트Y를 노드B가 coordinator하여 쓴다고 해보자. 여기서 중요한 것은 버전은 모든 오브젝트에 걸쳐 히스토리가 누적되기 때문에 Y[B,1]버전을 갖는것이 아니라 Y[A,2][B,1] 의 버전을 갖게 된다. 따라서 오브젝트의 종류에 상관없이 write된 순서를 대부분 추적할 수 있으며 버전이 충돌되는 경우 last win의 기준을 가지고 더 뒤에 쓰여진 오브젝트를 선택하게 된다. 이것을 구문적(Syntactic) 재조정이라고 하며 대부분의 충돌은 이 방법으로 해결되어 클라이언트의 개입이 필요없게 된다.

그러나 클라이언트가 개입하여 해결해주어야 하는 경우가 있다. 위의 에에서 노드 A가 X[A,2]버전으로 업데이트 한 상황을 다시 생각해보자. 노드B와 C는 모두 X[A,2]버전을 가지고 있는데 어떤 클라이언트는 노드 B를 통해서 값을 업데이트함과 동시에 다른 클라이언트가 동일key에 대해 노드 C를 통해 업데이트를 한다고 생각해보자. B는 X[A,2][B,1]버전을 새긴 후 전파하려고 하는 반면 C는 X[A,2][C,1]을 새겨서 전파하려고 할 것이다. 동일key를 가진 오브젝트로 동시에 업데이트를 하였기 때문에 둘 중 어떤 것이 last인지 알 수가 없는 상황이 된다. 클라이언트가 노드 A를 통해 해당 key로 get하게 되면 충돌된 두 오브젝트를 모두 합친 X[A,2][B,1][C,1]을 볼 수 있도록 하며 이 중 하나를 선택할 수 있게 된다. 이 과정을 의미적(Semantic) 재조정이라고 한다. 하나를 선택하면 노드A는 자신의 노드에 카운터를 하나 증가시켜 X[A,3][B,1][C,1] 버전을 생성한 후 저장한다. 이렇게 재조정된 오브젝트를 노드 B와 C로 다시 전파하며 노드 B와 C는 자신이 갖고 있는 버전보다 높은 버전이기 때문에 의심없이 덮어쓴다.

Dynamo에 대해 설명할 때 약한 일관성을 가진 AP모델이라고 설명하였는데 희생시킨 일관성을 어플리케이션 layer에서 극복하는 것이다.

   5. Coordinator 노드의 선정

(1) Preference list
Dynamo에서 정족수 기법을 사용하여 N개의 노드에만 저장을 하고, W개의 노드에서 OK만 되면 put 요청을 완료한 것으로 간주한다고 하였다. 만약 N 노드 중 특정 노드에 장애가 생겨 서비스가 불가한 상황이 생기면 어떻게 될까? 이 문제에 대비하기 위해 항상 N개의 노드가 가용하도록 예비 노드들을 확보하고 있다. 이 리스트를 preference list 라고 한다. 즉 preference list는 특정 key에 대한 read와 write을 처리해야 할 책임이 있는 노드들의 list라고 볼 수 있는데 이 중 정상 동작중인 N개만 get, put 요청에 우선 적용된다고 볼 수 있다.

(2) Coordinator 노드
위 그림(Ring 구조)을 설명할 때 key에 대한 consistent hashing 결과를 링 위에 매핑하여 시계 방향으로 처음 돌다가 마주치는 첫 노드가 coordinator 노드라고 하였는데 사실 항상 그렇지는 않다.
클라이언트가 접근할 때는 두 가지 방식이 있다.

1) 로드 밸런서를 통해 요청을 라우팅해 노드를 선택하는 방법
2) 클라이언트 application에 내장된 library를 통해 직접 코디네이션 노드로 요청을 보내는 방법

1)의 경우는 로드 밸런서는 요청을 아무 노드로 라우팅한다. 이 요청을 받은 노드는 자신이 preference list 중 N 노드 중 하나인지를 확인하며 (뒤에서 설명하겠지만 모든 노드들은 동일한 구성정보를 공유하기 때문에 모든 key에 대한 preference list를 알고있다) 만약 자신이 N 노드 중 하나라면 자신이 coordinator 노드가 된다. 만약 N 노드 중 어떤 노드에도 속하지 않다면 preference list중 가장 위에 있는 노드에게 요청을 전달한다.

2)의 경우는 클라이언트가 내장된 library를 통해 직접 coordinator 노드를 찾아가는데 정확히 어떤 기준으로 coordinator 노드를 찾아가는 것인지는 나와있지 않다. 아마도 그림에서 설명한 것 처럼 시계방향을 따라 이동하다가 마주친 첫 번째 노드라는 것이 이 케이스에 해당하는 것 같다.

장점은 1)에 비해 네트웍 홉(hop)이 줄어들어 속도가 빠르다. 하지만 클라이언트도 노드들의 구성정보를 알고 있어야 하므로 10초에 한번씩 임의의 노드에 접속하여 멤버십 정보를 받아와야 하고 최대 10초간은 잘못된 구성정보를 인지하고 있을 가능성이 있다는 단점이 있다.

 

DynamoDB(1/2)

Dynamo 는 2007년 경 Amazon.com에서 논문을 통해 공개하면서 처음 세상에 공개되었다. 그 후 2012년 1월경 AWS에서 서비스로 오픈하였는데 DynamoDB에 대해 자세히 알아보자.
NoSQL에 대해서 알아보면 NoSQL은 크게 구글에서 만든 Bigtable 게열과 Dynamo계열이 있다고 설명한 것을 볼 수 있다. 구글의 Bigtable과 Dynamo는 어떻게 다르며 Amazon은 왜 Dynamo 라는 분산저장시스템을 개발하였을까

    1. Amazon 비즈니스 요구사항

먼저 구글의 Bigtable은 2006년 11월경에 발표되었고 Dynamo는 2007년 10월 정도에 발표되어 Dynamo가 1년 가량 늦게 발표되었다. 하지만 개발하는데 몇 년 이상 소요되었을 것이므로 서로 다른 요구사항으로 인해 독립적으로 개발되었다고 볼 수 있다. Dynamo는 어떤 비즈니스 요구사항으로 인해 탄생하였을까? 논문을 읽어보면 자세히 나와있다.

 (1) 간단한 쿼리 모델

 Dynamo는 get(key) 동작과 put(key,object)동작만을 지원하는데 Amazon.com에서 서비스하는 대부분이 Unique한 key만으로 조회하거나 쓰면 된다는 것이다. 여러 key를 동시에 조회해야 할 만큼 복잡한 쿼리도 필요없으며 관계형 DB가 필요하지도 않기 때문에 단순하지만 빠른 동작을 지원하는 아키텍처를 필요로 하게 되었다는 뜻이다. 오브젝트의 사이즈도 1MB 이내의 상대적으로 작은 경우에 적합하다. (구글의 Bigtable의 경우 큰 사이즈의 파일을 chunk라는 단위로 나누어 노드에 저장하도록 하고 있으며 상대적으로 큰 사이즈의 오브젝트를 저장할 때 유리함)

(2) 가용성을 살린 ACID(Atomicity, Consistency, Isolation, Durability)

RDBMS의 특징으로 항상 거론되는 ACID의 단점 중 하나는 가용성이 떨어진다는 것이다. 물론 RDBMS에서도 샤딩으로 가용성을 높일 수는 있지만 구성이 복잡하며 노드를 쉽게 추가하거나 제거할 수도 없다. 오라클의 RAC도 마찬가지..
Dynamo는 일관성인 C를 어느정도 포기하는 대신 가용성을 살린 ACID모델이라 할 수 있다.

분산모델에서 흔히 CAP모델을 사용하는데 보통 CAP에 대해 얘기하길 C(일관성)과 A(가용성)과 P(네트웍 분단허용)중 2개만 고를 수 있다고 설명한다. 옳은 설명이지만 언뜻 이해가 잘 가질 않는다.
이렇게 생각하면 더 편할 듯 하다.

” 네트웍 분단이 발생하면 일관성과 가용성 중 하나만 선택할 수 있다”

물론 하나만 선택할 수 있다고 해서 나머지 하나는 꼭 버려야 한다는 것은 아니며 둘 중에 하나는 약간 포기해야 한다 정도로 이해하면 되겠다.
DynamoDB는 AP를 선택한 모델이라고 볼 수 있다. 참고로 구글의 Bigtable은 일관성을 선택한 CP모델에 가까운데 아마존은 왜 AP모델을 선택했을까?
Amazon.com의 특징상 시스템이 잠시라도 멈추면 peak 시간대의 고객이 이탈하게 되고 금전적 손실이 많아진다는 사업모델의 특징 때문이다.
즉 일관성이 손상되는 한이 있더라도 항상 시스템이 가용하도록 하겠다는 것인데, 일관성은 필요 없다는 것인가? 아니면 극복할 방안을 마련하는 것일까?
결론부터 말하자면 필요시 어플리케이션(클라이언트)의 개입에 의해 일관성을 맞출 수 있는 방법으로 일관성 충돌을 극복한다.

    2. Dynamo의 디자인

Amazon 비즈니스에서 필요로 하는 데이터 저장소의 요건은 위에서 대략 살펴보았으며 이제 Dynamo의 디자인 원칙에 대해 알아보자.

(1) 확장성(Scalability)

모든 NoSQL이 갖추고 있는 특징이기도 하지만 Dynamo도 확장성을 전제조건으로 한다. 시스템에 노드가 추가되거나 제거될 때 시스템에 주는 영향을 최소화해야 한다.

(2) 대칭성(Decentralization)

구글의 Bigtable과 가장 차이나는 부분 중 하나인데, 마스터 노드를 두는 Bigtable 과는 달리 Dynamo는 마스터 노드가 없으며 모든 노드가 동등한 역할을 한다. 아마존의 경험상 이런 대칭성이 시스템 프로비저닝과 유지보수을 더 쉽게 할 수 있다고 한다.

(3) 비집중화(Decentralization)

위에서 설명한 대칭성과 같은 의미인데, 읽기나 쓰기 요청을 수행하는 마스터노드의 개입이 없이 클라이언트와 노드가 직접 통신하는 구조의 Peer to Peer 를 사용한다는 것이다. 클라이언트가 노드를 찾아가는 방식에는 두 가지 방법이 있는데 로드밸런서가 클라이언트의 요청을 load balancing하는 방법과 클라이언트가 어플리케이션 로직을 통해 직접 노드를 찾아가는 방식이 있다. 후자의 방법이 네트웍 Hop이 적기 때문에 더 빠르다고 한다.

(4) 이질성(Heterogeneity)

이질성 단어만 보면 무슨 의미인지는 좀 헷갈리는데
Dynamo는 노드들의 성능에 따라 Load의 분배가 이루어진다. 더 좋은 성능의 Node가 추가되면 해당 노드가 로드를 더 받는다는 것이다. 노드의 성능을 업그레이드 할 때 전체를 Down시키고 업그레이드 하는 것보다 하나씩 추가하는 방법은 가용성 측면에서 필수 불가결한 방식이다.

 (5) 항상 쓰기 가능

네트웍 단절이나 노드 장애가 발생한 상황이거나 동시쓰기가 들어오더라도 쓰기 요청은 하나라도 소실되지 않고 모두 기록된다. 이것이 Dynamo가 일관성을 약간 희생시킨 부분인데 동시 쓰기로 인해 충돌이 발생한 경우 충돌난 값을 모두 보여주어 클라이언트가 개입, 하나를 선택하도록 한다.

다음 글에서는 위에서 설명한 Dynamo의 디자인 특징을 구체적으로 어떻게 구현하였는지 자세히 알아보도록 한다.