클라우드 컴퓨팅 & NoSQL/Cassandra

Cassandra Node CRUD Architecture

Terry Cho 2012. 7. 17. 18:09

Cassandra Node CRUD Architecture

이번 글에서는 Cassandra 클러스터를 구성하는 각 노드에서 Local Read/Write가 어떤 원리로 이루어지는 지 설명한다.
Cassanda에 대한 기반 지식은 아래 예전 포스팅을 참고하기 바란다.


Insert Record

큰 흐름의 Write 시나리오는 다음과 같다.
  1. Cluster에 Write 요청을 받으면, Insert하고자 하는 Record의 Key 값에 따라 Cassandra의 어느 Node에 데이타를 저장할 지, Hash 값을 가지고 판단하여, 데이타를 저장할 Node를 찾는다.
  2. 해당 Node에 데이타를 저장한다.
  3. 저장된 데이타를 Replication 정책에 따라 다른 Node에 복제 한다.
여기서 설명할 Write 로직은 2번의 한 Node에 데이타를 내부적으로 어떻게 저장하는 가에 대해서 설명한다.
  1. Node로 Write Request가 들어오게 되면, 먼저 Local의 Commit Log에 Write Request를 기록한다. 이는 서버가 갑자기 죽어버리는 경우 데이타 유실을 막기 위해서, Write Request 전체를 기록해놓고, 서버 장애시 다시 Restart되었을 때, 데이타 저장소에 저장되지 않은 데이타를 이 Commit Log로 부터 읽어서 복구하기 위함으로, Oracle과 같은 RDBMS나 다른 NoSQL도 비슷한 구조를 사용하고 있다.
  2. Insert가 요청된 데이타는 DISK에 바로 기록되지 않고 메모리 내의 MemTable이라는 곳에 기록이 된다. (뒤에 설명하겠지만, READ시에는 이 MemTable과 Disk 양쪽을 뒤져서 데이타를 READ한다. )
  3. MemTable이 어느정도 꽉 차면, 이 MemTable 전체를 통채로 Disk에 Write하는데, 이 과정을 Flushing이라고 한다. 이때 Disk로 Write되는 파일을 SSTable (Sorted String Table)이라고 하는데, 이 파일은 한번 저장되면 절대 삭제나 변경이 불가능하다. 
  4. MemTable을 SSTable로 쓰고 나면 CommitLog를 비워준다.
이렇게 Write는 Memtable 내용을 통째로 dump 하는 방식으로 이루어 지며, 절대 수정되지 않는다. 내부 구현상에서도 file sequential write로 구현되기 때문에 disk seek time이 없어서 매우 빠른 write 성능 실현이 가능하다. (random access를 하면 매번 위치를 찾기 위해서 disk seek 과정을 거쳐야 하기 때문에 성능 저하가 발생한다.)

Select Record


데이타를 읽는 큰 흐름은 다음과 같다.
1. 클러스터로 들어온 Read 요청의 Key값을 이용하여 Hash를 생성하고, 이 Hash 값을 기반으로 클러스터 링(Ring)내에 데이타가 저장된 Node를 찾아낸다.
2. 데이타를 해당 노드로 부터 읽어온다.
3. 복제된 다른 노드로 부터도 데이타를 읽어 온후에, 이 값을 비교하여 리턴한다. 이 부분에 대한 자세한 설명은 Cassandra Consistency와 Quorum에 대한 개념을 읽어보도록 한다.

여기서는 각 노드에서 READ가 어떻게 실행되는지 위의 2번 과정에 대해서 설명한다.

  1. 노드에서 READ 요청을 받으면 먼저 MemTable 내에 데이타가 있는지 찾아보고, 있으면 그 데이타를 리턴하고 끝낸다.
  2. 만약 MemTable에 데이타가 없다면, 디스크를 검색해야 하는데, 디스크의 SSTable이 실제 데이타가 저장되어 있는 곳이다. 
    SSTable에 저장되는 정보는 용도에 따라서 크게 3가지로 나뉘어 진다.
    1) Bloom Filter File - Bloom Filter는 통계적 로직을 이용하여, 해당 Key의 데이타가 SSTable에 저장되어 있는지 없는지만 판단하여 리턴한다. 각 SSTable의 Bloom Filter의 데이타는 메모리에 로딩이 되어 있는데, SSTable을 접근하여 Disk IO를 발생시키기전에 먼저 해당 SSTable에 데이타가 있는지 없는지를 먼저 검사하는 것이다.
    2) Index File - SSTable에 데이타가 있는 것으로 판단이 되면, Index File을 검색한다. Index 파일은 해당 Key에 해당하는 데이타가 Data File의 어느 위치에 있는 지에 대한 포인팅 정보를 가지고 있다. 이 Index File에서 Data File상의 레코드의 위치(Offset 정보)를 얻는다.
    3) Data File - 실제로 데이타가 저장되는 파일로 Index File에 의해서 얻은 Offset 정보를 가지고 레코드를 찾아서 리턴한다.
※ 근래 버전에는 SSTable에 Secondary Index를 지원하기 위한 Bitmap Index 파일등 기타 파일들이 추가되어 있다.

여기서는 언급하지 않았지만, 실제고 Read Operation의 성능 향상을 위해서 Index와 Data Record는 메모리에 캐슁 된다.

Record Update/Delete


앞에서 Insert & Select 구조에 대해서 알아보았다. 그렇다면 나머지 Update & Delete 는 어떤 방식으로 수행 될까?
먼저 Update의 경우에는 Delete & Insert 방식으로 내부 구현되어 있다.
Delete의 경우, 앞에서도 잠깐 언급했듯이, 한번 Write된 SSTable은 그 내용을 변경할 수 없다. (immutable) 그래서 tombstom 이라는 marking 방식을 이용하는데, 해당 record를 insert하고, tombstob 마크 (이 레코드는 삭제 되었다)라고 마킹을 한다.


위의 그림과 같이 각 레코드는 Deleted Mark와 Time Stamp를 가지고 있는데, 삭제된 레코드는 이 Delete Mark를 "True"로 표시해서 Insert하게 된다.

그러면 여기서 새로운 의문점이 생기는데, Delete란 기존의 데이타를 지우는 것이고 SSTable은 immutable (변경 불가)라고 했으니 기존의 데이타가 해당 SSTable이나 다른 SSTable에 남아 있지 않은가?

이에 대한 처리를 하기 위해서 Timestamp가 존재하는 것인데, 여러 SSTable에 걸쳐서 동일 데이타가 존재할 경우 이 Timestamp를 이용하여 최신의 데이타를 사용하게 되고, 최신의 데이타의 [Deleted Mark]가 True로 되어 있으면 데이타가 삭제 된것으로 간주한다.

Compaction
이렇게 Delete시에도 실제로 파일에서 데이타를 지우지 않고 계속 Insert만 한다면 어떻게 될까? 실제 삭제된 데이타가 계속 Disk에 남아 있기 때문에 디스크 용량이 낭비될 수 밖에 없다. 언젠가는 실제로 데이타를 지워야 하는데, Cassandra는 이를 Compaction이라는 작업을 통해서 해결한다.
SSTable내에는 유효한 데이타 뿐만 아니라 실제로 삭제된 데이타가 존재한다. 이런 공간을 없애야 하는데, 
두 개의 SSTable을 병합하면서 삭제된 레코드는 빼고 새로운 SSTable을 만든다. 새로운 SSTable에는 삭제된 레코드가 존재하지 않는다.
SSTable은 Sorting이 된 상태이기 때문에 병합 역시 매우 빠르게 이루어진다. 

결론
간략하게 각 Node의 CRUD 메커니즘에 대해서 알아보았다.
이를 소개하는 이유는 Cassandra를 사용할 때 내부 메커니즘을 이해함으로써, 어떤 형태의 데이타 설계나 API 사용이 올바른지에 대한 이해를 돕고 제대로된 Cassadra의 사용을 돕기 위함이다.
Cassandra는 기본적으로 빠른 write에 최적화가 되어 있고, delete를 tombstorm 방식을 이용하기 때문에, 이 tombstorm이 다른 노드에 복제 되기 전까지는 데이타의 불일치성이 발생한다.
또한 Key/Value 저장 방식에 최적화 되어 있기 때문에, 설사 Index를 사용한다 하더라도 Range Query나 Sorting등에는 그다지 적절하지 않으며 굉장히 빠른 Write 성능과 Commit Log 기반의 장애시 데이타 복구 능력을 보장함을 알 수 있다.

참고 자료


그리드형