예시 (Examples)
#Create a namespace
create_namespace 'my_ns'
#create my_table in my_ns namespace
create 'my_ns:my_table', 'fam'
#drop namespace
drop_namespace 'my_ns'
#alter namespace
alter_namespace 'my_ns', {METHOD => 'set', 'PROPERTY_NAME' => 'PROPERTY_VALUE'}
사전 정의된 네임스페이스 (Predefined namespaces)
두 개의 사전 정의된 특수 네임스페이스가 있어요:
- hbase — 시스템 네임스페이스로, HBase 내부 테이블을 담는 데 사용돼요.
- default — 명시적으로 네임스페이스를 지정하지 않은 테이블이 자동으로 들어가는 네임스페이스예요.
예시 (Examples)
#namespace=foo and table qualifier=bar
create 'foo:bar', 'fam'
#namespace=default and table qualifier=bar
create 'bar', 'fam'
hbase:namespace 테이블에 관하여 (About hbase:namespace table)
우리는 네임스페이스 정보를 저장하기 위해 hbase:namespace라는 시스템 테이블을 사용했었어요.
이 테이블은 과거에 몇 가지 고통스러운 버그를 유발했는데, 특히 master 시작을 멈추게 해 클러스터 전체를 멈추게 할 수 있어요. 이는 meta 테이블에도 네임스페이스가 있어서 namespace 테이블에 의존하기 때문이에요. 하지만 namespace 테이블도 meta 테이블에 의존해요. meta 테이블이 모든 region의 위치를 저장하니까요. 이는 순환 의존성이라 때로 namespace와 meta 테이블이 서로 온라인되기를 기다리며 master 시작을 멈추게 해요.
수정이 쉽지 않아서 3.0.0에서 우리는 hbase:namespace 테이블을 완전히 제거하고 그 내용을 hbase:meta 테이블의 ns 패밀리로 접어넣기로 결정했어요. 2.x에서 3.x로 업그레이드할 때 마이그레이션이 자동으로 수행되고, 마이그레이션 완료 후 hbase:namespace 테이블은 비활성화돼요. 얼마간 그대로 두었다가 마침내 drop해도 자유예요.
자세한 내용은 https://issues.apache.org/jira/browse/HBASE-21154를 참고하세요.
테이블 (Table)
테이블은 스키마 정의 시점에 미리 선언돼요.
행 (Row)
행 키는 해석되지 않은 바이트(byte)예요. 행은 사전식(lexicographic)으로 정렬되며 가장 낮은 순서가 테이블에서 먼저 나타나요. 빈 바이트 배열은 테이블 네임스페이스의 시작과 끝을 모두 나타내는 데 사용돼요.
컬럼 패밀리 (Column Family)
Apache HBase의 컬럼은 column families(컬럼 패밀리)로 그룹화돼요. 컬럼 패밀리의 모든 컬럼 멤버는 동일한 접두사를 가져요. 예를 들어 컬럼 courses:history와 courses:math는 모두 courses 컬럼 패밀리의 멤버예요. 콜론 문자(:)가 컬럼 패밀리와 컬럼 패밀리 퀄리파이어를 구분해요. 컬럼 패밀리 접두사는 printable(인쇄 가능한) 문자로 구성되어야 해요. 퀄리파이어 꼬리 부분인 컬럼 패밀리 qualifier는 임의의 바이트로 만들 수 있어요. 컬럼 패밀리는 스키마 정의 시점에 미리 선언해야 하지만, 컬럼은 스키마 시점에 정의할 필요가 없고 테이블이 실행되는 동안 즉석에서 만들 수 있어요.
물리적으로 모든 컬럼 패밀리 멤버는 파일시스템에서 함께 저장돼요. 튜닝과 저장 사양이 컬럼 패밀리 수준에서 이루어지기 때문에, 모든 컬럼 패밀리 멤버가 일반적으로 동일한 접근 패턴과 크기 특성을 갖는 것이 좋아요.
셀 (Cells)
{row, column, version} 튜플은 HBase의 cell을 정확히 지정해요. 셀 내용은 해석되지 않은 바이트예요.
데이터 모델 연산 (Data Model Operations)
네 가지 주요 데이터 모델 연산은 Get, Put, Scan, Delete예요. 연산은 Table 인스턴스를 통해 적용돼요.
Get
Get은 지정된 행의 속성을 반환해요. Gets는 Table.get로 실행돼요.
Put
Put은 테이블에 새 행을 추가하거나(키가 새것인 경우) 기존 행을 업데이트해요(키가 이미 존재하는 경우). Puts는 Table.put(non-writeBuffer) 또는 Table.batch(non-writeBuffer)로 실행돼요.
Scans
Scan은 지정된 속성에 대해 여러 행을 반복하는 것을 허용해요.
다음은 Table 인스턴스에서의 Scan 예시예요. "row1", "row2", "row3" 키를 가진 행들로 테이블이 채워져 있고, 그다음 "abc1", "abc2", "abc3" 키를 가진 또 다른 행 집합이 있다고 가정해요. 다음 예시는 "row"로 시작하는 행을 반환하도록 Scan 인스턴스를 설정하는 방법을 보여줘요.
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Table table = ... // instantiate a Table instance
Scan scan = new Scan();
scan.addColumn(CF, ATTR);
scan.setStartStopRowForPrefixScan(Bytes.toBytes("row"));
ResultScanner rs = table.getScanner(scan);
try {
for (Result r = rs.next(); r != null; r = rs.next()) {
// process result...
}
} finally {
rs.close(); // always close the ResultScanner!
}
일반적으로 스캔의 특정 중지 지점을 지정하는 가장 쉬운 방법은 InclusiveStopFilter 클래스를 사용하는 거예요.
Delete
Delete는 테이블에서 행을 제거해요. Deletes는 Table.delete로 실행돼요.
HBase는 데이터를 제자리에서 수정하지 않으므로, deletes는 tombstones라는 새 마커를 만들어 처리해요. 이 tombstone들은 죽은 값과 함께 major compaction 시 정리돼요. 컬럼 버전 삭제에 대한 자세한 내용은 version.delete, 컴팩션에 대한 자세한 내용은 compaction을 참고하세요.
버전 (Versions)
{row, column, version} 튜플은 HBase의 cell을 정확히 지정해요. 행과 컬럼이 같지만 셀 주소가 버전 차원에서만 다른 무제한 수의 셀을 가질 수 있어요.
행과 컬럼 키가 바이트로 표현되는 반면, 버전은 long 정수로 지정돼요. 일반적으로 이 long은 java.util.Date.getTime()이나 System.currentTimeMillis()가 반환하는 것 같은 시간 인스턴스를 포함해요. 즉, 현재 시간과 1970년 1월 1일 자정 UTC 사이의 밀리초 단위 차이예요.
HBase 버전 차원은 내림차순으로 저장되므로, store file에서 읽을 때 가장 최근 값이 먼저 발견돼요.
HBase에서 cell 버전의 의미에 대해 혼란이 많아요. 특히:
- 셀에 대한 여러 쓰기가 같은 버전을 가지면 마지막으로 쓰여진 것만 가져올 수 있어요.
- 버전 순서가 증가하지 않는(non-increasing) 순서로 셀을 쓰는 것은 괜찮아요.
아래에서는 HBase의 버전 차원이 현재 어떻게 동작하는지 설명해요. HBase 버전에 대한 논의는 HBASE-2406을 참고하세요. Bending time in HBase는 HBase의 버전, 즉 시간 차원에 대한 좋은 읽을거리예요. 여기 제공된 것보다 버전에 대한 더 많은 세부 사항이 있어요.
이 글을 쓰는 시점에서 해당 글에서 언급한 기존 타임스탬프의 값 덮어쓰기 제한은 HBase에서 더 이상 유지되지 않아요. 이 섹션은 기본적으로 Bruno Dumon의 이 글을 요약한 것이에요.
저장할 버전 수 지정 (Specifying the Number of Versions to Store)
특정 컬럼에 저장할 최대 버전 수는 컬럼 스키마의 일부이며 테이블 생성 시 또는 alter 명령으로 HColumnDescriptor.DEFAULT_VERSIONS를 통해 지정돼요. HBase 0.96 이전에는 유지되는 기본 버전 수가 3이었지만, 0.96 이상에서는 1로 변경됐어요.
예시: 컬럼 패밀리의 최대 버전 수 수정 (Example: Modify the Maximum Number of Versions for a Column Family)
이 예제는 HBase Shell을 사용해 컬럼 패밀리 f1의 모든 컬럼을 최대 5개 버전으로 유지해요. ColumnFamilyDescriptorBuilder를 사용할 수도 있어요.
hbase> alter 't1', NAME => 'f1', VERSIONS => 5
예시: 컬럼 패밀리의 최소 버전 수 수정 (Example: Modify the Minimum Number of Versions for a Column Family)
컬럼 패밀리당 저장할 최소 버전 수를 지정할 수도 있어요. 기본적으로 0으로 설정되어 있어 기능이 비활성화돼요. 다음 예시는 HBase Shell을 통해 컬럼 패밀리 f1의 모든 컬럼에 최소 버전 수를 2로 설정해요. ColumnFamilyDescriptorBuilder를 사용할 수도 있어요.
hbase> alter 't1', NAME => 'f1', MIN_VERSIONS => 2
HBase 0.98.2부터 hbase-site.xml에 hbase.column.max.version을 설정해 새로 생성되는 모든 컬럼에 대해 유지되는 최대 버전 수의 전역 기본값을 지정할 수 있어요. hbase.column.max.version을 참고하세요.
버전과 HBase 연산 (Versions and HBase Operations)
이 섹션에서는 핵심 HBase 연산 각각에 대한 버전 차원의 동작을 살펴봐요.
Get/Scan
Gets는 Scans 위에 구현돼요. 아래의 Get에 대한 논의는 Scans에도 동일하게 적용돼요.
기본적으로, 즉 명시적인 버전을 지정하지 않으면 get을 수행할 때 버전 값이 가장 큰 셀이 반환돼요(가장 최근에 쓰여진 것일 수도 있고 아닐 수도 있어요. 나중 참고). 기본 동작은 다음과 같은 방식으로 수정할 수 있어요:
- 둘 이상의 버전을 반환하려면 Get.readVersions(int)를 참고하세요.
- 최신이 아닌 버전을 반환하려면 Get.setTimeRange(long,long)를 참고하세요.
주어진 값보다 작거나 같은 최신 버전을 검색해서 특정 시점의 레코드 '최신' 상태를 얻으려면 0부터 원하는 버전까지의 범위를 사용하고 max versions를 1로 설정하면 돼요.
기본 Get 예시 (Default Get Example)
다음 Get은 행의 현재 버전만 검색해요.
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(Bytes.toBytes("row1"));
Result r = table.get(get);
byte[] b = r.getValue(CF, ATTR); // returns current version of value
버전이 있는 Get 예시 (Versioned Get Example)
다음 Get은 행의 마지막 3개 버전을 반환해요.
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Get get = new Get(Bytes.toBytes("row1"));
get.setMaxVersions(3); // will return last 3 versions of row
Result r = table.get(get);
byte[] b = r.getValue(CF, ATTR); // returns current version of value
List| cells = r.getColumnCells(CF, ATTR); // returns all versions of this column |
Put
put을 수행하면 항상 특정 타임스탬프에서 cell의 새 버전이 생성돼요. 기본적으로 시스템은 서버의 currentTimeMillis를 사용하지만, 컬럼 수준에서 버전(= long 정수)을 직접 지정할 수도 있어요. 즉 과거나 미래의 시간을 할당하거나, 시간이 아닌 목적으로 long 값을 사용할 수 있어요.
기존 값을 덮어쓰려면 덮어쓰려는 셀과 정확히 같은 행, 컬럼, 버전에서 put을 수행하세요.
암시적 버전 예시 (Implicit Version Example)
다음 Put은 HBase에 의해 현재 시간으로 암시적으로 버전이 매겨져요.
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put(Bytes.toBytes(row));
put.add(CF, ATTR, Bytes.toBytes( data));
table.put(put);
명시적 버전 예시 (Explicit Version Example)
다음 Put은 버전 타임스탬프가 명시적으로 설정돼 있어요.
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put( Bytes.toBytes(row));
long explicitTimeInMs = 555; // just an example
put.add(CF, ATTR, explicitTimeInMs, Bytes.toBytes(data));
table.put(put);
주의: 버전 타임스탬프는 HBase 내부적으로 time-to-live 계산 같은 것에 사용돼요. 이 타임스탬프를 직접 설정하는 것은 보통 피하는 게 좋아요. 행의 별도 타임스탬프 속성을 사용하거나, 타임스탬프를 행 키의 일부로 만들거나, 둘 다 하세요.
셀 버전 예시 (Cell Version Example)
다음 Put은 이미 관련 Type과 Row가 설정된 CellBuilder 인스턴스를 얻기 위해 getCellBuilder() 메서드를 사용해요.
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Put put = new Put(Bytes.toBytes(row));
put.add(put.getCellBuilder().setQualifier(ATTR)
.setFamily(CF)
.setValue(Bytes.toBytes(data))
.build());
table.put(put);
Delete
세 가지 유형의 내부 delete 마커가 있어요. 다른 유형을 추가하려 한 시도에 대한 논의는 Lars Hofhansl의 블로그 Scanning in HBase: Prefix Delete Marker를 참고하세요.
- Delete: 컬럼의 특정 버전.
- Delete column: 컬럼의 모든 버전.
- Delete family: 특정 ColumnFamily의 모든 컬럼.
전체 행을 삭제할 때 HBase는 내부적으로 각 ColumnFamily마다(즉, 각 개별 컬럼이 아니라) tombstone을 만들어요.
Deletes는 tombstone 마커를 만들어 동작해요. 예를 들어 행을 삭제하고 싶다고 가정해 봐요. 버전을 지정하거나, 기본적으로 currentTimeMillis가 사용돼요. 이것이 의미하는 바는 버전이 이 버전보다 작거나 같은 모든 셀을 삭제한다는 거예요. HBase는 데이터를 제자리에서 수정하지 않으므로, 예를 들어 delete는 삭제 조건에 해당하는 storage file의 항목을 즉시 삭제하거나(삭제됨으로 표시) 하지 않아요. 대신 소위 tombstone이 쓰여지고, 이것이 삭제된 값을 마스킹해요. HBase가 major compaction을 수행하면 tombstone들이 처리되어 실제로 죽은 값들과 함께 tombstone 자체도 제거돼요. 행을 삭제할 때 지정한 버전이 행의 어떤 값 버전보다 크다면 전체 행이 삭제된 것으로 간주할 수 있어요.
deletes와 버전이 어떻게 상호작용하는지에 대한 유익한 논의는 사용자 메일링 리스트의 스레드 Put w/timestamp -> Deleteall -> Put w/ timestamp fails를 참고하세요.
내부 KeyValue 형식에 대한 자세한 내용은 keyvalue도 참고하세요.
Delete 마커는 컬럼 패밀리에 KEEP_DELETED_CELLS 옵션이 설정되어 있지 않으면(Keeping Deleted Cells 참고) 스토어의 다음 major compaction 중에 제거돼요. 삭제를 구성 가능한 시간 동안 유지하려면 hbase-site.xml의 hbase.hstore.time.to.purge.deletes 속성으로 delete TTL을 설정할 수 있어요. hbase.hstore.time.to.purge.deletes가 설정되지 않았거나 0으로 설정되면, 미래 타임스탬프를 가진 것들을 포함한 모든 delete 마커가 다음 major compaction 중에 제거돼요. 그 외에는 미래 타임스탬프를 가진 delete 마커는 마커의 타임스탬프가 나타내는 시간에 hbase.hstore.time.to.purge.deletes 값을 밀리초 단위로 더한 시간 이후에 발생하는 major compaction까지 유지돼요.
이 동작은 HBase 0.94에서 도입된 예상치 못한 변경에 대한 수정을 나타내며, HBASE-10118에서 수정됐어요. 이 변경은 HBase 0.94 이상 브랜치로 백포트됐어요.
HBase-2.0.0의 선택적 새 버전·삭제 동작 (Optional New Version and Delete behavior in HBase-2.0.0)
hbase-2.0.0에서 운영자는 컬럼 디스크립터 속성 NEW_VERSION_BEHAVIOR를 true로 설정해 대체 버전·삭제 처리를 지정할 수 있어요(컬럼 패밀리 디스크립터에 속성을 설정하려면 먼저 테이블을 disable하고 컬럼 패밀리 디스크립터를 alter해야 해요. 컬럼 패밀리 디스크립터의 속성 편집 예시는 Keeping Deleted Cells를 참고하세요).
'새 버전 동작'은 아래 나열된 제한을 해제해요. 즉 Delete는 어느 것이 먼저 도착했는지와 무관하게 같은 위치 — 같은 행, 컬럼 패밀리, 퀄리파이어, 타임스탬프 — 에 있으면 항상 Put을 압도(overshadow)해요. 버전 계산도 변경돼서, 삭제된 버전이 전체 버전 수에 포함돼요. 이는 major compaction이 개입하더라도 결과가 바뀌지 않도록 하기 위함이에요. 논의는 HBASE-15968과 연결된 이슈들을 참고하세요.
이 새 설정으로 실행하는 것은 현재 비용이 들어요. 매 compare마다 Cell MVCC를 계산해서 CPU를 더 소모해요. 느려지는 정도는 달라요. 테스트에서 0%~25% 저하를 봤어요.
복제(replication)를 사용한다면 새 직렬 복제(serial replication) 기능으로 실행하는 것이 좋아요(HBASE-9465 참고. 직렬 복제 기능은 hbase-2.0.0에는 들어가지 않았지만 이후 hbase-2.x 릴리스에서 올 거예요). 이제 Mutation이 도착하는 순서가 요소가 되니까요.
현재 제한 사항 (Current Limitations)
아래 제한 사항은 hbase-2.0.0에서 해결됐어요. 위의 HBase-2.0.0의 선택적 새 버전·삭제 동작 섹션을 참고하세요.
Deletes가 Puts를 압도함 (Deletes mask Puts)
Deletes는 put을 압도해요. delete가 입력된 후에 발생한 put조차도요. HBASE-2256을 참고하세요. delete는 tombstone을 쓰며, 이는 다음 major compaction이 실행된 후에만 사라진다는 것을 기억하세요. <= T인 모든 것을 삭제한다고 가정해 봐요. 그 후 타임스탬프 <= T인 새 put을 수행해요. 이 put은 delete 이후에 발생했더라도 delete tombstone에 의해 마스킹돼요. put을 수행하는 것은 실패하지 않지만, get을 하면 put이 아무 효과가 없었다는 것을 알 수 있어요. major compaction이 실행된 후에 다시 동작하기 시작해요. 행에 대한 새 puts에 항상 증가하는 버전을 사용하면 이러한 문제는 없어야 해요. 하지만 시간을 신경 쓰지 않더라도 발생할 수 있어요. delete와 put을 연달아 수행하면 같은 밀리초 내에 발생할 가능성이 있거든요.
Major compactions가 쿼리 결과를 바꿈 (Major compactions change query results)
...t1, t2, t3에 세 개의 셀 버전을 만들고 max-versions 설정을 2로 했다고 가정하자. 그래서 모든 버전을 가져올 때 t2와 t3의 값만 반환된다. 하지만 t2나 t3의 버전을 삭제하면 t1이 다시 나타난다. 분명히 major compaction이 실행되면 그러한 동작은 더 이상 발생하지 않는다... (Bending time in HBase의 Garbage Collection 참고)
정렬 순서 (Sort Order)
모든 데이터 모델 연산이 반환하는 데이터는 정렬된 순서로 반환돼요. 먼저 행 순으로, 그다음 ColumnFamily, 그다음 컬럼 퀄리파이어, 마지막으로 타임스탬프(역순 정렬이라 최신 레코드가 먼저 반환돼요)로요.
ColumnFamily의 내부 KeyValue 인스턴스 외부에는 컬럼 메타데이터가 저장되지 않아요. 따라서 HBase가 행당 매우 많은 컬럼 수를 지원할 뿐만 아니라 행 사이의 이질적인 컬럼 집합도 지원하지만, 컬럼 이름을 추적하는 것은 여러분의 책임이에요.
ColumnFamily에 존재하는 전체 컬럼 집합을 얻는 유일한 방법은 모든 행을 처리하는 것이에요. HBase가 데이터를 내부적으로 저장하는 방법에 대한 자세한 내용은 keyvalue를 참고하세요.
조인 (Joins)
HBase가 조인을 지원하는지 여부는 dist-list의 흔한 질문이며 간단한 답이 있어요: 지원하지 않아요. 적어도 RDBMS가 지원하는 방식(예: SQL의 equi-joins나 outer-joins)으로는요. 이 챕터에서 설명했듯이 HBase의 읽기 데이터 모델 연산은 Get과 Scan이에요.
하지만 그렇다고 애플리케이션에서 동등한 조인 기능을 지원할 수 없다는 뜻은 아니에요. 직접 해야 한다는 것뿐이에요. 두 가지 주요 전략은 HBase에 쓸 때 데이터를 비정규화(denormalize)하거나, 룩업 테이블을 두고 애플리케이션이나 MapReduce 코드에서 HBase 테이블 사이를 조인하는 것이에요(RDBMS가 보여주듯, 테이블 크기에 따라 몇 가지 전략이 있어요. 예: nested loops 대 hash-joins). 그럼 어떤 접근이 최선일까요? 무엇을 하려는지에 달려 있어요. 모든 사용 사례에 적용되는 단일 답은 없어요.
ACID
ACID Semantics를 참고하세요. Lars Hofhansl은 ACID in HBase에 대한 노트도 작성했어요.
더 알아보기 (Learn more)