HBase는 어떻게 HDFS 위에서 랜덤 읽기와 쓰기를 만들까

HBase의 Region, RegionServer, WAL, MemStore, HFile, BlockCache, ZooKeeper, HDFS의 역할과 읽기/쓰기 흐름을 정리합니다.

HDFS는 큰 파일을 여러 노드에 나누어 안전하게 보관하는 데 강하다. 하지만 HDFS 자체가 작은 row 하나를 빠르게 찾아 고치기 위한 데이터베이스는 아니다.

HBase는 그 위에 한 겹을 더 올린다.

큰 파일은 HDFS에 맡기고, row key 단위의 읽기와 쓰기는 RegionServer가 맡는다.

이 한 문장 안에 HBase의 핵심 구조가 들어 있다. HBase는 데이터를 row key 순서로 나누고, 각 구간을 Region으로 관리한다. 쓰기는 먼저 WAL과 MemStore에 받아 두고, 나중에 HFile이라는 불변 파일로 HDFS에 내려보낸다. 읽기는 MemStore, BlockCache, HFile을 함께 보면서 가장 최신의 cell을 찾아낸다.

HBase의 위치: HDFS를 데이터베이스처럼 쓰기

HDFS는 NameNode와 DataNode로 구성된다. NameNode는 파일 시스템의 네임스페이스와 블록 위치 메타데이터를 관리하고, DataNode는 실제 블록을 저장한다. HBase는 이 HDFS 위에 올라가서 HFile과 WAL을 저장한다.

그렇다고 클라이언트가 HDFS 파일을 직접 뒤지는 것은 아니다. 클라이언트는 row key가 속한 Region을 가진 RegionServer에 요청을 보낸다. RegionServer가 MemStore, BlockCache, HFile, WAL을 관리하면서 데이터베이스처럼 보이는 인터페이스를 제공한다.

역할을 나누면 이렇게 볼 수 있다.

Region은 row key 범위다

HBase 테이블은 row key 순서로 정렬된다. 하나의 테이블은 여러 Region으로 나뉘고, 각 Region은 연속된 row key 범위를 맡는다.

table: event_log

Region A  0000 .. 3999
Region B  4000 .. 7999
Region C  8000 .. ffff

Region이 커지면 row key 범위를 기준으로 split된다. 이 덕분에 테이블 하나가 여러 RegionServer에 분산될 수 있다.

반대로 row key가 한 방향으로만 증가하면 문제가 생긴다. 예를 들어 timestamp를 그대로 앞에 둔 key는 최신 쓰기가 항상 마지막 Region으로 몰릴 수 있다. 이것이 hotspotting이다. 그래서 HBase의 row key 설계는 단순한 식별자 설계가 아니라 부하 분산 설계에 가깝다.

Store는 Column Family 하나에 대응한다

Region 안에는 Store가 있다. Store는 Column Family 하나에 대응한다. 그리고 Store는 MemStore 하나와 StoreFile, 즉 HFile들을 가진다.

중요한 점은 Column Family가 물리적으로도 분리된다는 것이다. 같은 row key의 cell이라도 Column Family가 다르면 다른 Store에 들어가고, 다른 MemStore와 다른 HFile 세트를 거친다.

이 구조는 장점이 있다. 자주 함께 읽는 컬럼을 같은 Family에 묶고, 압축, TTL, Bloom Filter 같은 설정을 Family별로 다르게 줄 수 있다. 하지만 Column Family를 너무 많이 만들면 MemStore, flush, compaction, 파일 관리가 모두 늘어난다. 그래서 HBase에서는 보통 Column Family를 적게 유지하는 설계가 권장된다.

쓰기: WAL에 먼저 남기고 MemStore에 쌓는다

HBase의 쓰기는 LSM Tree 계열의 느낌을 갖는다.

Put(row, cf:qualifier, value)

1. 대상 RegionServer에 도착한다.
2. 변경 내용을 WAL에 append한다.
3. 해당 Column Family의 MemStore에 cell을 넣는다.
4. MemStore가 커지면 HFile로 flush한다.
5. HFile이 많아지면 compaction으로 병합한다.

WAL은 메모리 버퍼가 아니다. 장애 복구를 위해 HDFS의 WAL 경로에 남는 append-only 로그다. RegionServer가 죽어도 WAL을 재생하면 아직 HFile로 flush되지 않은 MemStore 변경분을 복구할 수 있다.

MemStore는 메모리 안의 정렬된 구조다. HBase의 cell은 row key, column family, qualifier, timestamp, value를 포함하고, MemStore와 HFile 모두 이 논리적 cell 구조를 유지한다. 차이는 저장 위치와 물리적 형식이다. MemStore는 메모리 구조이고, HFile은 HDFS에 저장되는 불변 파일이다.

Flush는 보통 Region 단위로 이해하는 편이 안전하다. 특정 MemStore가 임계치에 닿으면 같은 Region에 속한 MemStore들이 함께 flush될 수 있다. 그래서 Column Family를 많이 만들면 작은 flush와 많은 HFile이 생기기 쉽다.

읽기: 최신 계층부터 보고 HFile을 좁혀간다

읽기는 여러 계층을 합쳐서 본다. 아직 flush되지 않은 최신 값은 MemStore에 있고, 이미 내려간 값은 HFile에 있다. 같은 row와 column에 여러 timestamp 버전이 있을 수 있으므로, HBase는 후보들을 합쳐 가장 적절한 cell을 반환한다.

Bloom Filter는 “없음”을 빠르게 말해준다. 어떤 row key가 특정 HFile에 없다는 판단이 나오면 그 파일은 읽지 않아도 된다. “있을 수 있음”이 나오면 index를 따라 실제 block을 확인한다.

BlockCache는 읽기 성능의 중요한 완충지대다. HFile의 데이터 block이나 index block이 캐시에 있으면 HDFS read를 줄일 수 있다. 읽기 중심 워크로드에서는 BlockCache 효율이 p99 latency에 직접 영향을 준다.

HFile은 단순한 덤프 파일이 아니다

HFile은 MemStore를 그대로 파일로 던진 결과가 아니다. 정렬된 cell들이 block 단위로 들어가고, index와 Bloom Filter, metadata가 붙는다. 큰 HFile에서도 필요한 block만 찾기 위해 multi-level index가 사용된다.

대략 이렇게 보면 된다.

HFile
  Data Blocks       실제 cell 데이터
  Leaf Index        data block 위치
  Bloom Blocks      부재 확인 최적화
  Meta/File Info    통계, 설정, 메타데이터
  Trailer           파일 각 영역의 위치

HFile이 불변이라는 점도 중요하다. 이미 flush된 HFile을 그 자리에서 고치지 않는다. delete도 실제 삭제가 아니라 tombstone이라는 새 cell로 기록된다. 나중에 compaction이 여러 HFile을 병합하면서 오래된 version과 안전해진 tombstone을 정리한다.

Compaction은 청소이면서 비용이다

Flush가 반복되면 Store마다 HFile이 많아진다. 파일이 많아지면 읽기 때 확인해야 할 후보도 많아진다. 그래서 HBase는 compaction으로 여러 HFile을 더 적은 수의 HFile로 합친다.

Minor compaction은 일부 작은 StoreFile을 묶어 읽기 후보 수를 줄인다. Major compaction은 Store의 파일들을 더 크게 정리하면서 삭제 마커와 오래된 version 정리까지 더 적극적으로 수행할 수 있다.

하지만 compaction은 공짜가 아니다. 기존 HFile을 읽고 새 HFile을 쓴다. HDFS와 디스크 대역폭을 사용하고, compaction이 밀리면 읽기 지연과 쓰기 backpressure가 생길 수 있다.

운영자가 실제로 보는 신호

신호 의미 확인할 것
특정 RegionServer만 바쁨 row key hotspotting 가능성 row key prefix, salt, region 분포, request skew
flush가 너무 잦음 MemStore 압박 또는 Region/CF 수 과다 MemStore size, Region 수, Column Family 수
읽기 p99가 흔들림 HFile 수 증가, cache miss, compaction 영향 BlockCache hit ratio, StoreFile 수, compaction queue
디스크 사용량 증가 compaction 중복 공간 또는 tombstone 누적 major compaction, TTL, version 정책
Region 이동이 잦음 split, balancing, 장애 복구 영향 HMaster log, Region-in-transition, RegionServer 상태

HBase 성능 문제는 보통 하나의 설정값으로 끝나지 않는다. row key 설계, Region 분포, Column Family 수, MemStore, BlockCache, HFile 수, compaction이 함께 얽힌다.

한 문장으로 정리하면

HBase는 HDFS의 안정적인 분산 파일 저장 위에 RegionServer, WAL, MemStore, HFile, BlockCache를 얹어 row key 기반의 랜덤 읽기/쓰기를 만든다.

WAL은 방금 쓴 값을 잃지 않게 붙잡고, MemStore는 그 값을 잠시 따뜻하게 정렬해 둔다. HFile은 식은 기록처럼 HDFS에 남고, compaction은 흩어진 기록을 다시 읽기 좋게 모은다. 그래서 HBase를 이해한다는 것은 단순히 “HDFS 위의 NoSQL”을 외우는 것이 아니라, 메모리와 파일, row key와 Region, 빠른 쓰기와 나중의 정리 비용이 어떻게 균형을 이루는지 보는 일이다.

참고한 자료


© 2024. All rights reserved.