Tag Archives: NoSQL

[NoSQL/Cassandra] 카산드라에서의 인덱싱(Indexing) 전략

사용자 삽입 이미지
카산드라는 굉장히 특이합니다. 특이할수밖에 없는 것이 카산드라는 아마존의 다이나모(Dynamo)와 구글의 빅테이블(BigTable)의 장점만을 수용하여 발전한 형태인 NoSQL의 대표적인 DB입니다. 아마존의 다이나모에서는 클러스터 관리(Cluster Management), 리플리케이션(Replication), 결함허용(Fault Tolerance – 한국말이 더 어렵네요, 문제가 발생해도 시스템 운영 전반적인 운영에 영향을 끼치지는 않는 형태를 의미합니다)과 같은 장점을 가져왔고 구글의 빅테이블에서는 스파스(Sparse – 실제 사용량보다 더 많은 공간을 미리 차지하게 하는 기술입니다. 대용량 파일을 사용가능하게 합니다), 주상 데이터 모델(Columnar Data Model – 데이터의 시작점과 끝이 연결되는 원형으로 저장을 합니다. 범위를 지정하여 데이터를 가져오는데에 도움이 됩니다), 저장소 설계(Storage Architecture)를 가져왔습니다.

지금까지의 RDB에서는 정규화라는 이름의 과정을 거친 정형화된 데이터를 보관해두고 SELECT라는 엄청난 부가적인 기능을 제공하는 쿼리문을 사용하여 자신이 원하는 다양한 형태로 데이터를 가져올 수 있었습니다. 하지만 카산드라는 조금 예시가 다릅니다. 카산드라는 항상 원하는 형태로 데이터를 이미 보관하고 있다가 있는 그대로를 꺼내주게 됩니다. 이러한 형태의 설계가 가능한점은 카산드라는 읽기보다 쓰기가 더 빠르기 때문입니다. 쓰기가 엄청나게 빠르게 때문에 읽기 쉬운 형태로 데이터를 가공하여 저장을 해둔후에 그대로 꺼내 쓰기 때문에 결과적으로 읽기에도 엄청나게 큰 이득을 얻게 됩니다.

결과적으로 카산드라는 인덱스 생성킷이 됩니다. 이것이 무슨말이냐 하면 쓰기가 유용하기 때문에 언제든지 읽고 싶은 형태로 저장을 해도 됩니다. 언제 어떻게 쓰일지 모르는 RDB의 인덱스들에 비해서는 설계를 어떻게 하느냐에 따라 엄청나게 큰 효과를 얻을 수 있습니다. 우선 다음의 이야기를 이해하기 위해 다음의 것들을 다시한번 짚고 넘어가도록 하겠습니다. 컬럼과 슈퍼컬럼에 대한 이해가 부족하실 경우 다음의 글을 읽어보시면 도움이 될 것 같습니다.

[code]// Two-Level Map (Column Family)
key: {
  column: value,
  column: value,
  …
}

// Three-Level Map (Super Column Family)
key: {
  supercolumn: {
    column: value,
    column: value

  },
  supercolumn: {
    …
  }
}[/code]

각각의 컬럼들은 CompareWithCompareSubcolumnsWith 옵션에 의해 정렬이 결정됩니다. 이러한 타입에는 제 글들에 항상 언급되어서 또 쓰기 그렇지만 UTF8Type, LongType, ASCIIType, LexicalUUIDType, TimeUUIDType등이 있습니다. 이제 간단한 정렬에 대한 예제를 보여드리겠습니다. 우선 파티셔너(Partitioner)에 대한 설명이 필요할것 같은데 그건 다음에 작성할 글에서 언급해 보겠습니다.

[code]<ColumnFamily Name=”Users”
              CompareWith=”UTF8Type”/>

“b”:       {“name”:”Ben”, “street”:”1234 Oak St.”,
            “city”:”Seattle”, “state”:”WA”}
“jason”:   {”name”:”Jason”, “street”:”456 First Ave.”,
            “city”:”Bellingham”, “state”:”WA”}
“zack”:     {”name”: “Zack”, “street”: “4321 Pine St.”,
             “city”: “Seattle”, “state”: “WA”}
“jen1982”: {”name”:”Jennifer”, “street”:”1120 Foo Lane”,
            “city”:”San Francisco”, “state”:”CA”}
“albert”:  {”name”:”Albert”, “street”:”2364 South St.”,
            “city”:”Boston”, “state”:”MA”}[/code]

이제 여기서 다음과 같은 쿼리의 결과를 가져올려면 어떻게 해야 할까요?
[code]SELECT name FROM Users WHERE state = “WA”[/code]

우선, RDB적인 접근 방식으로는 답이 안나옵니다. 내가 만약에 state로 데이터를 검색해야 하는 상황이 있다고 인지하였다면 거기에 적절한 형태의 인덱싱용 컬럼패밀리를 만들어주면 됩니다.

1. 첫번째 – 슈퍼컬럼 인덱싱(SuperColumn Indexing)

인덱싱을 위하여 다음과 같은 슈퍼컬럼을 포함하는 컬럼패밀리를 생성하도록 합시다.
[code]<ColumnFamily Name=”LocationUserIndexSCF”
              CompareWith=”UTF8Type”
              CompareSubcolumnsWith=”UTF8Type”
              ColumnType=”Super”/>[/code]
이제 이와 같은 슈퍼컬럼을 갖는 컬럼패밀리를 가지고 원하는 형태로 데이터를 맞추어 넣습니다. 우리는 state로 검색을 할것이므로 state를 로우의 키로하여 데이터를 다시 넣어줍니다.
[code]”CA”: { // Row Key
  “San Francisco”/*Super Column*/: {“Jennifer”/*Column*/: “jen1982″/*Value*/}
}
“MA”: {
  “Boston”: {“Albert”: “albert”}
}
“WA”: {
  “Bellingham”: {“Jason”: “jason”},
  “Seattle”: {“Ben”: “b”, “Zack”: “zack”}
}[/code]

이제 워싱턴(WA)에 사는 모두를 가져오기 위해서는 다음과 같은 쿼리 한번이면 됩니다. 참고로 앞으로 나온느 문법들은 Ruby의 문법입니다. 그냥 이해만 하신다는 느낌으로 봐주시면 될듯하네요.
[code]get(:LocationUserIndexSCF, “WA”)

// 결과

{
  “Bellingham”: {“Jason”: “jason”},
  “Seattle”: {“Ben”: “b”, “Zack”: “zack”}
}[/code]

2. 두번째 – 복합 키 인덱싱(Composite Key Indexing)

이번의 복합키 인덱싱에서는 Order Preserving Partitioner(OPP)와 범위쿼리(Range Queries)를 사용합니다. OPP에 대해서는 다음에 좀더 자세히 언급하겠습니다만 여기 예제를 보시다 보면 어떤것인지 느낌이 오실지도 모르겠네요.
[code]<ColumnFamily Name=”LocationUserIndexCF”
              CompareWith=”UTF8Type” />

// 보유 데이터
“CA/San Francisco”: {“Jennifer”: “jen1982”}
“MA/Boston”: {“Albert”: “albert”}
“WA/Bellingham”: {“Jason”: “jason”}
“WA/Seattle”: {“Ben”: “b”, “Zack”: “zack”}[/code]
위에서 알 수 있듯이 로우의 키를 복합적인 인덱스 활용을 위해 /를 붙여서 한꺼번에 입력한 것을 볼 수 있습니다. 여기서 키를 비교할때는 UTF8Type을 이용한다는것을 눈여겨 봐두시기 바랍니다.

이제 워싱턴(WA)에 사는 모두를 가져오기 위해서는 다음과 같은 범위쿼리 한번이면 됩니다.
[code]get_range(:LocationUserIndexCF, {:start: ‘WA’, :finish:’WB’})

// 결과

{
  “WA/Bellingham”: {“Jason”: “jason”},
  “WA/Seattle”:    {“Ben”: “b”, “Zack”: “zack”}
}[/code]
자 설명을 해보겠습니다. 위의 쿼리는 WA부터 WB사이의 모든 값을 반환합니다. UTF8Type으로 키를 구분하므로 WA/는 WA보다 더 다음의 순서입니다. 마찬가지로 WAA도 WA보다 더 다음의 순서입니다. 결과적으로 WA뒤에 그 어떤 문자가 더 붙더라도 WA보다 뒤의 순서가 됩니다. 그리고 또한 키의 이름을 [state]/[city]로 정의한 이상 WA만을 키로 갖는 경우는 존재하지 않습니다. 결과적으로 WA로 시작하는 모든 키가 여기에 포함됩니다.

그럼 다음으로 WB까지라는 조건에 대해서 생각해 보겠습니다. WB는 WA때와 마찬가지로 WB자신을 포함할 상황이 존재하지 않습니다. WA는 WB보다 이전의 순서인 키입니다. 마찬가지로 WAA는 WA보다는 뒤이지만 WB보다는 앞의 키입니다. WALAKJSDLKASJDLKJALKSDJSKLADJ 역시도 WB보다 앞의 키입니다.(UTF8Type이므로) 결과적으로 WA ~ WB범위의 값을 가져오라는 것은 워싱턴 전체의 데이터를 가져오라는 말과 같습니다. 그럼 다시 처음으로 돌아가서 워싱턴에서도 시애틀에 사는 사람만을 뽑아오고 싶다면?
[code]get(:LocationUserIndexSCF, “WA/Seattle”)

// 결과
{
  “WA/Seattle”:    {“Ben”: “b”, “Zack”: “zack”}
}[/code]
이런식으로 2차인덱스의 성격으로 자유롭게 활용할 수 있습니다. 슈퍼컬럼의 좋은 활용예이기도 합니다.

참고:
http://www.slideshare.net/benjaminblack/cassandra-basics-indexing (Thanks to Benjamin)

[NoSQL/Cassandra] 카산드라를 이용한 간단한 블로그 설계 예제

사용자 삽입 이미지
[도대체 슈퍼컬럼이 무엇일까? 카산드라 데이터모델에 대한 소개]에서 카산드라의 데이터 모델에 대해 중구난방으로 설명을 해보았습니다. 이제 이러한 정보를 한데 모아서 간단한 블로그 어플리케이션을 만들어보도록 하겠습니다. 이 간단한 블로그는 다음과 같은 기능을 가집니다.

– 단일 블로그 지원
– 다수의 필자 지원
– 하나의 글은 제목, 글내용, 퍼머링크, 글 작성 시간을 데이터로 갖습니다.
– 하나의글은 다수의 태그를 가질 수 있습니다.
– 방문객들은 가입은 할 수 없지만 댓글을 작성할수는 있습니다.
– 댓글은 글내용, 작성시각, 글작성자의 이름을 데이터로 갖습니다.
– 글은 가장 시간을 기준으로 역정렬하여 보여줍니다. (가장 최근글이 처음으로 나옵니다)
– 태그를 선택시에 해당하는 모든 글은 시간을 기준으로 역정렬되어 보여줍니다.

이제 설명하게 되는 각각의 컬럼패밀리들은 하나의 단일 키스페이스에 포함되게됩니다. 예시를 위해 XML정의도 함께 보여드리겠습니다. CF는 컬럼패밀리(ColumnFamily)입니다.

필자의 컬럼패밀리(ColumnFamily)
필자의 정보를 담는 컬럼패밀리를 모델링하는 것은 매우 간단합니다. 필자는 자기 자신의 로우를 가질것이며 자신의 이름을 키로써 사용할 것입니다. 로우의 안에는 필자의 프로필 정보를 뜻하는 몇가지의 컬럼이 들어갈 것입니다. 여기서 컬럼패밀리의 로우의 데이터를 가져오기 위해 필자의 이름인 키를 이용하여 데이터를 가져올 것이며 모든 포함된 컬럼들을 가져올 것입니다. 여기의 필자의 이름은 특별히 정렬되어야 할 필요가 없으며 그러므로 정렬기준이 되는 옵션으로 BytesType을 사용하였습니다. 이 값은 데이터를 추가시에 아무런 Validation처리를 하지 않고 맨 뒤로 값을 추가하게 됩니다.

<!--
    ColumnFamily: Authors
    모든 필자의 정보를 이곳에 저장합니다.

    Row Key => 필자의 이름 (필자의 이름은 유니크함을 의미합니다)
    Column Name: 하나의 글에 대한 속성값들 (제목, 글내용, 기타)
    Column Value: 컬럼들의 값

    Access: 필자의 이름을 통해 모든 정보를 가져오게 됩니다
-->
    Authors : { // CF
        Arin Sarkissian : { // row key
            // 컬럼의 정보는 필자의 프로필입니다
            numPosts: 11,
            twitter: phatduckk,
            email: arin@example.com,
            bio: "bla bla bla"
        },
        // 다른 필자의 정보
        Author 2 {
            ...
        }
    }

<ColumnFamily CompareWith="BytesType" Name="Authors"/>

블로그글 컬럼패밀리(ColumnFamily)
블로그글 역시도 마찬가지로 단순한 키/밸류 방식을 따르고 있습니다. 하나의 로우는 하나의 글을 저장하게 됩니다. 이러한 로우의 컬럼들은 블로그글의 다양한 데이터를 포함하고 있습니다. 태그의 경우에는 일명 demornalize라고 불리는 사용에 필요한 형태로 데이터를 퇴화(?)랄까 변화시키는 과정을 거쳐 입력을 하게 됩니다. 여기서 태그에는 콤마를 붙여 구분하도록 입력하였습니다.
각각의 블로그글의 키로써는 퍼머링크(slug)를 사용하였습니다. 블로그글의 주소로써 곧바로 요청을 하면 바로 데이터를 꺼내줄 수 있어 용이할 듯 하네요.

<!--
    ColumnFamily: 블로그글
    모든 블로그글을 포함합니다.

    Row Key: 글의 퍼머링크(slug)
    Column Name: 블로그글에 필요한 각 요소(글제목, 글내용, 기타)
    Column Value: 컬럼들의 값

    Access: 퍼머링크 값을 통해 데이터를 가져오게 됩니다.
-->
    BlogEntries : { // CF
        i-got-a-new-guitar : { // 로우의 키
            title: This is a blog entry about my new, awesome guitar,
            body: this is a cool entry. etc etc yada yada
            author: Arin Sarkissian  // 필자컬럼패밀리의 로우키
            tags: life,guitar,music  // 태그들을 콤마로 분류
            pubDate: 1250558004      // unixtimestamp 형식의 글 작성시각
            slug: i-got-a-new-guitar
        },
        // 또다른 글
        another-cool-guitar : {
            ...
            tags: guitar,
            slug: another-cool-guitar
        },
        scream-is-the-best-movie-ever : {
            ...
            tags: movie,horror,
            slug: scream-is-the-best-movie-ever
        }
    }

<ColumnFamily CompareWith="BytesType" Name="BlogEntries"/>

태그의 컬럼패밀리(ColumnFamily)

여기서는 이제 카산드라의 흥미로운점에 대해 다루어보도록 하겠습니다. 태그의 컬럼패밀리에서는 지금까지와는 달리 조금 어려워질수도 있겠습니다. 여기서는 기본적으로 태그와 블로그글의 상호 연관관계에 대해 이해를 할 필요가 있습니다. 다음의 예제에서는 카산드라의 이미 정렬된채로 저장되는 데이터에 대한 설명과 함께 데이터를 가져올때 어떤식으로 가져올 수 있는지를 설명하게 됩니다. 이미 정렬된 태그정보를 이용하여 블로그글을 정렬된 순서로 가져올 수 있습니다.

여기서 태그를 구현하기 위해 중요한 설계 요소로는 모든 블로그 글에 대하여 “__notag__”라는 태그를 붙일것이라는 것입니다. 블로그 최근글보기를 수행할 경우 이 “__notag__”태그를 가지고 최신글을 가져오게 됩니다. 결과적으로 블로그에 3개의 태그를 달았다면 이 “__notag__”를 포함하여 총 4개의 태그를 가지는것이 됩니다.

또한 추가로 이해해야 할 점으로는 각각의 로우들을 시간순으로 정렬하기 위해서는 각각의 컬럼들이 Time UUID라는 타입을 기준으로 정렬할 수 있도록 컬럼패밀리의 CompareWith설정을 TimeUUIDType으로 변경해 주어야 합니다.  이경우에 모든 컬럼들은 시간순으로 정렬이 될것이며 특정 태그를 포함한 최근글을 가져오라는 명령을 수행하기에 적절한 상태가 됩니다.

이제 최근글 10개를 가져오기 위해 다음과 같은 과정을 수행합니다.

1. __notag__라는 태그를 가진 최근 글 10개를 가져옵니다. (이태그를 포함한다는 조건은 글 전체라는 의미가 됩니다)
2. 가져온 컬럼들의 묶음을 가지고 순환(Loop)을 돌며 처리를 합니다.
3. 순환을 돌며 블로그글 컬럼패밀리의 로우의 키를 알수가 있습니다.
4. 이 값(블로그글의 로우 키)을 가지고 블로그글을 가져옵니다.
5. 이 블로그글의 “author”컬럼의 값을 가지고 필자 컬럼패밀리의 로우를 가져오게 되면 필자의 프로필 데이터를 가져올 수 있습니다.
6. 이시점에서 우리는 블로그글뿐만 아니라 필자의 데이터도 가져올 수 있게 되었습니다.
7. 이제 블로그글 로우의 “tag”컬럼값을 ,를 기준으로 분리할 수 있습니다.
8. 이제 댓글을 제외한 블로그글을 보여주기 위한 모든 과정을 거쳤습니다.
<!--

    ColumnFamily: 태그
    블로그글을 정렬하기 위해 2차 인덱스로써 사용되기도 하는 요소

    Row Key => tag
    Column Names: TimeUUIDType
    Column Value: 블로그글 컬럼패밀리의 로우 키

    Access: 특정 태그가 달린 글을 가져올 수 있습니다.
-->
    TaggedPosts : { // CF
        // "guitar" 태그가 달린 글들
        guitar : {  // 태그 이름이 키가 됨
            // 컬럼의 이름은 TimeUUIDType이고 값은 블로그글의 로우의 키입니다
            timeuuid_1 : i-got-a-new-guitar,
            timeuuid_2 : another-cool-guitar,
        },
        // 여기에 모든 블로그글이 저장됩니다
        __notag__ : {
            timeuuid_1b : i-got-a-new-guitar,

            // notice this is in the guitar Row as well
            timeuuid_2b : another-cool-guitar,

            // and this is in the movie Row as well
            timeuuid_2b : scream-is-the-best-movie-ever,
        },
        // "movie" 태그가 달린 글
        movie: {
            timeuuid_1c: scream-is-the-best-movie-ever
        }
    }

<ColumnFamily CompareWith="TimeUUIDType" Name="TaggedPosts"/>

댓글 컬럼패밀리(ColumnFamily)
이제 마지막으로 댓글의 설계 모델을 표현해 보겠습니다. 여기서는 슈퍼컬럼(SuperColumn)을 사용합니다. 하나의 로우는 하나의 블로그글을 의미합니다. 이 블로그 주소를 키로 갖는 요소 하위에 있는 녀석들을 슈퍼컬럼이라고 합니다. 말그대로 컬럼과 로우 키의 중간쯤에 있는 요소입니다. 슈퍼컬럼은 UUID값이 될것이며 댓글도 마찬가지로 시간순으로 정렬을 하기 위해 TimeUUIDType을 사용할 것입니다. 모든 댓글은 시간순으로 정렬이 될 것이며 이 안에 있는 각각의 컬럼들은 댓글에 표현되기 위한 정보가 들어가게됩니다. 생각보다 복잡한 이야기는 아닙니다.

<!--
    ColumnFamily: 댓글
    댓글은 여기에 저장됩니다.

    Row key => 블로그글의 로우 키
    SuperColumn name: TimeUUIDType

    Access: 블로그 글마다 작성된 모든 댓글을 가져옵니다
-->
    Comments : {
        // scream-is-the-best-movie-ever에 대한 모든 댓글들
        scream-is-the-best-movie-ever : { // 로우 키
            // 오래된 글이 먼저 나옵니다
            timeuuid_1 : { // 슈퍼컬럼의 이름
                // 이 댓글(슈퍼컬럼)의 모든 컬럼들
                commenter: Joe Blow,
                email: joeb@example.com,
                comment: you're a dumb douche, the godfather is the best movie ever
                commentTime: 1250438004
            },

            ... scream-is-the-best-movie-ever에대한 더 많은 댓글들

            // 가장 최근에 작성된 댓글
            timeuuid_2 : {
                commenter: Some Dude,
                email: sd@example.com,
                comment: be nice Joe Blow this isnt youtube
                commentTime: 1250557004
            },
        },

        // i-got-a-new-guitar에 달린 댓글
        i-got-a-new-guitar : {
            timeuuid_1 : { // 슈퍼컬럼의 이름
                commenter: Johnny Guitar,
                email: guitardude@example.com,
                comment: nice axe dawg...
                commentTime: 1250438004
            },
        }
    }

<ColumnFamily CompareWith="TimeUUIDType" ColumnType="Super"
    CompareSubcolumnsWith="BytesType" Name="Comments"/>

이제 마지막으로 위에서 언급된 모든 컬럼 패밀리들을 한데모아서 보도록 하죠.

<Keyspace Name="BloggyAppy">
    <ColumnFamily CompareWith="BytesType" Name="Authors"/>
    <ColumnFamily CompareWith="BytesType" Name="BlogEntries"/>
    <ColumnFamily CompareWith="TimeUUIDType" Name="TaggedPosts"/>
    <ColumnFamily CompareWith="TimeUUIDType" Name="Comments"
        CompareSubcolumnsWith="BytesType" ColumnType="Super"/>
</Keyspace>

참고:
http://arin.me/blog/wtf-is-a-supercolumn-cassandra-data-model (Thanks!)