HBase 테이블 수준의 리전 균형 조정 기법

HBase 환경에서 데이터가 리전에 고르게 분포되지 않을 경우, 특히 예비 분할 없이 생성된 리전일 경우, 쓰기 부하의 불균형 문제가 발생할 수 있습니다. 전통적인 해시 또는 솔트 방식은 분산을 개선할 수 있지만, 이후 조회 성능에 영향을 미치는 문제점이 있습니다.

본 문서에서는 테이블 단위에서 리전의 분포를 최적화하는 새로운 균형 조정 전략을 제안합니다. 이 방법은 HBase 관리자 인터페이스의 기본 balancer() 함수를 대체하며, 각 리전 서버의 리전 수가 평균값 근처로 유지되도록 합니다.

다음은 필요한 의존성 설정입니다:

<repositories>
    <repository>
        <id>cloudera</id>
        <url>https://repository.cloudera.com/artifactory/cloudera-repos</url>
    </repository>
</repositories>

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>1.2.1</version>
    <exclusions>
        <exclusion>
            <groupId>javax.servlet</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.2.1</version>
    <exclusions>
        <exclusion>
            <groupId>javax.servlet</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>

주요 로직 구현 코드는 다음과 같습니다:

package hbase_balance;

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

import java.io.IOException;
import java.util.*;

/**
 * 테이블 단위 리전 균형 조정 전략
 * - 각 리전 서버의 리전 수가 평균치에 가까워지도록 재배치
 * - 기존 admin.balancer() 대신 사용 가능
 */
public class HbaseBalancer {

    public static final String TABLE_NAME = "data1";
    public static final String ZK_QUORUM = "hadoop01:2181,hadoop02:2181,hadoop03:2181";

    public static final Integer REGIONS_PER_SERVER = 50;

    public static void main(String[] args) throws IOException {
        Configuration config = HBaseConfiguration.create();
        config.set(HConstants.ZOOKEEPER_QUORUM, ZK_QUORUM);
        Connection connection = ConnectionFactory.createConnection(config);

        Admin admin = connection.getAdmin();
        ClusterStatus clusterStatus = admin.getClusterStatus();
        Collection<ServerName> serverList = clusterStatus.getServers();

        System.out.println("리전 서버 목록:");
        Map<String, RegionServerInfo> regionServerMap = new HashMap<>();

        for (ServerName server : serverList) {
            RegionServerInfo rsInfo = new RegionServerInfo();
            rsInfo.setServerName(server);
            regionServerMap.put(server.getHostname(), rsInfo);
            System.out.println(server.getStartcode() + " " + server.getHostname() + " " + server.getHostAndPort());
        }

        Table table = connection.getTable(TableName.valueOf(TABLE_NAME));
        RegionLocator locator = connection.getRegionLocator(table.getName());
        List<HRegionLocation> regionLocations = locator.getAllRegionLocations();

        int avgRegionCount = regionLocations.size() / serverList.size();
        System.out.println("평균 리전 수: " + avgRegionCount);

        List<HRegionLocation> toMove = new ArrayList<>();
        System.out.println("=== 리전 위치 정보 수집 완료 ===");

        // 현재 리전 배치 상태 반영
        for (HRegionLocation loc : regionLocations) {
            String host = loc.getHostname();
            RegionServerInfo rs = regionServerMap.get(host);
            if (rs.getRegionCount() < avgRegionCount) {
                rs.addRegion(loc.getRegionInfo().getRegionNameAsString());
            } else {
                toMove.add(loc);
            }
        }

        System.out.println("=== 리전 재배치 시작 ===");
        Iterator<HRegionLocation> iterator = toMove.iterator();

        while (iterator.hasNext()) {
            HRegionLocation region = iterator.next();
            HRegionInfo info = region.getRegionInfo();
            byte[] encodedName = info.getEncodedNameAsBytes();
            String sourceHost = region.getHostname();

            // 가장 적은 리전 수를 가진 서버 선택
            String targetHost = findMinRegionServer(regionServerMap);
            RegionServerInfo targetRs = regionServerMap.get(targetHost);

            if (targetRs.getRegionCount() >= avgRegionCount) {
                break; // 평균 이상으로 증가하지 않도록 조건 체크
            }

            String destServerName = targetRs.getServerName().getServerName();
            admin.move(encodedName, Bytes.toBytes(destServerName));
            targetRs.addRegion(info.getRegionNameAsString());

            System.out.println("리전 이동: " + info.getRegionNameAsString() + " → " + destServerName);
        }

        System.out.println("=== 리전 재배치 완료 ===");
    }

    /**
     * 리전 수가 가장 적은 리전 서버의 호스트 이름 반환
     */
    private static String findMinRegionServer(Map<String, RegionServerInfo> map) {
        String minHost = null;
        int minCount = Integer.MAX_VALUE;

        for (Map.Entry<String, RegionServerInfo> entry : map.entrySet()) {
            int count = entry.getValue().getRegionCount();
            if (count < minCount) {
                minCount = count;
                minHost = entry.getKey();
            }
        }
        return minHost;
    }
}

리전 서버 정보를 관리하기 위한 보조 클래스입니다:

package hbase_balance;

import org.apache.hadoop.hbase.ServerName;

import java.util.ArrayList;
import java.util.List;

public class RegionServerInfo {
    private ServerName serverName;
    private List<String> regionNames;

    public RegionServerInfo() {
        this.regionNames = new ArrayList<>();
    }

    public ServerName getServerName() {
        return serverName;
    }

    public void setServerName(ServerName serverName) {
        this.serverName = serverName;
    }

    public List<String> getRegionNames() {
        return regionNames;
    }

    public int getRegionCount() {
        return regionNames.size();
    }

    public void addRegion(String regionName) {
        regionNames.add(regionName);
    }
}

태그: HBase 리전 균형 테이블 수준 조정 ZooKeeper RegionServer

7월 4일 18:15에 게시됨