[발 번역] Apache HBase Internals: Locking and Multiversion Concurrency Control

해당 글은 http://blog.cloudera.com/blog/2013/01/apache-hbase-internals-locking-and-multiversion-concurrency-control/ 라는 글을 발 번역한 것입니다. 오역에 주의하시길 바랍니다.

Apache HBase Internals: Locking and Multiversion Concurrency Control

[노트] 해당 글은 Apache HBase 가 어떻게 concurrency control 을 하는지 설명하고 있습니다. 이 글은 HBase Write Path에 대해서 알고 있다고 가정하고, 추가적인 부분은 다음 글을 읽기를 바랍니다.

Introduction

Apache HBase 는 Consistent 하고 이해할 수 있는 데이터 모델을 사용자에게 제공하면서도 높은 성능을 냅니다. 이 글에서, HBase 데이터 모델의 보장성에 대해서 얘기하고, 전통적인 데이터베이스와 어떻게 다른지 설명합니다. 그리고, concurrent 한 쓰기에 대해서 공부함으로 써, concurrency control 의 필요에 대해서 이야기 하고, 간단한 concurrency control 방법을 소개합니다. 마지막으로,  읽기/쓰기 concurrency control에 대해서 공부하고 MVCC라고 불리는 효과적인 방법에 대해서 이야기할 것입니다.

Why Concurrency Control?

HBase의 concurrency control을 이해하기 위해서, 먼저 HBase에서 왜 concurrency control 이 필요한지 이해해야 합니다. 어떤 속성들로인해 concurrency control 이 필요한 데이터에 대해서 HBase에서 보장해 주는지에 대해서 살펴보겠습니다.

이에 대한 대답은 HBase는 Row 단위의 ACID semantics 를 보장합니다. ACID는 다음 특성들의 첫글자를 딴 것입니다.

  • Atomicity: 모든 트랜잭션은 완료되거나 실패되어야 한다.
  • Consistency: 올바른 데이터만 저장되어야 한다.
  • Isolation: 동시에 발생하는 트랜잭션이 서로에게 영향이 없어야 한다.
  • Durability: 트랜잭션이 커밋되면, 해당 결과가 유지되어야 한다.

전통적인 관계형 데이터베이스에 경험이 있다면, 이 단어들이 친숙할 것입니다. 전통적인 관계형 데이터베이스는 일반적으로 데이터베이스의 모든 데이터에 ACID semantics를 제공합니다. 성능상의 이유로, HBase는 오직 ACID semantics를 Row단위로만 제공합니다. 이 단어들이 익숙하지 않더라도, 걱정하지 마십시오. 정확한 정의에 대해서 살펴보는 대신에, 간단한 예들을 보도록 하겠습니다.

Writes and Write-Write Synchronization

HBase 의 {company, role} 에 동시에 두개의 쓰기가 발생하다고 가정합니다.



Image 1.  Two writes to the same row

이전에 올렸던 HBase Write Path 에서, 각 쓰기에 대해서 다음과 같은 작업을 수행한다는 것을 알고 있습니다.

(1) Write-Ahead-Log (WAL) 쓰기

(2) MemStore 업데이트: (row, column) 쌍의 각 data cell 을 memstore 에 쓴다.

List 1. Simple list of write steps

재해 복구의 목적으로 WAL을 작성하고, 데이터를 메모리에(MemStore)에 저장합니다.

이제, 다음과 같은 순서로 이벤트가 발생한고, concurrency control 이 없다고 가정하자.


Image 2.  One possible order of events for two writes

결과적으로 다음과 같은 결과를 얻게 될 것이다.

Image 3.  Inconsistent result in absence of write-write synchronization

결코 용납할 수 없는 일입니다. ACID 에서 말하는, 쓰기에 대한 Isolation을 제공하지 않았기 때문에, 두 개의 쓰기가 섞여 버렸습니다.

이래서 확실히 concurrency control 이 필요합니다. 간단한 방법은 배타 락을 Row마다 제공해서 같은 Row를 업데이트 해야할 경우, 쓰기가 독립적으로 일어나도록 합니다.(파란색 안에 새로운 단계가 있습니다.)

(0) Row Lock 획득
(1) Write-Ahead-Log (WAL) 작성
(2) MemStore 업데이트: memstore 에 각 셀의 내용을 저장
(3) Row Lock 반환

List 2: List of write-steps with write-write synchronization

Read-Write Synchronization

이제, ACID semantics를 보장하기 위해서 쓰기에 row lock을 추가합니다. 읽기를 위해서도 concurrency control이 필요할까요? 예를 들어, 다음과 같은 순서로 이벤트가 일어난다고 가정합니다.( List 2의 규칙을 따릅니다.)


Image 4.  One possible order of operations for two writes and a read

읽기에 대해서는  Concurrency control 이 없고, 두 개의 쓰기가 발생하는 중에 read를 동시에 요청했다고 하고, 그리고 read 가 “Waitor”가 MemStore에 저장되기 직전에 바로 실행되었다고 가정합니다. 이 읽기는 그림에서 붉은 줄로 표시되어 있습니다. 이 경우에, read에는 row 데이터의 불일치가 발생하게 됩니다.



Image 5.  Inconsistent result in absence of read-write synchronization

그러므로 읽기-쓰기 동기화를 위해서도 concurrency control이 필요합니다. 간단한 방법은 쓰기와 같은 방법으로 읽기 시에도 락을 획득하는 방법입니다. 이 방법은 ACID 위반은 해결할 수 있지만, row lock으로 인해서 읽기와 쓰기에 성능저하가 일어나게 됩니다.

대신, HBase 에서는 row lock을 읽기를 위해서 획득하는 대신에 Multiversion Concurrency Control (MVCC) 라는 방법을 이용합니다. HBase 에서 MVCC는 다음과 같이 동작합니다.

쓰기:

(w1) RowLock 획득 뒤에, 각 쓰기 연사은 즉시 쓰기 번호를 할당 받는다.
(w2) 각 데이터 셀은 쓰기 번호를 저장한다.
(w3) 각 쓰기 연산은 쓰기 번호를 저장하면서 완료한다.

읽기:

(r1) 각 읽기 연산은 읽기 시간을 할당 받고, 이를 read point라고 한다.
(r2) 각 read point는  쓰기 번호 중에 가장 높은 값(완료된 값 중에서) 을 할당 받는다.
(r3)각 읽기 r은 r 의 read point 와 같거나 작은 것 중에서 쓰기 번호가 가장 높은 것을 가지는 (row, column)과 일치하는 데이터 셀에서 (row, column) 의 조합을 가져온다.


List 3. Multiversion Concurrency Control steps

그림 4의 동작을 다시 살펴보자, 여기서 MVCC 를 사용하고 있다.


Image 6.  Write steps with Multiversion Concurrency Control

MVCC 때문에 새로운 단계가 추가되었다. 각 쓰기는 쓰기 번호를 할당받고(w1), 각 데이터 셀은 memstore에 자신의 쓰기 번호와 함께 저장한다.(w2, 예를 들어 “Cloudera [wn=1]”) , 그리고 쓰기 번호를 저장하면서 각각의 쓰기는 완료된다.(w3)

이제 그림 4에서의 읽기에 대해서 생각해 보자. 예를 들어 “Restarant[wn=2]” 이 후, “Waiter[wn=2]” 단계 이전에 읽기가 발생한다면, r1 과 r2의 규칙에 따라서, read point는 1이 할당된다. r3로 인해서 읽기에서는 쓰기 번호가 1인 것의 값을 읽게 되고, 다음과 같은 결과를 얻게 된다.

Image 7.  Consistent answer with Multiversion Concurrency Control

읽기에 대한 락 없이 필요없이 일관성있는 응답을 얻게되었다.

MVCC의 쓰기 과정을 정리하면(붉은색 안의 읽기-쓰기 동기화를 획득하는 새로운 단계를 포함해서)

(0)  Row Lock 획득
(0a) 쓰기 번호 획득
(1) Write-Ahead-Log (WAL) 쓰기
(2) MemStore 업데이트 : memstore 에 각 셀의 데이터 저장
(2a) Write Number 저장
(3) Row Lock 반환

Conclusion

이 글에서는 처음에는 HBase의 row-level ACID의 보장성에 대해서 얘기했고, 동시 쓰기에 대해서 배우므로써 concurrency control의 필요성에 대해서 이야기한 다음, row-level  락 솔루션을 소개했다. 마지막으로 읽기-쓰기 concurrency control 에 대해서 살펴보고 MVCC라고 불리는 효과적인 방법을 알아보았다.

이 블로그는 HBase 0.92에 맞춰져 있고 HBase 0.94에서는 좀 더 다양한 최적화가 이루어졌다. HBASE-5541 는 차후에 다시 다루도록 하겠다.

Gregory Chanan은 클라우데라의 소프트웨어 엔지니어이고 HBase committer이다.

[발 번역] Apache HBase 는 어떻게 스케일링 하는가?

오래간만에 발 번역을 하게 됩니다. 이걸 번역할 시간이 없긴 한데, 공부 겸으로 제가 관심이 가는 분야로 이해도를 높이기 위해서 -_-, 발로 다시 번역을 시작했습니다.  원 글은 http://blog.cloudera.com/blog/2013/04/how-scaling-really-works-in-apache-hbase/ 에서 보실 수 있습니다.

How Scaling Really Works in Apache HBase

얼핏보면, Apache HBase 아키텍처는 master/slave 모델을 따르고, 마스터가 모든 요청을 받지만, 실제 작업은 slave 들에서 실제로 작업이 완료되는 걸로 보입니다. 그러나 이건 옳지 않고, 이 글에서, 실제로 master 와 slave에 의해서 어떻게 작업이 처리되는지 설명하려고 합니다.(역자 주: 여기서 master/slave 모델이라고 하는 것은 HBase Master 와 Region Server 들을 의미합니다. 일반적인 DB 의 Replication을 생각하지 마시고, Master 노드가 작업을 분배하고  실제 Worker들이 작업을 처리하는 모델을 생각하시면 됩니다.)

Regions and Region Servers

HBase 는 HDFS 기반으로 low-latency random reads 와 write 를 다루는 하둡 스토리지 매니저이고 페타데이터를 다룰 수 있습니다. HBase의 재미난 기능 중에 하나는 오토-샤딩입니다. 오토-샤딩은 시스템의 테이블이 너무 커졌을 때 동적으로 재분배 해주는 기능입니다.

Region 은 HBase 에서 수평 확장을 위한 기본 단위입니다. Region 은 함께 저장되는, 연결되어 있고, 정렬되어 있는 데이블 데이터의 부분집합입니다.

처음에는 테이블에는 오직 한개의 Region만 있고, 아래에서 볼 수 있듯이, Region이 row들이 추가되어 너무 커지면, Region 은 적당히 반으로 가르는 중간 키를 이용해서 나눠지게 됩니다.

HBase 에서 Slave는 Region Server  라 불리고, 각 Region Server 는 region들의 집합을 관리하는 역할을 하고 하나의 Region은( 데이터 row들의 범위를 가지는) 오직 하나의 Region Server 에 의해서만 처리됩니다.

HBase 아키텍처는 두 개의 메인 서비스로 구성되는데, 클러스터를 조직화하고, 관리 작업을 수행하는 HMaster와 각 테이블 데이터의 부분을 서비스하는 HRegionServer 로 되어 있습니다.

HMaster, Region Assignment, and Balancing

이전에 언급했듯이, HBase Master는 HBase 클러스터를 조직화하고, 관리하는 작업을 하게 됩니다.

Region Server는 하나 이상의 Region을 서비스하고, 각 Region은 하나의 Region Server에 시작시 할당됩니다. Master는 로드밸런싱의 결과로 Region을 원래의 Region Server 에서 다른쪽으로 이동하도록 결정할 수 있습니다.

Region과 Region Server의 매핑은 META라고 불리는 시스템 테이블에 의해서 유지되고, META 를 읽음으로서, 어떤 키가 어느 Region에 속하고 서비스 받는지 알 수 있습니다. 이것은 Read 와 Write 작업에 master가 전혀 참여하지 않고, 클라이언트가 직접 요청되는 데이터를 처리하는 Region Server와 통신할 수 있다는 의미입니다.

Locating a Row-Key: Which Region Server is Responsible?

row 를 쓰거나 읽기 위해서, 클라이언트는 master와 연결된 필요가 없고, 직접적으로 해당 row를 가진 Region Server에 연결될 수 있다. scan 의 경우에, 다뤄야 할 키들의 집합을 가진 Region Server들에 직접적으로 연결될 수 있다.

Region Server를 찾기 위해서, 클라이언트는 META 테이블로 쿼리한다.

META는 Region 들을 모니터링 하기 위해서 사용되는 시스템 테이블입니다. 서버의 이름과 테이블 이름을 비교하기 위한 Region 식별자, 그리고 시작 row-key 를 가지고 있습니다. start-key 와 다음 Region 의 start-key를 이용해서 클라이언트는 특정 Region에 속해있는 row의 범위를 알 수 있습니다.

클라이언트는 Region의 위치를 위한 캐시를 유지하고, 이것은 같은 Region을 확인해야 할 때, 매번 META table을 확인해야 하는 것을 피하게 해줍니다. Region이 나눠지거나, 다른 Region Server로 이동할 때(할당 정책이나, 밸런싱 때문에), 클라이언트는 예외를 받게 되고, 이로 인해 META 테이블에서 가져온 새로운 정보로 캐시가 갱신되게 됩니다.

 META 역시 다른 테이블과 유사하기 때문에, 클라이언트는 META 서버가 어디에 있는지 찾아야 할 필요가 있고, META 의 위치는 ZooKeeper 노드에 Master에 의해서 저장되게 됩니다.  그리고 클라이언트는 META가 저장된 Region Server의 주소를 바로 가져오게 됩니다.

HBase 는 BigTable 에 기반해서 설계가 되었고, -ROOT-라고 불리는 또 다른 테이블이, META 위치를 가지고 있고, Apache ZooKeeper 가 그 위치를 가리키고 있다. HBase 0.96 에서는 META가 나눠지지 않고, 하나의 Region으로만 구성되기 때문에, 이런 구조가 제거되고, ZooKeeper에만 의존하게 되었다.(역자 주: -ROOT- 테이블이 드랍되고 META 의 위치가 주키퍼에 바로 저장되게 되었다. https://issues.apache.org/jira/browse/HBASE-3171)

Client API: Master and Regions Responsibilities

HBase Java client API는 두 가지 주요 인터페이스가 있다.

  • HBaseAdmin 은 테이블의 생성/삭제/수정시에 “table schema”과 연결되도록 해준다. Region의 할당과 할당해제시, Region을 합치거나, 제거를 요청했을 때, 등등에 클러스터와 연결되게 한다. 이 인터페이스는 Master와 통신한다.
  • HTable 은 클라이언트가 get, put, delete, 그 외의 다른 데이터 명령어를 통해서 특정 테이블의 데이터를 다룰 수 있도록 해준다. 요청된 키를 처리하는 Region Server와 바로 통신하도록 해준다.

이 두 인터페이스는 서로 다른 역활을 한다. HBaseAdmin은 관리자 명령과 Master와 동작하고 HTable 은 Region과 통신하고 데이터를 처리한다.

Conclusion

여기서 살펴봤듯이, Master/Slave 아키텍처를 가지는 것이 모든 작업이 마스터를 통한다는 것은 아닙니다. HBase 클라이언트는, 데이터를 읽고 쓰기 위해서, 실제로,  모든 데이터 작업이 HTable 을 통해서 , 요청한 row key를 다루는 특정 Region Server  에 바로 요청하게 됩니다. Master는 테이블의 생성, 수정, 삭제 연산에만 사용됩니다.(HBaseAdmin)

Master라는 개념이 존재하지만, HBase 클라이언트는 데이터 연산시에는 master에 의존하지 않으므로, master가 다운되더라도 데이터 서비스를 할 수 있다.

Matteo Bertozzi 는 플랫폼팀의 소프트웨어 엔지니어이며 HBase 커미터입니다.

[발 번역] HBase Replication: Operational Overview

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

해당 글은 http://www.cloudera.com/blog/2012/08/hbase-replication-operational-overview/ 글을 발 번역한 것입니다. 이전에 번역한 HBase Replication Overview 가 전체적인 동작에 대한 간단한 설명이었다면, 이번 글은 실제로 Operational-Overview로 간단하게 어떻게 동작하는지에 대해서 보여주는 글입니다. 과연 실제로 어떻게 사용하는지 살펴보면 재미있을 듯 합니다. 클라우드 서비스를 이용하다보면, 결국 클라우드 서비스 자체도 장애가 날 수 있습니다. 그럴 경우,  클라우드 서비스간에 서비스를 이전하기 위해서 가장 중요한 것은 데이터 스토어입니다. 결국 클라우드간 Replication까지도 생각을 해두셔야 할 것 같습니다.

HBase Replication: Operational Overview

 해당 글은 HBase Replication 에 대한 두 번째 글 입니다. 이전 글 HBase Replication Overview 에서 유즈케이스나, 아키텍처, HBase Replication의 다양한 모드 지원에 대해서 알아보았습니다. 이번 글은 운영적인 관점에서 HBase Replication 을 어떻게 설정하고, 그것에 연관된 중요한 개념들, 부트스트래핑, 스키마 변경, 장애 안정성 등에 대해서 이야기하도록 하겠습니다.

Configuration

HBase Replication Overview에서 이미 언급했듯이, 마스터 클러스터는 WALEdit 들을 하나 이상의 슬레이브 클러스터로 전달합니다. 이번 섹션에서는 마스터-슬레이브 모드에서 리플리케이션을 설정하는 방법에 대해서 이야기하겠습니다.

  1. 리플리케이션되어야 할 모든 테이블과 컬럼 패밀리가 양 쪽 클러스터에 반드시 존재해야 합니다.
  2. 양 쪽 클러스터의 $HBASE_HOME/conf/hbase-site.xml 에 있는 다음 설정이 true로 셋팅되어야 합니다.
<property>
<name>hbase.replication</name>
<value>true</value>
</property>

 마스터 클러스터에서는 다음과 같은 추가 작업이 필요합니다.

  1. 리플리케이션을 원하는 테이블/컬럼 패밀리에 리플리케이션 범위를( REPLICATION_SCOPE  속성) 지정해야 합니다.
hbase shell> disable ‘table’
hbase shell> alter ‘table’, {NAME => ‘column-family’, REPLICATION_SCOPE => 1}
hbase shell> enable ‘table’

 REPLICATION_SCOPE 는 컬럼 패밀리 레벨의 속성이며 해당 값은 0이나 1이 될 수 있습니다. 0의 의미는 리플리케이션을 사용하지 않는 다는 뜻이고, 1은 리플리케이션을 사용하겠다는 뜻입니다.  위 예에서 보여준 것과 같이 사용자가 리플리케이션하기를 원하는 모든 컬럼 패밀리에서 각각 alter 명령어를 사용하여 변경해야 합니다.

 만약 사용자가 테이블을 생성하면서 바로 리플리케이션을 하기를 원하면 다음과 같은 과정을 따르면 됩니다.

hbase shell> create ‘table’, ‘column-family1’, ‘‘column-family2’, {NAME => ‘column-family1’, REPLICATION_SCOPE => 1}

 위의 명령은 ‘column-family1’ 이라는 위의 테이블에 대해서 리플리케이션을 사용하도록 설정할 것입니다.

       2. HBase Shell에서, Slave 노드를 추가합니다. 사용자는 슬레이브 클러스터의 zookeeper quorum 정보와, zookeeper의 클라이언트 port, 그리고 root hbase znode 를 peerId와 함께 제공해야 합니다.

hbase shell>add_peer 'peerId', "<zookeeper_quorum>:<zookeeper_client_port>:<root_hbase_znode>"

 peerId 는 한 글자나 두 글자의 long 스트링입니다. 그리고 응답하는 znode 는 peers znode 아래에 생성됩니다. 이전 블로그에서 설명한 것 처럼, 사용자가 add_peer 명령을 실행시키면, 리플리케이션 코드는 해당 peer를 위한 ReplicationSource를 인스턴스화합니다. 그리고 모든 마스터 클러스터의 region 서버들이 슬레이브 클러스터의 Region 서버에 접속하려고 시도합니다.  또한, 슬레이브 클러스터의 ClusterId (UUID, 슬레이브 클러스터의 zookeeper quorum에 등록되어 있습니다.) 도 가져옵니다.

 마스터 클러스터의 region 서버는 슬레이브 클러스터의 이용가능한 region 서버 목록을 “/hbase/rs” znode를 읽음으로써 가져옵니다. 슬레이브 클러스터의 zookeeper quorum에서 해당 노드의 자식 노드에 등록된 서버들과 연결을 맺습니다. 마스터 클러스터의 각 Region 서버들은 “replication.source.ratio” 에 정의된 값에 따라서, 슬레이브 Region 서버들의 일부를 선택합니다. 기본 값은 0.1 입니다. 익서은 각 마스터 클러스터의 Region 서버들이 슬레이브 클러스터의 Region 서버들 전체의 10%에 대해서 연결을 시도할 거라는 것을 의미합니다.

  배치 트랜잭션 작업을 보내기 위해서, 마스터 클러스터의 Region 서버는 연결된 Region 서버들 중에 랜덤으로 서버를 하나 선택할 것입니다.( 리플리케이션은 catalog 테이블에 대해서는 수행되지 않습니다. .META 와 _ROOT_ )

마스터-마스터 모드로 설정하기 위해서는, 양쪽의 클러스터에서 위의 작업을 모두 수행하면 됩니다.

Schema Change

이전 섹션에서 언급했듯이, 리플리케이션 되는 테이블과 컬럼 패밀리는 반드시 양쪽 클러스터에 모두 존재해야 합니다. 이번 섹션에서는 리플리케이션이 진행되고 있는 중에 스키마를 변경하는 동안에 발생할 수 있는 여러가지 가능한 시나리오에 대해서 논의하려고 합니다.

a) 마스터에서 컬럼 패밀리를 삭제하는 경우:  컬럼 패밀리의 삭제는 해당 컬럼 패밀리에 대해서 어떠한 아직 처리 되지 않은 변경사항에 대한 리플리케이션에 대해서 영향을 주지 않습니다. 이것은 리플리케이션 코드가 WAL을 읽고 각각의 WALEdit 가 컬럼 패밀리의 리플리케이션 범위에 있는지 체크하기 때문입니다. 각각의 WALEdit는 컬럼 패킬리가 리플리케이션 되는지에 대한 맵 정보를 가지고 있습니다. 리플리케이션 범위안에 들어가는지 아닌지에 대해서 모든 Key/Value 의 컬럼 패밀리에 대해서 체크를 합니다.  만약 맵안에 존재하면, 리플리케이션을 위해서 준비하게 됩니다. 해당 컬럼 패밀리가 삭제 되기 전에 해당 WALEdit 오브젝트가 생성되었다면, 해당 리플리케이션은 아무런 영향을 받지 않습니다.

b) 슬레이브에서 컬럼 패밀리를 삭제하는 경우: WALEdit 들이 마스터 클러스터에서 특정한 슬레이브 클러스터의 Region 서버로 전송될 때, 정상적인 HBase client와 같이 처리됩니다.( HTablePool 오브젝트를 이용합니다. ) 해당 컬럼 패밀리가 삭제되었기 때문에, put 명령은 실패할 것이고 마스터 클러스터의 Region 서버에 “예외”가 전달될 것입니다.

Start/Stop Replication

시작/종료 명령은 “Kill Switch” 처럼 동작합니다. stop_replication 명령을 HBase Shell에서 수행할 때, /hbase/replication/state 의 값을 false로 바꿀 것이고, 이로 인해 모든 Replication Source 오브젝트들이 log를 읽는 것을 멈추게 합니다. 그러나 이미 읽어둔 엔트리들은 전송이 될 것입니다. 사용자가 stop_replication 커맨드를 사용하면, 새롭게 변경되는 log들은 리플리케이션을 위해 큐에 저장되지 않을 것입니다. 비슷하게, start_replication 명령이 실행되면, 명령이 실행된 시간 부터가 아니라 현재 WAL 부터 리플리케이션을 시작할 것입니다.( 몇몇 이전 트랜잭션을 포함하는 )

 

Figure 1 explains the start-stop switch behavior, where the sequence of events flows in the direction of arrows.

Version Compatibility

 마스터 클러스터 Region 서버들은 슬레이브 클러스터 Region 서버에 정상적인 HBase Client와 같은 형태로 접속합니다. 그래서 HBase 클라이언트 버전 xxx 가 HBase 서버 버전 yyy에 접속하는 것과 동일한 룰이 적용이 됩니다.

 다른 점에서, 리플리케이션이 계속 진화하는 중이므로( 계속 많은 기능들이 지속적으로 추가되고 있습니다. ) 사용자가 현재 기능에 대해서 잘 알고 있어야 합니다. 예를 들어, CDH4 는 HBase 0.92 버전을 기반으로 하고 있습니다. 마스터/마스터 그리고 cyclic 리플리케이션, peer level에서의 리플리케이션 소스 사용 여부는 HBase 0.94에 추가되어 있습니다.

Boot-strapping

 리플리케이션은 마스터 클러스터 Region 서버들의 WAL 을 읽음으로써 동작합니다. 사용자가 이전 데이터를 리플리케이션 하기를 원한다면, 리플리케이션이 되는 동안에 copyTable 명령을 ( 시작/끝 타임스탬프를 제공함으로써 ) 실행할 수 있습니다. copyTable 명령은 시작/끝 타임스탬프의 범위 내의 데이터를 모두 복사할 것이며, 리플리케이션에서 해당 데이터를 관리할 것입니다. 전체적인 것을 요약하면 다음과 같습니다.

  1. 리플리케이션을 시작합니다.( 타임스탬프를 기억합니다. )
  2. copyTable 명령을 종료 타임스탬프를 위에서 기억한 타임스탬프와 동일한 값을 이용하여 실행시킵니다.
  3. 리플리케이션이 현재의 WAL 부터 시작되기 때문에, 슬레이브에는 아마도 copyTable 과 리플리케이션에 의해서 key/value 데이터가 복사되고 있을것입니다. 그러나 멱등한 오퍼레이션이기 때문에 문제가 없습니다. (역자 주: 즉, 리플리케이션은 현재 시점부터 시작이 되는 것이므로 그 이전 작업은 copyTable로 복사해야 한다는 의미입니다.)

 마스터/마스터 리플리케이션의 경우, 리플리케이션을 시작하기 전에 copyTable 을 수행해야 합니다. 리플리케션 수행 중에 copyTable을 사용자가 수행하면, 두번째 마스터는 첫번째 마스터로 데이터를 다시 전송할 것입니다. 왜냐하면 copyTable 작업은 해당 edit의 ClusterId를 변경하지 않기 때문입니다. 전체 과정을 요약하면 다음과 같습니다.

  1. copyTable 을 수행합니다.( job의 시작 타임스탬프를 기억해야합니다.)
  2. 리플리케이션을 시작합니다.
  3. 시작 타임스탬프를 1단계에서 저장한 시작시간으로 해서  copyTable을 다시 수행합니다.

 이것 역시 약간의 데이터가 두 클러스터 사이에서 중복으로 전달되지만 그 양을 최소화 할 수 있습니다.(역자 주: 이전 데이터를 copyTable로 복사하고, 리플리케이션 사이에 시작되는 시간 사이의 데이터만 중복으로 전달되지만, 그 양이 적을 것입니다. 그리고 리플리케이션 연산은 멱등하므로 중복된다고 해서 문제가 될 것은 없습니다.)

Fault Tolerance

Master Cluster Region Server Failover

 모든 마스터 클러스터의 Region 서버들은 “/hbase/replication/rs” 밑에 znode를 생성합니다. 아키텍처 섹션에서 언급했듯이, Region 서버는 각 WAL을 위한 자식 znode를 추가하고 byte-offset 을 저장합니다. Figure.1 에서 보여주듯이, Region 서버가 장애가 나면, 다른 Region 서버는 장애난 Region 서버의 해당znode 아래에 있는 로그들을 처리할 필요가 있습니다. 모든 Region 서버들은 다른 Region 서버의 znode를 감시하고 있습니다.(“/hbase/rs”) , 그래서 하나의 Region 서버가 장애가 나면, 다른 Region 서버들은 마스터가 해당 Region 서버가 장애라고 표시했다는 이벤트를 받게 됩니다. 이 경우에, 모든 다른 Region 서버들은 장애난 Region 서버의 znode 에서 자신의 znode로 WAL들을 옮기기 위해서 경쟁하게 됩니다. 그리고 다른 정상 log와 구별하기 위해서,  slave id 와 장애난 Region 서버의 이름을 prefix로 추가합니다.  별도의 replication source(NodeFailoverWorker instance) 는 처리하다가  장애난 전송 log 들을 위해서 인스턴스화 됩니다.

 HBase Replication Overview 의 Figure 1 을 replication znode들의 계층의 기본으로 생각한다면, 해당 포스트의 Figure 2는 foo1.bar.com 이 장애가 나서 foo2.bar.com이 foo1의 큐를 대신 처리하는 새로운 replication znode들의 계층을 보여줍니다.  새로운 znode “1-foo1.bar.com,40020,1339435481973” 이 foo2.bar.com znode 밑에 생성된 것을 기억하십시오.

If one consider Figure 1 of the HBase Replication Overview as the base figure of replication znodes hierarchy, Figure 2. shows the new replication znodes hierarchy in case server foo1.bar.com dies and foo2.bar.com takes over its queue. Note the new znode “1-foo1.bar.com,40020,1339435481973” which is created under foo2.bar.com znode


/hbase/hbaseid: b53f7ec6-ed8a-4227-b088-fd6552bd6a68 ….
/hbase/rs/foo2.bar.com,40020,1339435481973:
/hbase/rs/foo3.bar.com,40020,1339435486713: /hbase/replication:
/hbase/replication/state: true /hbase/replication/peers: /hbase/replication/peers/1: zk.quorum.slave:281:/hbase /hbase/replication/rs:
/hbase/replication/rs/foo1.bar.com.com,40020,1339435084846:
/hbase/replication/rs/foo1.bar.com,40020,1339435481973/1:
/hbase/replication/rs/foo1.bar.com,40020, 1339435481973/1/foo1.bar.com.1339435485769: 1243232 /hbase/replication/rs/foo3.bar.com,40020,1339435481742:
/hbase/replication/rs/foo3.bar.com,40020,1339435481742/1:
/hbase/replication/rs/foo3.bar.com,40020,
1339435481742/1/foo3.bar..com.1339435485769: 1243232
/hbase/replication/rs/foo2.bar.com,40020,1339435089550:
/hbase/replication/rs/foo2.bar.com,40020,1339435481742/1:
/hbase/replication/rs/foo2.bar.com,40020,
1339435481742/1/foo2.bar..com.13394354343443: 1909033
/hbase/replication/rs/foo2.bar.com,40020,1339435481742/1- foo1.bar.com,40020,1339435481973/foo1.bar.com.1339435485769: 1243232

Figure 2. Regionserver failover znodes hierarchy

 그 동안, log splitting은 장애난 Region 서버의 log를 보관하고 처리하기 시작할 것입니다. Replication source는 로그를 정규 디렉토리와 보관용 디렉토리에서 찾을 것입니다.

Slow/unresponsive slave cluster (or regionservers)

 슬레이브 클러스터가 다운되거나 일시적으로 네트웍이 단절되면, 슬레이브로 리플리케이션 되지 못한 로그는 HBase log cleaner 에 의해서 지워지지 않습니다.

 Log 삭제는 설정된 시간 마다 동작하는 LogCleaner class 에 의해서 수행되며, 리플리케이션 코드는 ReplicationLogCleaner 플러그인을 LogCleaner 클래스에 추가합니다. 마지막 특정 로그를 지우려고 시도할 때, ReplicaionLogCleaner 는 replication znode 아래에 로그가 존재하는 지 확인할 것입니다.( /hbase/replication/rs/ znode 아래의 ). 만약 로그가 발견되면, 해당 로그는 리플리케이션 중이라고 뜻이고, 해당 로그의 삭제는 진행하지 않습니다. 로그가 리플리케이션이 되면, 해당 로그의 znode 는 리플리케이션 znode 트리에서 삭제 될 것입니다. 그리고 다음 실행시에 LogCleaner 는 해당 로그가 이미 리플리케이션 되었으므로 성공적으로 삭제할 것입니다.

Verification

더 작은 양의 데이터라면 실제로 리플리케이션이 되는지 아닌지에 확인하기 위해서 슬레이브 클러스터에서 hbase shell을 이용하여 테이블 row를 간단하게 찾아볼 수 있습니다. 리플리케이션 동작 확인을 위한 일반적인 방법은 verifyrep 이라는 mapreduce 작업을 HBase에서 수행하는 것입니다. 해당 작업은 마스터 클러스터에서 수행이 되어야 하고 슬레이브 ClusterId와 확인할 테이블명이 필요합니다. 시작/종료 타임스탬프와 컬럼 패밀리 정보등의 추가적인 파라매터도 사용할 수 있습니다. GOODROWS와 BADROWS로 명명된 두 개의 카운터를 출력하는데, 리플리케이션 된 숫자와 되지 않은 숫자를 각각 의미합니다.

Future Work

 현재 버전의 HBase Replication 에 존재하는 모든 기능들은, 점차 더 개선될 것입니다. 마스터/슬레이브 간의 리플리케이션 시간이나 갭을 줄이는 등의 성능 개선과, Region 서버 장애에 대한 좀 더 안정적인 대응(HBase-2611) 등 다양합니다. 차후 개선의 범위에는 peer-level 테이블 리플리케이션의 활성화와 IncrementColumnValue(HBase-2804) 에 대한 적합한 핸들링이 포함되어 있습니다.

Conclusion
 해당 포스트는 다양한 모드에 대한 설정, 존재하는 클러스터의 부트스트래핑, 스키마 변경의 영향, 장애 안전성등을 포함한 운영 관점에서 HBase Replication 에 대해서 논의했습니다.

[발 번역] HBase Log Splitting

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

해당 글은 http://www.cloudera.com/blog/2012/07/hbase-log-splitting/ 을 발번역한 글입니다. HBase 의 리플리케이션의 핵심이기도 한 실제 WAL 로그가 어떻게 동작하는지에 대한 글입니다.

HBase Log Splitting

HBase Write Path 라는 최근 블로그 글에서, HBase Region 서버 장애시 데이터가 유실되는 것을 방지하는 중요한 역할을 하는 Write-ahead-log(WAL) 에 대해서 얘기하였습니다. 해당 블로그 글에서는 Region 서버 장애 이후에 HBase가 어떻게 데이터 유실을 방지하는지, 특히, Log Splitting를 통한 유실한 업데이트를 어떻게 복구하는지에 대한 중요한 프로세스에 대해서 얘기하도록 하겠습니다.

Log splitting

이전 Write Path 글에서 언급했듯이, HBase 데이터 업데이트는 빠른 쓰기를 위해서 memstore 라고 불리는 메모리에 데이터를 저장해둡니다. Region 서버가 장애가 났을 때, memstore는 디스크에 쓰여지지 않았기 때문에, 모두 유실되게 됩니다. 이런 경우, 데이터 유실을 방지하기 위해서 memstore에 데이터를 저장하기 전에, WAL 파일에 변경 사항을 저장해둡니다. Region 서버가 장애가 나면, memstore의 유실된 데이터는 WAL 파일안의 변경사항(또는 edits 라고 불리는) 을 재현함으로써 재생성 할 수 있습니다.

하나의 Region 서버는 많은 Region들을 서비스 합니다. 하나의 Region 서버안의 모든 Region들은 하나의 동일한 WAL 파일을 공유하게 됩니다. WAL 파일안의 하나의 변경 사항은 각각 어떤 Region에 속하는지 정보를 가지고 있습니다. 그러므로 WAL file의 edit는 특정한 region의 데이터를 재현해서 재생성할 수 있도록 Region 별로 그룹화 될 수 있습니다. Region 별 WAL edit 그룹화는 “Log Splitting” 라고 불립니다. 이것은 Region 서버 장애시 데이터를 복구하는 중요한 프로세스입니다.

Log Splitting는 Region 서버가 셧다운되거나, HMaster가 클러스터를 시작할 때 수행됩니다. 일관성을 보장하기 위해서, 영향을 받는 Region들은 모두 데이터가 복구될 때 까지 모두 이용불가능하게 됩니다. 그러므로 해당 Region 들을 모두 이용가능하게 만들기 전에 모든 WAL edits를 재현해서 데이터를 복구할 필요가 있습니다. 그 결과, 모든 필요한 edit 들이 적용되고, 프로세스가 완료될 때 까지 Region들은 “Log Splitting”에 의해서 이용불가능한 상태가 됩니다.

Log Splitting가 시작되면, 로그 디렉토리 이름은 다음과 같이 변경됩니다.

/hbase/.logs/<host>,
<port>,<startcode>-splitting

예를 들면:

/hbase/.logs/srv.example.com,60020,1254173957298-splitting

폴더 명을 변경하는 것은 중요합니다. Region 서버는 여전히 동작중이지만, 마스터는 해당 Region 서버가 다운되었다고 생각합니다. Region 서버가 바로 응답하지 못하고, 그리고 Zookeeper의 heartbeat 세션을 유지하지 못합니다. HMaster는 이것을 Region 서버가 장애가 난 것으로 인식하기 시작합니다. 폴더 이름이 변경되면,  이미 존재하는 유요한 WAL 파일들이 Active 서버에 의해서 이용되어지지만, 바쁜 Region 서버때문에 실수로 내용이 기록되지는 않습니다.

각 로그 파일은 시간에 의해서 나뉘어집니다. Log Splitting는 한 번에 하나의 edit 를 로그 파일에서 읽고 변경되어야 할 region의 버퍼에 각 edit를 추가합니다. 동시에, 해당 Log Splitting는 여러 개의 쓰기 스레드를 실행시킵니다. 쓰기 스레드는 버퍼에서 edit를 가져와서, 버퍼 안의 edit를 임시로 복구된 edit 파일에 쓰게 됩니다.

해당 파일의 위치와 이름은 다음과 같습니다.


/hbase/
<pre><table_name>/<region_id>/recovered.edits/.temp

<sequenceid> 는 파일에 쓰여진 최초 로그의 첫번째 아이템의 sequence id 를 보여줍니다. 임시로 복구된 edit 파일은 해당 Region을 위한 WAL 파일안에 모든 변경 사항을 기록하고, “Log Splitting”이 완료되면 임시 파일의 이름은 다음과 같이 변경됩니다.

<pre>/hbase/
<table_name>/<region_id>/recovered.edits/<sequenceid>

해당 예제에서 <sequenceid> 는 recoverd edits 파일의 edit 중에서 가장 높은(가장 최신의)  sequence id 값입니다. 그 결과, recoverd edits 파일을 재현할 때, 모든 edit 가 쓰여져 있는지 판별이 가능합니다. HFile 에 쓰여진 마지막 edit 가 파일 이름에 포함된 sequence id 보다 크거나 같으면, 모든 쓰기가 edit 파일에 제대로 쓰여졌다고 확신할 수 있습니다.

log splitting 이 완료되면, 각 Region 서버로 각각의 영향 받은 region 들이 할당되기 시작합니다. region 이 오픈될때 recover.edits 폴더는 recovered.edits 파일들을 체크합니다. 어떤 recovered.edits 파일이라도 존재하면, 해당 파일에서 edit 들을 읽고, mestore에 저장되어 집니다. 결국, 모든 edit 파일들이 재현되면, memstore의 내용들은 디스크(HFile) 로 저장되고, edit 파일들은 삭제됩니다.

싱글 스레드에서 log splitting이 완료되는 시간은 다 다르지만, 여러개의 Region 서버에서 장애가 발생하면 많은 시간이 소모될 수 있습니다. 분산 log splitting은 HBase 0.92(HBASE-1364) 에서 페이스북의    Prakash Khemani 에 의해서 추가되었습니다. 해당 패치는 해당 작업이 완료되는 시간은 극적으로 줄여주고 그로 인해, region들과 table의 가용성을 증가시켜줍니다. 예를 들어, 경험했던 것 중에 하나는, 하나의 클러스터가 장애가 나면, 싱글 스레드 log splitting 에서는 9 시간이 걸리던 작업이 분산 log splitting 에서는 6분 정도 밖에 걸리지 않았습니다.( 역자 주: 음, 장비 수가 많아야 될것 같다라는 생각이 듭니다. 쿨럭…)

Distributed log splitting

HBase 0.90 의 log splitting은 HMaster에 의해서 모든 것이 수행됩니다. 하나의 log splitting이 실행되면, 모든 로그 파일이 순서대로 처리됩니다. 장해 복구 후에 클러스터가 재시작되면, 불행히도, 모든 region 서버는 idle 상태가 되고, 마스터가 log splitting 작업을 끝내기를 기다리게 됩니다. 모든 region 서버가 idle 상태로 남아있는 대신에, log splitting 프로세스 중에 유용하고 도움이 되는 것들을 만들면 안되나? 라는 생각이 분산 log splitting의 시초입니다.

분산 log splitting 에서도, 마스터가 모든 것을 관리합니다. 모든 스캔되고 분할될 log 파일들을 관리하기 위한 split log 매니저를 가지고 있습니다. Split log 매니저는 모든 로그 파일들을 Splitlog zookeeper node(/hbase/splitlog) 에 태스크로 추가합니다. 예를 들어서, zkcli 에서 “ls /hbase/splitlog” 를 수행하면 다음과 같은 결과를 얻을 수 있습니다.

</pre>
[hdfs%3A%2F%2Fhost2.sample.com%3A56020%2Fhbase%2F.logs%2Fhost8.sample.com%2C57020%2C1340474893275-splitting%2Fhost8.sample.com%253A57020.1340474893900, hdfs%3A%2F%2Fhost2.sample.com%3A56020%2Fhbase%2F.logs%2Fhost3.sample.com%2C57020%2C1340474893299-splitting%2Fhost3.sample.com%253A57020.1340474893931, hdfs%3A%2F%2Fhost2.sample.com%3A56020%2Fhbase%2F.logs%2Fhost4.sample.com%2C57020%2C1340474893287-splitting%2Fhost4.sample.com%253A57020.1340474893946]

몇몇 글자가 ASCII 형태로 변경되면 다음과 같습니다.


[hdfs://host2.sample.com:56020/hbase/.logs/host8.sample.com,57020,1340474893275-splitting/host8.sample.com%3A57020.1340474893900, hdfs://host2.sample.com:56020/hbase/.logs/host3.sample.com,57020,1340474893299-splitting/host3.sample.com%3A57020.1340474893931, hdfs://host2.sample.com:56020/hbase/.logs/host4.sample.com,57020,1340474893287-splitting/host4.sample.com%3A57020.1340474893946]

로그 분할 작업의 목록이고, 스캔되고 분할된 WAL 파일명 목록입니다.

Split log 매니저는 모든 태스크를 splitlog znode에 저장합니다. 매니저는 해당 작업들을 모니터링 하고 작업이 모든 작업이 처리되기를 대기합니다.

Split Log Manager

각 Region 서버에서는, Split log worker 라고 불리는 데몬 스레드가 존재합니다. Split log worker는 log를 실제로 분할하는 작업을 합니다. worker는 항상 splitlog zonde를 관찰하고 있다가, 새로운 태스크가 들어오면, Split log worker 는 해당 태스크 경로를 탐색해서, 다른 worker가 아직 처리하지 않은 태스크을 루프를 돌면서 수집합니다. 하나를 수집한 후에, 해당 태스크에 대한 소유권을 요청하고, 성공적으로 소유권을 얻게 되면, 해당 태스크를 처리하게 됩니다. 그리고 분할된 결과에 따라서 해당 태스크 상태 정보를 업데이트 합니다. split worker 가 현재 태스크를 완료하면, 남아있는 것들 중에서 다른 것을 처리하기 위해서 다른 태스크를 수집하려고 동작합니다.

해당 기능은 설정의 hbase.master.distributed.log.splitting 에 의해서 제어됩니다. 기본적으로, 사용함으로 설정되어 있고( CDH3u3는 0.90을 기반으로 패치되었습니다만, 해당 기능은 CDH3u3에서 기본적으로 사용안함으로 되어 있습니다. 사용하길 원한다면, hbase.master.distributed.log.splitting  설정을 true로 바꾸길 바랍니다.) HMaster가 시작될 때, Split log 매니저 인스턴스는 해당 파라매터가 명시적으로 false 가 아니면 생성됩니다. Split log 매니저는 모니터링 스레드를 생성하고, 모니터링 스레드는 정기적으로 다음과 같은 작업을 수행합니다.

  1.  큐에 이미 종료된 split log worker가 있는지 체크하고, 만약 존재하면, 이미 종료된 split log worker 에게 소유된 태스크를 다시 추가합니다. 만약 zookeeper 예외로 인해서 추가되지 않으면, 재시도를 위해서 다시 종료된 worker를 큐에 추가합니다.
  2.  아직 할당되지 않은 태스크가 있는지 체크하고, 만약 존재하면, 임시적인 nodeChildrenChanged  Zookeeper 이벤트를 통해서 다른 split log worker 가 미할당된 태스크를 재탐색 할 수 있도록, 임시 재스캔 노드를 생성합니다.
  3. 할당된 태스크들이 작업시간을 초과했는지 확입합니다. 작업시간을 초과한 태스크가 있으면, 해당 태스크가 재시도 될 수 있도록 TASK_UNASSIGNED 로 변경합니다. 해당 태스크들은 느린 worker에게 할당되어 있고 log splitting 작업의 멱등성(역자 주: 여러 번 반복되더라도 값이 동일한 연산) 덕분에 재시도 되더라도 괜찮습니다. 이것이 여러개의 log splitting 작업이 동시에 실행되어도 어떤 문제도 없는 이유입니다.

Split log 매니저는 HBase split log znode를 항상 모니터링 하고 있고, 어떤 split log 태스크 znode의 데이터가 변경되면, 해당 노드의 데이터를 검색합니다. 해당 노드의 데이터는 해당 태스크의 현재 상태 정보를 가지고 있습니다. 예를 들어, zkcli 에서 다음과 같은 명령을 실행시키면


get /hbase/splitlog/hdfs%3A%2F%2Fhost2.sample.com%3A56020%2Fhbase%2F.logs%2Fhost6.sample.com%2C57020%2C1340474893287-splitting%2Fhost6.sample.com%253A57020.1340474893945

다음과 같은 결과를 얻게 됩니다.

</pre>
<em><strong>unassigned</strong> host2.sample.com:57000</em>
<pre> <em>cZxid = 0×7115</em>
 <em>ctime = Sat Jun 23 11:13:40 PDT 2012</em>
 <em>mZxid = 0×7115</em>
 <em>mtime = Sat Jun 23 11:13:40 PDT 2012</em>
 <em>pZxid = 0×7115</em>
 <em>cversion = 0</em>
 <em>dataVersion = 0</em>
 <em>aclVersion = 0</em>
 <em>ephemeralOwner = 0×0</em>
 <em>dataLength = 33</em>
 <em>numChildren = 0</em>

해당 태스크가 아직 미할당 되었다는 것을 보여주고 있습니다.
데이터가 변경된 태스크의 상태에 따라서, Split Log 매니저는 다음 중에 하나의 동작을 하게 됩니다.

  1. 미할당된 태스크를 다시 제출합니다.
  2. 할당된 태스크가 정상적으로 동작중인지 체크합니다.
  3. 중간에 취소된 태스크를 다시 제출하거나 실패로 처리합니다.
  4. 에러가 발생했지만 완료된 태스크를 다시 제출하거나 실패로 처리합니다.
  5. 에러로 인해서 실패한 태스크를 다시 제출하거나 실패로 처리합니다.
  6. 성공적으로 완료되었거나 실패한 태스크를을 삭제합니다.

다음과 같은 경우에 태스크는 실패하게 됩니다.

  1. 태스크가 삭제 된 경우
  2. 해당 노드가 더 이상 존재하지 않는 경우
  3. 해당 태스크의 상태를 TASK_UNASSIGNED 로 바꾸는 데 실패한 경우
  4. 다시 제출되는 수가 임계치보다 큰 경우

Split log worker는 region 서버에 의해서 만들어지고 시작되어집니다. 그래서 각 Region 서버마다 Split log worker가 존재합니다. Split log worker 가 시작되었을 때, 스스로 HBase znode에 자기 자신을 등록하게 됩니다.

어떤 splitlog znode 의 자식노드들이 변경되고, worker thread가 잠들어 있다면, 다른 태스크들을 찾기 위해서  worker 스레드를 깨우기 위해, worker thread로 해당 변경에 대해 통지합니다. 현재 태스크의 노드 데이터가 변경되면, 해당 태스크가 다른 worker에 의해서 소유되었는지 체크합니다. 만약 그렇다면, 해당 worker 스레드는 중단되고 현재 태스크는 중단되어집니다.

If any splitlog znode children change, it notifies the worker thread to wake up to grab more tasks if it is sleeping. If current task’s node data is changed, it checks if the task is taken by another worker. If so, interrupt the worker thread and stop the current task.

Split log worker 스레드는 splitlog znode의 어떤 자식 노드라도 변경되면 해당 노드에 대해서 체크하게 됩니다.

각 태스크를 위해서 다음과 같은 일 들을 수행 하게 됩니다.

  1. 태스크 상태를 읽고, TASK_UNASSIGNED 상태가 아니라면 아무런 작업도 하지 않습니다.
  2. 해당 태스크가 TASK_UNASSIGNED  상태라면, woker에 의해서 TASK_OWNED로 변경을 시도합니다. 해당 작업이 실패하며, 다른 worker가 다시 해당 태스크를 처리할려고 시도할 것입니다. Split log 매니저는 해당 태스크가 미할당으로 남아있으면, 추후에 모든 worker에게 재스캔하라고 요청할 것입니다.
  3.  worker 가 해당 태스크를 소유하게 되면, 정말로 그것을 소유했는지 확인하기 위해서 태스크의 상태를 다시 한번 비동기적으로 확인하게 됩니다. 그 동안에, 실제 작업을 처리하기 위해서 Split task executor를 실행시킵니다.
  4. HBase root 폴더를 찾아서, 루트 밑에 임시 폴더를 생성합니다. 그리고 분할된 로그 파일을 해당 임시 폴더로 옮깁니다.
  5. 모든 것이 정상적으로 완료되면, task executor는 해당 태스크의 상태를 TASK_DONE으로 변경합니다.
  6. 예상치 못한 IOException 이 발생하면, 해당 태스크의 상태는 TASK_ERR로 셋팅됩니다.
  7. 해당 작업이 종료되어 버리면, 해당 태스크의 상태는 TASK_RESIGNED로 설정됩니다.
  8. 태스크가 다른 worker에 의해서 소유되어 있다면  단지 해당 정보를 로깅해둡니다.

Split Log Worker

Split log 매니저는 모든 태스크가 성공적으로 완료 되었을 때, 리턴합니다. 만약 모든 태스크가 완료될 때 몇몇 에러가 있다면, log splitting 작업을 재시도 하기 위해서 예외를 발생시킵니다. 비동기적인 구현 때문에, 매우 흔치 않은 경우에, Split log 매니저는 몇몇 완료된 태스크를 잃어버릴 수 있습니다. 그래서 정기적으로 아직 완료되지 않은 태스크가 taskmap 이나 zookeeper 에 남아있는지 체크합니다. 만약 아무것도 없다면, 무슨 일이 발생하기를 계속 기다리며 대기하기보다는 log splitter 가 바로바로 재시작 할 수 있도록 예외를 발생시킵니다.

Conclusion

해당 글에서, 장애 복구시 잃어버린 데이터 복구를 위한 Log Splitting라는 중요한 프로세스에 대해서 이야기 하고 있습니다. Log Splitting는 HMaster에 의해서 연속적으로 수행되고, 0.92 에서 분산 Log Splitting의 발전에 대해서 소개되었스니다. 실제 작업은 Region 서버에서병렬적으로 수행되고, 클러스터에 매우 많은 리전 서버가 존재하므로, 클러스터에 있는 리전 서버들의 분산 Log Splitting는 로그를 나누는 시간 줄이기, Region가용성 향상등의 장점이 잇습니다.

[발 번역] HBase Replication Overview

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

해당 글은 http://www.cloudera.com/blog/2012/07/hbase-replication-overview-2/ 라는 글을 발 번역한 것입니다. cloudera의 경우 하둡 에코시스템을 이끌고 있는 메인 기업중에 하나인데, 관련 구조에 대해서도 좋은 글이 자주 올라오는 사이트이니, 관심이 있으시면 유심히 살펴보시면 좋은 자료가 많을듯 합니다.

HBase Replication Overview

HBase의 리플리케이션은 하나의 HBase 클러스터로 부터 분리된 다른 HBase 클러스터로 데이터를 복사하는 방법입니다. 원본 클러스터에서 다른 클러스터로 데이터를 집어넣는 트랜잭션을 기초해서 동작합니다. HBase Jargon에서는 푸쉬를 하는 클러스터는 마스터, 트랜잭션을 받는 쪽은 슬레이브라고 합니다. 해당 푸쉬 트랜잭션은 비동기적으로 동작합니다. 그리고 해당 트랜잭션은 설정된 크기(기본적으로 64MB) 에 따라서 배치작업으로 일어납니다. 비동기 모드는 마스터에는 최소한의 오버헤드만 초래하고, 배치 작업에서 변경 사항을 보내는 것은 총 전송량은 증가시킵니다.

해당 글에서는 가능한 유즈케이스에 대해서 논의합니다. 아키텍처와 HBase 리플리케이션 모드는 CDH4(0.92버전을 기반으로 한) 에서 지원되고 있습니다. 리플리케이션 설정과 부트스트래핑, 장애 안정성에 대해서는 다음 번 블로그 글에서 얘기하도록 하겠습니다.

Use cases

HBase 리플리케이션은 데이터 센터간 리플리케이션을 지원합니다. 이것은 마스터 사이트가 장애시에도, 슬레이브 서버가 실시간 트래픽을 처리할 수 있어서. 장애 복구 시나리오에도 이용될 수 있습니다. HBase 리플리케이션이 명시적으로 자동 장애복구를 의미하지는 않기 때문에, 슬레이브 클러스터에서 마스터로 스위칭 하는 작업은 유저가 직접 해야 합니다. 그 뒤에, 마스터 클러스터가 정상으로 회복되면, CopyTable blogpost 에서 설명한거 처럼 마스터 클러스터에(시작/정지 타임스탬프를 제공함으로써) 차이가 나는 부분을 복사해 주기 위해서 CopyTable Job을 수행할 수 있습니다.

리플리케이션의 다른 유즈 케이스는 유저가 HBase 클러스터에 MapReduce 작업을 해서 부하가 올라갈 경우에 유용합니다. 마스터 노드의 성능에 아주 조금 영향을 주긴 하지만, 슬레이브 클러스터에서 해당 MapReduce 작업을 할 수 있습니다.

Architecture

HBase 리플리케이션의 기본 원칙은 마스터에서 발생했던 모든 트랜잭션을 슬레이브에서 재현하는 것입니다. 이것은 다음 섹션에서 설명하는 것처럼, 마스터 클러스터에 있는 WAL 파일안의 WALEdits(WAL에 쓰여져 있는 변경 기록들) 을 재현함으로써 수행됩니다. 이 WALEdits들은 슬레이브 클러스터의 Region 서버들로 전송되고, 필터링 후에( 어떤 변경 기록들은 리플리케이션이 되지 않는 범위일 수 있습니다.), 지정된 사이즈로 저장됩니다.(기본 64MB)  WAL 리더가 현재 WAL 파일의 끝에 다다르면, 그 때 까지 읽은 WALEdits를 전송하려고 준비할 것입니다. 리플리케이션이 비동기 모드를 이용하므로,  수 분을 소모하는 무거운 헤비 어플리케이션에서는 마스터에 비해서 복제가 느릴 수 있습니다.

WAL/WALEdits/Memstore

HBase 에서 모든 변경 작업(Puts/Deletes) 는 특정 Region 서버의 memstore에 저장되고 동시에 WAL 파일안에 WALEdit 형태로 저장됩니다. WALEdit는 하나의 트랜잭션을 나타내는 오브젝트이고, 하나 이상의 변경 작업을 가지고 있습니다. HBase가 Single-Row 레벨의 트랜잭션을 지원하기 때문에, WALEdit 는 오직 하나의 로우에 대한 정보만 가지고 있습니다. WAL 파일들은 설정된 시간(기본적으로 60분 마다) 이후에 지속적으로 계속 변경되므로, 특정 시간에 하나의 Region 서버에는 오직 하나의 WAL 파일만 존재합니다.

IncrementColumn 값의 경우는 CAS(check and substitute) 동작으로써, WAL에 쓰여질 때 Put으로 변경되어집니다.( 역자 주: CAS 연산은 하나의 오퍼레이션이 Atomic 한 것을 보장하기 위한 연산입니다. LockFree 쪽을 살펴보게 되면 핵심적으로 사용되는것이 CAS 연산이고, CPU Operation 적으로 Atomic이 보장되는 명령을 사용합니다. )

memstore 는 메모리에 있고,  sorted map 에 Region에 저장되는 key/value 이 저장되더 있습니다. region 의 하나의 컬럼 패밀리 마다 하나의 memstore가 있습니다. memstore는 설정된 크기에 도달할때 마다(기본적으로 64MB) HFile 형태로 디스크로 저장됩니다.

WAL에 저장하는 것은 설정가능하지만, region 서버 장애시 데이터 유실을 피하기 위해서 필요합니다.  Region 서버 장애시에, 가지고 있는 모든 memstore 를 읽어버리게 될 것입니다. Region 서버가 장애가 나면, WAL에 있는 데이터들을 복구하기 위해서 Log splitting process에 의해서 모든 WAL 내용이 재현되게 됩니다.

또, 리플리케이션이 동작하기 위해서는 WAL이 무조건 동작해야 합니다.

ClusterId

모든 HBase 클러스터는 UUID 형태로 HBase 에 의해서 자동으로 생성되는 ClusterId를 가지고 있습니다. ClusterId는 리스타트시에 바뀌지 않기 의해서 파일시스템에 저장됩니다.(항상 HDFS) 그리고 zookeeper의 /hbase/bhaseid znode 에 저장됩니다. 그리고 마스터/마스터 acyclic 리플리케이션에 이용됩니다. WAL은 Region 서버안에 저장된 Region 들의 변경정보를 가지고 있습니다. 리플리케이션 코드는 리플리케이션 범위안에 있는 모든 key/value 에 대해서 읽어들이고 필터링합니다. key/value 에 있는 컬럼 패밀리 속성 값을 읽고, 이것을 WALEdit 의 컬럼 패밀리 맵 데이터 구조체와 비교하므로써 해당 작업을 수행합니다. 리플리케이션해야 하는 범위에 포함된 key/value의 경우에는, key/value의 clusterId 값을 해당 HBase ClusterId 값으로 수정합니다.

ReplicationSource

ReplicationSource는 Region 서버 프로세스 안에 있는 자바 스레드 객체로써, WAL 정보를 지정된 슬레이브 클러스터로 복제하는 작업을 하게 됩니다. 이것은 리플리케이션되어야 하는 로그 파일을 담고 있는 우선 순위 큐를 가지고 있습니다. 로그가 처리되는 만큼, 큐에서는 제거됩니다. 우선 순위 큐는 로그 파일이 생성된 시간(로그 파일에 추가된 시간)을 기준으로 비교합니다. 그래서 생성된 시간의 순서와 같은 순서로 처리되게 됩니다.( 오래된 로그가 먼저 처리됩니다. ) 우선 순위 큐에 오직 하나의 로그 파일만 있다면,  그것은 현재 WAL을 의미하므로, 지워지지 않을 것입니다.

Role of Zookeeper

Zookeeper 는 HBase 리플리케이션에서  중요한 역할을 합니다. Zookeeper는 슬레이브 클러스터를 등록하거나, 리플리케이션을 시작/중지 하거나, 새로운 WAL을 큐에 넣거나, Region 서버의 장애를 핸들링 하거나 등의 대배분의 중요한 리플리케이션 관련 작업들을 관리합니다. 항상 동작하기 위해서, 안정적인 Zookeeper Quorum(최소 3대 이상의) 을 사용하는 것이 권장됩니다.  Zookeeper 개별적으로 동작해야 합니다.(HBase 에 의해서가 아니라, 역자 주: HBase 옵션 중에 Zookeeper를 자체적으로 띄우는 설정이 있는데, 이것을 의미하는 듯 합니다.) 아래의 내용들은 마스터 클러스터에서 사용중인 리플리케이션 관련된 znode 들의 구조에 대한 예입니다.( ‘:’ 뒤에 내용이 znode의 데이터입니다. )

<div>
<pre>/hbase/hbaseid: b53f7ec6-ed8a-4227-b088-fd6552bd6a68
….
/hbase/rs/foo1.bar.com,40020,1339435481742:
/hbase/rs/foo2.bar.com,40020,1339435481973:
/hbase/rs/foo3.bar.com,40020,1339435486713:
/hbase/replication:
/hbase/replication/state: true
/hbase/replication/peers:
/hbase/replication/peers/1: zk.quorum.slave:281:/hbase
/hbase/replication/rs:
/hbase/replication/rs/foo1.bar.com.com,40020,1339435084846:
/hbase/replication/rs/foo1.bar.com,40020,1339435481973/1:
/hbase/replication/rs/foo1.bar.com,40020,1339435481973/1/foo1.bar.com.1339435485769: 1243232
/hbase/replication/rs/foo3.bar.com,40020,1339435481742:
/hbase/replication/rs/foo3.bar.com,40020,1339435481742/1:
/hbase/replication/rs/foo3.bar.com,40020,1339435481742/1/foo3.bar..com.1339435485769: 1243232
/hbase/replication/rs/foo2.bar.com,40020,1339435089550:
/hbase/replication/rs/foo2.bar.com,40020,1339435481742/1:
/hbase/replication/rs/foo2.bar.com,40020,1339435481742/1/foo2.bar..com.13394354343443: 1909033

Figure 1. Replication znodes hierarchy

Figure 1 에서 보여주듯이, 마스터 클러스터에는 3대의 Region 서버가 있고, 이름은 foo[1-3].bar.com 입니다. 다음 3개의 znode 들이 리플리케이션과 연관이 있습니다.

  1. state: 해당 znode는 리플리케이션이 켜져있는지 아닌지에 대한 정보를 알려줍니다. 모든 기본 단계에서 실제 동작 전에 해당 값을 체크하게 됩니다.( 리플리케이션 큐에 새로운 WAL 파일을 집어넣는다든지, 로그 파일을 읽어서 WALEdit들 만든다든지 등의 ) 이것은 hbase-conf.xml 에 있는 “hbase.replication” 값이 true 일 때, 설정되게 됩니다. 해당 값은 hbase shell 에서 “start/stop replication” 명령을 사용할 때도 설정되게 됩니다. 다음번 블로그 포스트에서 해당 내용을 얘기하도록 하겠습니다.
  2. peers: 해당 znode는 자식노드로 연결된 peers 와 slaves 들에 대한 정보를 가지고 있습니다. 해당 Figure 1에서 peerId 가 1인 하나의 슬레이브만 존재합니다. 그리고 해당 값은  다음 형태의 연결 정보입니다. (Zookeeper_quorum_of_slave:Zookeeper_client_port:root_hbase_znode), Zookeeper_quorum_of_slave는 ‘,’ 로 구분되는 zookeeper 서버 목록입니다. peerId znode의 이름은 피어를 추가하는 동안 주어진 이름과 동일합니다.
  3. rs: 해당 znode는 마스터 클러스터 내의 Active Region 서버의 목록을 저장하고 있습니다. 각 Region서버 znode 들은 복제되어야 하는 WAL들의 목록을 가지고 있고, 해당 log znode 들의 값은 null (아직 리플리케이션을 위한 로그가 열리지 않았을 경우 )이거나, 해당 log 파일에서 현재 읽은 위치 정보 입니다. WAL znode 의 byte offset 값은 WAL 파일에서 현재 읽고, 리플리케이션 되어서 응답 받은 로그 파일의 위치를 가리킵니다. 슬레이브 클러스터가 하나 이상이라면, 리플리케이션 지행 사항은 각각 많이 다를 수 있습니다.( 예를 들어, 하나가 다운되었다고 가정하면 ) 모든 WAL 은 rs 아래 peerId를 자체적으로 가지고 있습니다. 그리고, 위의 figure 1에서, WAL znode 들은 /rs//1 아래에 있고 “1”은 peerId 입니다.

Replication Modes

HBase 리플리케이션은 3가지 모드로 셋팅 가능합니다.

  1. Master-Slave: 해당 모드에서는, 리플리케이션은 한 방향으로만 진행됩니다. 예를 들어, 하나의 클러스터로 부터 트랜잭션이 다른 클러스터로 들어갑니다. 슬레이브 클러스터는 다른 클러스터과 비슷하고, 자기 소유의 테이블이나 트래픽 등을 가지고 있습니다
  2. Master-Master: 해당 모드에서는, 리플리케이션은 같거나 다른 테이블을 위해 양방향으로 이루어집니다. 예를 들어, 모든 클러스터는 마스터이자, 슬레이브로써 동작하게 됩니다. 같은 테이블을 리플리케이션 하는 경우에, 혹시 끝나지 않는 루프가 발생할지도 모른다고 생각할 수 있지만, 원본 클러스터에서 변경 작업(Put/Delete) 에 ClusterId를 지정함으로써 이런 상황을 피하게 됩니다. Figure 2는 태양과 지구라고 명명된 두대의 클러스터를 이용해서 이것을 설명합니다. Figure 2에는 두 HBase 클러스터를 나타내는 두 개의 블럭이 있습니다. 각각 ClusterId 100 과 200을 가지고 있고, 각 클러스터는 ReplicationSource 를 가지고 있고, 각각, 자신의 ClusterId 와 리플리케이션을 해야하는 슬레이브 클러스터의 ClusterId 를 가지고 있습니다.
    Figure 2. Sun and Earth, two HBase clusters
    클러스터 “태양”이 두 클러스터 간에서 리플리케이션되어야 할 범위에 있는 새로 발생한 변경작업 M이 발생했다면, 그것은 기본으로 설정된 ClusterId(OL)를 가지게 될 것입니다. Replication Source 인스턴스인 ReplicationSrc-E는 해당 변경 작업의 ClusterID를 자신의 ClusterId(100) 와 동일하게 설정할 것입니다. 그리고 클러스터”지구”로 보낼것입니다.  그리고 Cluster “지구” 가 해당 변경사항을 받았을 때, 정상적인 경우에, 그것을 수행하고 WAL에 변경 사항을 저장할 것입니다.  변경 사항의 CluseterId는 클러스터”지구” 의 로그 파일안에 남겨져 있을 것입니다. 클러스터”지구”의 ReplicationSource 인스턴스 ReplicationSrc-S가 해당 변경 작업을 읽고, 변경 사항의 ClusterId를 체크해서 그것이 자신의 슬레이브 클러스터의 ClusterId 와 같은 지를 비교한 다음 해당 값이 동일하면 해당 변경 작업에 대한 WALEdit 를 만드는 것을 하지않고 넘어가게 됩니다.
  3. Cyclic: 해당 모드에서, 두개의 HBase 클러스터 이상이 리플리케이션 셋팅에 참여하게 됩니다. 그리고 다양한 마스터-슬레이브, 마스터-마스터 리플리케이션의 조합이 두 개 이상의 클러스터 간에 설정될 수 있습니다. 위의 두 클러스터가 해당 케이스를 잘 다루고 있고, 사이클을 가지는 새로운 상황이 있습니다.  Figure 3. A circular replication set up

Figure-3 는 원형 리플리케이션 설정을 보여줍니다. 클러스터”태양” 은 클러스터 “지구”로 리플리케이션을 하는 중이고, 클러스터”지구”는 클러스터”금성”으로, 클러스터 “금성”은 클러스터 “태양”으로 리플리케이션 중이니다.
클러스터”태양”이 다른 클러스터로 리플리케이션 해야 하는 변경 사항이 발생했을 때, 위의마스터/마스터  리플리케이션에서 설명한 대로 클러스터 “지구”로 리플리케이션이 될 것입니다. 클러스터”지구”의 ReplicationSource 인스턴스 ReplicationSrc-V 는 WAL 파일을 읽고, 변경 사항을 본 다음 클러스터 “금성”으로 리플리케이션 합니다. 변경 사항의 ClusterId는 클러스터”태양”의 것으로 클러스터”금성”에서도 계속 유지됩니다. 그리고 해당 클러스터에서는 클러스터”태양”을 위한 Replication Source 인스턴스인 ReplicationSrc-S 는 해당 변경 작업의 ClusterId가 자신의 슬레이브 클러스터의 ClusterId 동일 한 것을 체크해서 해당 리플리케이션을 건너뛰게 됩니다.(역자 주: Figure 3의 그림이 조금 잘못되어 있어서 추가 정보를 넣습니다.  그리고 간단하게 해당 내용을 요약하면 최초에 리플리케이션을 발생시킨 클러스터 Id가 리플리케이션 시에 계속 전달되고 슬레이브 클러스터 정보와 비교해서 동일하면 건너뛴다가 핵심입니다. 너무 어렵게 설명되어 있어서 쿨럭…

#Cluster”태양” : 지구로 리플리케이션 하기 위한 ReplicationSrc-E 를 가지고 있습니다.

#Cluster”지구” : 금성으로 리플리케이션 하기 위한 RepliationSrc-V를 가지고 있습니다.( 계속 ReplicationSrc-S를 가진 것으로 나오는데 이전 그림을 재 활용해서 생긴 오류인거 같습니다. )

#Cluster”금성” : 태양으로 리플리케이션 하기 위한 ReplicationSrc-S를 가지고 있습니다.

Conclusion

HBase 리플리케이션은 재해 복구 시스템에서도 사용할 수 있는 매우 강력한 기능입니다. 0.90 버전에서는 맛보기 기능 정도로 추가 되고, HBase와 함께 발전되고 있습니다. 마스터/마스터 리플리케이션 이라든지 acyclic 리플리케이션(0.92에서 둘 다 추가되었습니다.), 피어 레벨 리플리케이션 설정/비설정 기능등이 있습니다.( 0.94에서 추가되었습니다. )

다음 블로그 포스트에서는 HBase 리플리케이션의 설정등의 다양한 동작 기능들에 대해서 얘기해보도록 하겠습니다.

[발 번역] HBase I/O – HFile

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

Introduction

아파치 HBase 는 하둡 오픈 소스 패밀리이며, 분산되고, 랜덤, 실시간 읽고/쓰기에 적합한 스토리지 매니저입니다.

엥 잠시만요? 랜덤 읽기, 실시간 읽고 쓰기가 된다구요?

어떻게 그게 가능하죠? Hadoop은 단지 연속된 읽고/쓰기만 지원하고, 배치 프로세싱을 위한 시스템 아닌가요?

예, 우리는 같은 것에 대해서 이야기를 하고 있습니다. 그리고 몇 단락 뒤에서, 어떻게 HBase가 Random I/O를 제공하는지, 데이터를 어떻게 저장하는지, HBase의 HFile format 의 변화에 대해서 설명하겠습니다.

Hadoop I/O file formats

Haddop은 Key/Value 를 추가할 수만 있는 SequenceFile[1] file format 로 시작했고, 삽인된 데이터의 변경이나, 삭제가 불가능합니다. 오직 추가 동작많이 허용되었습니다. 만약 특정한 키를 찾기를 원한다면, 키를 발견할 때 까지, 전체 파일을 읽어야 합니다.

 음, 당신도 알다시피, 순차적으로 읽고/쓰는 걸 따르도록 강제하고 있는데, 어떻게 HBase에서 랜덤하게 데이터를 만들고, 빠르게 읽고 쓸수 있는거죠?

해당 문제를 풀도록 돕기 위해서, 하둡은 MapFile 이라는 다른 파일 포맷을 가지고 있습니다. MapFile은 SequenceFile의 확장 형태이며, 실제로, 두 개의 SequenceFile을 가진 디렉토리입니다. 각각 “/data” 라는 데이터 파일과 “/index” 라는 index 파일입니다.  MapFile은 정렬된 key/value 쌍을 붙이고,  매 N개의 key들 마다(N은 설정가능한 값입니다.) index와 key와 offset이 저장됩니다. 모든 데이터를 실제로 스캔하는 대신에, 훨씬 적은 개수의 index를 먼저 살펴본 다음에, 필요한 블럭을 발견하면, 그 때, 실제 데이터로 바로 이동할 수 있습니다.

MapFile은 key/value 쌍을 빨리 찾을 수 있어서 좋지만, 두 가지 의문이 생깁니다.

  • 어떻게 key/value 쌍을 지우거나 업데이트 해야할까요?
  • 데이터의 입력이 정렬되어 있지않으면, MapFile을 사용 못할 꺼 같은데요?

HBase & MapFile

HBase Key는 row key, column family, column qualifer, timestamp 그리고 type 으로 구성되어 있습니다.

HBase Key

key/value 쌍을 지울때의 문제점을 해결하기 위한 아이디어는, “type” 속성을 삭제됨(tombstone markers)이라고 표시를 하는 것입니다.(역자 주: 보통 이것을 lazy delete 라고 부르고, 성능을 위해서, 특정 시점까지는 표시만 하고 특정 시점에, 데이터를 재구성하든가 해서 이런 부분을 삭제하는 방식을 일반적인 파일시스템이나, DB엔진에서 많이 사용합니다.), key/value 쌍을 바꾸는 문제를 해결하기 위해서, 단순히 timestamp가 뒤에 것만 가져오면 되는 문제입니다.( 즉, 여러개 가 있을 경우, 파일의 끝에 가까운데 위치한 값이 올바른 값입니다. 추가만 가능하다는 것은 파일의 끝에 가장 마지막 입력값이 있다는 것을 의미합니다.)(역자 주: 이 말은 데이터의 수정이 있을 때 마다, 실제로는 새로운 데이터가 생긴다는 것과 동일합니다. 그래서 여러 개의 같은 키와 데이터가 존재하게 되는데, 이 때, 단순히 timestamp가 마지막 것을 선택한다고 생각하시면 될 것 같습니다. 그럼 당연히 저장공간이나 속도면에서 문제가 발생할 것입니다. 이것은 뒤에 설명하는 “compaction” 이라는 것을 통해서 해결하게 됩니다.)

“정렬 되지 않은” key 문제를 해결하기 위해서, 마지막으로 추가된 key/value 쌍을 메모리에 유지합니다.  임계값에 다다랐을 때, HBase는 그것을 MapFile에 저장합니다. 이 방법으로 key/value 가 정렬된 채로 MapFile에 추가됩니다.

HBase 는 정확히 이렇게 동작합니다.[2] table.put()을 이용하여 값을 추가할 때, key/value 는 MemStore에 추가됩니다.( MemStore 는 정렬된 ConcurrentSkipListMap 입니다. ) memstore 의 임계값에 다다르거나( hbase.hregion.memstore.flush.size ), RegionServer 가 너무 많은 메모리를 memstore 에 사용하고 있으면( hbase.regionserver.global.memstore.upperLimit ) 데이터는 새로운 MapFile 로써 디스크에 저장됩니다.

각각의 flush 의 결과는 새로운 하나의 MapFile 입니다. 이것은 하나의 키를 찾고자 할 때 하나 이상의 파일을 검색해야 한다는 것을 의미합니다. 많은 리소스를 필요로 하게 되고, 잠재적으로 느려질 수 있습니다.

get이나 scan이 실행되는 시간마다, HBase는 결과를 찾기 위해서 각 파일을 스캔합니다. 너무 많은 파일을 여는 것을 피하기 위해서, 특정 개수 이상(hbase.hstore.compaction.max) 의 파일에 도달하는 것을 특정 스레드가 확인합니다. 그리고 compaction 이라고 불리는 과정을 통해서 여러 MapFile 들을 하나로 합치기를 시도합니다. file 머지의 결과로 새로운 큰 파일 하나가 생성됩니다.

HBase는 두 종류의 compaction을 가지고 있습니다.  하나는 “minor compaction”이라고 불리고, 단지 두 개 이상의 조그만 파일들을 하나로 합칩니다. 다른 하나는 “major compaction”이라고 불리며, Region 안의 모든 파일을 몇 가지 청소를 수행하며 머지합니다. major compaction 시에, 삭제되었다고 표시된 key/value를 실제로 삭제하고, 새로운 파일에는 tombstone maker도 가지지 않고, 중복된 key/value 도 제거됩니다.(replace로 인해서 발생했던)

version 0.20 까지는 HBase는데이터를 저장할 때 MapFile format을 사용하고 있지만 0.20 부터는 새로운 HBase에 특화된 MapFile 역시 포함하고 있습니다.(HBASE-61)

HFile v1

HBase 0.20 에서는 MapFile 은 HFile로 변경되었습니다. HFile은 HBase를 위해서 고안된 특별한 map file 구현입니다. 아이디어는 MapFile과 상당히 유사합니다. 그러나, 단순한 key/value 파일이 아니라 metadata의 지원이나 와 index 가 동일한 파일에 보관되는  특별한 기능을 더 추가하였습니다.

데이터 블럭은 MapFile 처럼 실제 key/value 쌍을 저장하고 있습니다. 각각의 “블럭 close 동작”은 index에 처음 key를 저장하고, index는 HFile 이 close 될 때 해당 index가 저장되게 됩니다.

HFile Format은 두 개의 “metadata” 블록 형식을 추가했습니다. Meta 와 FileInfo 가 그것입니다. 이 두 key/value 블럭은 file이 close 될 때 저장됩니다.

Meta 블럭은 다수의 데이터들의 키를 String으로 저장하기 위해서 디자인되었고, 반면 FileInfo 는 Simple Map 형태로 key와 value 들이 byte-array 형태로 작은정보를 저장합니다. RegionServer 의 StoreFile은 Meta-Block들을 Bloom Filter를 저장하기 위해서 사용하고 FileInfo는 MaxSequenceId, Major Compaction Key, Timerange info 등을 저장하기 위해서 사용합니다. 해당 정보는 키가 존재하지 않을 때, (bloom filter), 또는 만약 파일이 너무 오래되거나(Max SequenceId), 파일이 너무 새거라서(Timerange) 우리가 원하는 데이터를 담고 있지 않아서 파일을 읽는 것을 피할 때 유용합니다.(역자 주: floom filter는 일종의 캐시로, 해당 키가 존재하는지 아닌지 판별할 때 사용하게 됩니다. bloom filter의 특징은 해당 키가 존재한다고 응답이 와도, 실제로 존재하지 않을 수 있지만, 해당 키가 없다고 대답하면, 확실히 데이터가 존재하지 않는다라는 특성이 있습니다. 즉, 일반적으로 bloom filter를 메모리에 저장해두고, 키가 존재한다고 하면, 메모리나 디스크를 찾는 것이고, 없다고 하면, 아예 찾을 필요가 없어져서 디스크에 접근할 필요가 없어지는 것입니다.)

HFile v2

HBase 0.92 에는 다수의 데이터가 저장될 때, 성능 향상을 위해서  HFile Format 이 조금 변경되었습니다.(HBASE-3857) HFile v1에서의 큰 문제점 중에 하나는 모든 index들과 큰 Bloom Filter들을 메모리에 올려야 할 필요가 있을 때 였습니다. 이 문제를 해결하기 위해서 v2에서는 multi-level index와, block-level Bloom Filter를 도입하였습니다. 그 결과 HFile v2는 속도, 메모리, 캐시 사용에서 발전된 모습을 보여줍니다.

v2의 주요 기능은 “inline blocks” 입니다. 하나의 큰 Index와 Bloom Filter를 가지는 대신에, Index 와 Bloom Filter 를 블럭마다 쪼개는 것입니다. 이 방법으로 메모리에 필요한 만큼만 유지할 수 있습니다.

index 가 block level 로 변경된 후에,  multi-level index를 가지게 되었습니다. 이는 각각의 블럭마다 자기 자신의 index(leaf-index)를 가진다는 의미입니다. 각 블럭의 마지막 key는 multilevel-index 를 b+ tree 처럼 만들기 위해서 유지됩니다.(역자 주: b+ tree와 b tree를 보면 b+ tree는 range scan이 가능합니다. 해당 블럭의 마지막 데이터에서 다음 블럭 정보를 가지고 있다고 보시면 될 것 같습니다.)

블럭 헤더는 몇 가지 정보를 포함하고 있습니다. “Block Magic” 이라는 필드에서 바뀐 “Block Type” 이라는 필드는 “Data”, “Leaf-Index”, “Bloom”, “MetaData”, “Root-Index” 등등의 블럭의 내용을 표시하기 위해서 사용합니다. 또한, 압축 사이즈/원본 사이즈/이전 블럭의 위치를 가지는 세 가지 필드를 앞뒤 이동을 빠르게 하기 위해서 추가적으로 가지고 있습니다.

Data Block Encodings

key들은 정렬되고, 그래서 매우 유사합니다. 그래서 공용 목적의 알고리즘이 하는 것 보다 좀 더 좋은 압축을 하도록 디자인 하는 것이 가능합니다.

HBASE-4218 에서 해당 문제를 해결하기 위해서 시도하였고, HBase 0.94 에서는 Prefix 와 Diff Encoding 이라는 두 가지 다른 알고리즘 중에서 선택이 가능합니다.

Prefix Encoding의 주요 특징은 row 들이 정렬되고, 일반적으로 같은 이름으로 시작하면 공통적인 Prefix를 오직 한번만 저장하는 것입니다.

Diff Encoding은 해당 컨셉을 좀 더 확장했습니다. 불확실한 연속된 키 대신에, Diff Encoder는 각각의 key 를 좀 더 좋은 방법으로 압축하기 위해서 나눕니다. 이것은 하나의 column family는 한번에저장되기 때문에 가능합니다.  key 길이나, value의 길이, type 이 하나라도 같으면 해당 항목은 누락시킵니다. 또한 압축을 높이기 위해서, timestamp는 이전 값과 차이값만 저장합니다.

쓰기와 스캔작업이 느려지지만, 좀 더 데이터를 캐시하기 위해서 해당 기능은 기본적으로는 꺼져있다는 것을 기억하십시오. 해당 기능을 사용하기 위해서는 DATA_BLOCK_ENCODING = PREFIX | DIFF | FAST_DIFF 라고 table info에 설정해야 합니다.

HFile v3

압축을 높이기 위해서 HFile Layout을 재설계 하자는 제안이 HBASE-5313에 포함되었습니다.

  • 블럭의 앞부분에 모든 Key들을 한번에 묶어서 저장하고, 모든 Value들을 한번에 묶어서 block의 뒷부분에 저장합니다. 해당 방법으로, key와 value에 서로 다른 알고리즘을 key와 value에 적용할 수 있습니다.
  • timestamp의 첫번째 값을 long 대신에 VInt를 사용하고 XOR 을 이용해서 압축합니다.

또한 컬럼 형식의 포맷이나 컬럼 인코딩 방법이 연구중에 있습니다. AVRO-806 for a columnar file format by Doug Cutting 를 살펴보시기 바랍니다.

알다시피, 진화의 경향은 파일 안에 어떤 내용이 있는지 더 많이 알아야 한다는 것입니다. 좋은 압축을 하거나, 더 좋은 위치에 배치하는 것은 디스크에 적은 데이터를 읽고 쓴다는 것입니다. 적은 I/O는 더 빠른 속도를 의미합니다.

[1] http://www.cloudera.com/blog/2011/01/hadoop-io-sequence-map-set-array-bloommap-files/

[2] http://www.cloudera.com/blog/2012/06/hbase-write-path/

[발 번역] HBase Write Path

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

해당 글은 Cloudera의 http://www.cloudera.com/blog/2012/06/hbase-write-path/ 을 발 번역한 것입니다. 개인적으로 HBase 의 구조가 좀 궁금하기도 해서 번역을 해봅니다. 오역에 주의하세요.(개발자영어 분들의 도움을 얻어서 내용을 순화했습니다.)

Apache HBase 는 Hadoop database 입니다. 그리고 Hadoop 분산 파일 시스템(HDFS) 를 기반으로 동작합니다. HBase는 HDFS의 데이터에 랜덤 액세스를 하거나 데이터를 업데이트 할 수 있게 만들었습니다. 그러나 HDFS는 오직 파일에 내용을 추가하는 것만 가능하고,  파일이 생성된 후에는 수정이 불가능합니다.  그럼 분명히 궁금증이 생길껍니다. HBase는 어떻게 빠른 속도로 읽고 쓰는 작업을 수행하는가? 라고 말이죠. 해당 글에서 이것에 대해서 설명을 하고, HBase의 쓰기 과정이 어떻게 되는지, 즉, HBase 에서 어떻게 데이터를 업데이트 하는지 설명하려고 합니다.

쓰기 과정은 HBase 가 어떻게 put, delete 같은 명령을 처리하는지 입니다. 이 과정은 client 로 부터 시작해서 region server 로 넘어오고, 마지막으로 데이터가 HFile이라는 이름의 HBase 데이터 파일에 기록되면 이 쓰기 과정(write path)은 끝이 납니다. HBase의 쓰기 과정은 region server 장애시에 데이터 분실을 방지하기 위한 부분이 디자인시에 고려되었습니다. 따라서 쓰기 과정(write path) 에 대해 이해하면 HBase 자체의 데이터 유실 방지 방법에 대해 깊이 이해할 수 있습니다.

HBase table은 각각 서버 군에서 저장하고 관리하는데, 이 서버 군은 세 가지로 나눌 수 있습니다.

  1. active 마스터 서버 한 대
  2. 백업 마스터 서버 한대 이상
  3. Region 서버 여러 대

Region servers 는 HBase table을 다룹니다. HBase table이 매우 클 수 있기 때문에, region 이라고 불리는 여러 개의 파티션으로 나눠집니다. 각각의 Region 서버는 한 개 이상의 이런 region을 다룹니다.  region 서버 만이 HBase table data를 처리하는 유일한 서버라는 것을 기억해야 합니다. 그래서 마스터 서버의 장애는 데이터 유실을 야기하지 않습니다.(역자 주: HBase의 마스터 서버는 region을 어느 서버가 관리하고 있는지에 대한 정보만 저장하고 있습니다. 그래서 HBase의 마스터 서버가 장애가 나더라도 데이터가 유실되지 않습니다. 착각하기 쉬운 것이, Hadoop의 경우는 마스터 서버가 장애나고, Seconday 로 백업을 하더라도, 최신 데이터가 아니라면 데이터가 유실될 수 있습니다.)

HBase의 데이터는 구조적으로 Sorted map 과 유사합니다. 정렬된 키 공간으로 다른 샤드나 Region에 데이터를 저장합니다. HBase Client는 put 이나 delete 명령을 보냄으로써, table에 업데이트를 합니다. client 가 변경을 요청했을 때, 해당 요청은 기본적으로 바로 region 서버로 전달됩니다. 그러나,  계획에 따라, autoflush를 꺼둠으로써, 일단 client 측에서 변경을 캐시할 수 있습니다. 그리고 batch 형태로 region 서버에 해당 변경을 적용할 수 있습니다. autoflush 를 꺼두면, flush 명령이 실행될 때 까지 변경을 캐시하게 됩니다. 그리고 해당 버퍼의 크기는  “hbase.client.write.buffer” 파라매터로 설정된 크기나 프로그램적으로 버퍼의 크기를 설정할 수 있습니다.

Key가 정렬이 되면, 어떤 Region 서버가 해당 Key를 관리하는지 쉽게 알 수 있습니다. 특정 row에 변경 요청이 생기면, 각 row key 를 포함하는 특정 region 은 하나의 Region 서버에 의해서 서비스 됩니다. put/delete 되는 key에 따라, HBase client는 적합한 region 서버의 위치를 알 수 있습니다. 첫번째로, ZooKeeper quorum 으로 부터 ROOT-region 의 위치를 알게 됩니다. ROOT-region 으로 부터 client는 META-region의 위치를 발견하게 되고,  마지막으로 META-region 서버로 부터, 실제 서비스를 처리할 region 서버의 위치를 알게 됩니다. 이 세 단계가 진행하기 전에, region 서버의 위치가 캐시되어 있으면, 이런 비싼 작업을 피할 수 있습니다. 만약 캐시된 정보가 올바르지 않다면( 예를 들어, unknown region 예외가 발생한다면) 캐시를 업데이트 하고 region을 재배치하게 됩니다.

region 서버에서 요청을 받게 된 후에는, 해당 변경 사항이 바로 HFile에 쓰여지지는 않습니다. HFile의 데이터는 row key에 의해서 정렬되어야 하기 때문입니다. 이로 인해, 읽기 시에 랜덤 row를 효과적으로 찾을 수 있습니다. 데이터는 랜덤하게 HFile에 저장되지 않고 대신, 새로운 파일에 변경이 저장되어야 할껍니다. 그런데, 만약 매 업데이트가 파일에 저장되면, 많은 적은 크기의 파일들이 생성될 것입니다. 나중에 이걸 읽어서 합치는 것은 확장성이 있지도 않고, 효과적이지 못합니다. 그래서, 변경 사항은 바로 새로운 HFile 에 저장되지 않습니다.

각 변경 사항은 memstore 라고 불리는 싸고 랜덤 액세스에 효과적인 메모리에 저장되게 됩니다. memstore 안의 데이터는 HFile의 데이터 처럼 정렬됩니다. memstore 가 충분한 데이터를 가지게 되면, 전체 정렬된 데이터는 HDFS 내의 새로운 HFile에 저장되게 됩니다.( 역자 주: 그냥 충분한 데이터가 차면 고대로 그 부분을 저장한다는 겁니다. ) 하나의 큰 쓰기 작업을 처리하는 것이 효과적이고 HDFS의 장점을 취하는 방법입니다.

Write Path

memstore 에 데이터를 쓰는 것이 효과적이긴 하지만, 몇가지 위험도 가지고 있습니다.  memstore에 저장된 데이터는 휘발성 메모리에 저장됩니다. 그래서 system이 장애가 나면, 모든 memstore의 데이터는 유실이됩니다.  이런 위험을 피하기 위해서 HBase 는 memstore에 데이터를 쓰기 전에 Write-ahead-log(WAL) 에 업데이트를 저장합니다. (역자 주: 보통 transaction 등을 지원하는 일반적인 RDBMS에서 이 WAL이라는 기법을 아주 예전부터 사용하고 있습니다. 당연히 WAL을 사용하면 성능은 디스크 접근으로 인해서 떨어질 수 밖에 없습니다. hadoop append 가 되기 전에는 여기서도 유실의 가능성이 있었습니다. 자세한 건 다음에 ^^) 이 방법으로, region 서버가 장애가 나더라도 WAL을 이용해서 서버에 저장된 memstore 정보를 복구할 수 있습니다.

주의: 기본적으로 WAL이 켜져 있습니다. 그러나 WAL의 쓰기 과정에서 파일을 디스크에 저장하므로 리소스를 어느정도 사용하게 됩니다. WAL을 끈다면, 데이터를 유실할 수 있는 위험이 생깁니다. 만약 WAL을 끄기로 결정했다면, 자신만의 재해 복구 솔루션에 대해서 고민하고, 데이터 유실의 가능성에 대비해야 합니다.

WAL 안의 데이터는 HFile 과는 다르게 저장되어 있습니다. WAL은 변경 정보 리스트를 저장하고 있고, 하나의 변경 정보는 하나의 put/delete 를 나타냅니다. 해당 변경 정보는 변경 내역과, 어떤 Region에 변경이 있었는지를 저장합니다. 변경 정보는 시간순으로 기록되어 있고, 보존을 위해서 WAL 파일의 끝에 계속 추가되어서 디스크에 저장됩니다. WAL 파일의 경우 시간순으로 저장되므로, 랜덤으로 쓸 필요가 없습니다.

WAL 파일이 커지면, 해당 파일을 닫고, 새로운 WAL 파일을 생성해서 새로운 변경 정보르 저장하게 됩니다. 이것을 WAL 파일의 “Rolling” 이라고 합니다. WAL 파일이 바뀌면, 이전 파일에는 변경이 생기지 않습니다.(역자 주:  예전 서버들의 경우 파일시스템의 한계등으로 인해서 2GB 가 넘으면 로그 파일을 Rolling 하는 기능이 들어있는 서버들이 많았습니다. 그러나 HBase에서는 그것보다 훨씬 큰 파일들이 저장되게 됩니다.)

기본적으로, WAL 파일은 HDFS의 block size의 95% 정도 크기가 되면 바뀌게 됩니다.  “hbase.regionserver.logroll.multiplier” 파라매터를 이용해서 값을 설정할 수 있고 block size는 “hbase.regionserver.hlog.blocksize” 로 설정이 가능합니다. WAL 파일을 일정 간격으로 변경되도록 할 때는 “hbase.regionserver.logroll.period” 을 사용하면 됩니다. 기본적으로 한 시간으로 설정되어 이씁니다. WAL 파일의 크기가 설정 값 보다 작아도 한시간 마다 바뀌게 됩니다.

WAL 파일의 사이즈를 제한하는 것은, 복구가 필요할 때, 파일을 좀 더 효과적으로 이용할 수 있게 합니다.  이것은 특히 region의 WAL 파일을 이용할 때 특히 중요한데, 복구를 위해 파일을 이용하는 동안에, 해당 region을 이용할 수 없기 때문입니다.  결국 WAL 파일에 모든 변경을 기록하고, HFile의 내용을 영구적으로 저장하기 위해서 입니다. 이것이 끝나면, WAL 파일은 보관되고, LogCleaner 데몬 스레드에 위해서 결과적으로 삭제됩니다. WAL 파일은 데이터 복호를 위한 수단이라는 것을 기억해야합니다. WAL 파일은 오직 region 서버 장애 후에 데이터 복구시에만 이용하게 됩니다.  없으면 데이터가 유실되겠죠.

하나의 region 서버는 많은 region들을 서비스합니다. 그러나 각 region을 위한 WAL 파일은 가지고 있지 않습니다. 대신에,  하나의 WAL 파일이 region 서버에 의해서 모든 region이 공유하게 됩니다. WAL 파일이 정기적으로 변경되기 때문에 하나의 region 서버는 많은 WAL 파일을 가지게 됩니다. 한 순간에 하나의 region 서버에는 오직 하나의 WAL 파일에만 저장이 됩니다.

HBase의 root 가 “/hbase” 라고 가정하고, region server 인스턴스를 위한 모든 WAL 파일은 같은 root 폴더 밑에 저장됩니다. 다음과 같은 형태를 따르게 됩니다.

/hbase/.logs/<host>,
<port>,<startcode>

예를 들면:

/hbase/.logs/srv.example.com,60020,1254173957298

WAL 로그 파일은 다음과 같이 이름이 만들어집니다.:

/hbase/.logs/<host>,
<port>,<startcode>/<host>%2C
<port>%2C<startcode>.<timestamp>

예를 들면:

/hbase/.logs/srv.example.com,60020,1254173957298/srv.example.com%2C60020%2C1254173957298.1254173957495

WAL 파일안의 각각의 변경 정보는 유일한 sequence id를 가지고 있습니다. 해당 id는 변경 순서를 지켜서 증가합니다. 로그 파일이 변경될 때, 다음 sequence id 과 이전 파일 이름이 memory map 형태로 들어가서, memstore 가 디스크로 저장되고, 파일이 보관될 때, 각 WAL 파일의 최대 sequence id 쉽게 찾을 수 있습니다.

변경 정보와 sequence id들은 한 region 안에서 유일하고, 언제라도 WAL log에 추가될 수 있습니다. 변경 정보의 sequence id 는 항상 마지막 sequence id로 기록됩니다. memstore 정보가 디스크에 저장될 때, 해당 region을 위한 마지막 sequence id는 지워집니다. 디스크에 쓰여진 마지막 sequence id는 WAL 파일의 최대 sequence id와 동일합니다. region을 위한 하나의 WAL 파일안의 모든 변경 기록은 디스크에 저장이 된다고 할 수 있습니다.

WAL file은 변경과 memstore 의 flush는 서로 분리된 두 개의 액션입니다. 그리고 함께 실행되지 않습니다. 그러나, region 서버 장애의 경우에, 시간을 너무 많이 소모한ㄴ 것을 피하기 위해서, region 서버마다 너무 많은 WAL file을 유지하지를 원치 않습니다. 그러므로, WAL 파일이 변경될 때, HBase는 너무 많은 WAL 파일이 있는지 체크하고, 어떤 region을 디스크에 저장할 것인지 결정하고, 몇몇 WAL 파일을 보관하게 됩니다.

이번 포스트에서, HBase 의 쓰기 경로에 대해서 어떻게 HBase가 데이터를 생성하고 업데이트 하는지 설명하였습니다.  중요한 부분은 다음과 같습니다.

  1. Client 가 어떻게 region server를 알아내는가?
  2. Memstore 가 어떻게 빠른 랜덤 쓰기를 지원하는가?
  3. Region 서버 장애시에 데이터 유실을 피하는 방법으로의 WAL 파일

We will talk about HFile formats, WAL file splitting and so on in subsequent posts.