1. 서론
이번 포스팅에서는 Hbase가 제공하는 클라이언트 API 중 기본적인 기능에 대해 알아보겠습니다.
2. 일반 정보
Hbase에 접근하는 주요 클라이언트 인터페이스는 org.apache.hadoop.hbase.client 패키지에 있는 HTable 입니다.
HTable은 Hbase에 데이터를 저장, 삭제의 일을 할 수 있도록 제공합니다.
단, HTable을 사용할 시 주의할 점이 있습니다.
HTable은 인스턴스화될 시 메타(.META.) 테이블을 스캔하여 테이블이 있는지, 있다면 활성화 되있는지를 확인하고
그 외에도 부수적인 작업들을 수행하기 때문에 인스턴스화 작업은 느립니다.
따라서, HTable 인스턴스화는 되도록 한번만 수행하며 스레드당 하나씩만 생성하도록 하는것이 좋습니다.
그리고, 한번 생성된 HTable는 어플리케이션이 끝날때까지 재사용하는 편이 성능상 이점을 볼 수 있습니다.
3. CRUD 기능
HTable의 CRUD 역할에 대해 소개하겠습니다.
1) Put 메소드
데이터를 적재할 때 사용하는 API입니다.
단일로우와 멀티로우를 적재할 수 있도록 모두 제공하고 있습니다.
1. 단일 Put
put 메서드의 명세는 아래와 같습니다.
void put(Put put) throws IOException
인자로 받는 Put은 하나 또는 리스트 형태로 받을 수 있습니다.
아래는 Put의 생성자 메서드 명세입니다.
Put(byte[] row)
Put(byte[] row, RowLock rowLock)
Put(byte[] row, long ts)
Put(byte[] row, long ts, RowLock rowLock)
Hbase의 로우는 고유한 키입니다.
이 키는 타입이 자바의 바이트 배열로 어떠한 값이 들어와도 상관이 없습니다.
단, 로우 키는 사전편찬식으로 정렬된다는 것을 명심하고 키설계를 하여 사용해야 합니다.
아래는 어떠한 타입이든 바이트 배열로 만들어주는 헬퍼 클래스인 Bytes의 메서드입니다.
static byte[] toBytes(ByteBuffer bb)
static byte[] toBytes(String s)
static byte[] toBytes(boolean b)
static byte[] toBytes(long val)
static byte[] toBytes(float f)
static byte[] toBytes(int val)
Put 인스턴스를 생성 한 다음에는 컬럼패밀리, 퀄리파이어, 값, 타임스탬프 등을 추가할 수 있습니다.
아래는 Put의 데이터 추가 메서드입니다.
Put add(byte[] family, byte[] qualifier, byte[] value)
Put add(byte[] family, byte[] qualifier, long ts, byte[] value)
Put add(KeyValue kv) throws IOException
add 메서드를 한번 수행할 때마다 하나의 컬럼이 추가됩니다.
KeyValue 클래스의 경우에는 하나의 고유한 셀을 나타내는 고급 클래스입니다.
이 클래스를 얻기 위해서는 get 메서드를 통해 얻을 수 있습니다.
아래는 KeyValue를 가져오는 get 메서드입니다.
List<KeyValue> get(byte[] family, byte[] qualifier)
Map<byte[], List<KeyValue>> getFamilyMap()
특정 셀이 존재하는지 알아볼때에는 아래와 같이 get이 아닌 has 메서드를 제공합니다.
boolean has(byte[] family, byte[] qualifier)
boolean has(byte[] family, byte[] qualifier, long ts)
boolean has(byte[] family, byte[] qualifier, byte[] value)
boolean has(byte[] family, byte[] qualifier, long ts, byte[] value)
아래는 put을 통해 데이터를 적재하는 간단한 예제입니다.
public class PutExample {
public static void main(String[] args) {
Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "testtable");
Put put = new Put(Bytes.toBytes("row1"));
put.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val1"));
put.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"), Bytes.toBytes("val2"));
hTable.put(put);
}
}
위의 Configuration은 org.apache.hadoop.conf 패키지에 존재하며 설정 정보를 주입하는 역할입니다.
기존 설정정보는 hbase-site.xml에 기입해야하지만, 동적으로 변경해야 할때는 Configuration를 사용하면 됩니다.
아래는 주키퍼 쿼럼 정보를 동적으로 Configuration를 사용하여 세팅하는 예제입니다.
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "zk1.foo.com,zk2.foo.com,");
put의 경우 타임스탬프를 부여하여 각 셀에 대하여 버저닝을 할 수도 있습니다.
부여하지 않을 시에는 로우가 저장소에 추가되는 순간에 해당 리전서버의 시각으로 자동 부여가 됩니다.
Hbase의 경우, 셀은 타임스탬프값을 기준으로 정렬되어져 저장됩니다.
2. KeyValue 클래스
코드상에서 KeyValue 인스턴스를 처리해야 하는 경우가 종종 있습니다.
때문에, KeyValue 클래스에 대해 간단히 살펴보겠습니다.
우선, KeyValue 클래스는 특정 셀의 정보를 가지고 있습니다.
특정 셀의 정보는 로우 키, 컬럼패밀리, 컬럼 퀄리파이어, 타임스탬프를 의미합니다.
메서드로는 아래와 같이 있습니다.
// 아래 3개는 KeyValue 인스턴스에 저장되어 있는 전체 바이트 배열에 관한 메서드입니다.
byte[] getBuffer()
int getOffset()
int getLength()
// 아래 2개는 로우키와 데이터가 저장된 워시 좌표 정보의 바이트 배열을 반환하는 메서드입니다.
byte[] getRow()
byte[] getKey()
또한, KeyValue 클래스는 내부적으로 Comparator를 통해 값에 대해서 커스텀하게 정렬을 할 수 있도록 제공합니다.
아래는 KeyValue 클래스에서 제공하는 Comparator 종류입니다.
비교 연산자 | 설명 |
KeyComparator | KeyValue 2개의 키를 비교합니다. 즉, getKey 메서드를 통해 원시 바이트 배열을 비교합니다. |
KVComparator | 원시 형태의 KeyComparator를 감싼 형으로서, KeyValue 2개를 비교할 때 사용합니다. |
RowComparator | getRow 로 얻은 값으로 비교합니다. |
KeyValue 클래스는 type이라는 필드를 가지고 있습니다.
이 type은 KeyValue에 하나의 차원이 더 추가되는것과 같습니다.
type 에 사용가능한 값은 아래와 같습니다.
유형 | 설명 |
Put | 해당 KeyValue 인스턴스가 일반적인 Put 연산임을 의미합니다. |
Delete | 해당 KeyValue 인스턴스가 일반적인 Delete 연산임을 의미합니다. |
DeleteColumn | Delete와 같지만, 더 광범위하게 전체 컬럼을 삭제한다는 의미입니다. |
DeleteFamily | Delete와 같지만, 더 광범위하게 전체 컬럼패밀리 및 그에 속한 모든 컬럼을 삭제한다는 의미입니다. |
마지막으로 KeyValue에 정보를 출력하고 싶으실때는 아래 메서드를 사용하시면 됩니다.
String toString()
아래는 toString 메서드의 결과 형식을 나타냅니다.
<row-key>/<family>:<qualifier>/<version>/<type>/<value-length>
3. 클라이언트 측 쓰기 버퍼
Hbase의 경우 쓰기 연산은 RPC를 통해 이루어 집니다.
이 RPC는 remote procedure call로서 갯수가 적을때는 괜찮지만 초당 수천개의 값을 테이블에 저장하는 데에는 적합하지 않습니다.
그로인해 Hbase 클라이언트는 내부적으로 쓰기 버퍼를 두어 한번의 RPC로 서버에 데이터를 전송하도록 제공하고 있습니다.
쓰기버퍼의 사용여부는 아래와 같이 설정할 수 있습니다.
현재 포스팅을 작성하는 때에는 아래와 같이 버퍼 관련은 deprecated 되었습니다. -> 최신 버전에서는 아예 메서드에서 제외되었습니다.
대신, BufferedMutator 클래스를 별도로 제공하여 아래와 같은 기능을 제공하고 있습니다.
지금은 책내용을 기반으로 진행하도록 하겠습니다.
void setAutoFlush(boolean autoFlush)
boolean isAutoFlush()
디폴트는 비활성 상태입니다.
활성으로 하기 위해서는 아래와 같이 setAutoFlush를 false로 설정합니다.
table.setAutoFlush(false)
위와 같이 false로 설정된 후부터는 put 메서드를 호출하더라도 클라이언트측 메모리 버퍼에 저장되고 서버로는 실제 전송이 이루어 지지 않습니다.
서버로 RPC를 날리기 위해서는 아래와 같은 메서드를 호출하면 됩니다.
void flushCommits() throws IOException
쓰기 버퍼의 경우 클라이언트에서 내부적으로 알아서 비워주기 때문에 크게 고려하지 않아도 됩니다.
추가로, 쓰기 버퍼의 크기도 조정이 가능한데 방법은 아래와 같습니다.
long getWriteBufferSize()
long setWriteBufferSize(long writeBufferSize) throws IOException
물론, 명시적으로 버퍼를 비울수도 있습니다.
개발자가 작성한 코드로 인해서 버퍼가 비워지는 경우는 아래와 같습니다.
- flushCommits 호출 시
- autoFlush가 true의 경우 put 메서드 호출 시
- setWriteBufferSize 호출 시
아래는 쓰기 버퍼를 사용한 예제 입니다.
public static void main(String[] args) {
Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "testtable");
System.out.println("Auto flush: " + hTable.isAutoFlush());
hTable.setAutoFlush(false);
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val1"));
hTable.put(put1);
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val2"));
hTable.put(put2);
Put put3 = new Put(Bytes.toBytes("row3"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val3"));
hTable.put(put3);
Get get = new Get(Bytes.toBytes("row1"));
Result res1 = hTable.get(get);
System.out.println("Result: " + res1);
hTable.flushCommits();
Result res2 = hTable.get(get);
System.out.println("Result: " + res2);
}
위 결과 출력은 아래와 같습니다.
Auto flush: true
Result: keyvalues=None
Result: keyvalues={ro1/colfam1/qual1/123412358/Put/vlen=4}
위에서 설명한것과 같이 버퍼에 쓰기만 하고 서버로 PRC를 하지 않았기 때문에
첫번째 Get에서는 None이 나온것을 볼 수 있습니다.
만약 버퍼에 있는 데이터를 보기 위해서는 아래 메서드를 사용하면 됩니다.
ArrayList<Put> getWriteBuffer()
하지만 이 방법은 멀티쓰레드 환경에서 조심해야 합니다.
이유로는 List에 접근 시 힙 크기를 확인하지 않고 접근하며, 또 버퍼 비우기가 진행되는 도중 다른 쓰레드가 값을 변경할 수 있기 때문입니다.
4. Put 리스트
클라이언트 API는 단일 put이 아닌 List<put>도 처리 가능하도록 제공합니다.
void put(List<Put> puts) throws IOException
위 메서드를 사용하면 List<Put>으로 데이터 적재가 가능합니다.
다만, List에 있는 모든 Put이 성공하지 않을 수 있습니다.
성공하지않은 Put이 있다면 클라이언트는 IOException을 받게됩니다.
하지만, Hbase는 List에 있는 put을 이터레이트돌며 적용하기 때문에 하나가 실패한다고 안에 있는것이 모두 실패하지는 않습니다.
실패한 Put은 쓰기버퍼에 남아있게 되고 다음 flush 작업에서 재수행하게 됩니다.
만약 데이터가 잘못되어 실패되는 케이스라면 계속 버퍼에 남게되어 재수행을 반복하게 될 것입니다.
이를 방지하기 위해서는 수동을 버퍼를 비워줘야 합니다.
아래는 일괄 put을 날린 후 try-catch를 통해 실패가 발생하게 있다면 명시적으로 버퍼를 비우는 예제 코드입니다.
List<Put> puts = new ArrayList<>();
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val1"));
puts.add(put1)
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("BOGUS"), Bytes.toBytes("qual1"), Bytes.toBytes("val2"));
puts.add(put2)
Put put3 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"), Bytes.toBytes("val3"));
puts.add(put3)
Put put4 = new Put(Bytes.toBytes("row2"));
puts.add(put4)
try {
hTable.put(puts);
} catch (Exception e) {
hTable.flushCommits();
}
추가로, 리스트 기반의 입력시에는 Hbase 서버에서 입력 연산의 순서가 보장되지 않습니다.
5. 원자적 확인 후 입력 연산
Hbase에서는 원자적 확인 후 입력이라는 특별한 기능을 제공합니다.
이 기능은 특정한 조건에 만족하는 경우 put 연산을 수행할 수 있도록합니다.
반환값으로는 boolean 값으로 put 연산이 수행되었는지의 여부를 의미합니다.
사용법으로는 아래와 같습니다.
최신 버전에서는 deprecated 되어 있습니다.
boolean checkAndPut(byte[] row, byte[] family, byte[] qualifier, byte[] value, Put put) throws IOException
boolean checkAndPut(final byte [] row, final byte [] family, final byte [] qualifier, final CompareOp compareOp, final byte [] value, final Put put) throws IOException
boolean checkAndPut(final byte [] row, final byte [] family, final byte [] qualifier, final CompareOp compareOp, final byte [] value, final Put put) throws IOException
2) Get 메서드
Hbase 클라이언트는 데이터를 읽어오는 Get 메서드를 제공합니다.
1. 단일 Get
특정 값을 반환받는데 사용하는 메서드입니다.
Result get(Get get) throws IOException
put과 유사하게 get 메서드도 전용 Get 클래스를 인자로 받고 있습니다.
Get 클래스의 생성자 메서드는 아래와 같습니다.
Get(byte[] row)
Get(byte[] row, RowLock rowLock)
아래는 한 로우에대해서 읽는 데이터 범위를 줄이기위해 필요한 보조적인 메서드들입니다.
Get addFamily(byte[] family)
Get addColumn(byte[] family, byte[] qualifier)
Get setTimeRange(long minStamp, long maxStamp) throws IOException
Get setTimeStamp(long timestamp)
Get setMaxVersions()
Get setMaxVersions(int maxVersions) throws IOException
읽어올때는 위에서 소개한 Bytes 헬퍼 클래스를 통해 byte[]을 원하는 데이터 타입으로 변환이 가능합니다.
static String toString(byte[] b)
static boolean toBoolean(byte[] b)
static long toLong(byte[] b)
static float toFloat(byte[] b)
static int toInt(byte[] b)
2. Result 클래스
get 메서드의 반환 타입은 Result 클래스입니다.
Result 클래스는 Hbase 데이터인 컬럼패밀리, 퀄리파이어, 타임스탬프, 값 등을 모두 가지고 있으며 내부 메서드로 제공하고 있습니다.
byte[] getValue(byte[] family, byte[] qualifier) // 특정 셀 값
byte[] value() // 사전 편찬식으로 정렬된 KeyValue 중에 첫번째 값
byte[] getRow() // 로우키
int size() // KeyValue 갯수
boolean isEmpty() // KeyValue 존재 여부
KeyValue[] raw() // KeyValue 접근을 위한 메서드
List<KeyValue> list() // raw에서 반환되는 KeyValue 배열을 단순히 list 형식으로 바꿔서 반환하는 메서드
아래는 부가적으로 컬럼단위로 제공하는 메서드입니다.
List<KeyValue> getColumn(byte[] family, byte[] qualifier)
KeyValue getColumnLatest(byte[] family, byte[] qualifier)
boolean containsColumn(byte[] family, byte[] qualifier)
아래는 Result의 데이터에 접근을 용이하게 제공하는 Map 지향적인 메서드입니다.
NavigableMap<byte[], NavigableMap<byte[], NavigableMap<Long, byte[]>>> getMap()
NavigableMap<byte[], NavigableMap<byte[], byte[]>> getNoVersionMap() // 최근 값만을 반환
NavigableMap<byte[], byte[]> getFamilyMap(byte [] family)
위와 같은 메서드는 상황에 따라 적절히 선택하여 사용하면 되며,
이미 Result 인스턴스는 서버에서 클라이언트로 데이터를 받았기 때문에 어떤 메서드를 선택하든지 성능에 이슈는 없습니다.
3. Get 리스트
get은 단일 결과가 아닌 멀티개의 결과도 읽을 수 있도록 제공합니다.
Result[] get(List<Get> gets) throws IOException
put 리스트와 차이점으로는 예외 발생시 입니다.
get의 경우에는 List<Get>의 동일한 크기의 배열을 반환받는것과 아예 예외를 반환받는 둘 중에 한가지로 동작이 됩니다.
4. Get 관련 기타 메서드
단순히 데이터를 읽어오는 메서드말고도 제공하는 메서드에 대해서 알아 보겠습니다.
boolean exists(Get get) throws IOException
Result getRowOrBefore(byte[] row, byte[] family)
exists 의 경우에는 메서드명 그대로 지정한 로우가 있는지 확인하기 위한 메서드입니다.
getRowOrBefore의 경우에는 지정한 로우가 있으면 반환하지만 없으면 바로 그 이전 로우를 반환합니다.
이전 로우도 없다면 null를 반환합니다.
Hbase는 로우키 기준으로 사전편찬식으로 정렬되어 저장되기 때문에 이러한 기능이 가능합니다.
3) Delete 메서드
이번엔 CRUD중 D에 해당하는 delete 메서드에 대해 알아 보겠습니다.
1. 단일 Delete
void delete(Delete delete) throws IOException
put과 get과 마찬가지로 Delete 전용 클래스를 인자로 받습니다.
Delete 클래스의 생성자는 아래와 같습니다.
Delete(byte[] row)
Delete(byte[] row, long timestamp, RowLock rowLock)
추가로 삭제 범위를 줄일때에는 아래 메서드를 활용하면 됩니다.
Delete deleteFamily(byte[] family)
Delete deleteFamily(byte[] family, long timestamp)
Delete deleteColumns(byte[] family, byte[] qualifier)
Delete deleteColumns(byte[] family, byte[] qualifier, long timestamp)
Delete deleteColumn(byte[] family, byte[] qualifier) // 최근 버전만삭제
Delete deleteColumn(byte[] family, byte[] qualifier, long timestamp) // 지정한 타임스탬프와 일치하는것만 삭제
void setTimestamp(long timestamp) // 전체 컬럼패밀리 대상으로 지정한 타임스탬프 이하인 값들을 삭제
2. Delete 리스트
delete도 위 put, get과 마찬가지로 단일 뿐만이 아니라 List<Delete>도 제공합니다.
void delete(List<Delete> deletes) throws IOException
Delete의 예외 방식에 대해 알아보겠습니다.
delete()는 인수로 전달했던 List<Delete> 에서 실패한 Delete 객체만이 남게됩니다.
결국, 모두 성공했을때는 인자로 받은 List<Delete>는 텅비게 되어집니다.
3. 원자적 확인 후 삭제 연산
delete에서도 put과 동일하게 원자적으로 확인 후 삭제 연산을 할 수 있도록 제공하고 있습니다.
boolean checkAndDelete(final byte[] row, final byte[] family, final byte[] qualifier, final byte[] value, final Delete delete) throws IOException
boolean checkAndDelete(final byte[] row, final byte[] family, final byte[] qualifier, final CompareOp compareOp, final byte[] value, final Delete delete) throws IOException
boolean checkAndDelete(final byte[] row, final byte[] family, final byte[] qualifier, final CompareOperator op, final byte[] value, final Delete delete) throws IOException
value 파라미터에 null을 넣어서 수행하게 되면 해당 컬럼 값이 존재하는지 여부를 검사합니다.
4. 일괄처리 연산
지금까지는 단일 또는 리스트 기반의 연산을 알아봤습니다.
이번에는 여러개의 로우에 대해서 다양한 연산을 일괄처리하는 API에 대해서 알아보겠습니다.
클라이언트는 이 일괄처리를 위해 Put, Get, Delete의 조상 클래스인 Row 클래스를 제공합니다.
메서드는 아래와 같습니다.
void batch(final List<? extends Row> actions, final Object[] results)
여러 Row 하위 클래스들을 인자로 받아 처리 후 2번째 인자로 받은 Objects 배열에 결과를 담습니다.
또 한가지 메서드가 있는데 명세는 아래와 같습니다.
Object[] batch(final List<? extends Row> actions) throws IOException, InterruptedException
이 메서드는 예외가 발생하면 throws가 되므로 결과값 배열에는 아무것도 담기지 않게됩니다.
위 두 batch 메서드 수행시에는 Put 인스턴스가 쓰기 버퍼에 저장되지 않습니다.
아래는 예제입니다.
private final static byte[] ROW1 = Bytes.toBytes("row1");
private final static byte[] ROW2 = Bytes.toBytes("row2");
private final static byte[] COLFAM1 = Bytes.toBytes("colfam1");
private final static byte[] COLFAM2 = Bytes.toBytes("colfam2");
private final static byte[] QUAL1 = Bytes.toBytes("qual1");
private final static byte[] QUAL2 = Bytes.toBytes("qual2");
public static void main(String[] args) {
Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "testtable");
List<Row> batch = new ArrayList<>();
Put put = new Put(ROW2);
put.addColumn(COLFAM2, QUAL1, Bytes.toBytes("val5"));
batch.add(put);
Get get1 = new Get(ROW1);
get1.addColumn(COLFAM1, QUAL1);
batch.add(get1);
Delete delete = new Delete(ROW1);
delete.deleteColumns(COLFAM1, QUAL2);
batch.add(delete);
Get get2 = new Get(ROW2);
get2.addFamily(Bytes.toBytes("BOGUS"));
batch.add(get2);
Object[] results = new Object[batch.size()];
try {
hTable.batch(batch, results);
} catch (Exception e) {
System.out.println("ERROR : " + e);
}
Arrays.stream(results).forEach(item -> System.out.println("Result : " + item));
}
5. 로우 락
Hbase의 경우 각 로우에 대해 원자성을 보장하여 순차적으로 처리합니다.
그렇기 때문에 위에서 알아본 put, delete, checkAndPut 등도 모두 원자성이 보장되면서 수행이 되어집니다.
한 예로 Put 인스턴스를 생성시에 RowLock을 부여하지 않는다면 서버측에서는 해당 로우에 대해서
메서드가 수행되는 동안에만 유지되는 락을 생성하여 적용하게 됩니다.
만약, 한 로우에 대해서 클라이언트측이 커스텀하게 락을 획득 및 해제를 하고 싶은 경우에는 아래 메서드를 이용하면 됩니다.
단, 잘못하면 다른 연산 메서드에 영향이 갈 수 있으니 신중히 사용해야 합니다.
RowLock lockRow(byte[] row) throws IOException
void unlockRow(RowLock rl) throws IOException
락은 다른 수행에 영향이 끼칠 수 있으니 무기한으로 설정되어 있지는 않습니다.
hbase-site.xml에 hbase.resionserver.lease.period 에 값만큼이 락의 유효 기간입니다.
디폴트 1분입니다.
6. 스캔
Hbase는 범위 탐색인 스캔 기능을 제공합니다.
스캔은 Hbase가 제공하는 순차적이고 정렬된 저장구조를 활용합니다.
1) 소개
Hbase 클라이언트는 Scan 이라는 별도의 클래스를 제공하고 있습니다.
아래는 사용 메서드입니다.
ResultScanner getScanner(Scan scan)
ResultScanner getScanner(byte [] family)
ResultScanner getScanner(byte [] family, byte [] qualifier)
Scan 인자를 주면 내부적으로 스캔한 결과를 담은 ResultScanner를 반환합니다.
2,3번째 메서드는 내부적으로 Scan을 만들어서 동작 후 결과를 반환합니다.
아래는 Scan의 생성자입니다.
Scan()
Scan(byte[] startRow, Filter filter)
Scan(byte[] startRow)
Scan(byte[] startRow, byte[] stopRow)
생성자에서 알 수 있듯이 Scan은 꼭 정확한 로우를 몰라도 탐색이 가능합니다.
startRow만 주게된다면 startRow보다 같거나 큰 로우들부터 탐색을하게 됩니다.
stopRow는 탐색이 해당 stopRow보다 크거나 같은 로우를 만나면 끝나게 됩니다.
Filter는 말그대로 탐색 중 사용자가 지정한 데이터만을 얻기위해서 있는 인자입니다.
인자가 없는 기본 생성자로도 수행은 되며, 이경우에는 모든 컬럼패밀리 및 그에 속한 컬럼을 포함한 전체 테이블을 스캔합니다.
아래는 위 CRUD의 메서드들과 같이 탐색의 범위를 줄이기 위한 메서드입니다.
Scan addFamily(byte [] family)
Scan addColumn(byte [] family, byte [] qualifier)
Scan setTimeRange(long minStamp, long maxStamp) throws IOException
Scan setTimeStamp(long timestamp)
Scan setMaxVersions()
Scan setMaxVersions(int maxVersions)
그 외에도 Scan 클래스의 경우에는 getter/setter가 있어 언제든지 활용이 가능합니다.
2) ResultScanner 클래스
스캔의 경우에는 한번의 RPC로 로우들을 클라이언트한테 반환하지 않습니다.
이유는, 방대한 로우가 탐색될 수도 있어 한번의 RPC로는 오버헤드가 발생할 수 있기때문입니다.
때문에, ResultScanner는 Get과 비슷한 연산으로 변환 후 Row에 대한 Result 인스턴스를 이터레이트할 수 있도록 감싼 형태입니다.
아래는 ResultScanner의 메서드입니다.
Result next() throws IOException // 한번에 한 Result 반환
Result[] next(int nbRows) throws IOException // nbRows 만큼의 Result 반환
void close()
스캐너의 경우 서버 측 리소스를 꽤 사용하기 때문에 힙 공간을 낭비하게 됩니다.
때문에, 꼭 작업이 끝난다음에는 close를 명시적으로 호출하여 자원을 반환하게 해야합니다.
아래는 스캐너 사용 예제입니다.
public static void main(String[] args) throws IOException {
Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "testtable");
Scan scan1 = new Scan();
ResultScanner scanner1 = hTable.getScanner(scan1);
for(Result res: scanner1) {
System.out.println(res);
}
scanner1.close();
Scan scan2 = new Scan();
scan2.addFamily(Bytes.toBytes("colfam1"));
ResultScanner scanner2 = hTable.getScanner(scan2);
for(Result res: scanner2) {
System.out.println(res);
}
scanner2.close();
Scan scan3 = new Scan();
scan3.addColumn(Bytes.toBytes("colfam1"), Bytes.toBytes("col-5"))
.addColumn(Bytes.toBytes("colfam2"), Bytes.toBytes("col-33"))
.setStartRow(Bytes.toBytes("row-10"))
.setStopRow(Bytes.toBytes("row-20"));
ResultScanner scanner3 = hTable.getScanner(scan3);
for(Result res: scanner3) {
System.out.println(res);
}
scanner3.close();
}
3) 캐싱 대 일괄처리
스캔의 경우 next메서드가 호출될때 마다 RPC가 발생합니다.
이는, 데이터 크기가 작은 경우에는 오히려 성능상으로 안좋을 수 있습니다.
때문에, Hbase에서는 스캐너 캐싱이라는 기능을 제공합니다.
이 캐싱 기능은 아래와 같이 2개의 다른 수중에서 활성화할 수 있습니다.
- 테이블 단위의 캐싱
- 스캔 단위의 캐싱
1. 테이블 단위 캐싱
테이블 단위의 캐싱을 사용할 때는 아래 메서드를 사용하면 됩니다.
void setScannerCaching(int scannerCaching)
int getScannerCaching()
아래는 스캔 단위의 캐싱 메서드입니다.
void setCaching(int caching)
int getCaching()
이러한 캐싱의 효과로는 RPC가 반환하는 로우 갯수를 사용자가 제어할 수 있다는 점입니다.
무조건 캐싱양을 늘리기보다는 클라이언트와 서버의 스펙을 고려하여 최적점을 찾아야 합니다.
무제한으로 올리게 된다면 최악에는 OutOfMemoryException이 발생하게 됩니다.
하지만 예기치 못하게 하나의 로우가 매우 커서 메모리 이슈가 발생할 수 도 있습니다.
Hbase에서는 이를 위해 일괄처리 기능을 제공합니다.
void setBatch(int batch)
int getBatch()
일괄처리는 캐싱과 다르게 로우단위가 아닌 컬럼단위로 동작합니다.
한마디로 next 메서드 호출 시 반환되는 컬럼갯수를 제어합니다.
예를들어, setBatch(5)로 세팅을 하게된다면 Result 인스턴스마다 다섯개의 컬럼이 반환되어집니다.
만약 로우의 컬럼이 17개이고 일괄처리를 5로 설정했을때는 하나의 로우를 조각내어
5->5->5->2 개로 반환받게 됩니다.
처음에는 이 2개를 고려하지 않아도 되지만,
점차 서비스가 커지면서 최적화를 해야한다면 위의 스캐너 캐싱과 일괄처리를 적절히 조합하여 사용해야합니다.
7. 마무리
이번 포스팅에서는 클라이언트 API : 기본 기능에 대해 진행하였습니다.
다음 포스팅에서는 챕터 4장인 클라이언트 API : 고급 기능에 대해 진행하겠습니다.
'BigData > Hbase' 카테고리의 다른 글
(5) 클라이언트 API : 관리 기능 (0) | 2020.06.02 |
---|---|
(4) 클라이언트 API : 고급 기능 (0) | 2020.04.19 |
(2) 설치 (0) | 2020.04.08 |
(1) 소개 (0) | 2020.04.07 |