HBase 알아보기 Part 1

Suewoon Ryu
11 min readMar 24, 2021

--

여기에서는 HBase가 어떤 성격의 데이터 저장소인지 알아본다. 그리고 HBase 내부에 데이터는 어떤식으로 저장되는지 그 데이터 모델과, 읽기/쓰기 작업이 일어나는 과정에 대해서도 정리해본다.

HBase의 성격은 아래와 같이 요약할 수 있을 것 같다. 이 글을 읽으면서 왜 HBase가 이런 특징을 가지는지 이해 할 수 있었으면 좋겠다.

  • scalability
  • random access with low latency
  • high write throughput

들어가기 전에..

LSM-Tree 자료구조를 이해하면 HBase의 구조를 이해하기 더 쉬울 것 같아서, SSTable과 LSM-Tree를 먼저 알아보자. SSTable은 LSM-Tree의 디스크 컴포넌트의 구현 중 하나이다. “SSTable”이라는 용어는 HBase의 전신인 Google BigTable 논문에서 소개되었다.

SSTable(Sorted Strings Table)

A file of immutable key/value pairs, sorted by keys. Those key, value are arbitrary byte strings

https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb/
  • 키, 값 쌍으로 구성된 파일이다. 키, 값은 모두 바이트 배열이다.
  • 한번 만들어진 SSTable은 불변(immutable) 이며, 대신 컴팩션 작업이 일어난다. 컴팩션 작업은 업데이트로 덮여쓰여진 값을 제거 하거나 삭제로 tombstone 마커가 붙은 값을 제거한다.
  • 쓰기 작업은 임의의 순서대로 일어나므로, SSTable을 만들기 위해서는 Red-Black Tree, AVL Tree같은 구조로 메모리에 버퍼해 두는것이 필요하다. 정해둔 크기 이상으로 데이터가 커지면 그걸 디스크에 SSTable 파일로 flush 함으로써, key에 의한 정렬을 구현할 수 있다. 이 메모리 공간을 memtable이라고도 한다.

LSM Tree (Log-Structured Merge Tree)

  • Log-Structured : 데이터를 순차적으로 디스크에 기록하기 때문에 Log-Structured file system에서 따온 이름. append-only storage
  • Merge: 파일을 수정할 수 없기 때문에, 병합정렬 방식으로 파일을 합치면서 각 Key에대해 최신 데이터만 유지한다. 이 작업은 읽기작업에 필요한 파일 수를 줄이고 공간도 확보한다.
Database Internals — 7. Log-Stuctured Storage

LSM Tree는 작은 공간의 메모리 컴포넌트와 큰 공간의 디스크 컴포넌트로 구성된다.

  • 메모리 컴포넌트 (mutable): 인 메모리 테이블, memtable이라고도 부른다. 데이터를 디스크에 바로 쓰지 않고, memtable에 변경사항을 버퍼하고 있다. 읽기, 쓰기작업 모두 memtable을 거쳐간다. 지정값 이상으로 크기가 커지면 디스크에 영구적으로 기록된다. memtable에 데이터를 쓰는것은 디스크 접근을 필요로 하지 않기 때문에 LSM 구조의 저장소는 쓰기작업 성능, 처리량이 좋은편이다. memtable과 별개로 WAL(write-ahead log)를 두어서 데이터의 지속성(durability)를 보장한다. 쓰기 작업은 WAL, memtable에 레코드가 기록 되어야 클라이언트에 ACK응답을 전달한다.
  • 디스크 컴포넌트 (immutable): 메모리에 버퍼된 내용을 디스크에 flush 함으로써 생성된다. 디스크에 기록된 내용은 읽기 작업에만 사용된다. 변경할 수 없다. 읽기 요청이 들어오면 memtable을 먼저 검색하고 가장 최신의 SSTable segment 부터 검색한다.

Architecture

https://medium.com/@lagrang09/introduction-to-apache-hbase-part-2-ee00bd1ec9bb

HBase Master

  • HBase 클러스터를 관리한다.
  • 어드민 작업 요청을 처리한다.

HBase RegionServer

  • 큰 테이블을 하나의 서버에서 서빙하면 요청을 분산 시킬 수 없다. 그래서 테이블을 여러 “리전”으로 쪼개서, 여러 노드에 할당한다. 리전을 서빙하는 호스트를 리전서버라고 한다. 하나의 리전 서버는 여러 리전을 서빙할 수 있다.
  • 한개의 리전서버가 죽으면, ZooKeeper가 이를 탐지하고 Master가 다른 서버에 죽은 리전서버가 서빙하던 리전을 할당한다. 새로운 Region Server는 WAL으로 죽은 리전서버에 있던 Memstore 데이터를 복구한다. 데이터가 HDFS에 복제되어 저장되어 있기 때문에 가능하다. 재할당, 데이터 복구가 완료되기 전까지는 클라이언트에서 그 데이터에 대한 접근이 Block 된다.
  • 서버 에러나 로드밸런싱을 위한 리전 재할당을 하게 되면 클라이언트 캐시가 Stale해지기 때문에 ZooKeeper서버와 META 리전을 서빙하는 리전서버에 질의하는 네트워크 라운드트립이 생길 수 밖에 없다.
  • 리전을 더 나누고(split), 옮기는 작업(assign)을 통해서 리전서버의 워크로드를 밸런싱 시킬 수 있다. 리전 split에 관여하는 정책을 지정 하면, 그 규칙대로 split 작업이 이루어지지만 사용자가 수동으로 split을 시킬 수 도 있다.
  • 리전서버만 더 투입하면 읽기, 쓰기 요청 처리량을 높일 수 있다.

ZooKeeper

HBase는 ZooKeeper에 클러스터의 설정정보, 실행중인 노드, 테이블 메타 정보등을 저장한다. ZooKeeper를 사용하면 클러스터 내 여러 노드에서 바라보는 설정정보를 싱크한 상태로 유지할 수 있으며 클라이언트에서 데이터에 접근할 때 필요한 리전서버 호스트정보를 제공할 수 있다. 마스터는 리전서버의 상태를 감시하면서 사용할 수 있는 노드 정보를 ZooKeeper에 업데이트 한다.

HBase 클러스터를 생성하려면 ZooKeeper가 필요하며 hbase-site.xml 에서 아래와 같은 설정을 추가해야 한다.

  • hbase.zookeeper.quorum : HBase에 연결할 ZooKeeper 쿼럼 주소
  • zookeeper.znode.parent : HBase데이터가 저장될 znode 경로. 이 경로 아래로 znode가 생성된다. 각 znode가 어떤 내용을 저장하고 있는지는 이 내용을 참고하면 좋을 것 같다.

hbase shell에서 zk_dump 를 실행하면 ZooKeeper에서 현재 hbase에 관련된 모든 정보를 살펴 볼 수 있다.

쓰기/읽기 과정

먼저 HBase에 데이터를 읽고, 쓸 때 대상이 되는 리전서버를 찾아야 한다.

  1. 클라이언트는 ZooKeeper 클러스터에 요청을 보내서 META table을 호스트 하는 리전서버를 알아낸다. 해당 리전 서버는 ${zookeeper.znode.parent}/meta-region-server znode에서 확인 할 수 있다.
  2. 클라이언트는 META 테이블을 확인해서, 어느 리전서버로 부터 데이터를 읽고/써야 할지를 알아낸다.

쓰기

Put p = new Put(Bytes.toBytes("MyRowKey"));
p.add(Bytes.toBytes("CF1"),
Bytes.toBytes("CQ1"));
MyTable.put(p);

memstore는 HBase가 디스크에 데이터를 쓰기 전에 저장해두고 있는 쓰기 버퍼이다. 이 버퍼는 공간(hbase.hregion.memstore.flush.size)이 다 차면 HFile 형식으로 디스크에 저장(flush)된다. HFile은 한번 쓰면 변하지 않으며 flush가 일어날 때 마다 새로운 HFile이 생성된다. HFile은 하나의 컬럼 패밀리에 종속되어 있으며, 한개의 컬럼 패밀리는 여러개의 HFile을 가질 수 있다. 컬럼 패밀리마다 memstore는 1개씩만 존재한다.

  • 데이터를 기록할 리전서버에서 먼저 WAL(write-ahead-log)에 변경사항을 기록한다. 그 다음, memstore에 기록한다. 쓰기 작업은 WAL과 memstore모두에 기록을 성공했을 때 끝난것으로 본다 .

읽기

Get g = new Get(Bytes.toBytes("MyRowKey"));
g.addColumn(Bytes.toBytes("CF1"),
Bytes.toBytes("CQ1"));
Result r= MyTable.get(g);

HBase에서 데이터 읽기 과정은 Block Cache, MemStore, HFile을 거치게 된다. BlockCache는 HBase가 HFile에서 자주 읽는 데이터를 메모리에 두려는 목적으로 사용하는 캐시이다. BlockCache와 MemStore는 JVM 힙에 존재하며, Column Family 하나당 한개의 Block Cache를 가지고 있다.

  1. 읽고자 하는 데이터가 있는 리전서버에서 먼저 MemStore를 확인한다. 있으면, 클라이언트에 전달한다. 이는 읽으려고 하는 데이터에 최근 변경사항을 확인하기 위함이다.
  2. MemStore에 데이터가 없다면 BlockCache를 확인한다. 있으면, 클라이언트에 전달한다.
  3. 그래도 데이터를 못찾았다면, HFile에서 데이터를 찾는다. 클라이언트에 전달하고 BlockCache에 이 데이터를 쓴다.

데이터 모델

논리 데이터 모델

  • 데이터는 테이블에 저장된다.
  • 테이블은 로우(row)의 집합이다.
  • 컬럼들의 집합인 컬럼 패밀리와 위에서 info가 컬럼 패밀리이다.
  • 각 로우의 컬럼 값은 셀(cell)이다.
로지컬 데이터 모델 (예시)
  • 각 로우는, 로우를 identify 할 수 있는 rowkey를 가지고 있다. 이 키는 모두 바이트 배열이다. memtable에서 정렬된 상태로 내려오기 때문에, rowkey에 의해 사전식 순서대로 정렬되어 있다.
  • 하나의 로우는 임의 갯수의 컬럼을 가질 수 있다. 위 테이블에서 shawn 로우는 postcode 컬럼을 가지고 있지 않다. 이렇게 하나의 로우가 컬럼 값이 있을수도 없을 수도 있는것을 sparse 하다고 표현한다.
  • 로우 단위로 이루어지는 읽기, 쓰기 요청에 대한 ACID를 보장한다.
  • 컬럼 패밀리는 물리적으로 비슷한 컬럼들의 집합이다. column family:column qualifier 형식으로 표현한다. 어떤 컬럼을 종종 같이 접근한다면, 이런 컬럼들을 컬럼 패밀리로 묶는다. memtable, Hfile이 컬럼 패밀리마다 존재하기 때문에 관련 없는 셀과는 아예 Hfile을 분리할 수 있게 된다. 다른 말로는 동시에 서로 다른 컬럼 패밀리의 컬럼을 접근한다면, 하나의 컬럼 패밀리만 접근할 때 보다는 성능이 떨어진다.

물리 데이터 모델

위에서 설명한 논리적 모델이 실제로 HFile에는 아래처럼 저장되어 있다.

  • HFile에서도 로우는 로우키, 컬럼에 의해서 사전순서대로 정렬되어 있다. 그리고 새로운 버전 값일 수록 위에 존재 한다.
  • rowkey, column, timestamp를 셀 에 대한 좌표가 된다. 로우는 이 셀 좌표에 의해 인덱싱 된다. 이 좌표만 있으면 한번에 셀을 읽을 수 있다.
  • HFile이 이렇게 정렬되어 있기 때문에 관련된 데이터를 효과적으로 스캔할 수 있다.
물리 데이터 모델 (예시)

HBase의 성격 (다시)

  • scalability: HBase의 실제 읽기, 쓰기 요청은 슬레이브 노드격인 Region Server에서 처리한다. 요청이 많다면 Region Sever를 추가해서 노드당 워크로드를 분산 시켜주면 된다. 보통 데이터 locality를 위해 HDFS DataNode와 같은 노드에 위치시키므로, DataNode도 같이 추가한다고 보면 된다.
  • random access with low latency: rowkey를 가지고 원하는 로우를 바로 가져 올 수 있다.
  • high write throughput: LSM Tree의 구조만 봐도 memtable에 데이터를 쓰는 작업은 디스크 접근을 필요로 하지 않는다. 업데이트시에도 기존 값이 있는 파일을 디스크에서 읽을 필요가 없다. 데이터를 쓸 Region만 찾으면 끝이다. 그리고 데이터를 쓸 Region도 여러 Region Server에 분산되어 저장되어 있으므로, Row Key만 제대로 설계 했다면 여러 노드에서 분산되어 쓰기요청이 수행된다.

마치며

보통은 HBase를 도입해볼까? 라는 생각에서 출발해서 HBase의 성격을 이해하고, 개발하고자 하는 어플리케이션에서 효과적으로 사용할 수 있을지를 판단한다. 여기서는 거꾸로, HBase는 이렇게 생겼고 이런 특징이 있는데 어느 경우 효율적으로 사용할 수 있을지를 얘기해보려고 한다. 다음 글에서는 HBase를 효율적으로 사용할 수 있는 케이스에 대해서 다룰 예정이다.

--

--