RegionServer
RegionServer
HRegionServer는 RegionServer의 구현체로, region을 서빙하고 관리하는 책임을 져요. 분산 클러스터에서 RegionServer는 DataNode에서 실행돼요. 인터페이스, 프로세스, 블록 캐시, region 분할 구현, WAL(Write Ahead Log)을 살펴볼게요.
출처: 문서
본문
HRegionServer는 RegionServer 구현이에요. region을 서빙하고 관리하는 책임을 져요. 분산 클러스터에서 RegionServer는 DataNode에서 실행돼요.
인터페이스 (Interface)
HRegionRegionInterface가 노출하는 메서드는 데이터 지향적 메서드와 region 유지 메서드를 모두 포함해요.
- Data (get, put, delete, next, 등)
- Region (splitRegion, compactRegion, 등) 예를 들어 Admin 메서드 majorCompact가 테이블에 대해 호출되면, 클라이언트는 실제로 지정된 테이블의 모든 region을 순회하며 각 region에 직접 major compaction을 요청해요.
프로세스 (Processes)
RegionServer는 다양한 백그라운드 스레드를 실행해요.
CompactSplitThread
분할을 확인하고 minor 압축을 처리해요.
MajorCompactionChecker
major 압축을 확인해요.
MemStoreFlusher
MemStore의 인메모리 쓰기를 주기적으로 StoreFile로 플러시해요.
LogRoller
RegionServer의 WAL을 주기적으로 확인해요.
Coprocessors
Coprocessor는 0.92에서 추가됐어요. CoProcessors에 대한 철저한 블로그 개요가 게시되어 있어요. 문서는 결국 이 참조 가이드로 옮겨질 것이지만, 현재 시점에서 가장 최신 정보는 블로그예요.
블록 캐시 (Block Cache)
HBase는 HDFS에서 읽은 데이터를 캐시하기 위한 두 가지 다른 BlockCache 구현을 제공해요: 기본 on-heap LruBlockCache와 (보통) off-heap인 BucketCache. 이 섹션은 각 구현의 이점과 단점, 적절한 옵션을 선택하는 방법, 각각의 구성 옵션을 논의해요.
블록 캐시 보고: UI
캐시 배포에 대한 자세한 내용은 RegionServer UI를 참고해 주세요. 구성, 크기 조정, 현재 사용량, 캐시 내 시간, 블록 수와 유형에 대한 세부 정보까지 확인할 수 있어요.
캐시 선택 (Cache Choices)
LruBlockCache는 원래 구현이며 전적으로 Java 힙 안에 있어요. BucketCache는 선택 사항이며 주로 블록 캐시 데이터를 off-heap에 두기 위한 것이지만, BucketCache는 파일 백업 캐시일 수도 있어요. 파일 백업에서는 file 모드 또는 mmaped 모드로 사용할 수 있어요. bucket cache가 영속 메모리 장치에 있는 pmem 모드도 있어요.
BucketCache를 활성화하면 2계층(two tier) 캐싱 시스템을 활성화하는 것이에요. 이전에는 그 계층을 "L1"과 "L2"라고 설명했지만, 이 용어는 hbase-2.0.0부터 폐기되었어요. "L1" 캐시는 LruBlockCache 인스턴스를, "L2"는 off-heap BucketCache를 가리켰어요. 대신 BucketCache가 활성화되면 모든 DATA 블록은 BucketCache 계층에 보관되고, meta 블록(INDEX 및 BLOOM 블록)은 LruBlockCache의 on-heap에 보관돼요. 이 두 계층의 관리와 블록이 계층 사이를 이동하는 방식을 결정하는 정책은 CombinedBlockCache가 수행해요.
일반 캐시 구성 (General Cache Configurations)
캐시 구현 자체 외에도 캐시 성능을 제어하기 위한 몇 가지 일반 구성 옵션을 설정할 수 있어요. CacheConfig를 참고해 주세요. 이러한 옵션 중 하나를 설정한 후 구성이 적용되도록 클러스터를 재시작하거나 rolling restart해 주세요. 오류나 예상치 못한 동작에 대해 로그를 확인해 주세요.
또한 HBASE-9857에서 도입된 새 옵션을 논의하는 Blockcache의 Prefetch Option도 참고해 주세요.
LruBlockCache 설계
LruBlockCache는 스캔 저항성(scan-resistance)과 in-memory ColumnFamily를 허용하기 위해 블록 우선순위의 세 가지 수준을 포함하는 LRU 캐시예요.
-
단일 접근 우선순위 (Single access priority): 블록이 HDFS에서 처음 로드될 때 보통 이 우선순위를 가지며, 퇴거(eviction) 중에 먼저 고려되는 첫 번째 그룹의 일부가 돼요. 이점은 스캔된 블록이 더 많이 사용되는 블록보다 퇴거될 가능성이 더 높다는 것이에요.
-
다중 접근 우선순위 (Multi access priority): 이전 우선순위 그룹의 블록이 다시 접근되면 이 우선순위로 승격돼요. 따라서 퇴거 중에 고려되는 두 번째 그룹의 일부가 돼요.
-
인메모리 접근 우선순위 (In-memory access priority): 블록의 패밀리가 "in-memory"로 구성되면, 접근 횟수와 관계없이 이 우선순위의 일부가 돼요. 카탈로그 테이블이 이렇게 구성돼요. 이 그룹은 퇴거 중에 마지막으로 고려돼요.
컬럼 패밀리를 in-memory로 표시하려면 java로 테이블을 만들 때 HColumnDescriptor.setInMemory(true); 를 호출하거나, shell에서 테이블을 만들거나 변경할 때 IN_MEMORY ⇒ true를 설정하세요. 예: hbase(main):003:0> create 't', {NAME => 'f', IN_MEMORY => 'true'} 자세한 내용은 LruBlockCache 소스를 참고해 주세요.
LruBlockCache 사용량 (LruBlockCache Usage)
블록 캐싱은 모든 사용자 테이블에 대해 기본적으로 활성화되어 있어, 어떤 읽기 연산이든 LRU 캐시를 로드한다는 뜻이에요. 이는 많은 사용 사례에 좋을 수 있지만, 더 나은 성능을 위해 추가 튜닝이 보통 필요해요. 중요한 개념은 작업 집합 크기(WSS, working set size)로, "문제의 답을 계산하는 데 필요한 메모리 양"이에요. 웹사이트의 경우 짧은 시간 안에 질의에 답하는 데 필요한 데이터가 될 거예요.
캐싱에 HBase에서 사용 가능한 메모리 양을 계산하는 방법은 다음과 같아요.
number of region servers * heap size * hfile.block.cache.size * 0.99
블록 캐시의 기본값은 0.4로, 사용 가능한 힙의 40%를 나타내요. 마지막 값(99%)은 퇴거가 시작된 후 LRU 캐시의 기본 허용 로딩 요소예요. 이 방정식에 포함된 이유는 새 블록을 로드하는 시점부터 프로세스를 차단하게 만들 것이므로 사용 가능한 메모리의 100%를 사용하는 것이 가능하다고 말하는 것은 비현실적이기 때문이에요. 여기 몇 가지 예시가 있어요.
- 힙 크기 1GB와 기본 블록 캐시 크기의 region server 하나는 405MB의 블록 캐시를 사용할 수 있어요.
- 힙 크기 8GB와 기본 블록 캐시 크기의 region server 20개는 63.3GB의 블록 캐시를 가져요.
- 힙 크기 24GB와 블록 캐시 크기 0.5의 region server 100개는 약 1.16TB의 블록 캐시를 가져요.
데이터만 블록 캐시에 상주하는 것은 아니에요. 다음도 고려해야 할 수 있어요.
- 카탈로그 테이블 (Catalog Tables) hbase:meta 테이블은 블록 캐시에 강제되며 인메모리 우선순위를 가지므로 퇴거가 더 어려워요. hbase:meta 테이블은 region 수에 따라 몇 MB를 차지할 수 있어요.
- HFiles 인덱스 HFile은 HBase가 HDFS에 데이터를 저장하는 데 사용하는 파일 형식이에요. 이는 전체 파일을 읽지 않고도 HBase가 데이터를 검색할 수 있게 해주는 다층 인덱스를 포함해요. 이 인덱스의 크기는 블록 크기(기본 64KB), 키 크기, 저장하는 데이터 양의 요인이에요. 큰 데이터 세트의 경우 region server당 1GB 근처의 숫자를 보는 것이 드물지 않지만, LRU가 사용되지 않는 인덱스를 퇴거시키므로 모두 캐시에 있지는 않아요.
- 키 (Keys) 저장된 값은 그림의 절반만 보여줘요. 각 값이 키(row key, family qualifier, timestamp)와 함께 저장되기 때문이에요. Try to minimize row and column sizes를 참고해 주세요.
- 블룸 필터 (Bloom Filters) HFile 인덱스처럼 이 데이터 구조들도 (활성화되면) LRU에 저장돼요.
현재 HFile 인덱스와 블룸 필터 크기를 측정하는 권장 방법은 region server 웹 UI를 보고 관련 지표를 확인하는 것이에요. 키의 경우 HFile 커맨드라인 도구를 사용해 샘플링하고 평균 키 크기 지표를 찾을 수 있어요. HBase 0.98.3부터 UI의 특수 블록 캐시 섹션에서 BlockCache 통계와 지표에 대한 세부 정보를 볼 수 있어요. HBase 2.4.14부터 JMX의 blockCacheCount와 blockCacheDataBlockCount를 사용해 HFile 인덱스와 블룸 필터를 다른 DATA 블록과 대략 추정할 수 있어요. (blockCacheCount - blockCacheDataBlockCount) * blockSize 공식은 BucketCache를 활성화하려 할 때 유용한 추정치를 제공할 거예요. post-BucketCache 구성이 BucketCache 이전의 비-DATA 블록 수와 적어도 같은 수를 담을 수 있을 만큼 on-heap LRU 캐시에 충분한 메모리를 주는지 확인해야 해요. BucketCache가 활성화되면 l1CacheSize, l1CacheCount, l1CacheEvictionCount 같은 L1 지표가 크기를 추가로 튜닝하는 데 도움을 줄 수 있어요.
WSS가 메모리에 맞지 않을 때 블록 캐싱을 사용하는 것은 일반적으로 나쁘다는 점을 알아두세요. 예를 들어 모든 region server의 블록 캐시에 40GB가 있지만 1TB의 데이터를 처리해야 하는 경우예요. 그 이유 중 하나는 퇴거로 인한 churn(교반)이 불필요하게 더 많은 가비지 컬렉션을 트리거하기 때문이에요. 두 가지 사용 사례가 있어요.
- 완전 임의 읽기 패턴 (Fully random reading pattern): 짧은 시간 안에 같은 행을 두 번 거의 접근하지 않아 캐시된 블록을 칠 확률이 0에 가까운 경우예요. 그러한 테이블에 블록 캐싱을 설정하는 것은 메모리와 CPU 주기의 낭비이며, 더 심하게는 JVM이 수집해야 할 더 많은 쓰레기를 생성할 거예요. GC 모니터링에 대한 자세한 내용은 JVM Garbage Collection Logs를 참고해 주세요.
- 테이블 매핑 (Mapping a table): 테이블을 입력으로 받는 전형적인 MapReduce 작업에서 모든 행이 한 번만 읽히므로 블록 캐시에 넣을 필요가 없어요. Scan 객체에는 setCacheBlocks 메서드(false로 설정)로 이를 끄는 옵션이 있어요. 빠른 임의 읽기 접근이 필요하다면 이 테이블에서 블록 캐싱을 계속 켜 둘 수도 있어요. 실시간 트래픽을 서빙하는 테이블의 행 수를 세는 예시에서 그 테이블의 모든 블록을 캐시하면 엄청난 churn이 생기고 확실히 현재 사용 중인 데이터를 퇴거시킬 거예요.
META 블록만 캐싱(DATA 블록은 fscache에)
흥미로운 설정 중 하나는 META 블록만 캐시하고 접근할 때마다 DATA 블록을 읽는 거예요. DATA 블록이 fscache 안에 들어맞는다면, 매우 큰 데이터셋에 접근이 완전히 임의적일 때 이 대안이 의미가 있을 수 있어요. 이 설정을 활성화하려면 테이블을 변경하고 각 컬럼 패밀리에 대해 BLOCKCACHE ⇒ 'false'를 설정해 주세요. 이 컬럼 패밀리에 대해서만 BlockCache를 '비활성화'하는 것이에요. META 블록의 캐싱은 절대 비활성화할 수 없어요. HBASE-4683 Always cache index and bloom blocks 이후로, BlockCache가 비활성화되어도 META 블록을 캐시할 거예요.
Off-heap Block Cache
BucketCache 활성화 방법 (How to Enable BucketCache)
BucketCache의 일반적인 배포는 LruBlockCache로 구현된 on-heap 캐시와 BucketCache로 구현된 두 번째 캐시라는 두 캐싱 계층을 설정하는 관리 클래스를 통해 이루어져요. 관리 클래스는 기본적으로 CombinedBlockCache예요. 이전 링크는 CombinedBlockCache가 구현하는 캐싱 '정책'을 설명해요. 간단히 말해, meta 블록(INDEX와 BLOOM)은 on-heap LruBlockCache 계층에, DATA 블록은 BucketCache 계층에 유지하는 방식으로 작동해요.
- Pre-hbase-2.0.0 버전 pre-hbase-2.0.0에서 BucketCache에서 가져올 때는 네이티브 on-heap LruBlockCache에 비해 항상 더 느릴 거예요. 하지만 GC보다 BlockCache 할당을 관리하므로 BucketCache를 사용할 때 GC가 적어 시간에 따른 지연 시간 변동이 덜하기 마련이에요. BucketCache가 off-heap 모드로 배포되면 이 메모리는 GC가 전혀 관리하지 않아요. 이것이 pre-2.0.0에서 BucketCache를 사용하는 이유예요. 지연 시간의 변동을 줄이고, GC와 힙 단편화를 완화하며, 더 많은 메모리를 안전하게 사용하기 위해서예요. on-heap vs off-heap 테스트 비교는 Nick Dimiduk의 BlockCache 101을 참고해 주세요. 또한 Comparing BlockCache Deploys를 참고하면 데이터셋이 LruBlockCache 배포 안에 들어맞으면 그것을 사용하고, 캐시 churn이 있거나(또는 캐시가 java GC의 변덕을 넘어 존재하길 원하거나) BucketCache를 사용하는 것이 좋다고 나와 있어요. pre-2.0.0에서 LruBlockCache 퇴거의 희생자(victim)를 받도록 BucketCache를 구성할 수 있어요. 모든 Data 및 index 블록이 먼저 L1에 캐시돼요. L1에서 퇴거가 발생하면 블록(또는 victims)이 L2로 이동해요. cacheDataInL1을 HColumnDescriptor.setCacheDataInL1(true)로 또는 shell에서 CACHE_DATA_IN_L1을 true로 설정해 컬럼 패밀리를 만들거나 수정해 설정하세요. 예: hbase(main):003:0> create 't', {NAME => 't', CONFIGURATION => {CACHE_DATA_IN_L1 => 'true'}}
- hbase-2.0.0+ 버전 HBASE-11425는 HBase 읽기 경로를 변경해 읽기 데이터를 off-heap에 보관하고 캐시된 데이터를 java 힙으로 복사하는 것을 피할 수 있게 했어요. Offheap read-path를 참고해 주세요. hbase-2.0.0에서 off-heap 지연 시간은 on-heap 캐시 지연 시간에 접근하며 GC를 유발하지 않는 추가 이점이 있어요. HBase 2.0.0부터 L1과 L2의 개념은 폐기됐어요. BucketCache가 켜지면 DATA 블록은 항상 BucketCache로 가고 INDEX/BLOOM 블록은 on-heap LRUBlockCache로 가요. cacheDataInL1 지원은 제거됐어요.
BucketCache 배포 모드 (BucketCache Deploy Modes)
BucketCache Block Cache는 offheap, file 또는 mmaped file 모드로 배포할 수 있어요.
hbase.bucketcache.ioengine 설정으로 어떤 것을 사용할지 정해요. offheap으로 설정하면 BucketCache가 off-heap에 할당하고, ioengine을 file:PATH_TO_FILE로 설정하면 BucketCache가 파일 캐싱을 사용하도록 지시해요(특히 SSD 같은 고속 I/O가 박스에 붙어 있으면 유용). 2.0.0부터 하나 이상의 파일로 BucketCache를 백업할 수 있어요. 이는 특히 캐시 크기 요구사항이 높을 때 매우 유용해요. 여러 백업 파일의 경우 ioengine을 files:PATH_TO_FILE1,PATH_TO_FILE2,PATH_TO_FILE3로 구성하세요. BucketCache는 mmapped 파일을 사용하도록 구성할 수도 있어요. 이를 위해 ioengine을 mmap:PATH_TO_FILE로 구성하세요.
CombinedBlockCache 정책을 우회하고 BucketCache를 L1 LruBlockCache의 엄격한 L2 캐시로 작동시키는 계층형 설정을 배포할 수 있어요. 이러한 설정을 위해 hbase.bucketcache.combinedcache.enabled를 false로 설정하세요. 이 모드에서 L1에서 퇴거되면 블록이 L2로 이동해요. 블록이 캐시되면 먼저 L1에 캐시돼요. 캐시된 블록을 찾으러 갈 때 먼저 L1을 보고, 없으면 L2를 검색해요. 이 배포 형식을 Raw L1+L2라고 부르자요. 참고: 이 L1+L2 모드는 2.0.0에서 제거됐어요. BucketCache를 사용하면 엄격히 DATA 캐시가 되고 LruBlockCache는 INDEX/META 블록을 캐시할 거예요.
다른 BucketCache 구성에는 재시작 간에 캐시를 유지할 위치 지정, 캐시 쓰기에 사용할 스레드 수 등이 포함돼요. 구성 옵션과 설명은 CacheConfig.html 클래스를 참고해 주세요.
활성화되었는지 확인하려면 캐시 설정을 설명하는 로그 줄을 찾아보세요. BucketCache가 어떻게 배포되었는지 자세히 설명될 거예요. UI도 참고해 주세요. 캐시 계층화와 그 구성을 자세히 보여줄 거예요.
BucketCache 예시 구성 (BucketCache Example Configuration)
이 샘플은 1GB on-heap 캐시로 4GB off-heap BucketCache를 위한 구성을 제공해요.
구성은 RegionServer에서 수행돼요.
hbase.bucketcache.ioengine과 hbase.bucketcache.size > 0을 설정하면 CombinedBlockCache가 활성화돼요. RegionServer가 5G 힙으로 실행되도록 설정되었다고 가정해 봐요: 즉 HBASE_HEAPSIZE=5g.
- 먼저 RegionServer의 hbase-env.sh를 편집하고 HBASE_OFFHEAPSIZE를 원하는 off-heap 크기(이 경우 4GB, 4G로 표현)보다 큰 값으로 설정해 주세요. 5G로 설정해 봐요. 그건 off-heap 캐시용 4G와 다른 off-heap 메모리 용도용 1G가 될 거예요(BlockCache 외에 off-heap 메모리를 사용하는 다른 사용자가 있음; 예: RegionServer의 DFSClient가 off-heap 메모리를 사용할 수 있음). 아래 HBase의 Direct Memory 사용량을 참고해 주세요.
HBASE_OFFHEAPSIZE=5G
- 다음으로 RegionServer의 hbase-site.xml에 다음 구성을 추가해 주세요.
<property>
<name>hbase.bucketcache.ioengine</name>
<value>offheap</value>
</property>
<property>
<name>hfile.block.cache.size</name>
<value>0.2</value>
</property>
<property>
<name>hbase.bucketcache.size</name>
<value>4196</value>
</property>
- 클러스터를 재시작하거나 rolling restart하고, 로그에서 문제가 없는지 확인해 주세요.
위에서 BucketCache를 4G로 설정했어요. on-heap LruBlockCache가 RegionServer 힙 크기의 20%(0.2)를 가지도록 구성했어요 (0.2 * 5G = 1G). 즉, L1 LruBlockCache를 (L2 캐시가 없는 것처럼) 평소처럼 구성하는 것이에요.
HBASE-10641은 HBase 0.98 이상에서 BucketCache의 버킷에 대해 여러 크기를 구성하는 능력을 도입했어요. 여러 버킷 크기를 구성하려면 새 속성 hbase.bucketcache.bucket.sizes를 공백 없이 작은 것에서 큰 것 순서로 정렬된 쉼표로 구분된 블록 크기 목록으로 구성하세요. 목표는 데이터 접근 패턴에 따라 버킷 크기를 최적화하는 거예요. 다음 예시는 크기 4096과 8192의 버킷을 구성해요.
<property>
<name>hbase.bucketcache.bucket.sizes</name>
<value>4096,8192</value>
</property>
HBase의 Direct Memory 사용량 (Direct Memory Usage In HBase)
기본 최대 direct memory는 JVM에 따라 달라요. 전통적으로 64M이거나 할당된 힙 크기(-Xmx)와 어떤 관련이 있거나 아예 제한이 없어요(JDK7은 그런 것 같음). HBase 서버는 direct memory를 사용하며, 특히 short-circuit reading(Leveraging local data 참고)에서 호스팅된 DFSClient가 direct memory 버퍼를 할당할 거예요. DFSClient가 얼마나 사용하는지 정량화하기는 쉽지 않아요. 그것은 열린 HFiles 수 * hbase.dfs.client.read.shortcircuit.buffer.size이며, 여기서 hbase.dfs.client.read.shortcircuit.buffer.size는 HBase에서 128k로 설정돼요 — hbase-default.xml 기본 구성을 참고해 주세요. off-heap 블록 캐싱을 하면 direct memory를 사용하게 될 거예요. RPCServer는 ByteBuffer 풀을 사용해요. 2.0.0부터 이 버퍼들은 off-heap ByteBuffers예요. JVM을 시작할 때 conf/hbase-env.sh의 -XX:MaxDirectMemorySize 설정이 off-heap BlockCache(hbase.bucketcache.size), DFSClient 사용량, RPC 측 ByteBufferPool 최대 크기를 고려하는지 확인해 주세요. 이는 off-heap BlockCache 크기와 최대 ByteBufferPool 크기의 합보다 약간 커야 해요. 최대 direct memory 크기에 1-2GB를 추가로 할당하는 것이 테스트에서 잘 작동했어요. Java 프로세스 힙의 일부인 direct memory는 -Xmx가 할당한 객체 힙과 분리돼요. MaxDirectMemorySize가 할당한 값은 물리적 RAM을 초과하면 안 되며, 다른 메모리 요구사항과 시스템 제약 때문에 총 사용 가능한 RAM보다 작을 가능성이 높아요.
RegionServer가 on-heap과 off-heap/direct로 얼마나 많은 메모리를 사용하도록 구성되었는지, 그리고 어느 한 시점에 얼마나 사용하는지 UI의 Server Metrics: Memory 탭을 보면 알 수 있어요. JMX로도 얻을 수 있어요. 특히 서버가 현재 사용하는 direct memory는 java.nio.type=BufferPool,name=direct bean에서 찾을 수 있어요. Terracotta는 Java에서 off-heap 메모리를 사용하는 것에 대한 좋은 글을 가지고 있어요. 그것은 그들의 제품 BigMemory를 위한 것이지만, 지적된 많은 문제가 일반적으로 off-heap으로 가는 어떤 시도에도 적용돼요. 확인해 보세요.
hbase.bucketcache.percentage.in.combinedcache
이것은 혼란스러워서 제거된 pre-HBase 1.0 구성이에요. 0.0과 1.0 사이의 값으로 설정하는 부동소수점이었어요. 기본값은 0.9였어요. 배포가 CombinedBlockCache를 사용한다면 LruBlockCache L1 크기는 (1 - hbase.bucketcache.percentage.in.combinedcache) * size-of-bucketcache로 계산되고 BucketCache 크기는 hbase.bucketcache.percentage.in.combinedcache * size-of-bucket-cache였어요. 여기서 size-of-bucket-cache 자체는 hbase.bucketcache.size가 Megabytes로 지정되었는지(그렇다면 그 값) 또는 hbase.bucketcache.size가 0과 1.0 사이인지(-XX:MaxDirectMemorySize * hbase.bucketcache.size)에 따라 EITHER였어요.
1.0에서는 더 직관적이어야 해요. Onheap LruBlockCache 크기는 hfile.block.cache.size 설정(최상의 이름은 아님)으로 java 힙의 일부로 설정되고, BucketCache는 위처럼 절대 Megabytes로 설정돼요.
BucketCache에 대한 시간 기반 우선순위 (Time Based Priority for BucketCache)
HBASE-28463은 BucketCache의 블록에 대한 시간 기반 우선순위를 도입했어요. 이는 개별 컬럼 패밀리 구성에 연령(age) 임계값을 정의할 수 있게 하며, 이 구성된 임계값보다 오래된 블록이 퇴거 대상으로 먼저 지정돼요.
연령 임계값을 정의하지 않은 컬럼 패밀리의 블록은 시간 기반 우선순위로 평가되지 않으며, LRU 퇴거 로직만 따라 퇴거될 거예요.
이 기능은 가장 최근 데이터가 더 자주 접근되므로 캐시에서 더 높은 우선순위를 받아야 하는 사용 사례에 대부분 유용해요. 가장 많이 접근되는 데이터의 "age"로 시간 기반 우선순위를 구성하면 내장 LRU 퇴거 로직보다 BucketCache의 블록 할당을 더 세밀하게 제어할 수 있어요.
BucketCache에 대한 시간 기반 우선순위는 데이터 연령을 정의하는 세 가지 다른 전략을 제공해요.
- Cell 타임스탬프 (Cell timestamps): 데이터 연령을 비교하기 위해 HBase cell의 타임스탬프 부분을 사용해요.
- 커스텀 cell qualifier (Custom cell qualifiers): 데이터 연령을 비교하기 위해 사용자 정의 날짜 qualifier를 사용해요. 주어진 qualifier 값을 가진 전체 행을 계층화하기 위해 그 값을 사용해요. 이는 커스텀 qualifier가 유효한 Java long 타임스탬프여야 함을 요구해요.
- 커스텀 값 제공자 (Custom value provider): 비교에 사용할 날짜 값을 식별하는 로직을 포함한 플러그 가능한 구현을 정의할 수 있게 해요. 이는 또한 주어진 행의 여러 부분에 다른 형식으로 저장되거나 다른 데이터에 포함된 날짜를 가질 수 있는 다양한 사용 사례에 추가 유연성을 제공해요.
HBase에서의 레코드 수집 순서(가장 최근이 가장 관련 있음)로 우선순위가 결정되는 사용 사례에서 내장 cell 타임스탬프가 연령 기반 우선순위를 구성하는 가장 편리하고 효율적인 방법을 제공해요. See Using Cell timestamps for Time Based Priority.
일부 애플리케이션은 커스텀 날짜 컬럼을 사용해 테이블 레코드의 우선순위를 정의할 수 있어요. 그러한 경우 커스텀 cell qualifier 기반 우선순위가 바람직해요. See Using Custom Cell Qualifiers for Time Based Priority.
마지막으로 더 정교한 스키마는 각 레코드의 연령을 정의하는 도메인 특정 로직을 통합할 수 있어요. 커스텀 값 제공자는 우선순위 비교에 사용해야 할 날짜 값을 적절히 파싱하는 구현을 통합하는 커스텀 코드를 용이하게 해요. See Using a Custom value provider for Time Based Priority.
BucketCache에 대한 시간 기반 우선순위로, 블록 연령은 블록을 캐시할지 결정할 때(즉 읽기, 쓰기, 압축, prefetch 중), 그리고 LRU 로직을 실행하기 전에 캐시 freeSpace 실행(대량 퇴거) 중에 평가돼요.
블록은 유형 외에 특정 메타 정보를 보유하지 않으므로, 특수화된 압축 구현(아래 구성 섹션에서 자세히)을 사용해 같은 "연령 그룹"의 블록을 별도 파일로 그룹화하는 것이 필요해요. 각 파일의 모든 블록 시간 범위는 파일 메타 정보 섹션에 이어 붙으며, 시간 기반 우선순위 로직에서 고려해야 할 블록의 연령을 평가하는 데 사용돼요.
BucketCache에 대한 시간 기반 우선순위 구성 (Configuring Time Based Priority for BucketCache)
각 블록의 연령을 찾는 것은 추가 오버헤드를 수반하므로, 이 기능은 전역 구성 수준에서 기본적으로 비활성화돼요.
이를 활성화하려면 RegionServer의 hbase-site.xml에 다음 구성을 설정해야 해요.
<property>
<name>hbase.regionserver.datatiering.enable</name>
<value>true</value>
</property>
전역으로 활성화되면 개별 컬럼 패밀리 수준에서 원하는 전략별 설정을 정의하는 것이 필요해요.
Cell 타임스탬프를 사용한 시간 기반 우선순위 (Using Cell timestamps for Time Based Priority)
이 전략은 실행에 가장 효율적인데, 데이터를 포함한 각 cell의 타임스탬프 부분을 사용해 블록 연령을 비교하기 때문이에요. 블록을 연령에 따라 별도 파일로 분할하기 위해 DateTieredCompaction이 필요해요.
아래 예시는 'orders' 테이블의 컬럼 패밀리 'cf1'에 대해 hot 연령 임계값을 1주일(밀리초)로 설정해요.
hbase(main):003:0> alter 'orders', {NAME => 'cf1',
CONFIGURATION => {'hbase.hstore.datatiering.type' => 'TIME_RANGE',
'hbase.hstore.datatiering.hot.age.millis' => '604800000',
'hbase.hstore.engine.class' => 'org.apache.hadoop.hbase.regionserver.DateTieredStoreEngine',
'hbase.hstore.blockingStoreFiles' => '60',
'hbase.hstore.compaction.min' => '2',
'hbase.hstore.compaction.max' => '60'
}
}
Date Tiered Compaction 특정 튜닝 (Date Tiered Compaction specific tunings)
위 예시에서 날짜 계층 압축의 창 수와 각 창의 기간을 관장하는 속성은 설정되지 않았어요. 기본 설정으로 압축은 처음에 6시간의 창 4개, 그다음 각 1일의 창 4개, 그다음 각 4일의 창 4개를 만들며 선택된 파일 사이의 최소 타임스탬프가 덮일 때까지 계속돼요. 이는 많은 수의 파일을 만들 수 있으므로 'hbase.hstore.blockingStoreFiles', 'hbase.hstore.compaction.min', 'hbase.hstore.compaction.max'에 대한 추가 변경이 권장돼요.
또는 초기 창 크기를 hot 연령 임계값과 같게 조정하고 계층당 창 두 개만 사용하는 것을 고려하세요.
hbase(main):003:0> alter 'orders', {NAME => 'cf1',
CONFIGURATION => {'hbase.hstore.datatiering.type' => 'TIME_RANGE',
'hbase.hstore.datatiering.hot.age.millis' => '604800000',
'hbase.hstore.engine.class' => 'org.apache.hadoop.hbase.regionserver.DateTieredStoreEngine',
'hbase.hstore.compaction.date.tiered.base.window.millis' => '604800000',
'hbase.hstore.compaction.date.tiered.windows.per.tier' => '2'
}
}
커스텀 Cell Qualifier를 사용한 시간 기반 우선순위 (Using Custom Cell Qualifiers for Time Based Priority)
이 전략은 시간 기반 우선순위를 위해 설계된 새 압축 구현을 사용해요. 날짜 계층 압축을 확장하지만, 다양한 시간 창의 여러 계층을 생성하는 대신 단순히 파일을 두 그룹으로 분할해요: "cold" 그룹(모든 블록이 정의된 임계값 연령보다 오래됨)과 "hot" 그룹(모든 블록이 임계값 연령보다 새 것)이에요.
아래 예시는 커스텀 cell qualifier 전략에서 블록 연령을 비교하는 데 사용할 cell qualifier 'event_date'를 정의해요.
hbase(main):003:0> alter 'orders', {NAME => 'cf1',
CONFIGURATION => {'hbase.hstore.datatiering.type' => 'CUSTOM',
'TIERING_CELL_QUALIFIER' => 'event_date',
'hbase.hstore.datatiering.hot.age.millis' => '604800000',
'hbase.hstore.engine.class' => 'org.apache.hadoop.hbase.regionserver.CustomTieredStoreEngine',
'hbase.hstore.compaction.date.tiered.custom.age.limit.millis' => '604800000'
}
}
시간 기반 우선순위 x 압축 연령 임계값 구성 (Time Based Priority x Compaction Age Threshold Configurations)
hot 연령 임계값을 정의하는 두 가지 다른 구성이 있다는 점에 주의해 주세요. 이것은 시간 기반 우선순위 집행자가 압축 구현과 독립적으로 작동하기 때문이에요.
시간 기반 우선순위를 위한 커스텀 값 제공자 사용 (Using a Custom value provider for Time Based Priority)
블록 우선순위를 비교하는 데 사용할 각 행의 데이터 연령을 정의하기 위해 도메인 특정 로직을 연결하는 것도 가능해요. 커스텀 시간 기반 우선순위 프레임워크는 CustomTieredCompactor.TieringValueProvider 인터페이스를 정의하며, 이는 압축이 임계값 연령에 따라 블록을 그룹화하는 데 사용할 특정 날짜 값을 제공하도록 구현할 수 있어요.
다음 예시에서 RowKeyPortionTieringValueProvider는 getTieringValue 메서드를 구현해요. 이 메서드는 "yyyyMMddHHmmss" 형식을 사용해 행 키 값의 한 세그먼트(구체적으로 14번째와 29번째 위치 사이)에서 날짜를 파싱해요. 파싱된 날짜는 long 타임스탬프로 반환되며, 커스텀 계층 압축이 정의된 hot 연령 임계값에 따라 블록을 그룹화하는 데 사용돼요.
public class RowKeyPortionTieringValueProvider implements CustomTieredCompactor.TieringValueProvider {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
@Override
public void init(Configuration configuration) throws Exception {}
@Override
public long getTieringValue(Cell cell) {
byte[] rowArray = new byte[cell.getRowLength()];
System.arraycopy(cell.getRowArray(), cell.getRowOffset(), rowArray, 0, cell.getRowLength());
String datePortion = Bytes.toString(rowArray).substring(14, 29).trim();
try {
return sdf.parse(datePortion).getTime();
} catch (ParseException e) {
//handle error
}
return Long.MAX_VALUE;
}
}
위 Tiering Value Provider는 시간 기반 우선순위를 위해 다음과 같이 구성할 수 있어요.
hbase(main):003:0> alter 'orders', {NAME => 'cf1',
CONFIGURATION => {'hbase.hstore.datatiering.type' => 'CUSTOM',
'hbase.hstore.custom-tiering-value.provider.class' =>
'org.apache.hbase.client.example.RowKeyPortionTieringValueProvider',
'hbase.hstore.datatiering.hot.age.millis' => '604800000',
'hbase.hstore.engine.class' => 'org.apache.hadoop.hbase.regionserver.CustomTieredStoreEngine',
'hbase.hstore.compaction.date.tiered.custom.age.limit.millis' => '604800000'
}
}
컬럼 패밀리 구성에서 커스텀 시간 기반 우선순위(커스텀 qualifier 또는 커스텀 값 제공자)를 활성화하면, 새로 구성된 우선순위가 버킷 캐시 내에 효과적으로 적용되도록 지정된 테이블에서 major compaction을 두 번 실행하는 것이 필수적이에요.
시간 기반 우선순위는 원래 cell 타임스탬프 전략만으로 구현됐어요. cell 타임스탬프 기반 전략을 다루는 원래 설계는 여기에서 볼 수 있어요.
위에서 언급한 두 커스텀 전략을 포함한 두 번째 단계는 이 별도 설계 문서에 자세히 설명돼 있어요.
압축된 BlockCache (Compressed BlockCache)
HBASE-11331은 지연(lazy) BlockCache 압축 해제를 도입했으며, 더 간단히는 압축된 BlockCache라고 불러요. 압축된 BlockCache가 활성화되면 데이터와 인코딩된 데이터 블록이 캐시되기 전에 압축 해제되고 복호화되는 대신, 온디스크 형식 그대로 BlockCache에 캐시돼요.
캐시에 들어갈 수 있는 것보다 더 많은 데이터를 호스팅하는 RegionServer의 경우 SNAPPY 압축으로 이 기능을 활성화하면 처리량이 50% 증가하고 평균 지연 시간이 30% 개선되는 반면, 가비지 컬렉션이 80% 증가하고 전체 CPU 부하가 2% 증가한다는 것이 보여졌어요. 성능이 어떻게 측정되고 달성되었는지에 대한 자세한 내용은 HBASE-11331을 참고해 주세요. 캐시에 편안하게 들어맞는 데이터를 호스팅하는 RegionServer의 경우, 또는 워크로드가 추가 CPU나 가비지 컬렉션 부하에 민감한 경우 혜택이 적을 수 있어요.
압축된 BlockCache는 기본적으로 비활성화돼요. 활성화하려면 모든 RegionServer의 hbase-site.xml에서 hbase.block.data.cachecompressed를 true로 설정해 주세요.
캐시 인식 로드 밸런서 (Cache Aware Load Balancer)
데이터 크기와 구성된 캐시 크기에 따라 캐시 워밍업은 몇 분에서 몇 시간까지 걸릴 수 있어요. 컴퓨팅이 저장소와 분리되는 클라우드 저장소 위의 HBase 배포에서는 이것이 훨씬 더 중요해져요. 이렇게 하는 것을 region server가 시작될 때마다 하는 것은 매우 비용이 큰 과정일 수 있어요. 이를 없애기 위해 HBASE-27313은 region server가 버킷 캐시에 캐시된 블록을 주기적으로 영속화하는 캐시 영속성 기능을 구현했어요. 이 영속화된 정보는 정상 재시작이나 크래시로 인한 region server 재시작 시 캐시를 부활시키는 데 사용돼요.
HBASE-27999는 캐시 인식 로드 밸런서를 구현하며, 새 할당 계획을 계산할 때 region server의 각 region에 대한 캐시 할당을 고려하는 능력을 로드 밸런서에 추가해요. region server가 보고한 region/region server 캐시 할당 정보를 사용해 호스팅 서버에서 각 region에 대해 캐시된 HFiles의 백분율을 계산해요. 이 정보는 최적의 새 할당 계획을 결정할 때 밸런서가 요인으로 사용해요.
master 노드는 모든 region server에서 캐싱 정보를 수집하고, 현재 캐시 할당에 최소한의 영향을 보장하면서 새 region 할당을 결정하는 데 이 정보를 사용해요. region은 현재 호스팅된 region server에 비해 더 좋은 캐시 비율을 가진 region server에 할당돼요.
CacheAwareLoadBalancer는 region 할당을 결정하기 위해 두 가지 비용 요소를 사용해요. 아래에 설명돼요.
- 캐시 비용 (Cache Cost) 캐시 비용은 현재 호스팅되거나 이전에 호스팅된 region server에 region의 데이터가 캐시된 백분율로 계산돼요. region은 각각 다른 크기의 여러 HFile을 가질 수 있어요. 이 파일의 모든 데이터 블록이 캐시에 있으면 HFile이 완전히 prefetch된 것으로 간주돼요. 이 region을 호스팅하는 region server는 캐시에 완전히 캐시된 HFiles 수와 region의 총 HFiles 수의 비율을 계산해요. 이 비율은 0(이 서버에 region이 호스팅되지만 HFile이 캐시에 없음)에서 1(이 서버에 region이 호스팅되고 이 region의 모든 HFiles가 캐시에 있음)까지 다양해요. 모든 region server는 현재 호스팅된 모든 region에 대해 이 정보를 유지해요. 추가로 이 캐시 비율은 이전에 이 region server에 호스팅된 region에 대해서도 유지되어 region에 대한 과거 정보를 제공해요.
- 스큐니스 비용 (Skewness Cost)
캐시 인식 밸런서는 다음 조건에서 region 할당 계획을 결정하기 위해 스큐니스 비용과 함께 캐시 비용을 고려할 거예요.
- 클러스터에 유휴 서버가 있을 때. 이는 기존 서버가 재시작되거나 새 서버가 클러스터에 추가될 때 발생할 수 있어요.
- 클러스터에서 균형을 유지하는 비용이 hbase.master.balancer.stochastic.minCostNeedBalance 구성이 정의한 최소 임계값보다 클 때.
CacheAwareLoadBalancer는 master 구성에서 다음 구성 속성을 설정해 클러스터에서 활성화할 수 있어요.
<property>
<name>hbase.master.loadbalancer.class</name>
<value>org.apache.hadoop.hbase.master.balancer.CacheAwareLoadBalancer</value>
</property>
<property>
<name>hbase.bucketcache.persistent.path</name>
<value>/path/to/bucketcache_persistent_file</value>
</property>
HBASE-29168에서 CacheAwareLoadBalancer는 region 이동 스로틀링을 구현해요. 이는 주로 region 스큐니스로 인해 밸런싱할 때 "캐시 요인"을 잃는 영향, 즉 새 region server가 클러스터에 추가될 때 캐시된 region의 많은 부분이 한 번에 새 서버로 이동해 캐시에 민감한 사용 사례에 눈에 띄는 읽기 성능 영향을 일으킬 수 있는 것을 완화해요. 스로틀링 수면 시간은 hbase.master.balancer.move.throttlingMillis 속성으로 결정되며 기본 60000 밀리초예요. 이동 계획된 region이 대상 서버에서 hbase.master.balancer.stochastic.throttling.cacheRatio 속성(기본 80%)으로 구성 가능한 임계값보다 높은 캐시 비율을 가지면, 그 region 이동에는 스로틀링이 적용되지 않아요.
RegionServer 분할 구현 (RegionServer Splitting Implementation)
쓰기 요청이 region server에 의해 처리되면 memstore라는 인메모리 저장 시스템에 축적돼요. memstore가 차면 그 내용이 추가 store 파일로 디스크에 기록돼요. 이 이벤트를 memstore flush라고 해요. store 파일이 축적되면 RegionServer는 그것들을 더 적고 더 큰 파일로 압축할 거예요. 각 플러시 또는 압축이 끝나면 region에 저장된 데이터 양이 변경됐어요. RegionServer는 region 분할 정책에 문의해 region이 너무 커졌는지 또는 다른 정책 특정 이유로 분할해야 하는지 판단해요. 정책이 권장하면 region 분할 요청이 큐에 들어가요.
논리적으로 region을 분할하는 과정은 단순해요. region을 반으로 나눠야 하는 키스페이스에서 적절한 지점을 찾고, 그 지점에서 region의 데이터를 두 개의 새 region으로 나눠요. 하지만 과정의 세부 사항은 단순하지 않아요. 분할이 발생하면 새로 생성된 딸 region이 모든 데이터를 즉시 새 파일로 다시 쓰지 않아요. 대신 분할 지점에 따라 부모 store 파일의 위쪽 또는 아래쪽 부분을 가리키는, Reference 파일이라고 불리는 심볼릭 링크 파일과 유사한 작은 파일을 만들어요. 참조 파일은 일반 데이터 파일처럼 사용되지만, 레코드의 절반만 고려돼요. region은 부모 region의 불변 데이터 파일에 대한 참조가 더 이상 없을 때만 분할될 수 있어요. 그 참조 파일들은 압축에 의해 점진적으로 정리되어, region이 부모 파일을 참조하는 것을 멈추고 더 분할될 수 있어요.
region 분할은 RegionServer가 내리는 로컬 결정이지만, 분할 과정 자체는 많은 행위자와 조정해야 해요. RegionServer는 분할 전후에 Master에 알리고, 클라이언트가 새 딸 region을 발견할 수 있도록 .META. 테이블을 갱신하며, HDFS에서 디렉터리 구조와 데이터 파일을 재배치해요. 분할은 다중 태스크 과정이에요. 오류 시 롤백을 가능하게 하기 위해 RegionServer는 실행 상태에 대한 인메모리 저널을 유지해요. RegionServer가 분할을 실행하기 위해 취하는 단계는 아래 "RegionServer Split Process" 스키마에 설명되어 있어요. 각 단계는 단계 번호로 레이블이 붙어 있어요. RegionServer 또는 Master의 작업은 빨간색으로, 클라이언트의 작업은 초록색으로 표시돼요.
- RegionServer가 지역적으로 region을 분할하기로 결정하고 분할을 준비해요. 분할 트랜잭션이 시작됐어요. 첫 번째 단계로 RegionServer는 분할 과정 중 스키마 수정을 막기 위해 테이블에 공유 읽기 잠금을 획득해요. 그런 다음 zookeeper의 /hbase/region-in-transition/region-name 아래에 znode를 만들고, znode의 상태를 SPLITTING으로 설정해요.
- Master가 이 znode에 대해 알게 돼요. 부모 region-in-transition znode에 대한 watcher를 가지기 때문이에요.
- RegionServer가 HDFS의 부모 region 디렉터리 아래에 .splits라는 하위 디렉터리를 만들어요.
- RegionServer가 부모 region을 닫고 로컬 데이터 구조에서 region을 오프라인으로 표시해요. 이제 분할 중인 region이 오프라인이에요. 이 시점에 부모 region으로 오는 클라이언트 요청은 NotServingRegionException을 던질 거예요. 클라이언트는 약간의 백오프로 재시도할 거예요. 닫히는 region은 플러시돼요.
- RegionServer가 .splits 디렉터리 아래에 딸 region A와 B 용 region 디렉터리를 만들고 필요한 데이터 구조를 만들어요. 그런 다음 store 파일을 분할하는데, 부모 region의 store 파일당 두 개의 Reference 파일을 만드는 의미로요. 그 참조 파일들은 부모 region의 파일을 가리킬 거예요.
- RegionServer가 HDFS에 실제 region 디렉터리를 만들고 각 딸에 대한 참조 파일을 이동시켜요.
- RegionServer가 .META. 테이블에 Put 요청을 보내 부모를 .META. 테이블에서 오프라인으로 표시하고 딸 region에 대한 정보를 추가해요. 이 시점에 .META.에는 딸에 대한 개별 항목이 없을 거예요. 클라이언트는 .META.를 스캔하면 부모 region이 분할되었음을 보게 되지만, .META.에 나타날 때까지 딸에 대해 알지 못할 거예요. 또한 이 .META. Put이 성공하면 부모가 사실상 분할될 거예요. 이 RPC가 성공하기 전에 RegionServer가 실패하면, Master와 그 region을 여는 다음 Region Server가 region 분할에 대한 더러운 상태를 정리할 거예요. 하지만 .META. 갱신 후에는 region 분할이 Master에 의해 롤포워드될 거예요.
- RegionServer가 딸 A와 B를 병렬로 열어요.
- RegionServer가 딸 A와 B를 .META.에, 그것들이 region을 호스팅한다는 정보와 함께 추가해요. 분할된 region(부모에 대한 참조를 가진 딸)이 이제 온라인이에요. 이 지점 이후 클라이언트는 새 region을 발견하고 요청을 발행할 수 있어요. 클라이언트는 .META. 항목을 로컬로 캐시하지만, RegionServer나 .META.에 요청을 하면 캐시가 무효화되고 .META.에서 새 region에 대해 알게 될 거예요.
- RegionServer가 ZooKeeper의 /hbase/region-in-transition/region-name znode를 SPLIT 상태로 갱신해 master가 알 수 있게 해요. 필요하면 밸런서가 딸 region을 다른 region server에 자유롭게 다시 할당할 수 있어요. 이제 분할 트랜잭션이 끝났어요.
- 분할 후 .META.와 HDFS에는 여전히 부모 region에 대한 참조가 포함될 거예요. 그 참조들은 딸 region의 압축이 데이터 파일을 다시 쓸 때 제거될 거예요. master의 가비지 컬렉션 태스크가 딸 region이 여전히 부모 region의 파일을 참조하는지 주기적으로 확인해요. 그렇지 않으면 부모 region이 제거될 거예요.
분할 디렉터리 동작에 대한 참고 (HBASE-26187)
새 분할 구현에서의 동작 변경
번호가 매겨진 분할 절차는 원래의 pre-HBASE-26187 구현을 문서화하며, 여기서 딸 region은 먼저 부모 region 디렉터리의 .splits 하위 디렉터리 아래에 생성돼요. HBASE-26187을 포함하는 더 새 HBase 구현에서 이 온디스크 레이아웃은 변경됐어요: 딸 region 디렉터리는 부모 region의 .splits 디렉터리 아래가 아니라 테이블 디렉터리 아래에 직접 생성돼요. 그러한 새 버전에 대해 번호가 매겨진 절차를 읽을 때, .splits 디렉터리를 만들거나 사용한다고 언급하는 단계는 딸 region 파일을 테이블 디렉터리 아래에 직접 준비·승격하는 것으로 해석해 주세요. 이 갱신은 높은 수준의 분할 의미론(참조, 메타 갱신, 딸의 온라인 전환)을 변경하지 않으며, HDFS에서 임시/초기 딸 파일이 준비되는 위치에만 영향을 미쳐요.
Write Ahead Log (WAL)
목적 (Purpose)
Write Ahead Log(WAL)는 HBase의 모든 데이터 변경을 파일 기반 저장소에 기록해요. 정상적인 운영에서 WAL은 필요 없는데, 데이터 변경이 MemStore에서 StoreFile로 이동하기 때문이에요. 하지만 MemStore가 플러시되기 전에 RegionServer가 크래시하거나 사용 불가능해지면, WAL은 데이터 변경이 재생될 수 있도록 보장해요. WAL에 쓰기가 실패하면 데이터를 수정하는 전체 연산이 실패해요.
HBase는 WAL 인터페이스의 구현을 사용해요. 보통 RegionServer당 WAL 인스턴스가 하나만 있어요. 예외는 hbase:meta를 담당하는 RegionServer로, meta 테이블은 자체 전용 WAL을 가져요. RegionServer는 Puts와 Deletes를 영향 받은 Store의 MemStore에 기록하기 전에 WAL에 기록해요.
The HLog
2.0 이전에 HBase의 WAL 인터페이스는 HLog라고 불렸어요. 0.94에서 HLog는 WAL 구현의 이름이었어요. 이전 버전에 맞춰진 문서에서 HLog에 대한 참조를 찾을 가능성이 높아요.
WAL은 HDFS의 /hbase/WALs/ 디렉터리에 있으며, RegionServer별로 하위 디렉터리를 가져요.
로그 선행 쓰기 개념에 대한 더 일반적인 정보는 Wikipedia의 Write-Ahead Log 문서를 참고해 주세요.
WAL 제공자 (WAL Providers)
HBase에는 여러 WAL 구현(또는 'Providers')이 있어요. 각각은 짧은 이름 라벨로 알려져 있어요(불행히도 항상 설명적인 것은 아님). hbase-site.xml에서 hbase.wal.provider 속성의 값으로 WAL 제공자 짧은 이름을 전달해 제공자를 설정해요 (hbase:meta용 제공자는 hbase.wal.meta_provider 속성으로 설정. 그렇지 않으면 hbase.wal.provider로 구성된 것과 같은 제공자를 사용).
- asyncfs: 기본값. hbase-2.0.0부터 새 것(HBASE-15536, HBASE-14790). RegionServer 로그에서 스스로를 AsyncFSWALProvider라고 식별하는 이 AsyncFSWAL 제공자는 새 비차단 dfsclient 구현 위에 구축됐어요. 현재는 hbase 코드베이스에 상주하지만 의도는 HDFS 자체로 다시 올리는 것이에요. WAL 편집은 기본 클라이언트가 하는 체인 파이프라인이 아니라 각 DataNode의 각 WAL-block 복제본에 동시에("fan-out" 스타일) 기록돼요. 지연 시간이 더 좋을 거예요. 구현에 대한 자세한 내용은 Xiaomi의 Apache HBase Improvements and Practices 슬라이드 14부터 참고해 주세요.
- filesystem: hbase-1.x 릴리스의 기본값. 차단 DFSClient 위에 구축되며 고전적인 DFSClient 파이프라인 모드로 복제본에 기록돼요. 로그에서는 FSHLog 또는 FSHLogProvider로 식별돼요.
- multiwal: 이 제공자는 asyncfs 또는 filesystem의 여러 인스턴스로 구성돼요. multiwal에 대한 자세한 내용은 다음 섹션을 참고해 주세요.
RegionServer 로그에서 아래 같은 줄을 찾아 어떤 제공자가 있는지 확인해 주세요(아래는 기본 AsyncFSWALProvider를 보여줌).
2018-04-02 13:22:37,983 INFO [regionserver/ve0528:16020] wal.WALFactory: Instantiating WALProvider of type class org.apache.hadoop.hbase.wal.AsyncFSWALProvider
AsyncFSWAL이 DFSClient 구현의 내부를 해킹하므로 단순한 패치 릴리스에서도 hadoop 의존성을 업그레이드하면 쉽게 깨질 수 있어요. 그래서 wal 제공자를 명시적으로 지정하지 않으면 먼저 asyncfs를 사용하려 시도하고, 실패하면 filesystem으로 폴백할 거예요. 이것이 항상 작동하지 않을 수 있으므로, AsyncFSWAL 시작 문제로 인해 HBase를 시작하는 데 여전히 문제가 있으면 구성 파일에서 filesystem을 명시적으로 지정해 주세요.
EC 지원은 hadoop-3.x에 추가됐으며, EC 출력 스트림이 hflush/hsync를 지원하지 않으므로 WAL과 호환되지 않아요. EC 디렉터리에서 비-EC 파일을 만들려면 FileSystem의 새 builder 기반 create API를 사용해야 하지만, 그것은 hadoop-2.9+에서만 도입되었고 HBase는 여전히 hadoop-2.7.x를 지원해야 해요. 그래서 해결 방법을 찾을 때까지 WAL 디렉터리에 EC를 활성화하지 마세요.
MultiWAL
RegionServer당 단일 WAL이 있으면 HDFS 파일이 순차적이어야 하므로 RegionServer가 WAL에 직렬로 써야 해요. 이는 WAL이 성능 병목이 되게 해요.
HBase 1.0은 HBASE-5699에서 MultiWal 지원을 도입해요. MultiWAL은 기본 HDFS 인스턴스에서 여러 파이프라인을 사용해 RegionServer가 병렬로 여러 WAL 스트림을 쓰도록 허용하며, 쓰기 중 총 처리량을 증가시켜요. 이 병렬화는 들어오는 편집을 그 Region별로 분할함으로써 이루어져요. 따라서 현재 구현은 단일 Region에 대한 처리량을 높이는 데는 도움이 되지 않을 거예요.
원래 WAL 구현을 사용하는 RegionServer와 MultiWAL 구현을 사용하는 RegionServer는 각각 어느 쪽 WAL 집합의 복구도 처리할 수 있어, rolling restart를 통해 무중단 구성 업데이트가 가능해요.
MultiWAL 구성 (Configure MultiWAL)
RegionServer에 MultiWAL을 구성하려면 다음 XML을 붙여넣어 hbase.wal.provider 속성의 값을 multiwal로 설정해 주세요.
<property>
<name>hbase.wal.provider</name>
<value>multiwal</value>
</property>
변경 사항이 적용되도록 RegionServer를 재시작해 주세요.
RegionServer에 MultiWAL을 비활성화하려면 속성을 해제하고 RegionServer를 재시작해 주세요.
WAL 플러싱 (WAL Flushing)
TODO (설명).
WAL 분할 (WAL Splitting)
RegionServer는 많은 region을 서빙해요. region server의 모든 region은 같은 활성 WAL 파일을 공유해요. WAL 파일의 각 편집에는 어떤 region에 속하는지에 대한 정보가 포함돼요. region이 열리면 WAL 파일에서 그 region에 속한 편집을 재생해야 해요. 따라서 WAL 파일의 편집은 특정 region의 데이터를 재생성하기 위해 특정 집합을 재생할 수 있도록 region별로 그룹화되어야 해요. WAL 편집을 region별로 그룹화하는 과정을 로그 분할(log splitting)이라고 해요. region server가 실패할 때 데이터를 복구하기 위한 중요한 과정이에요.
로그 분할은 클러스터 시작 중 HMaster가 수행하거나, region server가 종료될 때 ServerShutdownHandler가 수행해요. 일관성이 보장되도록 데이터가 복원될 때까지 영향 받은 region은 사용 불가능해요. 모든 WAL 편집은 주어진 region이 다시 사용 가능해지기 전에 복구되고 재생되어야 해요. 결과적으로 로그 분할의 영향을 받는 region은 과정이 완료될 때까지 사용 불가능해요.
절차: 로그 분할, 단계별 (Procedure: Log Splitting, Step by Step)
/hbase/WALs/HOST,PORT,STARTCODE 디렉터리가 이름이 바뀜
디렉터리 이름을 바꾸는 것이 중요한 이유는 HMaster가 RegionServer가 다운됐다고 생각하더라도 RegionServer가 여전히 살아 있어 요청을 받고 있을 수 있기 때문이에요. RegionServer가 즉시 응답하지 않고 ZooKeeper 세션을 하트비트하지 않으면, HMaster는 이를 RegionServer 실패로 해석할 수 있어요. 로그 디렉터리 이름을 바꾸면 활성 상태이지만 바쁜 RegionServer가 여전히 사용 중인 기존 유효 WAL 파일에 우연히 쓰이지 않도록 보장해요.
새 디렉터리는 다음 패턴으로 이름이 지정돼요.
/hbase/WALs/HOST,PORT,STARTCODE-splitting
이렇게 이름이 바뀐 디렉터리의 예시는 다음과 같을 수 있어요.
/hbase/WALs/srv.example.com,60020,1254173957298-splitting
각 로그 파일이 한 번에 하나씩 분할됨
로그 분할기는 로그 파일을 한 번에 하나의 편집 항목으로 읽고, 각 편집 항목을 해당 편집의 region에 대응하는 버퍼에 넣어요. 동시에 분할기는 여러 작성자 스레드를 시작해요. 작성자 스레드는 해당 버퍼를 집어 버퍼의 편집 항목을 임시 복구 편집 파일에 기록해요. 임시 편집 파일은 다음 명명 패턴으로 디스크에 저장돼요.
/hbase/TABLE_NAME/REGION_ID/recovered.edits/.temp
이 파일은 이 region의 WAL 로그에 있는 모든 편집을 저장하는 데 사용돼요. 로그 분할이 완료된 후 .temp 파일은 파일에 기록된 첫 번째 로그의 시퀀스 ID로 이름이 바뀌어요.
모든 편집이 기록되었는지 판단하기 위해 시퀀스 ID를 HFile에 기록된 마지막 편집의 시퀀스와 비교해요. 마지막 편집의 시퀀스가 파일 이름에 포함된 시퀀스 ID보다 크거나 같으면, 편집 파일의 모든 쓰기가 완료되었음이 분명해요.
로그 분할 완료 후 각 영향 받은 region이 RegionServer에 할당됨
region이 열리면 recovered.edits 폴더에서 복구 편집 파일이 있는지 확인해요. 그러한 파일이 있으면 편집을 읽어 MemStore에 저장함으로써 재생돼요. 모든 편집 파일이 재생된 후 MemStore의 내용이 디스크(HFile)에 기록되고 편집 파일이 삭제돼요.
로그 분할 중 오류 처리 (Handling of Errors During Log Splitting)
hbase.hlog.split.skip.errors 옵션을 true로 설정하면 오류는 다음과 같이 처리돼요.
- 분할 중 발생한 모든 오류가 기록됨
- 문제가 있는 WAL 로그가 hbase rootdir 아래의 .corrupt 디렉터리로 이동됨
- WAL 처리가 계속됨
hbase.hlog.split.skip.errors 옵션을 false(기본)로 설정하면 예외가 전파되고 분할이 실패로 기록돼요. HBASE-2958 참고 — hbase.hlog.split.skip.errors를 false로 설정하면 분할을 실패시키지만 그게 전부예요. 이 플래그가 설정되면 분할을 실패시키는 것보다 더 많이 해야 해요.
크래시한 RegionServer의 WAL을 분할할 때 EOFException이 어떻게 처리되는지
로그 분할 중 EOFException이 발생하면 hbase.hlog.split.skip.errors가 false로 설정되어 있어도 분할이 계속돼요. 분할할 파일 집합의 마지막 로그를 읽는 동안 EOFException이 발생할 가능성이 있는데, 크래시 시점에 RegionServer가 레코드를 쓰는 과정에 있었을 가능성이 있기 때문이에요. 배경은 HBASE-2643을 참고해 주세요.
로그 분할 중 성능 개선 (Performance Improvements during Log Splitting)
WAL 로그 분할과 복구는 크래시에 관여한 RegionServer 수와 region 크기에 따라 리소스 집약적이고 오래 걸릴 수 있어요. 분산 로그 분할은 로그 분할 중 성능을 개선하기 위해 개발됐어요.
분산 로그 분할 활성화/비활성화 (Enabling or Disabling Distributed Log Splitting)
분산 로그 처리는 HBase 0.92부터 기본적으로 활성화돼요. 설정은 hbase.master.distributed.log.splitting 속성으로 제어되며 true 또는 false로 설정할 수 있지만, 기본값은 true예요.
procedureV2 기반 WAL 분할
HBASE-20610 이후 procedureV2 프레임워크로 WAL 분할 조정을 하는 새 방법을 도입했어요. 이는 WAL 분할 과정을 단순화하고 더 이상 zookeeper에 연결할 필요가 없어요.
배경 (Background)
현재 WAL 분할 과정은 zookeeper가 조정해요. 각 region server가 zookeeper에서 태스크를 가져오려 시도해요. 그리고 region server 수가 증가하면 부담이 더 무거워져요.
Master 측 구현
ServerCrashProcedure 중에 SplitWALManager가 분할해야 할 각 WAL 파일에 대해 하나의 SplitWALProcedure를 만들어요. 그런 다음 각 SplitWALProcedure가 SplitWalRemoteProcedure를 생성해 region server에 요청을 보내요. SplitWALProcedure는 StateMachineProcedure이며 여기에 상태 전이 다이어그램이 있어요.
Region Server 측 구현
Region Server는 SplitWALCallable을 받아 실행할 거예요. 이전보다 훨씬 간단해요. 성공하면 null을, 오류가 있으면 예외를 반환할 거예요.
성능 (Performance)
region server 5개와 master 1개가 있는 클러스터의 테스트에 따르면, procedureV2로 조정된 WAL 분할이 전체 클러스터를 재시작하거나 하나의 region server가 크래시할 때 ZK 조정 WAL 분할보다 더 나은 성능을 보여요.
이 기능 활성화 (Enable this feature)
이 기능을 활성화하려면 먼저 HBase 패키지에 이 코드가 포함되어 있는지 확인해야 해요. 아니면 먼저 구성 변경 없이 HBase 클러스터 패키지를 업그레이드해 주세요. 그런 다음 구성 'hbase.split.wal.zk.coordinated'를 false로 변경해 주세요. 새 구성으로 master를 rolling upgrade해 주세요. 이제 WAL 분할이 새 구현으로 처리돼요. 하지만 region server는 여전히 zookeeper에서 태스크를 가져오려 시도하므로, 새 구성으로 region server를 rolling upgrade해 그것을 멈추게 해요.
- 단계는 다음과 같아요. 전체 클러스터를 업그레이드해 새 구현을 얻는다. 새 구성 'hbase.split.wal.zk.coordinated'=false로 Master를 업그레이드한다. zookeeper에서 태스크 가져오기를 멈추도록 region server를 업그레이드한다.
WAL 압축 (WAL Compression)
WAL의 내용은 LRU Dictionary 압축으로 압축할 수 있어요. 이는 다른 datanode로의 WAL 복제를 빠르게 하는 데 사용할 수 있어요. 사전은 최대 2^15개의 요소를 저장할 수 있어요. 이 수를 초과하면 퇴거가 시작돼요.
WAL 압축을 활성화하려면 hbase.regionserver.wal.enablecompression 속성을 true로 설정해 주세요. 이 속성의 기본값은 false예요. 기본적으로 WAL 압축이 활성화되면 WAL 태그 압축이 켜져요. hbase.regionserver.wal.tags.enablecompression 속성을 'false'로 설정해 WAL 태그 압축을 끌 수 있어요.
WAL 압축의 가능한 단점은 WAL의 마지막 블록이 쓰기 도중 제대로 종료되지 않으면 더 많은 데이터를 잃는다는 것이에요. 이 마지막 블록의 항목이 사전 항목과 함께 추가되었지만 갑작스러운 종료로 인해 수정된 사전을 영속화하지 못했다면, 이 마지막 블록을 읽으면 마지막에 기록된 항목을 해석하지 못할 수 있어요.
내구성 (Durability)
각 Mutation 또는 Table 기준으로 내구성을 설정할 수 있어요. 옵션은 다음과 같아요.
- SKIP_WAL: Mutation을 WAL에 쓰지 않아요(다음 섹션 WAL 비활성화 참조).
- ASYNC_WAL: WAL을 비동기로 써요. 클라이언트가 자신의 쓰기의 파일시스템 동기화를 기다리게 하지 않고 즉시 반환해요. 편집이 보이게 돼요. 한편 백그라운드에서 Mutation이 나중에 WAL로 플러시될 거예요. 이 옵션은 현재 데이터를 잃을 수 있어요. HBASE-16689 참고.
- SYNC_WAL: 기본값. 각 편집이 클라이언트에 성공을 반환하기 전에 HDFS에 동기화돼요.
- FSYNC_WAL: 각 편집이 클라이언트에 성공을 반환하기 전에 HDFS와 파일시스템에 fsync돼요.
Mutation 또는 Table의 ASYNC_WAL 옵션을 AsyncFSWAL 작성자와 혼동하지 마세요. 그것들은 불행히도 이름이 가까운 별개의 옵션이에요.
커스텀 WAL 디렉터리 (Custom WAL Directory)
HBASE-17437은 1.3.3/2.0+부터 HBase 루트 디렉터리 밖에, 또는 심지어 다른 FileSystem에 WAL 디렉터리를 지정하는 지원을 추가했어요. 일부 FileSystem(예: Amazon S3)은 append나 일관된 쓰기를 지원하지 않는데, 그러한 시나리오에서는 쓰기 손실을 피하기 위해 WAL 디렉터리를 다른 FileSystem에 구성해야 해요.
이를 달성하기 위해 다음 구성이 추가됐어요.
- hbase.wal.dir 루트 WAL 디렉터리의 위치를 정의해요. 루트 디렉터리와 다른 FileSystem에 있을 수 있어요. WAL 디렉터리는 루트 디렉터리의 하위 디렉터리로 설정할 수 없어요. 설정하지 않으면 기본값은 루트 디렉터리예요.
- hbase.rootdir.perms 루트 디렉터리에 설정할 FileSystem 권한을 구성해요. 기본 '700'이에요.
- hbase.wal.dir.perms WAL 디렉터리 FileSystem에 설정할 FileSystem 권한을 구성해요. 기본 '700'이에요.
커스텀 WAL 디렉터리로 마이그레이션할 때(HBase 루트 디렉터리 밖 또는 다른 FileSystem) 기존 WAL 파일은 새 WAL 디렉터리로 수동으로 복사해야 해요. 그렇지 않으면 HMaster가 이전 WAL 디렉터리에 대한 정보가 없으므로 데이터 손실/불일치로 이어질 수 있어요.
WAL 비활성화 (Disabling the WAL)
특정 상황에서 성능을 개선하기 위해 WAL을 비활성화할 수 있어요. 하지만 WAL을 비활성화하면 데이터가 위험해져요. 권장되는 유일한 상황은 벌크 로드 중이에요. 문제 발생 시 데이터 손실 위험 없이 벌크 로드를 다시 실행할 수 있기 때문이에요.
WAL은 HBase 클라이언트 필드 Mutation.writeToWAL(false)를 호출하여 비활성화해요. Mutation.setDurability(Durability.SKIP_WAL)와 Mutation.getDurability() 메서드를 사용해 필드의 값을 설정하고 가져와요. 특정 테이블에 대해서만 WAL을 비활성화할 방법은 없어요.
벌크 로드가 아닌 다른 용도로 WAL을 비활성화하면 데이터가 위험해져요.
더 알아보기 (Learn more)
Block Cache, BucketCache, WAL, Splitting 등 HBase RegionServer 관련 문서와 아키텍처 문서를 이어서 보시길 권해요.