Hadoop学习之-Parquet

关于Parquet

传统的,关系型数据的列式存储比较容易处理,但是嵌套类型的列式存储处理起来相当之困难,因为,嵌套类型的数据不仅仅只又基本数据类型,它还可以是List,Map,Set类型,虽然,依然可以快速定位到某一列的值,但如何把他们合并一行数据呢?2010年Google公司发表了一篇名为Dremel: Interactive Analysis of Web-Scale Datasets的论文,解决了这个问题。
于是Parquet的缔造者们(Twitter和Cloudera的工程师),基于Dremel的基础之上,开发出了Parquet。

1.Parquet的特点

  1. Apache Parquet是一种能够有效存储嵌套数据的列式存储格式
  2. 由于是列式存储格式,在文件大小和查询性能上表现优秀。
    列式存储的优势:
    ①由于每一列的数据类型相同,所以,可以针对不同数据类型的列进行不同的编码和压缩,大大降低数据存储所占空间。
    下图展示了,使用不同存储格式来存储TPC-DS数据集中的两个不同的表数据的文件大小。可以看出,Parquet较其他二进制文件存储格式能够更有效地节省存储空间,尤其是新版的Parquet 2,它使用了更高效的也存储方式。
    在这里插入图片描述
    ②读取数据的时候可以把映射下推,只需要读取需要的列,这样可以大大减少每次查询的I/O数据量。
    ③由于每一列的数据类型相同,可以使用更加适合CPU pipeline的编码方式,减小CPU的缓存失效。
  3. 灵活性:
    ①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为2case 2 Url:'http://B'第二次出现,与case 1 共享根节点,但在Name节点出现重复(repeated),r为1,d值同上为2case 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为2case 6 Forward:40case 5共享同一个Links节点,在Forward出现重复(repeated)`※注意,这里有点儿坑,Links是optional的并未repeated,所以r应该为1而不是2`,d为2不在赘述。
case 7 Forward:60,同上。r为1,d为2case 8 Forward:80,同case 6,r为1,d为2}
Links.Backward{
case 9 Backward:null,首先r为0不赘述,d为1,同3case 10 Backward:10,r为0,d同case 1 所以d值为2case 11 Backward:30,同case 6r为1,d为2}
Name.Language.Code{
case 12 Code:'en-us',首次出现r为0,Code为required类型,所以d不计算Code节点,所以d为2case 13 Code:'en',在Language节点出现重复,所以r为2,d同上值也为2case 14 Code:null,同case 3,r为1,d为1case 15 Code:'en-gb',r为1,d为2case 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"));
		}
	}
}
发布了62 篇原创文章 · 获赞 3 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Leonardy/article/details/91435315