쿼카러버의 기술 블로그
[NoSQL] Cassandra & Scylla DB 시리즈 - 1탄 : Cassandra 기본 개념 쉬운데다 읽기 편하게 써봄 본문
[NoSQL] Cassandra & Scylla DB 시리즈 - 1탄 : Cassandra 기본 개념 쉬운데다 읽기 편하게 써봄
quokkalover 2023. 9. 9. 15:37
ScyllaDB는 세계에서 가장 빠른 NoSQL 데이터베이스라고 소개될 만큼 높은 성능을 자랑한다. SycllaDB에 대해 한 줄 요약하자면 C++로 구현된 Cassandra DB다. 실제로 Apache Cassandra와 완벽하게 호환되는 대체품으로 카산드라의 커맨드와 메서드를 그대로 사용할 수 있다. Cassandra의 아키텍처를 기반으로 하되 C++로 재작성되어 성능 최적화에 신경을 쓴 제품이기 때문이다.
ScyllaDB가 자랑하는 장점들을 요약해보면 다음과 같다.
- Cassandra에 비해 5배 빠른 성능 : lower latency, higher throughput
- Cassandra에 비해 1/10정도의 규모의 클러스터로 동일 성능을 낼 수 있음
- GC로 인한 Stop the world 현상 없음 : C++로 구현했기 때문
- CQL호환 가능 (Cassandra Query Language)
사실 나는 Cassandra DB를 들어보기만 했지 직접 써볼 기회가 없었다. 근데 ScyllaDB를 접했을 때 자신들을 Cassandra 대체품이라고 홍보하고 실제로 호환성을 보장할만큼 정말 많은 기업들이 Cassandra DB를 사용한다는 것을 알고나니 호기심이 생겨서 공부를 시작했다. Cassandra의 어떤 장점 때문에 많은 기업들이 사용해왔고, ScyllaDB는 도대체 어떤 마법을 부렸길래 이렇게 높은 성능을 이끌어낼 수 있는지 알아보고 싶었다. 당연히 개발 언어를 Java에서 C++로 바꾼 것 만으로는 이런 성능 개선을 이뤄내진 못했을테니 이 주제에 대해 공부해보면 많은 것들을 배울 수 있을 것 같다는 직감이 생겼다.
따라서 본 글은 이 글의 본론인 “왜 ScyllaDB는 빠른가?”
에 대해 바로 이야기 하기 전에 먼 길을 돌고 돌아 다음의 순서로 설명해보려고 한다.
(1) Cassandra에 대한 간단한 탐구 <- 이번 글
(2) Discord의 카산드라 이용 사례 & SycllaDB로의 마이그레이션
(3) SycllaDB 소개 : ScyllaDB가 빠른 이유
(4) 부록 : (글 반응이 괜찮으면 블록체인과의 연결고리, consistent hashing, elasticsearch 등등 핵심개념들 좀 더 다룰수도..)
ScyllaDB를 보기 전에 Cassandra부터 보는 이유는, ScyllaDB의 성능 향상에 대한 이해를 하기 위해서는 먼저 Cassandra가 어떻게 동작하는지 알아두어야 하기 때문이다. C++버전인 ScyllaDB가 기본적으로 Cassandra 아키텍처에서 어떤 점을 개선했는지를 이해해야 본 글의 목적을 달성한다고 볼 수 있다.
자 그럼 시작해보자. 이번 글은 유명한 Cassandra DB에 대해서 다뤄본다.
참고로 이 글은 NHN에서 다뤘던 글을 많이 참고해서 내가 좀 더 읽기 편하게 재구성했다. https://meetup.nhncloud.com/posts/58
카산드라의 아키텍처
카산드라를 포함한 많은 NoSQL 데이터베이스들은 하드웨어나 소프트웨어의 장애가 언제든 발생할 수 있다는 것을 전제로 설계됐다. 그 중에 카산드라는 Masterless
아키택처를 선택했다.
사실 카산드라 DB의 특징은 Masterless
아키텍처의 특징이라고 해도 과언이 아니다. 노드간의 통신과 정보교환은 gossip 프로토콜을 통한 peer-to-peer
방식으로 이루어진다. 이는 전통적인 master-slave
구조와는 대조적으로, 모든 노드가 동등한 역할을 하는 분산 시스템을 구성한다.
이렇게 Master 노드가 따로 없고 모든 노드가 동등하다는 것은 모든 노드에 데이터를 읽고 쓸 수 있다는 점을 의미한다. 이러한 특징 덕분에 카산드라는 쉽게 확장할 수 있는 특징(Scallable Architecture
)을 가지고 있다. 특정 노드에 의존하지 않기 때문에 노드를 추가하거나 제거하는게 더 간단하기 때문이다. 또한 마스터-슬레이브 구조와는 다르게 개별 노드의 실패가 전체 시스템에 큰 영향을 주지 않는다. (No Single point of failure
)
카산드라의 특징
위 아키텍처에서 설명한 Scallable
하고 Single point of failure
가 없다는 점과 더불어 아래와 같은 특징을 갖고 있다.
(1) Linear Scale Performance
Linear Scale Performance란 시스템에 노드를 추가할 때마다 성능이 선형적으로 증가한다는 의미다. 노드를 한 대 추가하면 동등한 권한을 가진 노드가 추가되기때문에 즉시 읽기 및 쓰기 작업을 수행할 수 있다.
(2) Fault Detection and Recovery
노드들은 Gossip 프로토콜을 통해 서로 클러스터와 관련된 정보를 주고받기 때문에 노드의 장애 감지에도 매우 용이하다. 주기적인 Gossip 메시지를 수신하지 않는 경우, 해당 노드가 다운되었거나 네트워크 문제가 발생했다고 판단하고 복제된 데이터 활용, Hinted Handoff, 클러스터 내 다른 노드들로부터 복구 및 재분배 등 조치를 수행한다.
(3) Data Protection
카산드라는 데이터가 노드에 쓰여질 때 디스크에 직접 저장되기 전에 Commit Log에 데이터를 먼저 쓴다. 이는 일종의 데이터 보호망 역할을해 만약 노드에 문제가 발생해도 최근에 쓰여진 데이터를 복구할 수 있다.
얘기가 나온김에 데이터 저장 방식을 간단하게 요약하면 Commit Log에 데이터가 쌓이고 나면 Memtable이라는 메모리 자료구조로 데이터가 옮겨지고, 이 Memtable이 일정 수준의 사이즈가 되거나 일정 시간이 지나고 나면 SSTable이라는 on-disk storage structure의 형태로 디스크에 flush된다.
카산드라의 데이터 구조
카산드라의 데이터 구조는 v1.2에 들어서 Keyspace
> Table
> Row
> Column(Column Name + Column Value)(KV)
의 포함관계로 이어지는 데이터 구조를 가지고 있다. 우리에게 익숙한 RDBMS의 DB
-Table
-Row
-Column
의 형태와 유사한 구조다.
카산드라는 이러한 특징적인 데이터 구조 때문에 Wide Column Store
로 분류된다. (columnar store
, column-oriented store
와는 살짝 다름 주의)
Wide Column Store
는 한 테이블 내의 모든 row들이 모두 같은 column을 가지는 관계형 데이터베이스와 비슷하지만 약간 다르다. 각 row들이 다른 column을 가질 수 있도록 허용하기 때문이다. 뿐만 아니라 partition key에 묶여있는 모든 column들을 특정 노드에 파티셔닝하는 방식으로 저장되기 때문에 데이터 파티셔닝에 용이하다.
예를 들어 partition key가 user_id인 row 중 user_id가 1인 특정 row를 조회하려고 한다면 카산드라는 어느 노드를 통해 데이터를 가져오면 되는지 바로 찾을 수 있다.
참고로 wide-column
이라는 명칭이 붙여진건 위 그림처럼 각각의 row가 다른 column을 가질 수 있어서다.
카산드라의 데이터 분산 방식
카산드라의 분산시스템은 기본적으로 Ring 구조를 띠고 있다. 그리고 Ring을 구성하는 각 노드에 Data를 분산하여 저장한다. 이렇게 말하면 처음 듣는사람은 이게 뭔가 싶을텐데, 좀만 참고 계속 들어보자.
(1) 데이터 분산 기준 : Partition Key (Row Key)
Partition Key(Row Key) 데이터의 hash값을 기준으로 데이터를 분산한다
(2) Ring에 참여하는 방식 : 해슁
노드가 Cluster에 참여하게 되면 Cassandra의 conf/cassandra.yaml에 정의된 설정을 통해 노드마다 고유의 Hash 값 범위를 부여받는다. 그리고 외부에서 요청이 오게되면 해당 데이터의 partition Key(Row Key)의 Hash값을 계산하기만 하면 해당 데이터가 어느 노드에 저장되어 있는지 알 수 있다.
Virtual Node
앞서 Ring에 참여하는 방식이 해슁(Consistent-Hashing)이라고 했는데, 이를 이해하는데는 Virtual Node
개념이 핵심이다.
분산 시스템 하에서 노드 수가 늘어날 때 hash값을 어떻게 계산하느냐
이 개념을 이해하려면 virtual node
를 이해해야 하기 때문이다.
사실 Cassandra의 초창기에는 직접 수작업이나 스크립트를 이용해 Hash 값의 범위를 구하여 Cassandra의 각 노드별 conf/cassandra.yaml에 "initial_token"이라는 옵션에 해당 Hash 값을 지정해주어야 했다고 한다. 이때 만약 특정 노드를 추가, 제거해야 할 때는 특정 노드에 데이터가 몰리지 않도록 수작업으로 initial token을 다시 계산하여 conf/cassandra.yaml을 갱신한 뒤 Cassandra를 구동했다고 한다.
하지만 이제는 virtual node를 사용해서 이런 방식이 아니라 알아서 gossip protocol을 통해 의논해 token(hash 값)의 범위를 결정하고 리밸런싱하게 된다.
다시 돌아와서 virtual node가 뭐냐면 하나의 실제 Cassandra 노드 안에 가상 노드를 여러 대 두고, 아주 잘게 나누어진 token 범위를 가상노드들에게 할당하여 데이터를 분산하는 개념이다.위 그림을 보면 "Ring without virtual node"와 "Ring with virtual node"그림을 비교해보면 Node가 담당하고 있는 node의 갯수가 좀 더 자잘하게 쪼개지고 더 작은 단위의 범위를 맡고 있다는 것을 볼 수 있다. virtual node의 이점은 이렇게 다수의 가상 노드들을 통하여 좀 더 데이터를 균일하게 분산하기 쉽고, 데이터가 이미 여러 대의 노드에 분산되어 있으므로 노드의 추가, 제거시 데이터의 이동, 복제, 리밸런싱에 높은 성능 향상을 가져다 준다.
conf/cassandra.yaml에서 "num_tokens" 항목이 바로 한대의 node에 몇 대의 virtual node를 둘 것인지를 정하는 부분이다.
그리고 위 그림만 보면 Node들이 중복된 range를 가지고 있는 것처럼 보일 수 있는데, 각 노드는 parition key를 해슁해서 나온 token 값의 범위를 담당하고 있지만 replication factor에 따라 다른 node들의 데이터의 복제본도 가지고 있게 된다. 예를 들어 replication factor가 3이면, range E는 node 5, 6, 1가 가지고 있게 된다. 그리고 그림을 보면 각 노드가 하나 이상의 연속된 range를 가지고 있지 않고 있다.
참고로 virtual node개념을 잘 이해하려면 사실 Consistent Hashing이라는 개념을 이해하면 된다. 추후에 Consistent Hashing의 개념과 구현을 간단하게 다루는 포스팅도 한번 써보도록 하겠다.
카산드라의 Read / Write아키텍처
Bloom filter, commit log, mem table, ssl table 등 어떻게 데이터를 읽고 쓸 때 여러 컴포넌트를 통해 성능을 보완하는 등 내용은 본 글에 본 목적이 아님. 특징적인 내용만 두개 언급해보면 아래와 같다.
(1) Cassandra는 Delete를 바로 수행하지 않는다. 모든 데이터에는 Tombstone이라는 marker가 존재하며, 특정 데이터의 Delete 요청이 일어날 경우 이 Tombstone에 마킹을 한 뒤에 주기적인 Garbage Collection이나 SSTable의 Compaction 등의 이벤트가 발생 할 때 비로소 데이터를 정말로 삭제한다.
(2) Cassandra의 Update는 내부적으로 Delete/Write로 구현되어있다. Elasticsearch와 비슷하다. 앞서 말했듯이 데이터가 저장되어 있는 SSTable은 immutable하므로 Delete를 통해 Tombstone에 마킹을 하게 되고, Update 해야 할 새로운 데이터는 다른 곳에 쓰여지게 된다.
데이터 저장 예시
엄청 간단한 예시로 한번 데이터를 직접 저장하고 조회해보자
CREATE KEYSPACE your_keyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
USE your_keyspace;
우선 테이블은 다음 스키마로 정의한다. (Discord 블로그 글에서 가져옴)
CREATE TABLE messages (
channel_id bigint,
bucket int,
message_id bigint,
author_id bigint,
content text,
PRIMARY KEY ((channel_id, bucket), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
데이터를 입력해보자.
INSERT INTO your_keyspace.messages (channel_id, bucket, message_id, author_id, content) VALUES (1234567890, 1, 101, 1111, 'Hello from Seoul');
INSERT INTO your_keyspace.messages (channel_id, bucket, message_id, author_id, content) VALUES (1234567890, 1, 102, 2222, 'Hi from Gangnam');
INSERT INTO your_keyspace.messages (channel_id, bucket, message_id, author_id, content) VALUES (1234567891, 1, 103, 3333, 'Greetings from Seongnam');
INSERT INTO your_keyspace.messages (channel_id, bucket, message_id, author_id, content) VALUES (1234567891, 1, 104, 4444, 'Hey there from Pangyo');
INSERT INTO your_keyspace.messages (channel_id, bucket, message_id, author_id, content) VALUES (1234567892, 2, 105, 5555, 'Hello from Jungja');
그리고 Select 쿼리를 실행해보면
SELECT * FROM your_keyspace.messages;
위와 같이 5개의 row와 5개의 column으로 이루어진 데이터들이 출력된다.
근데 사실 Cassandra는 tabular하게 데이터를 저장하는게 아니라 RowKey를 기준으로 데이터를 저장한다고 했으니, 어떤식으로 저장되는지를 좀 더 직관적으로 보기 위해서는 bin/cassandra-cli로 실행해보면 된다. (참고로 version3.x부터는 cassandra-cli를 사용하지 않는다.
use your_keyspace;
list messages;
결과 값 예시 (cassandra-cli)
- 이해를 위해 chatGPT로 생성한거임. (M1에선 안돌아감..)
Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: 1234567890:1
=> (name=1:author_id, value=1111, timestamp=1452481808814357)
=> (name=1:content, value=Hello from Seoul, timestamp=1452481808814358)
=> (name=1:author_id, value=2222, timestamp=1452481808817685)
=> (name=1:content, value=Hi from Gangnam, timestamp=1452481808817686)
-------------------
RowKey: 1234567891:1
=> (name=1:author_id, value=3333, timestamp=1452481808823138)
=> (name=1:content, value=Greetings from Seongnam, timestamp=1452481808823139)
=> (name=1:author_id, value=4444, timestamp=1452481808829752)
=> (name=1:content, value=Hey there from Pangyo, timestamp=1452481808829753)
-------------------
RowKey: 1234567892:2
=> (name=2:author_id, value=5555, timestamp=1452481808833645)
=> (name=2:content, value=Hello from Jungja, timestamp=1452481808833646)
3 Rows Returned.
Elapsed time: 80 ms.
즉 각 데이터에서 Partition KEy로 지정된 Column들의 Value가 Row Key가 되고, 이 Row Key를 해슁해서 계산된 값(token)을 가지고 이 값의 범위를 가진 Node를 찾아서 CRUD를 진행할 수 있는 것이다.
무튼 카산드라가 데이터를 저장하는 과정을 요약하면 이렇다.
(1) Cassandra는 Row Key의 해시 값을 이용해 데이터를 분산하다.
(2) Cassandra Data Layer의 Row key는 partition key의 값이다. 만약 복수의 partition key가 있다면, 해당 Column 값들과 ":" 문자를 조합한다.
(3) Cassandra Data Layer의 Column Name은 CQL cluster key의 Column 값과 primary key에 속하지 않은 Column Name들, 그리고 ":" 문자를 조합한다.
Cassandra 사용 사례
카산드라는 정말 많은 곳에서 활용되고 있다. 그 중에서 구글링을 해보면, 아키텍처상 적합하지 않은 use case를 소개하고 있는 경우가 많아서 그런 케이스들은 생략하고 내가 공감이 되는 use case들만 몇 개 가져와봤다.
1) time series : 시계열 데이터를 다루는 경우.
예: 메시지(보낸 순으로 정리)
기본적인 접근법은 고유 식별자와 시간 버킷의 조합을 사용한다.
- 고유 식별자 : 채널 ID, 장치 ID등 고유 식별자
- 시간 버킷(bucket) : 파티션이 너무 커지는 것을 방지하기 위해 데이터를 데이터 양과 쿼리 패턴에 따라 시간 간격(예: 시간별, 일별, 월별 버킷)으로 분류
참고로 나중에 다루겠지만 위와 같은 접근법은 Discord의 사용방식이랑 같다.
2) E-commerce
카산드라는 데이터 모델 설계 자체가, product ID, 유저 등 특정 한 entity 중심으로 나누어서 데이터를 저장하는데 적합하고,
아키텍처 상으로 쓰기 중심적(write-intensive)인데다가, 선형 확장성을 보장하기 때문에, e-commerce에서도 잘 활용될 수 있다. 제품 카탈로그, 추천, 개인화 엔진 등이 필요한 e-commerce에 매우 적합하다고 할 수 있다. 물론 결제 처리 같이 Transaction 특성이 모두 보장돼야 하는 경우에는 사용하면 안된다.
먼저 cassandra의 특징 중 e-commerce에 적합한 특징들을 먼저 나열해보자.
(1) Resilient with zero downtime:
다중 지역에 replicate 하기가 매우 용이하고, 그 과정에서 downtime이 없다. 그래서 특정 전체 지역의 loss도 전체 클러스터가 다운되지는 않는다고 한다. 실제로 다른 나라에 위치한 노드간에도 이후 동기화를 맞출 수 있다.
(2) Highly responsive:
Cassandra의 P2P구조는 전 세계 다양한지역에 위치시킬 수 있도록 해주기 때문에, 특정 고객에게 더 가까이 데이터를 위치시켜 높은 반응성을 가지고 빠르게 동작하도록 할 수 있다. 예를 들어 유럽에 살고 있는 고객은 유럽에 위치한 노드와 통신할 수 있기 대문에 response time이 훨씬 빠를 것이다.
(3) Predictable scalability
cassndra는 liner scalability의 특징을 가지기 때문에 확장이 매우 직관적이고, 예측가능하며 비용 효율적이다.
위 내용으로 e-commerce에 적용해 생각해보면
(1) multi-region
e-commerce인데 여러 국가들의 트래픽을 같이 받아야 하는 경우 카산드라 DB를 사용하면 아키텍처상으로 multi-region관리가 매우 용이한 특징이 있다고 한다. 특정 고객의 장바구니 데이터를 관리한다고 했을 때, eventual consistency의 한계 때문에 미국에서 장바구니 데이터를 수정하고, 한국에서 볼 때는 일관적인 데이터가 리턴되지 않을 수 있지만, 미국 안에서는 local_quorum을 사용해서, 그 안에서 만큼은 빠른 response를 가져갈 수 있게끔 설정할 수 있다.
(2) 추천
카산드라는 특정 기준 또는 특성에 따라 그룹화된 방문자들의 활동을 같은 노드에 저장할 수 있다. 예를 들어 20대 남성, 30대 여성같이 연령대와 성별에 따라 고객들을 그룹화할 수 있기도 하고, 구매 패턴, 관심사 등 다양한 기준으로 세그먼트를 나눌 수 있다. 이렇게 하면 분석 툴은 카산드라를 활용해서 더욱 빠르게 데이터에 접근해 방문자가 웹사이트를 떠나기전에 빠르게 맞춤형 추천을 제공해줄 수 있다.
(3) 쓰기 중심적(write-intensive) 데이터 로깅
e-commerce에서 write이 훨씬 intensive한 경우 제품 별 ID를 기준으로 partitioning해서, 구매 transaction을 로깅한다든지, 배송 상태와 같은 정보를 트래킹을 한다든지, 시계열적으로 데이터를 쓸일이 있다든지 할 때 잘 활용할 수 있을 것 같다.
자 이렇게 간단하게 Cassandra에 대해 알아보았다. 이제 이어서 Discord가 어떻게 Cassandra를 이용해왔고, 왜 Cassandra에서 ScyllaDB로 넘어가게 됐는지를 알아볼 차례다! 다음 글에서 봅시다~~~
참고자료
https://towardsdatascience.com/when-to-use-cassandra-and-when-to-steer-clear-72b7f2cede76https://medium.com/building-the-open-data-stack/advanced-data-modeling-on-apache-cassandra-e30edad0d2b7