카산드라 DB 톺아보기 (Cassandra DB)
May 5, 2023
Table Of Content
Cassandra DB는 분산형 NoSQL 데이터베이스로, 성능, 확장성, 고가용성의 요구사항을 해결하기 위해
을 이용한 데이터베이스입니다. 아래에서는 어떤 구조로 이러한 기능을 제공하는지 알아보겠습니다.
카산드라(Cassandra)의 Commit Log는 데이터의 내구성(Durability)을 보장하기 위한 핵심적인 역할을 합니다.
카산드라는 데이터의 내구성을 위해 Write-Ahead Logging(WAL) 방식을 사용합니다. 이 방식은 데이터 변경 작업이 발생할 때마다 변경 내용을 memtable에 쓰기 전 Commit Log를 기록합니다. 이렇게 기록된 로그는 주기적으로 디스크에 플러시됩니다.
Commit Log는 Cassandra에서의 데이터 복구 과정에서 핵심적인 역할을 합니다. 만약 노드나 디스크가 장애로 인해 다운되었다면, 해당 노드의 데이터를 복구하기 위해 Commit Log를 사용합니다. 장애가 발생한 노드에서 마지막으로 플러시된 Commit Log부터 장애가 발생한 시점까지의 로그를 읽어서 데이터를 복구합니다.
Memtable은 in-memory 자료구조로 키-값 쌍으로 데이터를 저장하는 테이블입니다. 메모리 기반의 임시 저장소를 사용하기 때문에 디스크보다 훨씬 빠른 액세스 속도를 가지고 있습니다. Memtable은 또한 검색 작업에서도 매우 빠른 응답 속도를 제공합니다. 하지만, 메모리에 저장되기 때문에 제한된 용량을 가지고 있으며, 데이터가 디스크에 플러시되기 전에 노드가 다운될 경우 데이터의 일부 또는 전체가 손실될 수 있습니다.
SSTable는 Sorted String Table의 약자로, 정렬된 문자열 테이블을 의미합니다. SSTable은 카산드라에서 Memtable에서 디스크로 플러쉬된 뒤 데이터를 영구적으로 저장하는데 사용되는 파일 기반의 데이터 구조입니다. 키가 정렬되어 있기 때문에 키를 이진 검색(binary search)할 수 있어 빠른 읽기 작업이 가능해집니다.
카산드라는 블록 기반의 SSTable 압축을 지원합니다. 이를 통해 SSTable의 크기를 줄이고, 중복된 데이터를 제거합니다. 압축(Compaction)은 여러 SSTable 파일을 병합하여 하나의 SSTable 파일로 만드는 과정입니다. 이를 통해 디스크 사용 공간을 최적화하고, 읽기 성능을 향상시킵니다.
이미지 출처: Datastax
카산드라는 모든 데이터를 Hash Function으로 파티셔닝하여 여러 노드에 분산 저장할 수 있게 합니다. 분산 저장을 통해 데이터가 많아지더라도 노드나 서버의 수를 늘리는 수평적 확장을 할 수 있고, 서버를 클라이언트의 가용 영역에 가까이 위치시킬 수 있기 때문에 가용성이 올라가는 효과를 얻게 됩니다.
올바른 파티셔닝을 위해서는 좋은 Hash function이 필요합니다. Hash function이 제대로 작동하지 않는다면 특정 Partition에 데이터가 몰리게 되고, 이는 데이터의 불균형을 초래하게 됩니다. 이를 해결하기 위해 카산드라는 Consistent Hashing을 이용합니다.
가장 단순한 Hash Function은 보통 모듈러 연산(노드 개수로 나눠 나머지를 구하는 방식)을 이용합니다.
노드 A, B, C 3개일 때,
해시 값이 10인 데이터는 10 % 3 = 1이므로 노드 B에
해시 값이 21인 데이터는 21 % 3 = 0이므로 노드 A에
해시 값이 12인 데이터는 12 % 3 = 0이므로 노드 A에
저장됩니다.
하지만 이럴 경우 노드가 추가되거나 삭제될 때마다 대부분의 데이터를 다시 파티셔닝해야 합니다. 노드가 A, B, C, D 4개로 늘어났을 때, 해시 값이 10인 데이터는 10 % 4 = 2이므로 노드 C에 해시 값이 21인 데이터는 21 % 4 = 1이므로 노드 B로 (해시 값이 12인 데이터는 12 % 4 = 0이므로 노드 A에 유지) 모두 이동하게 되어 이는 많은 비용을 초래하게 됩니다.
카산드라는 Consistent hashing을 이용하여 이를 해결합니다. Consistent hashing 알고리즘은 각 노드(Node)에 대해 해시 함수를 사용하여 고유한 토큰(Token) 값을 생성하고, 이를 원형 형태의 토큰 링(Token Ring)에 배치합니다.
예를 들어, 아래 그림은 Token ring을 이용하여 Consistent hashing 알고리즘을 구현한 예시입니다.
위의 그림에서, 각 노드는 고유한 토큰(Token) 값을 가지고 있으며, 이를 원형 형태의 노드링(Node Ring) 상에 배치합니다. 데이터의 키(Key) 값에 대한 해시 함수를 사용하여 해시된 값이 생성됩니다. 이 해시된 값은 원형 형태의 노드링(Node Ring) 상에서 위치를 결정합니다. 예를 들어, 위의 예시의 데이터는 다음과 같이 저장될 수 있습니다.
노드의 추가 또는 삭제가 발생할 경우, 해당 노드의 토큰(Token) 값을 변경하여 새로운 토큰 값에 따라 노드를 노드링(Token Ring) 상에 배치합니다. 이때, 노드의 추가 및 삭제 작업은 원형 형태의 노드링(Token Ring) 상에서 진행되므로, 기존의 데이터 분산 상태를 유지하면서 노드를 추가 또는 삭제할 수 있습니다.
많은 노드들이 있을 경우에는 각 노드에 하나의 토큰 값을 배정하여 Consistent hashing을 사용해도 데이터의 분산이 잘 이루어지지만, 노드의 수가 적을 경우에는 데이터의 분산이 잘 이루어지지 않습니다.
몇 개의 복제 서버가 더 추가된다고 했을 때 후자의 경우에는 각 노드별로 같은 양의 데이터가 배정되게 하는 것이 쉽지 않습니다.
이러한 문제를 해결하기 위해 Dynamo에서는 가상 노드(Virtual Node)를 사용합니다. 가상 노드는 하나의 노드에 여러개의 토큰을 할당하여 새로운 노드들이 추가될 때 각 노드에서 배정된 토큰을 옮겨주는 방식으로 데이터의 분산을 유지해줄 수 있습니다.
Amazon의 Dynamo는 분산형 키-값 저장소로, 데이터를 여러 노드에 복제하여 고가용성을 보장합니다. 복제 시에 모든 서버가 클라이언트로부터 쓰기를 직접 받을 수 있게 하는 리더 없는 복제 방식을 사용하고 있습니다. 카산드라 역시 Dynamo의 복제 방식을 참고하여 리더 없는 복제 방식을 이용합니다.
RF(Replication Factor)는 하나의 데이터가 얼마나 많은 복제 서버에 저장될지를 나타내는 값입니다. 예를 들어, RF가 3이라면 하나의 데이터는 3개의 복제 서버에 저장됩니다.
카산드라의 경우 모든 복제 서버에서 쓰기 요청을 받을 수 있게 하기 위해 모든 키를 버전 정보를 추가하여 기록합니다. 버전을 통해 읽기 요청 시에 변경사항을 발견할 수 있고, 새로운 버전의 변경 사항을 다른 서버에 알릴 수 있습니다. 이때 여러 버전의 변경 사항이 있을 경우, Last-Write-Wins 방식으로 가장 최신의 요청만 적용합니다. 이를 위해 카산드라의 모든 쓰기, 삭제, 업데이트 요청은 시각(timestamp)이 기록되어 어떤 것이 가장 최신인지 판단할 수 있습니다.
최신의 값을 정확히 판단하기 위해 정확한 시각을 유지하는 것이 정말 중요합니다. 카산드라의 경우, NTP(Network Time Protocol)를 이용하여 모든 노드의 시각을 동기화할 것을 권장합니다.
카산드라는 데이터의 일관성을 위해 Read Repair와 Hinted Handoff를 사용합니다. Read Repair는 읽기 요청 시에 데이터의 불일치를 발견하면, 데이터를 갱신하여 일치하게 만드는 방식입니다. Hinted Handoff는 데이터를 쓰기 요청 시에 복제 서버가 다운되어 있으면, 다운된 서버가 다시 온라인이 되었을 때 데이터를 복제하는 방식입니다.
Dynamo는 일관성과 가용성과의 트레이드오프를 조정하기 위해 Consistency Levels를 사용합니다.
Consistency Levels은 일반적으로 쓰기 작업에서 사용됩니다. Consistency Levels은 크게 5가지로 구분됩니다.
-
ANY
ANY Consistency Level은 쓰기 작업에서 가장 낮은 일관성 수준으로, 데이터를 쓰는 하나의 노드에서 성공적으로 쓰여졌거나 쓰기가 operator에 전송되었을 때 응답을 반환합니다. 이 경우, 데이터가 모든 노드에 복제되지 않을 수 있으며, 읽기 작업에서 데이터가 일관성 없이 반환될 가능성이 있습니다. -
ONE
ONE Consistency Level은 쓰기 작업에서 노드 중 하나에 데이터가 성공적으로 쓰여졌을 때 응답을 반환합니다. -
TWO
TWO Consistency Level은 데이터를 쓰는 노드와 쓰기 작업 후에 복제되는 노드 중 두 개 이상의 노드에 데이터가 성공적으로 쓰여졌을 때 응답을 반환합니다. -
THREE
THREE Consistency Level은 데이터를 쓰는 노드와 쓰기 작업 후에 복제되는 노드 중 세 개 이상의 노드에 데이터가 성공적으로 쓰여졌을 때 응답을 반환합니다. -
QUORUM
QUORUM Consistency Level은 쓰기 작업에서 모든 노드의 과반수 이상(n/2 + 1)의 노드에 데이터가 성공적으로 쓰여졌을 때 응답을 반환합니다.
카산드라에서 쓰기 작업은 기본적으로 QUORUM
Consistency Level을 사용합니다.
쓰기 작업의 경우 Consistency Level과 상관없이 모든 복제 서버에 전송됩니다.
다만 Consistency Level에 따라 성공적으로 쓰여졌다고 응답한 노드의 수가 어느 정도에 도달했을 때 쓰기가 성공했다고
클라이언트에 알려주는지가 달라집니다.
다중 데이터 센터 환경에서는 LOCAL_QUORUM
이 사용될 수 있습니다.
LOCAL_QUORUM Consistency Level은 쓰기 작업에서 단일 데이터 센터 내의
과반수 이상(n/2 + 1)의 노드에 데이터가 성공적으로 쓰여졌을 때 응답을 반환합니다.
Gossip 프로토콜은 복제된 노드끼리 서로의 상태 정보를 주기적으로 교환하는 것으로 작동합니다.
예를 들어, 새로운 노드가 클러스터에 추가되면 Gossip 프로토콜을 통해 다른 노드에 대한 정보를 수집하고 클러스터의 모든 노드에 대한 정보를 교환합니다. 이러한 정보에는 노드의 상태, 역할 및 위치 정보가 포함됩니다.
Gossip 프로토콜에서는 각 노드는 다른 노드와의 상태 정보를 교환하며,
이러한 정보는 (generation, version)
튜플의 벡터 시계로 버전 관리됩니다.
이 문장에서 "generation"은 단조증가 시간 스탬프를 의미하며, "version"은 논리적 시계를 말합니다.
이 시계는 각 노드가 가진 정보의 버전을 추적하며, 이전 버전의 정보는 무시됩니다.
복제된 서버는 각각 독립적으로 1초마다 Gossip 프로토콜을 실행합니다.
- 자신의 heartbeat 상태 정보를 업데이트하고 클러스터 상태 정보를 구성합니다.
- 클러스터 내에서 다른 노드를 무작위로 선택하여 상태 정보를 교환합니다.
- 접근 불가능한 노드가 있는 경우, 해당 노드에 대한 상태 정보를 교환합니다.
- 2단계에서 상대 노드를 선택하지 못한 경우, seed node에 대한 상태 정보를 교환합니다.
새로운 Cassandra 클러스터를 생성할 때, seed node를 특정 노드로 지정해야 합니다. 클러스터가 생성되면, seed node는 상태 정보를 교환할 hotspots 역할을 합니다.
비-seed node는 클러스터에 진입하기 위해 적어도 하나의 seed node에 연결되어야 합니다. 이를 위해 보통 다수의 seed node가 사용되며, 한 데이터센터나 랙에 하나씩 지정하는 것이 일반적입니다.
또한, gossip task는 토큰 메타데이터와 스키마 버전 정보도 전파합니다. 이 정보는 데이터 이동과 스키마 동기화에 사용되며, 노드 간의 데이터 소유권을 알려주는 역할도 합니다. 이때, 노드에서 스키마 버전 불일치를 감지하면 스키마 동기화 작업을 예약합니다.
gossip task로 노드의 상태 정보를 전파하면서, 해당 노드에서 읽기 적용을 할 수 있는지 여부도 전파합니다. 이를 통해 오퍼레이터가 노드의 상태를 확인하고, 작동하지 않는 노드를 다시 배포하거나 제거할 수 있습니다.
Cassandra는 분산 환경에서 데이터를 저장하고, 읽고, 쓰는 데에 최적화된 NoSQL 데이터베이스입니다. LWW(Last Write Wins), Consistence Levels, LSM Tree 구조를 사용해서 데이터를 쓰는게 많은 서비스에 적합합니다.