[발 번역] Cassandra의 Write 에 관하여

해당 블로그는 KT UCloud의 지원을 받습니다.

최근 몇일 동안 갑자기 Cassandra의 Read/Write Path 에 대해서 궁금해져서 소스를 파게 되었는데, 그걸 정리하기 전에 공식 문서에 정리된 내용을 한번 더 정독하는게 좋을듯 해서 해당 글을 번역하게 되었습니다. 해당 글은 http://www.datastax.com/docs/1.1/dml/about_writes 발 번역한 것입니다. 오역에 주의하세요.

카산드라는 매우 빠르고, 고가용성을 가지는 쓰기에 최적화되어 있습니다. 관계형 데이테베이스는 일반적으로 최소한의 중복만 유지하기 위해서 테이블을 구조화합니다. 쿼리에 적합한 데이터를 제공하기 위해서, 정보는 여러 조각으로 나뉘어서 미리 정의된 여러 테이블에 저장되게 됩니다. 관계형 데이터베이스의 데이터 구조 때문에, 데이터 쓰기는 여러 연관된 테이블들의 데이터 정합성을 보장하기 위해서 추가적인 작업을 하는 만큼 느립니다. 그 결과, 관계형 데이터베이스는 일반적으로 쓰기가 빠르지 않습니다.

반대로, 카산드라는 쓰기 성능에 최적화 되어 있습니다. 카산드라의 쓰기는 처음에 Commit Log 에 쓰여집니다.(데이터 안전성을 위해서), 그리고, memtable이라고 부르는 메모리 내 테이블 구조에 저장합니다. 쓰기는 Commit Log와 메모리에 쓰여지면 성공하게 됩니다. 그래서, 최소한의 Disk I/O만 쓰기시에 발생합니다.( 역자 주: Commit Log를 쓰는 것도 설정할 수 있습니다. )  쓰기는 메모리내에서 배치작업형태로 , 주기적으로 SSTable(Sorted String Table) 로 만들어서 디스크에 저장합니다. Column family 마다  Memtable 과 SSTable 을 유지합니다.( 역자 주: ColumnFamilyStore 안에 Memtable 이 존재하고 Memtable은 ConcurrentSkipListMap 형태로 Key를 관리합니다. 실제로 flush 시에 SSTable은 Memtable의 데이터를 단순히 순회하면서, 출력하는 작업을 하게 됩니다. )  Memtable은 row key에 의해서 정렬되어서 저장되어 있고, SSTable 에 순차적으로 저장하게 됩니다.( 관계형 데이터베이스에서의 no random seeking )

SSTable 은 변경 이 불가능합니다.( flush가 되고 나면 다시 쓸 수 없습니다. ) 이것은 일반적으로 하나의 Row 가 여러개의 SSTable 파일들에 걸쳐서 저장된다는 것을 의미합니다. 읽을 때는, 요청이 들어왔을 때 하나의 Row는 반드시  디스크의 전체 SSTable 과, 아직 flush되지 않은 memtable 들에서 읽은 데이터를 결합해서 만들어지게 됩니다.(역자 주: HBase 도 그렇지만, 카산드라는 그 정도가 더더욱 심합니다. 최악의 경우에는 정말 극악의 읽기 속도가 예상되는 부분입니다.) 이런 문제를 보완하기 위해서, 카산드라는 Bloom filter 라고 부르는 메모리내 데이터 구조를 사용합니다. 각각의 SSTable은 Bloom Filter 와 연결되어 있고, row 키가 요청되었을 때, SSTable에 존재하는지를 디스크를 찾기 전에 Bloom Filter를 통해서 체크합니다.

카산드라에서 클라이언트의 요청을 어떻게 처리하는지에 대해서 자세히 알고 싶은 분은 About Client Requests in Cassandra 를 보시기 바랍니다.

Managing Stored Data

카산드라는 현재 column family들을 디스크에 저장할 때, 디렉토리와 파일명의 포맷을 다음과 같은 형태로 합니다.

/var/lib/cassandra/data/ks1/cf1/ks1-cf1-hc-1-Data.db

(역자 주: ks는 key space, cf 는 column family 입니다. ) 카산드라는 각각의 Column Family를 위한 디렉토리를 생성하고, 개발자나 관리자에게 다른 데이터 볼륨이나 물리 드라이브에 링크를 거는 것을 허용합니다. 이것은 매우 접근이 많은 Column Family들을 SSD 등의 더 좋은 퍼포먼스를 내는 장비에 옮기는 것이 가능하게 해주고, column family 에 따라서 스토리지 레이어에서 더 좋은 I/O 밸런스를 가지도록 스토리지 디바이스를 나눠주는게 가능해줍니다. ( 역자 주: 카산드라는 그냥 로컬 장비의 디스크의 파일을 이용하므로 이런 것이 가능합니다. )

bulk loading을 더 쉽게 하기 위해서 keyspace 이름이 SST 이름의 한 부분을 차지합니다. 더 이상 sstable 을 keyspace 이름으로 만들어진 디렉토리에 넣거나,  클러스터를 분리하기 위해서 새로운 sstable을 생성할 때 keyspace 에 대해서 고민할 없습니다

About Compaction

백그라운드에서, 카산드라는 정기적으로 SSTabe 들을 더 큰 SSTable로 합치는 작업을 진행하고 있고, 이를 Compaction 이라고 부릅니다. Compaction 은 조각난 row 들을 합쳐주고, tombstone 들을 제거하고,  주 인덱스와 보조 인덱스 들을 재구성합니다.(역자 주: SSTable이 immutable 하므로, 삭제는 tombstone이라고 해서, 해당 컬럼이 삭제되었다는 추가 정보를 넣어서 lazy delete 형태로 처리합니다. ) SSTable이 row key로 정렬되어있기 때문에, 머지 작업이 매우 효과적입니다.( Random I/O가 없으므로 ) 새로운 SSTable이 머지가 끝나서 만들어졌을 때, 원본 SSTable 들은 더 이상 쓰지 않는다고 표시되고, 나중에 JVM garbage collection(GC) 프로세스에 의해서 제거됩니다. 그러나, Compaction 을 하는 동안에는  disk 공간과 disk I/O를 일시적으로 많이 사용합니다.

Compaction 은 읽기 성능에 두 가지 형태로 영향을 미칩니다.(역자 주: 한 row 자체가 여러개로 나눠져서 들어갈 수 있으므로, 대충 어떤 얘기를 할지 생각해보시면 될듯 합니다.) compaction이 진행되는 동안에 일시적으로 disk I/O와 디스크 사용율이 증가합니다. 이로 인해서 cache에 있지 않은 데이터에 대한 read의 경우 read 성능이 영향을 받습니다. 그러나, compaction 가 완료된 후에는, cache에 없는 데이터라도 read 성능이 증가됩니다. read 요청을 처리하기 위해서 몇 개의 SSTable 파일만 체크하면 되기 때문입니다.

카산드라 1.0의 경우, Column family-size-tiered compaction 과 Leveed Compaction의 두 가지 다른  설정할 수 있는 compaction 전략이 있습니다. 해당 Compaction 전략에 대한설명은  Tuning Compaction 을 보시기 바랍니다

카산드라 1.1 부터 실제 서비스 환경에 부담을 주지 않고 다양한 compaction 전략을 테스트 할 수 있도록 옵션이 추가되었습니다. Testing Compaction and Compression 를 보시기 바랍니다. 게다가, Compaction을 멈출 수도 있습니다.

About Transactions and Concurrency Control

카산드라는 완전한 ACID-compliant 트랜잭션을 제공하지 않습니다. 관계형 DB에서 트랜잭션은 다음과 같습니다.

  • Atomic. 트랜잭션내의 모든 작업이 성공하거나 아니면 완전하게 롤백되어야 한다.
  • Consistent. 하나의 트랜잭션은 데이터베이스를 불일치하는 형태로 남겨두면 안된다.
  • Isolated. 트랜잭션이 다른 트랜잭션에 영향을 주면 안된다.
  • Durable. 서버 장애나 다른 종류의 문제에도 완료된 트랙잭션은 문제가 없어야 한다.

비관계형 데이터베이스처럼, 카산드라는 join이나 foreign key 를 제공하지 않고, 당연히 ACID 의 consistency도 제공하지 않습니다. 예를 들어서, 계정 A에서 계정 B로 돈을 옮긴다면, 전체 어카운트의 돈은 바뀌지 않습니다. 카산드라는 atomicity 와 isolation은 row 수준에서만 제공합니다. 트랜잭션 isolation 과  atomicity 를 높은 가용성과 빠른 쓰기를 위해서 포기했습니다. 카산드라의 Write는 durable 합니다.

Atomicity in Cassandra

카산드라에서 row 단위에서는 쓰기가 atomic 합니다. 즉 하나의 주어진 row에 대한 insert나 update는 하나의 쓰기 작업으로 다루어집니다. 카산드라는 여러개의 row 사이에 발생하는 update에 대해서는 트랜잭션을 제공하지 않습니다.( 역자 주: all or nothing, 다 되거나, 아예 안되거나 하는 transaction의 특성을 제공하지 못합니다. ) 하나의 리플리카에 성공적으로 썼을 때는 롤백을 하지만, 다른 리플리카에 대해서는 실패하게 됩니다. 카산드라에서는 클라이언트에 쓰기 실패에 대해서 보고하지만, 실제로 하나의 리플리카에 데이터가 남아있을 수 있습니다.

예를 들어서, 복제 개수가 3으로 설정된 상황에서 QUORUM 을 일관성 레벨로 설정해서 쓰기를 하게 되면, 카산드라는 2개의 리플리카에 쓰기를 요청할 것입니다. 만약에 하나의 리플리카에 쓰기가 실패하고 다른 한곳에는 성공하면, 카산드라는 쓰기 실패를 클라이언트에게 알려줄 것입니다. 하지만, 다른 리플리카에서 자동적으로 롤백되지는 않습니다.

카산드라는 timestamp를 컬럼에 최신 데이터를 판단하기 위해서 사용하는데, 해당 timestamp는 클라이언트 어플리케이션에서 제공한 것입니다. 하나의 row에 대해서 동시에 여러 개의 클라이언트들이 업데이트를 요청하면 가장 최신의 timestamp를 가진 요청이 항상 적용될 것입니다. 가장 최근의 업데이트가 결국 디스크에 저장되게 됩니다.( 역자 주: 결국 이런 상황이면 만약에 데이터 읽기를 ONE으로 이용해서 사용하면 실패한 결과를 읽을 수 도 있다라는 뜻이됩니다.  만약에 읽기도 QUORUM이라면 읽는 시점에, 값이 다른 하나는 보정되게 될 것입니다. )

Tunable Consistency in Cassandra

여러개의 row나 column family들에 대해서 동시에 업데이트가 발생할 때 적용할 수 있는 Lock이나 트랜잭션 전략이 카산드라에는 없습니다. 카산드라에서는 가용성과 일관성 사이를 설정할 수 있는 기능을 제공합니다.(tuning between availability and consistency) 그리고 항상 Partition tolerance 기능을 제공합니다. 카산드라는 분산 데이터베이스 클러스터내에서 모든 노드들이 CAP 이론에서 말하는 Strong Consistency를 주도록 설정할 수 있습니다. 유저는 하나의 오퍼레이션에 얼마나 많은 노드들이 DML 명령이나 SELECT query에 응답하도록 설정할 수 있습니다

Isolation in Cassandra

Cassandra 1.1 이전에는, 다른 유저가 같은 row를 읽는 동안에 다른 유저가 해당 row를 부분적으로 업데이트 하는 것이 가능했습니다. 만약 한 유저가 2000개의 컬럼과 하나의 row를 쓰고 있는 중에, 다른 유저가 해당 row 와 컬러중에 일부를 읽을 수 있습니다. 하지만, 전부는 아니지만 쓰기 작업은 계속 진행 중 일 수도 있습니다.

한 유저가 쓰기를 수행중에 다른 유저가 완료되기 전까지는 변경 중 내용을 볼 수 없게 하는 완벽한 row 단위의 isolation은 현재 적용중입니다.

전통적인 ACID((atomic, consistent, isolated, durable)) 관점에서, 해당 개선으로 카산드라는 트랙잭션의 AID를 지원하게 되었습니다. 쓰기 작업은 스토리지 엔진에서 row 단위로 분리되어 있습니다.

Durability in Cassandra

카산드라의 쓰기는 안전합니다. 복제 노드의 모든 쓰기는 성공을 리턴하기 전에 메모리와 Commit Log 양쪽에 저장됩니다. 메모리 테이블을 디스크로 저장하기 전에 서버 장애나 뭔가 충돌이 생기면, 재 시작시에 Commit Log를 이용해서 잃어버린 테이터를 모두 복구합니다.

About Inserts and Updates

동시에 여러 개의 Column들이 insert 될 수 있습니다. Column이 하나의 Column Family에 insert나 update 될 때, 클라이언트 애플리케이션은 어떤 Column의 내용을 업데이트 할 것인지 row key를 통해서 확인하게 됩니다. Row Key는 하나의 Column Family 내에서 각각의 row를 유일하게 구분해주는 primary key와 유사합니다. 그러나 primary key와는 다르게, 중복된 row key를 통한 insert는 primary key 중복 제한 위반이 발생하지 않습니다. 그 때는 UPSERT( 데이터가 없으면 insert, 데이터가 있으면 update로 동작하는 ) 로 다뤄집니다.

 

컬럼은 오직 현재 존재하는 버전보다 최신의 새 버전의 timestamp를 가질 때만 update 됩니다. 그래서 만약 update가 자주 발생한다면, 정확한 timestamp 가 필요합니다.  timestamp는 클라이언트에게서 전달받기 때문에, 모든클라이언트 장비는 NTP(network time protocol)를 통해서 동기화 시켜야 합니다.

 

About Deletes

카산드라에서 Row 나 Column 을 지울 때, 관계형 데이터베이스 일어나는 동작과 비교해서 다른 부분이 몇가지 있습니다.

When deleting a row or a column in Cassandra, there are a few things to be aware of that may differ from what one would expect in a relational database.

  1. 삭제된 데이터는 즉시 디스크에서 지워지지 않는다. 카산드라에 들어가 있는 데이터는 디스크의 SSTable에 저장되어 있습니다. SSTable이 쓰여질 때, SSTable은 immutable(파일이 더 이상 DML 오페레이션으로 업데이트가 되지 않는) 상태입니다. 이것은 삭제된 Column이 바로 사라지는 것이 아니라, tombstone 이라고 불리는 삭제 마크가 새로운 Column 정보를 가리키게 됩니다. tombstone에 설정된 삭제된 컬럼은 설정에 기록된 시간동안 존재하게 됩니다.( Column Family 에 gc_grace_seconds 라고 설정되어 있습니다. ), 해당 시간이 지나면 compaction 과정에서 실제 디스크에서 삭제되게 됩니다.
  2. 정기적인 node 복원 작업이 동작하지 않으면삭제된 컬럼이 다시 나타날 수 있다. tombstone 에서 삭제된 컬럼을 마킹하는 것은 replica가 다운되었더라도 나중에 해당 시간동안의 삭제 정보를 노드가 복구되었을 때 삭제 정보를 다시 받을 수 있는 것을 보장합니다. 하지만, 해당 노드가 tombstone 정보를 저장하는 시간보다 오래 다운되어 있다면( ColumnFamily의 gc_grace_seconds  에 정의되어 있습니다. ) 삭제 했다는정보를 전부 잃어버릴 수도 있습니다. 그리고 삭제된 데이터가 노드가 복구되면서 삭제되었던 정보가 다시 복제되어서 데이터가 다시 나타날 수 있습니다. 지워진 데이터가 다시 살아나는 현상을 방지하기 위해서, 관리자는 반드시 , 크러스터의 모든 클러스터의 노드에 정기적으로 복원작업을 해주어야 합니다.(기본적으로 10일마다 한번씩)
  3. 삭제된 row key는 range query 에서 여전히 나타날 수 있다. 카산드라에서 row를 하나 삭제했을 때, 해당 row key를 위한 모든 컬럼이 compaction에 의해서 해당 tombstone들이 삭제될 때 까지 tombstone 에 표시됩니다. 만약 row key에 어떤 컬럼도 없는 빈 row key를 가지고 있다면, 이 삭제된 key들은 get_range_slices()의 결과로 보여질 수 있습니다. 클라이언트 어플리케이션이 해당 row들에 range queries를 수행한다면, 반환되는 빈 컬럼 리스트에 대해서 필터링을 수행해야 할 수 있습니다.

About Hinted Handoff Writes

Hinted handoff 는 카산드라의 장애난 서버가 클러스터로 돌아옸을 때, consistency를 맞추기 위한 시간을 줄이기 위한 추가적인 기능입니다. (역자 주: Hinted handoff 는 카산드라의 모든 노드가 coordinator 가 되고, 이 때, 요청이 간 서버가 다운되서 정보를 저장하지 못하면, 해당 서버가 대신 정보를 저장하고 있다가 실제 서버가 복구되었을 때, 이를 알려주는 기능입니다. ) 또, 클라이언트가 쓰기 실패는 허용하지 않지만, 읽기의 불일치는 허용할 수 있을 때, 쓰기 가용성을 높이기 위해서도 사용됩니다.( 역자 주: 꼭 그렇다는 건 아니지만, SNS 류의 데이터의 경우, 잠시 쓰인 것이 읽히지 않더라도 문제 되지 않는 상황이 있을 수 있습니다. 내 트위터나 페이스북 내용이 보는 사람들마다 조금 다르다고 해서 무슨 문제가 있겠습니까? 쿨럭.. )

 

쓰기가 실행될 때, 카산드라는 영향 받는 row key를 가진 모든 리플리카에서 쓰기를 시도합니다. 쓰기가 일어난 시점에 하나의 리플리카가 다운된걸로 확인되면, 응답을 할 살아있는 리플리카가 Hint를 저장합니다. 해당 hint 는 해당 데이터의 위치 정보 뿐만 아니라( 복제해야할 노드와 복제해야할 row key ), 실제로 쓰여질 데이터도 포함됩니다. 리플리카에 hint를 저장하는데는 이미 자신에게 써야할 데이터를 쓰는 동안  쓰기 과정을 통해서 hint에 써야할 데이터들이 분석되어서 최소한의 오버헤드만 발생합니다.(역자 주: 해당 케이스는 리플리카가 3인데 하나가 죽어서 같은 키를 저장하는 리플리카에 hint가 저장되는 케이스입니다. )

 

만약 해당 row key를 저장해야하는 모든 리플리카가 다운도면, write consistency level 를 ANY로 선택함으로써, 쓰기에 성공하는 것이 가능합니다. 해당 시나리오에서는, 해당 hint 와 data 들이 coordinator 노드에 저장됩니다. 그러나 실제 리플리카에 해당 정보가 쓰여지기 전까지는 읽기는 이용할 수 없습니다. ANY consistency level 은 데이터를 쓴 후에 언제 읽을 수 있다는 것을 보장하지 않기 때문에 완벽한 쓰기 가용성을 제공합니다. ( 물론 얼마나 오랜 시간동안 리플리카가 다운되어 있는지에 달려있습니다.) ANY consistency level  을 이용하는 것은 coordinator 노드가 쓰기를 받아들일 리플리카가 없는 경우, 추가적인 row 정보를 저장해야 해서, 잠재적으로 클러스터의 부하를 높입니다.

 

Note

기본적으로 hint는 한 시간 동안만 저장됩니다. 만약 쓰기를 할 때, 모든 리플리카가 다운되어 있고, 모든 리플리카가 max_hint_window_in_ms 에 설정된 시간보다 길게 장애가 유지되면, ANY Consistency Level을 이용하더라도 잠재적으로 데이터를 잃어버릴 수 있습니다.

Hinted handoff는 ANY Consistent Level 이외에는 동작하지 않습니다. 예를 들어서 ONE Consitency Level 을 이용하면 쓰기시에 모든 리플리카가 다운되었을 때, 해당 쓰기는 hint를 저장했든 아니든, 실패로 처리됩니다.

하나의 hint를 저장하고 있는 리플리카가 gossip 프로토콜로 장애난 녿가 복구 된것을 알게 되면,  해당 리플리카가 따라잡아야 하는 놓친 쓰기들을 보내기 시작할 것입니다.

 

Note

Hinted handoff 는 정기적인 node 복구 작업을 수행하는 것을 대체하기에는 완전하지 않습니다.