Cassandra Data Modeling Best Practices, Part 1

By | 2014-11-09

NoSQL 분야의 강자 Cassandra에 대한 좋은 글이 있어 번역해 올립니다.

들어가기전


원문 : http://www.ebaytechblog.com/2012/07/16/cassandra-data-modeling-best-practices-part-1

Cassandra 는 맵기반의 중첩된  데이타 구조를 수용하여 스키마의 설계시 이점(RDBMS 보다 자유로운 확장성)을 가지나, 높은 (디스크 공간과 같은 물리적)확장성을 지향하는 분산서버의 특성상 데이타 조인이 어렵습니다. 이러한 특징 때문에 데이터 중복 및 비정규화 사용이 중요하며, 지나친 비정규화 역시 걸림돌이 되기 때문에 이를 제한적으로 사용하기 위해 조회 패턴기반의 최적화 설계가 필요합니다.

기존 관계형 데이터베이스를 기반으로한 설계시 중복 및 비정규화를 사용하지 않는 게 원칙이라고 볼수 있지만, 분산환경을 지원하는 데이타베이스에서는 꼭 필요한 설계요소이며, Cassandra 역시 이러한 특징을 잘 활용해야 합니다. 그러기 위해선 초기 설계시 다음과 같은 몇가지 사항을 고려하셔야 합니다.

    • 데이타 중복 허용
    • 비정규화 사용 -> 맵구조의 특성을 잘 활용하고 서비스 수행속도를 보장
    • 비정규화 사용시 조회 패턴에 맞는 범위 내에서 적절하게 사용
    • 지연시간에 민감하거나 리스크가 높은 패턴 분리

이번 포스트의 핵심은, 분산환경 기반의 DBMS에 대한 경험이 적은 사용자들이 Cassandra 사용시 비정규화가 왜 중요한 설계 포인트가 되는지 이해하는 것입니다.  그렇지만  결국 서비스 구조와 사용하는 툴이 조화를 이루는 설계가 중요하다는 것으로 이해할 수 있습니다.

이해를 돕기위해 맵을 활용한 정규화된 데이터를  3단계의 비정규화 과정을 거쳐가며 Cassandra에 맞는 적절한 비정규화를 예를 들어 자세히 설명하고 있으니, 천천히 읽어 보시고 적절한 비정규화의 범위가 어디까지인지 eBay사례를 통해 확인해 보시길 바랍니다.(Cassandra에 대한 사전 지식이 없더라도 이해하는데는 문제 없을 것입니다.)

eBay내에서 Cassandra사용에 대한 몇가지..


eBay는 과도한 쓰기가 발생하는 logging 와 tracking부터 복합적인 기능에 이르기까지 다양한 케이스에 Cassandra를 적용 중이며, “Social Signal” 프로젝트 및  eBay 상품 페이지에 “like/own/want” 기능에 활용하고 있습니다. Cassandra 사용률은 지속적인 증가 추세를 보이고 있으며, 기능과 위험도에 따라 수십 개의 노드를 작은 클러스터 단위로 쪼개어 여러 개의 데이터센터에 분산 적용하였습니다. 그리고 Cassandra이외에도 MongoDB 와 HBase도 활용 중으로 보입니다.

 이 글은 eBay에서 적용한 Cassandra 데이터모델링 best practices에 중점을 두고 있기 때문에, 이외 여러 가지 사항들이 궁금 하시다면 Cassandra summit에 방문해 보시길 권해 드립니다.

Terms and Conventions

    • 몇 가지 규칙을 먼저 설명 드리면.“Column Name” 과 “Column Key” 그리고  “Super Column Name” 과  “Super Column Key” 는 서로 바꾸어 사용 가능합니다.
    • 아래 그림은 Column Family (CF)의 형태입니다.

    • 다음 그림은 Super Column Family (SCF)의 형태입니다.

    • 아래 내용은 복합 컬럼 형태의 Column Family를 하나의 row로 표현한 그림입니다. 복합 컬럼의 구분 자는  ‘|’를 사용합니다.
    • ‘|’ 단순히 표현상의 내용이지, Cassandra는 실제 ‘|’를 사용하지는 않습니다.

위 세가지 형태의 Column Family를 기반으로 설명을 진행하도록 하겠습니다.

Don’t think of a relational table

Cassandra는 관계형 테이블 대신 중첩된 맵 구조를 사용합니다. 아래 그림은 관계형 모델을 기반으로 Cassandra를 설명하기 위해 자주 사용되는 내용입니다.
 RDBMS에 익숙한 사용자들 위해 Cassandra의 데이터 모델을 관계형 모델에 비유한 그림입니다. 위 그림은 그냥 참고용 표현일뿐, 데이터 디자인 시 이러한 연관관계를 기반으로  설계 하는 것은 잘못된 설계의 원인이 될 수 있으며, 설계시 외부는 Row Key 이고 내부는  Column Key 형태로 둘다 정렬된 Cassandra column family를 사용하는 게 좀더 설계에 도움이 되는 구조이며 아래와 같습니다.

SortedMap<Row Key, SortedMap<Column Key, Column Value>>

위와 같이 Cassandra column family는 map 기반의 정렬된 Row Key와 Column Key형태가 적합하다고 볼수 있습니다.

Why?

하나의 중첩 정렬된 맵은 관계형 테이블에 비해 보다 자세한 표현이 가능 하기 때문에 Cassandra 데이터 모델링에 적합니다.

(참고: http://en.wikipedia.org/wiki/List_of_data_structures)


How?

    • 맵의 정렬구조는 조회를 보다 빠르게 수행할 수 있도록 도와줍니다. Cassandra에서는 row keys와 column keys 기반으로 조회나 범위 검색을 효율적으로 처리할 수 있습니다.
    • 컬럼 키의 수는 제약이 없으며, 하나의 키 자체가 값으로 사용될 수 있습니다. 물론 Value에는 Null(Null도 값으로 볼수 있지만, 여기서는 데이타가 없음을 의미하며 단지 Null로 표현 하였습니다)값도 허용합니다.

row keys 기반의 범위 검색은  데이터 Order Preserving Partitioner (OOP)를 사용한 클러스터에서 가능하며, 대부분 사용을 피하기 때문에 대신 정렬되지 않은 outer map을 고려해볼 수  있습니다.

(여기서 잠깐, Cassandra는 두가지 타입의 Partitioner를 지원하며 기본은 RandomPartitioner입니다. 약간 설명을 추가 하자면

    • RandomPartitioner  : MD5 기반의 hash Key를 발급하여 트래픽을 골고루 분산할수 있는 장점이 있으나 Range Scan 불가능 합니다.
    • ByteOrderedPartitioner : Key를 hexadecimal 형태로 관리하여 각 노드에 할당하는 구조로 key 값을 기준으로 각 노드에 대한 바이트 추적이 가능하나 병목 유발의 가능성이 존재하여 일반적으로 사용을 안합니다.)

Map<RowKey, SortedMap<ColumnKey, ColumnValue>>

Cassandra 안에는 “Super Column” 이라는 게 존재합니다.  “Super Column”을 그룹화된 컬럼이라고 간주한다면 두 개의 중첩된 맵을 3개의 중첩된 맵으로 다음과 같이 변환할 수 있습니다.

Map<RowKey, SortedMap<SuperColumnKey, SortedMap<ColumnKey, ColumnValue>>>

Notes:

    • Cassandra의 전반적인 문제점 해결을 위해 각각의 컬럼 밸류에 time stamp가 필요합니다. 그러나 모델링 시점에서는 무시해도 상관 없기 때문에, 어플리케이션 데이터에 time stamp를 고려한 내용은 생략하도록 하겠습니다. HBase와 달리 맞춤형 모델이 아니며, 데이타의 새로운 버전도 정의 되어 있지 않습니다.(HBase는 각각의 데이타에 대한 버전 관리를 지원합니다)
    • Cassandra의 커뮤니티는 수퍼컬럼의 사용을 2차 인덱스의 지원과 퍼포먼스 상의 이유로 반대하고 있으며, 수퍼컬럼 대신 복합컬럼을 사용하는 방법도 존재합니다.

Model column families around query patterns


조회 패턴을 고려하여 컬럼 패밀리를 구성하되, 엔티티와 관계를 디자인 시작 시 같이 검토하시기 바랍니다.

    • 관계형 데이터베이스와 달리, 조인, 오더, 그룹을 사용하는 복잡한 SQL을 만드는 것과 2차 인덱스를 생성하는 것은 확장성을 지향하는 Cassandra에서는 사실상 불가능합니다. 그렇기 때문에 조회패턴을 고려한 컬럼 패밀리 디자인이 필요합니다.
    • 앞서 얘기한 중첩된 맵구조를 기반으로 어떻게 쿼리가 요구한 내용을 안전하고 빠르게 조회/정렬/그룹/필터/병합등을 맵형태의 데이타 구조로 처리할 지 고려해야 합니다.

그러나  엔티티나 관계에 대한 추가 수정이 필요한 상태입니다. (로그 저장이나 시간기준의 데이타와 같은 특별한 경우에도 포함해서) . 만약 전자 상거래 웹사이트 모델링 시  조회패턴이 엔티티나 관계에 대한 아무런 정보가 없다면, 아마도 엔티티와 관계에 대해 조회 패턴이나 사전에 이해하고 있는 도메인 정보 기반으로 조사하려고 할 것입니다.  엔티니와 관계를 이해하는 것으로부터의 시작은 중요하며, 지속적으로 중복과 비정규화된 조회 패턴들 위주의 모델링 수정 작업이 필요합니다. 만약 잘 이해가 되지 않는다면, 다음 포스트에서 확인해보시기 바랍니다.

Note: 조회 빈도의 많고 적음에 대해 예를 들면, 쿼리 기준으로 단지 몇 천 번 처리된 것과 1억 번 이상 수행된 것 둘 중 어느 것이 많고 적은지는 분명합니다. 당연히 조회빈도가 1억건 이상인 쿼리를 우선시하여 설계할 필요가 있습니다. 추가적으로 고려해야할 사항은 지연시간에 대한 민감도입니다. 가장 높은 빈도와 위험도가 높은 쿼리 순으로 문제가 없는지 확인이 필요합니다.

De-normalize and duplicate for read performance


비정규화는 가능한 필요한 부분에만 적용하고 남용하는 것 역시 금물입니다. 모든 것이 적합한 밸런스를 찾기 위함이지 비정규화가 목적은 아니기 때문입니다.

정규화의 장점(최소한의 중복 등)이나 단점(테이블간 많은 조인이 필요할경우 속도가 느려짐)은 잘 알려져 있습니다. Cassandra 역시 마찬가지로 분산환경의 특성상 조인이 불가능한 것은 단점 중에 하나입니다. 그래서 스키마 정규화 시 많은 어려움이 있을 수 있습니다. 모델링과 조회 패턴에 대한 이해, 그리고 지금 이 내용은 아주 중요한 부분이기 때문에 나머지 부분에서 상세한 예제와 함께 집중적으로 다루어 볼 생각입니다.

Example: ‘Like’ relationship between User & Item

아래는 전자 상거래 시스템에서 하나 또는 여러 아이템에 대한 like 기능 수행 기능에 관련된 예제입니다.
각각의 사용자는 여러 아이템을 “like” 할 수 있으며, 각각의 아이템 역시 여려 사용자에게 “like”로 선택될 수 있습니다. 이는 관계형 모델 기반으로 N:N 형태로 나타나게 됩니다.


위그림을 기반으로 예를 들면 아래와 같이 데이타를 조회할 수 있습니다.

    • Get user by user id
    • Get item by item id
    • Get all the items that a particular user likes
    • Get all the users who like a particular item

아래에서 Cassandra의 데이타 모델링을 위한 몇 가지 옵션들을 비정규화를 적용하여 가장 낮은 순에서부터 가장 높은 순으로 적용한 예를 보여 드리도록 하겠습니다. 곧 알게 되겠지만 가장 최선의 방법은 조회 패턴에 기반한 모델링입니다.

Option 1: Exact replica of relational model

위 모델은 아이템 아이디와 유저아이디 기반의 조회가 가능합니다. 그러나 특정 유저가 좋아하는 또는 모든 유저들이 어떤 아이템을 좋아하는지 표현하기엔 부족할수 있으며, 보충하기 위해선  User_Item_Like 조회 기능의 최적화가 필요합니다. ‘time stamp’ 컬럼은 (유저가 “like”할 때 저장되는) 간결하게 하기위해 User_Item_Like 에서 삭제되었으며, 이 부분은 뒤에 설명할 예정입니다.

Option 2: Normalized entities with custom indexes

 

위 모델은 유저 아이디와 아이템 아이디를 중복으로 저장하여 맵핑하는 것을 제외하면 상당히 정규화된 형태를 취하고 있습니다.
일부 유저가 Item_By_User를 사용하여 “like”한  모든 아이템을 쉽게 조회할수 있으며, User_By_Item을 사용하여 일부 아이템을 “like”한 모든 사용자를 조회할 수 있습니다.그렇지만 컬럼 패밀리가 분리돼 있기 때문에 조인이 필요한 상황이며 조인을 할 수 없기 때문에 문제가 발생할수 있습니다. 예를 들면 특정 유저에 의해 “liked”된 아이템 아이디와 제목을 원한다고 가정해 보면,

  • 첫째, 사용자가 “like”한 아이템 아이디를 얻기 위해 Item_By_User 를 조회하기 위한 쿼리가 필요합니다.
  • 그리고  각각 아이템 아이디의 타이틀을 가져오기 위한 조회가 또 한번 필요합니다.

다른 예를 들면, 특정 아이템을 좋아하는 사용자의 아이디와  이름을 동시에 조회해야 할 상황이라면,

  • 해당 아이템을 좋아하는 모든 유저들을 찾기 위해 User_By_Item에 대한 조회 후,
  • 각각의 유저 아이디별로 유저 이름에 대한 추가 조회를 해야만 원하는 결과를 가져올 수 있습니다.


하나의 아이템이 수백 명의 사용자에 대해 “like” 되거나 또는 한명의 유저가 많은 아이템을 선택하는 것이 현실적으로 가능한 상황이기 때문에 이러한 조회패턴을 염두해서 설계할 필요가 있습니다. 이렇게 해당 아이템을 좋아하는 사용자 이름을 조회하기 위해, 또는 그 반대일 경우에도 많은 추가 조회 작업이 필요한 상황은 개선할 필요성이 있다고 생각됩니다. 이는 Item_by_User 와 User_by_Item에 아이템 제목에 대한 비정규 형태의 최적화가 더욱더 필요하며 옵션3과 같이 나타낼 수 있습니다.

Option 3: Normalized entities with de-normalization into custom indexe


이번 예제의 모델에서는, User_By_Item과 Item_By_User 안에 사용자 이름과 제목이 비정규화 되어 있습니다.  하나의 사용자가 “like”한 모든 아이템 제목과 하나의 아이템을 “like”한 모든 사용자 이름을 효과적으로 조회할수 있도록 설계를 변경 했습니다.

조회 패턴을 좀더 발전시켜 한 명의 사용자가 “like”한 아이템들에 대해 주어진 모든 정보를 가져와야 한다면? 만약 정말 이러한 조회가 필요한 지 고민해 볼 필요가 있습니다. 대신 사용자가 좋아할만한 제목을 모두 노출시키고, 클릭시 추가적인 정보를 끌어오는 방법으로 처리해서 부적합한 조회 패턴을 적절하게 변경할 수 있기 때문입니다. 때문에 이러한 극도의 비정규화는 적절한 형태로 바꾸는것이 가능합니다. (일반적으로 제목과 가격은 노출이 되며, 간단히 목적에 맞추어 처리하기 할 수 있습니다.)
이어서 다음 두 개의 조회 패턴을 검토해보겠습니다.

  • 주어진 하나의 아이템 아이디를 기반으로 그 아이템을 좋아하는 사용자의 이름과 함께 모든 아이템 데이타를 조회한다.
  • 주어진 한명의 사용자 아이디를 기반으로 그 사용자가 좋아한 아이템 제목들과 함께 모든 사용자 데이터를 조회한다.


사용자 상세 페이지나 상품 상세 페이지에 기능으로  잘 동작할 것입니다. 둘 다 위 모델을 기반으로  두  번의 조회가 필요하며 ,

  • 첫 번째로 아이템(또는 사용자) 데이터를 조회하고
  • 두 번째로 사용자 이름(또는 아이템 제목)을 조회할 것입니다.


만약 사용자가 활동적이더라도 (수천 개의 아이템을 좋아한다면) 또는 상품이 반응이 뜨거워 수백 만의 사용자가 좋아 하더라도 추가 조회는 발생하지 않을 것입니다.

이정도면 꽤 많은 최적화가 진행됐다고 생각 되며 option 2에 비해서 비정규화 부분은 그리 많지는 않았습니다. 그러나 설명을 위해 option 4에서 좀더 최적화를 할수 있는지 알아보도록 하겠습니다.

Option 4: Partially de-normalized entities

 

11

option 4는 지나친 비정규화로 한 예로 볼수 있습니다. 수퍼 컬럼을 기반으로한 option 3는  사용자와 아이템이 많은 항목(eBay에서 처리했던것과 유사하게)을 공유한다면, 옵션4 보다는 3번을 선택할 것입니다.

모든 아이템 데이터를 사용자 엔티티에 넣거나 또는 그 반대가 되는 극도의 비정규화는 아니기 때문에 여기서는 부분 비정규화라는 용어를 사용했습니다. 모든 데이터를 한쪽 속성으로 편입시키는 극도의 비정규화는 고려하지도 않았으며, 이러한 케이스는 부적절하다고 여러분도 생각할 것입니다.

Note: 여기서는 수퍼 컬럼을 단지 데모 목적으로 사용했습니다. 실제로는 수퍼 컬럼보다는 복합 컬럼을 선호할 수 있습니다.

The best model

위의 예제에서 Best는 3번입니다(4번은 필요 이상의 비정규화 과정을 거쳤기 때문..). 추가적으로 time stamp는 제외 되었지만 timeuuid 형태로 아래 마지막 모델에는 포함 하도록 해보겟습니다.

User_By_Item 와 Item_By_User 컬럼 패밀리들을 안의 timeuuid 와 userid를 합쳐서 하나의 컬럼키로 구성합니다.

column keys는 물리적으로 정렬되고 저장된 구조로 이를 재호출하는 구조입니다. User_By_Item과 Item_By_User 내의 timeuuid 에 의해 정렬 저장 되었으며, 시간에 의한 범위 검색에 효율적이라 볼 수 있습니다. 모든 데이타를 읽을 필요없이, 사용자가 “like” 한 내용이나 또는 어떤 아이템이 “like”한 내용을 마지막(최근)에 저장된 데이타에 대해 범위검색시 이점을 가진다고 볼 수 있습니다.
  

 

Summary


지금까지 Cassandra 데이타 모델을 디자인하기 위해 몇 개의 기본적인 사례를 기반으로 상세한 예제와 함께 살펴 보았습니다. 몇 가지 주요 내용을 살펴보면

 

    • Cassandra 컬럼 패밀리를 디자인할 때 관계형 테이블을 고려하지 말고, 중첩된 맵구조를 고려하라.
    • 조회 패턴에 기반하여 컬럼 패밀리를 디자인하라. 그러나 가능한 엔티티와 그 관계를 디자인 시작할 때 고려하라.
    • 읽기 성능을 위해 비정규화와 중복을 허용하라. 그렇지만 비정규화는 필요할 때만 사용하라.
    • 모델링을 하기 위한 많은 방법이 존재하지만 최고의 방법은 유즈케이스와 조회 패턴들을 잘 활용 하는 것이 중요하다.


Cassandra 의 장점을 최대한 살리기 위해 트래픽 비중을 고려하여 적절한 비정규화 에 대해 간단히 알아 보았습니다.

쌩뚱맞지만 마지막으로 이글을 읽으시는 분들중에 NoSQL의 선택에 대한 고민을 가지시는 분들도 있으실거라 생각 되어(NoSQL 도입시 선택에 대한 질문을 몇 차례 받은 바가 있어서..), 마무리는 NoSQL의  초기 도입 전략에 대한 제 소견을 짧게 정리해보고 마치도록 하겠습니다.

관계형DB가 범용에 가까운 특징을 가졌다고 하면,  NoSQL은 각각 태생적 특성에 맞추어 차별화된 기능을 가지고 있습니다. 많은 NoSQL이 존재하며 전문가도 부족한 상황입니다. 그렇기에 도입 분석에 대한 시간이나 역량이 부족하다고 판단될 시는 Mongo DB와 같은 초기 도입이 쉬운 것을 추천 드리며, 기능을 어느 정도 이해하신 후 세부사항이 도출되면 거기에 따라 적합한 NoSQL을 선택 하시길 권해드립니다. 만약 전문가라면 한번에 적절한 걸 선택 하실지도 모르지만, 최초 설계가 마지막까지 지속되지 않을 수 있음으로, 되도록 유연하고 빠른 개발이 가능 한것을 선택하시는게 현명하다고 생각됩니다.