RegionServer 오프힙 읽기/쓰기 경로
RegionServer 오프힙 읽기/쓰기 경로 (RegionServer Off-Heap Read/Write Path)
HBase 2.x에서 도입된 오프힙(off-heap) 읽기·쓰기 경로를 설명하는 페이지예요. GC 압박을 줄여 P99/P999 RPC 지연 시간을 낮추는 데 목적이 있어요. 성능 최적화를 원하는 운영자에게 유용합니다.
출처: 문서
본문
개요 (Overview)
P99/P999 RPC 지연 시간을 줄이기 위해 HBase 2.x는 읽기·쓰기 경로가 오프힙 버퍼 풀을 사용하도록 만들었어요. Cell은 JVM 가비지 컬렉터의 범위를 벗어난 오프힙 메모리에 할당되어 GC 압박이 줄어들어요. 쓰기 경로에서 클라이언트로부터 받은 요청 패킷은 사전 할당된 오프힙 버퍼로 읽히고, 그 cell들이 WAL과 Memstore에 성공적으로 영속화될 때까지 오프힙에 유지돼요. Memstore의 메모리 데이터 구조는 cell 메모리를 직접 저장하지 않고, 오프힙 버퍼에 인코딩된 cell을 참조해요. 읽기 경로도 마찬가지예요. 먼저 블록 캐시를 읽으려 하고, 캐시 미스면 HFile로 가서 해당 블록을 읽어요. 블록을 읽어 클라이언트로 cell을 보내는 워크플로는 on-heap 메모리 할당을 피해 GC가 해야 할 작업을 줄이기 위해 최선을 다해요.
(다이어그램)
위 다이어그램의 읽기 섹션에서 onheap이 단 한 번 언급된 것에 대한 해명은 Read block from HDFS to offheap directly를 참고하세요.
오프힙 읽기 경로 (Offheap read-path)
HBase-2.0.0에서 HBASE-11425는 HBase 읽기 경로를 변경해 읽기 데이터를 오프힙에 유지할 수 있게 했어요. 캐시된 데이터(BlockCache)를 java 힙에 복사하는 것을 피했죠(캐시되지 않은 데이터는 위 섹션 다이어그램 아래 메모 참고). 이는 쓰레기가 줄어들고 정리할 것이 줄어들어 GC pause를 줄여요. 오프힙 읽기 경로는 on-heap LRU 캐시와 유사하거나 더 나은 성능을 가질 수 있어요. 이 기능은 HBase 2.0.0부터 사용 가능해요. 오프힙 읽기 경로에 대한 자세한 내용과 테스트 결과는 아래 블로그를 참고하세요. Offheaping the Read Path in Apache HBase: Part 1 of 2와 Offheap Read-Path in Production - The Alibaba story.
엔드투엔드 오프힙 읽기 경로를 위해, 당신이 해야 할 일은 오프힙 기반 Off-heap Block Cache(BC)를 활성화하는 것뿐이에요. 이를 위해 hbase-site.xml에서 hbase.bucketcache.ioengine을 offheap으로 설정하세요(BucketCache Deploy Modes에서 hbase.bucketcache.ioengine 옵션에 대해 더 알아보세요). 또한 hbase.bucketcache.size로 BC의 총 용량을 지정하세요. hbase-env.sh에서 'HBASE_OFFHEAPSIZE' 값을 조정하는 것을 잊지 마세요(BucketCache Example Configuration에서 크기 측정과 활성화 예시 도움 참고). 이 설정은 RegionServer java 프로세스에 대한 최대 가능 오프힙 메모리 할당을 지정하기 위한 것이에요. 이것은 Server RPC 버퍼 풀이나 short-circuit reads 같은 오프힙 메모리를 사용하는 다른 기능의 사용을 수용하기 위해 오프힙 BC 크기보다 커야 해요(BucketCache Example Configuration의 논의 참고).
hbase.bucketcache.ioengine에는 기본값이 없어서 BlockCache가 기본적으로 꺼져 있다는 점을 명심하세요(BucketCache Example Configuration의 "Direct Memory Usage In HBase" 정보 섹션 참고).
이것이 오프힙 읽기 경로를 활성화하는 데 필요한 전부예요. HBase의 대부분 버퍼는 이미 오프힙이에요. BC를 오프힙으로 하면 읽기 파이프라인은 HDFS와 서버 소켓 사이에서 데이터를 복사해요 — hbase.ipc.server.reservoir.initial.max 주의 — 결과를 클라이언트로 보내요.
RPC 버퍼 풀 튜닝 (Tuning the RPC buffer pool)
RPC 서버 측의 cell 바이트를 축적하고 클라이언트 측으로 보낼 결과 cell 블록을 만드는 데 사용되는 ByteBuffer 풀을 튜닝할 수 있어요. hbase.ipc.server.reservoir.enabled로 이 풀을 켜거나 끌 수 있어요. 기본적으로 이 풀은 켜져 있고 사용 가능해요. HBase는 오프힙 ByteBuffer를 만들고 기본적으로 pool해요. 읽기 경로에서 엔드투엔드 오프힙을 원한다면 이것을 끄지 않도록 주의하세요.
이 풀이 꺼지면 서버는 cell 바이트를 축적하고 결과 cell 블록을 만들기 위해 임시 버퍼를 onheap으로 만들어요. 이는 읽기 부하가 높은 서버에서 GC에 영향을 줄 수 있어요.
hbase.ipc.server.reservoir 접두사로 시작하는 설정 키는 hbase-3.x에서 deprecated 됐어요(내부 풀 구현이 변경됐어요). 여전히 hbase-2.2.x나 그 이하라면 옛 설정 키를 사용하세요. 그 외에 hbase-3.x나 hbase-2.3.x+라면 새 설정 키를 사용하세요(HBase3.x의 deprecated 및 새 설정 참고).
다음으로 튜닝할 것은 RPC 서버 측의 ByteBuffer 풀이에요. 사용자는 풀에 몇 개의 버퍼가 있는지, 각 ByteBuffer의 크기가 어떤지에 대해 이 풀을 튜닝할 수 있어요. hbase.ipc.server.reservoir.initial.buffer.size 설정으로 각 버퍼 크기를 튜닝하세요. 기본값은 hbase-2.2.x 이하에서 64KB이고, hbase-2.3.x+에서 기본 65KB로 변경됐어요(HBASE-22532 참고).
결과 크기가 하나의 64KB(기본) ByteBuffer 크기보다 크면 서버는 하나 이상의 ByteBuffer를 잡아 고정 크기 ByteBuffer의 모음으로 결과 cell 블록을 만들려 해요. 풀에 버퍼가 부족하면 서버는 풀을 건너뛰고 임시 on-heap 버퍼를 만들어요.
풀의 최대 ByteBuffer 수는 hbase.ipc.server.reservoir.initial.max 설정으로 튜닝할 수 있어요. 기본값은 region server 핸들러 수의 배수예요(hbase.regionserver.handler.count 설정 참고). 기본적으로 읽기 결과당 결과 cell 블록 크기를 2 MB로 간주하고 각 핸들러가 읽기를 처리한다고 계산해요. 2 MB 크기에는 각각 64 KB인 버퍼 32개가 필요해요(풀의 기본 버퍼 크기 참고). 따라서 핸들러당 32 ByteBuffers(BB)예요. 핸들러 하나가 응답을 만들어 RPC Responder 스레드에 넘긴 뒤, 새 요청을 처리해 새 응답 cell 블록(pooled 버퍼 사용)을 만들 수 있도록 최대 BB 수를 이의 두 배로 할당해요. 응답자가 첫 TCP 응답을 즉시 보내지 못하더라도, 우리의 수는 힙에 임시 버퍼를 만들지 않고도 풀에 충분한 버퍼가 남도록 해줘요. 크기가 작은 랜덤 행 읽기에서는 이 최대 개수를 튜닝하세요. 이것들은 지연 생성되는 버퍼이며 개수는 풀링될 최대 개수예요.
엔드투엔드 읽기 경로를 오프힙으로 만든 뒤에도 여전히 GC 문제가 보인다면, 적절한 버퍼 풀의 문제를 찾아보세요. HBase2.x에서 아래 RegionServer 로그 라인을 INFO 수준으로 확인하세요.
Pool already reached its max capacity : XXX and no free buffers now. Consider increasing the value for 'hbase.ipc.server.reservoir.initial.max' ?
또는 HBase3.x에서 다음 로그 메시지를 확인하세요.
Pool already reached its max capacity : XXX and no free buffers now. Consider increasing the value for 'hbase.server.allocator.max.buffer.count' ?
hbase-env.sh의 HBASE_OFFHEAPSIZE 설정은 이 서버 측 오프힙 버퍼 풀도 고려해야 해요. RegionServer에 대해 이 최대 오프힙 크기를 이 최대 풀 크기와 오프힙 캐시 크기의 합보다 약간 높게 설정해야 해요. TCP 통신을 위해 TCP 계층도 직접 바이트버퍼를 만들어야 해요. DFS 클라이언트도 특히 short-circuit reads가 구성된 경우 작업을 위해 약간의 오프힙이 필요해요. 최대 다이렉트 메모리 크기에 1~2 GB를 추가로 할당하는 것이 테스트에서 효과가 있었어요.
coprocessor를 사용하고 읽기 결과의 Cell을 참조한다면, CP 훅 메서드의 범위 밖으로 이 Cell에 대한 참조를 저장하지 마세요. 때로 CP는 다음 CP 훅 호출에서 고려하기 위해 cell에 대한 정보(예: 행 키)를 저장하고 싶어해요. 그런 경우에는 사용 사례에 따라 전체 Cell의 필수 필드를 clone하세요. [CellUtil#cloneXXX(Cell) API를 참고하세요].
HDFS에서 오프힙으로 블록 직접 읽기 (Read block from HDFS to offheap directly)
HBase-2.x에서 RegionServer는 HDFS에서 임시 onheap ByteBuffer로 블록을 읽은 뒤 BucketCache로 flush해요. BucketCache가 오프힙이더라도 먼저 HDFS 읽기를 onheap으로 가져온 뒤 오프힙 BucketCache로 작성해요. 캐시 적중률이 낮을 때(cacheHitRatio ~ 60% 같은) 많은 GC 압박을 관찰할 수 있어요. HBASE-21879가 이 문제를 해결해요(hbase-2.3.x/hbase-3.x 필요). 그것은 지원하는 HDFS가 있음(hadoop-2.10.x 또는 hadoop-3.3.x)에 의존하며 (이 글을 쓰는 시점에) HBase 자체에 패치가 필요할 수 있어요. HBASE-21879 Read HFile's block to ByteBuffer directly instead of to byte for reducing young gc purpose를 참고하세요. 적절히 설정하면 HDFS에서의 읽기가 오프힙 버퍼로 들어가 오프힙 BlockCache로 오프힙 전달되어 캐시될 수 있어요.
설계와 성능 개선에 대한 자세한 내용은 Design Doc - Read HFile's block to Offheap를 참고하세요.
여기서 성능 튜닝에 대한 몇 가지 베스트 프랙티스를 공유하지만, 먼저 새 내부 풀 구현(ByteBuffAllocator vs 옛 ByteBufferPool)과 함께 가는 새(hbase-3.x/hbase-2.3.x) 설정 이름을 소개해요. 그중 일부는 위 Tuning the RPC buffer pool에서 논의한 deprecated hbase-2.2.x 설정을 흉내 낸 거예요. 이곳의 많은 조언은 구현이 비슷한 설정을 가지므로 위 Tuning the RPC buffer pool에서 준 것과 겹쳐요.
hbase.server.allocator.pool.enabled는 RegionServer가 pooled 오프힙 ByteBuffer 할당자를 사용할지 여부예요. 기본값은 true예요. hbase-2.x에서 deprecatedhbase.ipc.server.reservoir.enabled가 비슷한 일을 했고, 옛 설정에 대한 지원이 제거될 때까지 이 설정에 매핑돼요. 이 새 이름은 hbase-3.x와 hbase-2.3.x+에서 사용될 거예요.hbase.server.allocator.minimal.allocate.size는 풀에서 할당을 시작하는 임계값이에요. 그렇지 않으면 고정 크기 ByteBuffer의 풀에서 작은 것을 할당하는 것은 낭비이므로 요청이 직접 onheap에서 할당돼요. 기본 최소값은hbase.server.allocator.buffer.size/6이에요.hbase.server.allocator.max.buffer.count: 새 pool/reservoir 구현인ByteBuffAllocator는 고정 크기 ByteBuffer를 가져요. 이 설정은 얼마나 많은 버퍼를 풀링할지예요. 기본값은 2MB _ 2 _ hbase.regionserver.handler.count / 65KB예요(RPC 버퍼 풀 튜닝의 위 논의와 유사). 기본hbase.regionserver.handler.count가 30이면 기본값은 1890이 돼요.hbase.server.allocator.buffer.size: 각 ByteBuffer의 바이트 크기예요. 기본값은 66560(65KB)이며, 여기서 HBASE-22532 때문에 64KB가 아닌 65KB를 선택했어요.
hbase-2.x에 도입된 세 개의 설정 키 — hbase.ipc.server.reservoir.enabled, hbase.ipc.server.reservoir.initial.buffer.size, hbase.ipc.server.reservoir.initial.max — 는 hbase-3.x/hbase-2.3.x에서 이름이 바뀌고 deprecated 됐어요. 대신 새 설정 키를 사용하세요: hbase.server.allocator.pool.enabled, hbase.server.allocator.buffer.size, hbase.server.allocator.max.buffer.count.
다음으로 성능에 관한 몇 가지 제안이 있어요.
ByteBuffAllocator에 충분한 pooled DirectByteBuffer가 있는지 확인하세요.
ByteBuffAllocator는 먼저 DirectByteBuffer 풀에서 ByteBuffer를 할당해요. 풀에 사용 가능한 ByteBuffer가 없으면 onheap에서 ByteBuffers를 할당해요. 기본적으로 각 RPC 핸들러에 대해 4MB를 사전 할당해요(핸들러 수는 hbase.regionserver.handler.count 설정으로 결정되며 기본값 30이에요). 즉 hbase.server.allocator.buffer.size가 65KB라면 풀에는 2MB _ 2 / 65KB _ 30 = 945 DirectByteBuffer가 있어요. 큰 스캔과 큰 캐시가 있다면 바이트 크기가 2MB보다 큰 RPC 응답(rpc 요청 수신용 2MB 추가)이 있을 수 있으므로 hbase.server.allocator.max.buffer.count를 늘리는 것이 좋아요.
RegionServer web UI에 ByteBuffAllocator에 대한 통계가 있어요.
(스크린샷)
다음 조건이 충족되면 max buffer.count를 늘려야 할 수 있어요.
heapAllocationRatio >= hbase.server.allocator.minimal.allocate.size / hbase.server.allocator.buffer.size * 100%
버퍼 크기가 블록 크기보다 큰지 확인하세요.
기본 블록 크기가 64KB이므로 거의 모든 데이터 블록은 64KB + 작은 델타가 되며, 델타는 마지막 Cell의 크기에 따라 매우 작아요. hbase.server.allocator.buffer.size=64KB로 설정하면 각 블록이 두 개의 ByteBuffer로 할당돼요. 하나는 64KB DirectByteBuffer, 다른 하나는 델타 바이트용 HeapByteBuffer예요. 이상적으로는 데이터 블록을 하나의 ByteBuffer로 할당해야 해요. 더 단순한 데이터 구조, 더 빠른 접근 속도, 더 적은 힙 사용을 갖기 때문이에요. 또한 블록이 여러 ByteBuffer의 복합이면 체크섬을 검증하기 위해 임시 힙 복사를 수행해야 해요(HBASE-21917 참고). 반면 단일 ByteBuffer면 hadoop의 체크섬 네이티브 lib를 호출해 체크섬을 빠르게 처리할 수 있어요. 더 빠르죠.
또한 HBASE-22483을 참고하세요.
그에 맞춰 HBASE_OFFHEAPSIZE를 올리는 것을 잊지 마세요.
오프힙 쓰기 경로 (Offheap write-path)
hbase-2.x에서 HBASE-15179는 HBase 쓰기 경로를 오프힙으로 동작하게 했어요. 기본적으로 HBase의 MemStores는 항상 메모리 단편화를 피하기 위해 MemStore Local Allocation Buffers(MSLAB)를 사용했어요. MSLAB은 더 큰 고정 크기 청크를 만들고 MemStores Cell의 데이터가 이 MSLAB 청크로 복사돼요. 이 청크들은 pool也可能 수 있으며, hbase-2.x부터 MSLAB 풀은 기본적으로 켜져 있어요. Write 오프힙은 MSLAB 풀을 활용해요. MSLAB 청크를 Direct ByteBuffers로 만들어 pool해요.
hbase.regionserver.offheap.global.memstore.size는 오프힙 데이터 양을 제어하는 설정 키예요. 값은 MSLAB이 사용해야 하는 오프힙 메모리의 메가바이트 수예요(예: 25는 25MB의 오프힙이 된다는 뜻). JVM의 MaxDirectMemorySize 속성을 설정하는 HBASE_OFFHEAPSIZE를 늘리는 것을 잊지 마세요(HBASE_OFFHEAPSIZE에 대한 자세한 내용은 Tuning the RPC buffer pool 참고). hbase.regionserver.offheap.global.memstore.size의 기본값은 0이며, 이는 MSLAB가 기본적으로 onheap(오프힙이 아닌) 청크를 사용한다는 뜻이에요.
hbase.hregion.memstore.mslab.chunksize는 각 오프힙 청크의 크기를 제어해요. 기본값은 2097152(2MB)예요.
Cell이 MemStore에 추가되면 그 Cell의 바이트가 이 오프힙 버퍼로 복사되고(hbase.regionserver.offheap.global.memstore.size가 0이 아닐 때) Cell POJO가 이 메모리 영역을 참조해요. 이는 쓸모가 많은 워크로드에서 MemStores의 on-heap 점유를 크게 줄이고 RegionServer의 전체 힙 사용을 줄일 수 있어요. On-heap과 off-heap 메모리 사용률은 낮은 수준과 높은 수준의 메모리 관리를 구현하기 위해 여러 수준에서 추적돼요. MemStore를 flush할지 결정할 때 그 MemStore의 on-heap과 off-heap 사용을 둘 다 고려해요. Region 수준에서 on-heap과 off-heap 사용을 합산해 region flush 크기(기본 128MB)와 비교해요. 전역적으로 모든 memstore의 on-heap 크기 점유뿐 아니라 off-heap 크기도 추적돼요. 이 크기 중 하나라도 하한(hbase.regionserver.global.memstore.size.lower.limit)이나 최대 크기(hbase.regionserver.global.memstore.size)를 넘어서면 모든 region이 강제 flush 대상으로 선택돼요.