2011년 6월 30일 목요일

Cassandra (5/12)

5. 카산드라 구조

이 장에서 우리는 카산드라가 어떻게 일하는지 이해하기 위해 그 디자인의 여러가지 면을 살펴보겠다. 우리는 peer-to-peer 디자인과 그 프로토콜을 고려하고 카산드라가 읽기, 쓰기 요청에 대해 어떻게 반응하는지 보고 확장성, 내구성, 유용성, 관리성 등의 구조적인 고려사항이 어떻게 영향을 받는지 살펴보겠다. 카산드라의 플랫폼 요청의 위임자로 동작하는Staged Event-Driven Architecture 의 채용도 논의한다.
카산드라의 구조는 정교하고 몇 가지 이론적인 구조에 의지하고 있다. 아직 알지 못하는 몇가지 용어를 참조하지 않고 또 새로운 용어 한 개를 논하는 것이 어렵다. 이는 조금 당황스러울수도 있기 때문에 후반에 Glossary를 넣었다.

5.1. 시스템 키스페이스

카산드라에는 system이라고 불리는 내부적 키스페이스가 있어서 클러스터가 동작에 도움을 주는 메타데이터를 저장하고 있다. Microsoft의 SQL서버에서 두 개의 메타 데이터베이스가 유지되는데 그것은 master와 tempdb이다. Master는 디스크 공간, 사용, 시스템 셋팅, 일반적인 서버 설치 노트 등의 정보가 저장된다. Tempdb는 중간 결과물들이나 일반적인 일들을 저장하는 워크스페이스이다. Oracle 데이터베이스는 항상 SYSTEM이라는 테이블 공간을 가지고 있으며 비슷한 목적으로 쓰인다. 카산드라 시스템의 키스페이스는 이것들과 유사하다.
특별히 시스템 키스페이스는 로컬 노드를 위한 메타데이터를 저장한다. 메타데이터는 다음을 포함한다.
 노드의 토큰
 클러스터의 이름
 키스페이스와 다이나믹 로딩을 지원하기 위한 스키마 정의
 마이그레이션 데이터
 노드가 bootstrapped 되었는지 여부
스키마 정의는 두개의 컬럼군에 저장된다. Schema 컬럼군은 사용자 키스페이스와 스키마 정의를 가지고 있고, Migrations 컬럼군은 키스페이스에 변경사항을 기록한다.
당신은 시스템 키스페이스를 변경하거나 쓸수는 없다.

5.2. Peer-to-Peer

전통적인 데이터베이스에서 MySQL처럼 다수의 기기에 배포될 수 있었다. 새로운 모델인 Google’s Bigtable 등도 마찬가지 였는데 어떤 노드들은 master로 어떤 것들은 slave로 설정되었다. 이것들은 클러스터 상에서 각각 다른 역할을 가지고 있었다. Master는 데이터의 소스이고 slave는 데이터들의 싱크를 master와 맞추었다. master에게 변경이 가해지면 slave에게도 전달이 되었다. 이러한 모델은 데이터를 읽는데 최적화되어있어서 어떤 slave로부터도 데이터를 읽을 수 있었다. 하지만 복사는 일방향으로서 master로부터 slave에게로 였다. 이것은 중요한 영향이 있는데 모든 쓰기는 master에게 보내져야 하며, 그것은 한 지점이 실패할 경우 위험하다는 것을 의미한다. Master/slave 셋팅에서 master 노드는 만약 오프라인이 된다면 많은 영향이 있을 수 밖에 없다.
반면에 카산드라는 peer-to-peer 분산 모델이어서 어느 노드이던지 동등하다. 그래서 master 노드가 별개로 존재하지는 않는다. 카산드라 디자인의 목적은 모든 시스템이 확장성이 쉽고 유용성이다. 만약 어떤 카산드라 노드를 오프라인 시킨다면 잠재적으로 모든 성능에 영향은 있겠지만 서비스를 불가능하게 하지는 않는다. 당신이 적당한 복사 전략을 쓰고 있다고 생각하면 어떤 노드에 문제가 생겼어도 그 데이터는 읽고 쓰기가 가능하다.
그리고 이 디자인은 카산드라에 새로운 노드를 추가하여 확장하기 쉽게한다. 각 노드의 동작은 같기 때문에 새로운 서버를 추가하기 위해서는 그냥 클러스터에 추가하면 된다. 처음에는 데이터를 받아들이고 링의 토폴로지를 배우기 위한 시간이 있어서 추가된 서버는 처음부터 모든 요청에 응답하는 것은 아니다. 이 과정을 마친후에는 링에 조인하고 요청을 받아들이게 된다. 이것은 설정은 최소로 필요로 하고 자동으로 이루어 진다. 이러한 이유 때문에 P2P 디자인은 확장 또는 축소하는 것을 master/slave 구조에서 보다 쉽게 만든다.

5.3. 소문과 실패 검출

비중앙화와 파티션 내구성을 지원하기 위해 카산드라는 gossip protocol 을 써서 링안에서의 통신과 각 노드들이 다른 노드들의 상태 정보를 갖고 있게 한다. Gossiper는 매우 바쁘게 움직인다. Anti-entropy 는 수동의 프로세스이어서 gossip에 의해 작동하지는 않는다.
Gossip 프로토콜(“epidemic protocol”로 불리기도 한다.)은 일반적으로 오류가 많은 네트워크를 가정하여 복사하는 분산 데이터베이스 환경에서 자동 메커니즘을 쓴다. 그것은 peer간에 필요한 정보를 교환하고 통신하기 때문에 그것에서 이름을 따왔다.
카산드라의 gossip 프로토콜은 org.apache.cassandra.gms.Gossiper 에 주로 구현되어 있으며 로컬 노드들간의 gossip을 관리하는게 주요임무이다. 서버 노드가 시작되었을 때 gossiper에 등록하여 각 종단의 상태 정보를 받아볼수 있게한다.
카산드라 gossip이 실패를 검출하는데 사용되기 때문에 Gossiper 클래스는 살아있고 죽은 노드들의 리스트를 관리하게 된다.
여기 gossiper가 작동하는 방식이 있다.
1. 주기적으로 (TimerTask에 설정한대로) G=gossiper 는 무작위로 링안의 노드를 골라서 gossip 세션을 초기화한다. 각 gossip의 주기는 세 개 메시지를 필요로 한다.
2. Gossio 초기화하는 것은 그 고른 친구에게 GossipDigestSynMessage를 보낸다.
3. 친구가 이 메시지를 받았을 때 GossipDigestAckMessage를 반환한다.
4. Ack 메시지를 받으면 GossipDigestAck2Message를 다시 보내서 종료한다.
Gossiper가 다른 종단이 죽었다고 결정되면 그것의 로컬 리스트에 죽었다고 선언하고 로깅한다.
카산드라는 분산컴퓨팅을 위한 유명한 알고리즘인 Phi Accrual Failure Detection으로 실패 검출을 잘 지원한다. 이 실패 검출 방법은 2004년 일본의 Advanced Institute of Science and Technology에 의한 것이다.
Accrual 실패 검출은 두 가지 주된 아이디어에 기반하였다. 첫번째 일반적인 아이디어는 실패 검출은 모니터링하는 애플리케이션에 decoupling 되어 유연해야 한다는 것이다. 두번째로 더욱 참신한 아이디어는 간단히 heartbeat 이라는 것을 두어 그에 따라 노드가 살았는지 죽었는지 판단할 수 있는것으로써 전통적인 실패 검출을 뒤집은 것이다. Accrual 실패 검출은 이 접근방법이 순진하고 죽었다는 것과 살아있다는 것 사이에 suspicion level 이라는 것을 두어 접근한다.
그러므로 실패 모니터링 시스템은 suspicion 레벨을 끊임없이 나타내어 노드가 죽었는지 잘 나타낸다. 이것은 네트워크 환경에서 계정의 변동을 나타내서 바람직하다. 예를 들어 한 개의 연결이 이상이 있다고 해서 전체 노드가 죽었다고 할 필요는 없는것이다. 이것은 단순한 두 가지 상태변화로 보는것보다 heartbeat의 샘플링을 통해 전체적으로 더 강하고 더 약한 실패의 가능성을 따져봄으로써 더 좋다.
실패 검출은 카산드라에서 org.apache.cassandra.gms.FailureDetector 클래스에 구현되어있고 이것은 org.apache.cassandra.gms.IFailureDetector 인터페이스를 구현한다. 이것들은 함께 아래 동작을 지원한다.
isAlive(InetAddress)
주어진 노드가 살아있는지 보고한다.
Interpret(InetAddress)
Suspicion 레벨에 근거해서 gossiper가 어떤 노드가 살았는지 죽었는지 결정하도록 한다.
Report(InetAddress)
어떤 노드가 heartbeat를 받았을 때 이 메서드를 부른다.

5.4. Anti-Entropy 와 Read Repair

당신이 gossip 프로토콜을 볼때는 종종 그에 카운터파트인 컴퓨팅의 유행성의 이론에 역시 기반한 anti-entropy 라는 이론을 보게될 것이다. Anti-entropy 는 카산드라에서 각 다른 노드들의 데이터가 최신으로 업데이트 되도록 하는 복사본 싱크 메커니즘이다.
여기 어떻게 작동하는지 본다. 주된 작동동안 서버는 Merkle 트리를 이웃 노드들과 교환하기 위해서 TreeRequest/TreeResponse 대화를 초기화한다. Merkle 트리는 컬럼 군에서 데이터를 표현하는 해쉬이다. 다른 노드의 트리와 맞지 않으면 그것이 셋팅되어야 하는 가장 최근의 데이터 값으로 repair 혹은 reconcile 하게 된다. 이 트리 비교는 org.apache.cassandra.service.AntiEntropyService 클래스가 하게 된다. AntiEntropyService 는 싱글톤 패턴을 구현하며 Differencer 클래스도 정의하는데 이것은 두 트리를 비교하여 무슨 차이점을 발견하면 repair를 시작하게 된다.
Anti-entropy 는 Amazon의 Dynamo에도 사용되며 카산드라 구현은 이를 모델링한다.
Dynamo는 Merkle 트리를 anti-entropy 를 위해 사용한다. 카산드라가 그것들을 역시 사용하며 하지만 구현은 조금 다르다. 카산드라에서 각 컬럼군은 각 고유의 Merkle 트리를 가진다. 트리는 각 비교 동작시에 snapshot 처럼 생성된다. 그리고 링에서 이웃하는 노드에게 보내져야 할 때 까지는 보관된다. 이 구현의 장점은 disk I/O를 줄인다는데 있다.
각 업데이트 후에 anti-entropy 알고리즘이 작동한다. 이것은 데이터베이스에 대하여 checksum을 수행하고 peer간 checksum을 비교한다. 만약 checksum이 다르면 데이터가 교환된다. 이것은 시스템이 불필요한 anti-entropy를 수행하지 않도록 peer들이 가장 최근의 업데이트를 받도록 시간 창을 사용하는 것이 필요하다. 동작을 빠르게 하기 위해 노드는 내부적으로 타임스탬프에 의한 변환된 인덱스를 유지하고 가장 최근의 업데이트만을 교환한다.
카산드라에서 클러스터를 이루기 위해 당신은 여러 개의 노드를 가지고 있고 한 개나 그 이상의 여러 개의 노드는 주어진 데이터의 복사본 처럼 작동한다. 이 데이터를 읽기 위해서 클라이언트는 클러스터 안의 어느 노드든과 연결하고 클라이언트에 의해 보여진 일관성 레벨에 따라 몇 개의 노드를 읽는다. 클라인언트의 일관성 레벨이 맞을 때까지 읽기 동작은 블록되어있다. 만약 어떤 노드가 기간이 지난 값으로 대답을 하면 카산드라는 클라이언트에게 가장 최근의 값을 돌려주게 된다. 반환한 후에는 카산드라는 read repair 라는 것을 백그라운드로 실행하게 된다. 이 동작은 값을 가장 최근의 값으로 유지한다.
카산드라에 의해 수행되는 이 디자인은 Project Voldemort 와 Riak 같은 straight key/value 를 저장한다. 이것은 클라이언트가 모든 노드가 읽혀질때까지 블록하지는 않지만 read repair 가 데이터를 신선하게 유지하는일을 백그라운드로 실행하기 때문에 퍼포먼스를 더 좋게하는 역할을 한다. 만약 클라이언트가 많다면 정족수로부터 읽어오는 것은 적어도 하나는 가장 최근의 값을 가지고 있다는 점을 분명히 하기 위해서 중요하다.
만약 클라이언트가 약한 일관성 레벨을 보여준다면 read repair는 클라이언트로 돌아온 후에 백그라운드로 실행하게 된다. 만약 당신이 더 강한 일관성 레벨을 사용한다면 read repair는 데이터가 클라이언트에게 반환되기 전에 실행된다.
만약 읽기 동작이 같은 타임스탬프에 저장이 되었으나 다른 값을 보여준다면 카산드라는 tie-breaking 메커니즘으로 값을 직접 비교하여 그 읽기가 무한 루프에 빠지지 않도록 해준다. 이런 케이스는 매우 드물다.

5.5. Memtables, SSTables, and Commit Logs

쓰기 동작을 수행할 때 이것은 바로 commit log에 기록된다. Commit log는 카산드라의 내구성 목표를 지원하기 위한 crash-recovery 메커니즘이다. 쓰기는 commit log에 기록될 때까지 성공했다고 여겨지지 않는다. 쓰기가 메모리 저장이 아직 안되었다는 것을 확실히 하여 아직 데이터를 복구할 수 있다.
Commit log에 쓰여진 후 값은 memtable이라고 불리는 메모리 데이터 구조에 저장된다. Memtable에 여러 개의 오브젝트가 저장되어 쓰레쉬 홀드를 넘어가면 memtable은 SSTable이라고 불리는 디스크의 한 파일로 배출된다. 새로운 memtable은 생성된다. 이 배출은 블록블럭 않는 동작이다. 한 컬럼군에 대해 여러 개의 memtable이 존재할 수 있다. 하나는 현재 동작하고 나머지는 배출되기를 기다린다. 이것은 보통은 과부하가 걸리지 않은 이상 노드가 그것을 빨리 배출하기 때문에 크게 오래 기다리지는 않는다.
각 commit log는 내부적으로 비트 플래그 가지고 있어서 배출이 필요한지 아닌지 나타낸다. 처음 쓰기 동작이 받아들여 졌을 때 commit log에 쓰여지고 비트 플래그는 1로 셋팅된다. 컬럼군에는 한 개의 비트 플래그가 있다. 왜냐하면 모든 서버에 대하여 한 개의 commit log가 쓰여지기 때문이다. 모든 컬럼군에의 쓰기동작은 같은 commit log로 간다. 그래서 비트 플래그는 특정한 commit log가 특정한 컬럼군에 대해서 배출되지 않았는지의 여부를 담고 있다. 한 번 memtable이 디스크로 잘 배출되면 해당하는 commit log의 비트 플래그는 0으로 셋팅된다. 이것은 commit log가 내구성을 위하여 더 이상 그 데이터를 보관하지 않아도 됨을 말한다. 일반적인 logfile처럼 commit log는 설정가능한 쓰레쉬 홀드를 가지며 한번 이 쓰레쉬 홀드가 도달하면 없어지도 다시 시작한다.
SSTable은 구글의 Bigtable에서 빌려온 개념이다. Memtable이 SSTable로 디스크에 배출되면 애플리케이션에 의해서 바뀔 수가 없다. SSTable이 꽉 압축되었다는 사실에도 불구하고 이 압축은 디스크상의 표현을 바꿀 뿐이다. 이것은 근본적으로 합치는 단계를 거쳐서 새파일을 만들고 기존에 성공했던 예전 파일을 지운다.
각 SSTable은 관련된 Bloom filter를 가진다. 이것은 추가적인 퍼포먼스 향상을 위해 쓰인다. 모든 쓰기 동작은 순차적이어서 카산드라에서 쓰기가 잘 동작되는 이유이다. 카산드라에서 모든 쓰기동작은 추가적인 동작이어서 다른 읽기 찾기 동작은 값을 쓰기 위해서 필요하지 않다. 이것은 한가지 디스크상의 속도 퍼포먼스에 제한사항을 가진다. 압축은 데이터의 재정렬을 분할상환하는데 순차적인 IO를 사용한다. 그래서 퍼포먼스 이득은 각 각 나누는 데서 생긴다. 쓰기 동작은 즉각적인 추가이다 그래서 압축은 차후 읽기의 퍼포먼스를 좋게한다. 카산드라가 값은 최종적으로 속한 곳에 삽입한다면 쓰기 클라이언트는 추후에 찾을 것이다.
읽기 동작에서 카산드라는 값을 찾기위해 memtable을 처음 찾을 것이다. Memtable은 org.apache.cassandra.db.Memtable 클래스에 의해 구현된다.

5.6. Hinted Handoff
다음의 시나리오를 고려해보자. 쓰기 요청이 카산드라에게 보내졌다. 그러나 네트워크 파티션이나 하드웨어 문제 등 이유로 해당하는 노드가 가용하지 않다. 이런 상황에서 일반적으로 가용성을 확실히 하기 위해서 카산드라는 hinted handoff라는 것을 구현했다. 당신은 hint라는 것이 쓰기 동작에서 어떤 정보를 담은 작은 노트정도로 생각할 수 있다. 쓰기가 되어야 할 노드가 실패한다면 쓰기를 받은 카산드라 노드는 hint를 만든다. 이것은 “나는 노드 B에 해당하는 쓰기 정보를 가지고 있다. 나는 노드 B가 온라인으로 돌아오면 쓰기 요청을 다시 보낼것이다.” 라는 리마인더와 같다. 이것은 노드 A가 노드B에게 쓰기에 대해서 hint를 준것이다.
이것은 카산드라가 항상 쓰기 가능하게 해준다. 그리고 실패한 노드가 온라인으로 되돌아 온 후에 일관성없는 시간을 줄여준다. 우리는 이전에 일관성 레벨에 관해서 논했다. 그리고 일관성 레벨 ANY 는 0.6 버전에서 추가되었고 hinted handoff 가 쓰기 동작의 성공에 대해 충분하다는 것을 기억할 것이다. 이것은 hint가 기록될 수 있어도 쓰기는 여전히 성공적이라고 보이는 것이다.
Hinted handoff에 대한 걱정이 카산드라 커뮤니티 멤버사이에 있었다. 처음에는 전체적인 내구성을 위해서 좋은 디자인이라고 보였다. 그리고 Java Message Service(JMS) 같은 많은 분산형 컴퓨팅 패러다임에 익숙했기 때문에 문제가 없다고 보였다. 배달이 보장된 JMS 큐에서 만약 메시지가 받는 이에게 전달이 되지 못하면 JMS는 잠시의 인터벌 동안 기다리고 받을 때까지 다시 보낸다. 하지만 JMS와 카산드라 hinted handoff 양쪽에는 실제적인 문제점이 있다. 만약 노드가 일정 시간동안 오프라인으로 되면 hint는 다른 노드에 많이 쌓이게 된다. 그리고 다른 노드가 실패한 노드가 다시 온라인으로 돌아온 것을 통보하면 그 노드는 많은 요청의 홍수에 둘러싸이고 위험한 상태가 된다.
이 걱정에 대하여 지금은 hinted handoff를 끌수도 있거나 좀 더 극적이지는 않게 프라이어리티를 줄여서 새로운 쓰기 요청보다 낮게 할 수 있다.

5.7. 압축

카산드라에서 압축 동작은 SSTable을 합치기 위해서 한다. 압축하는 동안 SSTable에 있는 데이터는 합쳐진다. Key도 합쳐진다. 컬럼도 합쳐지고 tombstone은 버려진다. 그리고 새로운 인덱스가 만들어진다.
압축은 많은 축적된 데이터를 합쳐줌으로써 빈 공간을 만드는 과정이다. 이것은 관계형 세상에서 table을 새로 만드는 과정과 대략 비슷하다. 그러나 Stu Hood가 지적했듯이 카산드라의 주된 다른점은 서버의 생명주기동안 분할 상환될 투명한 동작이다.
압축에서 합쳐진 데이터는 정렬된다. 새로운 인덱스는 정렬된 데이터에 대해 만들어진다. 새로 합쳐지고 정렬되고 인덱스가 만들어진 데이터는 한 개의 SSTable에 쓰여진다. 각 SSTable은 데이터, 인덱스, filter로 이루어 진다. 이 과정은 클래스 org.apache.cassandra.db.CompactionManager에 의해 관리된다. CompactionManager는 MBean을 구현한다.
압축의 또 다른 중요한 기능은 필요한 찾기의 수를 줄여서 퍼포먼스를 향상시키는 것이다. 주어진 key에 대해서 컬럼 데이터를 찾기 위해서는 일정 수의 SSTable을 찾아야 한다. Key가 자주 변형된다면 그 변형은 배출된 SSTable에서 종료될 것이다. 이것은 압축하는 것은 데이터베이스가 찾기를 실행해서 각 SSTable로부터 데이터를 가져올 필요가 없게 해준다.
카산드라에는 여러가지 다른 타입의 압축이 있다. 주된 압축은 한 두가지 방법에 의해 시작된다. 노드 탐색에 의하거나 자동으로 이루어지는것이다. 노드 탐색은 TreeRequest메시지를 타겟 주변의 노드에게 보낸다. 노드가 TreeRequest를 받았을 때 곧바로 read-only 압축을 실행하여 컬럼군을 검사한다.
Read-only 압축은 아래의 단계를 거친다.
1. 컬럼군에서 key 배분을 가져온다.
2. 행이 검사항목에 더해지면 만약 컬럼군이 검사되어야 하면 Merkle tree를 만들어 이웃한 노드들에게 브로드캐스팅한다.
3. Merkle tree가 Differences의 리스트와 함께 가져온다.
4. 비교가 StageManager에 의해서 실행되고 이것은 일을 실행하는데 동시성을 다루는데 책임이 있다. 이 케이스에서 StageManager는 Anti-Entropy Stage를 사용한다. 이것은 org.apache.cassadra.concurrent.JMXEnabledThreadPoolExecutor클래스를 사용하며 한 쓰레드안에서 압축을 실행하여 MBean 에서 처럼 동작이 가능하게 만든다.
당신은 압축 쓰레드의 priority를 줄여서 전반적인 퍼포먼스를 향상할 수 있다.
이것을 하기 위해서는 아래의 플래그를 사용한다.
-Dcassandra.compaction.priority=1
이것은 IO가 아니라 CPU 사용에 영향을 미친다.

5.8. Bloom Filters

Bloom filter는 퍼포먼스 향상기 처럼 사용된다. 그것을 발명한 Burton Bloom의 이름을 따서 이름지어 졌다. Bloom filter는 매우 빠르고 결정적이지 않은 알고리즘으로서 어떤 요소가 셋의 멤버인지 아닌지 테스트할 수 있다. 그것이 결정적이지 않다는 것은 Bloom filter로부터 false-positive 를 읽어올 수 있고 이것은 false-negative가 아니다. Bloom filter는 데이터의 값을 비트 어레이에 매핑하고 큰 데이터셋을 digest string에 매핑한다. Digest는 정의대로 원래의 데이터가 사용하는 것보다 적은 메모리를 쓴다. Filter는 메모리에 저장되며 key 를 찾을 때 데이터 접근을 줄임으로서 퍼포먼스를 향상시키는데 쓰인다. 디스크 접근은 일반적으로 메모리 접근보다 느리다. 그래서 Bloom filter는 cache의 특별한 한 종류이다. 쿼리가 실행될 때 Bloom filter는 디스크를 접근하기 전에 처음 체크된다. False-negative가 나오지 않기 때문에 filter가 그 인자가 셋안에 존재하지 않는다고 하면 그것은 확실히 없다. 그러나 필터가 엘리먼트가 셋안에 있다고 하면 디스크는 꼭 접근된다.
새로운 JMX MBean 이 Nodetool에 추가되어 false-positive의 수를 체크할수 있게 되면 당신의 Bloom filter는 반환된다. 이 동작은 getBloomFilterFalsePositives라고 불리운다.

5.9. Tombstones

관계형 세상에서 soft delete라는 아이디어에 익숙할 것이다. 실제 delete SQL문을 실행하는 대신에 애플리케이션은 delete라고 불리게 값을 업데이트 한다. 예를 들면 프로그래머는 때로 audit trail을 지원하기 위해 이것을 한다.
카산드라에는 tombstone이라는 비슷한 개념이 있다. 이것은 모든 delete가 일하는 방식이고 그래서 당신을 위해 자동으로 이루어질 것이다. 당신이 delete 동작을 실행할 때 데이터는 즉시 지워지지는 않는다. 그 대신 값에 tombstone을 놓는 업데이트 처럼 작동할 것이다. Tombstone은 지우기를 표시한 마크와 같은데 압축이 실행될 때까지 SSTable의 오래된 데이터를 억누를 필요가 있다.
Garbage Collection Grace Seconds라는 관련된 셋팅이 있다. 이것은 서버가 tombstone을 garbage collect 하기까지 기다리는 시간이다. 디폴트로 이것은 864,000초 즉 10일이다. 카산드라는 tombstone의 나이를 따라간다. 그리고 tombstone이 GCGraceSeconds보다 오래되면 garbage collect가 일어난다. 이 딜레이의 목적은 가용하지 않았던 노드에게 깨어날 시간을 주고 만약 노드가 이 시간보다 오래 다운되면 실패로 간주되고 교체된다.
0.7 버전에서 이 셋팅은 컬럼군당 설정이 가능하다.

5.10. Staged Event-Driven Architecture (SEDA)

카산드라는 Staged Event-Driven Architecture를 구현한다. SEDA는 매우 동시적인 인터넷 서비스를 위한 일반적인 구조이다. 원래는 2001년 “SEDA: An Architecture for Well-Conditioned, Scalable Internet Services”라는 논문에서 Matt Welsh, David Culler, Eric Brewer에 의해서 제안되었다.
일반적인 애플리케이션에서 한 가지의 일은 한 쓰레드안에서 이루어진다. 예를 들면 쓰기 동작은 같은 쓰레드 안에서 이루어진다. 그러나 카산드라는 다르다. 이것의 동시성 모델은 SEDA에 기반했다. 그래서 한 동작은 한 쓰레드에서 시작하여 다른 쓰레드로 그 일을 넘겨줄수도 있다. 하지만 현재 수행중인 쓰레드에게 그 책임이 달린 것은 아니고 일은 stage라고 불리는 것으로 나누어졌고 쓰레드 풀 (java.util.concurrent.ExecutorService) 에 의해 결정이 된다. Stage는 기본적인 일의 단위이다. 그리고 한 일은 내부적으로 한 stage에서 다른 것으로 넘어가게 된다. 다른 stage는 다른 쓰레드에 의해서 다루어 질수 있기 때문에, 카산드라는 퍼포먼스에 향상을 보게 되었다. SEDA 디자인은 그 자신의 리소스를 카산드라가 관리할 수 있다는 것을 의미한다. 왜냐하면 다른 동작은 디스크 IO, 혹은 CPU 제약, 네트워크 동작 등을 필요로 할 수 있기 때문에 풀은 자신의 일을 사용가능한 리소스에 따라 관리한다.
Stage는 들어오는 이벤트 큐와 이벤트 핸들러로 이루어 지며 관련된 쓰레드 풀이 있다. Stage는 스케줄링과 쓰레드 배분을 결정하는 콘트롤러에의해 관리된다. 카산드라는 일종의 동시성 모델을 구현하며 쓰레드 풀인 java.util.concurrent.ExecutorService를 사용한다. 이것이 어떻게 일하는 지 보기위해 org.apache.cassandra.concurrent.StageManager 클래스를 보라.
아래의 동작은 카산드라에서 stage로 표현된다.
 Read
 Mutation
 Gossip
 Response
 Anti-Entropy
 Load Balance
 Migration
 Streaming
몇 개의 추가적인 동작이 stage로 구현되어 있다. Memtable에 수행이 되는 일들의 단위를 위한 stage가 있고 Consistency Manager는 StorageService의 stage이다.
Stage는 IVerbHandler인터페이스를 구현하여 주어진 동사에 대한 기능을 지원한다. Mutation의 아이디어도 stage로 표현이 되므로 insert와 delete 양쪽 동작에서 역할이 있다.
SEDA 는 강력한 구조이다. 이벤트 드리븐 이기 때문에 이름이 말하듯이 업무들이 매우 뛰어난 동시성을 가지고 수행될 수 있다.

5.11. Manager 와 Service 들
카산드라의 기본 내부적 콘트롤 메커니즘을 구성하는 클래스들이 있다. 나는 여기서 그것들의 간단한 오버뷰를 보여주어 당신이 더 중요한 것들에 익숙해 질 수 있도록 할 것이다. 고려해야할 첫번째 것은 org.apache.cassandra.thrift.CassandraServer 클래스이다. 이 클래스는 Thrift 인터페이스로의 호출을 구현하고 org.apache.cassandra.service.StorageProxy 로 실행하기 위한 쿼리들의 노력을 대리한다.

5.11.1. 카산드라 데몬

Org.apache.cassandra.service.CassandraDaemon인터페이스는 한 노드에서 수행되는 카산드라 서비스의 라이프 사이클을 표현한다. 이것은 당신이 예상할수 있는 일반적인 라이프 사이클로서 start, stop, activate, deactivate, destroy 등이다.

5.11.2. 스토리지 서비스

카산드라 데이터베이스 서비스는 org.apache.cassandra.service.StorageService에 의해 표현된다. 스토리지 서비스는 노드의 토큰을 담고 있는데 그것은 노드가 책임이 있는 데이터의 범위를 나타내준다.
서버는 이 클래스의 initServer 메서드에 호출을 하면서 시작된다. 이때 서버는 SEDA 에 등록을 하고 state에 관한 몇 결정을 하게 된다. 그리고 그 자신을 JMXServer에 MBean으로 등록한다.

5.11.3. 메시징 서비스

Org.apache.cassandra.net.MessagingService 의 목적은 메시지 교환, 서비스의 인바운드 아웃바운드 메시지를 듣는 소켓을 만드는 것이다. MessagingService.listen 메서드는 쓰레드를 만든다. 각 들어오는 연결은 org.apache.cassandra.net.IncomingTcpConnection을 사용하여 ExecutorService 쓰레드를 접근한다. 메시지는 검사되고 이것이 스트리밍 메시지인지 아닌지 결정하게 된다. 메시지 스트리밍은 카산드라가 이 노드에서 다른 노드로 SSTable 파일을 보내기위한 최적화된 길이다. 다른 모든 노드들간의 커뮤니케이션은 직렬화된 메시지를 통해 일어난다. 그것이 만약 스트리밍이면 메시지는 IncomingStreamReader 으로 전달된다. 그것이 스트리밍이 아니면 메시지는 MessagingService 비직렬화 수행기에 의해 다루어진다. 그것은 메시지를 Runnable을 구현한 일의 형태로 다룬다. 이 서버가 많은 stage의 사용을 하고 풀은 MBean으로 쌓여있기 때문에 이 서비스가 어떻게 일하는지 발견할 수 있다.

5.11.4. Hinted Handoff 매니저

이름이 말하듯이 org.apache.cassandra.db.HintedHandoffManager는 hinted handoff를 내부적으로 관리하는 클래스이다. 그렇게 하기위해 쓰레드 풀을 가지고 있으며 그것은 JMX monitoring이 HINTED-HANDOFF-POOL 로 가능하다.

2011년 6월 28일 화요일

Cassandra, JPA, Spring

인터넷 상에서 많은 카산드라용 JPA 라이브러리 내지는 클라이언트를 발견할 수 있었다. 이에 그 내용을 어느 정도 정리하려고 한다. 대부분의 솔루션들이 기존 카산드라에서 단점으로 지적될 만한 사항을 기술하고 이를 해결하려고 본 소스들이 추가되었다고 주장한다.
먼저 살펴볼 것이 Kundera 라는 것이다. Kundera의 아이디어는 카산드라와 함께 간단하게 작동하게 하는것이라고 한다. 어떤 클라이언트 라이브러리를 새로 만든다기 보다 기존에 존재하는 라이브러리, 빌드들을 래핑하고 필요없는 코드들을 쓰지 않게 하여 더 깔끔하고 깨끗한 만들어서 프로그램의 질을 높인다고 한다. 그리고 물론 생산성을 높이는 것이다.
목적을 살펴보자면
 불필요한 디테일을 제거하는데, 그것은 컬럼 리스트, 수퍼 컬럼리스트, 바이트 어레이, 데이터 인코딩 등이라고 한다.
 어노테이션의 도움으로 도메인 모델과 직접적으로 일하게 한다.
 데이터 프로세싱의 흐름을 깨끗하고 분명하게 하기위해 “code plumbing”을 제거한다.
 카산드라와 애플리케이션 레벨의 로직을 분리하여 애플리케이션 개발을 편하게한다.
 비즈니스 레이어에서 기존의 것을 망치지 않은채 카산드라의 가장 최신 개발 사항을 포함시키려고 한다.

- 카산드라 데이터 모델
가장 기본적인 사항으로 카산드라는 당신의 데이터를 저장하기 위해 컬럼과 수퍼 컬럼을 가지고 있다. 컬럼은 이름들 그리고 타임스탬프의 값이다. 수퍼컬럼은 컬럼의 컬럼이다. 컬럼은 컬럼군에 저장되며, 수퍼 컬럼은 수퍼컬럼군에 저장된다. 가장 중요한 점은 카산드라는 기존의 관계형 데이터베이스가 아니어서 평평한 시스템이다. Join, 외래키가 없다. 당신이 저장하는 모든 것은 100% 비정규화되어있다.

- Kundera 사용하기
Kundera는 현재 JPA 1.0 과 호환된다. 다양한 어노테이션을 JPA 어노테이션위에 빌드하고 그것의 필요성을 만족시킨다.

- 기본 법칙
엔터티 클래스는 디폴트로 인자없는 생성자를 가진다.
엔터티 클래스는 @Entity 어노테이션을 보여야한다.
컬럼군의 엔터티 클래스는 @ColumnFamily 어노테이션이다.
수퍼컬럼군의 엔터티 클래스는 @SuperColumnFamily 이다.
각각 엔터티는 @ld 라는 필드 어노테이션을 가져야한다. @ld 필드는 스트링 타입이다. 엔터티당 한 개의 @ld 만 있어야 한다.
Kundera는 현재 프라퍼티 레벨에서 작동하므로 모든 메서드 레벨 어노테이션은 무시된다.

- 컬럼군의 법칙
1. @ColumnFamily 에서 컬럼군의 이름을 정의해야 한다. 예를 들면 @ColumnFamily(“Authors”) . Kundera는 이 엔터티 클래스를 “Authors” 컬럼군과 링크한다.
2. @ColumnFamily 로 어노테이션된 엔터티는 프라퍼티를 위해 스캔되며 @Column 어노테이션 때문이다.
3. 각 필드는 카산드라 컬럼이 되기 위해 분별된다.
4. 컬럼의 이름은 디폴트로 프로퍼티의 이름이다. 그러나 이름을 바꿀때는 오버라이드 할 수 있다.
5. Integer, String, Long, Date 타입의 프로퍼티는 지원되며, 나머지는 저장되기 전에 직렬화되고 읽히기 전에 비직렬화된다. 직렬화는 제한사항이 있는데 Kundera가 당신이 카산드라 컬럼 프로퍼티로 커스텀 오브젝트를 쓰는 것을 막는 이유이다. 그러나 당신은 당신이 원하는 대로 할 수는 있다.
6. Kundera 는 Collection과 Map 프로퍼티를 지원한다. 몇 가지 주의해야할 것이 있는데
* Collection과 Map 프로퍼티를 다음과 같이 초기화 해주어야한다.
List list = new ArrayList();
Set set = new HashSet();
Map map = new HashMap ();
* 타입 파라터는 5번에서 설명한 규칙을 지켜야한다.
* 타입 파라미터를 명시적으로 정의하지 않으면 엘레먼트는 직렬화되며 저장 및 복구되기전에 비직렬화된다.
* Collection 엘레먼트 순서가 유지된다는 보장은 없다.
* Collection과 Map은 그가 가진 엘레먼트의 숫자만큼 컬럼을 생성한다.
* Collection은 Name~0은 인덱스 0의 엘레먼트 Name~1은 인덱스1의 엘레먼트 이런 식으로 된다.
* Map 은 Name~key1 : key1의 엘레먼트 Name~key2: key2의 엘레먼트 등이다.

- SuperColumnFamily 의 법칙
1. @SuperColumnFamily에 수퍼컬럼군의 이름을 정의한다.
2. @SuperColumnFamily로 어노테이션된 엔터티는 @Column, @SuperColumn 의 두개 어노테이션을 위해 스캔된다.
3. 양쪽 어노테이션에 의해 어노테이션된 것만 선별되어 Column과 SuperColumn이 되도록 검사된다.
4. Column군에서 했던것처럼 이름을 정의할 수 있다.
5. 그러나 SuperColumn의 이름을 지정해야한다.
6. 나머지는 위와 같다.

- 5분내에 작동시키기
예제로서 배워보자. 간단한 Blog 애플리케이션을 만들어본다. 여기에는 Post, Tag, Author가 있다.
“Author”를 위한 카산드라 데이터모델은 아래와 같다.
01 ColumnFamily: Authors = {
02 “Eric Long”:{ // row 1
03 “email”:{
04 name:“email”,
05 value:“eric (at) long.com”
06 },
07 “country”:{
08 name:“country”,
09 value:“United Kingdom”
10 },
11 “registeredSince”:{
12 name:“registeredSince”,
13 value:“01/01/2002”
14 }
15 },
16 ...
17 }
“Posts”를 위한 데이터 모델은 다음과 같다.
01 SuperColumnFamily: Posts = {
02 “cats-are-funny-animals”:{ // row 1
03 “post” :{ // super-column
04 “title”:{
05 “Cats are funny animals”
06 },
07 “body”:{
08 “Bla bla bla… long story…”
09 }
10 “author”:{
11 “Ronald Mathies”
12 }
13 “created”:{
14 “01/02/2010"
15 }
16 },
17 “tags” :{
18 “0”:{
19 “cats”
20 }
21 “1”:{
22 “animals”
23 }
24 }
25 },
26 // row 2
27 }

카산드라 키스페이스 “Blog” 만들기
1
2
3
4
5 org.apache.cassandra.locator.RackUnawareStrategy
6 1
7 org.apache.cassandra.locator.EndPointSnitch
8


“Posts”를 위한 SuperColumnFamily 와 “Authors”를 위한 ColumnFamily 만들기
01
02
03
04
05
06
07 org.apache.cassandra.locator.RackUnawareStrategy
08 1
09 org.apache.cassandra.locator.EndPointSnitch
10




엔터티 클래스 만들기
Author.java
01 @Entity // makes it an entity class
02 @ColumnFamily ("Authors") // assign ColumnFamily type and name
03 public class Author {
04
05 @Id // row identifier
06 String username;
07
08 @Column (name="email") // override column-name
09 String emailAddress;
10
11 @Column
12 String country;
13
14 @Column (name="registeredSince")
15 Date registered;
16
17 String name;
18
19 public Author () { // must have a default constructor
20 }
21
22 ... // getters/setters etc.
23 }

Post.java
01 @Entity // makes it an entity class
02 @SuperColumnFamily("Posts") // assign column-family type and name
03 public class Post {
04
05 @Id // row identifier
06 String permalink;
07
08 @Column
09 @SuperColumn(column = "post") // column 'title' will be stored under super-column 'post'
10 String title;
11
12 @Column
13 @SuperColumn(column = "post")
14 String body;
15
16 @Column
17 @SuperColumn(column = "post")
18 String author;
19
20 @Column
21 @SuperColumn(column = "post")
22 Date created;
23
24 @Column
25 @SuperColumn(column = "tags") // column 'tag' will be stored under super-column 'tags'
26 List tags = new ArrayList();
27
28 public Post () { // must have a default constructor
29 }
30
31 ... // getters/setters etc.
32 }

“tags” 프로퍼티가 어떻게 초기화 되었는지 보자. 이것은 매우 중요한데 Kundera가 Java Reflection을 사용하여 읽고 엔터티 클래스를 만든다.

- 엔터티매니저 초기화하기
Kundera 는 이제 JPA 프로바이더처럼 작동하고 엔터티메니저를 초기화하는 방법을 보자.
http://anismiles.wordpress.com/2010/07/14/kundera-now-jpa-1-0-compatible/#entity-manager
- 지원되는 동작
Kundera는 JPA 엔터티메니저 동작을 지원한다. JPA 쿼리를 비롯해서 아래를 보자.
http://anismiles.wordpress.com/2010/07/14/kundera-now-jpa-1-0-compatible/#entity-operations

- Kundera 와 Spring을 사용하기
우리는 여기서 Kundera와 Spring을 이용하는 법을 본다. Spring과 Hibernate는 적은 자바 코드로서도 데이터베이스 코드를 접근하는 아주 강력한 프레임워크이다. Spring을 사용하는것은 또한 당신의 데이터베이스 동작을 위해서 간단히 unit test 를 쓸 수 있게 해준다.

- 컴포넌트들
1. Spring 3.0 호환 : applicationContext.xml
우리는 Kundera와 동작하기 위해서 최소한의 FactoryBean을 정의해야 한다. JPA interface와 호환하기 때문에 Kundera는 LocalContainerEntityManagerFactoryBean과 직접 사용할 수 있다.
<빈의 정의>



제공하여야할 유일한 프로퍼티는 퍼시스턴스 유닛 이름이다. 이것은 persistence.xml에 정의되어 있다. Hibernate와 작동하는 것처럼 Spring 컨테이너는 Kundera 엔터티 매니저를 LocalContainerEntityManagerFactoryBean 을 통해서 생성, 관리할 것이다.
1. JPA 설정 : persistence.xml
Kundera에는 퍼시스턴스 설정을 쓰는 몇 가지 방법이 있다. 여기서 JPA 퍼시스턴스 선언을 META-INF에 있는 persistence.xml을 통해서 해본다. 가장 장점은 투명한 설정을 가지고 있다는 것이다.
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">

com.impetus.kundera.ejb.KunderaPersistence











여기서 중요한 것은 제공자 클래스를 com.impetus.kundera.ejb.KunderaPersistence에서 준다는 것이다. 다른 프로퍼티 값은 카산드라에게 연결 파라미터를 준다. Kundera.annotations.scan.package 라는 프로퍼티는 JPA 어노테이션을 찾기위해 어느 패키지를 스캔해야 하는지 보여준다.
1. 엔터티 자바 클래스
여기서 우리는 우리의 엔터티를 JPA에서 데이터베이스를 위해 했듯이 정의해야 한다. 그러나 Kundera를 사용하는 것은 카산드라에게 더 많은 부가 정보를 준다.
package com.wix.model;

import com.impetus.kundera.api.ColumnFamily;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.xml.bind.annotation.XmlRootElement;

@Entity
@ColumnFamily(keyspace = "Keyspace1", family = "SimpleComment")
@XmlRootElement(name = "SimpleComment")
public class SimpleComment {

@Id
private String id;

@Column(name = "userId")
private String userId;

@Column(name = "comment")
private String commentText;

public SimpleComment() {
}

......
}
1. Kundera를 사용한 DAO 서비스
여기서 우리는 우리의 엔터티를 정의한다. @PersistenceContext라는 스프링 어노테이션을 간단히 사용할수 있으며 퍼시스턴스 엔터티를 삽입한다.
package com.wix.cassandra;

import com.wix.model.SimpleComment;
import org.springframework.stereotype.Service;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.List;

@Service
public class KuneraService {

@PersistenceContext(type = PersistenceContextType.EXTENDED)
private EntityManager entityManager;

public SimpleComment addComment(String id, String userId, String commentText) {
SimpleComment simpleComment = new SimpleComment();
simpleComment.setId(id);
simpleComment.setUserId(userId);
simpleComment.setCommentText(commentText);

entityManager.persist(simpleComment);
return simpleComment;
}

public SimpleComment getCommentById(String Id) {
SimpleComment simpleComment = entityManager.find(SimpleComment.class, Id);
return simpleComment;
}

public List getAllComments() {
Query query = entityManager.createQuery("SELECT c from SimpleComment c");
List list = query.getResultList();

return list;
}

}

@PersistenceContext를 선언하는 것은 매우 중요하며 이것은 스프링이 카산드라 클라이언트를 각 트랜잭션마다 닫는 것을 막는다. 이제 클라이언트를 직접 닫고 매니지하는 것이 필요하다.

2011년 6월 26일 일요일

Cassandra (4/12)

4. Sample 애플리케이션

이 장에서는 완전한 샘플 애플리케이션을 만들어서 모든 파트가 잘 맞는 것을 볼것이다. 데이터를 입력하고, 배치 업데이트를 실행하고 컬럼군과 super 컬럼군을 검색하는 등 API의 다양한 면을 실행하여 어떻게 동작하는지도 볼 것이다.
예제를 만들기 위하여, 다양한 데이터 구조와 기본 API의 동작들을 보여줄 만큼 복잡한 것들을 사용하기 원한다. 하지만 너무 자세한 것까지 다루어 당신을 지치게 하지는 않을 것이다. 데이터베이스에서 충분한 데이터를 갖기 위해서 우리의 검색을 제대로 돌아가게 할것이다. 데이터베이스의 밀도 때문에 거기는 조금 중복이 있을 것이다. 그리고 또 나는 카산드라가 어떻게 동작하는지에 집중할 수 있도록 익숙한 도메인을 사용해서 애플리케이션 도메인에 관해서 신경쓰지 않도록 할 것이다.

4.1. 데이터 디자인

당신이 관계형 데이터베이스를 사용하는 새 데이터 드리븐 애플리케이션을 빌드하려고 할 때, 당신은 적당히 정규화된 테이블과 같이 도메인을 모델링하거나 다른 테이블의 관계형 데이터를 참조하기위해 외래 키를 사용함으로써 시작할 수 있다. 이제 카산드라가 어떻게 데이터를 저장하는지 알므로 관계형 세상에서도 이해하기 쉬운 작은 도메인 모델을 만든다. 그리고 카산드라에서 관계형에서 분산 해쉬테이블 모델로 어떻게 매핑하는지 본다.
관계형 모델링은 간단히 말해서 당신이 개념적인 도메인에서 시작해서 도메인에 있는 명사들을 테이블에 표현하는 것이다. 당신은 primary key와 외래키를 모델 관계에 할당한다. 당신이 다대다 관계를 가졌을 때 그 키들을 표현할 join table을 만든다. Join table은 실제 세계에는 존재하지 않는다. 그리고 관계형 모델들이 작동하는데 필요한 영향을 미친다. 당신이 모든 테이블들을 만들어 놓은 후에 키에 의해 정의된 관계를 사용하는 흩어진 데이터를 소집하는 쿼리를 쓸 수 있다. 관계형 세상에서의 쿼리는 부차적이다. 테이블이 옳게 모델이 되어있다면 당신이 원하는 데이터를 언제나 가져올 수 있다고 가정한다. 당신이 여러 개의 복잡한 서브쿼리나 join statement를 사용해도 이것은 진실이다.
반면에 카산드라에서 당신은 데이터 모델로부터 시작하지는 않고 쿼리 모델로부터 시작한다.
이 예를 위해 쉽게 이해되고 모두 관계될 수 있는 도메인을 사용해보자. 어떤 호텔이 있고 손님이 예약을 할 수 있도록 하는 예이다.
우리의 개념적인 도메인은 호텔과 거기에 묵는 손님, 각 호텔의 방들, 어떤 방에 어떤 손님이 얼마기간 동안 묵는다는 예약 기록 등이있다. 호텔은 일반적으로 흥미있는 지역을 모아 유지하고 있다. 이것은 공원, 박물관, 쇼핑 갤러리, 기념물, 다른 장소 등 호텔 근처의 손님들이 그들이 머무는 동안 방문해 볼 만한 곳이다. 호텔과 이 장소들은 모두 지정학 위치 데이터를 가지고 지도 상에서 매쉬업, 거리 측정등을 통해 발견되기를 바란다.
여기서 카산드라의 애플리케이션 디자인을 해보겠다. 첫째, 당신의 쿼리를 정한다. 아래와 같은 것들이 있을 것이다.
 주어진 지역의 호텔을 찾는다.
 이름, 위치 등 호텔에 대한 정보를 찾는다.
 주어진 호텔 근처에 흥미로운 장소를 찾는다.
 생각하고 있는 기간 동안에 사용가능한 방이 있는지 찾는다.
 방의 가격과 편의 시설 등을 찾는다.
 손님 정보란에 들어가서 선택한 방을 예약한다.

4.2. 호텔 애플리케이션 RDBMS 디자인
그림 4-1은 우리가 어떻게 간단히 호텔 예약 시스템을 관계형 데이터베이스 모델을 사용하여 만들수 있는지 보여준다. 관계형 모델은 몇 개의 “join” 테이블을 포함한다. 이는 다대다 관계들 해결하기 위함인데 이는 호텔 대 흥미로운 장소, 방 대 편의 시설 등이다.

그림4-1. RDBMS를 사용한 간단한 호텔 검색 시스템

4.3. 호텔 애플리케이션 카산드라 디자인

여러가지 방법이 있겠지만, 우리는 여기서 그림 4-2에 보여진 피지컬한 카산드라 모델을 사용하여 논리적인 데이터 모델을 나타내 본다.
이 디자인에서 우리는 관계형 디자인에서 했던것처럼 해본다. 호텔, 손님 같은 테이블들을 컬럼군으로 변환해본다. PointOfInterest 같은 다른 테이블은 super 컬럼군으로 비정규화되었다. 관계형 모델에서 SQL 문장을 이용해서 도시 이름을 사용하여 호텔을 찾아볼 수 있다. 그러나 카산드라에 SQL 없기 때문에 HotelByCity 컬럼군 형태로 인덱스를 만들었다.
우리는 방과 편의시설을 합쳐서 Room이라는 하나의 컬럼군으로 만들었다. 타입, 요금 등의 컬럼은 해당하는 값을 가지고 있다. 뜨거운 욕조 등의 컬럼은 단지 컬럼이름 자체의 존재만을 사용할 것이며, 안그러면 비어있다.

그림4-2. 카산드라 모델로 표현된 호텔 검색

4.4. 호텔 애플리케이션 코드
이 섹션에서는 우리는 코드를 살펴보고 주어진 디자인을 어떻게 구현할지 본다. 여러가지 다른 API의 동작을 보여주기 때문에 이것은 유용하다.
우리가 만드는 애플리케이션은 아래와 같은 일을 수행할 것이다.
1. 데이터베이스 구조를 만든다.
2. 호텔과 흥미로운 장소 등의 데이터를 가지고 데이터베이스를 미리 만든다. 호텔은 표준 컬럼군에 저장이 될 것이고 다른 흥미로운 장소는 super 컬럼군에 저장된다.
3. 주어진 도시에서 호텔의 리스트를 검색한다. 이것은 두번째 인덱스를 사용한다.
4. 검색에서 반환된 호텔을 고른다. 그리고 고른 호텔 주변의 흥미로운 장소의 리스트를 검색한다.
5. Reservation 컬럼에 삽입 동작을 해서 예약을 한다.
모든 애플리케이션을 구현하기에는 공간이 모자란다. 그러나 주된 부분만 하고 나머지는 같은 것의 반복일 뿐인 구현은 놔둔다.

4.5. 데이터베이스 만들기

첫번째 단계는 스키마 정의를 만드는것이다. 이 예제에서 우리는 클라이언트 코드를 정의하기 위해 사용할 수도 있지만 스키마를 YAML로 정의하고 로드한다.
YAML 파일은 아래 Example 4-1에 보여지며 필요한 키공간과 컬럼군을 정의했다.

Example 4-1. Cassandra.yaml 스키마 정의
keyspaces:
- name: Hotelier
replica_placement_strategy: org.apache.cassandra.locator.RackUnawareStrategy
replication_factor: 1
column_families:
- name: Hotel
compare_with: UTF8Type
- name: HotelByCity
compare_with: UTF8Type
- name: Guest
compare_with: BytesType
- name: Reservation
compare_with: TimeUUIDType
- name: PointOfInterest
column_type: Super
compare_with: UTF8Type
compare_subcolumns_with: UTF8Type
- name: Room
column_type: Super
compare_with: BytesType
compare_subcolumns_with: BytesType
- name: RoomAvailability
column_type: Super
compare_with: BytesType
compare_subcolumns_with: BytesType

이 정의는 예제를 작동하기 위한 모든 컬럼군을 제공한다. 그리고 RDBMS에서 변환되었기 때문에 애플리케이션 코드에서 직접적으로 참조하지는 않는것도 있다.

4.5.1. 스키마 로딩하기

YAML에 스키마가 정의되면 로드를 해야한다. 이것을 하기위해서는 console을 열고 jconsole 애플리케이션을 실행한다. 그리고 카산드라에 JMX를 통해 연결한다. 그리고 loadSchemaFromYAML 이라는 동작을 실행한다. 이것은 org.apache.cassandra.service.StorageService MBean 의 일부이다. 이제 카산드라는 당신의 스키마를 알고 그것을 사용하기 시작한다. 당신은 또한 API를 사용하고 키스페이스와 컬럼군을 만들수 있다.

4.6. 데이터 구조

애플리케이션은 단지 변환 오브젝트처럼 우리를 위해 작동할 표준 데이터 구조를 필요로 한다. 이것은 특별히 흥미롭지는 않지만 이것저것이 잘 정돈되어 있기 위해 필요하다. Hotel 데이터 구조를 사용하여 모든 호텔관련 정보를 보관하여 다음에 보여준다.
package com.cassandraguide.hotel;
//data transfer object
public class Hotel {
public String id;
public String name;
public String phone;
public String address;
public String city;
public String state;
public String zip;
}

이 구조는 애플리케이션의 편리함을 위하여 컬럼 정보만을 가지고 있다.
흥미로운 장소 정보를 갖기 위하여 POI 데이터 구조도 가지고 있다. 다음에 보여준다.
package com.cassandraguide.hotel;
//data transfer object for a Point of Interest
public class POI {
public String name;
public String desc;
public String phone;
}

Constants 클래스도 가지고 있고 이는 변경하기 쉬운 장소에 공통으로 사용되는 스트링을 보관한다. 다음에 보여준다.
package com.cassandraguide.hotel;
import org.apache.cassandra.thrift.ConsistencyLevel;
public class Constants {
public static final String CAMBRIA_NAME = "Cambria Suites Hayden";
public static final String CLARION_NAME= "Clarion Scottsdale Peak";
public static final String W_NAME = "The W SF";
public static final String WALDORF_NAME = "The Waldorf=Astoria";
public static final String UTF8 = "UTF8";
public static final String KEYSPACE = "Hotelier";
public static final ConsistencyLevel CL = ConsistencyLevel.ONE;
public static final String HOST = "localhost";
public static final int PORT = 9160;
}

공통 사용 스트링을 이렇게 보관하는 것은 코드를 더 깨끗하고 간결하게 해주고 당신의 환경에서 의미가 있도록 값들을 변경하는 것이 쉽게 한다.

4.7. 커넥션 맺기

우리는 너무 많이 쓸데없이 반복하는 것을 피하고 편리를 위해 커넥션 코드를 한 클래스에 넣고 이를 Connector라고 부른다. 다음에 보여준다.
package com.cassandraguide.hotel;
import static com.cassandraguide.hotel.Constants.KEYSPACE;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.InvalidRequestException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
//simple convenience class to wrap connections, just to reduce repeat code
public class Connector {
TTransport tr = new TSocket("localhost", 9160);
// returns a new connection to our keyspace
public Cassandra.Client connect() throws TTransportException,
TException, InvalidRequestException {
TFramedTransport tf = new TFramedTransport(tr);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
tr.open();
client.set_keyspace(KEYSPACE);
return client;
}
public void close() {
tr.close();
}
}

데이터 베이스 동작을 수행할 필요가 있을 때 우리는 커넥션을 맺고 끊기 위해서 이 클래스를 사용할 수 있다.

4.8. 데이터베이스 만들기

Prepopulate 클래스는 다음에 보여지며 사용자가 검색할 호텔, 흥미로운 장소 정보를 데이터베이스에 미리 만들기위해서 insert, batch_mutates 등을 수행한다.
package com.cassandraguide.hotel;
import static com.cassandraguide.hotel.Constants.CAMBRIA_NAME;
import static com.cassandraguide.hotel.Constants.CL;
import static com.cassandraguide.hotel.Constants.CLARION_NAME;
import static com.cassandraguide.hotel.Constants.UTF8;
import static com.cassandraguide.hotel.Constants.WALDORF_NAME;
import static com.cassandraguide.hotel.Constants.W_NAME;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Clock;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ColumnPath;
import org.apache.cassandra.thrift.Mutation;
import org.apache.cassandra.thrift.SuperColumn;
import org.apache.log4j.Logger;
/**
* Performs the initial population of the database.
* Fills the CFs and SCFs with Hotel, Point of Interest, and index data.
* Shows batch_mutate and insert for Column Families and Super Column Families.
*
* I am totally ignoring exceptions to save space.
*/
public class Prepopulate {
private static final Logger LOG = Logger.getLogger(Prepopulate.class);
private Cassandra.Client client;
private Connector connector;
//constructor opens a connection so we don't have to
//constantly recreate it
public Prepopulate() throws Exception {
connector = new Connector();
client = connector.connect();
}
void prepopulate() throws Exception {
//pre-populate the DB with Hotels
insertAllHotels();
//also add all hotels to index to help searches
insertByCityIndexes();
//pre-populate the DB with POIs
insertAllPointsOfInterest();
connector.close();
}
//also add hotels to lookup by city index
public void insertByCityIndexes() throws Exception {
String scottsdaleKey = "Scottsdale:AZ";
String sfKey = "San Francisco:CA";
String newYorkKey = "New York:NY";
insertByCityIndex(scottsdaleKey, CAMBRIA_NAME);
insertByCityIndex(scottsdaleKey, CLARION_NAME);
insertByCityIndex(sfKey, W_NAME);
insertByCityIndex(newYorkKey, WALDORF_NAME);
}
//use Valueless Column pattern
private void insertByCityIndex(String rowKey, String hotelName)
throws Exception {
Clock clock = new Clock(System.nanoTime());
Column nameCol = new Column(hotelName.getBytes(UTF8),
new byte[0], clock);
ColumnOrSuperColumn nameCosc = new ColumnOrSuperColumn();
nameCosc.column = nameCol;
Mutation nameMut = new Mutation();
nameMut.column_or_supercolumn = nameCosc;
//set up the batch


List cols = new ArrayList();
cols.add(nameMut);
String columnFamily = "HotelByCity";
muts.put(columnFamily, cols);
//outer map key is a row key
//inner map key is the column family name
mutationMap.put(rowKey, muts);
//create representation of the column
ColumnPath cp = new ColumnPath(columnFamily);
cp.setColumn(hotelName.getBytes(UTF8));
ColumnParent parent = new ColumnParent(columnFamily);
//here, the column name IS the value (there's no value)
Column col = new Column(hotelName.getBytes(UTF8), new byte[0], clock);
client.insert(rowKey.getBytes(), parent, col, CL);
LOG.debug("Inserted HotelByCity index for " + hotelName);
} //end inserting ByCity index
//POI
public void insertAllPointsOfInterest() throws Exception {
LOG.debug("Inserting POIs.");
insertPOIEmpireState();
insertPOICentralPark();
insertPOIPhoenixZoo();
insertPOISpringTraining();
LOG.debug("Done inserting POIs.");
}
private void insertPOISpringTraining() throws Exception {


List columnsToAdd = new ArrayList();
Clock clock = new Clock(System.nanoTime());
String keyName = "Spring Training";
Column descCol = new Column("desc".getBytes(UTF8),
"Fun for baseball fans.".getBytes("UTF-8"), clock);
Column phoneCol = new Column("phone".getBytes(UTF8),
"623-333-3333".getBytes(UTF8), clock);
List cols = new ArrayList();
cols.add(descCol);
cols.add(phoneCol);

Mutation columns = new Mutation();
ColumnOrSuperColumn descCosc = new ColumnOrSuperColumn();
SuperColumn sc = new SuperColumn();
sc.name = CAMBRIA_NAME.getBytes();
sc.columns = cols;
descCosc.super_column = sc;
columns.setColumn_or_supercolumn(descCosc);
columnsToAdd.add(columns);
String superCFName = "PointOfInterest";
ColumnPath cp = new ColumnPath();
cp.column_family = superCFName;
cp.setSuper_column(CAMBRIA_NAME.getBytes());
cp.setSuper_columnIsSet(true);
innerMap.put(superCFName, columnsToAdd);
outerMap.put(keyName.getBytes(), innerMap);
client.batch_mutate(outerMap, CL);
LOG.debug("Done inserting Spring Training.");
}
private void insertPOIPhoenixZoo() throws Exception {

List columnsToAdd = new ArrayList();
long ts = System.currentTimeMillis();
String keyName = "Phoenix Zoo";
Column descCol = new Column("desc".getBytes(UTF8),
"They have animals here.".getBytes("UTF-8"), new Clock(ts));
Column phoneCol = new Column("phone".getBytes(UTF8),
"480-555-9999".getBytes(UTF8), new Clock(ts));
List cols = new ArrayList();
cols.add(descCol);
cols.add(phoneCol);

String cambriaName = "Cambria Suites Hayden";
Mutation columns = new Mutation();
ColumnOrSuperColumn descCosc = new ColumnOrSuperColumn();
SuperColumn sc = new SuperColumn();
sc.name = cambriaName.getBytes();
sc.columns = cols;
descCosc.super_column = sc;
columns.setColumn_or_supercolumn(descCosc);
columnsToAdd.add(columns);
String superCFName = "PointOfInterest";
ColumnPath cp = new ColumnPath();
cp.column_family = superCFName;
cp.setSuper_column(cambriaName.getBytes());
cp.setSuper_columnIsSet(true);
innerMap.put(superCFName, columnsToAdd);
outerMap.put(keyName.getBytes(), innerMap);
client.batch_mutate(outerMap, CL);
LOG.debug("Done inserting Phoenix Zoo.");
}
private void insertPOICentralPark() throws Exception {

List columnsToAdd = new ArrayList();
Clock clock = new Clock(System.nanoTime());
String keyName = "Central Park";
Column descCol = new Column("desc".getBytes(UTF8),
"Walk around in the park. It's pretty.".getBytes("UTF-8"), clock);
//no phone column for park
List cols = new ArrayList();
cols.add(descCol);

Mutation columns = new Mutation();
ColumnOrSuperColumn descCosc = new ColumnOrSuperColumn();
SuperColumn waldorfSC = new SuperColumn();
waldorfSC.name = WALDORF_NAME.getBytes();
waldorfSC.columns = cols;
descCosc.super_column = waldorfSC;
columns.setColumn_or_supercolumn(descCosc);
columnsToAdd.add(columns);
String superCFName = "PointOfInterest";
ColumnPath cp = new ColumnPath();
cp.column_family = superCFName;
cp.setSuper_column(WALDORF_NAME.getBytes());
cp.setSuper_columnIsSet(true);
innerMap.put(superCFName, columnsToAdd);
outerMap.put(keyName.getBytes(), innerMap);
client.batch_mutate(outerMap, CL);
LOG.debug("Done inserting Central Park.");
}
private void insertPOIEmpireState() throws Exception {

List columnsToAdd = new ArrayList();
Clock clock = new Clock(System.nanoTime());
String esbName = "Empire State Building";
Column descCol = new Column("desc".getBytes(UTF8),
"Great view from 102nd floor.".getBytes("UTF-8"), clock);
Column phoneCol = new Column("phone".getBytes(UTF8),
"212-777-7777".getBytes(UTF8), clock);
List esbCols = new ArrayList();
esbCols.add(descCol);
esbCols.add(phoneCol);

Mutation columns = new Mutation();
ColumnOrSuperColumn descCosc = new ColumnOrSuperColumn();
SuperColumn waldorfSC = new SuperColumn();
waldorfSC.name = WALDORF_NAME.getBytes();
waldorfSC.columns = esbCols;
descCosc.super_column = waldorfSC;
columns.setColumn_or_supercolumn(descCosc);
columnsToAdd.add(columns);
String superCFName = "PointOfInterest";
ColumnPath cp = new ColumnPath();
cp.column_family = superCFName;
cp.setSuper_column(WALDORF_NAME.getBytes());
cp.setSuper_columnIsSet(true);
innerMap.put(superCFName, columnsToAdd);
outerMap.put(esbName.getBytes(), innerMap);
client.batch_mutate(outerMap, CL);
LOG.debug("Done inserting Empire State.");
}
//convenience method runs all of the individual inserts
public void insertAllHotels() throws Exception {
String columnFamily = "Hotel";
//row keys
String cambriaKey = "AZC_043";
String clarionKey = "AZS_011";
String wKey = "CAS_021";
String waldorfKey = "NYN_042";
//conveniences

createWMutation(columnFamily, wKey);
client.batch_mutate(cambriaMutationMap, CL);
LOG.debug("Inserted " + cambriaKey);
client.batch_mutate(clarionMutationMap, CL);
LOG.debug("Inserted " + clarionKey);
client.batch_mutate(wMutationMap, CL);
LOG.debug("Inserted " + wKey);
client.batch_mutate(waldorfMutationMap, CL);
LOG.debug("Inserted " + waldorfKey);
LOG.debug("Done inserting at " + System.nanoTime());
}
//set up columns to insert for W

String columnFamily, String rowKey)
throws UnsupportedEncodingException {
Clock clock = new Clock(System.nanoTime());
Column nameCol = new Column("name".getBytes(UTF8),
W_NAME.getBytes("UTF-8"), clock);
Column phoneCol = new Column("phone".getBytes(UTF8),
"415-222-2222".getBytes(UTF8), clock);
Column addressCol = new Column("address".getBytes(UTF8),
"181 3rd Street".getBytes(UTF8), clock);
Column cityCol = new Column("city".getBytes(UTF8),
"San Francisco".getBytes(UTF8), clock);
Column stateCol = new Column("state".getBytes(UTF8),
"CA".getBytes("UTF-8"), clock);
Column zipCol = new Column("zip".getBytes(UTF8),
"94103".getBytes(UTF8), clock);
ColumnOrSuperColumn nameCosc = new ColumnOrSuperColumn();
nameCosc.column = nameCol;
ColumnOrSuperColumn phoneCosc = new ColumnOrSuperColumn();
phoneCosc.column = phoneCol;
ColumnOrSuperColumn addressCosc = new ColumnOrSuperColumn();
addressCosc.column = addressCol;
ColumnOrSuperColumn cityCosc = new ColumnOrSuperColumn();
cityCosc.column = cityCol;
ColumnOrSuperColumn stateCosc = new ColumnOrSuperColumn();
stateCosc.column = stateCol;
ColumnOrSuperColumn zipCosc = new ColumnOrSuperColumn();
zipCosc.column = zipCol;
Mutation nameMut = new Mutation();
nameMut.column_or_supercolumn = nameCosc;
Mutation phoneMut = new Mutation();
phoneMut.column_or_supercolumn = phoneCosc;
Mutation addressMut = new Mutation();
addressMut.column_or_supercolumn = addressCosc;
Mutation cityMut = new Mutation();
cityMut.column_or_supercolumn = cityCosc;
Mutation stateMut = new Mutation();
stateMut.column_or_supercolumn = stateCosc;
Mutation zipMut = new Mutation();
zipMut.column_or_supercolumn = zipCosc;
//set up the batch

List cols = new ArrayList();
cols.add(nameMut);
cols.add(phoneMut);
cols.add(addressMut);
cols.add(cityMut);
cols.add(stateMut);
cols.add(zipMut);
muts.put(columnFamily, cols);
//outer map key is a row key
//inner map key is the column family name
mutationMap.put(rowKey.getBytes(), muts);
return mutationMap;
}
//add Waldorf hotel to Hotel CF
createWaldorfMutation(
String columnFamily, String rowKey)
throws UnsupportedEncodingException {
Clock clock = new Clock(System.nanoTime());
Column nameCol = new Column("name".getBytes(UTF8),
WALDORF_NAME.getBytes("UTF-8"), clock);
Column phoneCol = new Column("phone".getBytes(UTF8),
"212-555-5555".getBytes(UTF8), clock);
Column addressCol = new Column("address".getBytes(UTF8),
"301 Park Ave".getBytes(UTF8), clock);
Column cityCol = new Column("city".getBytes(UTF8),
"New York".getBytes(UTF8), clock);
Column stateCol = new Column("state".getBytes(UTF8),
"NY".getBytes("UTF-8"), clock);
Column zipCol = new Column("zip".getBytes(UTF8),
"10019".getBytes(UTF8), clock);
ColumnOrSuperColumn nameCosc = new ColumnOrSuperColumn();
nameCosc.column = nameCol;
ColumnOrSuperColumn phoneCosc = new ColumnOrSuperColumn();
phoneCosc.column = phoneCol;
ColumnOrSuperColumn addressCosc = new ColumnOrSuperColumn();
addressCosc.column = addressCol;
ColumnOrSuperColumn cityCosc = new ColumnOrSuperColumn();
cityCosc.column = cityCol;
ColumnOrSuperColumn stateCosc = new ColumnOrSuperColumn();
stateCosc.column = stateCol;
ColumnOrSuperColumn zipCosc = new ColumnOrSuperColumn();
zipCosc.column = zipCol;
Mutation nameMut = new Mutation();
nameMut.column_or_supercolumn = nameCosc;
Mutation phoneMut = new Mutation();
phoneMut.column_or_supercolumn = phoneCosc;
Mutation addressMut = new Mutation();
addressMut.column_or_supercolumn = addressCosc;
Mutation cityMut = new Mutation();
cityMut.column_or_supercolumn = cityCosc;
Mutation stateMut = new Mutation();
stateMut.column_or_supercolumn = stateCosc;
Mutation zipMut = new Mutation();
zipMut.column_or_supercolumn = zipCosc;
//set up the batch

List cols = new ArrayList();
cols.add(nameMut);
cols.add(phoneMut);
cols.add(addressMut);
cols.add(cityMut);
cols.add(stateMut);
cols.add(zipMut);
muts.put(columnFamily, cols);
//outer map key is a row key
//inner map key is the column family name
mutationMap.put(rowKey.getBytes(), muts);
return mutationMap;
}
//set up columns to insert for Clarion
createClarionMutation(
String columnFamily, String rowKey)
throws UnsupportedEncodingException {
Clock clock = new Clock(System.nanoTime());
Column nameCol = new Column("name".getBytes(UTF8),
CLARION_NAME.getBytes("UTF-8"), clock);
Column phoneCol = new Column("phone".getBytes(UTF8),
"480-333-3333".getBytes(UTF8), clock);
Column addressCol = new Column("address".getBytes(UTF8),
"3000 N. Scottsdale Rd".getBytes(UTF8), clock);
Column cityCol = new Column("city".getBytes(UTF8),
"Scottsdale".getBytes(UTF8), clock);
Column stateCol = new Column("state".getBytes(UTF8),
"AZ".getBytes("UTF-8"), clock);
Column zipCol = new Column("zip".getBytes(UTF8),
"85255".getBytes(UTF8), clock);
ColumnOrSuperColumn nameCosc = new ColumnOrSuperColumn();
nameCosc.column = nameCol;
ColumnOrSuperColumn phoneCosc = new ColumnOrSuperColumn();
phoneCosc.column = phoneCol;
ColumnOrSuperColumn addressCosc = new ColumnOrSuperColumn();
addressCosc.column = addressCol;
ColumnOrSuperColumn cityCosc = new ColumnOrSuperColumn();
cityCosc.column = cityCol;
ColumnOrSuperColumn stateCosc = new ColumnOrSuperColumn();
stateCosc.column = stateCol;
ColumnOrSuperColumn zipCosc = new ColumnOrSuperColumn();
zipCosc.column = zipCol;
Mutation nameMut = new Mutation();
nameMut.column_or_supercolumn = nameCosc;
Mutation phoneMut = new Mutation();
phoneMut.column_or_supercolumn = phoneCosc;
Mutation addressMut = new Mutation();
addressMut.column_or_supercolumn = addressCosc;
Mutation cityMut = new Mutation();
cityMut.column_or_supercolumn = cityCosc;
Mutation stateMut = new Mutation();
stateMut.column_or_supercolumn = stateCosc;
Mutation zipMut = new Mutation();
zipMut.column_or_supercolumn = zipCosc;
//set up the batch

List cols = new ArrayList();
cols.add(nameMut);
cols.add(phoneMut);
cols.add(addressMut);
cols.add(cityMut);
cols.add(stateMut);
cols.add(zipMut);
muts.put(columnFamily, cols);
//outer map key is a row key
//inner map key is the column family name
mutationMap.put(rowKey.getBytes(), muts);
return mutationMap;
}
//set up columns to insert for Cambria
createCambriaMutation(
String columnFamily, String cambriaKey)
throws UnsupportedEncodingException {
//set up columns for Cambria
Clock clock = new Clock(System.nanoTime());
Column cambriaNameCol = new Column("name".getBytes(UTF8),
"Cambria Suites Hayden".getBytes("UTF-8"), clock);
Column cambriaPhoneCol = new Column("phone".getBytes(UTF8),
"480-444-4444".getBytes(UTF8), clock);
Column cambriaAddressCol = new Column("address".getBytes(UTF8),
"400 N. Hayden".getBytes(UTF8), clock);
Column cambriaCityCol = new Column("city".getBytes(UTF8),
"Scottsdale".getBytes(UTF8), clock);
Column cambriaStateCol = new Column("state".getBytes(UTF8),
"AZ".getBytes("UTF-8"), clock);
Column cambriaZipCol = new Column("zip".getBytes(UTF8),
"85255".getBytes(UTF8), clock);
ColumnOrSuperColumn nameCosc = new ColumnOrSuperColumn();
nameCosc.column = cambriaNameCol;
ColumnOrSuperColumn phoneCosc = new ColumnOrSuperColumn();
phoneCosc.column = cambriaPhoneCol;
ColumnOrSuperColumn addressCosc = new ColumnOrSuperColumn();
addressCosc.column = cambriaAddressCol;
ColumnOrSuperColumn cityCosc = new ColumnOrSuperColumn();
cityCosc.column = cambriaCityCol;
ColumnOrSuperColumn stateCosc = new ColumnOrSuperColumn();
stateCosc.column = cambriaStateCol;
ColumnOrSuperColumn zipCosc = new ColumnOrSuperColumn();
zipCosc.column = cambriaZipCol;
Mutation nameMut = new Mutation();
nameMut.column_or_supercolumn = nameCosc;
Mutation phoneMut = new Mutation();
phoneMut.column_or_supercolumn = phoneCosc;
Mutation addressMut = new Mutation();
addressMut.column_or_supercolumn = addressCosc;
Mutation cityMut = new Mutation();
cityMut.column_or_supercolumn = cityCosc;
Mutation stateMut = new Mutation();
stateMut.column_or_supercolumn = stateCosc;
Mutation zipMut = new Mutation();
zipMut.column_or_supercolumn = zipCosc;
//set up the batch

List cambriaCols = new ArrayList();
cambriaCols.add(nameMut);
cambriaCols.add(phoneMut);
cambriaCols.add(addressMut);
cambriaCols.add(cityMut);
cambriaCols.add(stateMut);
cambriaCols.add(zipMut);
cambriaMuts.put(columnFamily, cambriaCols);
//outer map key is a row key
//inner map key is the column family name
cambriaMutationMap.put(cambriaKey.getBytes(), cambriaMuts);
return cambriaMutationMap;
}
}

이것은 꽤 긴 예제이지만 “hello world” 보다는 더 많은 걸 보여주려고 노력한다. 많은 수의 insert와 batch_mutate 동작이 있다. 이것은 표준 컬럼군과 super 컬럼군이다. 또한 많은 수의 행을 포함하여 정교한 쿼리가 필요하도록 했다.
이 클래스는 우리의 예제를 실행하기 위한 첫번째이다. Prepopulate 방법이 완료되면 당신의 데이버베이스는 검색을 수행하기 위한 모든 데이터를 갖게 된다.

4.9. 검색 애플리케이션

다음은 main 메소드를 가진 자바 클래스이며 당신이 수행해야 한다. Log4J에 의존하기 때문에 당신의 log4j.properties 파일에 맞추어 실행할 것이다. 당신이 할 일은 이 클래스를 실행하는것 뿐이며, 그러면 자동으로 모든 호텔과 흥미로운 장소 정보를 만들고 사용자가 주어진 도시에 대해서 검색할 수 있도록 해준다. 사용자는 한 호텔을 택하고 애플리케이션은 주변의 흥미로운 장소를 검색한다. 그리고 당신은 애플리케이션의 나머지 부분을 구현하여 예약을 마칠 수 있다.
package com.cassandraguide.hotel;
import static com.cassandraguide.hotel.Constants.CL;
import static com.cassandraguide.hotel.Constants.UTF8;
import java.util.ArrayList;
import java.util.List;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.KeyRange;
import org.apache.cassandra.thrift.KeySlice;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.apache.cassandra.thrift.SuperColumn;
import org.apache.log4j.Logger;
/**
* Runs the hotel application. After the database is pre-populated,
* this class mocks a user interaction to perform a hotel search based on
* city, selects one, then looks at some surrounding points of interest for
* that hotel.
*
* Shows using Materialized View pattern, get, get_range_slices, key slices.
*
* These exceptions are thrown out of main to reduce code size:
* UnsupportedEncodingException,
InvalidRequestException, UnavailableException, TimedOutException,
TException, NotFoundException, InterruptedException
Uses the Constants class for some commonly used strings.
*/
public class HotelApp {
private static final Logger LOG = Logger.getLogger(HotelApp.class);
public static void main(String[] args) throws Exception {
//first put all of the data in the database
new Prepopulate().prepopulate();
LOG.debug("** Database filled. **");
//now run our client
LOG.debug("** Starting hotel reservation app. **");
HotelApp app = new HotelApp();
//find a hotel by city--try Scottsdale or New York...
List hotels = app.findHotelByCity("Scottsdale", "AZ");
//List hotels = app.findHotelByCity("New York", "NY");
LOG.debug("Found hotels in city. Results: " + hotels.size());
//choose one
Hotel h = hotels.get(0);
LOG.debug("You picked " + h.name);
//find Points of Interest for selected hotel
LOG.debug("Finding Points of Interest near " + h.name);
List points = app.findPOIByHotel(h.name);
//choose one
POI poi = points.get(0);
LOG.debug("Hm... " + poi.name + ". " + poi.desc + "--Sounds fun!");
LOG.debug("Now to book a room...");
//show availability for a date
//left as an exercise...
//create reservation
//left as an exercise...
LOG.debug("All done.");
}
//use column slice to get from Super Column
public List findPOIByHotel(String hotel) throws Exception {
///query
SlicePredicate predicate = new SlicePredicate();
SliceRange sliceRange = new SliceRange();
sliceRange.setStart(hotel.getBytes());
sliceRange.setFinish(hotel.getBytes());
predicate.setSlice_range(sliceRange);
// read all columns in the row
String scFamily = "PointOfInterest";
ColumnParent parent = new ColumnParent(scFamily);
KeyRange keyRange = new KeyRange();
keyRange.start_key = "".getBytes();
keyRange.end_key = "".getBytes();
List pois = new ArrayList();
//instead of a simple list, we get a map whose keys are row keys
//and the values the list of columns returned for each
//only row key + first column are indexed
Connector cl = new Connector();
Cassandra.Client client = cl.connect();
List slices = client.get_range_slices(
parent, predicate, keyRange, CL);
for (KeySlice slice : slices) {
List cols = slice.columns;
POI poi = new POI();
poi.name = new String(slice.key);
for (ColumnOrSuperColumn cosc : cols) {
SuperColumn sc = cosc.super_column;
List colsInSc = sc.columns;
for (Column c : colsInSc) {
String colName = new String(c.name, UTF8);
if (colName.equals("desc")) {
poi.desc = new String(c.value, UTF8);
}
if (colName.equals("phone")) {
poi.phone = new String(c.value, UTF8);
}
}
LOG.debug("Found something neat nearby: " + poi.name +
". \nDesc: " + poi.desc +
". \nPhone: " + poi.phone);
pois.add(poi);
}
}
cl.close();
return pois;
}
//uses key range
public List findHotelByCity(String city, String state)
throws Exception {
LOG.debug("Seaching for hotels in " + city + ", " + state);
String key = city + ":" + state.toUpperCase();
///query
SlicePredicate predicate = new SlicePredicate();
SliceRange sliceRange = new SliceRange();
sliceRange.setStart(new byte[0]);
sliceRange.setFinish(new byte[0]);
predicate.setSlice_range(sliceRange);
// read all columns in the row
String columnFamily = "HotelByCity";
ColumnParent parent = new ColumnParent(columnFamily);
KeyRange keyRange = new KeyRange();
keyRange.setStart_key(key.getBytes());
keyRange.setEnd_key((key+1).getBytes()); //just outside lexical range
keyRange.count = 5;
Connector cl = new Connector();
Cassandra.Client client = cl.connect();
List keySlices =
client.get_range_slices(parent, predicate, keyRange, CL);
List results = new ArrayList();
for (KeySlice ks : keySlices) {
List coscs = ks.columns;
LOG.debug(new String("Using key " + ks.key));
for (ColumnOrSuperColumn cs : coscs) {
Hotel hotel = new Hotel();
hotel.name = new String(cs.column.name, UTF8);
hotel.city = city;
hotel.state = state;
results.add(hotel);
LOG.debug("Found hotel result for " + hotel.name);
}
}
///end query
cl.close();
return results;
}
}

각기 다른 문장들의 의도를 설명하기 위해 곳곳에 커멘트를 배치하였다.
애플리케이션을 실행시킨 결과를 다음에 보여준다.
DEBUG 09:49:50,858 Inserted AZC_043
DEBUG 09:49:50,861 Inserted AZS_011
DEBUG 09:49:50,863 Inserted CAS_021
DEBUG 09:49:50,864 Inserted NYN_042
DEBUG 09:49:50,864 Done inserting at 6902368219815217
DEBUG 09:49:50,873 Inserted HotelByCity index for Cambria Suites Hayden
DEBUG 09:49:50,874 Inserted HotelByCity index for Clarion Scottsdale Peak
DEBUG 09:49:50,875 Inserted HotelByCity index for The W SF
DEBUG 09:49:50,877 Inserted HotelByCity index for The Waldorf=Astoria
DEBUG 09:49:50,877 Inserting POIs.
DEBUG 09:49:50,880 Done inserting Empire State.
DEBUG 09:49:50,881 Done inserting Central Park.
DEBUG 09:49:50,885 Done inserting Phoenix Zoo.
DEBUG 09:49:50,887 Done inserting Spring Training.
DEBUG 09:49:50,887 Done inserting POIs.
DEBUG 09:49:50,887 ** Database filled. **
DEBUG 09:49:50,889 ** Starting hotel reservation app. **
DEBUG 09:49:50,889 Seaching for hotels in Scottsdale, AZ
DEBUG 09:49:50,902 Using key [B@15e9756
DEBUG 09:49:50,903 Found hotel result for Cambria Suites Hayden
DEBUG 09:49:50,903 Found hotel result for Clarion Scottsdale Peak
DEBUG 09:49:50,904 Found hotels in city. Results: 2
DEBUG 09:49:50,904 You picked Cambria Suites Hayden
DEBUG 09:49:50,904 Finding Points of Interest near Cambria Suites Hayden
DEBUG 09:49:50,911 Found something neat nearby: Phoenix Zoo.
Desc: They have animals here..
Phone: 480-555-9999
DEBUG 09:49:50,911 Found something neat nearby: Spring Training.
Desc: Fun for baseball fans..
Phone: 623-333-3333
DEBUG 09:49:50,911 Hm... Phoenix Zoo. They have animals here.--Sounds fun!
DEBUG 09:49:50,911 Now to book a room...
DEBUG 09:49:50,912 All done.

다시한 번 당신은 Thrift나 Avro에 반해서 쓰기를 원치 않아서 대신 8장에 리스트된 클라이언트를 사용하게 될 것이다. 여기서 목적은 당신이 이것이 어떻게 작동하는지 보여주고 완전한 작동 애플리케이션을 보여주어 insert 와 많은 검색들이 일하는 것을 보고 실제로 어떻게 작동하는지 보여주는 것이다.

4.10. Twissandra

당신이 카산드라를 어떻게 디자인하는지 궁금해하기 시작할 때 Eric Florenzano에 의해 쓰여진 Twissandra를 살펴보자. http://www.twissandra.com을 방문하여 다운로드하여 써볼수 있는 트위터 클론을 보자. 소스는 Phthon 이며 정렬하기 위해서 Django와 JSON 라이브러리에 좀 의존하고 있지만 시작하기 좋은 장소이다. 당신은 트위터와 같은 익숙한 데이터 모델을 사용할 수 있고 사용자, 타임라인, 트윗 등이 간단한 카산드라 데이터 모델에 작동하는 것을 볼 수 있다.
Eric Evans의 Twitssandra를 사용하는데 필요한 글도 있는데, 그것은 http://www.rackspacecloud.com/blog/2010/05/12/cassandra-by-example 이다.

2011년 6월 23일 목요일

Cassandra (3/12)

3. 카산드라 데이터 모델

이 장에서 카산드라의 디자인 목표, 데이터 모델, 그리고 일반적인 동작의 성질들을 알아보겠다.
관계형 세상에서 온 개발자들과 관리자들은 처음에는 카산드라의 데이터 모델은 매우 이해하기 어려울 것이다. Keyspace와 같은 용어는 완전히 새로운 것이고 column 같은 것은 양쪽에서 사용은 하고 있는 말이지만 그 의미는 양쪽에서 다르다. Dynamo나 Bigtable과 같은 것에 대한 문서를 보고 있어도 혼동이 될 수있는데 그것은 카산드라가 그것들에 기반을 두고는 있지만 자신만의 독특한 모델을 가지고 있기 때문이다.
그래서 여기서는 공통된 기반에서 시작하여 익숙하지 않은 용어들로 이동한다. 그리고 카산드라의 세상과 관계형의 세상의 사이 간극을 메우기 위해서 다리를 놓도록 한다.

3.1. 관계형 데이터 모델

관계형 데이터베이스에서 우리는 한 개의 애플리케이션에 응답하는 어떤 컨테이너가 되는 데이터베이스를 가지고 있다. 데이터베이스는 테이블을 가지고 있다. 테이블은 이름과 몇 개의 컬럼을 가지고 있다. 이 또한 이름을 가지고 있다. 우리는 여기 테이블에 데이터를 추가한다. 그리고 각 컬럼에 값을 지정한다. 어떤 칼럼에 값이 없으면 null을 사용한다. 이는 새로운 행을 테이블에 추가한다. 이후에 우리가 이의 primary key 라는 고유의 식별자를 알면 읽어볼 수 있다. 테이블 안의 값을 업데이트하고 싶으면 행 전체를 업데이트하거나 일부분만 할 수도 있다. 이것은 우리가 SQL 문의 where절에 사용하는 filter 에 따라서다.
카산드라를 배우기 위해 관계형 세상에서 알던 것은 잠시 접어두는 것이 유용할 것이다.

3.2. 간단한 소개

이 섹션에서 카산드라의 데이터 모델을 이해하기 위해 바텀 업 방식을 쓴다.
당신이 지금 필요한 가장 간단한 데이터 저장은 array나 list이며 다음과 같이 보인다.

그림3-1. 값들의 list
당신이 추후에 쿼리해볼 수 있는 이 리스트의 값들이지만 당신은 각 값이 무엇을 표현하는지 알기 위해 자세히 보아야하며 외부적으로 문서 같은 것을 통해 어떤 셀이 어떤 값을 가지고 있는지 등을 참조할 수 있다. 이는 옵셔널한 값들이 없을 때 채우지는 않고 null을 사용함으로써 크기는 유지할 수 있다. Array는 매우 유용한 데이터 구조이지만 문법적으로 아주 풍부하지는 않다.
우리는 값과 이름을 맞추기 위해 두번째 차원을 추가한다. 우리는 각 셀에 이름을 추가하고 지도 같은 구조를 가지며 아래 그림과 같다.

그림3-2. 이름/값 쌍의 지도
우리가 각 값의 이름을 알 수 있기 때문에 분명한 발전이다. 만약 사용자 정보를 가진다면 컬럼이름을 first Name, lastName, phone, email 등등 으로 가질 수 있다. 이것은 처리하고 다루기에 약간 더 풍부한 정보이다.
그러나 우리가 여태까지 살펴본 구조는 한 엔티티당 한 인스턴스를 가지고 있을 때만 작동한다. 이것은 마치 한 사람이나 사용자 혹은 호텔이나 트윗과 같다. 이것이 같은 구조에 여러개의 엔티티를 저장하고자 하면 잘 되지 않는다. 이름/값의 짝의 모임을 통일하거나 같은 컬럼의 이름을 되풀이 하는 방법은 없다. 그래서 우리는 컬럼값을 모으고 다른 주소들로 매길수 있는 것이 필요하다. 우리는 다같이 set으로 다루어 질수 있는 컬럼의 그룹을 레퍼런스할 수 있는 key가 필요하다. 우리는 행이 필요하다. 그리고 우리가 한 행을 갖는다면 우리는 모든 이름/값 쌍을 한 엔티티에서 가질수 있고 이름에 대한 값만 가질 수 있다. 우리는 이 이름/값 쌍을 column이라고 부를수 있다. 우리는 각 colum 행을 가지고 각 별개의 엔티티를 row라고 부를수 있다. 그리고 고유한 식별자를 row key라고 부른다.
카산드라는 비슷한 데이터를 관계짓는 논리적인 차원을 컬럼군이라고 정의한다. 예를 들어 User 라는 컬럼군을 가질수 있고 Hotel, Address Book 등등을 가질 수 있다. 이런 식으로 컬럼군은 관계형 세상의 테이블과 비슷하다.
모두 볼 때, 컬럼이라는 카산드라 데이터 구조의 기본을 보았으며 이름/값 쌍이며, 컬럼 군 즉 비슷한 하지만 같지는 않은 컬럼 셋을 가진 것이다.
관계형 데이터베이스에서는 컬럼 이름을 스트링으로만 가질 수 있었다. 하지만 카산드라는 그런 제한 사항이 없다. 행의 키와 컬럼의 이름 다 스트링이 될 수 있다 하지만 long integer나 UUID 등 어떤 형태의 바이트 array가 될 수 있다. 그래서 키 이름을 정하는데 좀 더 다양성을 둘 수 있다.
이것은 카산드라의 컬럼의 흥미로운 특징을 보여준다. 미리 정한 이름/값 쌍처럼 간단할 필요도 없고 값에 뿐 아니라 키에다가 직접 어떤 유용한 값을 저장할 수 있다. 이는 카산드라에서 인덱스를 만들 때 일반적이다. 여기서는 자세히 보지 않는다.
우리는 모든 새로운 엔티티를 저장할 때 마다 모든 컬럼의 값을 모두 저장할 필요는 없다. 아마 주어진 엔티티에 대해서 모든 컬럼의 값을 알지도 못한다. 예를 들어 어떤 사람들은 두번째 전화번호를 가지고 있고 어떤 사람들은 그렇지 않다 그리고 카산드라에 의해 지지되는 온라인 폼에서 어떤 것은 옵셔날이고 어떤 것은 필수이다. 좋다. 공간의 낭비처럼 우리가 모르는 값에 대해 null 값을 채우는 것 대신에 지금 당장은 그 컬럼을 저장하지 않는다. 그래서 여러 차원의 array 구조를 다음과 같이 가지고 있다.

그림3-3. 컬럼 군
이것을 그림으로만 보지 않고 JavaScript Object Notation (JSON) 의 용어로 생각하는 것도 도움이 된다.
Musician: ColumnFamily 1
bootsy: RowKey
email: bootsy@pfunk.com, ColumnName:Value
instrument: bass ColumnName:Value
george: RowKey
email: george@pfunk.com ColumnName:Value
Band: ColumnFamily 2
george: RowKey
pfunk: 1968-2010 ColumnName:Value
우리는 여기 두개의 컬럼군이 있다. Musician과 Band이다. Musician 컬럼은 두개의 행이 있다. “bootsy”와 “George”이다. 이 두개의 행은 관련한 두개의 컬럼이 있다. Bootsy 기록은 두개의 컬럼(email과 instrument)을 가지고 George 컬럼은 한 개의 컬럼을 가지고 있다. 카산드라에서 이것은 좋다. 두번째 컬럼군은 Band이다. 마찬가지로 George 행과 “pfunk”라는 이름의 컬럼을 가지고 있다.
컬럼은 카산드라에서 세번째 측면을 가지고 있다. 타임스탬프, 컬럼이 마지막으로 업데이트된 시간을 기록한다. 이것은 자동적인 메타데이터 프로퍼티가 아니라 클라이언트는 쓰기를 실행할때마다 타임스탬프를 제공해야 한다. 타임스탬프로 퀴리를 할 수는 없지만 서버에서 순수하게 conflict resolution하기 위해 사용된다.
행은 타임스탬프를 가지지 않고, 각 컬럼만 가지고 있다.
만약 우리가 새로운 차원을 추가하는 관련 컬럼끼리의 그룹을 만든다면 어떻겠는가? 카산드라는 super column family 라고 불리는 것을 가지고 우리가 이것을 할 수 있게 한다. Super colomn family 지도의 지도라고 생각할 수 있다. Super column family가 그림 3-4에 있다.

그림3-4. A super column family
컬럼 군에서의 행은 이름/값 쌍의 모음들을 가지고 있고, super column family는 부컬럼을 가지고 있으며, 부컬럼은 컬럼들의 모임이다. 그래서 일반적인 컬럼군에서 값의 주소는 값을 가르키고 있는 컬럼 이름을 가리키고 있는 row key 이다. Super로 분류되는 컬럼군에서 값의 주소는 값을 가리키는 부컬럼을 가리키는 컬럼 이름을 가리키는 row key이다. 조금 다르게 보아서, super column family에서 행은 컬럼을 가지고 있고 각각은 부컬럼을 가지고 있다.
그래서 이것은 카산드라 데이터 모델을 보는 버텀 업 접근 방법이다. 이제 기본적인 이해를 가지고 있는 상태에서 좀 넓게 보아 좀 더 상위 레벨을 보고 탑 다운 접근 방법을 가져보자. 완전히 이해하기 위해서 여러 번 다시 말해야 할만큼 이 주제에 대해서는 여러가지 혼란 사항이 있다.

3.3. 클러스터

카산드라는 한 개의 노드만을 운영한다면 좋은 해결책은 아니다. 이미 언급되었듯이 카산드라 데이터베이스는 사용자에게는 한 개의 기기처럼 보이게 여러 개의 기기를 분산 환경에서 운영할수 있도록 특별히 디자인 되어 있다. 그래서 가장 바깥에서 보이는 카산드라의 구조는 클러스터이다. ring이라고 불리기도 한다. 이는 카산드라가 ring에 어레인지하여 데이터를 노드들에 배분하기 때문이다.
노드는 다른 데이터들의 복사를 가진다. 첫번째 노드가 다운되면 복사본이 대신 쿼리에 대한 대답을 할 수 있다. Peer-to-peer 프로토콜이 사용자에게 투명하게 노드들 상에 복사되도록 하고 replication factor는 클러스터 상에서 같은 데이터의 복사본을 받게 되는 기기의 숫자이다. 6장에서 자세히 살펴볼 것이다.

3.4. Keyspaces
클러스터는 keyspace를 담고 있고 보통은 한 개를 담고 있다. Keyspace는 카산드라에서 가장 바깥쪽의 데이터 저장소이며 관계형 데이터베이스에게 잘 응답한다. 관계형 데이터베이스처럼 keyspace는 여러가지 행동을 정의하는 이름과 어트리뷰트 모음들을 가지고 있다. 애클리케이션당 한 개의 keyspace를 가지는 것이 좋다고 사람들이 조언하지만 실제적인 기반이 있는 사실은 아닌것같다. 실제적으로 받아들일 만한 조언이지만 애플리케이션이 필요로 하는 만큼 keyspace를 만들어도 된다. 하지만 애플리케이션 당 수천개의 keyspace를 만든다면 문제가 생길 것이다.
당신의 시큐리티 제한에 따라 한 클러스터에 여러 개의 keyspace를 돌리는 것이 가능하다. 예를 들어 당신의 애플리케이션이 Twitter라고 불린다면 당신은 Twitter-Cluster라고 불리는 클러스터를 가지고 Twitter라고 불리는 keyspace를 갖게 될 것이다. 내가 아는한은 카산드라에는 그런 것을 위한 naming 컨벤션이 없다.
카산드라에서 keyspace 당 설정할 수 있는 기본 어트리뷰트는
- Replication factor
가장 간단하게 말해서 replication factor는 각 데이터에대해 몇 개의 복사본이 되는지 노드의 개수다. 만약 당신의 replication factor가 3이라면 링안에 3개의 노드가 복사본을 모두 가지고 있다는 것이다.
replication factor 는 더 많은 일관성을 얻기위해 당신이 퍼포먼스에는 어느 정도 희생하는지 결정할 수 있도록 해준다. 그것은 당신의 읽기와 쓰기에서의 일관성은 replication factor에 기반을 둠을 의미한다.
- Replica placement strategy
replica placement 는 링안에서 어떻게 복사본들이 위치될 것인지를 말한다. 카산드라에 어떤 노드가 어떤 key의 복사본을 가질것인지 다른 전략이 있다. 이것들은 SimpleStrategy(예전에는 RackUnawareStrategy로 알려졌다.) OldNetworkTopologyStrategy(예전에는 RackAwareStrategy로 알려졌다.) 그리고 NetworkTopologyStrategy(예전에는 DatacenterShardStrategy로 알려졌다.)가 있다.
- Column families
데이터베이스가 테이블의 저장소인 것 처럼 keyspace는 한 개나 그 이상의 컬럼군의 리스트의 저장소이다. 컬럼 군은 대략 관계형 모델의 테이블과 비슷하다. 그리고 행들의 집합의 저장소이다. 각 행은 정렬된 컬럼을 가지고 있다. 컬럼군은 데이터의 구조를 나타낸다. 각 keyspace는 적어도 한 개 그리고 종종 많은 수의 컬럼군을 가진다.
나는 replication factor와 replica placement strategy 를 언급했는데 왜냐하면 keyspace당 셋이기 때문이다. 그러나 데이터 모델에 당장 직접적인 영향을 가지고 있지는 않다.
애플리케이션당 많은 수의 keyspace를 생성하는 것은 가능은 하지만 일반적으로 추천사항은 아니다. 애플리케이션을 여러 개의 keyspace로 나누는 것은 다른 replication factor나 replica placement strategy 를 어떤 컬럼 군에 원할 때 뿐이다. 예를 들어 만약 당신이 중요도가 낮은 데이터를 가지고 있다면 카산드라가 복사를 위해 열심히 일하지 않도록 이것을 고유한 keyspace에 낮은 replication factor를 가지고 둘 수 있다. 그러나 이것은 많이 복잡할 수도 있다. 아마도 한 개의 keyspace로 시작해서 모든 레벨에 조정이 필요한지 보는 것이 더 나은 아이디어일것이다.

3.5. 컬럼 군
컬럼 군은 행들의 정렬된 집합의 저장소이고 그것은 각각 정렬된 컬럼들의 집합이다. 관계형 세상에서 당신이 물리적으로 모델로부터 데이터베이스를 만들 때 데이터베이스(keyspace)의 이름을 정하고 테이블의 이름을 정하고 (컬럼 군과 테이블은 같지 않다.) 그리고 각 테이블에 있는 컬럼의 이름을 정의한다.
컬럼군이 관계형 테이블과 같다는 생각을 너무 깊게 가지지 말아야 할 이유들이 있다. 첫째로 카산드라는 스키마에 자유롭게 여겨지는데 그것은 컬럼군이 정의되었더라도 컬럼은 아니기 때문이다. 당신은 당신의 필요에 따라서 컬럼군에 컬럼을 언제든 추가 할 수 있다. 둘 째로 컬럼군은 두개의 이름과 비교자라는 애트리뷰트를 가지고 있다. 비교자는 컬럼이 쿼리에서 반환되었을 때 어떻게 소트되는지 long, byte, UTF8 이나 다른 순서에 따라 보여준다.
관계형 데이터베이스에서 테이블이 디스크에 어떻게 저장되는지는 사용자에게 자주 투명하게 보여지며 RDBMS 가 디스크에 테이블을 저장하는지 데이터 모델링은 그에 대한 추천사항을 듣기 어렵다. 이는 컬럼군이 테이블이 것을 기억해야할 또 다른 이유이다. 컬럼 군이 각각 다른 파일로 디스크에 저장되는 때문에 같은 컬럼군에서 저장되는 관련있는 컬럼은 같이 정의하는 것이 중요하다.
컬럼군이 관계형 테이블과 다른 점은 관계형 테이블은 컬럼만을 정의하고 사용자는 행의 값만을 제공한다는 것이다. 하지만 카산드라에서는 테이블은 컬럼을 가지거나 super 컬럼 군으로 정의 될 수 있다. Super 컬럼군을 사용하는 이점은 nesting 할 수 있는 것이다.
디폴트인 표준 컬럼군에서는 표준을 정하고 super 컬럼군에서는 super를 정한다.
당신이 카산드라에서 컬럼군에 데이터를 쓸 때, 한 개나 그 이상의 컬럼에 값을 정해준다. 그 고유한 식별자를 가진 값들의 모임을 행이라고 부른다. 행은 row key라고 불리는 고유한 key를 가지며 그것은 primary key 처럼 작동한다. 그래서 column-oriented 또는 columnar 라고 부르는 것이 틀리지는 않으며 행을 컬럼의 컨테이너로 생각하는 것이 모델을 이해하기 쉬운 방법이다. 이것이 사람들이 카산드라 컬럼 군을 사차원 해쉬와 비슷하다고 하는 이유이기도 하다.
[Keyspace][ColumnFamily][Key][Column]
우리는 아래에 보는바와 같이 Hotel 컬럼군을 나타내기위해 JSON 같은 노테이션을 쓸 수 있다.
Hotel {
key: AZC_043 { name: Cambria Suites Hayden, phone: 480-444-4444,
address: 400 N. Hayden Rd., city: Scottsdale, state: AZ, zip: 85255}
key: AZS_011 { name: Clarion Scottsdale Peak, phone: 480-333-3333,
address: 3000 N. Scottsdale Rd, city: Scottsdale, state: AZ, zip: 85255}
key: CAS_021 { name: W Hotel, phone: 415-222-2222,
address: 181 3rd Street, city: San Francisco, state: CA, zip: 94103}
key: NYN_042 { name: Waldorf Hotel, phone: 212-555-5555,
address: 301 Park Ave, city: New York, state: NY, zip: 10019}
}
이 예에서 행의 key는 hotal의 고유한 primary key이고 컬럼은 이름, 전화번호, 주소, 도시, 주, 우편번호 이다. 행이 우연히 모든 같은 컬럼의 값을 정의해도 당신은 쉽게 4개 컬럼의 한 행을 가질수 있고 같은 컬럼군에서 400컬럼짜리 다른 행을 가질수도 있으며 아무것도 오버랩되지는 않을 것이다.
우리는 CLI를 사용해서 아래처럼 한 컬럼군을 쿼리할 수 있다.
cassandra> get Hotelier.Hotel['NYN_042']
=> (column=zip, value=10019, timestamp=3894166157031651)
=> (column=state, value=NY, timestamp=3894166157031651)
=> (column=phone, value=212-555-5555, timestamp=3894166157031651)
=> (column=name, value=The Waldorf=Astoria, timestamp=3894166157031651)
=> (column=city, value=New York, timestamp=3894166157031651)
=> (column=address, value=301 Park Ave, timestamp=3894166157031651)
Returned 6 results.
이것은 컬럼군에서 6개의 컬럼이 있고 결과는 column-oriented 여서 6개의 결과를 보며 New York에 한 개의 hotel을 가졌음을 보여준다. 그 행에 6개의 컬럼을 가졌지만 다른 행에는 더 적거나 더 많은 컬럼을 가질 수 있다.

3.6. 컬럼 군 옵션
각 컬럼 군을 정의할 수 있는 몇 개 더 추가적인 파라미터가 있다. 이것은 :
- keys_cached
SSTable 당 캐쉬하는 위치의 개수다. 이것은 컬럼의 이름과 값은 참조하지 않고 key의 숫자를 컬럼군당 행의 위치로 참조하며 가장 최근에 사용된 순서로 메모리에 저장된다.
- rows_cached
행의 모든 컨텐츠가 메모리에 캐쉬된다.
- comment
당신의 컬럼 군의 정의에 필요한 중요한 사항을 기억할 수 있도록 해주는 표준 커멘트이다.
- Read_repair_chance
이것은 0과 1사이의 값으로서 특정값을 지정하지 않은채 쿼리를 수행해서 두 개나 더 많은 복사본을 같은 행에서 반환하고 한 개 이상의 복사본이 시간이 너무 지난 것이거나 해서 read repair 동작이 수행이 될 확률이다. 쓰기보다 읽기 동작을 많이 수행한다면 이 값을 낮추고 싶을 수 있다.
- Preload_row_cache
서버 시작시에 행 캐쉬를 미리 이동시킬지 말지 정해준다.
나는 이것들이 데이터 모델에 대한 것이기 보다는 설정과 서버 행동에 대한 것이기 때문에 이것을 최대한 단순화 시키려고 하였다. 6장에서 더 자세히 다룬다.

3.7. 컬럼
컬럼은 카산드라의 데이터 모델에서 가장 기본적인 단위이다. 컬럼은 이름,값,시간의 세 쌍이며 여기서 타임 스탬프를 생각할 수 있다. 다시 한 번 관계형 세계에서 우리는 컬럼이라는 용어에 익숙하지만 카산드라에서는 좀 다르므로 혼돈될 수 있다. 무엇보다 관계형 데이터베이스를 설계할 때 테이블 이름에서 모든 컬럼의 이름을 정해서 테이블의 구조를 정하고 후에 데이터를 쓸때는 단지 미리 정의된 구조에 값을 제공한다.
하지만 카산드라에서는 컬럼을 미리 정의하지 않고 keyspace의 컬럼군을 정의하고 컬럼을 정의하지 않은채 데이터를 아무곳에나 쓸 수 있다. 그것은 카산드라에서 모든 컬럼의 이름은 클라이언트에 의해서 제공되기 때문이다. 이는 당신의 애플리케이션의 데이터 와의 동작에 상당한 유연함을 더하여 시간이 지남에 따라 발전하게 한다.
이름과 값을 위한 데이터 타입은 종종 스트링처럼 제공되는 자바 바이트 array이다. 이름과 값이 바이트 타입이라서 그 길이는 자유롭다. 시간을 위한 데이터 타입은 org.apache.cassandra.db.IClock 이다. 하지만 0.7 버전에서 타임 스탬프는 백워드 컴패터빌리티가 있다. 이 컬럼의 구조는 그림 3-5에 보여진다.

그림3-5. 컬럼의 구조
여기에 당신이 정의할 만한 컬럼의 예제가 구조의 깨끗한 정리를 위해 JSON 노테이션으로 나타내져 있다.
{
"name": "email",
"value: "me@example.com",
"timestamp": 1274654183103300
}
이 예제에서 컬럼은 email 이라고 정해져 있는데 좀 더 정확히 이름의 이라는 애트리뷰트의 값이 email이다. 한 개의 컬럼군은 여러 개의 key를 가지고 있으며 그것은 이 컬럼을 가지고 있을 수도 있는 다른 행을 나타낸다. 이는 관계형 테이블이 모든 행에 같은 셋의 컬럼을 가지고 있다고 생각하기 때문에 관계형 모델에서 나오기 힘든 것이다. 그러나 카산드라에서 컬럼군은 많은 행을 가지고 있고 각각 같거나 다른 셋의 컬럼을 가질 수 있다.
서버 측에서 컬럼은 멀티 스레딩 이슈를 피하기 위해 변화불가능하다. 컬럼은 카산드라에서 org.apache.cassandra.db.IColumn 인터페이스에 의해서 정의되며 super 컬럼의 경우에 그 서브 컬럼의 값을 Collection 으로 가져오고 가장 최근의 변화를 가져오는 등 컬럼의 값을 바이트 array로 가져오는 동작을 포함하여 여러 동작을 가능하게 한다.
관계형 데이터베이스에서 행은 함께 저장된다. 이것은 카산드라의 초기 버전에서는 이렇지 않았다. 0.6버전에서 행은 같은 컬럼군과 함께 디스트에 저장된다.

3.8. 넓은 행, 좁은 행

전통적인 관계형 데이터베이스에서 테이블을 디자인할 때 당신은 보통 엔티티를 다루게 되거나 Hotel, User, Product 등 특정한 명사를 묘사하기 위해 애트리뷰트들을 다루게 된다. 당신의 테이블이 어떤 명사를 나타내는지 결정한 후에는 행의 크기는 조절할 수 없기 때문에 행의 크기 자체는 별로 고려대상이 아니다. 그러나 카산드라를 다룰때는 행의 크기를 결정해야 한다. 행이 가지고 있는 컬럼의 개수에 따라 넓거나 좁을 수도 있다.
넓은 행은 아마 몇 천이나 몇 백만의 컬럼을 가진 행을 의미한다. 보통은 많은 수의 컬럼을 가지는 불과 몇 개의 행이 있을 수 있다. 반대로 관계형 모델과 비슷하게 적은 수의 컬럼을 가진 많은 행을 정의할 수 있는데 이것은 좁은 모델이다.
넓은 행은 보통 자동으로 생성된 이름 (즉, UUID나 타임스탬프) 같은 것을 담고 있고 이것은 어떤 것들의 리스트를 저장하는데 사용된다. 모니터링 애플리케이션을 예로서 고려해보면 당신은 행의 key에 수정된 타임스탬프를 사용하는 시간을 잘게 나눈 것을 행으로 가질 수도 있고 인터벌 사이에 당신의 애플리케이션에 접근하는 IP 주소를 가진 컬럼을 저장할 수도 있다. 시간이 지나면 새로운 행의 key를 만들수 있다.
좁은 행은 좀 더 전통적인 RDBMS의 행과 같아서 각 행은 비슷한 컬럼 이름을 가진 셋이다. RDBMS 행과는 다르지만 모든 컬럼은 근본적으로 옵셔날하다.
넓은 행과 좁은 행의 다른 차이점은 넓은 행만이 컬럼 이름의 소팅 순서에 신경을 쓴다는 것이다. 다음 절에서 보자.

3.9. 컬럼 정렬

컬럼은 그 정의에서 다른 면이 있다. 카산드라에서 결과값이 클라이언트에게 반환 되었을 때 컬럼의 이름이 어떻게 정렬 순서와 비교되는지 정하게 된다. 컬럼은 닫히는 컬럼군에 정의된 대로 “Compare With” 타입대로 정렬된다. :AsciiType, BytesType, LexicalUUIDType, IntegerType, LongType, TimeUUIDType, 이나 UTF8Type이다.
- AsciiType
이것은 입력값이 US-ASCII로 파싱될 수 있다는 것을 검증하며 직접적으로 바이트값을 비교하는 것이다. US_ASCII는 영어 알파벳 순서에 기반한 문자 인코딩 메커니즘이다. 128 문자를 정의하고 94문자는 프린트 할 수 있다.
- BytesType
이것은 디폴트로서 직접적인 바이트값 비교에 의한 정렬이다. 하지만 검증하는 단계는 넘어간다. BytesType은 디폴트인 이유가 있는데 이는 거의 모든 타입의 데이터에 올바른 정렬방식을 제공한다. (UTF-8과 ASCII를 포함한다.)
- LexicalUUIDType
16바이트(128비트) Universally Unique Identifier (UUID) 문자적으로 비교된다.
- LongType
이 정렬은 8 바이트 (64 비트) 긴 숫자형이다.
- IntegerType
0.7 버전에서 소개되었는데 이것은 LongType보다 빠르고 LongType에서 제공되는 64비트의 수보다 더 작거나 더 큰 정수값을 허가한다.
- TimeUUIDType
이것은 16바이트의 타임스탬프값으로 정렬한다. 공통적인 버전을 타임스탬프 UUID로 생성하는 방법은 5가지가 있다. 카산드라가 사용하는 스키마는 버전 첫번째의 UUID로서 컴퓨터의 MAC주소에 기반하고 Gregorian 달력의 시작점에서 100 나노초의 인터벌로 수를 매긴 것이다.
- UTF8Type
UTF-8 을 캐릭터 인코더로 사용한 스트링이다. 이것이 좋은 디폴트 값으로 보일지몰라도 그것은 아마 XML이나 다른 데이터 교환 메커니즘을 사용하는 개발자들은 공동 인코딩을 사용하는데 더 편안함을 느끼기 때문일 것이다. 카산드라에서는 그러나 당신은 UTF8Type을 당신의 데이터가 검증되기 바라면 사용해야 한다.
- Custom
만약 당신이 원한다면 당신의 고유한 컬럼 정렬 방식을 만들수 있다. 카산드라의 많은 것들처럼 pluggable 하다. 당신이 해야 하는 일은 org.apache.cassandra.db.marshal.AbstractType 을 확장하고 클래스 이름을 정하는 것이다.
컬럼 이름은 compare_with 값에 따라 정렬된다. 행은 한 편, partitioner에 의해 정의된 대로 정렬된다 (예를 들면 RandomPartitioner 는 무작위 정렬순서 이다). 6장에서 partitioner를 살펴본다.
카산드라에서는 관계형 데이터베이스에서 했던 것처럼 값으로 정렬할 수는 없다. 이것은 좀 이상한 제한처럼 보이지만 카산드라는 모든 행을 메모리에 넣지 않은 채 많은 행을 각각의 컬럼에 펫칭하지 않도록 각 컬럼의 이름으로 정렬해야 한다. 퍼포먼스는 카산드라의 중요한 장점이어서 읽는 시점에 정렬하는 것은 퍼포먼스에 중요한 안좋은 영향을 미친다는 점은 주목할 만하다.

3.10. Super 컬럼들
Super 컬럼은 컬럼의 한 특별한 종류이다. 양 종류의 컬럼은 이름 값 쌍이고 보통의 일반적인 컬럼은 바이트 array를 저장하며 super 컬럼의 값은 서브컬럼의 지도이어서 바이트 array를 저장한다. 이것은 단지 컬럼의 지도를 저장한다는 것을 기억하고 다른 super컬럼의 지도를 저장하는 super 컬럼을 정의할 수는 없다. 그래서 super 컬럼의 개념은 한 레벨의 깊이만 갈수 있고, 제한없는 수의 컬럼은 가질 수 있다.
기본적인 구조의 super 컬럼은 이름이고 바이트 array이다. (보통 컬럼과 같다). 그 컬럼은 그 key는 컬럼 이름 그리고 값은 컬럼과 같은 지도와 같이 담겨져 있다.

그림3-6. Super 컬럼의 기본적 구조
각 컬럼 군은 각 고유한 파일들로 디스크에 저장이 된다. 그래서 퍼포먼스를 최적화 하기위해서 같이 쿼리할 것 같은 것은 같은 컬럼 군에 두는 것이 중요하고 super 컬럼은 이것을 위해 유용할 수 있다.
SuperColumn 클래스는 IColumn 과 IColumnContainer 를 구현하며 둘 다 org.apache.cassandra.db 패키지에 있다. Thrift API 는 카산드라에 원격 동작을 할 수 있는 기반 RPC 메커니즘이다. Thrift API가 상속 개념이 없어서 가끔은 데이터 구조가 이 타입을 사용할 때 기반이 되는 컬럼 군이 Super 나 Standard 타입인지 아닌지 알기를 예상하며 ColumnOrSupercolumn 으로의 API 참조를 보게된다.
여기서 데이터 모델에서의 좀 더 풍부한 점을 보게된다. 일반적인 컬럼을 사용할 때 앞에서 보았듯이 카산드라는 4차원 해쉬테이블 처럼 보이게 된다. 그러나 super 컬럼에서는 5차원 해쉬같이 된다.
[Keyspace][ColumnFamily][Key][SuperColumn][SubColumn]
Super 컬럼을 사용하기 위해 당신의 컬럼 군을 타입 Super로 정의한다. 그리고 나서 당신은 일반적인 컬럼 군에서 하듯이 행의 key를 가지고 있다. 그러나 Super 컬럼을 참조하고 있고 이는 단순히 일반적인 컬럼들의 지도의 리스트를 가리키는 이름이다. (때때로 서브컬럼이라고 불린다).
여기 PointOfInterest 라고 불리는 super 컬럼 군 정의의 예가 있다. 호텔 관련 도메인에서 “point of interest” 는 여행자들이 방문하고 싶은 공원, 박물관, 동물원 등과 같은 호텔 근처의 위치이다.
PointOfInterest (SCF)
SCkey: Cambria Suites Hayden
{
key: Phoenix Zoo
{
phone: 480-555-9999,
desc: They have animals here.
},
key: Spring Training
{
phone: 623-333-3333,
desc: Fun for baseball fans.
},
}, //end of Cambria row
SCkey: (UTF8) Waldorf=Astoria
{
key: Central Park
desc: Walk around. It's pretty.
},
key: Empire State Building
{
phone: 212-777-7777,
desc: Great view from the 102nd floor.
}
}
}
PointOfInterest라는 super 컬럼 군은 두 개의 super 컬럼을 가지고 있으며 각각은 Cambria Suites Hayden과 Waldorf=Astoria 라는 이름을 가진 다른 호텔이다. 행의 key는 각각 다른 point of interest의 이름으로서 “Phoenix Zoo” 그리고 “Central Park” 같은 것이다. 각 행은 묘사를 하기 위한 컬럼들을 가지고 있으며 (“desc” 컬럼이다.) 어떤 행은 전화번호를 자기고 어떤 것은 그렇지 않다. 같은 구조의 행을 묶는 관계형 테이블 같지 않게, 컬럼군과 super 컬럼군은 단지 비슷한 기록들을 묶는다.
CLI를 사용하여 우리는 super 컬럼 군을 다음과 같이 쿼리할 수 있다.
cassandra> get PointOfInterest['Central Park']['The Waldorf=Astoria']['desc']
=> (column=desc, value=Walk around in the park. It's pretty., timestamp=1281301988847)
이 쿼리는 어쩌다 Super 타입으로 정의된 PointOfInterest 컬럼군에서 행의 key “Central Park”라는 행의 key를 사용하며 묻고 있다. “Waldorf=Astoria” 라는 super 컬럼에서 “desc” 컬럼의 값을 가져다 준다. (이는 point of interest를 묘사하는 단순한 언어 텍스트이다).

3.11. 종합적 Keys
Super 컬럼을 모델링할 때 중요하게 고려해야 할 것이 있다. 카산드라는 서브컬럼을 인덱스 하지 않는다. 그래서 super 컬럼을 메모리로 로드할 때 그 모든 컬럼도 로드된다.
당신은 당신의 고유한 디자인에서 종합적인 key를 사용하여 쿼리를 할 때 도움이 될 수 있다. 종합적인 key는 과 같은 것이다.
이것은 당신이 모델링을 할 때 고려하는 것이 될 수 있으며, 이후에 하드웨어 크기를 정할 때 다시 확인 해야 할 것이다. 그러나 당신의 데이터 모델이 몇 천개의 서브컬럼을 필요로 한다면 당신은 다른 접근 방법을 택해서 super 컬럼을 사용하지 않을수도 있다. 대안으로는 종합적 key를 만드는 것이있다. Super 컬럼으로 컬럼을 나타내는 대신 종합적 key 접근방법은 당신이 보통 컬럼군을 보통 컬럼과 사용하여 custom delimiter를 키 이름에 사용하고 클라인언트 정보검색에 파싱하도록 하는 것이다.
여기 종합적인 key 패턴의 예가 있다. 카산드라 디자인 패턴의 예와 함께 사용되어 나는 Materialized View 라고 부르며 공통 카산드라 디자인 패턴과 함께 Valueless 컬럼이라고 부른다.
HotelByCity (CF) Key: city:state {
key: Phoenix:AZ {AZC_043: -, AZS_011: -}
key: San Francisco:CA {CAS_021: -}
key: New York:NY {NYN_042: -}
}
여기서 세가지 일이 발생한다. 첫째로 우리는 Hotel 이라는 다른 컬럼 군에 이미 호텔 정보를 정의하였다. 그러나 호텔 데이터를 정규화하지 않은 HotelByCity 라는 두번째 컬럼군을 만들수 있다. 우리는 이미 가지고 있는 정보를 되풀이 하게 되는데 그러나 그것을 RDBMS의 view 와 유사하게 저장을 한다. 이는 그것이 빠르고 직접적인 퀴리 쓰기를 하게 해주기 때문이다. 우리가 호텔을 도시로 찾는다는 것을 알았을 때 우리는 그 검색을 위한 행의 key를 정의하는 테이블을 만들 수 있다. 그러나 같은 이름의 도시를 가진 많은 주가 있기 때문에 단지 도시 만으로 행의 key를 이름짓지 않고 주와 함께 해야한다.
그리고 우리는 Valueless Column 이라고 불리는 다른 패턴을 사용한다. 우리가 알아야 할 것은 도시안에 어떤 호텔들이 있는가 이다. 그리고 더 이상 비정규화 할 필요는 없다. 그래서 컬럼의 이름을 값으로 사용한다. 그리고 컬럼은 해당하는 값을 갖지 않는다. 컬럼이 삽입되어지면 우리는 단지 비어있는 바이트 array를 저장한다.

3.12. RDBMS와 카산드라의 차이점을 디자인하기

카산드라의 모델과 RDBMS에서 가능한 쿼리 방법간에는 몇 가지 다른점이 있어서 이를 알아두는 것이 중요하다.

3.12.1. No Query 언어
SQL은 관계형 데이터베이스에서 쓰이는 표준 Query 언어이다. 카산드라는 쿼리 언어가 없다. RPC 직렬화 메커니즘을 통해 접근할 수 있는 Thrift API가 있을 뿐이다.

3.12.2. 비참고용의 진실성
카산드라는 참고용 진실성이라는 개념이 없어서 join이라는 개념도 없다. 관계형 데이터베이스에서 다른 테이블의 기록의 primary key를 테이블안의 외래 key로서 명시할 수 있다. 그러나 카산드라에서는 이런 것을 할 수 없다. 테이블에서 다른 엔티티에 관련된 ID를 저장하는 것이 아직 공통적인 디자인 요구사항이라해도 순차적 삭제가 아직 가능하지 않다.

3.12.3. 두번째 Index

여기서 당신이 호텔 프라퍼티를 위한 고유한 ID를 찾기 원한다고 할 때 두번째 인덱스가 왜 필요한지 살펴보자. 관계형 데이터베이스에서 아래와 같은 쿼리를 사용할 수 있다.
SELECT hotelID FROM Hotel WHERE name = 'Clarion Midtown';
이것은 당신이 찾는 호텔의 이름은 알지만 고유한 ID는 모를 때 사용해야할 쿼리이다. 이러한 퀴리를 전달받았을 때 관계형 데이터베이스는 당신이 찾는 값을 찾기 위해 각 행 열을 뒤지며 전체 테이블 스캔을 할 것이다. 하지만 이것은 당신의 테이블이 매우 크다면 매우 느릴 수 있다. 그래서 관계형 에서 이에대한 해결은 name 컬럼에 인덱스를 만들어 관계형 데이터베이스가 빠르게 찾아볼 수 있는 데이터의 복사본을 만드는 것이다. hotelID가 이미 고유한 primary key이기 때문에 자동으로 인덱스되고 그것이 primary index이다. 이름 컬럼에 또 다른 인덱스를 만드는 것은 두번째 인덱스를 만든다. 카산드라는 현재 이것을 지원하지 않는다.
카산드라에서 같은 것을 수행하기 위해서는 찾는 데이터를 가지고 있는 두번째 컬럼군을 만드는 것이다. 호텔 이름을 저장하기 위해 한 개의 컬럼군을 만들고 각 ID에 매핑한다. 두번째 컬럼군은 분명한 두번째 인덱스처럼 동작할 것이다.
두번째 인덱스를 지원하는 것은 카산드라 7.0에 추가되었다. 이것은 당신이 컬럼 값에 인덱스를 만드는 것을 허용한다. 그래서 만약 당신이 주어진 도시에 사는 사용자를 모두 보고 싶다면 두번째 인덱스 지원은 처음부터 다 하는 일을 하지 않도록 해줄것이다.

3.12.4. 정렬은 디자인 결정사항
RDBMS에서는 당신의 쿼리에서 ORDER BY 를 사용하므로써 기록이 반환되는 순서를 쉽게 바꿀수 있었다. 디폴트 정렬 순서는 설정할수 없다. 디폴트로 기록은 그것들이 쓰여진 순서대로 반환된다. 당신이 그 순서를 바꾸고 싶으면 쿼리를 바꾸어 어느 컬럼의 리스트이던지 정렬할 수 있다. 카산드라에서는 그러나 정렬은 다르게 다루어진다. 그것은 디자인 결정사항이다. 컬럼군 정의는 CompareWith 인자를 포함한다. 그것은 읽기에서 당신의 행을 정렬하는 순서를 말해주지만 쿼리당 설정가능한 것은 아니다.
RDBMS가 당신이 컬럼에 저장된 데이터 타입에 따라 정렬하는 것을 제한하는 반면, 카산드라는 바이트 array만을 저장하여 접근방법가 의미가 없게한다. 하지만 당신이 할 수 있는것은 컬럼이 몇가지 다른 타입(ASCII, Longinteger, TimestampUUID, lexicographically, etc.) 의 하나인것 처럼 정렬하는 것이다. 당신 자신의 플러그인할수 있는 비교자를 써서 정렬을 할수도 있다.
그밖에 SQL에 있는 것처럼 ORDER BY나 GROUP BY 같은 문장의 지원은 없다. SliceRange라고 하는 쿼리 타입이 있어서 4장에서 자세히 보겠지만 반전을 가능하게 한다는 점에서 ORDER BY 와 유사하다.

3.12.5. 비정규화
관계형 데이터베이스 디자인에서 정규화의 중요성을 가르침받는다. 이는 카산드라로 할 때는 데이터 모델이 비정규화 되었을 때 가장 좋은 퍼포먼스를 내므로 별로 장점이 아니다. 관계형 데이터베이스에서 어떤 회사가 비정규화된 데이터 상태로 끝내는 것은 자주 있는 일이다. 여기에는 두가지 공통적인 이유가 있다. 하나는 퍼포먼스이다. 회사들은 많은 join를 일년동안 수행하므로써만 그들이 원하는 퍼포먼스를 얻을 수는 없다. 그래서 알고 있는 쿼리에서 비정규화를 한다. 이것은 작동하게 된다. 하지만 관계형 데이터베이스가 디자인 되었던 것처럼 세밀한 동작은 하지 못한다. 그래서 궁극적으로는 관계형 데이터베이스를 사용하는 것이 이러한 환경에서 가장 좋은 것이 아닐까 질문하게 된다.
관계형 데이터베이스가 의도적으로 비정규화되는 두번째 이유는 비즈니스 문서구조가 기억을 필요로 하기 때문이다. 그것은 당신이 데이터가 시간이 지남에 따라 변할 수 있는 외부의 많은 테이블을 참조하는 둘러싸는 테이블을 가지고 있기 때문이다. 그러나 당신은 둘러싸는 문서들을 역사속의 스냅샷으로 보존할 필요가 있다. 여기서 공통적인 예로는 인보이스가 있다. 당신은 이미 고객과 제품 테이블이 있다. 그래서 그 테이블들을 참조하는 인보이스를 만들수 있다고 생각한다. 그러나 이것은 실제로는 안된다. 고객이나 가격 정보는 바뀔 수 있다. 그리고 당신은 인보이스의 진실성을 잊어버리게 된다.
관계형 세상에서 비정규화는 Codd의 정규화 폼을 위반한다. 그리고 우리는 그것을 피하려고 노력한다. 그러나 카산드라에서는 비정규화는 정상적인 것이다. 만약 데이터 모델이 간단하다면 필요하지도 않다. 그러나 그것을 두려워하지는 말라.
중요한 점은 데이터를 모델링하고 나서 쿼리를 쓰는 대신에 카산드라에서는 당신은 쿼리를 모델하고 데이터가 그 주위에서 정돈되도록 한다. 당신의 애플리케이션이 사용할 가장 공통적인 쿼리 패스를 생각하자. 그리고 그것을 지원하기 위한 컬럼 군을 만들자.
비방하는 사람들은 이것이 문제라고 한다. 그러나 당신이 관계형 도메인에서 그랬던 것처럼 당신의 애플리케이션에서 쿼리에 대해 열심히 생각하는 것은 너무 당연한 일이다. 당신이 잘못해서 양쪽 세계에서 문제를 가질 수 있다. 혹은 당신의 쿼리가 시간이 지남에 따라 바뀌고 당신의 데이터 셋을 업데이트 하기위해 일해야 할 것이다. 하지만 이것은 잘못된 테이블을 정하거나 RDBMS에서 추가적인 테이블을 원하는 것과 아무 다를게 없다.
Cloudkick이 카산드라에서 저장하고 데이터를 모니터링하는 흥미로운 기사를 보려면 http://www.cloudkick.com/blog/2010/mar/02/4_months_with_cassandra 를 보라.

3.13. 디자인 패턴

사람들이 카산드라를 디자인 패턴이라고 묘사되어질 말로 카산드라를 사용하는 공통적인 경우가 몇 가지 있다. 나는 이 공통적 패턴에 이름을 붙이겠다. Materialized View, Valueless Column, and Aggregate Key.

3.13.1. Materialized View

추가적인 쿼리를 나타내는 두번째 인덱스를 만드는 것은 일반적이다. SQL의 WHERE 문이 없기 때문에 당신의 데이터를 두번째 컬럼군에 씀으로써 그 쿼리를 나타내어 같은 효과를 볼 수 있다.
예를 들어 당신이 User 컬럼군을 가지고 있고 어떤 특정한 시에서 사용자를 찾고자 한다면 당신은 UserCity라는 두번째 컬럼군을 만들수 있고 거기에 사용자 데이터와 더불어 시를 key(사용자 이름 대신) 그리고 그 시에사는 사용자이름을 딴 컬럼을 가질 수 있다. 이것은 비정규화 테크닉이며 쿼리의 속도를 향상시키고 특별히 당신의 쿼리 주위에 데이터를 디자인하는 예이다. 이러한 사용법은 카산드라 세계에서는 일반적인 것이다. 당신이 어떤 한 도시의 사용자를 위하여 쿼리를 하려고 할 때, 당신은 User컬럼군을 쿼리하여 클라이언트에 많은 데이터 작업을 하지 않고 단지 UserCity 컬럼군을 쿼리하면 된다.
여기서 “materialized” 는 당신이 쿼리에 대답하기위해 필요한 것이 거기에 있고 원래 데이터를 찾아보지 않게 원래 데이터의 모든 복사본을 저장하는 것을 의미한다. 당신이 사용중인 컬럼 이름을 저장하기 위해 두번째 쿼리를 수행한다면 그것은 두번째 인덱스이다.
0.7 버전에서 카산드라는 두번째 인덱스를 위한 네이티브를 지원한다.

3.13.2. Valueless Column

우리의 User/UserCity 예제에 더 추가해보자. 우리가 User 컬럼군에 참조 데이터를 저장하기 때문에 두가지 일이 일어난다. 첫째, 당신은 고유하고 사려깊은 key를 가져야 할 필요가 있다. 그것은 참조적 진실성을 더하게 한다. 둘째, UserCity 컬럼군안의 컬럼 값을 필요로 하지는 않는다. 만약 당신이 Boise의 행의 key를 가진다면 컬럼 이름은 그 도시의 사용자의 이름이 될 수 있다. 당신의 참조 데이터가 User 컬럼군에 있어서 컬럼은 아무런 의미있는 값을 갖지 않는다. 당신은 단지 조립식의 리스트와 같이 그것을 사용할 뿐이며 하지만 당신은 참조 컬럼군에서 추가적인 데이터를 그 리스트에서 얻기 원할 수도 있다.

3.13.3. Aggregate Key

당신이 Valueless Column 패턴을 사용할 때, Aggregate Key 패턴을 또한 필요로 할 수도 있다. 이 패턴은 두개의 스칼라 값을 모으기 위한 구분자와 함께 함께 융합한다. 우리의 예제를 더 진행하기 위해, 도시 이름은 일반적으로 고유하지 않다. US안의 많은 주가 Springfield라는 도시가 있고, Paris, Texas, Paris, Tenessee가 있다. 그래서 여기서는 우리의 Materialized View 에서 사용될Aggregate Kye를 만들기 위해 주이름과 도시이름을 융합하는 것이 좋다. 이 키는 TX:Paris나 TN:Paris 같은 모양일 것이다. 많은 카산드라 사용자는 colon을 보통 구분자로 사용한다. 그러나 키에서 의미가 없는 다른 문자를 사용해도 된다.
3.14. 명심해야 할 것들
당신이 관계형 정신세계에서 카산드라 데이터 모델로 옮겨오기 위해서 노력할 때는 몇 가지 주의해야 할 것이 있다. 당신이 관계형 데이터베이스와 오랜 기간 일했다면 그리 쉬운 일은 아니다. 여기 몇 가지 포인트가 있다.
 당신의 쿼리를 시작하라. 당신의 애플리케이션이 어떤 쿼리를 필요로 하는지 묻고 당신이 관계형 세상에서 그랬던 것 처럼 데이터 모델링을 처음으로 하지 말고 그 주위의 데이터를 먼저 모델하라. 이것은 어떤 사람들에게는 충격적이다. 어떤 스마트한 사람들은 새로운 쿼리를 할 때 이 접근 방법은 문제를 일으키기도 한다고 말했다. 나의 대답은 그들이 왜 그들의 데이터 타입이 그들의 쿼리보다 더 고정되어 있다고 가정하는가 질문하는 것이다.
 당신은 매 쿼리에 타임스탬프를 제공해야 하기 때문에 많은 클라이언트와 싱크를 하는 전략이 필요하다. 이것은 카산드라가 가장 최근에 쓰기한 값을 결정하기 위해 타임스탬프를 사용하는 것이 중요하다. 여기서 Network Time Protocol (NPT) 서버를 사용하는 것이 한 개의 좋은 방법이다. 다시 한 번, 어떤 스마트한 사람들이 내게 물었다. 왜 서버가 시간을 관리하게 하지 않느냐고. 나의 대답은 대칭적인 분산 데이터베이스에서 서버측은 같은 문제를 가지고 있다는 것이다.

2011년 6월 21일 화요일

Cassandra (2/12)

2. Cassandra의 설치

카산드라가 새로운 용어들을 좀 정의하기 때문에 앞으로 나아가면서 좀 익숙하지 않은 용어들이 눈에 띨수 있다. 그러나 괜찮다. 여기서 원하는 것은 간단한 설정으로 빠르게 셋업을 하여 모든 것이 실행이 되는지 확인하는 것이다. 초반 소개 작업처럼 진행이 될 것이다. 그러고난 후에 한 발짝 물러나서 더 이해하도록 하자.

2.1. 바이너리를 설치하기
카산드라는 http://cassandra.apache.org 에서 다운로드 가능하다. 가장 최신버전을 링크를 통해서 다운받도록 하자. 바이너리의 이름은 apache-cassandra-x.x.x-bin.tar.gz이다. x.x.x 는 버전 번호를 뜻한다. 다운로드는 약 10메가 근처의 크기이다.

2.1.1. 다운로드 압축풀기

가장 쉬운 출발점은 바이너리를 다운로드 받는것이다. 일반적인 ZIP 유틸리티를 써서 압축을 풀수있다. 리눅스에서는 GZip 압축풀기 유틸리티가 미리 설치되어 있어야 한다. 윈도우즈에서는 WinZip이나 7-Zip 같은 상용화 애플리케이션이나 프리웨어를 사용한다. 7-Zip의 프리웨어는 http://www.7-zip.org 에서 받을 수 있다.
압축풀기 프로그램을 연다. ZIP 파일과 TAR 파일을 각 단계별로 압축해제해야 한다. Apache-cassandra-x.x.x 같은 폴더가 생기고 나면 카산드라를 실행할 준비가 끝났다.

2.1.2. 그 안에 무엇이 있는가?

Tarball을 압축해제하고 나면 서너개의 디렉토리를 포함하여 카산드라 배포본을 볼 수 있다.
- Bin
이 디렉토리는 카산드라를 실행하기 위한 실행파일과 커맨드라인 인터페이스(CLI) 클라이언트가 있다. 클러스터가 제대로 설정이 되어있는지 또 다른 유지보수 어플래케이션이 잘 실행하기 위한Nodetool을 실행하기 위한 스크립트도 있다. Nodetool에 대해서는 뒤에서 더 자세히 본다. SSTable (데이터파일)을 JSON으로 컨버팅하기 위한 스크립트도 있다.
- Conf
팩키지 루트아래에 있는 이 디렉토리는 설정을 위한 파일들이 들어있다. Storage-conf.xml에는 키스페이스와 컬럼군을 설정함으로써 당신의 데이터 저장소를 만들수 있는 곳이다. Log4j 프로퍼티를 통해 로깅 레벨을 설정할 수 있다. 6장에서 설정을 알아볼 때 더 자세히 논할 것이다.
- Interface
0.6 버전과 그 이전에는 이 디렉토리에는 Cassandra.thrift 라는 파일 하나만 존재했었다. 이 파일은 Remote Procedure Call (RPC) 클라이언트 API를 표현했다. 인터페이스는 Thrift 문법을 이용해 정의되어있고 클라이언트를 만들기위한 쉬운 방법을 제공한다. 카산드라가 지원하는 모든 기능을 보기위해 이 파일을 일반적인 텍스트 에디터로 열어본다. 카산드라가 Java, C++, PHP, Ruby, Python, Perl, C#과 같은 언어의 인터페이스를 이 인터페이스를 통해 클라이언트 지원함을 볼 수 있다.
- Javadoc
이 디렉토리에는 Java의 JavaDoc을 이용하여 만들어진 각종 문서와 웹사이트가 들어있다. JavaDoc은 자바코드에 직접 들어있는 커멘트들만을 보여주는 것이지 모든 문서가 들어있는 것은 아니다. 코드가 어떻게 구성되어있는지 보려면 유용할 것이다. 카산드라는 매우 훌륭한 프로젝트이지만 코드안의 커멘트는 제한적이다. 만약 자바에 익숙하다면 단순히 클래스 파일들을 직접 보는 것이 유용할 것이다. JavaDoc 파일을 읽기 위해서는 javadoc/index.html 파일을 브라우저에서 열어본다.
- Lib
이 디렉토리는 카산드라가 작동하기 위한 모든 외부 라이브러리들을 담고있다. 예를 들면 두개의 다른 JSON 라이브러리를 담고있고 구글 프로젝트와 몇 개의 아파치 라이브러리가 있다. 카산드라와 상호작용하기위해 Thrift 와 Avro RPC 도 포함하고 있다.

2.2. 소스로부터 빌드하기

카산드라는 빌드 스크립트로 Apache Ant를 사용하고 상호의존관계 관리를 위해서는 Ivy 플러그인을 사용한다.
Ant는 http://ant.apache.org에서 받을수 있다. 카산드라 빌드를 위해서 Ivy를 별도로 받을 필요는 없다.
Ivy는 Ant를 필요로한다. 소스에서 빌드를 하기위해 JDK 1.6.0_20 이나 그 이후 버전으로서 단지 JRE가 아니다. Ant가 tools.jar를 가지고 있지 않다는 메시지가 나오면 JDK가 모두 있지 않거나 패스를 틀린것이다.
만약 모든 소스를 받고 싶다면 Hudson 에서 받을 수 있다. http://hudson.zones.apache.org/hudson/job/Cassandra/ 위치이다.
만약 당신이 Git 팬이라면 읽기전용의 카산드라 버전을 아래 명령어를 통해서 받을 수 있다.
>git clone git://git.apache.org/Cassandra.git
Git는 리누스 토발즈가 만든 소스 코드 관리 시스템이다. 지금은 유명해졌고 Android, Fedora, Ruby, Perl 와 많은 카산드라 클라이언트에의해 사용되어진다. Ubuntu와 같은 리눅스 시스템에 있다면 Git을 이용하기 쉽다. 콘솔에서 그냥 >apt-get install git 을 치면 설치된다. 더 많은 정보를 위해서는 http://git-scm.com/ 을 방문하라.
Ivy가 모든 상관관계를 관리하기 때문에 소스코드가 있다면 빌드하기가 쉽다. 단지 당신이 루트 디렉토리에 있고 소스가 다운로드되어서 ant 프로그램을 실행하기만 하면 된다. 그것은 build.xml을 현재 디렉토리에서 찾아서 디폴트 빌드를 할 것이다. Ant와 Ivy가 나머지를 알아서 처리한다. Ant를 실행하고 소스 컴파일을 하기 위해서는 >ant 라고 타이프한다.
이제 모두 끝났다. Ivy가 모든 상관관계를 처리하고 Ant가 350개 정도되는 소스 파일을 빌드하고 테스트를 수행할 것이다. 만약 잘 된다면 BUILD SUCCESSFUL이라는 메시지를 보게된다. 잘 안되면 패스 설정이 모두 올바른지 확인한다. 당신이 다운로드 받은 소스가 컴파일 되는지 Hudson 보고서를 확인할 수 있다.
만약 빌드되면서 자세한 정보를 보고 싶으면 -v 로 Ant에 옵션을 켜주면 된다.

2.2.1. 추가적인 빌드 타겟

서버를 컴파일하기 위해 ant를 위에서 본 것 처럼 실행해주면 된다. 하지만 빌드 파일에서 당신이 흥미를 가질 만한 다른 사항들이 있다.
- Test
유닛 테스트들을 실행하기 때문에 사용자가에게 가장 유용할 것이다. 카산드라와 상호작용하는 좋은 예제로서 소스파일들을 볼 수도 있다.
- Get-thrift-java
자바 데이터베이스와 상호작용하기위해 아파치 Thrift 클라이언트 인터페이스를 생성한다.
- Gen-thrift-py
파이선 사용자를 위한 Thrift 클라이언트 인터페이스를 생성한다.
- Build-jar
Java Archive (JAR)를 배포하기위해 >ant jar를 실행한다. 이것은 빌드를 완전히 몇 가지 실행하고 빌드 디렉토리 apache-cassandra-x.x.x.jar 에 아웃풋 파일을 생성한다.

2.2.2. Maven으로 빌드하기

초기 카산드라를 만든 사람들은 Maven에 대해서 많은 신경을 쓰지 않은 것 같다. 이른 버전의 카산드라는 Maven POM 파일을 포함하고 있지 않다. 하지만 많은 자바 개발자들이 Ant보다 Maven을 사용하기 하고 IDE에서 Maven을 지원하자 pom.xml 파일이 생겨서 이제 원하면 Maven으로 빌드할 수 있다.
Maven에서 빌드하기 위해서는 /contrib./maven으로 이동하여 아래 명령을 수행한다.
$mvn clean install
Maven으로 빌드하는데 어려움이 있다면 필요한 JAR파일을 수동으로 얻을 수도 있다. 버전 0.6.3 에서는 libthrift.jar와 같은 리파지토리에 존재하지 않는 파일에 대한 의존성 때문에 Maven POM이 제대로 작동하지 않을수 있다.

2.3. Cassandra 실행하기

카산드라의 이전 버전에서는 실제로 서버를 실행하기 전에 Ivy에서 몇 가지 일을 하고 설정 변수를 몇 개 맞추어 주어야 했다. 하지만 개발자들이 노력하여 이제 시작하는 것을 매우 쉽게 해 놓았다.


2.3.1. Windows 에서

당신이 바이너리를 가지고 있거나 소스를 다운로드해서 컴파일했다면 이제 데이터베이스 서버를 시작할 준비가 되어있다.
JAVA_HOME 환경 변수를 또 설정해야 할 필요가 있을 것이다. Windows 7에서 이것을 하기 위해서 시작 버튼을 누르고 컴퓨터를 오른쪽 클릭한다. 그리고 Advanced System Settings 를 클릭하고 환경 변수를 클릭한다. 새로 만들기 버튼을 클릭하여 새로운 시스템 환경 변수를 만든다. 변수 이름에 JAVA_HOME을 쓴다. 그리고 값에는 JDK 가 설치된 위치를 넣는다. 이것은 C:\Program Files\Java\jdk1.6.0_20과 같은 것일 것이다. 환경 변수를 새로 설정했을때는 열려있는 터미널 등도 다시 열어서 환경 변수가 제대로 인식되는지 검사해 본다. 제대로 설정이 되었는지 보기위해서 >echo %JAVA_HOME% 를 터미널에서 실행해본다. 이것은 당신의 환경변수의 내용을 프린트할 것이다.
카산드라를 처음 실행하면 시스템에 디렉토리 두개를 추가할 것이다. 첫번째는 C:\var\lib\cassandra 인데 commitlog라고 불리는 데이터 파일을 저장하는 곳이다. 다른 것은 c:\var\log\cassandra 인데 system.log 라는 파일에 로그를 기록할 것이다. 만약 무슨 어려움을 만난다면 이 디렉토리에 있는 파일들을 참조하여 무슨 일이 생기는지 알수있다.

2.3.2. Linux 에서

리눅스에서도 윈도우즈에서와 비슷하다. JAVA_HOME이 1.6.0_20 이상 버전에 잘 설정되어 있는지 확인한다. 그리고 Cassandra 압축을 gunzip을 사용하여 풀어준다. 그리고 데이터와 로그를 저장하는 몇 개 디렉토리를 만들어 준다.
ehewitt@morpheus$ cd /home/eben/books/cassandra/dist/apache-cassandra-0.7.0-beta1
ehewitt@morpheus$ sudo mkdir -p /var/log/cassandra
ehewitt@morpheus$ sudo chown -R ehewitt /var/log/cassandra
ehewitt@morpheus$ sudo mkdir -p /var/lib/cassandra
ehewitt@morpheus$ sudo chown -R ehewitt /var/lib/cassandra
물론 ehewitt 대신에 당신의 사용자 이름을 넣어준다.

2.3.3. 서버 시작하기
어떤 OS에서든 카산드라를 시작하기 위해 터미널에 커맨드를 열고 /bin으로 이동한다. 그리고 아래 커맨드를 시작하기 위해 입력한다. 제대로 설정되었으면 아래와 같은 로그를 보게된다.
eben@morpheus$ bin/cassandra -f
INFO 13:23:22,367 DiskAccessMode 'auto' determined to be standard, indexAccessMode
is standard
INFO 13:23:22,475 Couldn't detect any schema definitions in local storage.
INFO 13:23:22,476 Found table data in data directories.
Consider using JMX to call org.apache.cassandra.service.StorageService
.loadSchemaFromYaml().
INFO 13:23:22,497 Cassandra version: 0.7.0-beta1
INFO 13:23:22,497 Thrift API version: 10.0.0
INFO 13:23:22,498 Saved Token not found. Using qFABQw5XJMvs47lg
INFO 13:23:22,498 Saved ClusterName not found. Using Test Cluster
INFO 13:23:22,502 Creating new commitlog segment /var/lib/cassandra/commitlog/
CommitLog-1282508602502.log
INFO 13:23:22,507 switching in a fresh Memtable for LocationInfo at CommitLogContext(
file='/var/lib/cassandra/commitlog/CommitLog-1282508602502.log', position=276)
INFO 13:23:22,510 Enqueuing flush of Memtable-LocationInfo@29857804(178 bytes,
4 operations)
INFO 13:23:22,511 Writing Memtable-LocationInfo@29857804(178 bytes, 4 operations)
INFO 13:23:22,691 Completed flushing /var/lib/cassandra/data/system/
LocationInfo-e-1-Data.db
INFO 13:23:22,701 Starting up server gossip
INFO 13:23:22,750 Binding thrift service to localhost/127.0.0.1:9160
INFO 13:23:22,752 Using TFramedTransport with a max frame size of 15728640 bytes.
INFO 13:23:22,753 Listening for thrift clients...
INFO 13:23:22,792 mx4j successfuly loaded
HttpAdaptor version 3.0.2 started on port 8081

-f 옵션을 켜면 카산드라에게 백그라운드 프로세스로 실행하지 않고, 포어그라운드에 머물러서 표준 출력에게 모든 로그를 출력한다.
축하한다. 당신의 카산드라 서버는 Test Cluster라는 한 개의 노드로 9160 포트를 열고 실행이 되었다.

2.4. Command-Line 클라이언트 인터페이스 실행하기

카산드라 설정을 한 개 실행시켰다. 모든 것이 잘 설정이 되었는지 확인해보자. 리눅스에서 커맨드 라인 인터페이스는 잘 작동할 것이다. 윈도우즈에서는 몇 가지 더 해줄일이 있다.
윈도우즈에서 카산드라 홈 디렉토리로 이동해서 터미널을 새로 연후 다음을 실행해준다.
>bin\cassandra-cli
윈도우즈에서는 클라이언트를 시작할 때 아래와 같은 에러를 볼 수 있다.
Starting Cassandra Client
Exception in thread "main" java.lang.NoClassDefFoundError:
org/apache/cassandra/cli/CliMain
이는 당신이 카산드라를 bin 디렉토리에서 곧바로 실행했음을 의미한다. 그리고 Java 클래스패스를 잘 설정하지 못하여 CliMain을 찾지 못하는 것이다. CASSANDRA_HOME이라는 환경변수를 카산드라를 설치한 최상위 디렉토리로 설정하여 카산드라를 어디서 시작하든지 신경을 별로 쓰지 않도록 한다.
리눅스에서 커맨드 라인 인터페이스를 실행하기 위해서 카산드라 홈으로 이동하고 Cassandra-cli 를 bin 디렉토리에서 실행한다.
>bin/Cassandra-cli
카산드라 클라이언트가 시작한다.
eben@morpheus$ bin/cassandra-cli
Welcome to cassandra CLI.
Type 'help' or '?' for help. Type 'quit' or 'exit' to quit.
[default@unknown]
이제 커맨드를 실행할 수 있는 상호작용 쉘이 열린것이다.
하지만 당신이 Oracle SQL Plus에 익숙해 있다면 당황할 것이다. 카산드라 CLI는 개발에 적합한 것이다. 그래서 카산드라를 시작하기에는 적당하다.

2.5. 기본 CLI 명령들

카산드라에 대해서 더 깊이 공부하기전에 클라이언트 API의 오버뷰를 통해서 당신이 서버에게 보낼수 있는 명령어 들에 대해 알아본다. 우리는 기본 환경 커맨드를 보고 데이터를 집어넣고 빼고 하는 것을 본다.

2.5.1. Help

커맨드 라인 인터페이스에서 도움말을 받기 위해서는 help 나 ?를 타이핑하여 쓸수 있는 명령어들의 목록을 본다. 아래 목록은 메타데이터와 설정에 관한 것을 보여준다. 다른 명령어들도 있어서 값들을 설정하거나 읽을 수 있으며 그것은 나중에 보도록 한다.
[default@Keyspace1] help
List of all CLI commands:
? Display this message.
help Display this help.
help Display detailed, command-specific help.
connect / Connect to thrift service.
use [ 'password'] Switch to a keyspace.
describe keyspace Describe keyspace.
exit Exit CLI.
quit Exit CLI.
show cluster name Display cluster name.
show keyspaces Show list of keyspaces.
show api version Show server API version.
create keyspace [with = [and = ...]]
Add a new keyspace with the specified attribute and value(s).
create column family [with = [and = ...]]
Create a new column family with the specified attribute and value(s).
drop keyspace Delete a keyspace.
drop column family Delete a column family.
rename keyspace Rename a keyspace.
rename column family Rename a column family.

2.5.2. 서버에 연결하기

클라이언트를 이렇게 시작하는 것이 카산드라 서버에 자동으로 연결을 해주는 것은 아니다. 카산드라를 시작한 후에 특정 서버에 연결하기 위해서는 connect 명령을 사용한다.
eben@morpheus:~/books/cassandra/dist/apache-cassandra-0.7.0-beta1$ bin/cassandra-cli
Welcome to cassandra CLI.
Type 'help' or '?' for help. Type 'quit' or 'exit' to quit.
[default@unknown] connect localhost/9160
Connected to: "Test Cluster" on localhost/9160
[default@unknown]
간략하게 클라이언트를 시작하고 특정서버에 호스트와 포트 등 파라미터를 넘겨줌으로써 연결할 수 있다. 다음과 같다.
eben@morpheus:~/books/cassandra/dist/apache-cassandra-0.7.0-beta1$ bin/
cassandra-cli localhost/9160
Welcome to cassandra CLI.
Type 'help' or '?' for help. Type 'quit' or 'exit' to quit.
[default@unknown]
CLI가 Test Cluster라는 카산드라 서버에 연결되었다고 보여준다. localhost라는 한 개의 노드가 있는 클러스터가 디폴트로 설정되어 있기 때문이다.

2.5.3. 환경을 기술하기

Test Cluster라는 카산드라에 접속하고 바이너리 배포판을 사용한다면 비어있는 keyspace 나 카산드라 데이터베이스가 테스트하기 위해 설정되어 있을 것이다.
현재 사용중인 클러스터의 이름을 보고싶다면 다음과 같이 타이프한다.
[default@unknown] show cluster name
Test Cluster
클러스터에서 어떤 keyspace가 쓸 수 있는지 보려면 다음을 친다.
[default@unknown] show keyspaces
system
만약 당신이 당신만의 keyspace를 만들었다면 그것도 역시 보여질 것이다. System keyspace는 카산드라에 의해서 내부적으로 쓰여질 것이고 우리가 데이터를 쓸수는 없다. Microsoft SQL 서버의 마스터와 임시 데이터베이스와 같다. 이 keyspace는 스키마 정의를 담고있고 런타임시에 수정되는 스키마에 대한 모든 사항을 알고 있다. 이것은 타임스탬프에 따라 한 개의 노드에 만들어진 변경사항을 클러스터 전체에 알려지게 한다. 당신이 사용하는 API의 버전을 보기위해서는 다음과 같이 타이핑한다.
[default@Keyspace1] show api version
10.0.0
실험해 볼 다른 명령어 들도 있다. 지금은 데이터베이스에 데이터를 추가해보고 다시 얻어보자.

2.5.4. Keyspace 와 컬럼군을 만들기

카산드라의 keyspace는 관계형 데이터베이스와 유사하다. 한 개나 두개의 컬럼군을 정의하여 관계형 세상에서와 대강 유사한 테이블을 만든다. 당신이 특별한 keyspace를 설정하지 않고 CLI 클라이언트를 시작하면 출력은 다음과 같을 것이다.
>bin/cassandra-cli --host localhost --port 9160
Starting Cassandra Client
Connected to: "Test Cluster" on localhost/9160
Welcome to cassandra CLI.
Type 'help' or '?' for help. Type 'quit' or 'exit' to quit.
[default@unknown]
당신이 키스페이스와 특정 사용자를 정의하지 않았기 때문에 쉘 프롬프트는 default@unknown 이다. (6장에서 방법을 살펴볼 것이다.)
우리의 keyspace를 만들어보자.
[default@unknown] create keyspace MyKeyspace with replication_factor=1
ab67bad0-ae2c-11df-b642-e700f669bcfc
replication_factor에 대해서는 현재로서는 신경을 쓰지 말자. 그것은 차후에 자세히 볼 사항에 대해서 설정한다. 당신의 keyspace를 만든후에 쉘에서 다음을 타이핑하여 전환할 수 있다.
[default@unknown] use MyKeyspace
Authenticated to keyspace: MyKeyspace
[default@MyKeyspace]
MyKeyspace는 어떤 크리덴셜을 필요로 하지 않으므로 이것을 사용할 수 있게 인증되어 있는 상태이다.
우리는 이제 우리의 keyspace에 컬럼군을 만들수 있다. CLI에서 이것을 하기위해 아래의 명령을 사용한다.
[default@MyKeyspace] create column family User
991590d3-ae2e-11df-b642-e700f669bcfc
[default@MyKeyspace]
이것은 User 라는 컬럼군을 우리의 keyspace에 만든다. 그리고 컬럼군 셋팅의 디폴트값을 갖는다. 우리는 CLI에서 describe keyspace 명령을 통해서 keyspace 정보를 보고 우리가 원하는 keyspace를 가지고 있는지 본다. 다음과 같다.
[default@MyKeyspace] describe keyspace MyKeyspace
Keyspace: MyKeyspace
Column Family Name: User
Column Family Type: Standard
Column Sorted By: org.apache.cassandra.db.marshal.BytesType
flush period: null minutes
------
[default@MyKeyspace]
Type, Sorted By, flush period 에 대해서는 이후에 본다. 당장은 시작하는데 충분하다.

2.5.5. 데이터를 쓰고 읽기

이제 keyspace와 컬럼군을 가지고 있다. 이제 데이터를 쓰고 다시 읽어보겠다. 현재 여기서는 어떻게 되어가는지 자세히 알 필요는 없다. 카산드라의 데이터 모델에 관해서는 차후에 살펴본다. 당신은 현재 컬럼군을 가진 keyspace(데이터베이스) 를 가지고 있다. 여기서는 컬럼군을 다차원의 지도와 같이 생각하고 미리 정의할 필요는 없다. 컬럼군은 컬럼을 가지고 있고 컬럼은 데이터 저장할 수 있는 한 개의 유닛이다.
어떤 값을 쓰기위해서 set 명령을 사용한다.
[default@MyKeyspace] set User['ehewitt']['fname']='Eben'
Value inserted.
[default@MyKeyspace] set User['ehewitt']['email']='me@example.com'
Value inserted.
[default@MyKeyspace]
여기 ehewitt 라는 키를 위한 두 개의 컬럼을 만들었다. 컬럼의 이름은 fname과 email이다. Count 명령을 사용하여 한 개의 키에 두개의 컬럼을 썼는지 볼수 있다.
[default@MyKeyspace] count User['ehewitt']
2 columns
Get 명령을 사용하여 데이터를 읽어보자.
[default@MyKeyspace] get User['ehewitt']
=> (column=666e616d65, value=Eben, timestamp=1282510290343000)
=> (column=656d61696c, value=me@example.com, timestamp=1282510313429000)
Returned 2 results.
Del 명령을 사용하여 컬럼을 지울수도 있다. 여기서 ehewitt의 email 컬럼을 지워본다.
[default@MyKeyspace] del User['ehewitt']['email']
column removed.
여기서는 모든 로우를 지워본다. 명령은 같지만 컬럼 이름을 지정하지 않는다.
[default@MyKeyspace] del User['ehewitt']
row removed.
지워진 것을 확인하기 위해서 다음과 같이 쿼리해볼수 있다.
[default@Keyspace1] get User['ehewitt']
Returned 0 results.