이번에는 Hive에서의 데이터 아키텍처 최적화 방법 중 '적은 데이터를 많게 만들어라'는 사상으로 HQL을 튜닝한 케이스를 살펴보도록 하겠습니다.

 

 

 

  여기서 표현하고 있는 '적은 데이터'는 단순히 데이터의 건수가 적다는 것만을 뜻하는 건 아닙니다.

  Equal 조인이 불가능하도록 데이터가 축약되어 있어 데이터의 분포도가 나쁜 경우를 의미합니다. 데이터가 축약되어 있기에 테이블의 건수가 상대적으로 적게 저장되어 있을 것입니다. 이런 경우에는 축약된 데이터를 상세화하여 하나의 로우로 표현된 데이터를 다수의 로우로 확장시켜줘야 합니다.

  즉, 적은 데이터를 많게 만들어(상세화하여) 데이터의 분포도를 좋게 만들고 Equal 조인이 가능하도록 테이블 아키텍처를 변경해주는 작업이 필요합니다.

 

 

  이번 튜닝 케이스에서 테이블 아키텍처를 변경하고자 하는 대상은 '국가별IP주소대역' 테이블입니다.

  우리가 외부 네트워크에 접속하기 위해서는 우선 IP(이 포스트에서는 IPv4 기준으로 설명)를 할당받아야 합니다. 그런데 이 IP라는 것이 표현상의 한계(000.000.000.000 ~ 255.255.255.255)가 있기 때문에 IP 주소의 수도 한계가 있습니다. 따라서 국가별로 사용할 수 있는 IP대역을 정의하여 IP의 무분별한 사용을 제어하고 있습니다.  

 

[출처] https://krnic.or.kr/jsp/infoboard/stats/landCurrent.jsp

 

 

  국가별로 사용할 수 있는 IP 대역이 지정되어 있으므로 우리는 IP주소만으로 접속자가 현재 어느 나라에서 접속을 하고 있는지 확인할 수 있습니다.

  국가별 IP주소대역은 한국인터넷정보센터(KRNIC)에서 확인 가능하며, CSV 파일로 다운로드도 가능합니다.

* URL : https://krnic.or.kr/jsp/infoboard/stats/landCurrent.jsp

 

[출처] https://krnic.or.kr/jsp/infoboard/stats/landCurrent.jsp

 

 

  한국인터넷정보센터에서 다운로드한 국가별IP주소대역 CSV 파일의 구조는 아래와 같습니다.

 

 

  국가별로 IP 주소 대역이 할당되므로 '시작IP'와 '끝IP'로 IP 대역의 범위를 표현해주고 있습니다.

  '시작IP'와 '끝IP' 사이에 무수한 IP가 존재할 수 있는데 범위 값으로 데이터를 축약한 상태입니다.

  '이 축약된 데이터를 어떻게 풀어나갈 것인가?'가 이 튜닝 케이스의 핵심이 되겠습니다.

 

 

  우선 이번 튜닝의 결과는 아래와 같습니다. 

 

 

 

  위 결과는 제 PC에서 테스트용 데이터를 생성하여 측정한 결과입니다. 

  실제 운영 환경에서는 제가 디멘전 테이블 아키텍처 변경과 데이터 입력 작업만 수행하고, 실제 운영상의 쿼리 변경 작업은 운영 담당자가 진행하여 상세한 튜닝 결과는 확인하지 못했습니다. 운영 클러스터에서는 튜닝 전 약 2시간 정도 수행되는 쿼리가 튜닝 후에는 수행 시간이 약 7분 정도로 단축되었다고 합니다.

  아마도 조인하고자 하는 Fact 테이블의 데이터 건수가 많을수록 성능 차이는 더욱더 크게 벌어질 것입니다.

  참고로, 쿼리에 명시된 TEST_ODS.ACC_LOG 테이블(보안을 위해 스키마, 테이블, 컬럼 이름을 임의로 변경하여 사용)의 실 운영 데이터는 사양이 다른 다수의 인프라에서 수집된 로그입니다. 해당 DW 시스템에서 1일 수집되는 데이터는 약 500GB 이상이고요. 수집 대상 로그 데이터에 국가 정보 추가 요청을 원천 시스템에 요청할 수 있는 환경이 아니였습니다.
  ( 문의에 답변하고자 위 단락 내용 추가하였습니다. 2021/07/08 )

 

  그럼 데이터 건수가 적은 '국가별IP주소대역(CNTRY_IP_RANGE)' 테이블의 아키텍처를 어떻게 변경하고 어떻게 데이터를 입력했는지 퀴즈를 풀듯이 하나하나씩 보여드리도록 하겠습니다.

 

 

 

 

[ 튜닝 전 ]

 

> 튜닝 전 쿼리

  먼저 튜닝 전 쿼리를 살펴보도록 하겠습니다.

INSERT INTO TEST_DW.ACC_IP_CNTRY_MAPP PARTITION (ACC_ID, ACC_START_DT)
SELECT H.IP_ADDR
     , G.CNTRY_CD
     , G.CNTRY_NM
     , H.C
     , H.ACC_ID
     , H.ACC_START_DT
  FROM (
        SELECT IP_ADDR
             , COALESCE(SPLIT(IP_ADDR,'\\.')[0], '_')   AS IP_OCTET1
             , CAST( SPLIT(IP_ADDR,'\\.')[0] AS BIGINT) * 16777216 +
               CAST( SPLIT(IP_ADDR,'\\.')[1] AS BIGINT) * 65536 +
               CAST( SPLIT(IP_ADDR,'\\.')[2] AS BIGINT) * 256 +
               CAST( SPLIT(IP_ADDR,'\\.')[3] AS BIGINT) AS IP_ADDR_NUMT
             , COUNT(*)            AS ACC_CNT  
             , MAX(ACC_ID)         AS ACC_ID
             , MAX(ACC_START_DT)   AS ACC_START_DT
          FROM TEST_ODS.ACC_LOG
         WHERE ACC_ID       = '${ACC_ID}'
           AND ACC_START_DT = '${ACC_START_DT}'
         GROUP BY IP_ADDR
     ) H
 INNER JOIN
       (
        SELECT IP_OCTET1
             , START_IP_NUMT
             , END_IP_NUMT
             , CNTRY_CD
             , CNTRY_NM
          FROM TEST_DW.CNTRY_IP_RANGE
     ) G
    ON H.IP_OCTET1 = G.IP_OCTET1
 WHERE H.IP_ADDR_NUMT BETWEEN G.START_IP_NUMT AND G.END_IP_NUMT;

 

  쿼리문 하단에 보면 조인 컬럼으로 IP_OCTET1(파티션 키 컬럼)이 사용되었다는 것을 확인할 수 있습니다. WHERE 절에서는 BETWEEN 연산을 이용하여 IP 주소(IP_ADDR_NUMT)가 범위(START_IP_NUMT ~ END_IP_NUMT)에 포함되는 매핑 데이터만 필터링하고 있습니다.

  위 텍스트 상으로는 성능 부하가 존재하는 쿼리인지 판단하는게 쉽지 않을 수 있습니다.
  실제 테이블안 데이터의 형태와 분포도 등을 확인하면 좀 더 쉽게 성능상의 문제점을 파악할 수 있을 것입니다.

 

 


> IP 주소 구조

  우선 IP 주소 구조를 간략하게 살펴보도록 하겠습니다.

  IP 주소(정확하게는 IPv4)는 32자리의 이진수로 구성되어 있습니다. 이진수로 표현된 IP주소는 가독성이 떨어지므로 사람들이 쉽게 읽을 수 있도록 10진수로 변환하여 표기합니다. 그리고 더 가독성을 향상하기 위해 8비트마다 점( . , 옷뎃, OCTET)을 찍어 표현하도록 규정되어 있습니다.

 

 

  일반적으로 우리가 데이터로 저장하고 관리하는 IP주소는 옷뎃으로 구분된 10진수로 된 문자열 형태(예: 10.168.80.15)입니다.

  IP주소가 문자열 형태인 경우에는 가독성이 향상되지만 정렬이나 범위 검색 시에는 문제가 발생합니다.
  예를 들어, 일부 문자열 형태의 IP 주소를 정렬한 결과는 아래와 같을 것입니다.

1.x.x.x  196.x.x.x  2.x.x.x  20.x.x.x  213.x.x.x  5.x.x.x  77.x.x.x  80.x.x.x  9.x.x.x  99.x.x.x

 

  위와 같은 이유로 문자열 IP주소로 정렬이나 범위 검색을 하는 경우에는 숫자로 변환하는 작업이 선행되어야 합니다.

 

  문자열 IP 주소를 10진수 숫자로 변환하는 수식은 아래와 같습니다.

옷뎃1 * 16777216  +  옷뎃2 * 65536  +  옷뎃3 * 256  +  옷뎃4

  위 수식을 SQL로 표현하면 아래와 같습니다.

SELECT CAST( SPLIT(IP_ADDR,'\\.')[0] AS BIGINT) * 16777216 +
       CAST( SPLIT(IP_ADDR,'\\.')[1] AS BIGINT) * 65536 +
       CAST( SPLIT(IP_ADDR,'\\.')[2] AS BIGINT) * 256 +
       CAST( SPLIT(IP_ADDR,'\\.')[3] AS BIGINT) AS IP_ADDR_NUMT
  FROM (SELECT '10.168.80.15' AS IP_ADDR) I;

 

  물론 10진수로 표현된 IP 주소를 문자열 형태로도 변환 가능합니다.
  아래는 문자열 IP주소로 변환하는 SQL문 예제입니다.

SELECT CONCAT( CAST(CAST(IP_ADDR_NUMT / 16777216 AS INT)    AS STRING) , '.',
               CAST(CAST(IP_ADDR_NUMT / 65536 % 256 AS INT) AS STRING) , '.',
               CAST(CAST(IP_ADDR_NUMT / 256 % 256 AS INT)   AS STRING) , '.',
               CAST(CAST(IP_ADDR_NUMT % 256 AS INT) AS STRING) 
             ) AS IP_ADDR
  FROM (SELECT 178802703 AS IP_ADDR_NUMT) I;

 

  실제 쿼리에서 IP 주소의 범위 검색 연산은 위와 같이 10진수로 표현된 숫자 IP 주소를 사용하였습니다.
  하지만, 이 포스트에서는 장표나 부연 설명을 단순화 하기 위해 문자열 형태의 IP주소로 표기하였으니 참고 바랍니다.

 

 


> 데이터 처리 방식 1 (국가별IP주소대역이 비 파티션 테이블인 경우)

 

  접속로그(ACC_LOG)의 IP 주소가 해당되는 국가 정보를 확인하기 위해서는 우선 국가별IP주소대역(CNTRY_IP_RANGE) 테이블과 조인을 해야 합니다.
  그런데 국가별IP주소대역 테이블에는 국가코드(CNTRY_CD)별 시작IP(START_IP)와 끝IP(END_IP) 정보만 존재합니다.
  국가별 IP주소대역이 시작과 끝이라는 범위로 표현되어 있기 때문에 Equal 조인이 불가능합니다. 어쩔 수 없이 크로스 조인(카테시안 곱) 후 BETWEEN 연산자를 사용해 접속로그 IP주소에 해당되는 국가코드를 찾도록 필터링해야 합니다.

 

 

 

 
  위와 같이 '국가별IP주소대역' 테이블과 '접속로그' 테이블을 크로스 조인하게 되면 조인 결과 데이터 셋의 건수는 '국가별IP주소대역 데이터 건수 * 접속로그 테이블 건수'가 될 것입니다.
  문제는 하나의 잡에 할당되는 맵과 리듀스의 건수는 조인하고자 하는 테이블 데이터의 크기를 기반으로 산정되어, 조인 결과 셋 크기에 비해 적은 수의 맵과 리듀스 태스크가 할당될 수 있습니다. 과도하게 큰 데이터를 적절하게 분산 처리하지 못하고 적은 수의 태스크에서 큰 데이터를 처리하게 되니 연산 속도가 떨어지게 됩니다. 심한 경우에는 자원 부족에 의해 태스크 실패가 발생할 수도 있습니다.

  이러한 이유에서인지 하이브에서는 크로스 조인에 의한 부하를 방지하기 위해서 기본적으로 카테시안 곱 연산을 막아놨습니다. (일부 옵션을 변경 후 크로스 조인 가능) 그만큼 부하가 큰 연산인 관계로 쿼리 작성 시 지양해야 할 조인 방식입니다.

  크로스 조인의 부하를 제거하기 위해서는 Equal 조인이 가능하도록 '국가별IP주소대역' 테이블 아키텍처를 변경해 Equal 조인이 가능한 컬럼을 추가해줘야 합니다.

 

 


> 데이터 처리 방식 2 (국가별IP주소대역 테이블을 파티셔닝)

 

  위 튜닝 전 쿼리에서 매핑되는 데이터는 IP주소입니다.
  각 IP주소를 Equal 조인하기 위해서는 '국가별IP주소대역' 테이블에 Equal 조인이 가능한 IP주소 기반의 추출 속성을 추가해줘야 합니다.

  '국가별IP주소대역' 테이블에는 시작IP와 끝IP 컬럼이 존재합니다. 매핑시에도 두 컬럼을 이용한 범위 조건 연산을 하므로 신규 추출 컬럼은 시작IP와 끝IP 간의 공통 정보를 기반으로 생성되어야 합니다.

  IP주소 구조에서 설명했듯이 IP 주소는 옷뎃(.)으로 구분됩니다. 시작IP와 끝IP에서 각 옷뎃별로 동일한 데이터가 공통적으로 존재한다면 공통된 옷뎃을 Equal 조인 컬럼으로 사용할 수 있을 것입니다.

  한국인터넷정보센터에서 다운로드한 국가별IP주소대역 CSV 파일(2020년 7월 기준) 데이터를 기반으로 시작IP와 끝IP의 옷뎃별 일치율을 점검한 결과는 아래와 같습니다.

 

 

  시작IP와 끝IP에서 100% 동일한 값을 가진 옷뎃은 옷뎃1 뿐입니다. 이 옷뎃1(IP_OCTET1)을 조인 키 컬럼으로 사용하여 Equal 조인 연산을 할 수 있습니다.

  부수적으로 국가별IP주소대역 테이블은 대용량의 테이블과 조인되므로 건수는 적지만(190,350건) 옷뎃1별로 파티셔닝 하였습니다.

 

 

  이제 파티션 된 국가별IP주소대역 테이블과 접속로그 테이블을 조인 시 데이터 처리 방식을 확인해보겠습니다. 참고로 IP_OCTET1 컬럼으로 파티션된 테이블이 튜닝 전 쿼리에 해당되는 테이블 형상입니다.

 

 

 

  IP_OCTET1 추출 속성을 생성하고 파티셔닝을 적용하여 파티션 단위의 Equal 조인이 가능하게 되었습니다. 크로스 조인 결과 데이터 셋도 당연히 크기가 감소하게 됩니다.

  하지만 형식상 Equal 조인 연산 일뿐, 매핑되는 데이터가 N:M이므로 사실상 파티션 별 크로스 조인입니다. 범위가 넓은 M:N 매핑이라 Equal 조인에 의한 부하 감소는 크지 않습니다. 실제 운영환경에서도 잡 실패가 발생할 만큼 완벽하게 부하를 제거했다고 볼 수는 없는 아키텍처입니다.

 

  특히 파티션 키 컬럼인 IP_OCTET1 컬럼의 데이터 분포를 확인해보면 파티셔닝 전략이 성공적이었다고 볼 수는 없을 것입니다.

 

 

  파티션 별 데이터 건수의 편차가 너무 큰 관계로 데이터가 몰려있는 파티션 데이터를 할당받은 태스크는 부하가 아주 클 것이라고 예상하실 수 있을 것입니다.

 

 

 

 

[ 튜닝 후 ]

 

> 국가별IP주소대역 테이블 아키텍처 전략

  아래 그림의 예제처럼 기존의 '국가별IP주소대역' 테이블 내 데이터는 국가별 IP 주소 값이 시작IP와 끝IP로 축약되어 있습니다. 시작IP와 끝IP 사이에는 무수히 많은 IP가 존재할 수도 있는데도 말입니다.
  축약되어 표현된 데이터로 '국가별IP주소대역' 테이블의 건수는 적지만 IP주소와 매핑 시 크로스 조인으로 인하여 대량의 조인 결과 데이터 셋이 생성되게 됩니다.

 

 

 

 

  대량의 조인 결과 데이터 셋을 방지하는 방법은 '국가별IP주소대역' 테이블에서 축약되어 표현된 IP주소 정보를 풀어서 다수의 로우로 더 상세하게 표현하는 것입니다. 시작IP와 끝IP 사이에 존재하는 IP대역 정보를 세분화하여 각각의 로우로 저장을 하는 것이죠.

 

 

 

 

  위 그림의 예제 '국가별IP주소대역' 테이블의 첫 번째 로우의 시작IP는 '103.106.140.0'이고 끝IP는 '103.106.143.255'입니다. 이 두 IP 사이에는 '103.106.140.x', '103.106.141.x', '103.106.142.x', '103.106.143.x'의 IP가 존재하게 됩니다.

  이 IP 주소를 '옷뎃1.옷뎃2.옷뎃3' 단위로 세분화하여 각각의 로우로 저장할 수 있습니다.
  세분화된 각 로우의 시작IP와 끝IP는 옷뎃1, 옷뎃2, 옷뎃3의 값이 동일하기 때문에 '옷뎃1.옷뎃2.옷뎃3' 형태의 IP_OCTET123라는 추출 속성을 만들 수 있습니다.

  세분화 단위에서 옷뎃4는 왜 제외되었는지 궁금해 하실 수 있습니다. 국가별IP주소대역 데이터를 보면 거의 모든 데이터가 옷뎃3 단위로 구분되어 있으며, 옷뎃4 단위로 데이터가 구분되는 경우는 미비합니다.
  옷뎃3까지의 데이터 만으로도 IP주소와 매핑 시 거의 모든 데이터가 1:1로 매핑될 수 있습니다.
  옷뎃4 단위로 즉, 전체 IP 주소의 국가 정보를 저장하게 된다면 '국가별IP주소대역_확장' 테이블 데이터 건수의 256배의 데이터를 저장해야 하는데, 이는 쿼리 수행 시 액세스 대상 데이터의 크기를 증가시키는 원인이 될 수 있습니다.
  위와 같은 이유로 '국가별IP주소대역_확장' 테이블은 옷뎃3 단위로 데이터를 세분화하였습니다.

 

  위와 같은 방법으로 '국가별IP주소대역_확장' 테이블을 생성하면 IP_OCTET123 컬럼을 조인 키 컬럼으로 사용할 수 있게 됩니다.

 

 



  IP_OCTET123 컬럼을 조인 키 컬럼으로 사용하게 되면 Equal 조인 시 두 테이블 데이터 간 매핑 정확도가 높아지게 됩니다. 이로 인하여 조인 결과 데이터 셋의 크기가 감소되고 태스크에서 처리해야 할 작업량이 줄어들어 성능 부하도 자연스럽게 감소됩니다.

 

 

 

  또한, 파티션 별 데이터 건수는 아래와 같이 거의 고르게 분포되어 있습니다. 파티션 별 데이터 건수의 편차가 거의 없기에 특정 태스크에 작업이 몰리는 Skew가 발생할 가능성이 더 낮아지게 됩니다.

 

 

  참고로, 파티션 키 컬럼은 IP_OCTET1으로 '국가별IP주소대역' 테이블과 동일합니다. 건수가 적은 테이블이고 테이블 당 파티션 수에 대한 제약이 존재하여 파티션 키는 동일하게 관리하였습니다.

 

 


> 튜닝 후 쿼리

 

  튜닝 후 쿼리는 아래와 같습니다.

INSERT INTO TEST_DW.ACC_IP_CNTRY_MAPP PARTITION (ACC_ID, ACC_START_DT)
SELECT H.IP_ADDR
     , G.CNTRY_CD
     , G.CNTRY_NM
     , H.ACC_ID
     , H.ACC_START_DT
  FROM (
        SELECT IP_ADDR
             , COALESCE(SPLIT(IP_ADDR,'\\.')[0], '_')   AS IP_OCTET1
             , CAST( SPLIT(IP_ADDR,'\\.')[0] AS BIGINT) * 16777216 +
               CAST( SPLIT(IP_ADDR,'\\.')[1] AS BIGINT) * 65536 +
               CAST( SPLIT(IP_ADDR,'\\.')[2] AS BIGINT) * 256 +
               CAST( SPLIT(IP_ADDR,'\\.')[3] AS BIGINT) AS IP_ADDR_NUMT
             , ${ACC_ID}         AS ACC_ID
             , ${ACC_START_DT}   AS ACC_START_DT
             , CONCAT( SPLIT(IP_ADDR,'\\.')[0], '.', SPLIT(IP_ADDR,'\\.')[1], '.', SPLIT(IP_ADDR,'\\.')[2] ) AS IP_OCTET123_DIV
          FROM TEST_ODS.ACC_LOG
         WHERE ACC_ID       = ${ACC_ID}
           AND ACC_START_DT = ${ACC_START_DT}
         GROUP BY IP_ADDR
     ) H
 INNER JOIN
       (
        SELECT IP_OCTET1
             , IP_OCTET123_DIV
             , PART_START_IP_NUMT
             , PART_END_IP_NUMT
             , CNTRY_CD
             , CNTRY_NM
          FROM TEST_DW.CNTRY_IP_RANGE_EXTENT
     ) G
    ON H.IP_OCTET1       = G.IP_OCTET1
   AND H.IP_OCTET123_DIV = G.IP_OCTET123_DIV 
 WHERE H.IP_ADDR_NUMT BETWEEN G.PART_START_IP_NUMT AND G.PART_END_IP_NUMT
;

 

  접속IP주소(IP_ADDR) 별로 국가정보(CNTRY_CD와 CNTRY_NM)를 매핑하기 위해 아키텍처를 개선한 '국가별IP주소대역_확장'(CNTRY_IP_RANGE_EXTENT) 테이블을 조인합니다.
  이때, 조인 키 컬럼으로는 파티션 키 컬럼인 IP_OCTET1과 '옷뎃1.옷뎃2.옷뎃3' 정보를 저장한 IP_OCTET123_DIV 컬럼을 사용하게 됩니다.
  IP_OCTET123_DIV 컬럼을 조인 키 컬럼으로 사용하게 되어 거의 모든 데이터가 1:1로 매핑되고 조인 결과 데이터 셋의 크기가 감소되어 쿼리 성능이 향상되게 됩니다.

 

 


> 국가별IP주소대역_확장 테이블 데이터 만들기

  이번 포스트의 핵심은 '국가별IP주소대역' 테이블을 이용해 '국가별IP주소대역_확장' 테이블 데이터를 만드는 것이라고 볼 수 있습니다.

  '국가별IP주소대역_확장' 테이블 데이터를 만드는 쿼리문은 아래 Github에 업로드 해놨습니다.

 

* Github url : https://github.com/sparkdia/DB_Tuning/tree/master/IP_with_countries_mapping

  참고로 '국가별IP주소대역_확장' 테이블 데이터를 생성하는 쿼리 작업이 크로스 조인으로 인해 부하가 있을 수 있습니다.
  옷뎃별 확장되는 데이터 건수(OCTET2_GAP, OCTET3_GAP)나 파티션 키 컬럼(IP_OCTET1) 기준으로 Insert 작업을 분할하여 수행하는 Shell Script도 업로드해놨으니 참고 바랍니다.

 

  긴 글 읽어주셔서 감사드리고요. 하이브에서의 튜닝 케이스였지만 테이블 아키텍처 변경 작업을 통한 성능 개선이므로 다른 RDBMS에서도 적용이 가능할 겁니다.

  혹시 내용상의 오류나 궁금한 사항이 있으시면 친절한 댓글 부탁드립니다.

감사합니다.  

  

 

  CentOS 환경에서 Hive 설치 작업을 진행해보겠습니다. 설치할 Hive를 포함하여 사전에 설치된 프로그램들의 목록과 버전은 아래와 같습니다.

- Hive : 2.3.6
- Hadoop : 2.10.0

- Java : JDK 7
- MySQL : 5.7.28
- OS : CentOS 8 (VirtualBox 6에서 구동 중)

  Hive 설치에 앞서 우선 Hadoop과 MySQL 설치 작업이 완료되어야 합니다. 구성되기 전이라면 아래 포스트를 참고하여 Java, Hadoop 및 MySQL을 설치 작업을 먼저 진행해주시면 됩니다.

> JDK 7 설치하기 (CentOS 8 환경)
> 하둡(Hadoop) 설치하기[#1] - 설치 준비
> 하둡(Hadoop) 설치하기[#2] - 하둡 환경 설정하기
> 하둡(Hadoop) 설치하기[#3] - 데이터 노드 생성 및 하둡 실행
> MySQL 5.7 설치 (CentOS 8)


 

1. Hive 다운로드 및 압축해제

 

  사전 설치 작업이 완료되었으면 본격적으로 Hive를 설치해 보겠습니다. 우선 Hive 홈페이지에 접속해서 설치 파일을 다운로드합니다.

* URL : https://hive.apache.org/downloads.html

  Hive의 다운로드 홈페이지에 접속을하면 페이지 중간에 파란 글씨로 'Download a release now!'라는 문장을 확인 할 수 있을 것입니다. 해당 문장을 접속하면 아래와 같이 Hive 설치 파일을 다운로드 받을 수 있는 Mirror 사이트 중 사용자 접속 지역과 가까운 Mirror 사이트 목록이 나열되어 있을 것입니다. 이 중에서 한 곳을 임의로 선택해서 들어가봅니다.

  필자는 mirror.apache-kr.org 사이트를 선택했는데, 다운로드 가능한 Hive 버전별 디렉토리 목록을 확인 할 수 있으며, 이 중 설치하고자 하는 hive 버전을 선택해서 클릭하겠습니다. 현재 Hive의 최신 버전은 3.1.2이지만, 필자는 MR 테스트를 목적으로 Hive 설치 작업을 진행하기해 Hive-2.3.6 버전을 선택하고자 합니다.

  각 Hive 버전별 디렉토리 안에는 컴파일되어 실행가능한 바이너리 파일(apache-hive-2.3.6-bin.tar.gz)과 소스 파일(apache-hive-2.3.6-src.tar.gz)이 존재합니다. 이번 설치 과정에서는 컴파일 없이 직접 설치작업을 진행할 것이므로 바이너리 파일을 선택하여 다운로드하겠습니다. 로컬 PC가 아닌 서버에서 wget 명령을 사용하여 다운로드 할 계획이므로 파일의 링크 주소를 복사합니다.(파일명에서 오른쪽 마우스 버튼을 클릭하여 나타난 팝업 메뉴에 링크 주소 복사 메뉴가 존재할 것입니다.)

 

  다운로드 링크 주소를 복사하였으면 아래와 같이 서버에서 wget을 이용해 설치파일을 다운로드 합니다.

>  wget http://mirror.apache-kr.org/hive/hive-2.3.6/apache-hive-2.3.6-bin.tar.gz

 

  다운로드된 파일은 압축된 상태이기 때문에 tar 명령으로 압축을 해제하도록 합니다. 편의를 위해 압축 해제 경로를 설치 디렉토리인 '/usr/local'로 지정하였습니다.

> sudo tar -zxvf ./apache-hive-2.3.6-bin.tar.gz -C /usr/local/

  설치 디렉토리로 지정한 /usr/local 디렉토리는 root 소유이기 때문에 압축 해제된 파일의 소유자가 root로 되어 있습니다. 이를  Hive 운영 사용자 계정으로 소유자와 그룹을 변경해야 합니다.

> sudo chown -R hduser:hadoop ./apache-hive-2.3.6-bin/
> sudo mv apache-hive-2.3.6-bin/ hive-2.3.6

  추가로 hive 디렉토리명이 너무 긴 관계로 필자는 사용상 편의를 위해 hive-2.3.6으로 이름을 변경하였습니다. 물론 필수 작업은 아니며 추후 환경설정 작업 시 올바른 경로만 입력해주면 됩니다.

 

 

2. OS 사용자 환경 설정

  Hive 구동을 위해 $HIVE_HOME 환경 변수를 설정해줘야 합니다. 사용자 홈 디렉토리에 .bashrc파일을 열어 $HIVE_HOME 변수를 추가하고 PATH에 $HIVE_HOME/bin 경로도 추가해줍니다.

> vi ~/.bashrc
export HIVE_HOME=/usr/local/hive-2.3.6

PATH="$HOME/.local/bin:$HOME/bin:$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:$HIVE_HOME/bin"
export PATH

 

 

 

3. HDFS 디렉토리 생성

  Hive에서 테이블을 생성하기 위해서는 사전에 HDFS에 /tmp와 /user/hive/warehouse 디렉토리가 생성되어야 합니다. 생성된 디렉토리에는 추가로 그룹 쓰기 권한도 설정이 필요합니다.

> hadoop dfs -mkdir /tmp
> hadoop dfs -mkdir -p /user/hive/warehouse
> hadoop dfs -chmod g+w /tmp
> hadoop dfs -chmod -R g+w /user

 

4. Hive 설정

  hive의 기본 설정 값은 $HIVE_HOME/conf 디렉토리에 존재하는 hive-default.xml.template 파일에서 확인할 수 있습니다. 기본 설정 값을 변경하여 적용하고 싶다면 hive-site.xml 파일을 작성하여 설정값을 재정의하면 됩니다.

  이번 설치작업은 테스트 용도이므로 Metastore에 대한 설정 작업만 진행할 계획이며, Metastore에 관한 설정 변경 사항은 다음 단계(5.4. hive-site.xml 설정)에서 확인하도록 하겠습니다.

 

5. Metadata Store (MySQL) 설정

  Hive에서 메타데이터를 관리하기 위해 기본적으로 임베디드 데이터베이스인 Derby를 사용하고 있습니다. 만약, 다른 DBMS를 Metastore로 사용하기 위해서는 당연히 DBMS와 JDBC Connector를 먼저 설치해야 합니다. 설치가 완료되면 metastore로 사용한 데이터베이스와 사용자 계정을 생성하고 hive-site.xml 설정 정보를 변경해줘야 합니다.

5.1. MySQL 설치

  여기서는 Metastore로 MySQL 5.7.28을 사용하려고 합니다. CentOS 8에서 Mysql을 설치하는 방법은 아래 포스팅을 참고합니다.

> MySQL 5.7 설치 (CentOS 8)

 

MySQL 5.7 설치 (CentOS 8)

CentOS 환경에서 Hive 설치&테스트를 위해서 MySQL 설치가 선행되어야 했었다. MySQL 사용이 주목적이 아닌 관계로 yum을 이용해 최신 버전의 MySQL(8.0.17)을 쉽게 설치할 수 있었지만, 지금은 Hive metastore로..

sparkdia.tistory.com

 

5.2. MySQL내 Metastore 데이터베이스 생성

  mysql에 접속하여 Metastore로 사용할 데이터베이스와 계정을 생성해보겠습니다.
  Metastore로 사용할 데이터베이스명은 'metastore'로 정의하였으며, Hive의 기본 캐릭터셋이 utf8이므로, 생성할 데이터베이스의 기본 캐릭터셋도 utf8로 설정합니다. metastore 사용을 위해 별도의 mysql 사용자 계정 hive를 생성하였고, 생성된 데이터베이서 metastore에 대한 모든 권한을 부여해주었습니다.

mysql> CREATE DATABASE metastore DEFAULT CHARACTER SET utf8;
mysql> CREATE USER 'hive'@'%' IDENTIFIED BY '비밀번호';
mysql> GRANT ALL PRIVILEGES ON metastore.* TO 'hive'@'%';
mysql> FLUSH PRIVILEGES;

  데이터베이스 및 사용자 생성이 정상적으로 완료되었으면 위와 같은 화면을 볼 수 있을 것입니다. 추가적으로 Mysql의 메타 정보를 조회하여 확인하는 방법도 존재합니다.

  MySQL 사용자 계정은 mysql.user 테이블을 조회하면 됩니다.

mysql> SELECT Host,User FROM mysql.user;

  MySQL 데이터베이스 목록은 INFORMATION_SCHEMA.SCHEMATA 테이블을 조회하면 됩니다.

mysql> SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME 
       ->  FROM INFORMATION_SCHEMA.SCHEMATA; 

 

 

5.3 MySQL JDBC Connector/J 설치

  Hive에서는 JDBC를 이용해 MySQL로 액세스를 하게 됩니다. 따라서, 설치된 MySQL 버전에 적합한 MySQL JDBC Connector/J를 다운로드 받아서 설치해야 합니다. 각 Connector/J 버전 별 지원 내역은 아래 URL에서 확인 가능하며, 필자는 JDK 7 환경이기 때문에 5.1 버전의 Connector/J를 다운로드 받아 설치할 것입니다.

* URL : https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-versions.html

  JDBC Connector/J는 아래 사이트에서 다운로드 가능하다. 다운로드를 위해서는 Oracle 로그인이 필요합니다.

* URL : https://downloads.mysql.com/archives/c-j/

  다운로드 페이지에서 파일 링크 주소를 복사한 후 서버에서 wget을 이용해 파일을 다운로드합니다.

> wget https://downloads.mysql.com/archives/get/p/3/file/mysql-connector-java-5.1.47.tar.gz

  다운로드된 파일은 압축된 상태이므로 tar 명령어를 이용해 압축을 해제합니다.

> tar -zxvf ./mysql-connector-java-5.1.47.tar.gz

  압축해제가 완료되면 여러 파일을 확인할 수 있는데, 이중 파일명이 *-bin.jar인 파일만 사용하면 됩니다.

  해당 파일을 Hive의 라이브러리 파일들이 저장되어 있는 $HIVE_HOME/lib로 이동해놓겠습니다.

> mv mysql-connector-java-5.1.47-bin.jar /usr/local/hive-2.3.6/lib

 

5.4. hive-site.xml 설정

  Metastore DB를 MySQL로 사용하기 위해서는 hive-site.xml에서 JDBC Connection 정보 설정이 필요합니다. $HIVE_HOME/conf 디렉토리 내 hive-site.xml 파일을 vi를 이용해 아래와 같이 편집하겠습니다.

<configuration>
  <property>
    <name>javax.jdo.option.ConnectionURL</name>
    <value>jdbc:mysql://localhost:3306/metastore?createDatabaseIfNotExist=true</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionDriverName</name>
    <value>org.mysql.jdbc.Driver</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionUserName</name>
    <value>hive</value>
  </property>
  <property>
    <name>javax.jdo.option.ConnectionPassword</name>
    <value>비밀번호</value>
  </property>
</configuration>

 

5.5 Metastore 초기화

  MySQL 연결 작업이 완료되었으면 schematool을 이용하여 메타정보 초기화 작업을 진행해야 합니다.

> bin/schematool -dbType mysql -initSchema

  메타정보 초기화 작업이 정상적으로 완료되었으면, MySQL의 metastore 데이터베이스에 아래와 같이 메타 정보 관리를 위한 테이블이 생성되었음을 확인할 수 있을 것입니다.

 

 

6. Hive 실행 및 테스트

  이제 Hive 설치 작업은 완료되었으며, 본격적으로 Hive를 실행하여 테스트를 진행해 보겠습니다..

  hive를 실행하여 아래와 같이 데이터베이스를 생성하고 확인하는 기본 명령어를 실행해 보았습니다.

hive> create database test;     -- test 데이터베이스 생성
hive> show database;

 

  테이블 생성도 테스트 해보겠습니다. integer형 col1컬럼과 string형 col2 컬럼을 가진 tab1 테이블을 생성 해보았습니다.

hive> create table test1. tab1 (
       >   col1 integer,
       >   col2 string
       >  );

  생성된 테이블에 테스트용 데이터를 추가해 보았습니다.

hive> insert into table test1.tab1
       > select 1 as col1, 'ABCDE' as col1;

  INSERT 작업은 MapReduce 엔진에의해 처리되며, ResourceManager 웹페이지에 접속해보면 방금 실행된 insert문을 확인 할 수 있을 것입니다.

 

  위에서 입력한 테스트 데이터를 조회해보겠습니다.

hive> select * from tab1;

  방금 입력한 테스트 데이터가 조회된다면 정상적으로 Hive쿼리가 수행될 것입니다.

  참고로, 이렇게 입력된 데이터는 HDFS에서 확인할 수 있습니다. 테이블 생성 시 location 옵션을 설정하지 않았다면 아래와 같은 기본 저장 경로에서 데이터가 저장되어 있을 것입니다.

/user/hive/warehouse/데이터베이스명.db/테이블명

  필자가 생성한 테이블의 데이터베이스명과 테이블명은 각각 'test', 'tab1'이므로 아래 경로에 데이터 파일이 존재하게 됩니다.

/user/hive/warehouse/test.db/tab

  HDFS 내 데이터는 아래와 같이 Nodemanager 페이지 내 'Utilities'>'Browse the file system'메뉴를 클릭하여 표시되는 웹페이지나 확인하거나, 직업 서버에서 hadoop dfs 명령을 이용해서 확인 가능합니다.

 

7. 기타 - Warning 해결

  Hive에서 명령 실행 시 SSL 보안 인증서 관련 문제로 아래와 같은 Warning 문구가 표시되었습니다. 

 Thu Mar 12 16:11:26 KST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

  이 설치 작업은 테스트 용도이므로 SSL을 비활성화하도록 hive-site.xml 설정을 변경 해주었습니다. 아래 그림에서와 같이 javax.jdo.option.ConnectionURL 설정 값 뒤에 SSL을 비활성한다는 &useSSL=false 설정을 추가 해주었습니다.

 

 

  필자가 생각하는 Hive에서의 데이터 아키텍처 최적화 방법은 아래와 같이 3가지로 구분될 수 있습니다.

Hive에서 데이터 아키텍처 최적화 방법

 

  • 많은 데이터는 적게 만들어라
  • 적은 데이터를 많게 만들어라
  • Skew는 분할처리하라.

 

  이 포스트에서는 첫 번째 Case인 '많은 데이터는 적게 만들기' 방법으로 튜닝한 예제에 대해 설명하고자 합니다.

  먼저 튜닝 결과는 아래와 같습니다.

  리듀스의 수가 크게 증가한 것에 의문을 가지시는 분들이 계실 수도 있는데요. 이 부분에 대해서는 아래에서 궁금증을 풀어보도록 하겠습니다.

 

 

 

[ 튜닝 전 ]

 

  먼저 튜닝 전 쿼리를 살펴보도록 하겠습니다.

SELECT BASE_DT
     , MAX(ORG_CNT)                AS ORG_CNT
     , MAX(CAL_CNT)                AS CAL_CNT
     , MAX(ORG_CNT) - MAX(CAL_CNT) AS GAP_CNT
  FROM (
        SELECT '${BASE_DT}'        AS BASE_DT
             , COUNT(DISTINCT UID) AS ORG_CNT
             , 0                   AS CAL_CNT
          FROM ACT_TXN_DD
         WHERE BASE_DT BETWEEN TO_CHAR(DATE_ADD(FROM_UNIXTIME(UNIX_TIMESTAMP('${BASE_DT}', 'yyyyMMdd'), 'yyyy-MM-dd'), -179), 'yyyyMMdd') 
                           AND '${BASE_DT}'
         UNION ALL
        SELECT BASE_DT
             , 0                   AS ORG_CNT
             , COUNT(DISTINCT UID) AS CAL_CNT
          FROM ACT_TXN_180_SUM
         WHERE BASE_DT = '${BASE_DT}'
         GROUP BY BASE_DT
     ) A
 GROUP BY BASE_DT
;

  * 참고
   - BASE_DT는 파티션 키 컬럼입니다.
   - 위 조건절에 사용된 TO_CHAR 함수는 Hive 내장 UDF가 아니라 시스템 운영상 필요해 의해 만들어진 진짜 사용자 정의 UDF입니다.

 

  위 쿼리의 튜닝 포인트는 2가지가 존재합니다.

* Tuning Point #1 : 조건절 내 사용자 정의 UDF 제거

  사용자 정의 UDF는 Hive의 옵티마이저가 제대로 인식할 수 없는 UDF라고 볼 수 있습니다. 따라서 옵티마이저가 실행 계획을 수립할 때 사용자 정의 UDF는 비용 계산에서 제외됩니다.
  이러한 사용자 정의 UDF가 where 조건절에 사용되어 추출 데이터의 건수를 상당히 줄인다 해도 옵티마이저의 쿼리 수행 비용 계산 시에는 반영되지 않는 겁니다.

  특히 위 쿼리에서처럼 파티션 키 컬럼(BASE_DT)의 필터링 조건으로 사용자 정의 UDF가 사용되는 경우에 성능상의 큰 이슈가 발생할 수 있습니다. 파티션 키 컬럼의 필터링 조건이 존재하지만 Partition pruning을 시도하지 못하고 전체 테이블의 데이터를 읽어야(Full table scan) 하기 때문입니다.

  위 쿼리에서는 Partition pruning을 위해서 where 조건절에 사용된 사용자 정의 UDF를 제거하는 것이 필요합니다. 

 

  참고로 Hive에서 Partition pruning을 판단할 수 있는 방법은 아래 포스트에서 확인하실 수 있습니다.

 

Hive 튜닝 기본 -실행계획에서 Partition pruning 확인하기

Hive에서도 아래와 같이 'EXPLAIN' 명령문으로 쿼리 실행계획을 확인할 수 있습니다. hive> EXPLAIN 쿼리문; Hive의 실행계획은 다른 DBMS에 비해 실행계획의 가독성이 떨어진다는 점이 참으로 안타까운데

sparkdia.tistory.com

 

 

* Tuning Point #2 : DISTINCT COUNT 연산 부하

  COUNT(DISTINCT 컬럼) 함수는 '컬럼'내 중복을 제외한 데이터의 건수를 계산합니다.
  전체 데이터에서 중복이 제거된 건수를 계산해야 하므로 이 작업은 단 하나의 리듀스 태스크가 처리하게 됩니다.

  위 쿼리의 리듀스 태스크 수가 3개였습니다.

  - 서브쿼리 내 UNION ALL 절 상단의 쿼리
  - 서브쿼리 내 UNION ALL 절 하단의 쿼리
  - 최상위 쿼리

  데이터의 건수와는 상관없이 각 쿼리를 수행하는 Staage 별로 단 1개의 리듀스 태스크가 할당이 되어 집계 함수 연산을 수행하는 것입니다.

  큰 작업을 여러 개의 맵과 리듀스가 나눠서 빠른 시간 내에 처리하는 것이 하둡의 기본 사상이자 장점입니다.
  1개의 리듀스로 집계 함수 연산을 수행한다는 것은 하둡의 장점인 분산 처리의 이점을 전혀 활용하지 못하는 경우입니다. 

  위 쿼리에서는 다수의 리듀스가 연산을 처리할 수 있도록 COUNT(DISTINCT 컬럼) 연산의 부하 감소가 필요합니다.

 

 

 

 

[ 튜닝 후 ]

 

  아래는 튜닝 전 쿼리에서 부하를 유발한 사용자 정의 UDF를 제거하고 COUNT DISTINCT의 부하를 감소시킨 쿼리입니다.

SELECT BASE_DT
     , MAX(ORG_CNT)                AS ORG_CNT
     , MAX(CAL_CNT)                AS CAL_CNT
     , MAX(ORG_CNT) - MAX(CAL_CNT) AS GAP_CNT
  FROM (
        SELECT '${BASE_DT}'        AS BASE_DT
             , COUNT(DISTINCT UID) AS ORG_CNT
             , 0                   AS CAL_CNT
          FROM (
                SELECT BASE_DT
                     , UID
                  FROM ACT_TXN_DD
                 WHERE BASE_DT BETWEEN DATE_FORMAT(DATE_ADD(FROM_UNIXTIME(UNIX_TIMESTAMP('${BASE_DT}', 'yyyyMMdd'), 'yyyy-MM-dd'), -179), 'yyyyMMdd') 
                                   AND '${BASE_DT}'
                 GROUP BY BASE_DT, UID
             ) SUB
         UNION ALL
        SELECT BASE_DT
             , 0                   AS ORG_CNT
             , COUNT(DISTINCT UID) AS CAL_CNT
          FROM ACT_TXN_180_SUM
         WHERE BASE_DT = '${BASE_DT}'
         GROUP BY BASE_DT
     ) A
 GROUP BY BASE_DT
;

 

 

* Tuning Point #1 : 조건절 내 사용자 정의 UDF 제거

  파티션 키 컬럼인 BASE_DT의 필터링 조건을 사용자 정의 UDF가 아닌 Hive 내장 UDF인 DATE_FORMAT으로 변경하였습니다.

WHERE BASE_DT BETWEEN TO_CHAR(DATE_ADD(FROM_UNIXTIME(UNIX_TIMESTAMP('${BASE_DT}', 'yyyyMMdd'), 'yyyy-MM-dd'), -179), 'yyyyMMdd') AND '${BASE_DT}'
WHERE BASE_DT BETWEEN DATE_FORMAT(DATE_ADD(FROM_UNIXTIME(UNIX_TIMESTAMP('${BASE_DT}', 'yyyyMMdd'), 'yyyy-MM-dd'), -179), 'yyyyMMdd') AND '${BASE_DT}'

  Hive 내장 UDF를 사용하였기에 옵티마이저는 BASE_DT 컬럼의 필터링 연산을 실행 계획에 반영할 수 있습니다.
  필터링 연산이 적용되므로 Partition pruning이 발생하여 액세스 대상 데이터의 건수의 감소로 쿼리 성능이 향상되게 됩니다.

 

 

* Tuning Point #2 : DISTINCT COUNT 연산 부하

  앞서 DISTINCT COUNT 연산은 오직 하나의 리듀스가 처리한다고 했습니다.
  분산 처리가 불가능한 상황이므로, 리듀스 연산 시간을 단축하는 방법은 리듀스의 입력 데이터 건수를 줄이는 것입니다.

  튜닝 전 쿼리를 다시 한번 살펴보겠습니다.

 

  중복된 UID가 존재하는 180일간의 데이터를 읽어 중복되지 않은 UID의 건수를 계산합니다. 하나의 리듀스가 180일간의 데이터를 모두 처리므로 실행 속도가 느려지게 됩니다.

 

  DISTINCT COUNT 연산을 수행하는 리듀스의 입력 데이터를 줄이는 방법사전에 입력 데이터의 중복을 제거하는 것입니다.

  서브 쿼리 안에서 GROUP BY BASE_DT, UID 집계 연산으로 중복이 제거된 중간 데이터 결과 셋이 생성됩니다. 
  DISTINCT COUNT 연산을 처리하는 리듀스는 입력으로 크기가 감소된 중간 데이터 결과 셋을 사용하게 됩니다.

  위 GROUP BY 연산은 다수의 리듀스가 작업을 분배하여 처리합니다.
  하나의 리듀스가 처리하던 중복제거 작업을 다수의 리듀스가 처리하게 되는 겁니다. 작업의 분산 처리가 가능하니 당연히 연산 속도는 빨라지게 됩니다.

 

 

 

다시 한번 첫 번째 Hive에서의 데이터 아키텍처 최적화 방법을 요약 정리하면 아래와 같습니다.

최적화 방법 첫번째 : 많은 데이터는 적게 만들어라.

1. 액세스 대상 테이블 데이터 건수 감소 (Partition Pruring이 적용)
2. 집계 함수 연산 대상 데이터 건수 감소 (사전 Group by 연산 수행)
 

 

 

  하이브에서는 다양한 형식의 파일을 읽고 저장할 수 있도록 테이블 별로 파일 형식을 지정할 수 있습니다.

 

 

하이브(Hive) 파일 형식

하이브에서는 테이블 생성 시 'STORED AS 저장형식명' 옵션을 사용하여 테이블별로 파일 형식을 지정할 수 있습니다. 파일 형식을 그냥 쉽게 표현하자면 하이브에서 테이블 형태로 보여지는 데이��

sparkdia.tistory.com

  하이브에서 기본적으로 제공하는 파일 형식 중에서 현재 가장 많이 사용되는 것이 ORC 파일 형식입니다.

  ORC 파일 형식은 컬럼 기반으로 데이터를 저장하며, 기존의 컬럼 기반 저장 방식을 사용하는 RCFile을 개선한 파일 형식입니다. ( ※ 컬럼 기반 저장 방식은 위 하이브(Hive) 파일 형식 포스트를 참고 바랍니다. )

 

   RCFile과 비교하였을 때 ORC의 장점은 아래와 같습니다.

- 각 태스크의 결과가 하나의 파일로 생성되어 네임노드의 부하가 감소됨.
- Datetime, Decimal, complex types (struct, list, map, and union)등의 하이브 데이터 타입을 지원함.
- 파일에 경량 인덱스가 저장됨.
. 필터링 조건에 부합하지 않는 로우 그룹은 skip함. 
. 특정 row로 바로 탐색이 가능.
- 데이터 타입 기반의 block-mode 압축. 
. Integer 컬럼은 run-length encoding을 사용함.
. String 컬럼은 dictionary encoding을 사용함.
- 하나의 파일을 여러 개의 레코드리더를 사용해 동시 읽기 가능함
- 마커 스캐닝 없이 파일 분할이 가능함.
- 읽거나 쓰기에 필요한 메모리의 용량을 제한 가능함
- 메타 데이터를 Protocol Buffers를 사용해 저장함으로써 필드의 추가/삭제가 가능함.

 

 

파일 구조

  ORC의 파일 구조는 아래와 같습니다.

 

Stripe

  컬럼 기반 저장 방식에서는 저장 시 테이블의 데이터를 우선 수평적으로 분할합니다. 분할 기준은 로우 그룹의 크기입니다. 분할된 각각의 로우 그룹 내 데이터를 컬럼 기준으로 저장하게 됩니다. 

  이렇게 분할된 로우 그룹이 Stripe이며, Index Data, Row Data, Stripe Footer로 구성됩니다.

 

Index data

  Index data에는 각 컬럼의 최솟값, 최댓값과 각 컬럼 내 로우의 위치를 저장하고 있습니다. 

  필터링 조건에 의해 테이블의 일부 데이터만 추출하는 경우, 이 Index data를 참고하여 조건에 맞지 않은 로우 그룹은 skip하고 조건에 맞는 로우 그룹만 액세스하게 됩니다.

 

Row data

  실제 테이블 데이터가 저장되어 있습니다.

 

Stripe footer

  스트림 위치 정보가 저장되어 있습니다.

 

File footer

  File footer에는 아래와 같은 보조 정보가 저장되어 있습니다.

  • 파일 내 stripe의 목록
  • stripe별 로우의 수
  • 각 컬럼의 데이터 타입
  • 컬럼 레벨별 통계 정보(건수, 최솟값, 최댓값, 합계)

 

Postscript

  파일의 가장 끝에 위치하며, 압축된 footer의 크기와 압축 파라미터 정보가 저장됩니다.

 

 

 

Integer 컬럼 직렬화 (Integer Column Serialization)

  ORC에서 Interger 컬럼은 두 개의 스트림, bit stream(해당 값이 not-null인지 표현)과 data stream(integer의 스트림)으로 직렬화(serialize)됩니다.

  Interger 데이터는 아래와 같이 숫자의 특징에 따른 encoding 방식이 적용되어 직렬화됩니다.

  •   interger 값이 작아 데이터의 크기가 경량인 경우 variable-width encoding을 사용.
  •   값이 반복되는 경우 run-length encoding을 사용.
  •   정수 값의 범위가 -128에서 127인 경우 run-length encoding을 사용.

  

Variable-width encoding 

  Variable-width encoding은 Google의 프로토콜 버퍼 기반입니다.

  가변 폭 데이터를 인코딩하므로 각 Integer 값의 byte 크기는 각각 상이할 것입니다. 인코딩되는 데이터의 크기가 서로 다른 값들을 구분하기 위해 각 byte의 최상위 bit를 사용합니다. 각 byte의 최상위 bit는 현재 byte가 인코딩하는 데이터의 마지막 바이트를 나타냅니다. 하위 7bit는 인코딩된 데이터를 나타냅니다.

 

Run-length encoding

  Run-length encoding은 반복되는 문자를 반복회수로 표현하는 압축 방법입니다.

  예를들어, 문자열 'AAAAABBBCCCCCCCCDDDEEEEEEEE'인 경우에 Run-length encoding을 적용하면 '5A3B7C3D8E'로 인코딩 됩니다.

 

 

 

문자열 컬럼 직렬화 (String Column Serialization)

문자열 컬럼은 컬럼의 unique한 값들로 구성된 딕셔너리를 사용하여 직렬화됩니다. 딕서너리 내 값들은 정렬되어 있어 predicate filtering의 속도가 높아지고 압축률이 향상됩니다.

  문자열 컬럼은 아래 4개의 스트림으로 직렬화됩니다. 

  •   bit stream : 값이 not-null인지 표현
  •   dictionary data : 문자열의 크기(byte)
  •   dictionary length : 각 항목의 길이
  •   row data : 로우의 값 

 

 

압축 (Compression)

  ORC 파일 포맷을 사용하는 테이블의 모든 스트림은 지정된 코덱을 사용하여 압축됩니다. 압축 코덱은 테이블 생성 시 TBLPROPERTIES 옵션 내 orc.compress라는 key 값을 정의하여 지정 가능합니다. 코덱은 ZLIB, SNAPPY을 사용하거나 none을 선택할 수 있습니다.

  아래 Hortonworks(현재는 Cloudera로 흡수되었죠.)에서 성능 테스트를 한 결과를 보면 ORC 파일 형식이 다른 파일 형식에 비해 상당히 높은 압축률을 보여주고 있음을 확인할 수 있니다. 

[출처] https://blog.cloudera.com/orcfile-in-hdp-2-better-compression-better-performance/

 

 

 

[Ref.] https://cwiki.apache.org/confluence/display/Hive/LanguageManual+ORC#LanguageManualORC-HiveQLSyntax

    https://blog.cloudera.com/orcfile-in-hdp-2-better-compression-better-performance/

 

  하이브에서는 테이블 생성 시 'STORED AS 저장형식명' 옵션을 사용하여 테이블별로 파일 형식을 지정할 수 있습니다.

 

  파일 형식을 그냥 쉽게 표현하자면
  하이브에서 테이블 형태로 보여지는 데이터를
  어떤 구조로 HDFS에 저장하며,
  어떤 압축 방식을 사용할 것인지 정의한 저장 방식이라고 보시면 됩니다.

 

  하둡 개발자 입장에서는 이전 포스트(맵리듀스(Map-Reduce) 상세 작동 방법)에서 언급했었던 InputFormat과 OutputFormat을 확장하여 구현한 것이며, 추가로 <key, value> 형태의 데이터를 레코드 형태로 보여주는 SerDe까지 구현 한 라이브러리라고 보시면 이해가 쉬울 것 같습니다.

 

맵리듀스(Map-Reduce) 상세 작동 방법

맵리듀스(Map-Reduce) 데이터 흐름 하둡 맵리듀스는 클러스터 환경에서 대량의 데이터를 병렬로 처리하는 응용 프로그램을 쉽게 작성할 수 있는 소프트웨어 프레임워크입니다. 맵리듀스 작업은 일

sparkdia.tistory.com

 

  파일 형식기본 값TEXTFILE이며(hive.default.fileformat 옵션으로 변경 가능), 그 외에도 하이브에서는 아래와 같이 다양한 파일 형식을 제공하고 있습니다.

TEXTFILE
SEQUENCEFILE
RCFILE
ORC
PARQUET
AVRO  
JSONFILE

  물론, 하이브는 오픈 소스이므로 사용자 정의의 파일 형식도 적용할 수도 있습니다.

  이 포스트에서는 Hive에서 기본적으로 제공하고 있는 파일형식에 대해 살펴보도록 하겠습니다.

 

1. TEXTFILE

  말 그대로 텍스트 파일입니다. 우리가 메모장이나 vi에디터를 통해서 읽을 수 있는 그 텍스트 파일 맞습니다.

  일반적으로 TEXTFILE 파일 형식은 External 테이블 생성 시 사용합니다. 원천 시스템에서 수집한 데이터나 테스트 등의 목적으로 사용자가 생성한 데이터를 Hive에서 조회하기 위해서는 데이터를 텍스트 파일 형태로 HDFS에 저장한 뒤 External 테이블을 생성하면 됩니다.

  텍스트 파일 내 데이터는 구분자로 필드와 로우가 구별되어야 하며, 테이블 생성 시 DELIMITED 옵션을 사용하여 구분자를 명시해주면 됩니다.

[출처] https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-StorageFormats

 

  TEXTFILE은 기본적으로 압축되지 않은 파일형식이기 때문에(GZip같은 툴로 압축될 수는 있습니다.) 다른 파일형식에 비해 저장 공간을 많이 필요로한다는 단점이 있습니다.
  지속적으로 ODS 영역에 보관할 원천 시스템 데이터라면 압축된 파일형식의 테이블을 생성하여 데이터를 이관하여 보관해야 합니다. 압축된 파일형식의 테이블을 사용하는 것이 쿼리 성능과 저장 공간 활용도를 높이는 방법입니다. 

 

 

2. SEQUENCEFILE 

  SEQUENCEFILE 파일 형식은 데이터를 압축하여 저장합니다.

  SEQUENCEFILE 파일 형식은 로우 기반으로 데이터를 저장합니다.

  위 특징 때문에 코드 테이블, 디멘전 테이블처럼 데이터와 컬럼이 적은 테이블에 적용합니다. 

 

 

3. RCFile (Record Columnar File)

  RCFile은 컬럼 기반으로 데이터를 압축하여 저장하는 파일 형식입니다.

  아래 테이블의 데이터를 예를 들어 로우 기반(Row-oriented)컬럼 기반(Column-oriented) 저장 방식 차이점을 살펴보도록 하겠습니다. 

EmpId Lastname Firstname Salary
10 Smith Joe 40000
12 Jones Mary 50000
11 Johnson Cathy 44000
22 Jones Bob 55000

 

  아래 그림은 위 예제 데이터를 로우 기반 테이블과 컬럼 기반 테이블에 각각 저장하는 경우 저장되는 데이터 순서를 나타내는 예제입니다.

  로우 기반 저장 방식은 아래 예제처럼 데이터의 각 로우 내 데이터를 순서대로 저장(Serialize) 합니다. (각 로우별 저장 순서는 테이블 조회 시 보여지는 로우의 순서와 상이할 수 있습니다.)

001:10,Smith,Joe,40000;002:12,Jones,Mary,50000;003:11,Johnson,Cathy,44000;004:22,Jones,Bob,55000;

  위 방식은 로우의 전체 컬럼 데이터를 한번에 액세스 하는 경우에 효과적입니다.

  대표적으로 Oracle이 로우 기반 방식으로 데이터를 저장합니다.

 

  컬럼 기반 저장 방식아래 예제처럼 한 컬럼의 데이터를 먼저 저장(Serialize)한 후 다음 컬럼의 데이터를 저장합니다.

10 : 001,12 : 002,11 : 003,22 : 004; Smith : 001, Jones : 002, Johnson : 003, Jones : 004; Joe : 001, Mary : 002, Cathy : 003, Bob : 004; 40000 : 001,50000 : 002,44000 : 003,55000 : 004;

  만약에 컬럼 내 동일한 데이터가 존재한다면, 아래와 같은 방법으로 동일한 데이터를 한번만 저장하여 저장 공간을 절약할 수도 있습니다.

...; Smith : 001, Jones : 002,004, Johnson : 003; ...

 

  컬럼 기반 저장 방식은 아래와 같은 장점이 존재합니다.

  • 열 단위인 경우 데이터가 더 균일하므로 압축률이 향상됩니다. 압축률의 향상되면 부수적으로 저장 공간이 절약되고 액세스 시 Disk I/O가 감소됩니다.
  • 쿼리 시 쿼리문에 명시된 컬럼이 저장된 블록만 액세스하면 되므로 I/O가 감소됩니다.
  • 다양한 데이터 액세스 패턴을 적용하는게 수월해집니다.

 

 

 

4. ORC (Optimized Row Columnar)

  ORC는 Full name 그대로 RCFile을 개선시킨 파일 형식입니다. 물론 RCFile과 마찬가지로 ORC도 컬럼 기반 저장 방식을 사용합니다.

  현재 파일 형식중에서 가장 많이 사용되는 파일 형식이라고 말할 수 있습니다.

  ORC에 관해서는 정리할 내용이 많은 관계로 아래 별도의 포스트로 작성하였으니 참고 바랍니다.

 

ORC(Optimized Row Columnar) in 하이브(Hive)

하이브에서는 다양한 형식의 파일을 읽고 저장할 수 있도록 테이블 별로 파일 형식을 지정할 수 있습니다. https://sparkdia.tistory.com/56 하이브에서 기본적으로 제공하는 파일 형식 중에서 현재 가장

sparkdia.tistory.com

 

 

5. PARQUET 

  PARQUET도 컬럼 기반 저장 방식을 사용하는 파일 형식입니다.

  하이브와 피그만 지원되는 ORC에 비해 더 다양한 하둡 에코 시스템을 지원하고 있습니다.

  • Apache Hive
  • Apache Drill
  • Cloudera Impala
  • Apache Crunch
  • Apache Pig
  • Cascading
  • Apache Spark

  그리고 PARQUET는 Google Protocol Buffers와 비슷한 모델을 사용합니다. 이로 인해 복잡하게 중첩된 데이터 구조(List, Maps, Sets)를 효율적으로 저장할 수 있습니다. (참고 : Dremel made simple with Parquet)

 

 


6. AVRO  

  아파치 에이브로(AVRO)는 데이터 직렬화(Serialization) 시스템입니다.
  하이브에서는 이 에이브로의 파일을 하이브에서도 액세스 할 수 있도록 AVRO SerDe를 제공해줍니다.

  에이브로는 하둡의 직렬화 방식(Writable)방식의 단점인 언어 이식성을 해결하기 위해 만들어진 프로젝트 입니다. 데이터 파일에 스키마 정보를 같이 저장하므로 사전에 스키마 정보를 알지 못해도 데이터를 읽는 시점에 스키마 정보를 파악할 수 있게 됩니다. 이러한 방식으로 데이터를 관리하므로 다양한 개발 언어를 이용해 데이터 셋을 공유할 수 있는 것입니다.

  상세한 내용은 아파치 에이브로 홈페이지를 참고하시길 바랍니다.

  

 


7. JSONFILE

  Hive 4.0 버전에서부터 JSONFILE 파일 형식도 지원한다고 하네요. 

  기존에는 사용자 정의의 파일 형식을 이용하거나 전처리 과정을 통해 CSV 파일 형태로 만든 뒤 TEXTFILE 파일 형식을 사용하여 데이터를 액세스했었는데, 이런 번거로움이 사라지겠네요.

 

 

  이상으로 하이브의 파일 형식에 대해 알아보았습니다.
  위 내용중에서 추가/보완해야 할 사항있으면 친철한 댓글 부탁드립니다.
  감사합니다.

 

 

[Ref.] https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-StorageFormats
  https://cwiki.apache.org/confluence/display/Hive/FileFormats
  https://cwiki.apache.org/confluence/display/Hive/LanguageManual
  https://en.wikipedia.org/wiki/RCFile
  https://blog.twitter.com/engineering/en_us/a/2013/dremel-made-simple-with-parquet.html
  Hadoop, The Definitive Guide

 

  Hive에서도 아래와 같이 'EXPLAIN' 명령문으로 쿼리 실행계획을 확인할 수 있습니다.

hive> EXPLAIN 쿼리문;

  Hive의 실행계획은 다른 DBMS에 비해 실행계획의 가독성이 떨어진다는 점이 참으로 안타까운데요.
  쿼리 수행 작업을 여러 Stage로 나눠 실행하고, 각 작업 단계에서도 Map과 Reduce 태스크로 일감을 분할해 처리하는 hive의 특성상 장문의 실행계획은 다른 대안이 없는 결과라고 생각합니다.

  또 하나 아쉬운 점은, 파티션 테이블의 스캔 정보가 명확하지 않다는 점입니다.

  오라클의 실행계획을 예를 들어 살펴보면, 'PARTITION RANGE SINGLE'이라는 Operation으로 하나의 파티션만 액세스 된다는 것을 쉽게 알 수 있습니다.

 

  하지만, Hive에서의 실행계획에서는 PARTITION이라는 단어조차 찾아볼 수 없습니다.

  그럼, 내가 작성한 쿼리문이 '테이블의 전체 데이터를 액세스 하는지?' 아니면 '조건에 맞는 파티션만 액세스(이하 Partition puruning)를 하는지?' 확인할 수 있는 방법은 없는 건가요?

  방법은 두 쿼리 문의 실행계획 비교입니다.

 

  첫 번째, WHERE 조건절 없이 전체 테이블 데이터를 SELECT 하는 쿼리의 실행계획을 확인해봅니다.

hive> EXPLAIN SELECT UID FROM ACT_TXT_DD;

  위 실행계획에서 ACT_TXT_DD 테이블의 통계정보(Statistics)를 살펴봅니다.
  로우 수(Num rows)가 1,199,025건이고 데이터 크기(Data size)가 2,053,929,825 Byte이네요.

 

  두 번째, WHERE 조건절에 파티션 컬럼의 필터링 조건이 명시된 SELECT문의 실행계획을 확인해봅니다.

hive> EXPLAIN SELECT UID FROM ACT_TXT_DD WHERE BASE_DT='20200101';

  파티션 컬럼 BASE_DT의 값이 '20200101'인 데이터만 추출하는 SELECT문의 실행계획은 아래와 같습니다.


  통계정보(Statistics) 상의 로우 수(Num rows)는 101,835건이고 데이터 크기(Data size)는 174,443,355 Byte입니다.

  위 WHERE 조건절이 없는 SELECT문의 실행계획과 비교해서 로우 수와 데이터 크기가 적어진 것을 확인할 수 있습니다.
  Partition puruning으로 테이블 스캔 범위가 적어져 위와 같은 통계정보를 출력하게 된 것입니다.

 

  WHERE 조건절에 파티션 키 칼럼 필터링 조건을 입력했더라도, 형 변환 문제나 UDF 사용으로 인해 Partition puruning이 적용되지 않을 수도 있습니다.
  HQL 수행 속도가 매우 느리다면 위와 같은 방법으로 Partition puruning 여부를 확인해 보는 것이 HQL 최적화의 한 방법이 되겠습니다.

 

  Hive 설치 후 간단한 DML 문을 실행하였는데, 아래와 같이 OutOfMemoryError: PermGen space가 발생하였습니다. 실행했던 HQL이 단순하게 하나의 row를 삽입하는 문장이었는데 메모리 부족이 발생하다니, 초기 설정 그대로 테스트를 해보는 것은 욕심이었나봅니다.

 

  우선, 위에서 메모리 부족이라고 알려 준 PermGen space가 무엇인지 궁금해서 찾아보았습니다.
(참고로, 난 java 개발자가 아니라 데이터쟁이라서 Java 아키텍처에 대한 지식이 깊지않습니다. PermGen이 기본 중의 기본이라면 부끄러울 따름입니다.)

  구글링해서 찾은 Perm 영역에 대한 설명을 그대로 옮겨보았습니다.

Java 1.7 기준의 Perm 영역은 보통 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수 정보들이 저장되는 공간이다. 이 영역은 Java 1.8부터는 Native 영역으로 이동하여 Metaspace 영역으로 변경되었다.(다만, 기존 Perm 영역에 존재하던 Static Object는 Heap 영역으로 옮겨져 최대한 GC 대상이 될 수 있도록 하였다.)

조금 더 1.7과 1.8 메모리 구조의 변경에 대해 설명하자면, Java 1.8에서 JVM 메모리의 구조적인 개선 사항으로 Perm 영역이 Metaspace 영역으로 전환되고 기존 Perm 영역은 사라지게 되었다. Metaspace 영역은 Heap이 아닌 Native 메모리 영역으로 취급하게 된다. (이 말은 기존의 Perm은 Heap 안에 존재하는 것으로 알 수 있다.) Native 메모리는 Heap과 달리 OS레벨에서 관리 하는 영역이다.

출처: https://coding-start.tistory.com/205 [코딩스타트]
JDK 7: PermGen

Permanent Generation still exists in JDK 7 and its updates, and is used by all the garbage collectors. In JDK7, the effort to remove the permanent generation was started and some parts of the data residing in the permanent generation were moved to either the Java Heap or to the native heap. Permanent generation was not completely removed and it still exists in JDK 7 and its updates. Here's the list of things that were moved out of the permanent generation in JDK7:

출처 : https://blogs.oracle.com/poonam/about-g1-garbage-collector%2c-permanent-generation-and-metaspace

  간단하게 요약을 해보면 Garbage collector를 위해 Meta 정보를 저장하거나 Static 변수나 상수가 저장되는 공간이라고 합니다.

 

  아무튼, Permanent 공간이 부족하다고 하니 이 영역의 크기를 확장해 줄 필요가 있겠습니다.

  현재 설정된 상태를 살펴봅니다. 아래 명령어를 사용하면 Permanent 크기를 확인 할 수 있습니다.

> java -XX:+PrintFlagsFinal -version -server | grep PermSize

  현재 시스템에서 최대 Permanent 크기가 85,983,232 byte(82MB)로 설정되어 있는 것을 확인 할 수 있습니다.

 

  이 Permanet의 크기를 조정해주기 위해서는 HADOOP_OPTS에 관련 속성 설정 값을 추가해주면 됩니다. Hive 구동시마다 적용되도록 $HIVE_HOME/conf/hive-env.sh에 아래 옵션을 추가해줍니다.

export HADOOP_OPTS="$HADOOP_OPTS -XX:PermSize=512M -XX:MaxPermSize=1024M"

 

  옵션 적용을 위해서 hive 서비스를 재기동합니다.
  그리고 설정이 제대로 적용되었는지 확인하기 위해서 hive 서비스 프로세스를 확인해 보았습니다.

  위에 Hive를 실행하는 자바프로세스 옵션에 우리가 방금 설정한 PermSize와 MaxPermSize가 추가되어 있는 것을 확인할 수 있을 것입니다.
  위와 같이 설정된 상태에서 hive 클라이언트에 접속하여 수차례 Insert 구문을 실행해봐도 에러 없이 정상적으로 수행되었음을 확인 할 수 있을 것입니다.

 

 

 

+ Recent posts