본문 바로가기

nosql

cassandra 1편 -퍼옴

1. 들어가기에 앞서

안녕하세요. NHN엔터테인먼트 엄세진입니다.
최근 몇 달 동안 업무상의 이유로 Cassandra를 공부 할 기회가 있었습니다. 최근 Cassandra가 여기저기서 나름 HOT한 대접을 받고 있기도 하고, 직접 공부하는 동안 겪었던 이런저런 애로사항이나 특징들을 공유한다면 행여나 다른 분들이 조금이라도 삽질을 피하실 수 있지 않을까 싶은 생각에 그간 제가 겪었던 애로사항들을 반영하여 최대한 간략히 정리한 내용을 공유 드리고자 합니다. 만약 Cassandra에 대해 제대로 깊은 내용을 알고 싶으시다면 현재 시중에 판매되고 있는 국내 번역서들 보다는, 되도록이면 DataStax의 공식 문서나 Learning Apache Cassandra, Mastering Apache Cassandra, Cassandra High Availability와 같은 외국 도서를 보시는 것을 추천해 드립니다.
(국내 번역서들이 다루는 Cassandra의 버전은 0.X대여서 현재와는 많은 차이가 있습니다. 사실 개인적으로 이 부분 때문에 많은 고생을 했었습니다.)

2. Cassandra의 특징

Cassandra는 scalability와 high availability에 최적화된 대표적인 분산형 Data storage입니다. Consistent hashing을 이용한 Ring 구조와 Gossip protocol을 구현하였으며, 때문에 각 노드 장비들의 추가, 제거 등이 자유롭고, 데이터센터까지 고려 할 수 있는 데이터 복제 정책을 사용하여 안정성 측면에서 많은 장점을 가지고 있습니다. Cassandra를 이용하면 Sharding을 고려해야 할 필요도 없고 Master-Slave 와 같은 정책이 없이도 장애에 대응할 수 있으며, 필요에 따라 장비들을 늘리고 줄이는 데 큰 비용이 들지 않습니다.
물론 그렇다고 해서 Cassandra가 완벽한 솔루션은 아닙니다. 당연한 이야기지만 모든 일에는 trade off가 있듯이 저러한 강력한 기능들이 구현됨으로써 반대로 수많은 단점들 역시 발생하였기 때문입니다. Cassandra는 Join이나 Transaction을 지원하지 않고, Index 등의 검색을 위한 기능도 매우 단출합니다. 게다가 Cassandra의 구조상 RDBMS와 같은 Paging을 구현하는 것이 힘들고 Keyspace(RDBMS의 DB와 같은)나 Table 등을 과도하게 생성할 경우 Memory Overflow가 발생할 수 있음을 고려하여야 합니다.
따라서 Cassandra가 기존의 RDBMS들의 완벽한 대체품이라고 할 수는 없으므로, 개발하게 될 제품의 기능과 특징에 따라 Cassandra를 사용할 것인지 RDBMS를 사용할 것인지를 신중히 결정하여야 합니다.

3. Cassandra Data Structure



(그림 1 Cassandra Data Layer)

Cassandra의 데이터 구조는 비교적 간단합니다. 최상위에 논리적 Data 저장소인 Keyspace가 있고, Keyspace 아래에는 Table이 존재합니다. Table은 다수의 Row들로 구성되어있으며 각 Row는 Key-Value로 이루어진 Column들로 구성됩니다. RDBMS의 DB-Table-Row-Column의 형태와 유사한 구조를 하고 있다는 걸 알 수 있습니다. 더구나 Cassandra는 현재 CQL (Cassandra Query Language)을 지원하고 있으므로, 기존의 RDBMS를 사용한 적이 있는 사람들이라면 큰 거부감 없이 Cassandra를 사용할 수 있습니다. (물론 그냥 그렇게 쓰면 망하겠지만요.)



(그림 2 : Cassandra Ring. 알파벳은 token 범위를 의미한다. 출처 : Datastax Documents)

그렇다면 Cassandra를 제대로 사용하기 위해서, 우리는 먼저 Cassandra가 어떻게 데이터를 저장하는지부터 알아볼 필요가 있습니다.
Cassandra는 기본적으로 Ring 구조를 띠고 있습니다. 그리고 Ring을 구성하는 각 노드에 Data를 분산하여 저장합니다. 그렇다면 데이터를 분산하는 기준은 뭘까요? Partition Key라고 불리는(실제 Cassandra Data Layer에서 Row Key라고 불리는) 데이터의 hash값을 기준으로 Data를 분산하게 됩니다.
처음 각 노드가 Ring에 참여하게 되면, Cassandra의 conf/cassandra.yaml에 정의된 각 설정을 통하여 각 노드마다 고유의 hash 값 범위를 부여 받습니다. 그런 뒤에, 외부에서 data의 request가 오게 되면 해당 데이터의 partition key(Row key)의 hash 값을 계산하여 해당 데이터가 어느 노드에 저장되어 있는지(혹은 저장할 것인지) 알고 접근할 수 있는 것이죠. 그리고 Cassandra는 이렇게 계산된 hash의 값을 token이라고 부릅니다.
그럼 이제 partition key가 뭔지, Row key라는 게 뭔지 알아야 할 필요가 있습니다. 둘의 차이가 뭘까요? 전자는 CQL에서 사용하는 용어고 후자는 Cassandra Data Layer에서 사용되는 용어입니다. 하지만 이렇게만 말한다면 정확히 이해가 가지 않고 혼란스러워집니다. 더구나 지금 언급한 Row key와 앞서 말했던 Table의 구성요소인 Row와 뭐가 다른 걸까요? 용어의 혼란에 동공 지진이 일어납니다. 이를 정리하기 위해서는 잠시 Cassandra의 History를 훑어볼 필요가 있습니다.

현재 Cassandra는 CQL(Cassandra Query Language)의 사용을 권장하고 있지만 CQL이 처음부터 제공되었던 것은 아닙니다. 초기의 Cassandra는 Thrift protocol을 이용한 client API를 제공하였고, 아예 Cassandra에 직접 접근해보고 싶다면 bin/cassandra-cli 유틸리티를 이용하여 데이터를 확인할 수 있었습니다. 이러한 기존의 Thrift 기반의 api와 cli 유틸리티들은 Cassandra Data layer를 있는 그대로 표현해주었지만, Thrift가 가지는 여러 가지 한계점을 같이 가지고 있었죠. 때문에 Cassandra 1.2버전 이후에는 Native Protocol을 기반으로 한 API와 CQL 문법이 추가되었고, 3.0 버전부터는 기존의 Thrift 기반의 bin/cassandra-cli 유틸리티는 아예 Deprecated 되어 사라졌습니다.
더구나 이 시기에 Cassandra에는 다른 많은 변화가 같이 있었습니다. Super Column이라고 하는 Column 안에 Column 형태를 가지는 자료구조가 아예 스펙에서 제외되어 Collection이라는 것으로 새롭게 대체되었으며, 기존의 Column Family라고 불리는 자료구조는 Table로 명칭이 변경되었습니다. 이 과정에서 CQL은 이렇게 새롭게 구성된 Cassandra Data Layer를 추상적으로 표현하는 문법으로 구성되었고, 따라서 실제 Data Layer의 용어들과 CQL에서의 표현이 1:1로 매칭되지 않고 달라지게 되었던 것이죠.



(그림 3 : Ring에 저장되는 Data들)

초창기 Cassandra Data Structure는 위의 그림과 같이 Keyspace > Column Family > Row > Column(Column Name + Column Value) 형태로 구성되어 있었습니다. Keyspace와 Column Family에 대한 정보는 모든 Cassandra node의 memory에 저장되며, 실제 유저의 데이터들이 저장되는 Row는 각 Row key를 가지고 이것의 hash 값인 token을 기준으로 각 노드에 분산 저장되었습니다. 그리고 Row에 속하는 Column들은 Column name을 기준으로 정렬되어 저장됩니다.



(그림 4 : Ring에 저장되는 Data들. Column Family가 Table로 변경되었다.)

이러한 형태는 Cassandra 1.2 에 들어서 Keyspace > Table > Row > Column(Column Name + Column Value)로 명칭이 바뀌게 됩니다.
하지만 이때 함께 등장한 CQL은 이를 있는 그대로 표현하지 않고, 한 단계 추상화하여 표현합니다.



(그림 5 : CQL의 Table 형태. 여기에서의 Row와 Column은 Cassandra Data Layer의 Row와 Column을 의미하지 않는다.)

여전히 같은 의미로 사용되는 Keyspace와 Table과는 다르게, CQL에서의 Row와 Column은 실제 데이터가 저장되는 지금까지 보았던 Cassandra Data Layer에서의 Row, Column과 그 의미가 다릅니다. 그림에서 알 수 있듯, CQL에서 Row와 Column은 RDBMS의 Tuple, Attribute와 유사하다는 것을 알 수 있습니다. 테이블의 행과 열인 것이죠. 하지만 이렇게 구성된 CQL Table은 최소 1개 이상의 Column을 primary key라는 것으로 지정해야 하며, Cassandra는 이렇게 primary key로 지정된 column들 중에서 partition key로 지정된 column의 value를 기준으로 데이터를 분산하게 됩니다.

말이 매우 복잡하기 때문에 bin/cqlsh 유틸리티를 이용하여 예제를 하나 살펴보겠습니다.

 CREATE TABLE test_keyspace.test_table_ex1 ( 
    code text, 
    location text, 
    sequence text, 
    description text, 
    PRIMARY KEY (code, location)
);

'test_keyspace' Keyspace에 'test_table_ex1'이라는 Table을 생성합니다. 이때 'test_table_ex1'이라는 Table은 각각 'code', 'location', 'sequence', 'description'이라는 Column들을 가지며 각각의 자료형은 모두 text입니다. 그리고 primary key로 'code'와 'location'을 지정하였는데, 이때 CQL의 문법에 따라 가장 첫 번째의 'code'는 partition key로, 두 번째의 'location'은 자동적으로 'cluster key'라는 것으로 지정됩니다. (cluster key란 Cassandra Data Layer에서 Row 내부 Column들의 정렬을 담당하는 key로, 자세한 내용은 좀 더 뒤에 설명하겠습니다.) 이렇게 table이 생성되었다면 다섯 건의 데이터를 입력한 후 데이터를 확인해봅니다.

INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N1', 'Seoul', 'first', 'AA');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N1', 'Gangnam', 'second', 'BB');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N2', 'Seongnam', 'third', 'CC');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N2', 'Pangyo', 'fourth', 'DD');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N2', 'Jungja', 'fifth', 'EE');

Select * from test_keyspace.test_table_ex1;



5개의 Row와 4개의 Column으로 이루어진 데이터들이 정상적으로 출력됨을 알 수 있습니다.
그렇다면 이 Table을 이번엔 bin/cassandra-cli로 출력해보면 어떻게 될까요? 확인해보겠습니다.
(value의 값은 byte로 변환되어 저장되므로 아래와 같이 정수로 표현됩니다.)

use test_keyspace;
list test_table_ex1;

Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: N1
=> (name=Gangnam:, value=, timestamp=1452481808817684)
=> (name=Gangnam:description, value=4242, timestamp=1452481808817684)
=> (name=Gangnam:sequence, value=7365636f6e64, timestamp=1452481808817684)
=> (name=Seoul:, value=, timestamp=1452481808814357)
=> (name=Seoul:description, value=4141, timestamp=1452481808814357)
=> (name=Seoul:sequence, value=6669727374, timestamp=1452481808814357)
-------------------
RowKey: N2
=> (name=Jungja:, value=, timestamp=1452481808833644)
=> (name=Jungja:description, value=4545, timestamp=1452481808833644)
=> (name=Jungja:sequence, value=6669667468, timestamp=1452481808833644)
=> (name=Pangyo:, value=, timestamp=1452481808829751)
=> (name=Pangyo:description, value=4444, timestamp=1452481808829751)
=> (name=Pangyo:sequence, value=666f75727468, timestamp=1452481808829751)
=> (name=Seongnam:, value=, timestamp=1452481808823137)
=> (name=Seongnam:description, value=4343, timestamp=1452481808823137)
=> (name=Seongnam:sequence, value=7468697264, timestamp=1452481808823137)

2 Rows Returned.
Elapsed time: 67 ms.

이때 value에 아무것도 없는 Column들은 잘못된 데이터가 아니라, Cassandra가 CQL로부터 입력된 데이터를 내부적으로 변환하여 사용하는 데이터 Column이므로 이 부분들을 제외하여 다시 정리해보면 아래와 같습니다.

Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: N1
=> (name=Gangnam:description, value=4242, timestamp=1452481808817684)
=> (name=Gangnam:sequence, value=7365636f6e64, timestamp=1452481808817684)
=> (name=Seoul:description, value=4141, timestamp=1452481808814357)
=> (name=Seoul:sequence, value=6669727374, timestamp=1452481808814357)
-------------------
RowKey: N2
=> (name=Jungja:description, value=4545, timestamp=1452481808833644)
=> (name=Jungja:sequence, value=6669667468, timestamp=1452481808833644)
=> (name=Pangyo:description, value=4444, timestamp=1452481808829751)
=> (name=Pangyo:sequence, value=666f75727468, timestamp=1452481808829751)
=> (name=Seongnam:description, value=4343, timestamp=1452481808823137)
=> (name=Seongnam:sequence, value=7468697264, timestamp=1452481808823137)

2 Rows Returned.
Elapsed time: 67 ms.

CQL과는 달리 단지 2개의 Row가 있고, 각 Row마다 속해있는 Column의 개수가 다른 것을 알 수 있습니다.
실제 Cassandra는 'N1'과 'N2'를 각각 Hashing 하여 Token을 계산하고 이 Token이 Cassandra 노드 중에서 범위에 해당하는 노드를 찾아 데이터를 CRUD 하는 것이죠.
이를 맨 처음 CQL 출력 화면과 비교하고, primary key의 조합을 변경하여 다른 케이스를 좀 더 테스트해본다면 다음과 같은 결론을 내릴 수 있습니다.

(1) Cassandra는 Row Key의 Hash 값을 이용하여 데이터를 분산한다.
(2) 이때, Cassandra Data Layer에서의 Row key = CQL partition key의 value이다. (복수의 partition key라면 해당 Column value들과 ":"문자의 조합이다.)
(3) 그리고, Cassandra Data Layer에서의 Column Name = CQL cluster key의 Column value와 primary key에 속하지 않은 Column Name들 및 ":" 문자의 조합이다.

cluster key와 같이 아직 설명이 부족한 부분들이 있지만 CQL에서 사용되는 key들에 대한 설명은 잠시 뒤에 하도록 하겠습니다.

4. Virtual Node

비록 모든 부분에 대해서 확인하지는 않았지만, 지금까지의 내용을 통하여 일단은 Cassandra 가 어떻게 데이터를 분산하는 건지 대략 정리하였습니다. 하지만 Cassandra를 조사하다 보면 또다시 사람을 혼란스럽게 만드는 용어들이 튀어나오게 되는데, 바로 initial token과 num tokens, virtual node가 주인공들입니다. 예상하시겠지만 역시 이것들도 Cassandra가 몇 차례의 버전업을 하는 과정에서 생긴 용어들입니다.

앞선 내용들을 다시 복기해봅시다.
사용자는 Cassandra에 Data를 CRUD 합니다. 이때, 해당 데이터에서 Partition Key로 지정된 Column들의 Value들의 조합이 Row Key가 되는 것이고 이 Row Key를 Hashing 하여 token을 계산한 뒤, 해당 token의 범위에 속한 Node를 찾아 CRUD 합니다. 그렇다면 여기에 전제 조건이 하나 필요하게 됩니다. 무엇보다도 가장 먼저, 각 노드 별 token의 범위가 할당되어 있어야 하는 것이죠.



(그림 6 : 3 Replication 정책에서의 Ring과 노드의 구성. 하나의 노드에서 연속한 3개의 Token 범위에 대한 저장 의무를 가진다. 출처 : Datastax Documents)

이미 설명 드렸듯이, Cassandra는 Ring 구조로 이루어져 있습니다. 그리고 Cassandra의 초창기에는 직접 수작업이나 스크립트를 이용해 Hash 값의 범위를 구하여 Cassandra의 각 노드별 conf/cassandra.yaml에 "initial_token"이라는 옵션에 해당 Hash 값을 지정해주어야 했습니다. 즉, Cassandra가 구동 시 initial token에 지정된 값을 읽어 자신이 담당하는 hash 범위를 계산하였습니다. 이때 만약 특정 노드를 추가, 제거해야 할 때는 특정 노드에 데이터가 몰리지 않도록 수작업으로 initial token을 다시 계산하여 conf/cassandra.yaml을 갱신한 뒤 Cassandra를 구동합니다. 그리고 bin/nodetool 유틸리티의 move, remove, decommission, cleanup과 같은 명령어를 통해 수작업으로 기존 데이터들을 리밸런싱을 하였죠. 그리고 Cassandra 1.2부터 virtual node라는 기능이 추가되었습니다.



(그림 7 : 3 Replication에서의 Virtual Ring. 하나의 노드에서 옵션에서 설정한 만큼의 다수개의 가상노드와 가상노드 각각이 정해진 token 범위에 대한 저장 의무를 가진다. 출처 : Datastax Documents)

virtual node는 다름이 아니라 하나의 실제 Cassandra 노드 안에 가상 노드를 여러 대 두고, 아주 잘게 나누어진 token 범위를 가상노드들에게 할당하여 데이터를 분산한다는 개념입니다. virtual node의 이점은 이러한 다수의 가상 노드들을 통하여 좀 더 데이터를 균일하게 분산하기 쉽고, 데이터가 이미 여러 대의 노드에 분산되어 있으므로 노드의 추가, 제거시 데이터의 이동, 복제, 리밸런싱에 높은 성능 향상을 가져다 줍니다. 따라서 하나의 노드에 몇 대의 가상 노드를 운영할 것인지에 대한 옵션이 바로 conf/cassandra.yaml의 "num_tokens" 항목인 것이죠. 그리고 이렇게 virtual node를 사용할 경우 노드 추가 제거에 따른 token을 매번 수작업으로 생성하여 옵션에 넣어줄 필요가 없이, Ring에 속한 모든 node 들이 알아서 gossip protocol을 통해 쑥덕쑥덕 의논하여 token 범위를 결정하고 리밸런싱까지 처리해줍니다. 편리하고 강력한 기능인 셈입니다.

5. CQL key 용어 정리.

앞서 말한 적이 있듯, 사실 CQL에은 SQL과 거의 그 형태가 유사하기도 하여 사용에 큰 어려움이 있지는 않습니다. 더구나 이 문서에서 모든 CQL의 문법을 다루는 것은 다소 시간적, 공간적 낭비인 측면이 있기 때문에 CQL의 전부를 알아보기보다는, Cassandra에 적합한 데이터 모델링을 구현하기 위한 CQL에 등장하는 각종 key들의 의미를 간단히 정리하겠습니다.
(사실 용어의 혼란이 여기에서 가장 많이 일어나기 때문이기도 합니다.)

(1) partition key
: partition key는 CQL 문법에서 Cassandra에 data를 분산 저장하기 위한 unique한 key입니다.
partition key는 특정 table의 구성할 때 반드시 1개 이상이 지정 되어야 하며, 여러 개 지정될 수도 있습니다.
partition key가 단 1개일 경우, 해당 partition key로 지정된 CQL Column의 value가 실제 Cassandra Data Layer의 Row key로 저장됩니다.
partition key가 여러 개일 경우, 각 partition key로 지정된 CQL Column들의 value들을 ":"문자와 함께 조합한 값들이 실제 Cassandra Data Layer의 Row key로 저장됩니다.

(2) cluster key
: 아시다시피 Cassandra Data Layer에서 Row에 속한 모든 Column들은 항상 정렬된 상태로 저장됩니다.
따라서 cluster key는 이러한 정렬에 대한 기준 역할을 담당합니다.
CQL에서 cluster key로 지정된 CQL Column들의 value들은 나머지 Column들의 name 및 ":" 문자와 함께 조합되어, 이 값이 실제 Cassandra Data Layer의 Column Name으로 저장됩니다. 만약 cluster key가 전혀 없는 경우엔, CQL Column의 name이 그대로 Cassandra Data Layer의 Column Name이 됩니다.

(3) primary key
: primary key는 CQL table에서의 각 row를 각자 unique 하게 결정해주는 기준 역할을 담당합니다.
primary key는 최소 1개 이상의 partition key와 0개 이상의 cluster key로 구성됩니다.

(4) composite key(=compound key)
: 1개 이상의 CQL Column들로 이루어진 primary key를 composite key 혹은 compound key라고 부릅니다.

(5) composite partition key
: composite partition key는 2개 이상의 다수의 CQL Column으로 이루어진 partition key를 의미합니다.

6. 1편을 마치며.

최대한 간단하게 쓰려고 했는데, 역시 쉽지 않았습니다. 어떻게든 간추려서 한편으로 만들어 보고자 했지만 아무래도 힘들 것 같아 2편으로 나누어 남은 내용들은 다음 편에서 다루어야 할 것 같습니다. 2편에서는 1편에 대한 약간의 추가 내용 및 Cassandra에 데이터를 읽고 쓸 때 일어나는 절차와 Cassandra에서 제공하는 기능들, Cassandra에서 자주 사용되거나 혹은 피해야할 패턴에 대한 이야기 등을 정리하겠습니다.

감사합니다.

'nosql' 카테고리의 다른 글

apache Hbase  (0) 2016.04.07
nosql 범주  (0) 2016.04.07
nosql 등장배경  (0) 2016.04.07
cassandra 3편 -퍼옴  (1) 2016.04.06
cassandra 2편 -퍼옴  (0) 2016.04.05