본문 바로가기
개발/Hadoop eco-system

[HBase] Online Region Merge와 Empty Region Merge에 대해

by 달사쿠 2022. 6. 30.
반응형

Intro

운영하는 서비스에서 HBase 테이블의 column family에 ttl을 세팅하여 사용하고 있습니다. ttl을 걸어두게 되면, Hbase는 ttl(만료 시간)에 도달한 row를 삭제합니다.

참고로 HBase에서는 Delete 연산을 하더라도 바로 삭제되지는 않습니다. 'tombstone marker'를 사용해서 Scan/Get과 같은 조회연산에서 반영되지 않도록 하고, 실제로 삭제되는 시점은 Major Compaction 때 삭제됩니다. 이는 ttl을 설정한 row에도 적용이 됩니다.


갑자기 ttl이 왜나오냐 의문을 가지실 수 있는데요.

제가 운영하는 테이블의 rowkey는 timestamp가 포함된 형태고 1개의 column family를 사용하는데 ttl을 7일로 설정해두었습니다. (멍청했던 저는) 당연히 7일이 지나면 만료된 데이터가 사라지면서 Store file이 0Byte가 된 empty region도 삭제하는 줄 알았었지만....

 

멀쩡히 Store file이 0B인 채로 region이 살아있더라구요..?!


비슷한 경우를 Stack Overflow에서 찾고, Hbase 도큐먼트를 확인한 결과 아래와 같은 내용을 확인하게 되었습니다.

HBase splits big regions automatically but does not support merging small regions automatically.
> HBase는 큰 영역을 자동으로 분할하지만 작은 영역을 자동으로 병합하는 것은 지원하지 않습니다.

https://docs.cloudera.com/documentation/enterprise/6/6.3/topics/cdh_ig_hbase_online_merge.html

 

HBase Online Merge | 6.3.x | Cloudera Documentation

CDH 6 supports online merging of regions. HBase splits big regions automatically but does not support merging small regions automatically. To complete an online merge of two regions of a table, use the HBase shell to issue the online merge command. By defa

docs.cloudera.com

아무래도 HBase 특성 상 Hotspot을 방지하기 위해 region을 쪼개지만, merge를 하는 경우는 정말 많은 수의 region으로 쪼개져서 합치지 않는 한 사용하지 않을 수도 있겠다 싶네요..

어쨌거나 서론이 길었네요. 오늘 포스팅에서는 HBase에서 지원하는 Online Merge에 대해서 알아보고, 이 Online Merge를 활용해서 다수의 empty region을 합치는 스크립트를 공유하려고 합니다.

*참고로 본 포스트는 HBase2를 기준으로 작성했습니다.


HBase Online Merge

CDH6 (*HBase version: 2.1.1) 은 region의 online merge를 지원합니다. HBase는 큰 region을 자동으로 분할하지만 작은 region을 자동으로 병합하는 것은 지원하지 않습니다ㅠㅠ
HBase shell에서는 아래와 같은 command로 테이블의 두 region을 병합할 수 있습니다.

hbase > merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME'
hbase > merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true

기본적으로 병합할 두 region은 인접한 region이어야 하는데요. 즉, region의 한 end key가 다른 region의 start key여야 합니다.
위의 첫번째 커멘드가 이에 해당합니다.

두번째는 동일한 테이블의 두 region이 인접하지 않아도 병합할 수 있도록 "force merge"하는 옵션을 `true`로 주는 커멘드입니다. 이는 중복이 발생할 수 있으므로 권장하지 않습니다.


참고로 Master와 RegionServer는 모두 online merge에 참여합니다. 병합 요청이 Master로 전송되면 Master는 병합할 region을 동일한 RegionServer로 이동합니다.
일반적으로 부하가 높은 RegionServer에 있는 region을 옮깁니다. 그런 다음 Master는 RegionServer에 두 region을 병합하도록 요청합니다.

두 region이 병합되면 새 region은 'ONLINE' 상태가 되고, 서버 요청에 사용할 수 있으며 이전 region은 'OFFLINE' 상태가 됩니다.


empty regions의 병합


소수의 region을 병합한다면.. 커맨드로 쉽게 할 수 있지만, 여러개의 empty region을 (노가다가 아닌 방법으로) 병합하기 위해서 저는 스크립트를 작성하기로 했습니다.

반응형

본 코드는 아래의 오픈소스를 참고하여 작성하였습니다. (오픈소스는 HBase 1.x를 기준으로하며, ruby로 작성되어 있습니다.)
https://gist.github.com/ndimiduk/6594d55a7a282c5d3378e65b9582deaa

 

Drop all empty regions for a table. Dropping a region is implemented as a merge into an adjacent region.

Drop all empty regions for a table. Dropping a region is implemented as a merge into an adjacent region. - drop_empty_regions.rb

gist.github.com


Kotlin으로 작성했고, hbase-client library를 필요로 합니다. 또한, hbase connection을 만드는 코드는 생략했으며 메인 코드는 @Test를 붙여 돌릴 수 있게 스크립트를 작성했으니 참고해주세요.

먼저 스크립트에서 사용할 기능들을 묶어 Client로 만든 클래스입니다.

import org.apache.hadoop.hbase.NamespaceDescriptor
import org.apache.hadoop.hbase.RegionMetrics
import org.apache.hadoop.hbase.Size
import org.apache.hadoop.hbase.TableName
import org.apache.hadoop.hbase.client.Admin
import org.apache.hadoop.hbase.client.RegionInfo

class HbaseClient() {
	...
    
    private val conn = .. //생략
    private val admin = conn.admin
    
    /**
    * Storefile이 0B인 empty region을 가져오는 메서드
    * return: Map<테이블 명, 해당 테이블의 empty region 리스트>
    **/
    fun getEmptyRegions(): Map<String, MutableList<RegionMetrics>> { 
        val emptyRegions = mutableMapOf<String, MutableList<RegionMetrics>>() // key: table명, value: 해당 table의 empty region들의 리스트
        val regionServers = admin.regionServers.toList() // 모든 RegionServers를 가져와서 empty region을 찾기 위함.
        
        regionServers.forEach { serverName ->
            val regionMetrics = admin.getRegionMetrics(serverName) //특정 RegionServer의 정보를 가져옴
            regionMetrics.forEach { region -> //해당 RegionServer의 모든 region을 순회하면서 storeFileSize가 0B인 region을 emptyRegions에 넣어준다.
                val regionName = region.nameAsString
                val storeFileSize = region.storeFileSize.get(Size.Unit.BYTE)
                if (storeFileSize == 0.0) {
                    val tableName = regionName.substringBefore(",")
                    val value = emptyRegions[tableName] ?: mutableListOf()
                    value.add(region)
                    emptyRegions[tableName] = value
                }
            }
        }
        return emptyRegions
    }

    /**
    * 특정 table의 리전들을 모두 가져오는 메서드
    **/
    fun getRegionsInfo(tableName: String): List<RegionInfo> {
        return admin.getRegions(TableName.valueOf(tableName))
    }

    /**
    * merge할 0B empty region의 RegionInfo를 가져오는 메서드
    * 해당 region 정보를 가져올 수 없을 때, null을 반환
    **/
    fun getMergeTargetRegion(
        regionInfos: List<RegionInfo>,
        emptyRegion: RegionMetrics
    ): RegionInfo? {
        val regionInfo = regionInfos.filter { region -> region.regionNameAsString == emptyRegion.nameAsString }
        return if (regionInfo.isEmpty())
            null
        else
            regionInfo[0]
    }

    /**
    * 특정 region과 인접한 region의 RegionInfo를 가져오는 메서드
    * 인접한 region이 없을 때, null을 반환
    **/
    fun getAdjacentRegion(
        regionInfos: List<RegionInfo>,
        targetRegionInfo: RegionInfo
    ): RegionInfo? {
        val adjacentRegions = regionInfos.filter { region -> region.isAdjacent(targetRegionInfo) }
        return if (adjacentRegions.isEmpty())
            null
        else adjacentRegions[0]
    }
    
    /**
    * 두 region을 merge하는 메서드. 인접한 region간의 merge만 진행하기 위해 'force merge'를 false로 줌
    **/
    fun mergeRegionsAsync(targetRegion: RegionInfo, adjacent: RegionInfo) {
        admin.mergeRegionsAsync(targetRegion.regionName, adjacent.regionName, false)
    }


다음은 HBaseClient를 가져와 모든 empty region을 병합해주는 코드입니다.

class HBaseClientTest {

    private client = HBaseClient()
    
    @Test
    fun mergeAllEmptyRegions() {
        while (true) {
            try {
                val emptyRegions = client.getEmptyRegions()
                if (emptyRegions.isEmpty())		//empty region이 없을 땐 loop 종료
                    return
                emptyRegions.forEach { (table, emptyRegionList) -> 	
                    val regionsInfo = client.getRegionsInfo(table)	//특정 table의 모든 region들의 정보를 가져옴
                    emptyRegionList.forEach here@{ emptyRegion ->  	
                        val targetRegion = client.getMergeTargetRegion(regionsInfo, emptyRegion) ?: return@here
                        val adjacent = client.getAdjacentRegion(regionsInfo, targetRegion) ?: return@here
                        println("target region: $targetRegion")
                        println("adjacent region: $adjacent")

                        client.mergeRegionsAsync(targetRegion, adjacent)
                        Thread.sleep(5)
                    }
                }
                return
            } catch (e: Exception) {
                println(e)
                Thread.sleep(5)
            }
        }
    }


기본적으로 region간의 merge를 진행하면, region이 이동하기 때문에 간혹 region을 못찾는 error가 나올 수 있습니다.
때문에, region을 못 찾으면 다시 table의 region들의 정보를 가져와 병합하는 과정을 가집니다.


마무리

흔한 경우도 아니기도 하고, HBase2가 나오면서 hbase-client가 많이 바뀌었는데도 불구하고 HBase2에 대한 empty region merge 스크립트가 없어 직접 작성해보게 되었습니다.

주기적으로 region merge를 수동으로 시켜줘야 하는 것이 너무나도 귀찮은데... HBase3에서는 auto merge도 지원해줬으면 좋겠습니다ㅠㅠ

반응형

댓글