Parquet (파케이)
이번 글은 하둡 생태계에서 많이 사용되는 파일 포맷인 Parquet (이하 파케이)에 대해 정리한다.
글의 포함된 설명 중 많은 부분은 하둡 완벽 가이드에서 발췌한 것임을 밝힌다.
파케이는 columnar 저장 포맷이다. 구글에서 발표한 논문 Dremel: Interactive Analysis of Web-Scale Datasets를 토대로 Twitter, Cloudera에서 같이 개발했다.
columnar 포맷을 기존에 많이 사용하던 Row 기반 포맷과 비교하면 이해하는데 도움이 된다.
전통적인 row 기반 저장 방식은 A1, B1, C1과 같이 같은 row 값이 연속적으로 저장된다.
반면에 columnar 저장 방식은 A1, A2, A3과 같이 컬럼의 데이터가 연속된 구조로 저장한다.
columnar 저장 방식을 개발하게 된 배경은 무엇일까?
파케이 개발에 참여한 Twitter는 대표젹인 SNS 회사로서 대량의 데이터를 HDFS에 저장하고 있었다. HDFS가 낮은 가격의 장비에 빅데이터를 저장하는 용도로 만들어졌다 하더라도 SNS의 데이터를 저장할 때 너무 많은 디스크를 소모하므로 데이터 사이즈를 줄이는 파일 포맷 개발이 필요했다. 그래서 파케이 구조를 살펴보면 작은 파일 사이즈와 낮은 I/O 사용을 목적으로 개발되었음을 알 수 있다.
파케이의 장점을 먼저 살펴보고, 어떻게 장점이 나타났는지 파케이 아키텍처를 분석하자.
장점
- 압축률이 좋다. 컬럼 단위로 구성하면 데이터가 더 균일하므로 압축률이 높아진다.
- 데이터를 가져갈 때, 전체 컬럼 중에서 일부 컬럼을 선택해서 가져가면 I/O가 많이 줄어든다. 컬럼단위로 데이터가 저장되어 선택되지 않은 컬럼의 데이터에서는 I/O가 발생하지 않기 때문이다.
- 컬럼에 동일한 데이터 타입이 저장되기 때문에 컬럼별로 적합한(데이터형에 유리한) 인코딩을 사용할 수 있다.
장점으로 기술된 압축률, I/O가 줄어드는 부분은 전적으로 columnar 구조이기 때문에 가능하다. 컬럼 단위로 데이터를 저장하기에 비슷한 데이터가 나와서 압축의 효과도 높아지고, 일부 컬럼들만 가져오기도 용이한 것이다.
그럼 본격적으로 파케이의 구조를 살펴보자.
자료형
각 필드는 반복자 (required, optional, repeated), 자료형, 이름으로 되어 있다.
필드에 사용 가능한 기본 자료형은 다음과 같다.
타입 | 설명 |
boolean | 바이너리 값 |
int32 | 부호 있는 32비트 정수 |
int64 | 부호 있는 64비트 정수 |
int96 | 부호 있는 96비트 정수 |
float | 단정밀도(32비트) IEEE 754 부동소수점 숫자 |
double | 배정밀도(64비트) IEEE 754 부동소수점 숫자 |
binary | 순차 8비트 부호 없는 바이트 |
fixed_len_byte_array | 고정길이 8비트 부호 없는 바이트 |
주목한 점은 기본 문자열 자료형이 없다. 대신 기본 자료형에 대한 해석 방식을 정의한 논리 자료형을 정의한다.
문자열은 UTF8 어노테이션을 가진 binary 기본 자료형으로 표현된다.
자료형에서 깊게 살펴볼 부분은 논리 자료형을 반복자, 그룹을 사용해 논리적으로 구성하는데 있다.
간단한 타입으로 LIST, MAP을 구현하지 쉬지 않은데, 명세 수준과 반복 수준을 사용해서 중첩 구조로 구현했다. 해당 구조가 구글의 Dremel 논문에 포함된 개념이다.
말로 이렇게 설명하면 이해하기 쉽지 않은데, LIST, MAP를 적용한 스키마를 살펴보자. 그리고 데이터 예제를 통해 명세 수준과 반복 수준이 어떻게 표현되는지 보자.
# List
message m {
required group a (LIST) {
repeated group list {
required int32 element;
}
}
}
# Map
message m {
required group a (MAP) {
repeated group key_value {
required binary key (UTF8);
optional int32 value;
}
}
}
LIST와 MAP은 두 단게 그룹 구조로 만든다. LIST는 element 필드를 가진 반복 그룹으로 표현했다. 동일 타입의 필드가 여러개 있는 필드 list를 가지도록 했다.
MAP도 비슷하게 구현되어 있는데, 키-값 구조이기 때문에 key_value 필드에 key, value가 각각 필드로 포함되었다.
구조를 표현하는 대략적인 개념은 익혔는데, 명세 수준과 반복 수준은 무엇인지 어떻게 표현되는지 다른 예를 살펴보자.
다음과 같은 파케이 스키마가 있다고 하자.
message ExampleDefinitionLevel {
optional group a {
optional group b {
optional string c;
}
}
}
값이 없는 단층 레코드는 null을 사용하고 중첩이나 반복 수준이 올라가면 null이 아닌 값을 사용해서 비트 필드를 인코딩하는 일반적인 기법으로 명세 수준과 반복 수준을 저장한다.
위 구조에 해당하는 예시 값과 명세 수준을 보자. optional로 정의한 필드의 값 유뮤에 따라 명세 수준이 달라진다.
반복 수준은 LIST의 시작값이 어느 곳인지 나타내는 마커의 역할을 한다.
그럼 데이터는 실제 파일로 어떻게 저장될까?
파일 구조
파케이 파일은 헤더, 하나 이상의 블록, 꼬리말 순으로 구성된다. 헤더는 파케이 포맷의 파일임을 알려주는 4바이트 매직 숫자인 PAR1만 포함하고 있다. 파일의 모든 메타데이터는 꼬리말에 저장된다.
꼬리말 데이터는 포맷버전, 스키마, 추가 키-값 쌍, 파일의 모든 블록에 대한 메타데이터와 같은 정보를 포함하고 있다.
파케이 파일의 각 블록은 행 그룹을 저장한다. 행 그룹은 행에 대한 컬럼 데이터를 포함한 컬럼 청크로 되어 있다. 각 컬럼 청크의 데이터는 페이지에 기록된다.
각 페이지는 동일한 컬럼의 값만 포함하고 있다. 따라서 페이지에 있는 값은 비슷한 경향이 있기 때문에 페이지를 압축할 때 매우 유리하다.
데이터의 가장 최소 단위인 페이지에는 동일 컬럼의 데이터만 존재한다. 그래서 인코딩/ 압축을 할 때, 페이지 단위로 수행하면 된다.
위 구조가 머리 속에 그려지면 파케이 파일을 만들 때의 설정 값이 이해된다.
파케이 설정
속성 명 | 타입 | 기본값 | 설명 |
parquet.block.size | int | 128MB | 블록의 바이트 크기 (행 그룹) |
parquet.page.size | int | 1MB | 페이지의 바이트 크기 |
parquet.dictionary.page.size | int | 1MB | 일반 인코딩으로 돌아가기 전의 사전의 최대 허용 바이트 크기 |
parquet.enable.dictionary | boolean | true | 사전 인코딩 사용 여부 |
parquet.compression | String | UNCOMPRESSED | 파케이 파일에서 사용할 압축 종류 - UNCOMPRESSED, SNAPPY, GZIP, LZO |
블록 크기를 설정할 때 스킨 효율성과 메모리 사용률 사이의 트레이드오프 관계를 고려해야 한다. 블록의 크기를 높이면 더 많은 행을 가지므로 순차 I/O의 성능을 높일 수 있어 효율적으로 스캔할 수 있다. 하지만 개별 블록을 읽고 쓸 때 모든 데이터가 메모리에 저장되어야 하기 때문에 너무 큰 블록을 사용하는 것은 한계가 있다.
파이케 블록과 HDFS 블록을 동일하게 설정하는 것이 일반적이며, 실제 두 블록의 기본값은 128MB 이다.
패이지는 파케이 파일의 최소 저장 단위로, 원하는 행을 읽기 위해서는 그 행을 포함한 페이지의 압축을 해제하고 디코딩해야 한다. 단일 행 검색은 페이지가 작을수록 효율적인데, 원하는 값을 찾기 전에 읽어야 하는 값이 더 적어지기 때문이다. 하지만 페이지의 크기가 작으면 필요한 페이지의 수가 늘어남으로써 발생하는 추가적인 메타데이터(오프셋, 사전)로 인해 저장 용량과 처리 시간이 증가하는 단점이 있다.
설정 내용 중에 위 설명에서 언급하지 않은 사전이 있다.
사전은 데이터 인코딩 시에 사용한다. 값의 사전을 만들어 인코딩한 후 사전의 인덱스를 나타내는 정수로 그 값을 저장한다.
파케이는 파일을 기록할 때 컬럼의 자료형을 기준으로 적합한 인코딩을 자동으로 선택한다. boolean을 제외한 대부분의 자료형은 사전 인코딩을 주로 이용한다. 사전의 크기는 페이지의 크기와 관련이 있다. 이유는 한 페이지에 사전이 모두 들어가야 하기 때문인데, 페이지 크기보다 사전이 커지면 일반 인코딩(값을 그대로 기록)으로 대체된다.
파케이의 이론적인 부분을 설명했는데, 실제로 파케이 파일을 만들어보고 만든 파일을 확인하는 실습을 해보자.
파케이 파일 만들기
파케이를 만들려면 일단 스키마가 필요하다. 파케이는 스키마를 포함한 데이터 구조이기 때문이다.
필자는 Avro 프로코톨을 통해서 파케이 파일을 만들 예정이다. Avro를 사용해서 파케이 파일을 만들 경우, AvroParquetWriter를 사용하면 된다.
아래 코드를 보자.
AvroParquetWriter에 전달할 avro schema를 먼저 만든다. schema는 2개의 string, 1개의 int 타입을 갖도록 간단히 구성했다. 그리고 AvroParquetWriter.builder를 통해 미리 만든 schema를 전달했다. 옵션을 설정하는 함수들은 빌더 패턴으로 되어 있는데, 모든 옵션 설정이 끝나면 build로 writer를 생성한다.
생성한 writer는 GenericRecord 타입으로 데이터를 입력받게 했는데, GenericRecord는 avro schema에 의해 만들어진 데이터이다. 임의의 값으로 record를 생성하고 AvroParquetWriter.write(record) 함수를 통해 데이터를 넣는다.
데이터를 다 넣었으면 writer를 close 함으로써 파케이 파일을 만든다.
파케이 파일 검토
생성된 파케이 파일을 확인하기 위해 parquet-tools을 사용한다.
parquet-tools은 parquet 모듈에 포함된 것으로서 CLI를 통해 파일의 스키마, 메타, 데이터를 확인할 수 있게 한다.
GitHub: apache/parquet-mr을 빌드해서 만든 jar 파일로 실행하면 되는데, 필자는 mac 환경에서 brew로 간편하게 설치했다.
// https://formulae.brew.sh/formula/parquet-tools
// parquet-tools 설치
brew install parquet-tools
parquet-tools schema {file}
parquet-tools meta {file}
parquet-tools head {file
위와 같이 brew로 parquet-tools을 간편하게 설치 가능하다.
그럼 parquet-tools로 위에서 만든 파케이 파일을 확인해 보자.
먼저 schema 부터 확인하자.
schema를 살펴보면 정의한 대로 3개의 필드가 보인다. 또한 앞서 설명한 대로 string은 binary가 UTF8로 인코딩된 값임을 알 수 있다.
다음은 메타 정보이다.
메타를 보면 파케이 파일의 버전과 함께 압축 정보도 표시된다.
파케이 파일을 만들 때 별도의 압축 알고리즘을 넣지 않아서 기본값인 UNCOMPRESSED로 되어 있음을 알 수 있다.
그리고 avro schma로 파케이 파일을 만들어서 avro schema도 메타정보에서 확인할 수 있다.
마지막으로 실제 파케이 파일에 있는 데이터를 살펴보자.
parquet-tools에 head 옵션을 주면 일부 데이터를 확인할 수 있다.
이번 글에서는 파케이의 전반적인 개념과 함께 파케이 파일을 생성하고 확인하는 작업까지 해봤다.
실무에서 HDFS에 파케이 파일을 저장하거나 읽어서 가공하는 작업들을 할텐데 파케이의 개념을 숙지하고 작업하면 도움이 되지 않을까 싶다.
참고 문서