Skip to content

Split View: HBase 실전 가이드: 대규모 NoSQL 데이터 저장소 설계부터 운영까지

|

HBase 실전 가이드: 대규모 NoSQL 데이터 저장소 설계부터 운영까지

들어가며

HBase는 Google Bigtable 논문을 기반으로 만들어진 분산 NoSQL 데이터베이스입니다. HDFS 위에서 동작하며, 수십억 행과 수백만 컬럼을 처리할 수 있는 대규모 랜덤 읽기/쓰기에 최적화되어 있습니다. 시계열 데이터, 로그 분석, 실시간 서빙 레이어 등 대용량 데이터를 낮은 지연 시간으로 접근해야 하는 시나리오에서 많이 사용됩니다.

이 글에서는 HBase의 핵심 개념부터 실전 운영 노하우까지 체계적으로 다룹니다.

1. HBase란?

핵심 특성

특성설명
분산HDFS 위에서 수평 확장 가능
Column-Family 기반컬럼 패밀리 단위로 데이터 저장
Versioned셀마다 여러 버전(타임스탬프) 저장 가능
강한 일관성단일 행에 대해 원자적 읽기/쓰기 보장
자동 샤딩Region 단위로 데이터 자동 분배
높은 처리량수십만 QPS 처리 가능

언제 HBase를 사용하는가?

적합한 경우:

  • 수십억 행 이상의 대규모 데이터
  • 빠른 랜덤 읽기/쓰기가 필요한 경우 (< 10ms)
  • 시계열 데이터 (IoT 센서, 로그, 메트릭)
  • 넓은 테이블 (수백~수천 컬럼)
  • HDFS와 통합이 필요한 경우

부적합한 경우:

  • 수백만 행 이하의 작은 데이터셋
  • 복잡한 JOIN, 트랜잭션이 필요한 경우
  • 전문 검색 (Elasticsearch가 더 적합)
  • Ad-hoc 분석 쿼리 (Hive, Spark SQL이 더 적합)

2. HBase 아키텍처

전체 구조

┌────────────────────────────────────────────────────────┐
Client   (HBase Shell, Java API, REST, Thrift)└───────────────────────┬────────────────────────────────┘
                ┌───────▼───────┐
ZooKeeper                  (coordination│
& discovery)                └───┬───────┬───┘
                    │       │
            ┌───────▼──┐  ┌─▼──────────┐
HMaster  │  │  HMaster             (Active) (Standby)            └───────┬───┘  └────────────┘
     ┌──────────────┼──────────────┐
     │              │              │
┌────▼─────┐  ┌────▼─────┐  ┌────▼─────┐
│RegionSvr │  │RegionSvr │  │RegionSvr │
│┌────────┐│  │┌────────┐│  │┌────────┐│
││Region A││  ││Region C││  ││Region E││
│├────────┤│  │├────────┤│  │├────────┤│
││Region B││  ││Region D││  ││Region F││
│└────────┘│  │└────────┘│  │└────────┘│
└──────────┘  └──────────┘  └──────────┘
       │              │              │
       └──────────────┼──────────────┘
               ┌──────▼──────┐
HDFS                (Storage)               └─────────────┘

구성 요소별 역할

구성 요소역할장애 시 영향
HMasterRegion 할당, DDL 처리, 부하 분산DDL 불가, 자동 밸런싱 중단 (읽기/쓰기는 가능)
RegionServer데이터 읽기/쓰기 처리, Region 관리해당 Region 접근 불가 (자동 복구)
ZooKeeperHMaster 선출, RS 상태 추적, 메타 위치전체 클러스터 중단
HDFS실제 데이터 영구 저장데이터 손실 위험

Region 내부 구조

┌─────────────── Region ──────────────────┐
│                                          │
│  ┌─── Column Family: cf1 ─────────────┐ │
│  │  ┌──────────┐                      │ │
│  │  │ MemStore   (메모리, 쓰기 버퍼)  │ │
│  │  └──────────┘                      │ │
│  │  ┌──────────┐  ┌──────────┐       │ │
│  │  │ HFile 1  │  │ HFile 2  │       │ │
│  │  └──────────┘  └──────────┘       │ │
│  └────────────────────────────────────┘ │
│                                          │
│  ┌─── Column Family: cf2 ─────────────┐ │
│  │  ┌──────────┐                      │ │
│  │  │ MemStore │                      │ │
│  │  └──────────┘                      │ │
│  │  ┌──────────┐                      │ │
│  │  │ HFile 1  │                      │ │
│  │  └──────────┘                      │ │
│  └────────────────────────────────────┘ │
│                                          │
│  ┌──────────────────────────────────┐   │
│  │         WAL (Write-Ahead Log)    │   │
│  └──────────────────────────────────┘   │
└──────────────────────────────────────────┘

쓰기 경로 (Write Path)

1. ClientRegionServer에 Put 요청
2. WAL(Write-Ahead Log)기록 (HDFS에 저장, 장애 복구용)
3. MemStore에 기록 (메모리)
4. Client에 성공 응답
5. MemStore가 가득 차면 → HFile로 Flush (HDFS에 저장)

읽기 경로 (Read Path)

1. ClientRegionServer에 Get 요청
2. Block Cache 확인 (메모리)
3. MemStore 확인 (메모리)
4. HFile 확인 (디스크, Bloom Filter로 빠르게 필터링)
5. 결과 병합 (최신 버전 반환)

3. 데이터 모델

논리적 데이터 모델

Table: user_activity
─────────────────────────────────────────────────────────────────
RowKey          | Column Family: info      | Column Family: stats
                | name     | email        | login_count | last_login
─────────────────────────────────────────────────────────────────
user001         | 김영주   | yj@email.com | 142         | 2026-03-08
user002         | 박서준   | sj@email.com | 87          | 2026-03-07
user003         | 이하나   | hn@email.com | 256         | 2026-03-08
─────────────────────────────────────────────────────────────────

물리적 저장 구조

# 실제로는 Column Family 단위로 별도 저장
# Key-Value 형태: (RowKey, CF:Qualifier, Timestamp)Value

# Column Family: info
(user001, info:name, t3)"김영주"
(user001, info:name, t1)"김영주(구)"     # 이전 버전
(user001, info:email, t2)"yj@email.com"
(user002, info:name, t4)"박서준"
(user002, info:email, t4)"sj@email.com"

# Column Family: stats (별도 HFile)
(user001, stats:login_count, t5)142
(user001, stats:last_login, t5)"2026-03-08"

핵심 용어 정리

용어설명비유 (RDBMS)
Table데이터 컨테이너Table
RowRowKey로 식별되는 행Row
Column Family컬럼 그룹 (물리적 저장 단위)(없음)
Column QualifierCF 내의 컬럼명Column
Cell(Row, CF:Qualifier, Timestamp)의 값Cell
Timestamp셀의 버전 (기본: 밀리초)(없음)
Region테이블의 수평 분할 단위Partition

4. RowKey 설계 전략

RowKey 설계는 HBase 성능의 80%를 결정합니다.

핫스팟 문제

# 잘못된 RowKey: 순차적 키
# → 모든 쓰기가 마지막 Region에 집중 (핫스팟)

RowKey: 20260308_000001
RowKey: 20260308_000002
RowKey: 20260308_000003
        ↓ 모든 쓰기가 한 Region에 집중!

┌──────────┐ ┌──────────┐ ┌──────────┐
Region 1 │ │ Region 2 │ │ Region 3 (한가함) (한가함) (과부하!)└──────────┘ └──────────┘ └──────────┘

해결 전략 1: Salting (접두사 해싱)

// 원래 RowKey: "20260308_user001"
// Salt 적용: hash("20260308_user001") % NUM_REGIONS + "_" + 원래키

int numRegions = 10;
String originalKey = "20260308_user001";
int salt = Math.abs(originalKey.hashCode() % numRegions);
String saltedKey = String.format("%02d_%s", salt, originalKey);
// 결과: "07_20260308_user001"

// 데이터가 Region에 균등 분배됨
// Region 0: 00_xxx, Region 1: 01_xxx, ... Region 9: 09_xxx

장점: 쓰기 부하 균등 분배 단점: 범위 스캔 시 모든 Region을 스캔해야 함

해결 전략 2: Key Reversing (키 뒤집기)

// 도메인 기반 RowKey를 뒤집어서 분산
// 원래: "com.google.www" → 뒤집기: "www.google.com"
// 원래: "com.google.mail" → 뒤집기: "liam.elgoog.moc"

// 타임스탬프 뒤집기
long reverseTimestamp = Long.MAX_VALUE - System.currentTimeMillis();
String rowKey = userId + "_" + reverseTimestamp;
// 최신 데이터가 먼저 정렬됨 (가장 흔한 쿼리 패턴)

해결 전략 3: Hashing

// MD5 해시의 앞 N자리를 prefix로 사용
String hashPrefix = DigestUtils.md5Hex(userId).substring(0, 4);
String rowKey = hashPrefix + "_" + userId + "_" + timestamp;
// 결과: "a3f2_user001_20260308120000"

RowKey 설계 원칙 요약

원칙설명
짧게 유지RowKey는 모든 Cell에 반복 저장되므로 길면 저장 낭비
읽기 패턴 고려가장 빈번한 쿼리에 맞춰 설계
핫스팟 방지순차적/단조증가 키 사용 금지
버전 역순최신 데이터를 먼저 읽으려면 reverse timestamp
복합키 구분자_ 또는 \x00 사용

용도별 RowKey 예시

# 시계열 데이터 (IoT 센서)
salt_deviceId_reverseTimestamp
: 03_sensor042_9223370449055775807

# 사용자 활동 로그
userId_reverseTimestamp_activityType
: user001_9223370449055775807_login

# 웹 페이지 크롤링
reversedDomain_path_timestamp
: moc.elgoog_/search_20260308

# 메시징 시스템
chatRoomId_reverseTimestamp_messageId
: room001_9223370449055775807_msg12345

5. HBase Shell 핵심 명령어

테이블 관리

# HBase Shell 시작
hbase shell

# 테이블 생성
create 'user_activity', \
  {NAME => 'info', VERSIONS => 3, COMPRESSION => 'SNAPPY', BLOOMFILTER => 'ROW'}, \
  {NAME => 'stats', VERSIONS => 1, COMPRESSION => 'SNAPPY', TTL => 2592000}

# 테이블 목록
list

# 테이블 상세 정보
describe 'user_activity'

# 테이블 비활성화/삭제
disable 'user_activity'
drop 'user_activity'

# 테이블 구조 변경 (비활성화 필요)
disable 'user_activity'
alter 'user_activity', {NAME => 'info', VERSIONS => 5}
alter 'user_activity', {NAME => 'logs'}  # 새 CF 추가
enable 'user_activity'

# Pre-split 테이블 생성 (핫스팟 방지)
create 'events', 'data', SPLITS => ['10', '20', '30', '40', '50', '60', '70', '80', '90']

데이터 CRUD

# Put (삽입/업데이트)
put 'user_activity', 'user001', 'info:name', '김영주'
put 'user_activity', 'user001', 'info:email', 'yj@email.com'
put 'user_activity', 'user001', 'stats:login_count', '142'

# Get (단일 행 조회)
get 'user_activity', 'user001'
get 'user_activity', 'user001', {COLUMN => 'info:name'}
get 'user_activity', 'user001', {COLUMN => 'info:name', VERSIONS => 3}
get 'user_activity', 'user001', {TIMERANGE => [1709856000000, 1709942400000]}

# Scan (범위 스캔)
scan 'user_activity'
scan 'user_activity', {LIMIT => 10}
scan 'user_activity', {STARTROW => 'user001', STOPROW => 'user010'}
scan 'user_activity', {COLUMNS => ['info:name', 'stats:login_count']}
scan 'user_activity', {FILTER => "SingleColumnValueFilter('info','name',=,'binary:김영주')"}

# Delete
delete 'user_activity', 'user001', 'info:email'  # 특정 컬럼 삭제
deleteall 'user_activity', 'user001'               # 전체 행 삭제

# Count (주의: 대량 데이터에서는 느림)
count 'user_activity'
count 'user_activity', INTERVAL => 100000

# Truncate
truncate 'user_activity'

관리 명령어

# 클러스터 상태
status
status 'detailed'
status 'simple'

# Region 관리
list_regions 'user_activity'

# 수동 Region Split
split 'user_activity', 'user500'

# Major Compaction (수동 실행, 피크 시간 피하기)
major_compact 'user_activity'

# Flush
flush 'user_activity'

# Balancer 상태
balancer_enabled
balance_switch true

6. Java API 코드 예시

연결 설정

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

// Configuration 생성
Configuration config = HBaseConfiguration.create();
config.set("hbase.zookeeper.quorum", "zk1,zk2,zk3");
config.set("hbase.zookeeper.property.clientPort", "2181");

// Connection (스레드 세이프, 애플리케이션 수명과 동일)
Connection connection = ConnectionFactory.createConnection(config);

// Table (스레드 세이프하지 않음, 사용 후 close)
Table table = connection.getTable(TableName.valueOf("user_activity"));

CRUD 작업

// Put (쓰기)
Put put = new Put(Bytes.toBytes("user001"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("김영주"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("email"), Bytes.toBytes("yj@email.com"));
put.addColumn(Bytes.toBytes("stats"), Bytes.toBytes("login_count"), Bytes.toBytes(142));
table.put(put);

// Batch Put (대량 쓰기)
List<Put> puts = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    Put p = new Put(Bytes.toBytes(String.format("user%06d", i)));
    p.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"),
                Bytes.toBytes("User " + i));
    puts.add(p);
}
table.put(puts);

// Get (읽기)
Get get = new Get(Bytes.toBytes("user001"));
get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
Result result = table.get(get);
byte[] value = result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name"));
System.out.println("Name: " + Bytes.toString(value));

// Scan (범위 스캔)
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("user001"));
scan.withStopRow(Bytes.toBytes("user100"));
scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
scan.setCaching(500);   // RPC당 반환 행 수
scan.setBatch(10);      // 한 행에서 반환할 컬럼 수

try (ResultScanner scanner = table.getScanner(scan)) {
    for (Result r : scanner) {
        String rowKey = Bytes.toString(r.getRow());
        String name = Bytes.toString(r.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")));
        System.out.println(rowKey + ": " + name);
    }
}

// Delete
Delete delete = new Delete(Bytes.toBytes("user001"));
delete.addColumn(Bytes.toBytes("info"), Bytes.toBytes("email"));
table.delete(delete);

// 자원 해제
table.close();
connection.close();

BufferedMutator (대량 비동기 쓰기)

BufferedMutatorParams params = new BufferedMutatorParams(TableName.valueOf("user_activity"))
    .writeBufferSize(5 * 1024 * 1024); // 5MB 버퍼

try (BufferedMutator mutator = connection.getBufferedMutator(params)) {
    for (int i = 0; i < 1000000; i++) {
        Put put = new Put(Bytes.toBytes(String.format("user%08d", i)));
        put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"),
                      Bytes.toBytes("User " + i));
        mutator.mutate(put);
    }
    mutator.flush(); // 버퍼에 남은 데이터 전송
}

7. 읽기/쓰기 성능 최적화

쓰기 최적화

방법설명효과
WAL 비활성화put.setDurability(Durability.SKIP_WAL)빠르지만 데이터 손실 위험
배치 Puttable.put(List<Put>)네트워크 왕복 감소
BufferedMutator비동기 버퍼링 쓰기대량 로딩에 최적
Pre-split테이블 생성 시 Region 미리 분할초기 핫스팟 방지
BulkLoadMapReduce로 HFile 직접 생성초대량 데이터 로딩
압축SNAPPY, LZ4 사용I/O 감소

읽기 최적화

방법설명효과
Block CacheLRU + Bucket Cache 설정자주 읽는 블록 캐싱
Bloom FilterROW 또는 ROWCOL 레벨불필요한 HFile 읽기 방지
Scan Cachingscan.setCaching(500)RPC 호출 감소
Column 지정필요한 컬럼만 조회I/O 감소
Coprocessor서버사이드 처리네트워크 트래픽 감소
Short-circuit Read로컬 DataNode 직접 읽기HDFS 오버헤드 제거

BulkLoad 예시

# 1. CSV를 HFile로 변환 (MapReduce)
hbase org.apache.hadoop.hbase.mapreduce.ImportTsv \
  -Dimporttsv.separator=',' \
  -Dimporttsv.columns='HBASE_ROW_KEY,info:name,info:email,stats:login_count' \
  -Dimporttsv.bulk.output='/tmp/hbase-bulkload' \
  user_activity \
  /input/users.csv

# 2. HFile을 HBase에 로드
hbase org.apache.hadoop.hbase.tool.LoadIncrementalHFiles \
  /tmp/hbase-bulkload \
  user_activity

8. Region Split과 Compaction

Region Split

# Region이 일정 크기에 도달하면 자동 분할
# 기본 분할 정책: IncreasingToUpperBoundRegionSplitPolicy

# Region 크기 계산:
# min(r^2 * memstore.flush.size * 2, hbase.hregion.max.filesize)
# r = 같은 테이블의 같은 RS에 있는 Region
# 수동 설정
hbase.hregion.max.filesize = 10737418240  # 10GB

# 분할 과정:
# 1. 분할 지점(midpoint) 결정
# 2. 두 개의 새 Region(daughter) 생성
# 3. 기존 Region 비활성화
# 4. 메타 테이블 업데이트
# 5.Region 활성화
# 6. 기존 Region 정리 (compaction 후)

Compaction

# Minor Compaction
# - 작은 HFile들을 더 큰 HFile로 병합
# - 삭제 마커(tombstone) 유지
# - 자동으로 수시 실행

# Major Compaction
# - 모든 HFile을 하나의 HFile로 병합
# - 삭제 마커 제거, 만료된 버전 제거
# - 디스크 I/O 많음 → 피크 시간 피하기
# - 기본 7일 주기 자동 실행

# 수동 Major Compaction
major_compact 'user_activity'

# Major Compaction 자동 실행 비활성화
# hbase-site.xml
# hbase.hregion.majorcompaction = 0
# → cron으로 비피크 시간에 수동 실행
<!-- hbase-site.xml Compaction 설정 -->
<configuration>
    <!-- Minor Compaction 트리거 임계값 -->
    <property>
        <name>hbase.hstore.compactionThreshold</name>
        <value>3</value>  <!-- HFile 3개 이상이면 Minor Compaction -->
    </property>

    <!-- Major Compaction 주기 (0 = 비활성화) -->
    <property>
        <name>hbase.hregion.majorcompaction</name>
        <value>604800000</value>  <!-- 7일, 0으로 설정 시 비활성화 -->
    </property>

    <!-- MemStore Flush 크기 -->
    <property>
        <name>hbase.hregion.memstore.flush.size</name>
        <value>134217728</value>  <!-- 128MB -->
    </property>
</configuration>

9. 모니터링과 관리

핵심 모니터링 메트릭

카테고리메트릭경고 기준
RegionServerGC Pause Time> 5초
RegionServerHeap Used %> 80%
RegionServerCompaction Queue Size> 10 (지속)
RegionServerMemStore SizeFlush 임계값의 90%
RegionRequest Count (R/W)특정 Region에 집중
RegionStore File Count> 10 (Compaction 필요)
HMasterDead RegionServers> 0
HMasterRIT (Regions in Transition)> 0 (지속)

HBase Web UI

# HMaster UI: http://hmaster:16010
# RegionServer UI: http://regionserver:16030

# JMX 메트릭 확인
curl http://regionserver:16030/jmx?qry=Hadoop:service=HBase,name=RegionServer,sub=Server

운영 스크립트

#!/bin/bash
# HBase 클러스터 상태 체크 스크립트

echo "=== HBase Cluster Status ==="
echo "status" | hbase shell 2>/dev/null | grep -E "servers|dead|regions"

echo ""
echo "=== Region Distribution ==="
echo "status 'detailed'" | hbase shell 2>/dev/null | grep -E "regionserver|regions="

echo ""
echo "=== Table Sizes ==="
for table in $(echo "list" | hbase shell 2>/dev/null | grep -v "TABLE\|row(s)"); do
    size=$(hdfs dfs -du -s -h /hbase/data/default/$table 2>/dev/null | awk '{print $1 $2}')
    echo "$table: $size"
done

10. HBase vs Cassandra vs MongoDB

항목HBaseCassandraMongoDB
데이터 모델Column-FamilyWide ColumnDocument (JSON)
일관성강한 일관성튜닝 가능 (AP 기본)튜닝 가능
확장성수평 확장수평 확장 (뛰어남)수평 확장 (Sharding)
쿼리 언어Scan/Get APICQL (SQL-like)MQL (JSON-like)
2차 인덱스제한적기본 지원풍부한 인덱스
JOIN미지원미지원$lookup (제한적)
운영 복잡도높음 (HDFS, ZK 필요)중간낮음
적합한 워크로드대규모 순차 스캔 + 랜덤 읽기높은 쓰기 처리량유연한 스키마, CRUD
에코시스템Hadoop (Hive, Spark)독립적Atlas, Realm
최대 데이터 규모PB급PB급TB~PB급

11. 트러블슈팅

Region Server 장애

# RegionServer 상태 확인
echo "status" | hbase shell

# 특정 RegionServer 로그 확인
tail -f /var/log/hbase/hbase-hbase-regionserver-hostname.log

# Region 재할당
hbase hbck -reassign <encoded-region-name>

# hbck2 (HBase 2.x)
hbase hbck -j /path/to/hbase-hbck2.jar assigns <encoded-region-name>

GC 튜닝

# hbase-env.sh
export HBASE_REGIONSERVER_OPTS="
  -Xmx32g -Xms32g
  -XX:+UseG1GC
  -XX:MaxGCPauseMillis=100
  -XX:+ParallelRefProcEnabled
  -XX:G1HeapRegionSize=16m
  -XX:InitiatingHeapOccupancyPercent=65
  -verbose:gc
  -XX:+PrintGCDetails
  -XX:+PrintGCDateStamps
  -Xloggc:/var/log/hbase/gc-regionserver.log
"

핫스팟 Region 분리

# 특정 Region의 요청 수 확인
echo "status 'detailed'" | hbase shell | grep -A2 "requestsPerSecond"

# 핫스팟 Region 수동 분할
split 'ENCODED_REGION_NAME', 'split_key'

# 또는 테이블 레벨 분할
split 'user_activity', 'user500000'

RIT (Regions in Transition) 해결

# RIT 상태 확인
echo "status 'detailed'" | hbase shell | grep "transition"

# 강제 할당
hbase hbck -j /path/to/hbase-hbck2.jar assigns <region-encoded-name>

# 메타 테이블 복구
hbase hbck -j /path/to/hbase-hbck2.jar fixMeta

12. 운영 체크리스트

초기 설계 체크리스트

  • RowKey 설계: 핫스팟 방지 전략 적용 (Salting, Hashing)
  • Column Family 수 최소화 (2~3개 이하 권장)
  • TTL 설정: 불필요한 데이터 자동 삭제
  • VERSIONS 설정: 필요한 버전 수만 유지
  • Pre-split: 예상 데이터 분포에 맞게 Region 분할
  • Bloom Filter: ROW 또는 ROWCOL 설정
  • 압축: SNAPPY 또는 LZ4 설정
  • Coprocessor 필요 여부 검토

일상 운영 체크리스트

  • RegionServer 힙 사용률 모니터링
  • Compaction Queue 크기 확인
  • GC Pause 시간 확인 (5초 이상 경고)
  • Dead RegionServer 확인
  • Region 핫스팟 확인
  • HDFS 용량 확인
  • ZooKeeper 상태 확인

정기 점검 체크리스트

  • Major Compaction 비피크 시간에 실행
  • hbase hbck 실행하여 테이블 무결성 확인
  • Region 밸런싱 상태 확인
  • 테이블별 디스크 사용량 추이 분석
  • GC 로그 분석 및 튜닝
  • 백업/복구 테스트 (Snapshot, ExportSnapshot)
  • HBase, Hadoop 보안 패치 확인

마무리

HBase는 대규모 데이터를 낮은 지연 시간으로 처리해야 하는 시나리오에서 강력한 도구입니다. 하지만 RDBMS와는 완전히 다른 사고방식이 필요합니다.

핵심 정리:

  1. RowKey가 전부: 핫스팟 방지와 읽기 패턴에 맞는 설계가 핵심
  2. Column Family는 적게: 물리적 저장 단위이므로 2~3개 이하로 유지
  3. Compaction 관리: Major Compaction은 비피크 시간에 수동 실행
  4. 모니터링 필수: GC Pause, Region 분포, Compaction Queue 집중 감시
  5. 읽기/쓰기 패턴 분리: 대량 쓰기는 BulkLoad, 실시간 읽기는 Block Cache + Bloom Filter 활용

HBase Practical Guide: From Large-Scale NoSQL Data Store Design to Operations

Introduction

HBase is a distributed NoSQL database built on the Google Bigtable paper. It runs on top of HDFS and is optimized for large-scale random reads/writes that can handle billions of rows and millions of columns. It is widely used in scenarios requiring low-latency access to large volumes of data, such as time-series data, log analysis, and real-time serving layers.

This article systematically covers everything from HBase core concepts to real-world operational know-how.

1. What is HBase?

Key Characteristics

CharacteristicDescription
DistributedHorizontally scalable on top of HDFS
Column-Family BasedData stored in column family units
VersionedEach cell can store multiple versions (timestamps)
Strong ConsistencyGuarantees atomic reads/writes for a single row
Auto-ShardingData automatically distributed in Region units
High ThroughputCapable of handling hundreds of thousands of QPS

When to Use HBase?

Suitable cases:

  • Massive datasets with billions or more rows
  • Fast random reads/writes needed (< 10ms)
  • Time-series data (IoT sensors, logs, metrics)
  • Wide tables (hundreds to thousands of columns)
  • Integration with HDFS is required

Unsuitable cases:

  • Small datasets with only millions of rows or fewer
  • Complex JOINs or transactions required
  • Full-text search (Elasticsearch is more suitable)
  • Ad-hoc analytical queries (Hive, Spark SQL are more suitable)

2. HBase Architecture

Overall Structure

┌────────────────────────────────────────────────────────┐
Client   (HBase Shell, Java API, REST, Thrift)└───────────────────────┬────────────────────────────────┘
                ┌───────▼───────┐
ZooKeeper                  (coordination│
& discovery)                └───┬───────┬───┘
                    │       │
            ┌───────▼──┐  ┌─▼──────────┐
HMaster  │  │  HMaster             (Active) (Standby)            └───────┬───┘  └────────────┘
     ┌──────────────┼──────────────┐
     │              │              │
┌────▼─────┐  ┌────▼─────┐  ┌────▼─────┐
│RegionSvr │  │RegionSvr │  │RegionSvr │
│┌────────┐│  │┌────────┐│  │┌────────┐│
││Region A││  ││Region C││  ││Region E││
│├────────┤│  │├────────┤│  │├────────┤│
││Region B││  ││Region D││  ││Region F││
│└────────┘│  │└────────┘│  │└────────┘│
└──────────┘  └──────────┘  └──────────┘
       │              │              │
       └──────────────┼──────────────┘
               ┌──────▼──────┐
HDFS                (Storage)               └─────────────┘

Roles of Each Component

ComponentRoleImpact on Failure
HMasterRegion assignment, DDL processing, load balancingDDL unavailable, auto-balancing stops (reads/writes still work)
RegionServerData read/write processing, Region managementAccess to affected Regions unavailable (auto-recovery)
ZooKeeperHMaster election, RS status tracking, meta locationEntire cluster goes down
HDFSPersistent data storageRisk of data loss

Region Internal Structure

┌─────────────── Region ──────────────────┐
│                                          │
│  ┌─── Column Family: cf1 ─────────────┐ │
│  │  ┌──────────┐                      │ │
│  │  │ MemStore   (memory, write buf) │ │
│  │  └──────────┘                      │ │
│  │  ┌──────────┐  ┌──────────┐       │ │
│  │  │ HFile 1  │  │ HFile 2  │       │ │
│  │  └──────────┘  └──────────┘       │ │
│  └────────────────────────────────────┘ │
│                                          │
│  ┌─── Column Family: cf2 ─────────────┐ │
│  │  ┌──────────┐                      │ │
│  │  │ MemStore │                      │ │
│  │  └──────────┘                      │ │
│  │  ┌──────────┐                      │ │
│  │  │ HFile 1  │                      │ │
│  │  └──────────┘                      │ │
│  └────────────────────────────────────┘ │
│                                          │
│  ┌──────────────────────────────────┐   │
│  │         WAL (Write-Ahead Log)    │   │
│  └──────────────────────────────────┘   │
└──────────────────────────────────────────┘

Write Path

1. ClientPut request to RegionServer
2. Write to WAL (Write-Ahead Log) (stored on HDFS, for failure recovery)
3. Write to MemStore (memory)
4. Return success response to Client
5. When MemStore is full → Flush to HFile (stored on HDFS)

Read Path

1. ClientGet request to RegionServer
2. Check Block Cache (memory)
3. Check MemStore (memory)
4. Check HFile (disk, quickly filtered via Bloom Filter)
5. Merge results (return latest version)

3. Data Model

Logical Data Model

Table: user_activity
─────────────────────────────────────────────────────────────────
RowKey          | Column Family: info      | Column Family: stats
                | name     | email        | login_count | last_login
─────────────────────────────────────────────────────────────────
user001         | Kim YJ   | yj@email.com | 142         | 2026-03-08
user002         | Park SJ  | sj@email.com | 87          | 2026-03-07
user003         | Lee HN   | hn@email.com | 256         | 2026-03-08
─────────────────────────────────────────────────────────────────

Physical Storage Structure

# Data is actually stored separately per Column Family
# Key-Value format: (RowKey, CF:Qualifier, Timestamp)Value

# Column Family: info
(user001, info:name, t3)"Kim YJ"
(user001, info:name, t1)"Kim YJ (old)"     # Previous version
(user001, info:email, t2)"yj@email.com"
(user002, info:name, t4)"Park SJ"
(user002, info:email, t4)"sj@email.com"

# Column Family: stats (separate HFile)
(user001, stats:login_count, t5)142
(user001, stats:last_login, t5)"2026-03-08"

Key Terminology

TermDescriptionRDBMS Analogy
TableData containerTable
RowRow identified by RowKeyRow
Column FamilyColumn group (physical storage unit)(none)
Column QualifierColumn name within a CFColumn
CellValue at (Row, CF:Qualifier, Timestamp)Cell
TimestampCell version (default: milliseconds)(none)
RegionHorizontal partition unit of a tablePartition

4. RowKey Design Strategies

RowKey design determines 80% of HBase performance.

Hotspot Problem

# Bad RowKey: Sequential keys
# → All writes concentrate on the last Region (hotspot)

RowKey: 20260308_000001
RowKey: 20260308_000002
RowKey: 20260308_000003
All writes concentrate on one Region!

┌──────────┐ ┌──────────┐ ┌──────────┐
Region 1 │ │ Region 2 │ │ Region 3 (idle) (idle)(overload!)└──────────┘ └──────────┘ └──────────┘

Solution 1: Salting (Prefix Hashing)

// Original RowKey: "20260308_user001"
// With Salt: hash("20260308_user001") % NUM_REGIONS + "_" + originalKey

int numRegions = 10;
String originalKey = "20260308_user001";
int salt = Math.abs(originalKey.hashCode() % numRegions);
String saltedKey = String.format("%02d_%s", salt, originalKey);
// Result: "07_20260308_user001"

// Data is evenly distributed across Regions
// Region 0: 00_xxx, Region 1: 01_xxx, ... Region 9: 09_xxx

Pros: Even write load distribution Cons: Must scan all Regions for range scans

Solution 2: Key Reversing

// Reverse domain-based RowKeys for distribution
// Original: "com.google.www" → Reversed: "www.google.com"
// Original: "com.google.mail" → Reversed: "liam.elgoog.moc"

// Timestamp reversing
long reverseTimestamp = Long.MAX_VALUE - System.currentTimeMillis();
String rowKey = userId + "_" + reverseTimestamp;
// Latest data is sorted first (most common query pattern)

Solution 3: Hashing

// Use first N characters of MD5 hash as prefix
String hashPrefix = DigestUtils.md5Hex(userId).substring(0, 4);
String rowKey = hashPrefix + "_" + userId + "_" + timestamp;
// Result: "a3f2_user001_20260308120000"

RowKey Design Principles Summary

PrincipleDescription
Keep it shortRowKey is repeatedly stored in every Cell, so length wastes storage
Consider read patternsDesign for the most frequent queries
Prevent hotspotsAvoid sequential/monotonically increasing keys
Reverse versioningUse reverse timestamp to read latest data first
Composite key delimiterUse _ or \x00

RowKey Examples by Use Case

# Time-series data (IoT sensors)
salt_deviceId_reverseTimestamp
Example: 03_sensor042_9223370449055775807

# User activity logs
userId_reverseTimestamp_activityType
Example: user001_9223370449055775807_login

# Web page crawling
reversedDomain_path_timestamp
Example: moc.elgoog_/search_20260308

# Messaging system
chatRoomId_reverseTimestamp_messageId
Example: room001_9223370449055775807_msg12345

5. Essential HBase Shell Commands

Table Management

# Start HBase Shell
hbase shell

# Create table
create 'user_activity', \
  {NAME => 'info', VERSIONS => 3, COMPRESSION => 'SNAPPY', BLOOMFILTER => 'ROW'}, \
  {NAME => 'stats', VERSIONS => 1, COMPRESSION => 'SNAPPY', TTL => 2592000}

# List tables
list

# Table details
describe 'user_activity'

# Disable/drop table
disable 'user_activity'
drop 'user_activity'

# Alter table structure (must be disabled)
disable 'user_activity'
alter 'user_activity', {NAME => 'info', VERSIONS => 5}
alter 'user_activity', {NAME => 'logs'}  # Add new CF
enable 'user_activity'

# Create pre-split table (prevent hotspots)
create 'events', 'data', SPLITS => ['10', '20', '30', '40', '50', '60', '70', '80', '90']

Data CRUD

# Put (insert/update)
put 'user_activity', 'user001', 'info:name', 'Kim YJ'
put 'user_activity', 'user001', 'info:email', 'yj@email.com'
put 'user_activity', 'user001', 'stats:login_count', '142'

# Get (single row query)
get 'user_activity', 'user001'
get 'user_activity', 'user001', {COLUMN => 'info:name'}
get 'user_activity', 'user001', {COLUMN => 'info:name', VERSIONS => 3}
get 'user_activity', 'user001', {TIMERANGE => [1709856000000, 1709942400000]}

# Scan (range scan)
scan 'user_activity'
scan 'user_activity', {LIMIT => 10}
scan 'user_activity', {STARTROW => 'user001', STOPROW => 'user010'}
scan 'user_activity', {COLUMNS => ['info:name', 'stats:login_count']}
scan 'user_activity', {FILTER => "SingleColumnValueFilter('info','name',=,'binary:Kim YJ')"}

# Delete
delete 'user_activity', 'user001', 'info:email'  # Delete specific column
deleteall 'user_activity', 'user001'               # Delete entire row

# Count (caution: slow on large datasets)
count 'user_activity'
count 'user_activity', INTERVAL => 100000

# Truncate
truncate 'user_activity'

Administrative Commands

# Cluster status
status
status 'detailed'
status 'simple'

# Region management
list_regions 'user_activity'

# Manual Region split
split 'user_activity', 'user500'

# Major Compaction (run manually, avoid peak hours)
major_compact 'user_activity'

# Flush
flush 'user_activity'

# Balancer status
balancer_enabled
balance_switch true

6. Java API Code Examples

Connection Setup

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

// Create Configuration
Configuration config = HBaseConfiguration.create();
config.set("hbase.zookeeper.quorum", "zk1,zk2,zk3");
config.set("hbase.zookeeper.property.clientPort", "2181");

// Connection (thread-safe, same lifetime as application)
Connection connection = ConnectionFactory.createConnection(config);

// Table (NOT thread-safe, close after use)
Table table = connection.getTable(TableName.valueOf("user_activity"));

CRUD Operations

// Put (write)
Put put = new Put(Bytes.toBytes("user001"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("Kim YJ"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("email"), Bytes.toBytes("yj@email.com"));
put.addColumn(Bytes.toBytes("stats"), Bytes.toBytes("login_count"), Bytes.toBytes(142));
table.put(put);

// Batch Put (bulk writes)
List<Put> puts = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    Put p = new Put(Bytes.toBytes(String.format("user%06d", i)));
    p.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"),
                Bytes.toBytes("User " + i));
    puts.add(p);
}
table.put(puts);

// Get (read)
Get get = new Get(Bytes.toBytes("user001"));
get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
Result result = table.get(get);
byte[] value = result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name"));
System.out.println("Name: " + Bytes.toString(value));

// Scan (range scan)
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("user001"));
scan.withStopRow(Bytes.toBytes("user100"));
scan.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
scan.setCaching(500);   // Rows returned per RPC
scan.setBatch(10);      // Columns returned per row

try (ResultScanner scanner = table.getScanner(scan)) {
    for (Result r : scanner) {
        String rowKey = Bytes.toString(r.getRow());
        String name = Bytes.toString(r.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")));
        System.out.println(rowKey + ": " + name);
    }
}

// Delete
Delete delete = new Delete(Bytes.toBytes("user001"));
delete.addColumn(Bytes.toBytes("info"), Bytes.toBytes("email"));
table.delete(delete);

// Release resources
table.close();
connection.close();

BufferedMutator (Bulk Async Writes)

BufferedMutatorParams params = new BufferedMutatorParams(TableName.valueOf("user_activity"))
    .writeBufferSize(5 * 1024 * 1024); // 5MB buffer

try (BufferedMutator mutator = connection.getBufferedMutator(params)) {
    for (int i = 0; i < 1000000; i++) {
        Put put = new Put(Bytes.toBytes(String.format("user%08d", i)));
        put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"),
                      Bytes.toBytes("User " + i));
        mutator.mutate(put);
    }
    mutator.flush(); // Send remaining data in buffer
}

7. Read/Write Performance Optimization

Write Optimization

MethodDescriptionEffect
Disable WALput.setDurability(Durability.SKIP_WAL)Faster but risk of data loss
Batch Puttable.put(List<Put>)Reduced network round trips
BufferedMutatorAsync buffered writesOptimal for bulk loading
Pre-splitSplit Regions in advance at table creationPrevent initial hotspots
BulkLoadGenerate HFiles directly via MapReduceFor massive data loading
CompressionUse SNAPPY, LZ4Reduced I/O

Read Optimization

MethodDescriptionEffect
Block CacheLRU + Bucket Cache configurationCache frequently read blocks
Bloom FilterROW or ROWCOL levelPrevent unnecessary HFile reads
Scan Cachingscan.setCaching(500)Reduced RPC calls
Column SpecificationQuery only needed columnsReduced I/O
CoprocessorServer-side processingReduced network traffic
Short-circuit ReadDirect local DataNode readsEliminate HDFS overhead

BulkLoad Example

# 1. Convert CSV to HFile (MapReduce)
hbase org.apache.hadoop.hbase.mapreduce.ImportTsv \
  -Dimporttsv.separator=',' \
  -Dimporttsv.columns='HBASE_ROW_KEY,info:name,info:email,stats:login_count' \
  -Dimporttsv.bulk.output='/tmp/hbase-bulkload' \
  user_activity \
  /input/users.csv

# 2. Load HFiles into HBase
hbase org.apache.hadoop.hbase.tool.LoadIncrementalHFiles \
  /tmp/hbase-bulkload \
  user_activity

8. Region Split and Compaction

Region Split

# Regions automatically split when reaching a certain size
# Default split policy: IncreasingToUpperBoundRegionSplitPolicy

# Region size calculation:
# min(r^2 * memstore.flush.size * 2, hbase.hregion.max.filesize)
# r = number of Regions of the same table on the same RS

# Manual configuration
hbase.hregion.max.filesize = 10737418240  # 10GB

# Split process:
# 1. Determine split point (midpoint)
# 2. Create two new Regions (daughters)
# 3. Deactivate the old Region
# 4. Update meta table
# 5. Activate new Regions
# 6. Clean up old Region (after compaction)

Compaction

# Minor Compaction
# - Merges small HFiles into larger HFiles
# - Retains delete markers (tombstones)
# - Runs automatically at regular intervals

# Major Compaction
# - Merges all HFiles into a single HFile
# - Removes delete markers, expired versions
# - Heavy disk I/O → avoid peak hours
# - Default auto-run every 7 days

# Manual Major Compaction
major_compact 'user_activity'

# Disable automatic Major Compaction
# hbase-site.xml
# hbase.hregion.majorcompaction = 0
# → Run manually during off-peak hours via cron
<!-- hbase-site.xml Compaction configuration -->
<configuration>
    <!-- Minor Compaction trigger threshold -->
    <property>
        <name>hbase.hstore.compactionThreshold</name>
        <value>3</value>  <!-- Minor Compaction when 3+ HFiles -->
    </property>

    <!-- Major Compaction interval (0 = disabled) -->
    <property>
        <name>hbase.hregion.majorcompaction</name>
        <value>604800000</value>  <!-- 7 days, set to 0 to disable -->
    </property>

    <!-- MemStore flush size -->
    <property>
        <name>hbase.hregion.memstore.flush.size</name>
        <value>134217728</value>  <!-- 128MB -->
    </property>
</configuration>

9. Monitoring and Management

Key Monitoring Metrics

CategoryMetricAlert Threshold
RegionServerGC Pause Time> 5 seconds
RegionServerHeap Used %> 80%
RegionServerCompaction Queue Size> 10 (sustained)
RegionServerMemStore Size90% of flush threshold
RegionRequest Count (R/W)Concentrated on specific Region
RegionStore File Count> 10 (Compaction needed)
HMasterDead RegionServers> 0
HMasterRIT (Regions in Transition)> 0 (sustained)

HBase Web UI

# HMaster UI: http://hmaster:16010
# RegionServer UI: http://regionserver:16030

# Check JMX metrics
curl http://regionserver:16030/jmx?qry=Hadoop:service=HBase,name=RegionServer,sub=Server

Operations Script

#!/bin/bash
# HBase cluster status check script

echo "=== HBase Cluster Status ==="
echo "status" | hbase shell 2>/dev/null | grep -E "servers|dead|regions"

echo ""
echo "=== Region Distribution ==="
echo "status 'detailed'" | hbase shell 2>/dev/null | grep -E "regionserver|regions="

echo ""
echo "=== Table Sizes ==="
for table in $(echo "list" | hbase shell 2>/dev/null | grep -v "TABLE\|row(s)"); do
    size=$(hdfs dfs -du -s -h /hbase/data/default/$table 2>/dev/null | awk '{print $1 $2}')
    echo "$table: $size"
done

10. HBase vs Cassandra vs MongoDB

ItemHBaseCassandraMongoDB
Data ModelColumn-FamilyWide ColumnDocument (JSON)
ConsistencyStrong consistencyTunable (AP by default)Tunable
ScalabilityHorizontal scalingHorizontal scaling (excellent)Horizontal scaling (Sharding)
Query LanguageScan/Get APICQL (SQL-like)MQL (JSON-like)
Secondary IndexLimitedBuilt-in supportRich indexing
JOINNot supportedNot supported$lookup (limited)
Operational ComplexityHigh (requires HDFS, ZK)MediumLow
Suitable WorkloadsLarge-scale sequential scans + random readsHigh write throughputFlexible schema, CRUD
EcosystemHadoop (Hive, Spark)IndependentAtlas, Realm
Max Data ScalePB-scalePB-scaleTB~PB-scale

11. Troubleshooting

RegionServer Failure

# Check RegionServer status
echo "status" | hbase shell

# Check specific RegionServer logs
tail -f /var/log/hbase/hbase-hbase-regionserver-hostname.log

# Reassign Region
hbase hbck -reassign <encoded-region-name>

# hbck2 (HBase 2.x)
hbase hbck -j /path/to/hbase-hbck2.jar assigns <encoded-region-name>

GC Tuning

# hbase-env.sh
export HBASE_REGIONSERVER_OPTS="
  -Xmx32g -Xms32g
  -XX:+UseG1GC
  -XX:MaxGCPauseMillis=100
  -XX:+ParallelRefProcEnabled
  -XX:G1HeapRegionSize=16m
  -XX:InitiatingHeapOccupancyPercent=65
  -verbose:gc
  -XX:+PrintGCDetails
  -XX:+PrintGCDateStamps
  -Xloggc:/var/log/hbase/gc-regionserver.log
"

Hotspot Region Isolation

# Check request count per Region
echo "status 'detailed'" | hbase shell | grep -A2 "requestsPerSecond"

# Manual split of hotspot Region
split 'ENCODED_REGION_NAME', 'split_key'

# Or table-level split
split 'user_activity', 'user500000'

RIT (Regions in Transition) Resolution

# Check RIT status
echo "status 'detailed'" | hbase shell | grep "transition"

# Force assignment
hbase hbck -j /path/to/hbase-hbck2.jar assigns <region-encoded-name>

# Repair meta table
hbase hbck -j /path/to/hbase-hbck2.jar fixMeta

12. Operations Checklists

Initial Design Checklist

  • RowKey design: Apply hotspot prevention strategies (Salting, Hashing)
  • Minimize Column Family count (2~3 or fewer recommended)
  • TTL configuration: Automatically delete unnecessary data
  • VERSIONS configuration: Keep only needed version count
  • Pre-split: Split Regions according to expected data distribution
  • Bloom Filter: Configure ROW or ROWCOL
  • Compression: Configure SNAPPY or LZ4
  • Evaluate need for Coprocessors

Daily Operations Checklist

  • Monitor RegionServer heap usage
  • Check Compaction Queue size
  • Check GC Pause time (alert if > 5 seconds)
  • Check for dead RegionServers
  • Check for Region hotspots
  • Check HDFS capacity
  • Check ZooKeeper status

Regular Inspection Checklist

  • Run Major Compaction during off-peak hours
  • Run hbase hbck to verify table integrity
  • Check Region balancing status
  • Analyze disk usage trends per table
  • Analyze and tune GC logs
  • Test backup/recovery (Snapshot, ExportSnapshot)
  • Check HBase, Hadoop security patches

Conclusion

HBase is a powerful tool for scenarios requiring low-latency processing of massive datasets. However, it requires a completely different mindset from RDBMS.

Key Takeaways:

  1. RowKey is everything: Hotspot prevention and design aligned with read patterns are the key
  2. Keep Column Families minimal: As physical storage units, keep to 2~3 or fewer
  3. Compaction management: Run Major Compaction manually during off-peak hours
  4. Monitoring is essential: Focus on GC Pause, Region distribution, and Compaction Queue
  5. Separate read/write patterns: Use BulkLoad for bulk writes, Block Cache + Bloom Filter for real-time reads

Quiz

Q1: What is the main topic covered in "HBase Practical Guide: From Large-Scale NoSQL Data Store Design to Operations"?

A comprehensive guide covering everything you need for real-world HBase operations, from understanding the data model and architecture to table design, RowKey strategies, read/write performance optimization, Region management, and monitoring.

Q2: What is HBase?? Key Characteristics When to Use HBase? Suitable cases: Massive datasets with billions or more rows Fast random reads/writes needed (< 10ms) Time-series data (IoT sensors, logs, metrics) Wide tables (hundreds to thousands of columns) Integration with HDFS is required Unsuitable ca...

Q3: Describe the HBase Architecture. Overall Structure Roles of Each Component Region Internal Structure Write Path Read Path

Q4: What are the key aspects of Data Model? Logical Data Model Physical Storage Structure Key Terminology

Q5: Describe the RowKey Design Strategies. RowKey design determines 80% of HBase performance. Hotspot Problem Solution 1: Salting (Prefix Hashing) Pros: Even write load distribution Cons: Must scan all Regions for range scans Solution 2: Key Reversing Solution 3: Hashing RowKey Design Principles Summary RowKey Examples by...