Apache Parquet
关于Parquet
传统的,关系型数据的列式存储比较容易处理,但是嵌套类型的列式存储处理起来相当之困难,因为,嵌套类型的数据不仅仅只又基本数据类型,它还可以是List,Map,Set类型,虽然,依然可以快速定位到某一列的值,但如何把他们合并一行数据呢?2010年Google公司发表了一篇名为Dremel: Interactive Analysis of Web-Scale Datasets
的论文,解决了这个问题。
于是Parquet的缔造者们(Twitter和Cloudera的工程师
),基于Dremel的基础之上,开发出了Parquet。
1.Parquet的特点
- Apache Parquet是一种能够有效存储嵌套数据的列式存储格式。
- 由于是列式存储格式,在文件大小和查询性能上表现优秀。
列式存储的优势:
①由于每一列的数据类型相同,所以,可以针对不同数据类型的列进行不同的编码和压缩,大大降低数据存储所占空间。
下图展示了,使用不同存储格式来存储TPC-DS数据集中的两个不同的表数据的文件大小。可以看出,Parquet较其他二进制文件存储格式能够更有效地节省存储空间,尤其是新版的Parquet 2,它使用了更高效的也存储方式。
②读取数据的时候可以把映射下推,只需要读取需要的列,这样可以大大减少每次查询的I/O数据量。
③由于每一列的数据类型相同,可以使用更加适合CPU pipeline的编码方式,减小CPU的缓存失效。 - 灵活性:
①Parquet分为2部分,一部分是以与语言无关的方式来定义文件格式的Parquet规范(Parquet-format),另一部分是不同语言的实现规范。(MapReduce,pig,Hive,Cascading,Crunch和Spark都支持Parquet格式)
②Parquet的灵活性同样延伸至内存中的表示方法,Java的实现并没有绑定某一种表示方法,因而可以使用Avro,Thrift或者Protocol Buffers等多种内存数据表示法来将数据写入Parquet或者从Parquet文件中读取数据到内存。
2.Parquet数据类型
2-1.基本数据类型
类型 | 描述 |
---|---|
boolean | 二进制值 |
int32 | 32位有符号整数 |
int64 | 64位有符号整数 |
int96 | 96位有符号整数 |
float | 单精度(32位)IEEE 754浮点数 |
double | 双精度(32位)IEEE 754浮点数 |
binary | 8位无符号字节序列 |
fixed_len_byte_array | 固定数量的8位无符号字节 |
同Avro类似,Parquet文件中的数据也通过schema
描述。模式的根为message
,它包含一组字段,每个字段都由以下三部分内容构成:
①一个重复的关键字(required,optional或repeated)
※注:
required:表示有且仅有一个值。
optional:表示可选,0到1个值。
repeated:表示重复,0到N个值。
②一个数据类型
③一个字段名称
message WeatherRecord {
required int32 year;
required int32 temperature;
required binary stationId (UTF8);
}
※注:Parquet并没有String类型,事实上Parquet定义了一些逻辑类型,这些逻辑类型支出如何对基本数据类型就行解读。
例如,可以通过UTF8注解的binary基本数据类型来表示String.
2-2.Parquet的逻辑类型
类型 | 描述 | schema实例 |
---|---|---|
UTF8 | UTF8字节的字符串,可用于注解binary |
message m { required binary fieldName (UTF8); } |
ENUM | 枚举(一组命名值的集合)可用于注解binary |
message m { required binary fieldName (ENUM); } |
DECIMAL (precision,scale) |
任意精度的有符号小数,可以用于注解int32,int64,binary或fixed_len_byte_array | message m { requried int32 fieldName (DECIMAL(5,2)); } |
DATE | 不带时间的日期值,可以注解int32 用Unix元年(1970年1月1日)以来的天数表示 |
message m { required int32 fieldName (DATE); } |
LIST | 一组有序的值,可以用于注解group | message m { required group groupName (LIST) { repeated group listName { required int32 element } } } |
MAP | 一组无序的键值对,可以用于注解group | message m { required group groupName (MAP) { repeated group key_value { required binary key (UTF8); optional int32 value; } } } |
Parquet利用group类型来构造复杂类型,它可以增加一层嵌套。
2-3.嵌套编码
使用面向列式的存储格式时,同一列数据连续存储。
不同于Hive的RPCFile(通过将嵌套结构扁平化来回避对嵌套结构进行编码),Parquet使用的是Dremel编码方式,
即schema中的每个基本数据类型的字段都会被单独存储为一列
,且每个值都需要通过使用两个整数
来对其结构进行编码:
①列定义深度(definition level):
It specifies how many fields in path p that could be undefined(repeated & optional) are actually present.
表示在该值的路径上,有多少个字段
是可以不被定义的(repeated和optional)但是却已经被定义
。
(该值只对空值有意义)
而对于非空值它是没有意义的,因为非空值即意味着该值的每一层节点都已经定义,所以,对于已定义的值而言,definition level为到该节点为止的路径中(包括该节点)所包含的repeated和optional的节点数。
由于网上的说明和例子千篇一律但是却都解释的不够明晰,这里自己稍作改动。
// message定义
message DefinitionLevelExample {
optional group defLevel0 {
optional group defLevel1 {
optional binary defLevel2_col_a (UTF8);
}
}
}
// 四种情况
1.defLevel0:null
2.defLevel0:{defLevel1:null}
3.defLevel0:{defLevel1:{defLevel2_col_a:null}}
4.defLevel0:{defLevel1:{defLevel2_col:"parquet test"}}
// 为了方便观看把上面四种情况转换成下面的形式
DefinitionLevelExample
{
defLevel0:null
defLevel0:
defLevel1:null
defLevel0:
defLevel1:
defLevel2_col:null
defLevel0:
defLevel1:
defLevel2_col:"test"
}
case | value | path | definition level |
---|---|---|---|
① | defLevel0:null | /DefinitionLevelExample/defLevel0 | 0 |
② | defLevel0:{ defLevel1:null } |
/DefinitionLevelExample/defLevel0 /defLevel1 |
1 |
③ | defLevel0:{ defLevel1:{ defLevel2_col:null } } |
/DefinitionLevelExample/defLevel0 /defLevel1 /defLevel2_col |
2 |
④ | defLevel0:{ defLevel1:{ defLevel2_col:“parquet test” } } |
/DefinitionLevelExample/defLevel0 /defLevel1 /defLevel2_col |
3 |
※上面的表格中,以红色标注的即为,到该值的路径中可以不被定义,但是却已经被定义的字段。case④中,所有节点都已经被定义,所以repetition level的值为3。
②列元素重复次数(repetition level):
It tells us at what repeated field in the field’s path the value has repeated
repetition level的主要目的是为了记录repeated标记的节点,它表示当前节点的路径中,在哪个(第几个
)被repeated定义的字段被重复使用(这里的重复使用指的是下面定义的message中那些被定义为repeated的节点不止一次出现)。
(sorry,语文水平比较差,话不多说,还是上demo吧。)
// 定义message
message RepetitionLevelDemo {
optional group year {
repeated group month {
repeated binary day (UTF8);
optional binary week (UTF8);
}
}
repeated group music {
repeated group folkmusic {
required binary guitar (UTF8);
repeated binary bass (UTF8);
}
}
}
// struct结构展示
year
month
day:"10"
day:"20"
week:"1"
music
folkmusic
guitar:"guitar one"
folkmusic
guitar:"guitar two"
bass:"bass one"
bass:"bass two"
①/RepetitionLevelDemo/year/month
/day
=“10”
②/RepetitionLevelDemo/year/month
/day
=“20”
③/RepetitionLevelDemo/year/month
/week=“1”
④/RepetitionLevelDemo/music
/folkmusic
/guitar=“guitar one”
⑤/RepetitionLevelDemo/music
/folkmusic
/guitar=“guitar two”
⑥/RepetitionLevelDemo/music
/folkmusic
/bass
=“bass one”
⑦/RepetitionLevelDemo/music
/folkmusic
/bass
=“bass two”
现在来对上面的集中情况进行说明:上面数据被分为三组,并且所有被定义为repeated的字段已经被标记为红色。
①r为0,因为,/day="10"第一次出现,所以该路径中还没有被重复使用的字段。
(※注:这里所说的被重复使用是指,起始节点和终止节点完全一样的路径。
)
②r为2,因为,/day="20"在day
节点开始相对于①来讲相当于一个repeated的节点。所以是在第二个repeated节点出现重复,故r为2。(这里果然还是画图简单点
)
③r为0,因为,/week="1"第一次出现,所以该路径中还没有被重复使用的字段。
④r为0,因为,/guitar="guitar one"第一次出现,所以该路径中还没有被重复使用的字段。
⑤与④在music开始发生repeated,所以r为1。
⑥r为0
⑦r为3,因为,/bass="bass two"和⑥在/bass节点开始发生repeated。(和②的情况类似。)
下图为Dremel论文中给出的Document示例,文中给出2组数据,r1和r2,并演示了关于repetition level和definition level的计算,其中r代表repetition level,d代表definition level。
DcoId:{
在r1中→
值为10,为DocId节点的起始值,所以r为0,并且为required类型,忽略definition level所以d为0。
在r2中→
同理。
}
Name.Url{
在r1中→
case 1 Url:'http://A'首次出现,r为0,依照上面提到过的definition level的计算方法,Name(repeated)和Url(optional)这两个节点都是可以为null的但是已经有值了,所以definition level为2。
case 2 Url:'http://B'第二次出现,与case 1 共享根节点,但在Name节点出现重复(repeated),r为1,d值同上为2。
case 3 Url:null 由于url为Name下的节点,所以每次有Name节点出现,都需要标记Url的r和d的值,r同上为1。
但由于Url在这里并未出现(未定义,null),所以d为1。
在r2中→
case 4 Url:'http://C':同case 1 一样,在r2中首次出现,所以r为0,d的值也是2。
}
Links.Forward{
在r1中→
case 5 Forward:20,首次出现,r为0,repeated类型,同optional的Url,所以d为2。
case 6 Forward:40,case 5共享同一个Links节点,在Forward出现重复(repeated),`※注意,这里有点儿坑,Links是optional的并未repeated,所以r应该为1而不是2`,d为2不在赘述。
case 7 Forward:60,同上。r为1,d为2。
case 8 Forward:80,同case 6,r为1,d为2。
}
Links.Backward{
case 9 Backward:null,首先r为0不赘述,d为1,同3。
case 10 Backward:10,r为0,d同case 1 所以d值为2。
case 11 Backward:30,同case 6r为1,d为2。
}
Name.Language.Code{
case 12 Code:'en-us',首次出现r为0,Code为required类型,所以d不计算Code节点,所以d为2。
case 13 Code:'en',在Language节点出现重复,所以r为2,d同上值也为2。
case 14 Code:null,同case 3,r为1,d为1。
case 15 Code:'en-gb',r为1,d为2。
case 16 Code:null,首次出现r为0,d为1。
}
Name.Language.Country{
case 17 Country:'us',r为0,d为3,(因为Name,Language,Country都是可为空的节点,三层节点都不为null所以d为3)。
case 18 Country:null,r为2,d为2.(Language节点开始出现重复)
case 19 Country:null,r为1,d为1.(Name节点出现重复)
case 20 Country:'gb',r为1,d为3.(Name节点出现重复)
case 21 Country:null,r为0,d为1.
}
3.Parquet文件格式
Parquet由一个文件头(Header),一个或多个文件块(block),以及一个用于结尾的文件尾(footer)组成。
文件头中包含一个称为PAR1的4字节数字(Magic Number),它用来识别整个Parquet文件格式。文件中所有元数据(metadata)保存在尾部。
尾部中的元数据包括:
①文件格式的版本信息。
②schema信息。
③额外的键值对以及所有块的元数据信息。
文件尾部最后两个字段分别是:
①一个4字节的字段,其中包含了文件尾中元数据长度的编码
②一个PAR1文件(与头部中的相同)
Parquet文件中的每个块文件负责存储一个行组(row group),行组有列块(column chunk)组成,且每个列块负责存储一列数据。每个列块中的数据以页(page)为单位存储,如下图:
4.Parquet的配置
Parquet文件块大小不能超过HDFS块大小,只有这样,每个Parquet文件块仅需读取一个HDFS块,(因而只需从一个数据节点上读)。
比较常见的做法是为这两个属性设置相同的值,事实上,他们的默认值都是128M。
5.Parquet文件的读写
5-1.Parquet文件的基本操作
一般情况下我们使用高级工具来处理Parquet文件的读/写操作,比如Pig,Hive,Impala。不过像下面这样的低级的顺序访问有是也是需要的。
首先我们需要导入需要的jar包,下面以maven的导入为例。这里我们需要主角parquet和hadoop
(Parquet的maven导入)
<dependencies>
...
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-avro</artifactId>
<version>1.9.0</version> <!-- or latest version -->
</dependency>
...
(Hadoop的maven导入)
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.8.5</version>
</dependency>
</dependencies>
接下来我们来写一个简单的Demo,(由于自己是看<<Hadoop- The Definitive Guide, 4th Edition>>来学习的,所以在这里吐槽一下)
①对于第一次接触Hadoop生态圈内容的新人来说,第四版的部分内容也已经被标记为过时
,所以写Demo的时候有的问题无法得到解决。
②而且无论是书中的式样,还是网上其他学习者的blog都是直接上代码,but!!!没有包的信息,导致出现问题时无从下手。所以这里建议大家一下,以后往blog上贴代码时,尽量把包内容一起贴出来,对新人会有很大帮助。
package parquet;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.parquet.example.data.Group;
import org.apache.parquet.example.data.GroupFactory;
import org.apache.parquet.example.data.simple.SimpleGroupFactory;
import org.apache.parquet.hadoop.ParquetReader;
import org.apache.parquet.hadoop.ParquetReader.Builder;
import org.apache.parquet.hadoop.ParquetWriter;
import org.apache.parquet.hadoop.example.ExampleParquetWriter;
import org.apache.parquet.hadoop.example.GroupReadSupport;
import org.apache.parquet.schema.MessageType;
import org.apache.parquet.schema.MessageTypeParser;
public class ParquetDemo {
public static void main(String[] args) throws IOException {
ParquetDemo demo = new ParquetDemo();
demo.parquetTest();
}
public void parquetTest() throws IOException {
// parquer用message的准备
MessageType schema = MessageTypeParser.parseMessageType("message Pair {\n"
+ "required binary left (UTF8);\n"
+ "required binary right (UTF8);\n"
+ "}");
Configuration conf = new Configuration();
GroupFactory groupFactory = new SimpleGroupFactory(schema);
Group group = groupFactory.newGroup().append("left", "L").append("right", "R");
Path path = new Path("data.parquet");
FileSystem fs = FileSystem.get(conf);
if(fs.exists(path)) {
fs.delete(path, true);
}
ExampleParquetWriter.Builder builder = ExampleParquetWriter.builder(path);
ParquetWriter<Group> writer = builder.withConf(conf).withType(schema).build();
writer.write(group);
writer.close();
GroupReadSupport readSupport = new GroupReadSupport();
Builder<Group> readBuilder = ParquetReader.builder(readSupport, path);
ParquetReader<Group> reader = readBuilder.build();
Group line = null;
while((line = reader.read()) != null) {
System.out.println(line.getString("left", 0));
System.out.println(line.getString("right", 0));
}
}
}
5-2.Parquet文件基于Avro的操作。
同Parquet不同,这次我们需要的一个Avro的schema,例子中我们先创建一个StringPair.avsc
package parquet;
import java.io.IOException;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Parser;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.parquet.avro.AvroParquetReader;
import org.apache.parquet.avro.AvroParquetWriter;
import org.apache.parquet.hadoop.ParquetReader;
import org.apache.parquet.hadoop.ParquetWriter;
public class ParquetDemoWithAvro {
public static void main(String[] args) throws IOException {
ParquetDemoWithAvro demo = new ParquetDemoWithAvro();
demo.parquetTest();
}
public void parquetTest() throws IOException {
Parser parser = new Schema.Parser();
// 读入Avro的schema文件
Schema schema = parser.parse(getClass().getResourceAsStream("/StringPair.avsc"));
GenericRecord datum = new GenericData.Record(schema);
datum.put("left", "This is avro-parquet left");
datum.put("right", "This is avro-parquet right");
Configuration conf = new Configuration();
Path path = new Path("avro-parquet-data.parquet");
FileSystem fs = FileSystem.get(conf);
if(fs.exists(path)) {
fs.delete(path, true);
}
AvroParquetWriter.Builder<GenericRecord> builder = AvroParquetWriter.builder(path);
ParquetWriter<GenericRecord> writer = builder.withConf(conf).withSchema(schema).build();
writer.write(datum);
writer.close();
AvroParquetReader.Builder<GenericRecord> readBuilder = AvroParquetReader.builder(path);
ParquetReader<GenericRecord> reader = readBuilder.build();
GenericRecord record = null;
while((record = reader.read()) != null) {
System.out.println(record.get("left"));
System.out.println(record.get("right"));
}
}
}
5-3.投影schema
有些时候,我们只是希望读取文件中的少数几列,这正是Parquet这样的列存储个格式存在的原因:节省时间并提升I/O操作的效率。(下面的例子中,我们只读取上一个Demo中的right字段)
创建一个新的,用来读取的schema
接下来是示例代码
※注:由于书中代码部分已过时,且没查到相关资料,所以下面代码虽然能正常执行,但仅代表个人的理解,如代码有误还请指出。
package parquet;
import java.io.IOException;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.parquet.avro.AvroParquetReader;
import org.apache.parquet.avro.AvroReadSupport;
import org.apache.parquet.hadoop.ParquetReader;
public class ParquetProjectionAndReadSchema {
public static void main(String[] args) throws IOException {
ParquetProjectionAndReadSchema demo = new ParquetProjectionAndReadSchema();
demo.parquetTest();
}
public void parquetTest() throws IOException {
Schema schema = new Schema.Parser().parse(getClass().getResourceAsStream("/ProjectedStringPair.avsc"));
Configuration conf = new Configuration();
AvroReadSupport.setRequestedProjection(conf, schema);
AvroReadSupport.setAvroReadSchema(conf, schema);
Path path = new Path("avro-parquet-data.parquet");
// deprecation
//AvroParquetReader<GenericRecord> reader = new AvroParquetReader<GenericRecord>(conf, path);
AvroParquetReader.Builder<GenericRecord> builder = AvroParquetReader.builder(path);
ParquetReader<GenericRecord> reader = builder.withConf(conf).build();
GenericRecord result = null;
while ((result = reader.read()) != null) {
System.out.println(result.get("right"));
}
}
}