4. 쓰기/읽기 경로: Memtable, SSTable, Compaction

"카산드라는 왜 쓰기가 빠를까?", "읽기는 어떤 과정을 거칠까?" 이 질문에 대한 답은 카산드라의 내부 데이터 처리 경로에 있습니다. 이 경로를 이해하는 것은 카산드라의 성능 특성을 파악하고 트러블슈팅을 하는 데 매우 중요합니다.

쓰기 경로 (Write Path): 어떻게 그렇게 빠른가?

카산드라의 쓰기 속도가 빠른 이유는 디스크에 무작위로 접근(Random I/O)하는 대신, 메모리에 기록하고 디스크에는 순차적으로만(Sequential I/O) 쓰기 때문입니다.

쓰기 경로 흐름도

Client
Coordinator
Commit Log (Disk)
+
Memtable (Memory)
  1. Commit Log에 기록 (Append): 쓰기 요청이 들어오면, 노드는 먼저 데이터를 커밋 로그(Commit Log)에 순차적으로 추가합니다. 이는 노드가 갑자기 다운되었을 때 데이터를 복구하기 위한 안전장치입니다. 디스크에 순차적으로 쓰기 때문에 매우 빠릅니다.
  2. Memtable에 기록 (In-Memory): 동시에, 데이터는 메모리에 있는 멤테이블(Memtable)이라는 자료구조에 기록됩니다. 멤테이블은 키로 정렬된 상태를 유지합니다.
  3. 클라이언트에 응답: 위 두 과정이 끝나면 즉시 클라이언트에게 쓰기 성공 응답을 보냅니다. 디스크의 특정 위치를 찾아가는 과정이 없으므로 응답이 매우 빠릅니다.
  4. SSTable로 플러시 (Flush): 멤테이블이 일정 크기에 도달하거나 일정 시간이 지나면, 그 내용은 디스크에 SSTable(Sorted String Table)이라는 파일로 플러시됩니다. SSTable은 한번 쓰이면 절대 변경되지 않는 불변(Immutable)의 특징을 가집니다.

비유: 필기와 책 정리

쓰기 과정은 마치 필기를 하는 것과 같습니다. 중요한 내용(데이터)이 생기면, 일단 날아가지 않도록 녹음기(Commit Log)를 켜고, 동시에 연습장(Memtable)에 빠르게 메모합니다. 연습장이 꽉 차면, 그 내용을 주제별로 깔끔하게 정리해서 하나의 책(SSTable)으로 엮어 책장에 꽂아두는 것입니다.

왜 '불변(Immutable)' 설계를 선택했을까요? (쓰기 vs 읽기 비용의 트레이드오프)

결론부터 말하면, 극강의 쓰기 성능을 얻기 위해 읽기 성능과 공간 효율성의 비용을 나중으로 미루는 것이 바로 '불변' 설계의 핵심입니다. 네 말대로, 불변이기 때문에 컴팩션이 필요한 것은 맞지만, 그 인과관계는 '컴팩션을 하기 위해 불변을 선택한 것'이 아니라 '쓰기 성능을 위해 불변을 선택했고, 그 결과 컴팩션이 필요해진 것'입니다.

결국 카산드라의 '불변(Immutable) SSTable'과 '컴팩션(Compaction)'은 동전의 양면과 같습니다. '불변'은 빠른 쓰기를 위한 공격적인 선택이며, '컴팩션'은 그 선택으로 인해 발생한 부채(낡은 데이터, 툼스톤)를 청산하는 필수적인 후처리 작업입니다.

잠깐! HDD와 SSD가 왜 나올까? (순차 I/O의 비밀)

카산드라의 빠른 쓰기 성능의 비밀인 '순차 I/O'를 제대로 이해하려면, 데이터가 실제로 기록되는 하드웨어, 즉 HDD와 SSD의 동작 방식을 알아야 합니다. 카산드라의 아키텍처는 바로 이 하드웨어들의 물리적 한계를 회피하고 장점을 극대화하도록 설계되었기 때문입니다.

1. HDD (하드 디스크) - LP판의 비유

2. SSD (솔리드 스테이트 드라이브) - 지우고 다시 쓰는 오버헤드

SSD는 움직이는 부품이 없어 탐색 시간이 없지만, 다른 종류의 오버헤드가 존재합니다. 그 이유는 SSD의 데이터 처리 단위가 다르기 때문입니다.

SSD의 가장 중요한 규칙은 "데이터를 덮어쓸 수 없으며, 쓰기 전에 반드시 해당 공간이 '삭제' 상태여야 한다"는 것입니다. 그리고 삭제는 훨씬 큰 '블록' 단위로만 가능합니다.

결론적으로, 카산드라의 커밋 로그(Commit Log)는 바로 이 순차 쓰기의 장점을 극대화하여, 디스크에 데이터를 기록하면서도 마치 메모리에 쓰는 것과 같은 빠른 쓰기 성능을 확보하는 핵심적인 설계입니다.

읽기 경로 (Read Path): 복잡하지만 체계적인 과정

쓰기와 달리 읽기는 조금 더 복잡합니다. 데이터가 메모리(Memtable)와 여러 개의 SSTable 파일에 흩어져 있을 수 있기 때문입니다. 카산드라는 이 조각들을 모아 최신 데이터를 찾아냅니다.

읽기 경로 흐름도

Client
Coordinator
Memtable
Bloom Filter
SSTable(s)
  1. Memtable 확인: 가장 먼저 메모리의 멤테이블을 확인합니다. 가장 최신 데이터가 있을 확률이 높기 때문입니다.
  2. 블룸 필터 (Bloom Filter) 확인: 데이터가 멤테이블에 없다면, 이제 디스크의 SSTable들을 확인해야 합니다. 하지만 수많은 SSTable을 모두 뒤지는 것은 비효율적입니다. 이때 블룸 필터라는 확률적 자료구조를 사용합니다. 블룸 필터는 "이 SSTable에 해당 데이터가 절대 없다"는 것은 100% 확신할 수 있지만, "아마 있을 것 같다"고만 알려줍니다. 이 필터 덕분에 불필요한 디스크 접근을 획기적으로 줄일 수 있습니다.
  3. SSTable에서 데이터 조회: 블룸 필터가 "있을 것 같다"고 알려준 SSTable들만 대상으로 실제 데이터를 찾습니다. (내부적으로는 파티션 키 캐시, 파티션 서머리 등을 거쳐 더 효율적으로 찾습니다.)
  4. 결과 병합: 멤테이블과 여러 SSTable에서 찾아낸 데이터를 모아, 각 데이터의 타임스탬프(Timestamp)를 비교하여 가장 최신 버전의 데이터를 클라이언트에게 반환합니다. (Last Write Wins)

컴팩션 (Compaction): 디스크의 청소부

카산드라는 데이터를 수정하거나 삭제할 때 기존 SSTable을 건드리지 않습니다. 대신, 더 최신 타임스탬프를 가진 새로운 데이터를 쓰거나, 삭제 표시(툼스톤, Tombstone)를 남깁니다. 이로 인해 시간이 지날수록 디스크에는 낡은 데이터와 삭제 표시가 쌓이게 되고, 읽기 성능이 저하됩니다.

컴팩션(Compaction)은 이렇게 흩어져 있는 여러 SSTable을 읽어 하나의 새로운 SSTable로 병합하는 백그라운드 프로세스입니다.

컴팩션 전략 (Compaction Strategy)

컴팩션은 상당한 디스크 I/O와 CPU를 사용하므로, 서비스의 워크로드에 맞는 전략을 선택하는 것이 성능 튜닝의 핵심입니다.

SizeTieredCompactionStrategy (STCS) - 쓰기에 유리

동작 방식: 비슷한 크기의 SSTable이 여러 개(기본 4개) 모이면, 그것들을 하나의 큰 SSTable로 병합합니다. 10MB짜리 4개가 모이면 40MB짜리 1개가 되고, 40MB짜리 4개가 모이면 160MB짜리 1개가 되는 식입니다.

LeveledCompactionStrategy (LCS) - 읽기에 유리

동작 방식: SSTable을 여러 레벨(L0, L1, L2...)로 나누어 관리합니다. L0을 제외한 각 레벨의 SSTable들은 서로 키 범위가 겹치지 않습니다. 컴팩션은 L0의 SSTable을 L1으로, L1의 SSTable을 L2로 병합하는 식으로 점진적으로, 그리고 매우 자주 발생합니다.

TimeWindowCompactionStrategy (TWCS) - 시계열 데이터에 최적

동작 방식: STCS를 기반으로, SSTable을 시간대별 '윈도우(Window)'로 묶습니다. 예를 들어 '1일' 윈도우로 설정하면, 컴팩션은 같은 날짜에 속한 SSTable들 사이에서만 발생합니다. 어제 데이터와 오늘 데이터는 절대 함께 컴팩션되지 않습니다.

항목STCS (기본)LCSTWCS
쓰기 증폭낮음높음낮음
읽기 증폭높음낮음시간대 내에서는 높음
공간 증폭높음낮음낮음
주요 사용처쓰기 중심, 일반적인 워크로드읽기 중심, 낮은 지연시간이 중요한 경우시계열 데이터 (로그, IoT 센서 등)

면접관의 시선 💡

Q: "Cassandra는 왜 쓰기 성능이 빠른가요? 내부 동작과 함께 설명해주세요."

A: "카산드라의 빠른 쓰기 성능은 디스크에 대한 Random I/O를 피하는 아키텍처 덕분입니다. 쓰기 요청이 오면, 먼저 데이터를 디스크의 커밋 로그에 순차적으로 추가(Append-only)하고, 동시에 메모리의 멤테이블에 기록합니다. 이 두 과정은 디스크의 특정 위치를 찾아가는 작업이 없기 때문에 매우 빠릅니다. 실제 데이터 파일인 SSTable은 멤테이블이 가득 찼을 때 백그라운드에서 비동기적으로 플러시되므로, 쓰기 요청의 지연시간에 영향을 주지 않습니다. 이처럼 모든 쓰기 작업을 메모리 연산과 디스크 순차 쓰기로 처리하는 것이 빠른 쓰기 성능의 핵심입니다."


Q: "SSTable은 불변(Immutable)인데, Cassandra는 데이터의 수정(Update)과 삭제(Delete)를 어떻게 처리하나요? 그리고 이 방식 때문에 어떤 문제가 발생하며, 어떻게 해결하나요?"

A: "카산드라는 불변 SSTable의 특성상, 데이터를 수정할 때는 더 최신 타임스탬프를 가진 새로운 데이터를 추가로 기록하고, 삭제할 때는 '툼스톤(Tombstone)'이라는 삭제 마커를 기록하는 방식으로 처리합니다. 이 방식 때문에 디스크에는 동일한 키에 대한 낡은 데이터와 툼스톤이 계속 쌓이게 되어, 시간이 지날수록 읽기 성능이 저하되고 디스크 공간을 낭비하는 문제가 발생합니다. 카산드라는 이 문제를 '컴팩션(Compaction)'이라는 백그라운드 프로세스를 통해 해결합니다. 컴팩션은 여러 SSTable을 병합하여 새로운 SSTable을 만들면서, 낡은 데이터를 버리고 툼스톤으로 표시된 데이터를 최종적으로 삭제하여 읽기 성능을 최적화하고 디스크 공간을 회수합니다."


Q: "읽기 경로에서 블룸 필터(Bloom Filter)는 어떤 역할을 하나요?"

A: "블룸 필터는 특정 데이터가 SSTable에 '절대 존재하지 않는다'는 것을 매우 빠르고 가볍게 판단해주는 확률적 자료구조입니다. 읽기 요청이 발생했을 때, 수많은 SSTable 파일 각각에 대해 디스크 I/O를 발생시키는 것은 매우 비효율적입니다. 카산드라는 먼저 메모리에 있는 블룸 필터를 확인하여, 데이터가 존재할 가능성이 없는 SSTable을 즉시 건너뛸 수 있습니다. 이를 통해 불필요한 디스크 읽기를 획기적으로 줄여 읽기 성능을 크게 향상시키는 중요한 역할을 합니다."


Q: "읽기 성능이 매우 중요한 서비스에 Cassandra를 도입하려고 합니다. 어떤 컴팩션 전략을 선택해야 하며, 그 이유는 무엇인가요? 그리고 어떤 트레이드오프를 감수해야 하나요?"

A: "읽기 성능이 중요하다면 LeveledCompactionStrategy(LCS)를 선택해야 합니다. LCS는 SSTable을 여러 레벨로 나누고, 각 레벨 내에서는 키 범위가 겹치지 않도록 관리합니다. 덕분에 특정 데이터를 읽을 때 각 레벨에서 최대 하나의 SSTable만 확인하면 되므로, 읽어야 할 파일 수가 최소화되어 읽기 지연시간이 매우 빠르고 예측 가능합니다. 하지만 이 전략은 데이터를 여러 레벨로 유지하고 이동시키기 위해 지속적으로 작은 규모의 컴팩션을 수행해야 하므로, SizeTieredCompactionStrategy(STCS)에 비해 디스크에 데이터를 쓰고 지우는 I/O 작업, 즉 쓰기 증폭이 훨씬 심해지는 트레이드오프를 감수해야 합니다."


Q: "카산드라에서 툼스톤(Tombstone)은 왜 위험하며, 어떻게 관리해야 하나요?"

A: "툼스톤은 삭제된 데이터를 의미하는 마커로, 실제 데이터가 아니지만 디스크 공간을 차지하고 읽기 성능을 저하시키기 때문에 위험합니다. 읽기 요청 시, 카산드라는 실제 데이터뿐만 아니라 툼스톤도 읽어서 해당 데이터가 삭제되었음을 확인해야 합니다. 만약 쿼리 범위 내에 툼스톤이 대량으로 존재하면, 실제 반환되는 데이터가 없음에도 불구하고 수많은 툼스톤을 스캔하느라 쿼리가 매우 느려지거나 타임아웃이 발생할 수 있습니다.

이를 관리하기 위해서는 첫째, 테이블의 `gc_grace_seconds` 설정을 클러스터의 복구 시간에 맞춰 적절히 설정하여 컴팩션 시 툼스톤이 제때 제거되도록 해야 합니다. 둘째, 데이터를 자주 삭제하고 다시 삽입하는 안티 패턴을 피해야 합니다. 셋째, `nodetool`을 사용하여 툼스톤이 많은 테이블에 대해 수동으로 컴팩션을 트리거하거나, 툼스톤 관련 메트릭을 지속적으로 모니터링하여 문제가 되기 전에 조치해야 합니다."